Internationalization
Internationalization (i18n) refers to the design and development of products and applications to enable localization, making them suitable for users from different cultures, regions, or languages. You can use i18n libraries like i18next to achieve internationalization and provide an accessible experience for users.
Intl API
The Intl object is a namespace for the ECMAScript Internationalization API, providing a set of methods for handling internationalization and localization. With the Intl API, you can handle issues related to numbers, dates, and times, such as number formatting and date and time formatting.
Currently, the Intl API is not implemented in Lynx but will be supported in future versions. If you need to use the Intl API in Lynx, you can install the corresponding polyfills, such as @formatjs/intl-numberformat, @formatjs/intl-datetimeformat, and intl-pluralrules.
Using i18next
i18next is an internationalization-framework written in and for JavaScript. Using it in ReactLynx gives you:
- Simplicity:
i18next provides an easy-to-use API, making it simple to implement internationalization in ReactLynx applications.
- Dynamic Loading: Supports on-demand loading of language resources, reducing initial load time.
- Wide Support: Compatible with various formats and backends, allowing easy integration with different translation storage solutions such as JSON files, remote APIs, etc.
- Caching: Built-in caching mechanism speeds up the loading of language resources, enhancing user experience.
- Rich Community Support: A vast community and a wealth of plugins available to meet diverse internationalization needs.
- Reliability: Proven in numerous projects, offering stability and reliability.
- Hot Reloading: Changes to language resources can take effect immediately without needing to republish the application.
Installation
You need to install the i18next package:
npm install i18next@^23.16.8
yarn add i18next@^23.16.8
pnpm add i18next@^23.16.8
deno add npm:i18next@^23.16.8
Tip
Since the version 24.0.0+ of i18next, the running environment is required to have the Intl.pluralRules API. However, this implementation is currently not available on Lynx.
This means that you need to:
- Use v23 and must enable
compatibilityJSON: 'v3'.
- Use v24 and need to polyfill the
Intl.PluralRules API.
Create the first translation
Imagine we have a locale file src/locales/en.json like this:
Creating the translation function is as simple as these 3 steps:
- Import the locale JSON file
./locales/en.json.
- Create an i18next instance with the
createInstance() function.
- Initialize the i18n with the locale resource.
src/i18n.ts
import i18next from 'i18next';
import type { i18n } from 'i18next';
import enTranslation from './locales/en.json';
const localI18nInstance: i18n = i18next.createInstance();
localI18nInstance.init({
lng: 'en',
// The default JSON format needs `Intl.PluralRules` API, which is currently unavailable in Lynx.
compatibilityJSON: 'v3',
resources: {
en: {
translation: enTranslation, // `translation` is the default namespace
},
},
});
export { localI18nInstance as i18n };
Tip
If you import *.json in TypeScript file, you may need to set compilerOptions.resolveJsonModule to true in your tsconfig.json file.
tsconfig.json
{
"compilerOptions": {
"resolveJsonModule": true
}
}
Then, the i18n.t function can be used for translations:
src/App.tsx
import { useEffect } from '@lynx-js/react';
import { i18n } from './i18n.js';
export function App() {
useEffect(() => {
console.log(`Hello, ReactLynx x i18next!`);
}, []);
return (
<view>
<text>Hello, {i18n.t('world')}</text>
</view>
);
}
Load resources synchronously
In a real world project, there are usually multiple resource files for different languages.
Instead of static import them one-by-one,
you may use the import.meta.webpackContext API of Rspack to statically import all the JSON files.
import one-by-one
// Static-imported locales that can be shown at first screen
import enTranslation from './locales/en.json';
import zhTranslation from './locales/zh.json';
import itTranslation from './locales/it.json';
import jpTranslation from './locales/jp.json';
import deTranslation from './locales/de.json';
import esTranslation from './locales/es.json';
import frTranslation from './locales/fr.json';
import idTranslation from './locales/id.json';
import ptTranslation from './locales/pt.json';
import.meta.webpackContext
const localesContext = import.meta.webpackContext('./locales', {
recursive: false,
regExp: /\.json$/,
});
const enTranslation = localesContext('en.json');
These resources can be added to i18next.init() to make translation work at the first screen.
src/i18n.ts
import i18next from 'i18next';
import type { i18n } from 'i18next';
// Localizations imported statically, available at the initial screen
const localesContext = import.meta.webpackContext('./locales', {
recursive: false,
regExp: /\.json$/,
});
const localI18nInstance: i18n = i18next.createInstance();
localI18nInstance.init({
lng: 'en',
// The default JSON format needs Intl.PluralRules API, which is currently unavailable in Lynx.
compatibilityJSON: 'v3',
// Add all statically imported localizations to i18next resources.
resources: Object.fromEntries(
localesContext.keys().map((key) => [
key.match(/\/([^/]+)\.json$/)?.[1] || key,
{
translation: localesContext(key) as Record<string, string>,
},
]),
),
});
export { localI18nInstance as i18n };
Load resources asynchronously and lazily
Instead of bundling all the locales, we can use dynamic imports (import()) to load the locales lazily and asynchronously.
You need to install the i18next-resources-to-backend package:
npm install i18next-resources-to-backend
yarn add i18next-resources-to-backend
pnpm add i18next-resources-to-backend
bun add i18next-resources-to-backend
deno add npm:i18next-resources-to-backend
Then add the following code to src/i18n.ts:
src/i18n.ts
import i18next from 'i18next';
import type { i18n } from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
// Localizations imported statically, available at the initial screen
const localesContext = import.meta.webpackContext('./locales', {
recursive: false,
regExp: /(en|zh)\.json$/,
});
const localI18nInstance: i18n = i18next.createInstance();
// We can only loading resources on a background thread
if (__JS__) {
localI18nInstance.use(
// See: https://www.i18next.com/how-to/add-or-load-translations#lazy-load-in-memory-translations
resourcesToBackend(
(language: string) =>
// Dynamic-imported locales can be used with `i18n.loadLanguages`
import(`./locales/${language}.json`),
),
);
}
localI18nInstance.init({
lng: 'en',
// The default JSON format needs Intl.PluralRules API, which is currently unavailable in Lynx.
compatibilityJSON: 'v3',
// Add all statically imported localizations to i18next resources.
resources: Object.fromEntries(
localesContext.keys().map((key) => [
key.match(/\/([^/]+)\.json$/)?.[1] || key,
{
translation: localesContext(key) as Record<string, string>,
},
]),
),
partialBundledLanguages: true,
});
export { localI18nInstance as i18n };
-
An i18next backend i18next-resources-to-backend has been added to the background thread with localI18nInstance.use.
-
The languages can be loaded asynchronously (with some of them being loaded synchronously).
You will see two async JS chunks are created in the output:
src_locales_it-IT_json.js
'use strict';
exports.ids = ['src_locales_it-IT_json'];
exports.modules = {
'./src/locales/it-IT.json': function (module) {
module.exports = JSON.parse('{"world": "Mondo"}');
},
};
src_locales_ja-JP_json.js
'use strict';
exports.ids = ['src_locales_ja-JP_json'];
exports.modules = {
'./src/locales/ja-JP.json': function (module) {
module.exports = JSON.parse('{"world": "世界"}');
},
};
💡 Why is there no async chunk generated by src/locales/en.json
This is because this module is already included in the main chunk. Webpack/Rspack will remove it automatically.
See: optimization.removeAvailableModules and optimization.removeEmptyChunks for details.
You may also see that these two chunks are not loaded. This is why it is called lazily. The request to the resources is only sent when needed.
You may also see that these two chunks are not loaded. This is why it is called lazily. The request to the resources is only sent when needed.
Change between languages
The i18next.changeLanguage API can be used for changing between languages.
src/App.tsx
import { useEffect, useState } from '@lynx-js/react';
import { i18n } from './i18n.js';
export function App() {
const [locale, setLocale] = useState('en');
useEffect(() => {
console.log('Hello, ReactLynx3 x i18next!');
}, []);
const getNextLocale = (locale: string) => {
// mock locales
const locales = ["en", "zh-CN"];
const index = locales.indexOf(locale);
return locales[(index + 1) % locales.length];
};
return (
<view>
<text style={{ color: 'red' }}>Current locale: {locale}</text>
<text
bindtap={async () => {
const nextLocale = getNextLocale(locale);
await i18n.changeLanguage(nextLocale);
setLocale(nextLocale);
}}
>
Tap to change locale
</text>
<text>Hello, {i18n.t('world')}</text>
</view>
);
}
If you downloaded many translations but your project only uses some of them, you may need to extract the translations to reduce the bundle size.
There are two ways to extract the translations used in your source code.
Plugin (Recommended)
You can use the rsbuild-plugin-i18next-extractor to extract the translations used in the source code. This plugin is based on i18next-cli, and uses the Rspack module graph to extract i18n messages from modules that are actually imported by the current build. This allows it to extract translations on demand, avoid bundling unused messages, and reduce the risk of missing translations that are used through imported dependencies.
Tip
This plugin requires Node.js 22 or above.
First, install the plugin and its i18next-cli peer dependency:
npm install rsbuild-plugin-i18next-extractor i18next-cli -D
yarn add rsbuild-plugin-i18next-extractor i18next-cli -D
pnpm add rsbuild-plugin-i18next-extractor i18next-cli -D
bun add rsbuild-plugin-i18next-extractor i18next-cli -D
deno add npm:rsbuild-plugin-i18next-extractor npm:i18next-cli -D
Enable the plugin in lynx.config.ts and specify the localesDir option, which is the directory path of the raw translations:
The examples below assume your raw translations live in src/locales/.
lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
export default defineConfig({
plugins: [
pluginI18nextExtractor({
localesDir: './src/locales',
}),
],
});
You should import the translations from the localesDir which you specify above:
src/i18n.ts
import i18next from 'i18next';
import type { i18n } from 'i18next';
import enTranslation from './locales/en.json';
const localI18nInstance: i18n = i18next.createInstance();
localI18nInstance.init({
lng: 'en',
resources: {
en: {
translation: enTranslation,
},
},
});
Tip
After running DEBUG=rsbuild:i18next rspeedy dev or DEBUG=rsbuild:i18next rspeedy build, you can view the extracted translations in the node_modules/.rsbuild-plugin-i18next-extractor directory.
Configuration Options
The plugin scans node_modules by default. You can exclude files using the i18nextToolkitConfig.extract.ignore option:
lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
export default defineConfig({
plugins: [
pluginI18nextExtractor({
localesDir: './src/locales',
i18nextToolkitConfig: {
extract: {
ignore: ['node_modules/**'],
},
},
}),
],
});
You can also provide a callback for missing translation keys:
lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
export default defineConfig({
plugins: [
pluginI18nextExtractor({
localesDir: './src/locales',
onKeyNotFound: (key, locale, localeFilePath, entryName) => {
console.warn(`Missing translation key: ${key} for locale: ${locale}`);
},
}),
],
});
Advanced: Dedupe translations in Lynx bundles
For Lynx apps, it is recommended to use @lynx-js/i18next-translation-dedupe together with rsbuild-plugin-i18next-extractor to avoid bundling the same translations twice.
@lynx-js/i18next-translation-dedupe reads the translations extracted by rsbuild-plugin-i18next-extractor, skips the extractor's default rendered asset, and writes the translations into the Lynx bundle customSections for runtime loading.
First, install the package:
npm install @lynx-js/i18next-translation-dedupe -D
yarn add @lynx-js/i18next-translation-dedupe -D
pnpm add @lynx-js/i18next-translation-dedupe -D
bun add @lynx-js/i18next-translation-dedupe -D
deno add npm:@lynx-js/i18next-translation-dedupe -D
Then add pluginLynxI18nextTranslationDedupe():
lynx.config.ts
import { defineConfig } from '@lynx-js/rspeedy';
import { pluginLynxI18nextTranslationDedupe } from '@lynx-js/i18next-translation-dedupe';
import { pluginI18nextExtractor } from 'rsbuild-plugin-i18next-extractor';
export default defineConfig({
plugins: [
pluginI18nextExtractor({
localesDir: './src/locales',
}),
pluginLynxI18nextTranslationDedupe(),
],
});
At runtime, load the extracted translations from the Lynx bundle customSections:
src/i18n.ts
import i18next from 'i18next';
import type { i18n } from 'i18next';
import { loadI18nextTranslations } from '@lynx-js/i18next-translation-dedupe';
const localI18nInstance: i18n = i18next.createInstance();
localI18nInstance.init({
lng: 'en',
resources: loadI18nextTranslations(),
});
CLI
You can also use the i18next-cli to extract translations.
Tip
The i18next-cli requires Node.js 22 or above.
First, install the CLI:
npm install i18next-cli -D
deno add npm:i18next-cli -D
Add an i18next.config.ts file at the project root. For example:
i18next.config.ts
import { defineConfig } from 'i18next-cli';
export default defineConfig({
locales: ['en', 'zh'],
extract: {
input: ['src/**/*.{js,jsx,ts,tsx}'],
output: 'src/locales/{{language}}.json',
},
});
You can adjust locales, extract.input, and extract.output to match your project structure. If you use multiple namespaces, include {{namespace}} in the output pattern.
Then run the following command to extract translations:
deno i18next-cli npm:extract