Set up Locales

Start with what you do with any i18n library: set up locales and translation files.

Define supported Locales

Create an enum of the locales you support and create a type for them.

lib/poly-i18n/locales.ts
ts
export enum Locales {
en = 'English',
es = 'Español',
}
export type LocaleCode = keyof typeof Locales

Then add a function that receives the user's desired locale and returns the locale you support that is closest to what they want. This will also take dialect specific locales like en-US or en-GB and return en as the closest match.

lib/poly-i18n/locales.ts
ts
export function getSupportedLocale(userLocale: string | undefined): LocaleCode {
const locale = Object.keys(Locales).find((supportedLocale) => {
return userLocale?.includes(supportedLocale)
}) as LocaleCode | undefined
return locale || 'en' // 👈 default to English if no match
}

Add translation strings

Then add translation strings by creating a file for each locale you support. In this example we will support en and es.

lib/poly-i18n/locales/en.json
json
{
"hello": {
"world": "Hello world!"
}
}

Then add another language:

lib/poly-i18n/locales/es.json
json
{
"hello": {
"world": "¡Hola mundo!"
}
}

I initially used .js files for type-safety and then realized that I can get type-safety with .json files too, and .json works with i18n-ally. So I switched, but if you don't care about the extension you might consider using .js files because when we create another language we can add type safety with one line:

lib/poly-i18n/locales/es.js
js
/** @type {typeof import('./en.js').default} */
export default {
hello: {
world: '¡Hola mundo!',
},
}

But that's a moot point if you use any sort of automated system to generate your translations as they will use identical keys for each language.

Translation key type-safety

We can also use our en.json file to give us some nice intellisense and type-checking as we use our translation keys in our app. Let's create a TranslationKeys type:

lib/poly-i18n/types.ts
ts
import type en from './locales/en.json'
export type TranslationKeys = Flatten<TranslationKeysNested>
type TranslationKeysNested = {
[K in keyof typeof en]: {
[L in StringKeyof<typeof en[K]>]: `${K}.${L}`
}
}[keyof typeof en]
type StringKeyof<T> = Extract<keyof T, string>
type Flatten<T> = T extends infer U ? { [K in keyof U]: U[K] } extends Record<keyof U, infer V> ? V : never : never

I use a nested structure with two levels (section and item), as in header.title. If you do something different, you'll need to adjust your type-checking accordingly.

Now let's set up any needed [Formatting].

Edit page in GitHub