Skip to content

Documentation · ARDOR

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):

ts
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: DefaultI18nProvider from packages/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 ​

ExportKindMeaning
DefaultI18nProviderclassProvider that builds the react-admin I18nProvider from II18nProviderOptions
II18nProviderOptionsinterfaceOptions defining i18nSources and languages
englishMessagesconstEnglish translation bundle containing react-admin ra.* messages
vietnameseMessagesconstVietnamese translation bundle containing react-admin ra.* messages
useTranslatehookReturns a typed translate(key, options?) function
useUntypedTranslatehookReturns translate(key, options?) for a key that exists only at run time
useRequestHeaderLocalehookSynchronizes the active locale with REST data provider request headers
useHtmlLanghookSynchronizes document <html lang> with the active locale
useDocumentTitlehookSets the document title and restores the previous title on unmount
TUseTranslateFntypeFunction signature returned by useTranslate
TUseTranslateKeystypeAccepted keys: union of default keys and IUseTranslateKeysOverrides
TUseTranslateKeysDefaulttypeTFullPaths<typeof englishMessages>
IUseTranslateKeysOverridesinterfaceOverridable interface for augmenting application translation keys
IUseRequestHeaderLocaleOptionsinterfaceOptions for useRequestHeaderLocale
IUseDocumentTitleOptionsinterfaceOptions for useDocumentTitle
CoreBindings.DEFAULT_I18N_PROVIDERconstContainer binding key for the internationalization provider instance
CoreBindings.I18N_PROVIDER_OPTIONSconstContainer binding key for II18nProviderOptions
HeaderConstants.X_LOCALEconstDefault request header name ('x-locale') used by useRequestHeaderLocale

Options ​

II18nProviderOptions from packages/admin/src/common/types.ts defines the configuration options:

FieldTypeDefaultMeaning
i18nSourcesRecord<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.
languagesArray<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:

ts
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 { ... }, as import('./vi') gives). Anything else counts as a failed load.
  • First render: when the starting locale is lazy, ArdorApplication shows suspense until 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 next changeLocale() 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:

  1. It reads navigator.language in browser environments (typeof navigator !== 'undefined'). If navigator is undefined or navigator.language is empty, it uses 'en-US'.
  2. 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').
  3. If the extracted language is present in the languages array, that language is selected.
  4. Otherwise, it falls back to 'en'.
Browser languageConfigured languagesInitial 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:

NamespaceKeysWhat it covers
action41User actions, button labels, and menu items (add, edit, delete, save, export)
boolean3Text representations of boolean states (true, false, null)
page12Screen titles, empty states, loading indicators, and error screens (dashboard, list, error)
input4File upload, image upload, references, and password field labels
message22Confirmation dialogs, validation notices, and system alerts (are_you_sure, delete_content)
navigation17Pagination controls, page ranges, filter status, and skip links (page_range_info, next, previous)
sort3Column sorting directions and labels (sort_by, ASC, DESC)
auth8Authentication inputs, menu options, and sign-in status (username, password, sign_in, logout)
notification13Toast notifications, HTTP errors, and data provider status messages (created, updated, deleted)
validation10Form input validation rules (required, minLength, maxLength, email, regex)
saved_queries9Saved filter query dialogs, labels, and actions (label, query_name, new_dialog_title)
configurable6UI 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's i18nSources are 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, ArdorApplication in packages/admin/src/components/application.tsx invokes i18nProvider.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:

HookSignatureWhat it doesWhere to call it
useTranslate() => TUseTranslateFnReturns 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() => TUseUntypedTranslateFnReturns 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) => voidSynchronizes 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() => voidSynchronizes 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) => voidSets 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:

tsx
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):

tsx
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>;
};