From 36b5922a86c90bd838d49f604a43c16d20fcea9c Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Tue, 6 Sep 2022 12:06:45 +0200 Subject: [PATCH 1/6] Add support for getLocales in i18nProvider --- docs/Translation.md | 28 +++---------- docs/TranslationSetup.md | 56 ++++++++++++-------------- examples/simple/src/Layout.tsx | 54 ++----------------------- examples/simple/src/i18nProvider.tsx | 22 ++++++---- packages/ra-i18n-polyglot/src/index.ts | 14 +++++-- 5 files changed, 61 insertions(+), 113 deletions(-) diff --git a/docs/Translation.md b/docs/Translation.md index a0cae5a0a09..9bef665b39b 100644 --- a/docs/Translation.md +++ b/docs/Translation.md @@ -26,10 +26,11 @@ It should be an object with the following methods: ```jsx // in src/i18nProvider.js export const i18nProvider = { + // required translate: (key, options) => string, changeLocale: locale => Promise, getLocale: () => string, - // Optional. Used by LocalesMenuButton if available + // optional getLocales: () => [{ locale: string; name: string; }], } ``` @@ -102,37 +103,20 @@ import fr from 'ra-language-french'; const translations = { en, fr }; -export const i18nProvider = polyglotI18nProvider(locale => translations[locale], 'en'); - -// in src/MyAppBar.js -import { LocalesMenuButton, AppBar } from 'react-admin'; -import { Typography } from '@mui/material'; - -export const MyAppBar = () => ( - - - - +export const i18nProvider = polyglotI18nProvider( + locale => translations[locale], + 'en', // default locale + [{ locale: 'en', name: 'English' }, { locale: 'fr', name: 'Français' }], ); // in src/App.js import { Admin } from 'react-admin'; - -import { MyAppBar } from './MyAppBar'; import { i18nProvider } from './i18nProvider'; -const MyLayout = (props) => ; - const App = () => ( ... diff --git a/docs/TranslationSetup.md b/docs/TranslationSetup.md index e164eaad88c..8280ed0273b 100644 --- a/docs/TranslationSetup.md +++ b/docs/TranslationSetup.md @@ -54,46 +54,28 @@ import fr from 'ra-language-french'; const translations = { en, fr }; -export const i18nProvider = polyglotI18nProvider(locale => translations[locale], 'en'); -``` - -The second argument to the `polyglotI18nProvider` function is the default locale. - -Next, create a custom App Bar containing the `` button, which lets users change the current locale: - -```jsx -// in src/MyAppBar.js -import { LocalesMenuButton, AppBar } from 'react-admin'; -import { Typography } from '@mui/material'; - -export const MyAppBar = () => ( - - - - +export const i18nProvider = polyglotI18nProvider( + locale => translations[locale], + 'en', // default locale + [ + { locale: 'en', name: 'English' }, + { locale: 'fr', name: 'Français' } + ], ); ``` -Then, pass the custom App Bar to a custom ``, and the `` to your ``: +The second argument to the `polyglotI18nProvider` function is the default locale. The third is the list of supported locales - and is used by the `` component to display a list of languages. + +Next, pass the custom `i18nProvider` to your ``: ```jsx import { Admin } from 'react-admin'; - -import { MyAppBar } from './MyAppBar'; import { i18nProvider } from './i18nProvider'; -const MyLayout = (props) => ; - const App = () => ( ... @@ -117,7 +99,11 @@ const translations = { en, fr }; export const i18nProvider = polyglotI18nProvider( locale => translations[locale] ? translations[locale] : translations.en, - resolveBrowserLocale() + resolveBrowserLocale(), + [ + { locale: 'en', name: 'English' }, + { locale: 'fr', name: 'Français' } + ], ); ``` @@ -126,7 +112,11 @@ export const i18nProvider = polyglotI18nProvider( ```jsx export const i18nProvider = polyglotI18nProvider( locale => translations[locale] ? translations[locale] : translations.en, - resolveBrowserLocale('en', { fullLocale: true }) // 'en' => Default locale when browser locale can't be resolved, { fullLocale: true } => Return full locale + resolveBrowserLocale('en', { fullLocale: true }), // 'en' => Default locale when browser locale can't be resolved, { fullLocale: true } => Return full locale + [ + { locale: 'en', name: 'English' }, + { locale: 'fr', name: 'Français' } + ], ); ``` @@ -138,7 +128,7 @@ By default, the `polyglotI18nProvider` logs a warning in the console each time i But you may want to avoid this for some messages, e.g. error messages from a data source you don't control (like a web server). -The fastest way to do so is to use the third parameter of the `polyglotI18nProvider` function to pass the `allowMissing` option to Polyglot at initialization: +The fastest way to do so is to use the fourth parameter of the `polyglotI18nProvider` function to pass the `allowMissing` option to Polyglot at initialization: ```diff // in src/i18nProvider.js @@ -149,6 +139,10 @@ import fr from './i18n/frenchMessages'; const i18nProvider = polyglotI18nProvider(locale => locale === 'fr' ? fr : en, 'en', // Default locale + [ + { locale: 'en', name: 'English' }, + { locale: 'fr', name: 'Français' } + ], + { allowMissing: true } ); ``` diff --git a/examples/simple/src/Layout.tsx b/examples/simple/src/Layout.tsx index 7764c1821f5..22cacde9ac5 100644 --- a/examples/simple/src/Layout.tsx +++ b/examples/simple/src/Layout.tsx @@ -1,59 +1,13 @@ import * as React from 'react'; -import { forwardRef, memo } from 'react'; -import { ReactQueryDevtools } from 'react-query/devtools'; -import { - AppBar, - Layout, - Logout, - UserMenu, - useLocaleState, - useUserMenu, -} from 'react-admin'; -import { - MenuItem, - MenuItemProps, - ListItemIcon, - CssBaseline, -} from '@mui/material'; -import Language from '@mui/icons-material/Language'; - -const SwitchLanguage = forwardRef( - (props, ref) => { - const [locale, setLocale] = useLocaleState(); - const { onClose } = useUserMenu(); - - return ( - { - setLocale(locale === 'en' ? 'fr' : 'en'); - onClose(); - }} - > - - - - Switch Language - - ); - } -); -const MyUserMenu = () => ( - - - - -); - -const MyAppBar = memo(props => } />); +import { ReactQueryDevtools } from 'react-query/devtools'; +import { Layout } from 'react-admin'; +import { CssBaseline } from '@mui/material'; export default props => ( <> - + import('./i18n/fr').then(messages => messages.default), }; -export default polyglotI18nProvider(locale => { - if (locale === 'fr') { - return messages[locale](); - } +export default polyglotI18nProvider( + locale => { + if (locale === 'fr') { + return messages[locale](); + } - // Always fallback on english - return englishMessages; -}, 'en'); + // Always fallback on english + return englishMessages; + }, + 'en', + [ + { locale: 'en', name: 'English' }, + { locale: 'fr', name: 'Français' }, + ] +); diff --git a/packages/ra-i18n-polyglot/src/index.ts b/packages/ra-i18n-polyglot/src/index.ts index 21455c75f64..daa046b935e 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 { I18nProvider, TranslationMessages } from 'ra-core'; +import { I18nProvider, TranslationMessages, Locale } from 'ra-core'; type GetMessages = ( locale: string @@ -19,11 +19,16 @@ type GetMessages = ( * fr: frenchMessages, * en: englishMessages, * }; - * const i18nProvider = polyglotI18nProvider(locale => messages[locale]) + * const i18nProvider = polyglotI18nProvider( + * locale => messages[locale], + * 'en', + * [{ locale: 'en', name: 'English' }, { locale: 'fr', name: 'Français' }] + * ) */ export default ( getMessages: GetMessages, initialLocale: string = 'en', + availableLocales: Locale[] = [{ locale: 'en', name: 'English' }], polyglotOptions: any = {} ): I18nProvider => { let locale = initialLocale; @@ -36,7 +41,9 @@ export default ( const polyglot = new Polyglot({ locale, phrases: { '': '', ...messages }, - ...polyglotOptions, + ...(Array.isArray(availableLocales) + ? polyglotOptions + : availableLocales), }); let translate = polyglot.t.bind(polyglot); @@ -57,5 +64,6 @@ export default ( } ), getLocale: () => locale, + getLocales: () => availableLocales, }; }; From 5128ca11779ec682e1a53d77a8bac2cff60ec265 Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Thu, 8 Sep 2022 10:00:33 +0200 Subject: [PATCH 2/6] Update docs/TranslationSetup.md Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com> --- docs/TranslationSetup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TranslationSetup.md b/docs/TranslationSetup.md index 8280ed0273b..0c2277cfb55 100644 --- a/docs/TranslationSetup.md +++ b/docs/TranslationSetup.md @@ -64,7 +64,7 @@ export const i18nProvider = polyglotI18nProvider( ); ``` -The second argument to the `polyglotI18nProvider` function is the default locale. The third is the list of supported locales - and is used by the `` component to display a list of languages. +The second argument to the `polyglotI18nProvider` function is the default locale. The third is the list of supported locales - and is used by the [``](./LocalesMenuButton.md) component to display a list of languages. Next, pass the custom `i18nProvider` to your ``: From 31d05080462908a8974bfd0033c74c56341285b8 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 8 Sep 2022 10:02:10 +0200 Subject: [PATCH 3/6] review --- examples/simple/src/i18nProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/simple/src/i18nProvider.tsx b/examples/simple/src/i18nProvider.tsx index 88115935e9b..08414d47f42 100644 --- a/examples/simple/src/i18nProvider.tsx +++ b/examples/simple/src/i18nProvider.tsx @@ -1,6 +1,5 @@ import polyglotI18nProvider from 'ra-i18n-polyglot'; import englishMessages from './i18n/en'; -import frenchMessages from './i18n/fr'; const messages = { fr: () => import('./i18n/fr').then(messages => messages.default), From f10ed7872e4278bc1b5c0a902119f389ae62780b Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 8 Sep 2022 10:12:13 +0200 Subject: [PATCH 4/6] Review --- docs/TranslationSetup.md | 7 ++++++- docs/TranslationTranslating.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/TranslationSetup.md b/docs/TranslationSetup.md index 0c2277cfb55..9751c84de42 100644 --- a/docs/TranslationSetup.md +++ b/docs/TranslationSetup.md @@ -149,5 +149,10 @@ const i18nProvider = polyglotI18nProvider(locale => **Tip**: Check [the Polyglot documentation](https://airbnb.io/polyglot.js/#options-overview) for a list of options you can pass to Polyglot at startup. -This solution is all-or-nothing: you can't silence only *some* missing translation warnings. An alternative solution consists of passing a default translation using the `_` translation option, as explained in the [Using Specific Polyglot Features section](#using-specific-polyglot-features) above. +This solution is all-or-nothing: you can't silence only *some* missing translation warnings. An alternative solution consists of passing a default translation using the `_` translation option, as explained in the [default translation option](./TranslationTranslating.md#interpolation-pluralization-and-default-translation) section. + +```jsx +translate('not_yet_translated', { _: 'Default translation' }); +=> 'Default translation' +``` diff --git a/docs/TranslationTranslating.md b/docs/TranslationTranslating.md index 441fecbe5a4..3fcc7e0d101 100644 --- a/docs/TranslationTranslating.md +++ b/docs/TranslationTranslating.md @@ -216,6 +216,34 @@ const ValidateCommentButton = ({ id }) => { } ``` +## Interpolation, Pluralization and Default Translation + +If you're using [`ra-i18n-polyglot`](./Translation.md#ra-i18n-polyglot) (the default `i18nProvider`), you can leverage the advanced features of its `translate` function. [Polyglot.js](https://airbnb.io/polyglot.js/), the library behind `ra-i18n-polyglot`, provides some nice features such as interpolation and pluralization, that you can use in react-admin. + +```js +const messages = { + 'hello_name': 'Hello, %{name}', + 'count_beer': 'One beer |||| %{smart_count} beers', +}; + +// interpolation +translate('hello_name', { name: 'John Doe' }); +=> 'Hello, John Doe.' + +// pluralization +translate('count_beer', { smart_count: 1 }); +=> 'One beer' + +translate('count_beer', { smart_count: 2 }); +=> '2 beers' + +// default value +translate('not_yet_translated', { _: 'Default translation' }); +=> 'Default translation' +``` + +Check out the [Polyglot.js documentation](https://airbnb.io/polyglot.js/) for more information. + ## Translating Record Content Some of your records may contain data with multiple versions - one for each locale. From 30949d961043dd06d7af14e950a3810a2be7190f Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 8 Sep 2022 10:17:51 +0200 Subject: [PATCH 5/6] Fix types and breaking change --- packages/ra-i18n-polyglot/src/index.ts | 19 ++++++++++++++----- .../react-admin/src/defaultI18nProvider.ts | 5 ++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/ra-i18n-polyglot/src/index.ts b/packages/ra-i18n-polyglot/src/index.ts index daa046b935e..e61961c744f 100644 --- a/packages/ra-i18n-polyglot/src/index.ts +++ b/packages/ra-i18n-polyglot/src/index.ts @@ -28,7 +28,7 @@ type GetMessages = ( export default ( getMessages: GetMessages, initialLocale: string = 'en', - availableLocales: Locale[] = [{ locale: 'en', name: 'English' }], + availableLocales: Locale[] | any = [{ locale: 'en', name: 'English' }], polyglotOptions: any = {} ): I18nProvider => { let locale = initialLocale; @@ -38,12 +38,21 @@ export default ( `The i18nProvider returned a Promise for the messages of the default locale (${initialLocale}). Please update your i18nProvider to return the messages of the default locale in a synchronous way.` ); } + + let availableLocalesFinal, polyglotOptionsFinal; + if (Array.isArray(availableLocales)) { + // third argument is an array of locales + availableLocalesFinal = availableLocales; + polyglotOptionsFinal = polyglotOptions; + } else { + // third argument is the polyglotOptions + availableLocalesFinal = [{ locale: 'en', name: 'English' }]; + polyglotOptionsFinal = availableLocales; + } const polyglot = new Polyglot({ locale, phrases: { '': '', ...messages }, - ...(Array.isArray(availableLocales) - ? polyglotOptions - : availableLocales), + ...polyglotOptionsFinal, }); let translate = polyglot.t.bind(polyglot); @@ -64,6 +73,6 @@ export default ( } ), getLocale: () => locale, - getLocales: () => availableLocales, + getLocales: () => availableLocalesFinal, }; }; diff --git a/packages/react-admin/src/defaultI18nProvider.ts b/packages/react-admin/src/defaultI18nProvider.ts index a3f2a6c26ce..79823c1195c 100644 --- a/packages/react-admin/src/defaultI18nProvider.ts +++ b/packages/react-admin/src/defaultI18nProvider.ts @@ -4,7 +4,6 @@ import polyglotI18nProvider from 'ra-i18n-polyglot'; export const defaultI18nProvider = polyglotI18nProvider( () => defaultMessages, 'en', - { - allowMissing: true, - } + [{ name: 'en', value: 'English' }], + { allowMissing: true } ); From 59b26033bd1d746ffcf10253763ff8c8755af823 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 8 Sep 2022 14:07:08 +0200 Subject: [PATCH 6/6] Fix e2e test --- cypress/integration/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/edit.js b/cypress/integration/edit.js index fec79cf5ee2..e8ee74a906b 100644 --- a/cypress/integration/edit.js +++ b/cypress/integration/edit.js @@ -85,7 +85,7 @@ describe('Edit Page', () => { cy.window().then(win => { cy.on('window:confirm', () => true); }); - cy.get('[role="menuitem"]:first-child').click(); + cy.get('.RaSidebar-fixed [role="menuitem"]:first-child').click(); }); it('should change reference list correctly when changing filter', () => {