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.tsts
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.tsts
export function getSupportedLocale(userLocale: string | undefined): LocaleCode {const locale = Object.keys(Locales).find((supportedLocale) => {return userLocale?.includes(supportedLocale)}) as LocaleCode | undefinedreturn 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.jsonjson
{"hello": {"world": "Hello world!"}}
Then add another language:
lib/poly-i18n/locales/es.jsonjson
{"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.jsjs
/** @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.tsts
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].