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', () => {