Translation

Localize the admin with createI18n and message catalogs.

Chelone localizes through a small i18n instance you build with createI18n and hand to <Admin i18n={…}>. A catalog is a flat map of keys to strings; the components translate ra.* keys (page titles, pagination, bulk-action labels, booleans, auth) and the core controllers emit chelone.* keys (save notifications, validation). <Admin> works without an i18n instance; see Without i18n.

Building the i18n instance

src/i18n.ts
import { createI18n, defaultMessages, defineMessages } from "@chelone/core";
 
const en = defineMessages({
  ...defaultMessages, // the chelone.* core keys
  "ra.action.save": "Save",
  "ra.action.create": "Create",
  "ra.page.list": "{name}",
  "ra.boolean.true": "Yes",
  "ra.boolean.false": "No",
});
 
export function makeI18n() {
  return createI18n({
    locale: "en",
    messages: { en },
    fallbackLocale: "en",
  });
}

defineMessages is an identity helper that gives you key/value type-checking.

What ships in defaultMessages

defaultMessages holds only the chelone.* keys the core controllers emit (notifications, validation, auth and loading messages). No ra.* English catalog ships. Spread defaultMessages first, then write the ra.* labels your app needs yourself.

The ra.* keys are the ones the components pass to translate. Most call it with an English default, so a missing key still reads as English (see Missing keys). The keys, by group:

  • ra.action.*: add, add_filter, back, bulk_actions, bulk_edit_apply, cancel, clear_input_value, clear_search, close_menu, confirm, create, delete, edit, export, list, move_down, move_up, open_menu, refresh, remove, remove_all_filters, remove_filter, save, search, select_all, select_row, show, toggle_theme, undo, unselect, update
  • ra.auth.*: logout, sign_in, sign_in_error, user_menu
  • ra.boolean.*: true, false, null
  • ra.input.*: file.upload_single, file.upload_several, image.upload_single, image.upload_several, password.toggle_hidden, password.toggle_visible
  • ra.message.*: access_denied, auth_error, authentication_error, bulk_delete_content, bulk_delete_title, bulk_edit_description, bulk_edit_nested_source, bulk_update_content, bulk_update_title, delete_content, delete_title, error, loading
  • ra.navigation.*: next, no_results, page_range_info, page_rows_per_page, partial_page_range_info, previous
  • ra.page.*: access_denied, authentication_error, create, dashboard, error, list, loading
  • ra.saved_queries.*: new_label, query_name, remove_label_with_name
  • ra.sort.*: sort_by
  • ra.theme.*: dark, light, system

The list is read from the components, so it can grow. To check for a key, search the source for translate("ra..

Interpolation and plurals

Chelone uses single-brace interpolation — {name} — not react-admin's %{name}:

"ra.page.create": "Create {name}",
"ra.navigation.page_range_info": "{offsetBegin}-{offsetEnd} of {total}",

Plurals are an object keyed by Intl.PluralRules categories (not react-admin's a |||| b). other is required:

"ra.action.bulk_actions": {
  one: "1 item selected",
  other: "{count} items selected",
},

The form is picked from the count param, or smart_count (the name react-admin and the components use) when count is absent. Whichever you pass also fills {count} and {smart_count} in the text:

i18n.translate("ra.action.bulk_actions", { count: 1 }); // "1 item selected"
i18n.translate("ra.action.bulk_actions", { count: 4 }); // "4 items selected"

On an instance typed from a defineMessages catalog, a plural message requires count. smart_count is accepted only through useTranslate() and the catalog-erased instance <Admin> holds, which is how the components call it.

Without a numeric count, the other form is used. A category the locale needs but the message lacks (for example few in Polish) falls back to other.

Missing keys never throw

If a key isn't in the catalog, translate returns the key itself (or a _ string fallback) — it never throws. A _ fallback is interpolated with the same params, and most components pass one, so labels such as "Cancel" or "Refresh" read as English without a catalog entry:

translate("ra.action.cancel", { _: "Cancel" }); // "Cancel" when the key is missing

Where a component passes no _, a partial catalog shows the raw ra.page.list-style key. Add the keys you see.

Without i18n

i18n is optional on <Admin>. Without it, useTranslate() returns an echo: a key with no _ default comes back as the key, and a key with one renders the default with placeholders filled in. Components that pass English defaults read as English; the rest show raw keys. In React, the built-in <LoginPage> is the exception: it supplies its own English catalog (defaultMessages) and notification store for its subtree, so <Admin requireAuth> shows a working form with no i18n or notification setup. Pass i18n to <Admin> to localize it.

Multiple locales

Register more catalogs under messages and switch at runtime:

createI18n({
  locale: "en",
  messages: { en, fr },
  fallbackLocale: "en",
});

Read and change the active locale from your components:

import { useTranslate, useLocale, useSetLocale } from "@chelone/react";
 
const translate = useTranslate();   // translate("ra.action.save")
const locale = useLocale();         // "en"
const setLocale = useSetLocale();   // setLocale("fr")

Translatable records

Catalogs localize the interface. To localize the data, a translatable record stores { en, fr } under each translated field, for example { name: { en: "Chair", fr: "Chaise" } }. Edit those fields with TranslatableInputs and show them with TranslatableFields. Both take the list of locales to show; the app locale only picks the tab selected first.

Tips

  • setLocale throws on an unregistered locale — register every locale in messages before switching to it.
  • Keep the interpolation single-brace ({name}) — porting react-admin catalogs means rewriting %{name} → {name} and a |||| b → { one, other }.