Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] I18n provider new signature #3685

Merged
merged 13 commits into from
Sep 13, 2019
50 changes: 50 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,56 @@ If you had custom reducer or sagas based on these actions, they will no longer w

**Tip**: If you need to clear the Redux state, you can dispatch the `CLEAR_STATE` action.

## i18nProvider Signature Changed

The i18nProvider, that react-admin uses for translating UI and content, now has a signature similar to the other providers: it accepts a message type (either `I18N_TRANSLATE` or `I18N_CHANGE_LOCALE`) and a params argument.

```jsx
// react-admin 2.x
const i18nProvider = (locale) => messages[locale];

// react-admin 3.x
const i18nProvider = (type, params) => {
const polyglot = new Polyglot({ locale: 'en', phrases: messages.en });
let translate = polyglot.t.bind(polyglot);
if (type === 'I18N_TRANSLATE') {
const { key, options } = params;
return translate(key, options);
}
if type === 'I18N_CHANGE_LOCALE') {
const newLocale = params;
return new Promise((resolve, reject) => {
// load new messages and update the translate function
})
}
}
```

But don't worry: react-admin v3 exports a function called `polyglotI18nProvider`, that you can just wrap around your old `i18nProvider` to make it compatible with the new provider signature:

```diff
import React from 'react';
-import { Admin, Resource } from 'react-admin';
+import { Admin, Resource, polyglotI18nProvider } from 'react-admin';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

const messages = {
fr: frenchMessages,
en: englishMessages,
};
const i18nProvider = locale => messages[locale];

const App = () => (
- <Admin locale="en" i18nProvider={i18nProvider}>
+ <Admin locale="en" i18nProvider={polyglotI18nProvider(i18nProvider)}>
djhi marked this conversation as resolved.
Show resolved Hide resolved
...
</Admin>
);

export default App;
```

## The translation layer no longer uses Redux

React-admin translation (i18n) layer lets developers provide translations for UI and content, based on Airbnb's [Polyglot](https://airbnb.io/polyglot.js/) library. The previous implementation used Redux and redux-saga. In react-admin 3.0, the translation utilities are implemented using a React context and a set of hooks.
Expand Down
132 changes: 92 additions & 40 deletions docs/Translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: "Translation"

The react-admin user interface uses English as the default language. But you can also display the UI and content in other languages, allow changing language at runtime, even lazy-loading optional languages to avoid increasing the bundle size with all translations.

