Skip to content

Commit

Permalink
Merge pull request #3699 from marmelab/i18nProvider-object
Browse files Browse the repository at this point in the history
[RFR] Replace I18nProvider function by object
  • Loading branch information
djhi authored Sep 17, 2019
2 parents 4717447 + c31adcc commit 9ed77c8
Show file tree
Hide file tree
Showing 15 changed files with 113 additions and 106 deletions.
16 changes: 6 additions & 10 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1057,22 +1057,18 @@ If you had custom reducer or sagas based on these actions, they will no longer w

## 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.
The i18nProvider, that react-admin uses for translating UI and content, must now be an object exposing two methods: `trasnlate` and `changeLocale`.

```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;
const polyglot = new Polyglot({ locale: 'en', phrases: messages.en });
let translate = polyglot.t.bind(polyglot);
const i18nProvider = {
translate: (key, options) => translate(key, options),
changeLocale: newLocale => {
return new Promise((resolve, reject) => {
// load new messages and update the translate function
})
Expand Down
18 changes: 10 additions & 8 deletions docs/CustomApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { createHashHistory } from 'history';
import { Admin, Resource } from 'react-admin';
import restProvider from 'ra-data-simple-rest';
import defaultMessages from 'ra-language-english';
import polyglotI18nProvider from 'ra_i18n_polyglot';

import createAdminStore from './createAdminStore';
import messages from './i18n';
Expand All @@ -95,15 +96,15 @@ import { PostList, PostCreate, PostEdit, PostShow } from './Post';
import { CommentList, CommentEdit, CommentCreate } from './Comment';
import { UserList, UserEdit, UserCreate } from './User';

// side effects
const authProvider = () => Promise.resolve();
// dependency injection
const dataProvider = restProvider('http://path.to.my.api/');
const i18nProvider = locale => {
const authProvider = () => Promise.resolve();
const i18nProvider = polyglotI18nProvider(locale => {
if (locale !== 'en') {
return messages[locale];
}
return defaultMessages;
};
});
const history = createHashHistory();

const App = () => (
Expand Down Expand Up @@ -150,6 +151,7 @@ import { createHashHistory } from 'history';
+import { AuthContext, DataProviderContext, TranslationProvider, Resource } from 'react-admin';
import restProvider from 'ra-data-simple-rest';
import defaultMessages from 'ra-language-english';
import polyglotI18nProvider from 'ra_i18n_polyglot';
+import { ThemeProvider } from '@material-ui/styles';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
Expand All @@ -164,15 +166,15 @@ import { PostList, PostCreate, PostEdit, PostShow } from './Post';
import { CommentList, CommentEdit, CommentCreate } from './Comment';
import { UserList, UserEdit, UserCreate } from './User';

// side effects
const authProvider = () => Promise.resolve();
// dependency injection
const dataProvider = restProvider('http://path.to.my.api/');
const i18nProvider = locale => {
const authProvider = () => Promise.resolve();
const i18nProvider = polyglotI18nProvider(locale => {
if (locale !== 'en') {
return messages[locale];
}
return defaultMessages;
};
});
const history = createHashHistory();

const App = () => (
Expand Down
86 changes: 42 additions & 44 deletions docs/Translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,50 @@ You will use translation features mostly via the `i18nProvider`, and a set of ho

## Introducing the `i18nProvider`

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:
Just like for data fetching and authentication, react-admin relies on a simple object for translations. It's called the `i18nProvider`, and it manages translation and language change using two methods:

```jsx
const i18nProvider = (type, params) => string | Promise;
const i18nProvider = {
translate: (key, options) => string,
changeLocale: locale => Promise,
}
```

And just like for the `dataProvider` and the `authProvider`, you can *inject* the `i18nProvider` to your react-admin app using the `<Admin>` component:

```jsx
import i18nProvider from './i18n/i18nProvider';

const App = () => (
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
i18nProvider={i18nProvider}
>
<Resource name="posts" list={...}>
// ...
```
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:
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(); // returns the i18nProvider.translate() method
return (
<Button onclick={doSave}>
{translate('ra.action.save')} // will translate to "Save" in English and "Enregistrer" in French
</Button>;
);
};
```
## Using Polyglot.js
Here is the simplest possible implementation for an `i18nProvider` with English and French messages:
```jsx
import { I18N_TRANSLATE, I18N_CHANGE_LOCALE } from 'react-admin';
import lodashGet from 'lodash/get';

const englishMessages = {
Expand All @@ -50,23 +84,17 @@ const frenchMessages = {
},
},
};
let messages = englishMessages;

const i18nProvider = (type, params) => {
let messages = englishMessages;
if (type === I18N_TRANSLATE) {
const { key } = params;
return lodashGet(messages, key)
}
if (type === I18N_CHANGE_LOCALE) {
const newLocale = params;
const i18nProvider = {
translate: key => lodashGet(messages, key),
changeLocale: newLocale => {
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:
Expand Down Expand Up @@ -102,36 +130,6 @@ const i18nProvider = polyglotI18nProvider(locale =>
);
```
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(); // 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
</Button>;
);
};
```

And just like for the `dataProvider` and the `authProvider`, you can *inject* the `i18nProvider` to your react-admin app using the `<Admin>` component:

```jsx
import i18nProvider from './i18n/i18nProvider';

