Internationalization
DefaultI18nProvider creates the react-admin internationalization provider (I18nProvider) backed by ra-i18n-polyglot. The configuration class is the main way to configure ARDOR: a @configuration() class whose @provide({ key }) methods return the options or the provider instances; start() binds each as a lazy SINGLETON (see The bind order):
import {
type BaseArdorApplication as TArdorApplication,
configuration,
CoreBindings,
DefaultI18nProvider,
englishMessages,
type II18nProviderOptions,
inject,
provide,
vietnameseMessages,
} from '@venizia/ardor';
@configuration()
export class I18nConfiguration {
constructor(
@inject({ key: CoreBindings.APPLICATION_INSTANCE })
private readonly application: TArdorApplication,
) {}
@provide({ key: CoreBindings.I18N_PROVIDER_OPTIONS })
i18nProviderOptions(): II18nProviderOptions {
return {
i18nSources: {
en: englishMessages,
vi: vietnameseMessages,
},
languages: [
{ locale: 'en', name: 'English' },
{ locale: 'vi', name: 'Tiếng Việt' },
],
};
}
@provide({ key: CoreBindings.DEFAULT_I18N_PROVIDER })
i18nProvider() {
return this.application.instantiate(DefaultI18nProvider).value(this.application);
}
}- Default class:
DefaultI18nProviderfrompackages/admin/src/providers/i18n.ts. - Options key:
CoreBindings.I18N_PROVIDER_OPTIONS(@app/application/options/i18n). - Provider key:
CoreBindings.DEFAULT_I18N_PROVIDER(@app/application/i18n/default).
Calling this.application.instantiate(DefaultI18nProvider) injects I18N_PROVIDER_OPTIONS and optional FEATURES from the container into the provider. Calling .value(this.application) extracts the react-admin I18nProvider passed to the application in Application.
What it does
| Export | Kind | Meaning |
|---|---|---|
DefaultI18nProvider | class | Provider that builds the react-admin I18nProvider from II18nProviderOptions |
II18nProviderOptions | interface | Options defining i18nSources and languages |
englishMessages | const | English translation bundle containing react-admin ra.* messages |
vietnameseMessages | const | Vietnamese translation bundle containing react-admin ra.* messages |
useTranslate | hook | Returns a typed translate(key, options?) function |
useUntypedTranslate | hook | Returns translate(key, options?) for a key that exists only at run time |
useRequestHeaderLocale | hook | Synchronizes the active locale with REST data provider request headers |
useHtmlLang | hook | Synchronizes document <html lang> with the active locale |
useDocumentTitle | hook | Sets the document title and restores the previous title on unmount |
TUseTranslateFn | type | Function signature returned by useTranslate |
TUseTranslateKeys | type | Accepted keys: union of default keys and IUseTranslateKeysOverrides |
TUseTranslateKeysDefault | type | TFullPaths<typeof englishMessages> |
IUseTranslateKeysOverrides | interface | Overridable interface for augmenting application translation keys |
IUseRequestHeaderLocaleOptions | interface | Options for useRequestHeaderLocale |
IUseDocumentTitleOptions | interface | Options for useDocumentTitle |
CoreBindings.DEFAULT_I18N_PROVIDER | const | Container binding key for the internationalization provider instance |
CoreBindings.I18N_PROVIDER_OPTIONS | const | Container binding key for II18nProviderOptions |
HeaderConstants.X_LOCALE | const | Default request header name ('x-locale') used by useRequestHeaderLocale |
Options
II18nProviderOptions from packages/admin/src/common/types.ts defines the configuration options:
| Field | Type | Default | Meaning |
|---|---|---|---|
i18nSources | Record<string, TI18nSource> | { en: englishMessages } | Dictionary mapping locale codes to translation message bundles, each an object or a lazy loader (() => Promise<object>). Merged with feature messages per locale. |
languages | Array<Locale> | [{ locale: 'en', name: 'English' }] | Available locales with display names ({ locale: string, name: string }) passed to react-admin. |
Both options apply independently. If languages defines a locale code that is missing from i18nSources, selecting that locale resolves messages using englishMessages.
Lazy sources
A source is the messages object, or a function that returns a promise of it, called the first time its locale is used. A bundler turns () => import('./vi') into a chunk that stays out of the boot bundle:
const i18nProviderOptions: II18nProviderOptions = {
i18nSources: {
en: englishMessages, // read at once
vi: () => import('./vi'), // loaded when the user picks Vietnamese
},
languages: [
{ locale: 'en', name: 'English' },
{ locale: 'vi', name: 'Tieng Viet' },
], // ...
};- What a loader returns: the messages, or an ES module whose default export is the messages (
export default { ... }, asimport('./vi')gives). Anything else counts as a failed load. - First render: when the starting locale is lazy,
ArdorApplicationshowssuspenseuntil its messages arrive, as it does for a feature's lazy messages. - A failed load (a file that does not download, a chunk that did not arrive) does not stop the admin. The error is logged through the provider's
this.logger, the locale shows the English messages (and the features' messages) and nothing is remembered, so the nextchangeLocale()calls the loader again. - Loaded once: after a loader delivers, the messages are kept; a second
changeLocale()to the same locale does not call it again.
Locale resolution and fallback
In packages/admin/src/providers/i18n.ts, DefaultI18nProvider.resolveInitialLocale computes the startup locale when value() is called:
- It reads
navigator.languagein browser environments (typeof navigator !== 'undefined'). Ifnavigatoris undefined ornavigator.languageis empty, it uses'en-US'. - It splits the language string on
-and extracts the primary language subtag before the first hyphen (for example,'en-US'becomes'en', and'vi-VN'becomes'vi'). - If the extracted language is present in the
languagesarray, that language is selected. - Otherwise, it falls back to
'en'.
| Browser language | Configured languages | Initial locale |
|---|---|---|
en-US | ['en', 'vi'] | en |
vi-VN | ['en', 'vi'] | vi |
fr-FR | ['en', 'vi'] | en |
fr-FR | ['vi'] | en |
NOTE
The initial locale fallback is always 'en', even if 'en' is not included in languages.
When rendering messages, composeMessages reads i18nSources[locale]: the object, or the messages a lazy source has delivered. If that locale is not defined in i18nSources, or its lazy source has not delivered or failed, it defaults to englishMessages. Missing translation keys do not throw: polyglotI18nProvider sets allowMissing: true and onMissingKey to return the missing key string unchanged.
Shipped message bundles
ARDOR ships react-admin translation bundles in packages/admin/src/common/locales/en.ts (englishMessages) and packages/admin/src/common/locales/vi.ts (vietnameseMessages), structured under twelve ra namespaces:
| Namespace | Keys | What it covers |
|---|---|---|
action | 41 | User actions, button labels, and menu items (add, edit, delete, save, export) |
boolean | 3 | Text representations of boolean states (true, false, null) |
page | 12 | Screen titles, empty states, loading indicators, and error screens (dashboard, list, error) |
input | 4 | File upload, image upload, references, and password field labels |
message | 22 | Confirmation dialogs, validation notices, and system alerts (are_you_sure, delete_content) |
navigation | 17 | Pagination controls, page ranges, filter status, and skip links (page_range_info, next, previous) |
sort | 3 | Column sorting directions and labels (sort_by, ASC, DESC) |
auth | 8 | Authentication inputs, menu options, and sign-in status (username, password, sign_in, logout) |
notification | 13 | Toast notifications, HTTP errors, and data provider status messages (created, updated, deleted) |
validation | 10 | Form input validation rules (required, minLength, maxLength, email, regex) |
saved_queries | 9 | Saved filter query dialogs, labels, and actions (label, query_name, new_dialog_title) |
configurable | 6 | UI customization inspector titles, element labels, and toggle controls (inspector, customize) |
When adding custom messages, spread the base bundle and define translations under a dedicated namespace such as app.
WARNING
Object spreading is shallow. Writing { ...englishMessages, ra: { action: { save: 'Save changes' } } } replaces the entire ra tree. To override individual framework keys, spread the nested subtrees or declare custom messages in a distinct namespace.
Feature-contributed messages
Features mounted by the application contribute localized messages through the messages property of TFeature (see Features). In packages/admin/src/features/messages.ts, FeatureMessages coordinates feature translations:
- Feature messages are merged flat per locale without prefixes. If two features declare identical translation keys, the message merge throws an error naming both features to prevent silent collisions.
- In
DefaultI18nProvider.composeMessages(), feature messages are passed as base messages, and the application'si18nSourcesare merged over them by a deep merge: a later source wins, plain objects merge recursively, and an array or a string replaces. The application can therefore override or relabel any feature message. - Features may provide lazy message loaders:
() => Promise<object>. When a feature specifies a lazy loader for the active initial locale,ArdorApplicationinpackages/admin/src/components/application.tsxinvokesi18nProvider.changeLocale()to load the bundle before rendering the administration interface.
Hooks that read or update the locale
ARDOR provides five hooks that interact with the active locale and internationalized messages:
| Hook | Signature | What it does | Where to call it |
|---|---|---|---|
useTranslate | () => TUseTranslateFn | Returns a typed translate(key, options?) function. Reads the i18n provider of the tree; with none mounted, ra-core's default provider answers (packages/admin/src/hooks/use-translate.ts). | Inside any component rendered within the react-admin tree (or outside it, where ra-core's default provider answers). |
useUntypedTranslate | () => TUseUntypedTranslateFn | Returns translate(key: string, options?) for a key built at run time (errors.${code}). It is ra-core's useTranslate with a string key, and useTranslate is that same function with the key narrowed, so both read the same provider and catalogs (packages/admin/src/hooks/use-translate.ts). | Inside any component rendered within the react-admin tree (or outside it, where ra-core's default provider answers). |
useRequestHeaderLocale | (opts?: IUseRequestHeaderLocaleOptions) => void | Synchronizes the active locale from useLocaleState() into DefaultRestDataProvider request headers using useInjectable() on CoreBindings.DEFAULT_REST_DATA_PROVIDER. Header key defaults to HeaderConstants.X_LOCALE ('x-locale'). See Network and Data provider. | Inside a component mounted inside both the application context and the react-admin tree. |
useHtmlLang | () => void | Synchronizes document <html lang> with the active locale from useLocaleState() (packages/admin/src/hooks/use-html-lang.ts). | Inside any component rendered within the react-admin tree. |
useDocumentTitle | (opts: IUseDocumentTitleOptions) => void | Sets document.title to the given screen title segments joined with appTitle. Restores previous title on unmount if unchanged (packages/admin/src/hooks/use-document-title.ts). | Inside page or layout components. |
The three companion hooks can be mounted together in an administrative layout component:
import {
useDocumentTitle,
useHtmlLang,
useRequestHeaderLocale,
useTranslate,
} from '@venizia/ardor';
export const AdminLayoutSync = () => {
const translate = useTranslate();
useHtmlLang();
useRequestHeaderLocale({ key: 'accept-language' });
useDocumentTitle({
title: translate('ra.page.dashboard'),
appTitle: 'Store Admin',
});
return null;
};To enable type checking for custom application translation keys, augment IUseTranslateKeysOverrides in @venizia/ardor using Record<TAppKeys, true> and TFullPaths (see Module augmentation and Add a language):
import {
type TFullPaths,
englishMessages,
useTranslate,
} from '@venizia/ardor';
const appMessages = {
...englishMessages,
app: {
welcome: 'Welcome',
items: {
empty: 'No items found',
},
},
};
type TAppKeys = TFullPaths<typeof appMessages>;
declare module '@venizia/ardor' {
interface IUseTranslateKeysOverrides extends Record<TAppKeys, true> {}
}
export const ItemNotice = () => {
const translate = useTranslate();
return <p>{translate('app.items.empty')}</p>;
};