Skip to content

Commit

Permalink
Merge pull request #3685 from marmelab/i18nProvider_new_signature
Browse files Browse the repository at this point in the history
[RFR] I18n provider new signature
  • Loading branch information
djhi authored Sep 13, 2019
2 parents 43af3a5 + c42af75 commit 2f347d8
Show file tree
Hide file tree
Showing 46 changed files with 649 additions and 310 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ build-ra-data-graphql-simple:
@echo "Transpiling ra-data-graphql-simple files...";
@cd ./packages/ra-data-graphql-simple && yarn -s build

build-ra-i18n-polyglot:
@echo "Transpiling ra-i18n-polyglot files...";
@cd ./packages/ra-i18n-polyglot && yarn -s build

build-ra-input-rich-text:
@echo "Transpiling ra-input-rich-text files...";
@cd ./packages/ra-input-rich-text && yarn -s build
Expand All @@ -82,7 +86,7 @@ build-data-generator:
@echo "Transpiling data-generator files...";
@cd ./examples/data-generator && yarn -s build

build: build-ra-core build-ra-ui-materialui build-react-admin build-ra-data-fakerest build-ra-data-json-server build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphcool build-ra-data-graphql-simple build-ra-input-rich-text build-ra-realtime build-ra-tree-core build-ra-tree-ui-materialui build-data-generator ## compile ES6 files to JS
build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphcool build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-ra-realtime build-ra-tree-core build-ra-tree-ui-materialui build-data-generator build-react-admin ## compile ES6 files to JS

doc: ## compile doc as html and launch doc web server
@yarn -s doc
Expand Down
52 changes: 50 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,11 @@ import { reducer as formReducer } from 'redux-form';
export default ({
authProvider,
dataProvider,
i18nProvider = defaultI18nProvider,
history,
locale = 'en',
}) => {
const reducer = combineReducers({
admin: adminReducer,
i18n: i18nReducer(locale, i18nProvider(locale)),
form: formReducer,
- router: routerReducer,
+ router: connectRouter(history),
Expand Down Expand Up @@ -1034,6 +1032,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 contains a module called `ra-i18n-polyglot`, that is a wrapper 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 polyglotI18nProvider from 'ra-i18n-polyglot';
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}>
...
</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
133 changes: 95 additions & 38 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,96 @@ 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();
}
};
```

## Using Polyglot.js

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 'ra-i18n-polyglot';

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 @@ -92,9 +139,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 polyglotI18nProvider from 'ra-i18n-polyglot';
import frenchMessages from 'ra-language-french';

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

const App = () => (
<Admin locale="fr" i18nProvider={i18nProvider}>
Expand Down Expand Up @@ -157,14 +205,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 polyglotI18nProvider from 'ra-i18n-polyglot';
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 +281,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 'ra-i18n-polyglot';
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 +310,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,
resolveBrowserLocale,
} from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
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 +338,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 +405,8 @@ 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 } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';
// interface translations
import englishMessages from 'ra-language-english';
import frenchMessages from 'ra-language-french';
Expand All @@ -361,7 +418,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
1 change: 1 addition & 0 deletions examples/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"ra-data-fakerest": "^3.0.0-alpha.0",
"ra-data-graphql-simple": "^3.0.0-alpha.0",
"ra-data-simple-rest": "^3.0.0-alpha.0",
"ra-i18n-polyglot": "^3.0.0-alpha.4",
"ra-input-rich-text": "^3.0.0-alpha.0",
"ra-language-english": "^3.0.0-alpha.0",
"ra-language-french": "^3.0.0-alpha.0",
Expand Down
5 changes: 3 additions & 2 deletions examples/demo/src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Admin, Resource } from 'react-admin';
import polyglotI18nProvider from 'ra-i18n-polyglot';

import './App.css';

Expand All @@ -20,14 +21,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
25 changes: 15 additions & 10 deletions examples/demo/src/layout/AppBar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { forwardRef } from 'react';
import { AppBar, UserMenu, MenuItemLink, useTranslate } from 'react-admin';
import Typography from '@material-ui/core/Typography';
import SettingsIcon from '@material-ui/icons/Settings';
Expand All @@ -18,18 +18,23 @@ const useStyles = makeStyles({
},
});

const CustomUserMenu = props => {
const ConfigurationMenu = forwardRef((_, ref) => {
const translate = useTranslate();
return (
<UserMenu {...props}>
<MenuItemLink
to="/configuration"
primaryText={translate('pos.configuration')}
leftIcon={<SettingsIcon />}
/>
</UserMenu>
<MenuItemLink
ref={ref}
to="/configuration"
primaryText={translate('pos.configuration')}
leftIcon={<SettingsIcon />}
/>
);
};
});

const CustomUserMenu = props => (
<UserMenu {...props}>
<ConfigurationMenu />
</UserMenu>
);

const CustomAppBar = ({ props }) => {
const classes = useStyles();
Expand Down
Loading

0 comments on commit 2f347d8

Please sign in to comment.