A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
React Admin provides comprehensive internationalization (i18n) support for building multi-language admin applications. The system handles UI translations, locale-specific formatting, right-to-left (RTL) languages, and translatable content management.
The i18n provider defines how your application handles translations and locale management.
import { I18nProvider } from 'react-admin';
interface I18nProvider {
translate: (key: string, options?: TranslateOptions) => string;
changeLocale: (locale: string) => Promise<void>;
getLocale: () => string;
getLocales?: () => Locale[];
}
interface TranslateOptions {
smart_count?: number;
_?: string;
[key: string]: any;
}
interface Locale {
locale: string;
name: string;
}import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
import spanishMessages from 'ra-language-spanish';
const messages = {
en: englishMessages,
fr: frenchMessages,
es: spanishMessages,
};
const i18nProvider = polyglotI18nProvider(
locale => messages[locale],
'en', // default locale
[
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'Français' },
{ locale: 'es', name: 'Español' }
]
);
// Usage in Admin
<Admin
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<Resource name="posts" list={PostList} />
</Admin>Hook for translating messages in components.
import { useTranslate } from 'react-admin';
type TranslateFunction = (key: string, options?: TranslateOptions) => string;
const useTranslate: () => TranslateFunction;import { useTranslate } from 'react-admin';
const MyComponent = () => {
const translate = useTranslate();
return (
<div>
<h1>{translate('page.dashboard.title')}</h1>
<p>{translate('page.dashboard.welcome', { name: 'John' })}</p>
<p>{translate('post.count', { smart_count: 5 })}</p>
</div>
);
};
// Message files:
// en.json: { "page.dashboard.title": "Dashboard", "page.dashboard.welcome": "Welcome, %{name}!", "post.count": "One post |||| %{smart_count} posts" }
// fr.json: { "page.dashboard.title": "Tableau de bord", "page.dashboard.welcome": "Bienvenue, %{name} !", "post.count": "Un article |||| %{smart_count} articles" }Hook for translating field and resource labels.
import { useTranslateLabel } from 'react-admin';
const useTranslateLabel: () => (params: TranslateLabelParams) => string;
interface TranslateLabelParams {
source?: string;
resource?: string;
label?: string;
}import { useTranslateLabel } from 'react-admin';
const CustomField = ({ source, resource, label }) => {
const translateLabel = useTranslateLabel();
const fieldLabel = translateLabel({ source, resource, label });
return (
<div>
<label>{fieldLabel}</label>
{/* field content */}
</div>
);
};Hooks for getting and setting the current locale.
import { useLocale, useSetLocale } from 'react-admin';
const useLocale: () => string;
const useSetLocale: () => (locale: string) => Promise<void>;Hook for getting available locales.
import { useLocales } from 'react-admin';
const useLocales: () => Locale[] | undefined;import { useLocale, useSetLocale, useLocales } from 'react-admin';
const LanguageSwitcher = () => {
const locale = useLocale();
const setLocale = useSetLocale();
const locales = useLocales();
const handleLocaleChange = (newLocale) => {
setLocale(newLocale);
};
return (
<select value={locale} onChange={(e) => handleLocaleChange(e.target.value)}>
{locales?.map(locale => (
<option key={locale.locale} value={locale.locale}>
{locale.name}
</option>
))}
</select>
);
};
// Using LocalesMenuButton component
import { LocalesMenuButton } from 'react-admin';
const AppBar = () => (
<AppBar>
<TitlePortal />
<Box flex="1" />
<LocalesMenuButton />
<UserMenu />
</AppBar>
);Component for managing translatable form inputs.
import { TranslatableInputs } from 'react-admin';
interface TranslatableInputsProps {
locales?: string[];
defaultLocale?: string;
selector?: React.ComponentType<TranslatableInputsTabsProps>;
children: React.ReactNode;
className?: string;
sx?: any;
}
const TranslatableInputs: React.FC<TranslatableInputsProps>;import {
TranslatableInputs,
TextInput,
Edit,
SimpleForm
} from 'react-admin';
const PostEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="id" disabled />
<TranslatableInputs locales={['en', 'fr', 'es']}>
<TextInput source="title" validate={required()} />
<TextInput
source="content"
multiline
rows={4}
validate={required()}
/>
<TextInput source="metaDescription" />
</TranslatableInputs>
<DateInput source="publishedAt" />
</SimpleForm>
</Edit>
);
// Data structure:
// {
// id: 1,
// title: { en: "Hello", fr: "Bonjour", es: "Hola" },
// content: { en: "Content", fr: "Contenu", es: "Contenido" },
// publishedAt: "2024-01-15"
// }Component for displaying translatable content in show/list views.
import { TranslatableFields } from 'react-admin';
interface TranslatableFieldsProps {
locales?: string[];
selector?: React.ComponentType<TranslatableFieldsTabsProps>;
children: React.ReactNode;
className?: string;
sx?: any;
}
const TranslatableFields: React.FC<TranslatableFieldsProps>;import {
TranslatableFields,
TextField,
Show,
SimpleShowLayout
} from 'react-admin';
const PostShow = () => (
<Show>
<SimpleShowLayout>
<TextField source="id" />
<TranslatableFields locales={['en', 'fr', 'es']}>
<TextField source="title" />
<TextField source="content" />
<TextField source="metaDescription" />
</TranslatableFields>
<DateField source="publishedAt" />
</SimpleShowLayout>
</Show>
);import {
useTranslatable,
useTranslatableContext,
TranslatableContext,
TranslatableContextProvider
} from 'react-admin';
interface TranslatableContextValue {
locale: string;
locales: string[];
selectLocale: (locale: string) => void;
getSource: (source: string) => string;
getLabel: (source: string) => string;
}
const useTranslatable: (options?: UseTranslatableOptions) => TranslatableContextValue;
const useTranslatableContext: () => TranslatableContextValue;React Admin follows specific conventions for translation keys:
// Translation message structure
const messages = {
resources: {
posts: {
name: 'Post |||| Posts',
fields: {
title: 'Title',
content: 'Content',
publishedAt: 'Published At',
author: 'Author'
}
},
users: {
name: 'User |||| Users',
fields: {
firstName: 'First Name',
lastName: 'Last Name',
email: 'Email Address'
}
}
},
ra: {
action: {
save: 'Save',
delete: 'Delete',
cancel: 'Cancel',
edit: 'Edit',
show: 'Show',
create: 'Create'
},
boolean: {
true: 'Yes',
false: 'No'
},
page: {
list: 'List of %{name}',
edit: 'Edit %{name} #%{id}',
show: '%{name} #%{id}',
create: 'Create %{name}',
dashboard: 'Dashboard'
},
input: {
file: {
upload_several: 'Drop some files to upload, or click to select one.',
upload_single: 'Drop a file to upload, or click to select it.'
},
image: {
upload_several: 'Drop some pictures to upload, or click to select one.',
upload_single: 'Drop a picture to upload, or click to select it.'
}
},
message: {
yes: 'Yes',
no: 'No',
are_you_sure: 'Are you sure?',
about: 'About',
not_found: 'Either you typed a wrong URL, or you followed a bad link.'
},
navigation: {
no_results: 'No results found',
no_more_results: 'The page number %{page} is out of boundaries. Try the previous page.',
page_out_of_boundaries: 'Page number %{page} out of boundaries',
page_out_from_end: 'Cannot go after last page',
page_out_from_begin: 'Cannot go before page 1',
page_range_info: '%{offsetBegin}-%{offsetEnd} of %{total}',
page_rows_per_page: 'Rows per page:',
next: 'Next',
prev: 'Previous'
},
notification: {
updated: 'Element updated |||| %{smart_count} elements updated',
created: 'Element created',
deleted: 'Element deleted |||| %{smart_count} elements deleted',
bad_item: 'Incorrect element',
item_doesnt_exist: 'Element does not exist',
http_error: 'Server communication error',
data_provider_error: 'dataProvider error. Check the console for details.',
i18n_error: 'Cannot load the translations for the specified language',
canceled: 'Action cancelled',
logged_out: 'Your session has ended, please reconnect.'
},
validation: {
required: 'Required',
minLength: 'Must be %{min} characters at least',
maxLength: 'Must be %{max} characters or less',
minValue: 'Must be at least %{min}',
maxValue: 'Must be %{max} or less',
number: 'Must be a number',
email: 'Must be a valid email',
oneOf: 'Must be one of: %{options}',
regex: 'Must match a specific format (regexp): %{pattern}'
}
}
};// Custom application messages
const customMessages = {
// Custom page titles
page: {
analytics: 'Analytics Dashboard',
reports: 'Reports',
settings: 'Application Settings'
},
// Custom actions
action: {
publish: 'Publish',
unpublish: 'Unpublish',
archive: 'Archive',
duplicate: 'Duplicate',
export_pdf: 'Export to PDF'
},
// Custom notifications
notification: {
post_published: 'Post published successfully',
export_started: 'Export started. You will be notified when complete.',
bulk_update_success: '%{smart_count} item updated |||| %{smart_count} items updated'
},
// Status messages
status: {
draft: 'Draft',
published: 'Published',
archived: 'Archived',
pending: 'Pending Review'
}
};React Admin supports pluralization using the Polyglot.js format:
const messages = {
post: {
count: 'One post |||| %{smart_count} posts',
notification: {
deleted: 'Post deleted |||| %{smart_count} posts deleted'
}
}
};
// Usage
const translate = useTranslate();
const message1 = translate('post.count', { smart_count: 1 }); // "One post"
const message5 = translate('post.count', { smart_count: 5 }); // "5 posts"Variable interpolation in translations:
const messages = {
welcome: 'Welcome back, %{name}!',
last_login: 'Last login: %{date} at %{time}',
notification: 'You have %{count} new %{type}'
};
// Usage
const translate = useTranslate();
const welcome = translate('welcome', { name: 'John Doe' });
const login = translate('last_login', {
date: '2024-01-15',
time: '14:30'
});import { Admin } from 'react-admin';
import { createTheme } from '@mui/material/styles';
// RTL theme configuration
const rtlTheme = createTheme({
direction: 'rtl',
palette: {
primary: { main: '#1976d2' }
}
});
const App = () => {
const [locale, setLocale] = useState('en');
const theme = locale === 'ar' ? rtlTheme : defaultTheme;
return (
<Admin
theme={theme}
i18nProvider={i18nProvider}
dataProvider={dataProvider}
>
<Resource name="posts" list={PostList} />
</Admin>
);
};const i18nProvider = polyglotI18nProvider(
async (locale) => {
if (locale === 'en') {
return import('ra-language-english').then(messages => messages.default);
}
if (locale === 'fr') {
return import('ra-language-french').then(messages => messages.default);
}
// Fallback to English
return import('ra-language-english').then(messages => messages.default);
},
'en'
);import { useTranslate } from 'react-admin';
const T = ({ message, values, children, ...props }) => {
const translate = useTranslate();
const translatedText = translate(message, values);
if (children) {
return children(translatedText);
}
return <span {...props}>{translatedText}</span>;
};
// Usage
<T message="welcome.title" values={{ name: 'John' }} />
<T message="post.count" values={{ smart_count: 5 }}>
{(text) => <strong>{text}</strong>}
</T>import { TestTranslationProvider } from 'react-admin';
const TestComponent = () => (
<TestTranslationProvider messages={{ 'test.message': 'Test Message' }}>
<MyComponent />
</TestTranslationProvider>
);const mockTranslate = (key, options = {}) => {
// Simple mock that returns the key with interpolated values
return key.replace(/%\{(\w+)\}/g, (match, param) => options[param] || match);
};
jest.mock('react-admin', () => ({
...jest.requireActual('react-admin'),
useTranslate: () => mockTranslate
}));import { Admin, Resource, List, Edit, Create, SimpleForm, TextInput } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
// Custom messages
const customEnglishMessages = {
...englishMessages,
resources: {
posts: {
name: 'Post |||| Posts',
fields: {
title: 'Title',
content: 'Content',
status: 'Publication Status'
}
}
},
custom: {
actions: {
publish: 'Publish Post',
schedule: 'Schedule Publication'
},
status: {
draft: 'Draft',
published: 'Published',
scheduled: 'Scheduled'
}
}
};
const customFrenchMessages = {
...frenchMessages,
resources: {
posts: {
name: 'Article |||| Articles',
fields: {
title: 'Titre',
content: 'Contenu',
status: 'Statut de publication'
}
}
},
custom: {
actions: {
publish: 'Publier l\'article',
schedule: 'Programmer la publication'
},
status: {
draft: 'Brouillon',
published: 'Publié',
scheduled: 'Programmé'
}
}
};
const messages = {
en: customEnglishMessages,
fr: customFrenchMessages
};
const i18nProvider = polyglotI18nProvider(
locale => messages[locale],
'en',
[
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'Français' }
]
);
const PostEdit = () => (
<Edit>
<SimpleForm>
<TranslatableInputs locales={['en', 'fr']}>
<TextInput source="title" />
<TextInput source="content" multiline rows={4} />
</TranslatableInputs>
</SimpleForm>
</Edit>
);
const App = () => (
<Admin
dataProvider={dataProvider}
i18nProvider={i18nProvider}
>
<Resource name="posts" list={PostList} edit={PostEdit} />
</Admin>
);React Admin's internationalization system provides comprehensive multi-language support with flexible translation management, locale-specific formatting, and robust tools for building global admin applications.
Install with Tessl CLI
npx tessl i tessl/npm-react-admin