The react-admin translation layer is based on [polyglot.js](http://airbnb.io/polyglot.js/), which uses JSON files for translations. You will use translation features mostly via the `i18nProvider`, and a set of hooks (`useTranslate`, `useLocale`, `useSetLocale`).
You will use translation features mostly via the `i18nProvider`, and a set of hooks (`useTranslate`, `useLocale`, `useSetLocale`).

**Tip**: We'll use a bit of custom vocabulary in this chapter:

Expand All @@ -19,49 +19,94 @@ The react-admin translation layer is based on [polyglot.js](http://airbnb.io/pol
Just like for data fetching and authentication, react-admin relies on a simple function for translations. It's called the `i18nProvider`, and here is its signature:

```jsx
const i18nProvider = locale => messages;
const i18nProvider = (type, params) => string | Promise;
```

Given a locale, The `i18nProvider` function should return a dictionary of terms. For instance:
The `i18nProvider` expects two possible `type` arguments: `I18N_TRANSLATE` and `I18N_CHANGE_LOCALE`. Here is the simplest possible implementation for a French and English provider:

```jsx
const i18nProvider = locale => {
if (locale === 'en') {
return {
ra: {
notification: {
http_error: 'Network error. Please retry',
},
action: {
save: 'Save',
delete: 'Delete',
},
},
};
import { I18N_TRANSLATE, I18N_CHANGE_LOCALE } from 'react-admin';
import lodashGet from 'lodash/get';

const englishMessages = {
ra: {
notification: {
http_error: 'Network error. Please retry',
},
action: {
save: 'Save',
delete: 'Delete',
},
},
};
const frenchMessages = {
ra: {
notification: {
http_error: 'Erreur réseau, veuillez réessayer',
},
action: {
save: 'Enregistrer',
delete: 'Supprimer',
},
},
};

const i18nProvider = (type, params) => {
let messages = englishMessages;
if (type === I18N_TRANSLATE) {
const { key } = params;
return lodashGet(messages, key)
}
if (locale === 'fr') {
return {
ra: {
notification: {
http_error: 'Erreur réseau, veuillez réessayer',
},
action: {
save: 'Enregistrer',
delete: 'Supprimer',
},
},
};
if (type === I18N_CHANGE_LOCALE) {
const newLocale = params;
messages = (newLocale === 'fr') ? frenchMessages : englishMessages;
return Promise.resolve();
}
};
```

But this is too naive: react-admin expects that i18nProviders support string interpolation for translation, and asynchronous message loading for locale change. That's why react-admin bundles an `i18nProvider` *factory* called `polyglotI18nProvider`. This factory relies on [polyglot.js](http://airbnb.io/polyglot.js/), which uses JSON files for translations. It only expects one argument: a function returning a list of messages based on a locale passed as argument.

So the previous provider can be written as:

```jsx
import { polyglotI18nProvider } from 'react-admin';

const englishMessages = {
ra: {
notification: {
http_error: 'Network error. Please retry',
},
action: {
save: 'Save',
delete: 'Delete',
},
},
};
const frenchMessages = {
ra: {
notification: {
http_error: 'Erreur réseau, veuillez réessayer',
},
action: {
save: 'Enregistrer',
delete: 'Supprimer',
},
},
};

const i18nProvider = polyglotI18nProvider(locale =>
locale === 'fr' ? frenchMessages : englishMessages
);
```

If you want to add or update tranlations, you'll have to provide your own `i18nProvider`.

React-admin components use translation keys for their labels, and rely on the `i18nProvider` to translate them. For instance:

```jsx
const SaveButton = ({ doSave }) => {
const translate = useTranslate();
const translate = useTranslate(); // calls the i18nProvider with the I18N_TRANSLATE type
return (
<Button onclick={doSave}>
{translate('ra.action.save')} // will translate to "Save" in English and "Enregistrer" in French
Expand Down Expand Up @@ -91,10 +136,10 @@ The default react-admin locale is `en`, for English. If you want to display the

```jsx
import React from 'react';
import { Admin, Resource } from 'react-admin';
import { Admin, Resource, polyglotI18nProvider } from 'react-admin';
import frenchMessages from 'ra-language-french';

const i18nProvider = () => frenchMessages;
const i18nProvider = polyglotI18nProvider(() => frenchMessages);

const App = () => (
<Admin locale="fr" i18nProvider={i18nProvider}>
Expand Down Expand Up @@ -156,15 +201,15 @@ If you want to offer the ability to change locale at runtime, you must provide a

```jsx
import React from 'react';
import { Admin, Resource } from 'react-admin';
import { Admin, Resource, polyglotI18nProvider } from 'react-admin';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

const messages = {
fr: frenchMessages,
en: englishMessages,
};
const i18nProvider = locale => messages[locale];
const i18nProvider = polyglotI18nProvider(locale => messages[locale]);

const App = () => (
<Admin locale="en" i18nProvider={i18nProvider}>
Expand Down Expand Up @@ -232,20 +277,21 @@ export default LocaleSwitcher;

## Lazy-Loading Locales

Bundling all the possible locales in the `i18nProvider` is a great recipe to increase your bundle size, and slow down the initial application load. Fortunately, the `i18nProvider` may return a *promise* for locale change calls (except the initial call, when the app starts) to load secondary locales on demand. For example:
Bundling all the possible locales in the `i18nProvider` is a great recipe to increase your bundle size, and slow down the initial application load. Fortunately, the `i18nProvider` returns a *promise* for locale change calls to load secondary locales on demand. And the `polyglotI18nProvider` accepts when its argument function returns a Promise, too. For example:

```js
import { polyglotI18nProvider } from 'react-admin';
import englishMessages from '../en.js';

const i18nProvider = locale => {
const i18nProvider = polyglotI18nProvider(locale => {
if (locale === 'en') {
// initial call, must return synchronously
return englishMessages;
}
if (locale === 'fr') {
return import('../i18n/fr.js').then(messages => messages.default);
}
}
});

const App = () => (
<Admin locale="en" i18nProvider={i18nProvider}>
Expand All @@ -260,15 +306,20 @@ React-admin provides a helper function named `resolveBrowserLocale()`, which det

```jsx
import React from 'react';
import { Admin, Resource, resolveBrowserLocale } from 'react-admin';
import {
Admin,
Resource,
polyglotI18nProvider,
resolveBrowserLocale,
} from 'react-admin';
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';

const messages = {
fr: frenchMessages,
en: englishMessages,
};
const i18nProvider = locale => messages[locale] ? messages[locale] : messages.en;
const i18nProvider = polyglotI18nProvider(locale => messages[locale] ? messages[locale] : messages.en);

const App = () => (
<Admin locale={resolveBrowserLocale()} i18nProvider={i18nProvider}>
Expand All @@ -283,7 +334,7 @@ Beware that users from all around the world may use your application, so make su

## Translation Messages

The `message` returned by the `i18nProvider` value should be a dictionary where the keys identify interface components, and values are the translated string. This dictionary is a simple JavaScript object looking like the following:
The `message` returned by the `polyglotI18nProvider` function argument should be a dictionary where the keys identify interface components, and values are the translated string. This dictionary is a simple JavaScript object looking like the following:

```jsx
{
Expand Down Expand Up @@ -350,6 +401,7 @@ Using `resources` keys is an alternative to using the `label` prop in Field and
When translating an admin, interface messages (e.g. "List", "Page", etc.) usually come from a third-party package, while your domain messages (e.g. "Shoe", "Date of birth", etc.) come from your own code. That means you need to combine these messages before passing them to `<Admin>`. The recipe for combining messages is to use ES6 destructuring:

```jsx
import { Admin, polyglotI18nProvider } from 'react-admin';
// interface translations
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
Expand All @@ -361,7 +413,7 @@ const messages = {
fr: { ...frenchMessages, ...domainMessages.fr },
en: { ...englishMessages, ...domainMessages.en },
};
const i18nProvider = locale => messages[locale];
const i18nProvider = polyglotI18nProvider(locale => messages[locale]);

const App = () => (
<Admin i18nProvider={i18nProvider}>
Expand Down
6 changes: 3 additions & 3 deletions examples/demo/src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Admin, Resource } from 'react-admin';
import { Admin, Resource, polyglotI18nProvider } from 'react-admin';

import './App.css';

Expand All @@ -20,14 +20,14 @@ import reviews from './reviews';
import dataProviderFactory from './dataProvider';
import fakeServerFactory from './fakeServer';

const i18nProvider = locale => {
const i18nProvider = polyglotI18nProvider(locale => {
if (locale === 'fr') {
return import('./i18n/fr').then(messages => messages.default);
}

// Always fallback on english
return englishMessages;
};
});

class App extends Component {
state = { dataProvider: null };
Expand Down
41 changes: 41 additions & 0 deletions examples/simple/src/Layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { forwardRef } from 'react';
import { Layout, AppBar, UserMenu, useLocale, useSetLocale } from 'react-admin';
import { makeStyles, MenuItem, ListItemIcon } from '@material-ui/core';
import { Language } from '@material-ui/icons';

const useStyles = makeStyles(theme => ({
menuItem: {
color: theme.palette.text.secondary,
},
}));

const SwitchLanguage = forwardRef((props, ref) => {
const locale = useLocale();
const setLocale = useSetLocale();
const classes = useStyles();
return (
<MenuItem
ref={ref}
className={classes.menuItem}
onClick={() => {
setLocale(locale === 'en' ? 'fr' : 'en');
props.onClick();
}}
>
<ListItemIcon>
<Language />
</ListItemIcon>
Switch Language
</MenuItem>
);
});

const MyUserMenu = props => (
<UserMenu {...props}>
<SwitchLanguage />
</UserMenu>
);

const MyAppBar = props => <AppBar {...props} userMenu={<MyUserMenu />} />;

export default props => <Layout {...props} appBar={MyAppBar} />;
5 changes: 3 additions & 2 deletions examples/simple/src/i18nProvider.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { polyglotI18nProvider } from 'react-admin';
import englishMessages from './i18n/en';

const messages = {
fr: () => import('./i18n/fr.js').then(messages => messages.default),
};

export default locale => {
export default polyglotI18nProvider(locale => {
if (locale === 'fr') {
return messages[locale]();
}

// Always fallback on english
return englishMessages;
};
});
2 changes: 2 additions & 0 deletions examples/simple/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CustomRouteLayout from './customRouteLayout';
import CustomRouteNoLayout from './customRouteNoLayout';
import dataProvider from './dataProvider';
import i18nProvider from './i18nProvider';
import Layout from './Layout';
import posts from './posts';
import users from './users';
import tags from './tags';
Expand All @@ -22,6 +23,7 @@ render(
i18nProvider={i18nProvider}
title="Example Admin"
locale="en"
layout={Layout}
customReducers={{ tree }}
customRoutes={[
<Route
Expand Down
Loading