Skip to content

Instantly share code, notes, and snippets.

@davemackintosh
Last active September 26, 2023 08:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davemackintosh/dc3f6be8838fcdb3ee44f19e671fb1b0 to your computer and use it in GitHub Desktop.
Save davemackintosh/dc3f6be8838fcdb3ee44f19e671fb1b0 to your computer and use it in GitHub Desktop.
I got fed up with the Svelte i18n library being huge and the latest being broken so wrote my own for https://sale-nook.com

Svelte i18n

I was a bit tired of the issues I was facing with the existing svelte i18n library, version problems, no intellisense for existing translations and the enormous bundled size so I created a very basic "get the job done" i18n library for Svelte.

Usage

Pop this in your +layout.svelte

<script lang="ts">
  import { setupi18n } from "$src/i18n"
  
  // Add to this enum or change this value depending on your needs.
  setupi18n(AvailableTranslationLocales.EN)
</script>

Use the $T store to do your translations in your code, you get intellisense for the available keys.

<script lang="ts">
  import { T } from "$src/i18n"
</script>

<h1>{$T("hello")}</h1>

I'm aware of the lack of support for arrays (although this is superficial, it will be a type error not a runtime error) and I will fix that at some point.

Adding more translations

Create a JSON object (I use ChatGPT to transform my EN file into other languages) in the translations folder with the same name as the value in your enum and then use the addTranslation function to fetch and register the new translation dictionary.

/
  translations
    fr.json
    ch.json
<script lang="ts">
	import { addTranslation, setupI18n, AvailableTranslationLocales } from "$src/i18n"
	
	let selectedLang: AvailableTranslationLocales = AvailableTranslationLocales.EN
	
	async function onLangChange() {
		await addTranslation(selectedLang)
		setupI18n(selectedLang)
	}
</script>

<select bind:value={selectedLang} on:change={onLangChange}>
	{#each Object.keys(AvailableTranslationLocales) as translation}
		<option value={AvailableTranslationLocales[translation]}>{translation}</option>
	{/each}
</select>
import { writable, derived, get } from "svelte/store"
import type { Translation } from "./types/translation"
type PathsToProps<T, V> = T extends V
? ""
: {
[K in Extract<keyof T, string>]: Dot<K, PathsToProps<T[K], V>>
}[Extract<keyof T, string>]
type Dot<T extends string, U extends string> = "" extends U ? T : `${T}.${U}`
export enum AvailableTranslationLocales {
EN = "en",
FR = "fr",
}
export type AvailableTranslations = Record<AvailableTranslationLocales, Translation>
const i18nDictionary: Partial<AvailableTranslations> = {
// en gets bundled so we don't have to download it at runtime and it's the default.
[AvailableTranslationLocales.EN]: {
hello: "world",
},
}
const dictionary = writable<Partial<AvailableTranslations>>(i18nDictionary)
const locale = writable<AvailableTranslationLocales>(AvailableTranslationLocales.EN)
export function setupI18n(withLocale: AvailableTranslationLocales) {
locale.set(withLocale)
}
export async function addTranslation(
withLocale: AvailableTranslationLocales,
options?: { ignoreCache?: boolean },
) {
if (!Object.values(AvailableTranslationLocales).find((t) => t === withLocale)) {
throw new TypeError(`'${withLocale} is not a valid locale.`)
}
const current = get(dictionary)
if (!options?.ignoreCache && current[withLocale]) {
return Promise.resolve(current[withLocale])
}
return fetch(`/translations/${withLocale}.json`)
.then((res) => res.json())
.then((translation) => {
dictionary.set({
...current,
[withLocale]: translation,
})
return translation
})
}
export const T = derived(
[dictionary, locale],
([$dictionary, $locale]) =>
(
path: PathsToProps<(typeof i18nDictionary)[AvailableTranslationLocales.EN], string>,
): string => {
if (!$dictionary) throw new ReferenceError("You must set a dictionary.")
if (!$locale)
throw new TypeError("You must set the locale before trying to translate anything.")
if (!$dictionary[$locale])
throw new ReferenceError("Failed to find dictionary entry for locale")
const pathParts = path.split(".")
let accumulator: Partial<(typeof $dictionary)[typeof $locale]> = $dictionary[$locale]
for (const part of pathParts) {
if (!accumulator) {
throw new ReferenceError("Could not find part in dictionary")
}
const possibleValue = accumulator[part as keyof typeof accumulator]
if (!possibleValue) {
throw new ReferenceError("Could not find part in dictionary")
}
// @ts-ignore I'll fix this type inference another day.
accumulator = possibleValue
}
return accumulator as string
},
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment