From 358587acb098ad74d10eb0406fa526006f72e804 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 16 Sep 2019 22:10:36 +0200 Subject: [PATCH 1/3] Transform i18nProvider type to object --- .../src/i18n/TestTranslationProvider.tsx | 15 ++++++----- .../ra-core/src/i18n/TranslationContext.ts | 6 +++-- .../ra-core/src/i18n/useSetLocale.spec.js | 5 +++- packages/ra-core/src/i18n/useSetLocale.tsx | 3 +-- .../ra-core/src/i18n/useTranslate.spec.tsx | 16 +++++++++--- packages/ra-core/src/i18n/useTranslate.ts | 4 +-- packages/ra-core/src/types.ts | 10 ++++---- packages/ra-i18n-polyglot/src/index.ts | 25 +++++++------------ 8 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/ra-core/src/i18n/TestTranslationProvider.tsx b/packages/ra-core/src/i18n/TestTranslationProvider.tsx index 4aa31f2b935..a975ba05492 100644 --- a/packages/ra-core/src/i18n/TestTranslationProvider.tsx +++ b/packages/ra-core/src/i18n/TestTranslationProvider.tsx @@ -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} diff --git a/packages/ra-core/src/i18n/TranslationContext.ts b/packages/ra-core/src/i18n/TranslationContext.ts index 98bd924d061..ef9175b06fc 100644 --- a/packages/ra-core/src/i18n/TranslationContext.ts +++ b/packages/ra-core/src/i18n/TranslationContext.ts @@ -10,8 +10,10 @@ export interface TranslationContextProps { const TranslationContext = createContext({ 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'; diff --git a/packages/ra-core/src/i18n/useSetLocale.spec.js b/packages/ra-core/src/i18n/useSetLocale.spec.js index c44bd8ccf30..0ed16589b37 100644 --- a/packages/ra-core/src/i18n/useSetLocale.spec.js +++ b/packages/ra-core/src/i18n/useSetLocale.spec.js @@ -32,7 +32,10 @@ describe('useSetLocale', () => { const { getByText } = renderWithRedux( '', + i18nProvider: { + translate: () => '', + changeLocale: () => Promise.resolve(), + }, locale: 'de', setLocale, }} diff --git a/packages/ra-core/src/i18n/useSetLocale.tsx b/packages/ra-core/src/i18n/useSetLocale.tsx index 83f9254b496..228e373f826 100644 --- a/packages/ra-core/src/i18n/useSetLocale.tsx +++ b/packages/ra-core/src/i18n/useSetLocale.tsx @@ -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'; @@ -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(); diff --git a/packages/ra-core/src/i18n/useTranslate.spec.tsx b/packages/ra-core/src/i18n/useTranslate.spec.tsx index e219365b032..36b3a8ba76d 100644 --- a/packages/ra-core/src/i18n/useTranslate.spec.tsx +++ b/packages/ra-core/src/i18n/useTranslate.spec.tsx @@ -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( - type === 'I18N_TRANSLATE' ? 'hallo' : '', + i18nProvider: { + translate: () => 'hallo', + changeLocale: () => Promise.resolve(), + }, setLocale: () => Promise.resolve(), }} > @@ -39,7 +41,13 @@ describe('useTranslate', () => { it('should use the i18n provider when using TranslationProvider', () => { const { queryAllByText } = renderWithRedux( - 'bonjour'}> + 'bonjour', + changeLocale: () => Promise.resolve(), + }} + > ); diff --git a/packages/ra-core/src/i18n/useTranslate.ts b/packages/ra-core/src/i18n/useTranslate.ts index a2fd6cc415a..ffbc64578c1 100644 --- a/packages/ra-core/src/i18n/useTranslate.ts +++ b/packages/ra-core/src/i18n/useTranslate.ts @@ -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 @@ -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 ); diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index 116a01b2607..7362060dd39 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -28,12 +28,12 @@ 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; +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; +}; export type AuthActionType = | 'AUTH_LOGIN' diff --git a/packages/ra-i18n-polyglot/src/index.ts b/packages/ra-i18n-polyglot/src/index.ts index 425e76c235e..253b3a74cd1 100644 --- a/packages/ra-i18n-polyglot/src/index.ts +++ b/packages/ra-i18n-polyglot/src/index.ts @@ -1,6 +1,6 @@ import Polyglot from 'node-polyglot'; -import { I18N_TRANSLATE, I18N_CHANGE_LOCALE, I18nProvider } from 'ra-core'; +import { I18nProvider } from 'ra-core'; type GetMessages = (locale: string) => Object; @@ -37,27 +37,20 @@ export default ( }); let translate = polyglot.t.bind(polyglot); - return (type, params) => { - if (type === I18N_TRANSLATE) { - const { key, options = {} } = params as { - key: string; - options?: Object; - }; - return translate.call(null, key, options); - } - if (type === I18N_CHANGE_LOCALE) { - return new Promise(resolve => { + return { + translate: (key: string, options: any = {}) => translate(key, options), + changeLocale: (locale: string) => + new Promise(resolve => // so we systematically return a Promise for the messages // i18nProvider may return a Promise for language changes, - resolve(getMessages(params as string)); - }).then(messages => { + resolve(getMessages(locale as string)) + ).then(messages => { const newPolyglot = new Polyglot({ - locale: params, + locale, phrases: { '': '', ...messages }, ...polyglotOptions, }); translate = newPolyglot.t.bind(newPolyglot); - }); - } + }), }; }; From 1002f72a6cfae99364a46875336d45e2e42d6d3c Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Mon, 16 Sep 2019 22:39:55 +0200 Subject: [PATCH 2/3] Updates to the documentation --- docs/CustomApp.md | 18 ++--- docs/Translation.md | 86 +++++++++++------------ packages/ra-core/src/types.ts | 2 + packages/ra-core/src/util/TestContext.tsx | 2 - packages/ra-language-english/README.md | 5 +- packages/ra-language-french/README.md | 3 +- packages/ra-tree-ui-materialui/README.md | 3 +- 7 files changed, 61 insertions(+), 58 deletions(-) diff --git a/docs/CustomApp.md b/docs/CustomApp.md index 203d6aa4b4f..08848ed3c53 100644 --- a/docs/CustomApp.md +++ b/docs/CustomApp.md @@ -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'; @@ -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 = () => ( @@ -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'; @@ -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 = () => ( diff --git a/docs/Translation.md b/docs/Translation.md index 00afd9d592c..7d0152a9593 100644 --- a/docs/Translation.md +++ b/docs/Translation.md @@ -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 `` component: + +```jsx +import i18nProvider from './i18n/i18nProvider'; + +const App = () => ( + + + // ... ``` -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 ( + ; + ); +}; +``` + +## 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 = { @@ -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: @@ -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 ( - ; - ); -}; -``` - -And just like for the `dataProvider` and the `authProvider`, you can *inject* the `i18nProvider` to your react-admin app using the `` component: - -```jsx -import i18nProvider from './i18n/i18nProvider'; - -const App = () => ( - - - // ... -``` - ## 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: diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index 7362060dd39..84ff818bb2d 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -33,6 +33,7 @@ export type Translate = (key: string, options?: any) => string; export type I18nProvider = { translate: Translate; changeLocale: (locale: string, options?: any) => Promise; + [key: string]: any; }; export type AuthActionType = @@ -48,6 +49,7 @@ export type AuthProvider = { checkAuth: (params: any) => Promise; checkError: (error: any) => Promise; getPermissions: (params: any) => Promise; + [key: string]: any; }; export type LegacyAuthProvider = ( diff --git a/packages/ra-core/src/util/TestContext.tsx b/packages/ra-core/src/util/TestContext.tsx index 4be15ee92d2..6eb5783db3e 100644 --- a/packages/ra-core/src/util/TestContext.tsx +++ b/packages/ra-core/src/util/TestContext.tsx @@ -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: { @@ -17,7 +16,6 @@ export const defaultStore = { interface Props { initialState?: object; - i18nProvider?: I18nProvider; enableReducers?: boolean; } diff --git a/packages/ra-language-english/README.md b/packages/ra-language-english/README.md index 1a61e9711e3..c0165d6f3d8 100644 --- a/packages/ra-language-english/README.md +++ b/packages/ra-language-english/README.md @@ -14,11 +14,12 @@ npm install --save ra-language-english ```js import englishMessages from 'ra-language-english'; +import polyglotI18nProvider from 'ra-i18n-polyglot'; const messages = { 'en': englishMessages, }; -const i18nProvider = locale => messages[locale]; +const i18nProvider = polyglotI18nProvider(locale => messages[locale]); ... @@ -27,4 +28,4 @@ const i18nProvider = locale => messages[locale]; ## License -This translation is licensed under the MIT License, and sponsored by [marmelab](http://marmelab.com). \ No newline at end of file +This translation is licensed under the MIT License, and sponsored by [marmelab](http://marmelab.com). diff --git a/packages/ra-language-french/README.md b/packages/ra-language-french/README.md index dee1daf75da..4889429e7fd 100644 --- a/packages/ra-language-french/README.md +++ b/packages/ra-language-french/README.md @@ -14,11 +14,12 @@ npm install --save ra-language-french ```js import frenchMessages from 'ra-language-french'; +import polyglotI18nProvider from 'ra-i18n-polyglot'; const messages = { 'fr': frenchMessages, }; -const i18nProvider = locale => messages[locale]; +const i18nProvider = polyglotI18nProvider(locale => messages[locale]); ... diff --git a/packages/ra-tree-ui-materialui/README.md b/packages/ra-tree-ui-materialui/README.md index 1ad527de4ee..3ca00c2776e 100644 --- a/packages/ra-tree-ui-materialui/README.md +++ b/packages/ra-tree-ui-materialui/README.md @@ -41,6 +41,7 @@ import { Admin, Resource, mergeTranslations } from 'react-admin'; import { reducer as tree } from 'ra-tree-ui-materialui'; import englishMessages from 'ra-language-english'; import treeEnglishMessages from 'ra-tree-language-english'; +import polyglotI18nProvider from 'ra-i18n-polyglot'; import dataProvider from './dataProvider'; import posts from './posts'; @@ -49,7 +50,7 @@ import tags from './tags'; const messages = { 'en': mergeTranslations(englishMessages, treeEnglishMessages), }; -const i18nProvider = locale => messages[locale]; +const i18nProvider = polyglotI18nProvider(locale => messages[locale]); const App = () => ( Date: Mon, 16 Sep 2019 22:43:01 +0200 Subject: [PATCH 3/3] Update upgrade guide --- UPGRADE.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index 937f5b1c6a7..a64b4932f28 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -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 })