From 6a0a859563ed9e742260ea51a1839a1ef0f61fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Kalsnes=20Hagen?= <43886029+Snailedlt@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:56:15 +0100 Subject: [PATCH] feat: add internationalization (i18n) to locales (#4005) --- docs/configuration/localization.mdx | 32 ++++++++++++++++ .../Localizer/LocalizerLabel/index.tsx | 17 ++++++--- .../components/elements/Localizer/index.tsx | 14 +++++-- packages/payload/src/config/schema.ts | 8 +++- packages/payload/src/config/types.ts | 2 +- test/admin/config.ts | 17 ++++++++- test/admin/e2e.spec.ts | 37 +++++++++++++++++++ 7 files changed, 114 insertions(+), 13 deletions(-) diff --git a/docs/configuration/localization.mdx b/docs/configuration/localization.mdx index 2a052b7c1..86d064531 100644 --- a/docs/configuration/localization.mdx +++ b/docs/configuration/localization.mdx @@ -57,6 +57,38 @@ export default buildConfig({ }) ``` +**Example Payload config set up for localization with full locales objects (including [internationalization](/docs/configuration/i18n) support):** + +```ts +import { buildConfig } from 'payload/config' + +export default buildConfig({ + collections: [ + // collections go here + ], + localization: { + locales: [ + { + label: { + en: 'English', // English label + nb: 'Engelsk', // Norwegian label + }, + code: 'en', + }, + { + label: { + en: 'Norwegian', // English label + nb: 'Norsk', // Norwegian label + }, + code: 'nb', + }, + ], + defaultLocale: 'en', + fallback: true, + }, +}) +``` + **Here is a brief explanation of each of the options available within the `localization` property:** **`locales`** diff --git a/packages/payload/src/admin/components/elements/Localizer/LocalizerLabel/index.tsx b/packages/payload/src/admin/components/elements/Localizer/LocalizerLabel/index.tsx index 87ab32b01..ff49eefae 100644 --- a/packages/payload/src/admin/components/elements/Localizer/LocalizerLabel/index.tsx +++ b/packages/payload/src/admin/components/elements/Localizer/LocalizerLabel/index.tsx @@ -1,28 +1,33 @@ import React from 'react' -import { Chevron } from '../../..' -import { useLocale } from '../../../utilities/Locale' import { useTranslation } from 'react-i18next' +import { Chevron } from '../../..' +import { getTranslation } from '../../../../../utilities/getTranslation' +import { useLocale } from '../../../utilities/Locale' import './index.scss' const baseClass = 'localizer-button' export const LocalizerLabel: React.FC<{ - className?: string ariaLabel?: string + className?: string }> = (props) => { - const { className, ariaLabel } = props + const { ariaLabel, className } = props const locale = useLocale() const { t } = useTranslation('general') + const { i18n } = useTranslation() return (
{`${t('locale')}:`}
   - {`${locale.label}`} + {`${getTranslation( + locale.label, + i18n, + )}`}  
diff --git a/packages/payload/src/admin/components/elements/Localizer/index.tsx b/packages/payload/src/admin/components/elements/Localizer/index.tsx index 1393ba264..d06f3adf8 100644 --- a/packages/payload/src/admin/components/elements/Localizer/index.tsx +++ b/packages/payload/src/admin/components/elements/Localizer/index.tsx @@ -1,6 +1,8 @@ import qs from 'qs' import React from 'react' +import { useTranslation } from 'react-i18next' +import { getTranslation } from '../../../../utilities/getTranslation' import { useConfig } from '../../utilities/Config' import { useLocale } from '../../utilities/Locale' import { useSearchParams } from '../../utilities/SearchParams' @@ -18,9 +20,12 @@ const Localizer: React.FC<{ const config = useConfig() const { localization } = config + const { i18n } = useTranslation() const locale = useLocale() const searchParams = useSearchParams() + const localeLabel = getTranslation(locale.label, i18n) + if (localization) { const { locales } = localization @@ -44,8 +49,8 @@ const Localizer: React.FC<{ }), }} > - {locale.label} - {locale.label !== locale.code && ` (${locale.code})`} + {localeLabel} + {localeLabel !== locale.code && ` (${locale.code})`} ) : null} @@ -57,11 +62,12 @@ const Localizer: React.FC<{ locale: localeOption.code, } const search = qs.stringify(newParams) + const localeOptionLabel = getTranslation(localeOption.label, i18n) return ( - {localeOption.label} - {localeOption.label !== localeOption.code && ` (${localeOption.code})`} + {localeOptionLabel} + {localeOptionLabel !== localeOption.code && ` (${localeOption.code})`} ) })} diff --git a/packages/payload/src/config/schema.ts b/packages/payload/src/config/schema.ts index 9d77c93e8..5b7e96c8c 100644 --- a/packages/payload/src/config/schema.ts +++ b/packages/payload/src/config/schema.ts @@ -134,7 +134,13 @@ export default joi.object({ joi.array().items( joi.object().keys({ code: joi.string(), - label: joi.string(), + label: joi + .alternatives() + .try( + joi.object().pattern(joi.string(), [joi.string()]), + joi.string(), + joi.valid(false), + ), rtl: joi.boolean(), toString: joi.func(), }), diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 4e0ee9519..e05ad2ce3 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -288,7 +288,7 @@ export type Locale = { * label of supported locale * @example "English" */ - label: string + label: string | Record /** * if true, defaults textAligmnent on text fields to RTL */ diff --git a/test/admin/config.ts b/test/admin/config.ts index 6921f3d00..85bcfd2be 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -85,7 +85,22 @@ export default buildConfigWithDefaults({ }, localization: { defaultLocale: 'en', - locales: ['en', 'es'], + locales: [ + { + label: { + es: 'Español', + en: 'Spanish', + }, + code: 'es', + }, + { + label: { + es: 'Inglés', + en: 'English', + }, + code: 'en', + }, + ], }, collections: [ Posts, diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index e6fa8e2e6..ccac4cc53 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -488,6 +488,43 @@ describe('admin', () => { 'Home', ) }) + + test('should allow custom translation of locale labels', async () => { + const selectOptionClass = '.localizer .popup-button-list__button' + const localizorButton = page.locator('.localizer .popup-button') + const secondLocale = page.locator(selectOptionClass).nth(1) + + async function checkLocalLabels(firstLabel: string, secondLabel: string) { + await localizorButton.click() + await expect(page.locator(selectOptionClass).first()).toContainText(firstLabel) + await expect(page.locator(selectOptionClass).nth(1)).toContainText(secondLabel) + } + + await checkLocalLabels('English (en)', 'Spanish (es)') + + // Change locale to Spanish + await localizorButton.click() + await expect(secondLocale).toContainText('Spanish (es)') + await secondLocale.click() + + // Go to account page + await page.goto(url.account) + + const languageField = page.locator('.payload-settings__language .react-select') + const options = page.locator('.rs__option') + + // Change language to Spanish + await languageField.click() + await options.locator('text=Español').click() + + await checkLocalLabels('Inglés (en)', 'Español (es)') + + // Change locale and language back to English + await languageField.click() + await options.locator('text=English').click() + await localizorButton.click() + await expect(secondLocale).toContainText('Spanish (es)') + }) }) describe('list view', () => {