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
Merged
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