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] Replace I18nProvider function by object #3699

Merged
merged 3 commits into from
Sep 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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