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.

Usingi18next

i18next is an internationalization-framework written in and for JavaScript. Using it in ReactLynx gives you:

  1. Simplicity: i18next provides an easy-to-use API, making it simple to implement internationalization in ReactLynx applications.
  2. Dynamic Loading: Supports on-demand loading of language resources, reducing initial load time.
  3. Wide Support: Compatible with various formats and backends, allowing easy integration with different translation storage solutions such as JSON files, remote APIs, etc.
  4. Caching: Built-in caching mechanism speeds up the loading of language resources, enhancing user experience.
  5. Rich Community Support: A vast community and a wealth of plugins available to meet diverse internationalization needs.
  6. Reliability: Proven in numerous projects, offering stability and reliability.
  7. 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
yarn
pnpm
bun
npm install 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:

  1. Use v23 and must enable compatibilityJSON: 'v3'.
  2. 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:

src/locales/en.json
{
  "world": "World"
}

Creating the translation function is as simple as these 3 steps:

  1. Import the locale JSON file ./locales/en.json.
  2. Create an i18next instance with the createInstance() function.
  3. 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 };
TIP

You may need Rspeedy Type Declaration for import.meta.webpackContext.

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
yarn
pnpm
bun
npm install 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 };
  1. An i18next backend i18next-resources-to-backend has been added to the background thread with localI18nInstance.use.

  2. 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>
  );
}
Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.