const App = () => (
<Admin
dataProvider={dataProvider}
authProvider={authProvider}
i18nProvider={i18nProvider}
>
<Resource name="posts" list={...}>
// ...
```
## Changing The Default Locale
The default react-admin locale is `en`, for English. If you want to display the interface in another language by default, you'll have to install a third-party package. For instance, to change the interface to French, you must install the `ra-language-french` npm package, then use it in a custom `i18nProvider`, as follows:
Expand Down
15 changes: 9 additions & 6 deletions packages/ra-core/src/i18n/TestTranslationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ export default ({ translate, messages, children }: any) => (
value={{
locale: 'en',
setLocale: () => Promise.resolve(),
i18nProvider: (_, options: { key: string; options?: any }) =>
messages
? lodashGet(messages, options.key)
? lodashGet(messages, options.key)
: options.options._
: translate.call(null, options.key, options.options),
i18nProvider: {
translate: messages
? (key: string, options?: any) =>
lodashGet(messages, key)
? lodashGet(messages, key)
: options._
: translate,
changeLocale: () => Promise.resolve(),
},
}}
>
{children}
Expand Down
6 changes: 4 additions & 2 deletions packages/ra-core/src/i18n/TranslationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ export interface TranslationContextProps {
const TranslationContext = createContext<TranslationContextProps>({
locale: 'en',
setLocale: () => Promise.resolve(),
i18nProvider: (type, params: { key: string; options?: Object }) =>
params && params.key ? params.key : '',
i18nProvider: {
translate: x => x,
changeLocale: () => Promise.resolve(),
},
});

TranslationContext.displayName = 'TranslationContext';
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-core/src/i18n/useSetLocale.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ describe('useSetLocale', () => {
const { getByText } = renderWithRedux(
<TranslationContext.Provider
value={{
i18nProvider: () => '',
i18nProvider: {
translate: () => '',
changeLocale: () => Promise.resolve(),
},
locale: 'de',
setLocale,
}}
Expand Down
3 changes: 1 addition & 2 deletions packages/ra-core/src/i18n/useSetLocale.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useContext, useCallback } from 'react';

import { TranslationContext } from './TranslationContext';
import { I18N_CHANGE_LOCALE } from '../types';
import { useUpdateLoading } from '../loading';
import { useNotify } from '../sideEffect';

Expand Down Expand Up @@ -43,7 +42,7 @@ const useSetLocale = (): SetLocale => {
startLoading();
// so we systematically return a Promise for the messages
// i18nProvider may return a Promise for language changes,
resolve(i18nProvider(I18N_CHANGE_LOCALE, newLocale));
resolve(i18nProvider.changeLocale(newLocale));
})
.then(() => {
stopLoading();
Expand Down
16 changes: 12 additions & 4 deletions packages/ra-core/src/i18n/useTranslate.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ describe('useTranslate', () => {
expect(queryAllByText('hello')).toHaveLength(1);
});

it('should use the i18nProvider I18N_TRANSLATE verb', () => {
it('should use the i18nProvider.translate() method', () => {
const { queryAllByText } = renderWithRedux(
<TranslationContext.Provider
value={{
locale: 'de',
i18nProvider: type =>
type === 'I18N_TRANSLATE' ? 'hallo' : '',
i18nProvider: {
translate: () => 'hallo',
changeLocale: () => Promise.resolve(),
},
setLocale: () => Promise.resolve(),
}}
>
Expand All @@ -39,7 +41,13 @@ describe('useTranslate', () => {

it('should use the i18n provider when using TranslationProvider', () => {
const { queryAllByText } = renderWithRedux(
<TranslationProvider locale="fr" i18nProvider={() => 'bonjour'}>
<TranslationProvider
locale="fr"
i18nProvider={{
translate: () => 'bonjour',
changeLocale: () => Promise.resolve(),
}}
>
<Component />
</TranslationProvider>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-core/src/i18n/useTranslate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useContext, useCallback } from 'react';

import { TranslationContext } from './TranslationContext';
import { I18N_TRANSLATE, Translate } from '../types';
import { Translate } from '../types';

/**
* Translate a string using the current locale and the translations from the i18nProvider
Expand All @@ -26,7 +26,7 @@ const useTranslate = (): Translate => {
const { i18nProvider, locale } = useContext(TranslationContext);
const translate = useCallback(
(key: string, options?: any) =>
i18nProvider(I18N_TRANSLATE, { key, options }) as string,
i18nProvider.translate(key, options) as string,
// update the hook each time the locale changes
[i18nProvider, locale] // eslint-disable-line react-hooks/exhaustive-deps
);
Expand Down
12 changes: 7 additions & 5 deletions packages/ra-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ export interface Pagination {
export const I18N_TRANSLATE = 'I18N_TRANSLATE';
export const I18N_CHANGE_LOCALE = 'I18N_CHANGE_LOCALE';

export type I18nProvider = (
type: typeof I18N_TRANSLATE | typeof I18N_CHANGE_LOCALE,
params: { key: string; options?: Object } | string
) => string | Promise<any>;
export type Translate = (key: string, options?: any) => string;

export type Translate = (id: string, options?: any) => string;
export type I18nProvider = {
translate: Translate;
changeLocale: (locale: string, options?: any) => Promise<void>;
[key: string]: any;
};

export type AuthActionType =
| 'AUTH_LOGIN'
Expand All @@ -48,6 +49,7 @@ export type AuthProvider = {
checkAuth: (params: any) => Promise<void>;
checkError: (error: any) => Promise<void>;
getPermissions: (params: any) => Promise<any>;
[key: string]: any;
};

export type LegacyAuthProvider = (
Expand Down
2 changes: 0 additions & 2 deletions packages/ra-core/src/util/TestContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import merge from 'lodash/merge';
import { createMemoryHistory } from 'history';

import createAdminStore from '../createAdminStore';
import { I18nProvider } from '../types';

export const defaultStore = {
admin: {
Expand All @@ -17,7 +16,6 @@ export const defaultStore = {

interface Props {
initialState?: object;
i18nProvider?: I18nProvider;
enableReducers?: boolean;
}

Expand Down
Loading

0 comments on commit 9ed77c8

Please sign in to comment.