Skip to main content

react-admin-i18n

Guidelines for internationalizing a react-admin or shadcn admin kit application. Use when adding i18n support, creating translation files, translating hardcoded English strings in JSX, or reviewing i18n PRs in a react-admin project. Covers namespace conventions, interpolation, pluralization, locale-aware formatting, and the full workflow for converting an English-only app to multi-language. Trigger this skill whenever the user mentions i18n, internationalization, translation, localization, locale switching, or multi-language in a react-admin or shadcn admin kit context — even if they just say "translate this component" or "add French support".

Datos de origen

Repositorio
marmelab/react-admin
Última actividad en el origen
13 de marzo de 2026 a las 17:45
Idioma detectado de SKILL.md
inglés
Estrellas
26.967
Forks
5471

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
react-admin-i18n
description
Guidelines for internationalizing a react-admin or shadcn admin kit application. Use when adding i18n support, creating translation files, translating hardcoded English strings in JSX, or reviewing i18n PRs in a react-admin project. Covers namespace conventions, interpolation, pluralization, locale-aware formatting, and the full workflow for converting an English-only app to multi-language. Trigger this skill whenever the user mentions i18n, internationalization, translation, localization, locale switching, or multi-language in a react-admin or shadcn admin kit context — even if they just say "translate this component" or "add French support".
# Internationalizing a React-Admin Application This skill describes how to add i18n support to a react-admin / shadcn admin kit application. It covers the full workflow: setting up the i18n provider, structuring translation catalogs, converting hardcoded English strings in JSX to translation keys, and avoiding the many pitfalls that make this task tedious. ## High-Level Workflow When converting an English-only react-admin app to multi-language, follow this order: 1. **Install dependencies** — `ra-i18n-polyglot`, `ra-language-english`, `ra-language-<target>`, and any auth-provider language packs (e.g. `ra-supabase-language-french`). 2. **Create the i18nProvider** — wire up `polyglotI18nProvider` with merged catalogs and browser locale detection. 3. **Create the English CRM message catalog** — extract every hardcoded string from JSX into a typed object. 4. **Create the target-language catalog** — translate every key, using strict typing (`satisfies CrmMessages`) so missing keys fail at compile time. 5. **Convert every component** — replace hardcoded strings with `<Translate>` or `useTranslate()` calls or rely on react-admin's auto-inference. 6. **Disable browser auto-translation** — add `translate="no"` and `<meta name="google" content="notranslate" />` to your HTML shell. 7. **Add a language switcher** — typically on the Profile page, using `useLocaleState()` and `useLocales()`. 8. **Audit and clean up** — remove unused keys, inline `_` defaults, and duplicate labels. ## Namespace Conventions React-admin's translation system (Polyglot.js) uses a hierarchical key structure. Respect these namespaces: - **`ra.*`** — Framework-level messages (auth, navigation, pagination, CRUD notifications, validation). Never override unless you need to customize framework wording. - **`ra-supabase.*`** (or other auth provider namespaces) — Auth-provider-specific messages. You can override individual keys via a separate override object merged on top. - **`resources.*`** — Resource and field names. This is the **default namespace react-admin uses to infer labels** in forms, show views, datagrid columns, and filters. Structure: - `resources.<resourceName>.name` — singular/plural resource label (with `||||` for pluralization) - `resources.<resourceName>.fields.<fieldName>` — field label - `resources.<resourceName>.empty` / `resources.<resourceName>.invite` — custom empty-state messages (optional, react-admin falls back to `ra.page.empty` / `ra.page.invite`) - **`app.*`** (or a project-specific prefix like `crm.*`) — Application-specific copy: empty states, helper text, tooltips, dashboard titles, domain wording. Everything that is not a resource field name and not a framework message goes here. ### Why `resources.*` Matters When you place field translations under `resources.<resource>.fields.<field>`, react-admin **automatically** resolves labels for `<TextInput source="first_name" />`, `<TextField source="first_name" />`, and filter inputs — without passing an explicit `label` prop. This eliminates an entire class of duplication bugs. ### The `forcedCaseName` Pattern Some languages (like French) don't capitalize common nouns the same way English does. React-admin's `ra.page.empty` interpolates `%{name}` which comes from `resources.<resource>.name`, but pluralization via `||||` can produce awkward casing in sentences. Use a dedicated `forcedCaseName` key when you need a specific casing for interpolation contexts: ```ts resources: { contacts: { name: "Contact |||| Contacts", // used by react-admin for menu items, plurals forcedCaseName: "Contact", // used for custom interpolation in sentences } } ``` Then in the generic empty-state component: ```tsx const resourceName = translate(`resources.${resource}.forcedCaseName`, { smart_count: 0, _: resource ? getResourceLabel(resource, 0) : undefined, }); ``` ## Rules ### 1. Don't translate twice ```tsx // ❌ Bad: Components already call translate() internally, so this duplicates the translation <TabbedForm.Tab path="identity" label={translate('resources.users.tabs.identity')} /> // ✅ Good: just pass the message key and let the component handle translation <TabbedForm.Tab path="identity" label="resources.users.tabs.identity" /> ``` Only use translate() in your components when you know that the underlying component does not already handle translation. ### 2. Let react-admin infer labels — don't set them manually ```tsx // ❌ Bad: hardcoded label duplicates the translation file <TextInput source="first_name" label={translate('resources.contacts.fields.first_name')} /> // ✅ Good: react-admin resolves the label from the resources namespace <TextInput source="first_name" /> ``` Only use an explicit `label` when the desired label differs from the field-name-based default. ### 3. Don't duplicate labels across fields, inputs, and filters A single key in `resources.<resource>.fields.<field>` covers the `<TextField>`, the `<TextInput>`, and the filter input. **Never** create separate keys like `resources.contacts.fields.first_name`, `resources.contacts.inputs.first_name`, and `resources.contacts.filters.first_name`. ### 4. Use interpolation, never concatenation ```tsx // ❌ Bad: concatenation breaks in languages with different word order <span>{translate('crm.common.last_activity')} {relativeDate}</span> // ✅ Good: interpolation lets each language place the variable where it belongs <span>{translate('crm.common.last_activity_with_date', { date: relativeDate })}</span> // en: "last activity %{date}" // fr: "dernière activité %{date}" ``` Always pass dynamic values as interpolation variables ("Added on %{date}", "Followed by %{name}", etc.). Note that Polyglot only interpolates **string values**. You cannot interpolate React components. If you need to embed a component (e.g. `<DateField>`), format the value as a plain string first, then interpolate it. ### 5. Use Polyglot's built-in pluralization Polyglot supports pluralization via `||||`-separated forms, to support languages that have more than two plural forms. Don't reimplement plural logic in JS. For example, to render "1 task" or "5 tasks" based on a `nbTasks` variable: ```ts // ❌ Bad: handle pluralization by hand return <>{nbTasks}{" "}{translate(nbTasks === 1 ? "app.tasks.name.singular" : "app.tasks.name.plural")}</>; // english catalog "app.tasks.name.singular": "task", "app.tasks.name.plural": "tasks", // ✅ Good: use Polyglot's pluralization syntax return <>{translate('app.tasks.name', { smart_count: nbTasks })}</>; // english catalog "app.tasks.name": "%{smart_count} task |||| %{smart_count} tasks" ``` ### 6. Scope messages to their feature — avoid over-sharing "common" keys If a message is only ever used in one resource or feature, scope it there. Only promote a key to `*.common.*` when it is genuinely used in 3+ unrelated places. Shared "common" buckets grow into junk drawers that no one dares to clean up. Good scoping examples from a real project: - `resources.contacts.merge.title` — only used in contact merge - `resources.deals.archived.action` — only used in deal archiving - `app.dashboard.deals_chart` — only used on the dashboard - `app.common.retry` — genuinely shared across error states ### 7. Handle grammar edge cases with separate keys, not string tricks Some languages require different verb conjugation depending on the subject. For example, in French: - "Jean **a ajouté** une note" (third person) - "Vous **avez ajouté** une note" (second person formal) Interpolation alone can't fix this. Use separate translation keys and select at runtime based on the current user identity: ```tsx const { identity } = useGetIdentity(); const isCurrentUser = identity?.id === activity.sales_id; translate( isCurrentUser ? "crm.activity.you_added_contact" : "crm.activity.added_contact", { name: salesName }, ); ``` ```ts // en "crm.activity.added_contact": "%{name} added", "crm.activity.you_added_contact": "You added", // fr "crm.activity.added_contact": "%{name} a ajouté le contact", "crm.activity.you_added_contact": "Vous avez ajouté le contact", ``` ### 8. Leverage built-in defaults from controllers React-admin controllers (`useEditController`, `useCreateController`, etc.) already provide interpolated `defaultTitle` values. Don't rebuild page titles by hand: ```tsx // ❌ Bad: manually building the edit title <h1>{translate('resources.contacts.action.edit')} <RecordRepresentation /></h1> // ✅ Good: use the title provided by the controller const { defaultTitle } = useEditContext(); <h1>{defaultTitle}</h1> ``` ### 9. Use `_` defaults for progressive adoption, but remove them once catalogs are complete During migration, inline defaults like `translate('crm.auth.welcome_title', { _: 'Welcome to Atomic CRM' })` prevent the app from showing raw keys. Once you have complete translation files, **remove all inline defaults** — they mask missing keys and make it impossible to detect untranslated strings. Exception: keep `_` defaults for strings that may not exist in all catalogs (e.g. optional auth provider messages, or partial third-party locale packs). ### 10. Translate `notify()` calls, not just JSX Every `notify()` call with a hardcoded string must also be translated. Pass a translation key as the first argument: ```tsx // ❌ Bad notify("Profile updated successfully"); // ✅ Good notify("crm.profile.updated", { messageArgs: { _: "Your profile has been updated" }, }); ``` ### 11. Use Intl for number and date formatting Localization of numbers and dates is already handled by JavaScript's built-in `Intl` methods, and they use the browser's locale automatically. Use them for any number or date formatting in your app. Note: `<NumberField>` and `<NumberInput>` components already use `Intl.NumberFormat` under the hood, so they will automatically format numbers according to the current locale without any extra work. ### 12. Detect browser language as the initial locale To select the initial locale based on the user's browser settings, use `resolveBrowserLocale()` from `ra-core` when initializing your i18nProvider: ```tsx // in src/i18nProvider.js import { resolveBrowserLocale } from 'ra-core'; import polyglotI18nProvider from 'ra-i18n-polyglot'; import en from 'ra-language-english'; import fr from 'ra-language-french'; const translations = { en, fr }; export const i18nProvider = polyglotI18nProvider( locale => translations[locale] ? translations[locale] : translations.en, resolveBrowserLocale(), [ { locale: 'en', name: 'English' }, { locale: 'fr', name: 'Français' } ], ); ``` ### 13. Disable browser auto-translation When your app handles its own i18n, browser auto-translate (Chrome's Google Translate bar) creates conflicts. Disable it: ```html <html lang="en" translate="no" class="notranslate"> <head> <meta name="google" content="notranslate" /> ... </head> ``` Apply this to all HTML entry points (main `index.html`, auth callback pages, etc.). ### 14. Don't forget sr-only and aria labels Screen-reader-only text and aria labels are also user-facing strings that need translation: ```tsx // ❌ Bad <span className="sr-only">Toggle theme</span> // ✅ Good <span className="sr-only">{translate("ra.action.toggle_theme")}</span> ``` ### 15. Don't forget placeholder text Placeholders in search inputs, autocomplete fields, and text areas are easy to miss: ```tsx // ❌ Bad <CommandInput placeholder="Search..." /> // ✅ Good — pass a translated placeholder prop <CommandInput placeholder={placeholder} /> ``` ## Type-Safe Message Catalogs Define your English catalog as a `const` assertion and derive types from it. This catches missing keys at compile time for in-repo locales: ```ts // englishCrmMessages.ts export const englishCrmMessages = { resources: { /* ... */ }, crm: { /* ... */ }, } as const; type MessageSchema<T> = { [K in keyof T]: T[K] extends string ? string : T[K] extends Record<string, unknown> ? MessageSchema<T[K]> : never; }; type DeepPartial<T> = { [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K]; }; export type AppMessages = MessageSchema<typeof englishAppMessages>; export type PartialAppMessages = DeepPartial<AppMessages>; ``` For the strict in-repo locale (e.g. French), use `satisfies AppMessages`: ```ts // frenchAppMessages.ts import type { AppMessages } from "./englishAppMessages"; export const frenchAppMessages = { resources: { /* ... */ }, crm: { /* ... */ }, } satisfies AppMessages; ``` For external/community locales, use `PartialAppMessages` to allow incremental translation with English fallback. ## i18nProvider Setup The i18nProvider merges framework messages, auth-provider messages, any overrides, and your CRM messages. The non-default locale catalog should be merged **on top of the English catalog** so that untranslated keys fall back to English: ```ts import { mergeTranslations } from "ra-core"; import polyglotI18nProvider from "ra-i18n-polyglot"; import englishMessages from "ra-language-english"; import frenchMessages from "ra-language-french"; import { raSupabaseEnglishMessages } from "ra-supabase-language-english"; import { raSupabaseFrenchMessages } from "ra-supabase-language-french"; import { englishCrmMessages } from "./englishCrmMessages"; import { frenchCrmMessages } from "./frenchCrmMessages"; const englishCatalog = mergeTranslations( englishMessages, raSupabaseEnglishMessages, englishCrmMessages, ); // Merge French ON TOP of English so missing keys fall back to English const frenchCatalog = mergeTranslations( englishCatalog, frenchMessages, raSupabaseFrenchMessages, frenchCrmMessages, ); export const i18nProvider = polyglotI18nProvider( (locale) => { if (locale === "fr") return frenchCatalog; return englishCatalog; }, 'en', [
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub