Skip to content

Commit

Permalink
Merge pull request #8143 from marmelab/getlocales-polyglot
Browse files Browse the repository at this point in the history
Add support for getLocales in Polyglot i18nProvider
  • Loading branch information
slax57 authored Sep 8, 2022
2 parents ee731bc + 59b2603 commit 31317e7
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 118 deletions.
2 changes: 1 addition & 1 deletion cypress/integration/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
28 changes: 6 additions & 22 deletions docs/Translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>,
getLocale: () => string,
// Optional. Used by LocalesMenuButton if available
// optional
getLocales: () => [{ locale: string; name: string; }],
}
```
Expand Down Expand Up @@ -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 = () => (
<AppBar>
<Typography flex="1" variant="h6" id="react-admin-title"/>
<LocalesMenuButton
languages={[
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'Français' },
]}
/>
</AppBar>
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) => <Layout {...props} appBar={MyAppBar} />;

const App = () => (
<Admin
i18nProvider={i18nProvider}
dataProvider={dataProvider}
layout={MyLayout}
>
...
</Admin>
Expand Down
63 changes: 31 additions & 32 deletions docs/TranslationSetup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<LocalesMenuButton>` 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 = () => (
<AppBar>
<Typography flex="1" variant="h6" id="react-admin-title"/>
<LocalesMenuButton
languages={[
{ locale: 'en', name: 'English' },
{ locale: 'fr', name: 'Français' },
]}
/>
</AppBar>
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 `<Layout>`, and the `<Layout>` to your `<Admin>`:
The second argument to the `polyglotI18nProvider` function is the default locale. The third is the list of supported locales - and is used by the [`<LocaleMenuButton>`](./LocalesMenuButton.md) component to display a list of languages.

Next, pass the custom `i18nProvider` to your `<Admin>`:

```jsx
import { Admin } from 'react-admin';

import { MyAppBar } from './MyAppBar';
import { i18nProvider } from './i18nProvider';

const MyLayout = (props) => <Layout {...props} appBar={MyAppBar} />;

const App = () => (
<Admin
i18nProvider={i18nProvider}
dataProvider={dataProvider}
layout={MyLayout}
>
...
</Admin>
Expand All @@ -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' }
],
);
```

Expand All @@ -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' }
],
);
```

Expand All @@ -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
Expand All @@ -149,11 +139,20 @@ 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 }
);
```

**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'
```

28 changes: 28 additions & 0 deletions docs/TranslationTranslating.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 4 additions & 50 deletions examples/simple/src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLLIElement, MenuItemProps>(
(props, ref) => {
const [locale, setLocale] = useLocaleState();
const { onClose } = useUserMenu();

return (
<MenuItem
ref={ref}
{...props}
sx={{ color: 'text.secondary' }}
onClick={event => {
setLocale(locale === 'en' ? 'fr' : 'en');
onClose();
}}
>
<ListItemIcon sx={{ minWidth: 5 }}>
<Language />
</ListItemIcon>
Switch Language
</MenuItem>
);
}
);

const MyUserMenu = () => (
<UserMenu>
<SwitchLanguage />
<Logout />
</UserMenu>
);

const MyAppBar = memo(props => <AppBar {...props} userMenu={<MyUserMenu />} />);
import { ReactQueryDevtools } from 'react-query/devtools';
import { Layout } from 'react-admin';
import { CssBaseline } from '@mui/material';

export default props => (
<>
<CssBaseline />
<Layout {...props} appBar={MyAppBar} />
<Layout {...props} />
<ReactQueryDevtools
initialIsOpen={false}
toggleButtonProps={{ style: { width: 20, height: 30 } }}
Expand Down
21 changes: 14 additions & 7 deletions examples/simple/src/i18nProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ const messages = {
fr: () => 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' },
]
);
23 changes: 20 additions & 3 deletions packages/ra-i18n-polyglot/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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[] | any = [{ locale: 'en', name: 'English' }],
polyglotOptions: any = {}
): I18nProvider => {
let locale = initialLocale;
Expand All @@ -33,10 +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 },
...polyglotOptions,
...polyglotOptionsFinal,
});
let translate = polyglot.t.bind(polyglot);

Expand All @@ -57,5 +73,6 @@ export default (
}
),
getLocale: () => locale,
getLocales: () => availableLocalesFinal,
};
};
5 changes: 2 additions & 3 deletions packages/react-admin/src/defaultI18nProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import polyglotI18nProvider from 'ra-i18n-polyglot';
export const defaultI18nProvider = polyglotI18nProvider(
() => defaultMessages,
'en',
{
allowMissing: true,
}
[{ name: 'en', value: 'English' }],
{ allowMissing: true }
);

0 comments on commit 31317e7

Please sign in to comment.