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

Add support for getLocales in Polyglot i18nProvider #8143

Merged
merged 6 commits into from
Sep 8, 2022
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
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we throw a warning if the initialLocal is not in the availableLocales? Maybe I'm nitpicking here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to have ;)

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 }
);