Internationalization
ARDOR ships a react-admin I18nProvider built on ra-i18n-polyglot. You configure it with one options object bound in the container. Components read translations through useTranslate, and useRequestHeaderLocale forwards the current locale to the API as a request header.
Prerequisites
An application built on BaseArdorApplication with a container - see Application.
Quick Reference
| Export | Kind | Purpose |
|---|---|---|
DefaultI18nProvider | class | Provider that builds the react-admin I18nProvider from II18nProviderOptions |
II18nProviderOptions | interface | i18nSources and listLanguages |
englishMessages | const | English bundle with the react-admin ra.* keys |
vietnameseMessages | const | Vietnamese bundle with the react-admin ra.* keys |
useTranslate | hook | Typed translate(key, options?) |
TUseTranslateFn | type | Signature of the function returned by useTranslate |
TUseTranslateKeys | type | Accepted keys: default keys plus IUseTranslateKeysOverrides |
TUseTranslateKeysDefault | type | TFullPaths<typeof englishMessages> |
IUseTranslateKeysOverrides | interface | Empty interface you augment to add application keys |
TFullPaths | type | Dotted paths of a nested object type |
useRequestHeaderLocale | hook | Sets the locale header on the default REST data provider |
CoreBindings.I18N_PROVIDER_OPTIONS | binding key | Where the options object is read from |
HeaderConsts.X_LOCALE | const | Default header name used by useRequestHeaderLocale |
DefaultI18nProvider
DefaultI18nProvider extends BaseProvider<I18nProvider>. Its constructor receives II18nProviderOptions from the container under CoreBindings.I18N_PROVIDER_OPTIONS. Its value() returns a polyglotI18nProvider configured as follows:
- messages for a locale come from
i18nSources[locale], falling back toenglishMessages - missing keys are allowed; the provider returns the key itself as the translated text
- the initial locale is chosen from the browser language (see below)
class DefaultI18nProvider extends BaseProvider<I18nProvider> {
constructor(i18nProviderOptions: II18nProviderOptions);
value(container: Container): I18nProvider;
}You do not construct it yourself. The application resolves it from the container; see Binding keys.
II18nProviderOptions
interface II18nProviderOptions {
i18nSources?: Record<string | symbol, AnyType>;
listLanguages?: Locale[]; // { locale: string; name: string }
}| Option | Default | Meaning |
|---|---|---|
i18nSources | { en: englishMessages } | Message bundle per locale code |
listLanguages | [{ locale: 'en', name: 'English' }] | Locales offered to react-admin, with a display name |
Both defaults apply independently. If you pass listLanguages with vi but no i18nSources.vi, the vi locale is selectable and resolves to englishMessages.
Build the options object and bind it under CoreBindings.I18N_PROVIDER_OPTIONS:
import {
type II18nProviderOptions,
englishMessages,
vietnameseMessages,
} from '@venizia/ardor';
export const i18nOptions: II18nProviderOptions = {
i18nSources: {
en: englishMessages,
vi: vietnameseMessages,
},
listLanguages: [
{ locale: 'en', name: 'English' },
{ locale: 'vi', name: 'Tiếng Việt' },
],
};// inside your application's bindContext()
this.container.bind({ key: CoreBindings.I18N_PROVIDER_OPTIONS, value: i18nOptions });
...The binding API is described in Application.
Initial locale selection
The provider reads navigator.language once, at module load, and keeps the part before the first -. en-US becomes en, vi-VN becomes vi. If navigator.language is empty, en-US is assumed.
The initial locale is that language if it appears in listLanguages; otherwise it is 'en'. The fallback is always the literal 'en', even when 'en' is not in listLanguages.
| Browser language | listLanguages locales | Initial locale |
|---|---|---|
vi-VN | en, vi | vi |
fr-FR | en, vi | en |
fr-FR | vi | en |
Message bundles
englishMessages and vietnameseMessages are complete bundles for the react-admin ra.* keys (ra.action.*, ra.page.*, ra.message.*, and so on). Use them as the base of every locale you offer. The bundle contents are not reproduced here.
englishMessages is also the source of the default key type: TUseTranslateKeysDefault is TFullPaths<typeof englishMessages>, so every ra.* path is a valid useTranslate key out of the box.
Extending the bundles with application messages
Spread a bundle and add your own top-level namespace. Keep application keys in their own namespace so they never collide with ra.
import { type II18nProviderOptions, englishMessages, vietnameseMessages } from '@venizia/ardor';
export const appEn = {
...englishMessages,
app: {
welcome: 'Welcome',
products: {
empty: 'No products yet',
},
},
};
export const appVi = {
...vietnameseMessages,
app: {
welcome: 'Chào mừng',
products: {
empty: 'Chưa có sản phẩm',
},
},
};
export const i18nOptions: II18nProviderOptions = {
i18nSources: { en: appEn, vi: appVi },
listLanguages: [
{ locale: 'en', name: 'English' },
{ locale: 'vi', name: 'Tiếng Việt' },
],
};The spread is shallow. If you add a ra key on the application object, it replaces the whole ra subtree from the bundle. To override a single ra.* message, spread that subtree too.
useTranslate
type TUseTranslateFn = (key: TUseTranslateKeys, options?: AnyType) => string;
const useTranslate: () => TUseTranslateFn;useTranslate reads the current react-admin I18nProvider and returns a memoized translate function. options is forwarded unchanged to the provider's translate. When no provider is present in the tree, the hook returns an identity function that gives the key back, so components render without crashing outside the admin tree.
import { useTranslate } from '@venizia/ardor';
export const SaveLabel = () => {
const translate = useTranslate();
return <span>{translate('ra.action.save')}</span>;
};Because missing keys are allowed by the provider, a key that exists in no bundle renders as the key string itself.
Typing application keys
TUseTranslateKeys is the union of TUseTranslateKeysDefault and keyof IUseTranslateKeysOverrides. IUseTranslateKeysOverrides is empty by default. Augment it in @venizia/ardor-admin with the dotted paths of your application object, computed by TFullPaths, and translate('app.welcome') type-checks.
import { type TFullPaths, englishMessages } from '@venizia/ardor';
export const appEn = {
...englishMessages,
app: {
welcome: 'Welcome',
products: {
empty: 'No products yet',
},
},
};
type TAppKeys = TFullPaths<typeof appEn>;
declare module '@venizia/ardor-admin' {
interface IUseTranslateKeysOverrides extends Record<TAppKeys, string> {}
}Only the property names of IUseTranslateKeysOverrides matter; the value type is ignored. Put the augmentation in a .d.ts or a module that is always included in the build. See Module augmentation.
With the augmentation in the same build, the application key is accepted by translate. The snippet below repeats the augmentation so it stands on its own:
import { type TFullPaths, englishMessages, useTranslate } from '@venizia/ardor';
const appEn = {
...englishMessages,
app: {
welcome: 'Welcome',
products: {
empty: 'No products yet',
},
},
};
type TAppKeys = TFullPaths<typeof appEn>;
declare module '@venizia/ardor-admin' {
interface IUseTranslateKeysOverrides extends Record<TAppKeys, string> {}
}
export const EmptyProducts = () => {
const translate = useTranslate();
return <p>{translate('app.products.empty')}</p>;
};useRequestHeaderLocale
const useRequestHeaderLocale: (params?: { key?: string }) => void;useRequestHeaderLocale watches the react-admin locale (useLocaleState) and, on every change, calls setHeaders({ [key]: locale }) on the network service of the DefaultRestDataProvider bound at CoreBindings.DEFAULT_REST_DATA_PROVIDER. The header name defaults to HeaderConsts.X_LOCALE (x-locale); pass key to use another name.
Call it once, in a component that lives inside both the application context and the admin tree.
import { useRequestHeaderLocale } from '@venizia/ardor';
export const LocaleHeaderSync = () => {
useRequestHeaderLocale();
return null;
};With a custom header name:
import { useRequestHeaderLocale } from '@venizia/ardor';
export const AcceptLanguageSync = () => {
useRequestHeaderLocale({ key: 'accept-language' });
return null;
};The header is set on the default REST data provider only. Other network services are not affected; see Network.
Common pitfalls
- Locale listed, bundle missing. A locale in
listLanguageswithout an entry ini18nSourcessilently usesenglishMessages. - Fallback is
'en', not the first listed locale. When the browser language is not offered, the provider starts ineneven if yourlistLanguagesdoes not contain it. - Browser language is read once. The language is captured when the module loads, not per render. Changing the browser setting needs a reload.
- Missing keys do not throw. With
allowMissing: truethe key string is rendered. A typo shows up asapp.welcomon screen, not as an error. Typing the keys throughIUseTranslateKeysOverridescatches this at compile time. - Shallow spread.
{ ...englishMessages, ra: { ... } }replaces the entirerasubtree. useTranslateoutside the admin tree. It returns the identity function, so text appears untranslated rather than failing.useRequestHeaderLocaleneeds both contexts. It callsuseInjectableanduseLocaleState, so it must run under the application context and inside react-admin.
Related
- Application - where options are bound and the provider is resolved
- Binding keys -
CoreBindings.I18N_PROVIDER_OPTIONSandCoreBindings.DEFAULT_REST_DATA_PROVIDER - Data provider - the
DefaultRestDataProviderthat receives the locale header - Network -
setHeaderson the network service - Hooks - the other ARDOR hooks
- Types -
TFullPathsand related utility types - Module augmentation - extending
IUseTranslateKeysOverrides - Migrating from ra-core-infra