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".

소스 정보

저장소
marmelab/react-admin
최근 소스 활동
2026년 3월 13일 17:45
감지된 SKILL.md 언어
영어
스타
26,967
포크
5,471

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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', [
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기