compiled-i18n
Introduction
This module statically generates translated copies of code bundles, so that you can serve them to clients as-is, without any runtime translation code. This concept is based on $localize
from Angular.
Anywhere in your code, you have simple template string interpolation:
import {_} from 'compiled-i18n'
console.log(_`Logenv ${process.env.NODE_ENV}`)
export const Count = ({count}) => (
<div title={_`countTitle`}>{_`${count} items`}</div>
)
This combines with translations in JSON files. French example:
/i18n/fr.json
{
"locale": "fr",
"fallback": "en",
"name": "Français",
"translations": {
"Logenv $1": "Mode de Node.JS: $1",
"countTitle": "Nombre d'articles",
"$1 items": {
"0": "aucun article",
"1": "un article",
"*": "$1 articles"
}
}
}
On the server, these translations are loaded into memory and translated dependening on the current locale (you can define a callback with setLocaleGetter
to choose the locale per translation call).
On the client, the translations are embedded directly into the code. For example, the client code for the fr
locale becomes:
import {interpolate} from 'compiled-i18n'
console.log(`Mode de Node.JS: ${process.env.NODE_ENV}`)
export const Count = ({count}) => (
<div title="Nombre d'articles">
{interpolate(
{
0: 'aucun article',
1: 'un article',
'*': '$1 articles',
},
count
)}
</div>
)
Note that the interpolate
function is only added when a translation uses plurals.
You can also use the API functions to implement dynamic translations that you load at runtime.
Pros:
- 0-runtime in client
- all keys are known
- easy setup
Cons:
- changing language means reload
- must construct loading HTML to load the js from the per-locale dir
Installation
Add the plugin as a dev dependency:
npm install --save-dev compiled-i18n
or
pnpm i -D compiled-i18n
or
yarn add -D compiled-i18n
There are several parts to localization:
- the locale data
- the locale selection (cookie, url, ...)
- the translation function
- getting the data to the client
compiled-i18n
helps with most of these. You have to hook up the helpers to your project. To do this, you add the vite plugin and connect the locale selection.
The vite plugin will automatically create the JSON data files under the i18n/
folder, add new keys to existing files, and embed the data in the build.
Add the plugin to your vite config:
import {defineConfig} from 'vite'
import {i18nPlugin} from 'compiled-i18n/vite'
export default defineConfig({
plugins: [
i18nPlugin({
locales: ['en_us', 'en_uk', 'en', 'nl'],
}),
],
})
You have to set up your project so the plugin knows the current locale, both on the server during SSR, and on the client.
On the server, you can use the setLocaleGetter
function to set a callback that returns the current locale, or you can call the setDefaultLocale
function to set the locale directly if you only process one locale at a time. See qwik.md for an example setup for Qwik.
In the browser code during development, you need to either set the lang
attribute on the <html>
tag, or call setDefaultLocale
to set the locale.
In production, the locale is fixed, so no need to set it.
However, you need to make sure that your HTML file loads the correct bundle for the locale.
For example, if your entry point is /main.js
and your locales are en
and fr
, you need to load /en/main.js
or /fr/main.js
depending on the locale.
Qwik
See detailed instructions in qwik.md.
Usage
In your code, use the _
or localize
function to translate strings (you must use template string notation). For example:
import {_} from 'compiled-i18n'
const name = 'John'
const emoji = '👋'
const greeting = _`Hello ${name} ${emoji}!`
You will need to specify the translations for the key "Hello $1 $2!"
in the JSON files for the locales.
It is recommended to keep your keys short and descriptive, with capitalization when appropriate. If you would like to change what's shown in your base language, it's easiest if you change the translation and keep the key the same.
In your server code, you need to set the locale getter, which returns the locale that is needed for each translation. This differs per framework. For example, for Qwik:
import {defaultLocale, setLocaleGetter} from 'compiled-i18n'
import {getLocale} from '@builder.io/qwik'
setLocaleGetter(() => getLocale(defaultLocale))
How it works
In the server and in dev mode, all translations are loaded into memory eagerly, but for a production client build, all the localize`x`
calls are replaced with their translation.
Translations are stored in json files, by default under /i18n
in the project root. The plugin will create missing files and add new keys to existing files.
Types
See index.ts for the full types.
JSON translations format
The JSON files are stored in the project root under /i18n/$locale.json
, in the format I18n.Data
. A translation is either a string or a Plural object.
export type Data = {
locale: Locale
fallback?: Locale
name?: string
translations: {
[key: Key]: Translation | Plural
}
}
A Translation is a string can that contain $#
for interpolation, and $$
for a literal $
. For example, _`Hello ${name} ${emoji}`
looks up the key "Hello $1 $2"
and interpolates the values of name
and emoji
. Note that this definition means that the Key is also a Translation, so for missing translations, the Key is used as a fallback.
A Plural object contains keys to select a translation with the first interpolation value. The key "*"
is used as a fallback. String values are treated as a translation string, and numbers are used to point to other keys. For example, the plural object
{
"$1 items": {
"0": "no items",
"1": "some items",
"2": 1,
"3": "three items",
"three": 3,
"*": "many items ($1)"
}
}
will translate _`${count} items`
to "no items"
for count = 0
, "some items"
for count = 1
or count = 2
, "three items"
for count = 3
or count = "three"
, and `many items (${count})`
for any other number.
You can use any string or number as a Plural key, so you could also use it for enums, but perhaps it would be better to use runtime translation for that.
Client-side API
localize`str`
or _`str`
translate template string using in-memory maps
_`Hi ${name}!`
converts into a lookup of the I18nKey "Hi $1"
. A literal $
will be converted to $$
. Missing translations fall back to the key.
Nesting is achieved by passing result strings into translations again.
_`There are ${_`${boys} boys`} and ${_`${girls} girls`}.`
localize(key: I18nKey, ...params: any[])
or _(key: I18nKey, ...params: any[])
Translates the key, but this form does not get statically replaced with the translation.
This means that when you interpolate, you are generating new keys instead of a single key with a $1
placeholder. For example:
const name = 'Wout'
_`Hi ${name}!`
_(`Hi ${name}`)
It is also your duty to call loadTranslations
with data you provide, so the requested translations are present. The built client code will not include any translations.
Missing translations use the key.
currentLocale: readonly string
Current locale. Is automatically set by the locate getter that runs on every translation.
On the client side in production it is hardcoded.
During dev mode on the client it is automatically set to the lang
attribute of the HTML tag if that's valid. You can also set it with setDefaultLocale
.
locales: readonly string[]
e.g. ['en_US', 'fr']
.
localeNames: readonly const {[key: string]: string}
e.g. {en_US: "English (US)", fr: "Français"}
loadTranslations(translations: I18n.Data['translations'], locale?: string)
Add translations for a locale to the in-memory map. If you don't specify the locale, it uses currentLocale
.
This is only needed for dynamic translations.
You need to run this both on the client and the server (when using SSR), so they have the same translations available.
Server-side API
setLocaleGetter(getLocale: () => Locale)
getLocale
will be used to retrieve the locale on every translation. It defaults to defaultLocale
.
For example, use this to grab the locale from context during SSR.
This should not be called on the client, as the locale is fixed in production. Instead, the client should set the locale via the HTML lang
attribute or with setDefaultLocale
.
setDefaultLocale(locale: string)
Sets the default locale at runtime, which is used when no locale can be determined. This is useful during dev mode on the client side if you can't change the HTML's lang
attribute. In production on the client, the locale is fixed, and this function has no effect.
Utility API
defaultLocale: readonly string
Default locale, defaults to the first specified locale. Can be set with setDefaultLocale
, useful during dev mode on the client side if you can't change the HTML's lang
attribute.
guessLocale(acceptsLanguage: string)
Given an accepts-language
header value, return the first matching locale.
If the given string is invalid, returns undefined
.
Falls back to defaultLocale
interpolate(translation: I18nTranslation | I18nPlural, ...params: unknown[])
Perform parameter interpolation given a translation string or plural object. Normally you won't use this.
makeKey(...tpl: string[]): string
Returns the calculated key for a given template string array. For example, it returns "Hi $1"
for ["Hi ", ""]
Qwik API (from 'compiled-i18n/qwik')
This sets the base path for assets for a Qwik application. Pass it to the
base
property of the render options.
If running in development mode, the base path is simply /build
. Otherwise,
it's /build/${locale}
. It also includes the base path given to vite.
setSsrLocaleGetter(): void
Configure compiled-i18n to use the locale from Qwik during SSR.
Call this in your entry.ssr file.
Vite API (from 'compiled-i18n/vite')
The vite plugin accepts these options:
type Options = {
locales?: string[]
localesDir?: string
defaultLocale?: string
babelPlugins?: any[]
assetsDir?: string
addMissing?: boolean
tabs?: boolean
}
Choosing a key name
It is recommended to keep your keys short and descriptive, with capitalization when appropriate. If you would like to change what's shown in your base language, it's easiest if you change the translation and keep the key the same.
If you need to provide some context for the translator, put it inside the key. For example, when translating the word "right" as a capitalized button label, you might want to specify if it's the direction or the opposite of wrong. In that case, you could use the keys "Right-direction"
and "Right-correct"
.
If it's unclear what the parameter is, you can add a comment to the key. For example:
_`Greeting ${name}:name`
Newlines are not allowed in keys.
Automatic translation
The JSON format includes the keys and tools like Github Copilot actually have enough context to translate the keys for you. This is workable for small amounts of translations.
A more robust way is to a tool like deepl-localize. It includes support for compiled-i18n
's JSON format.
Roadmap
- allow specifying helper libs that re-export localize and interpolate, so those re-exports are also processed
- build client locales in dev mode as well, being smart about missing keys and hot reloading. In Qwik this might be hard because dev and prod are quite different.
- move unused keys to
unused
in the JSON files?