Multilingual support with react-i18next#
When your application serves multiple language communities — a citizen portal with Hindi and English, an ERP with local + English — add react-i18next alongside the Palmyra component stack. Form labels, error messages, button text, and grid headers all pull from translation files.
Install#
npm install react-i18next i18next i18next-browser-languagedetectorSetup#
// src/i18n/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './locales/en.json';
import hi from './locales/hi.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: { en: { translation: en }, hi: { translation: hi } },
fallbackLng: 'en',
interpolation: { escapeValue: false },
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
export default i18n;Import it once in main.tsx:
import './i18n/i18n';Translation files#
// src/i18n/locales/en.json
{
"app.title": "Employee Management",
"nav.departments": "Departments",
"nav.employees": "Employees",
"form.email": "Email",
"form.firstName": "First Name",
"form.lastName": "Last Name",
"form.department": "Department",
"form.save": "Save",
"form.cancel": "Cancel",
"form.required": "This field is required",
"grid.search": "Search",
"grid.add": "Add"
}// src/i18n/locales/hi.json
{
"app.title": "कर्मचारी प्रबंधन",
"nav.departments": "विभाग",
"nav.employees": "कर्मचारी",
"form.email": "ईमेल",
"form.firstName": "पहला नाम",
"form.lastName": "उपनाम",
"form.department": "विभाग",
"form.save": "सहेजें",
"form.cancel": "रद्द करें",
"form.required": "यह फ़ील्ड आवश्यक है",
"grid.search": "खोजें",
"grid.add": "जोड़ें"
}Language switcher#
// src/components/LanguageSwitcher.tsx
import { Button, Group, Popover } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useDisclosure } from '@mantine/hooks';
const languages = [
{ code: 'en', label: 'English' },
{ code: 'hi', label: 'हिन्दी' },
];
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
const [opened, { toggle, close }] = useDisclosure(false);
return (
<Popover opened={opened} onClose={close}>
<Popover.Target>
<Button variant="subtle" size="xs" onClick={toggle}>
{languages.find(l => l.code === i18n.language)?.label ?? 'Language'}
</Button>
</Popover.Target>
<Popover.Dropdown>
<Group>
{languages.map(l => (
<Button key={l.code} size="xs"
variant={i18n.language === l.code ? 'filled' : 'subtle'}
onClick={() => { i18n.changeLanguage(l.code); close(); }}>
{l.label}
</Button>
))}
</Group>
</Popover.Dropdown>
</Popover>
);
}Using in Palmyra form fields#
Palmyra’s MantineTextField accepts a label prop — pass the translated string:
import { useTranslation } from 'react-i18next';
import { MantineTextField } from '@palmyralabs/rt-forms-mantine';
function EmployeeForm() {
const { t } = useTranslation();
return (
<FieldGroupContainer columns={2}>
<MantineTextField attribute="loginName" label={t('form.email')} required
invalidMessage={t('form.required')} />
<MantineTextField attribute="firstName" label={t('form.firstName')} required />
<MantineTextField attribute="lastName" label={t('form.lastName')} required />
</FieldGroupContainer>
);
}Grid column labels and toolbar buttons follow the same pattern — the t() call is the only change.
Guidelines#
- Keep
attributein English. Onlylabel,placeholder,invalidMessage, and button text go throught(). Theattributeprop must match the backend’s field name. - Persist the choice.
i18next-browser-languagedetector+localStoragemeans the user picks once and it sticks. - Add languages incrementally. Start with two; each new language is one JSON file.
See also: MantineTextField.