Skip to content

Commit f716122

Browse files
authored
feat!: typed i18n (#6343)
1 parent 353c2b0 commit f716122

File tree

84 files changed

+1232
-581
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+1232
-581
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ jobs:
301301
- fields__collections__Lexical
302302
- live-preview
303303
- localization
304+
- i18n
304305
- plugin-cloud-storage
305306
- plugin-form-builder
306307
- plugin-nested-docs

docs/admin/components.mdx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -565,10 +565,11 @@ These are the props that will be passed to your custom Label.
565565
#### Example
566566

567567
```tsx
568+
'use client'
568569
import React from 'react'
569-
import { useTranslation } from 'react-i18next'
570+
import { getTranslation } from '@payloadcms/translations'
571+
import { useTranslation } from '@payloadcms/ui/providers/Translation'
570572

571-
import { getTranslation } from 'payload/utilities/getTranslation'
572573

573574
type Props = {
574575
htmlFor?: string
@@ -680,21 +681,21 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
680681

681682
### Getting the current language
682683

683-
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
684+
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `@payloadcms/ui/providers/Translation` in your components.
684685

685686
For example:
686687

687688
```tsx
688-
import { useTranslation } from 'react-i18next'
689+
import { useTranslation } from '@payloadcms/ui/providers/Translation'
689690

690691
const CustomComponent: React.FC = () => {
691692
// highlight-start
692-
const { t, i18n } = useTranslation('namespace1')
693+
const { t, i18n } = useTranslation()
693694
// highlight-end
694695

695696
return (
696697
<ul>
697-
<li>{t('key', { variable: 'value' })}</li>
698+
<li>{t('namespace1:key', { variable: 'value' })}</li>
698699
<li>{t('namespace2:key', { variable: 'value' })}</li>
699700
<li>{i18n.language}</li>
700701
</ul>

docs/configuration/i18n.mdx

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ desc: Manage and customize internationalization support in your CMS editor exper
66
keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
77
---
88

9-
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config.
9+
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. It comes included by default and can be extended in your config.
1010

1111
While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language.
1212

@@ -72,9 +72,7 @@ After a user logs in, they can change their language selection in the `/account`
7272

7373
Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
7474

75-
Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
76-
77-
Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more.
75+
Anywhere in your Payload app that you have access to the `req` object, you can access payload's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
7876

7977
### Configuration Options
8078

@@ -88,9 +86,8 @@ import { buildConfig } from 'payload/config'
8886
export default buildConfig({
8987
//...
9088
i18n: {
91-
fallbackLng: 'en', // default
92-
debug: false, // default
93-
resources: {
89+
fallbackLanguage: 'en', // default
90+
translations: {
9491
en: {
9592
custom: {
9693
// namespace can be anything you want
@@ -107,4 +104,63 @@ export default buildConfig({
107104
})
108105
```
109106

110-
See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more.
107+
## Types for custom translations
108+
109+
In order to use custom translations in your project, you need to provide the types for the translations. Here is an example of how you can define the types for the custom translations in a custom react component:
110+
111+
```ts
112+
'use client'
113+
import type { NestedKeysStripped } from '@payloadcms/translations'
114+
import type React from 'react'
115+
116+
import { useTranslation } from '@payloadcms/ui/providers/Translation'
117+
118+
const customTranslations = {
119+
en: {
120+
general: {
121+
test: 'Custom Translation',
122+
},
123+
},
124+
}
125+
126+
type CustomTranslationObject = typeof customTranslations.en
127+
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
128+
129+
export const MyComponent: React.FC = () => {
130+
const { i18n, t } = useTranslation<CustomTranslationObject, CustomTranslationKeys>() // These generics merge your custom translations with the default client translations
131+
132+
return t('general:test')
133+
}
134+
135+
```
136+
137+
Additionally, payload exposes the `t` function in various places, for example in labels. Here is how you would type those:
138+
139+
```ts
140+
import type {
141+
DefaultTranslationKeys,
142+
NestedKeysStripped,
143+
TFunction,
144+
} from '@payloadcms/translations'
145+
import type { Field } from 'payload/types'
146+
147+
const customTranslations = {
148+
en: {
149+
general: {
150+
test: 'Custom Translation',
151+
},
152+
},
153+
}
154+
155+
type CustomTranslationObject = typeof customTranslations.en
156+
type CustomTranslationKeys = NestedKeysStripped<CustomTranslationObject>
157+
158+
const field: Field = {
159+
name: 'myField',
160+
type: 'text',
161+
label: (
162+
{ t }: { t: TFunction<CustomTranslationKeys | DefaultTranslationKeys> }, // The generic passed to TFunction does not automatically merge the custom translations with the default translations. We need to merge them ourselves here
163+
) => t('fields:addLabel'),
164+
}
165+
```
166+

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
"docker:start": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml up -d",
5555
"docker:stop": "docker compose -f packages/plugin-cloud-storage/docker-compose.yml down",
5656
"fix": "eslint \"packages/**/*.ts\" --fix",
57-
"generate:types": "PAYLOAD_CONFIG_PATH=./test/_community/config.ts node --no-deprecation ./packages/payload/bin.js generate:types",
5857
"lint": "eslint \"packages/**/*.ts\"",
5958
"lint-staged": "lint-staged",
6059
"obliterate-playwright-cache": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",

packages/next/src/layouts/Root/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AcceptedLanguages } from '@payloadcms/translations'
1+
import type { AcceptedLanguages, I18nClient } from '@payloadcms/translations'
22
import type { SanitizedConfig } from 'payload/types'
33

44
import { rtlLanguages } from '@payloadcms/translations'
@@ -50,7 +50,11 @@ export const RootLayout = async ({
5050
})
5151

5252
const payload = await getPayloadHMR({ config })
53-
const i18n = await initI18n({ config: config.i18n, context: 'client', language: languageCode })
53+
const i18n: I18nClient = await initI18n({
54+
config: config.i18n,
55+
context: 'client',
56+
language: languageCode,
57+
})
5458
const clientConfig = await createClientConfig({ config, t: i18n.t })
5559

5660
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)

packages/next/src/utilities/buildFieldSchemaMap/traverseFields.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { FieldSchemaMap } from './types.js'
88
type Args = {
99
config: SanitizedConfig
1010
fields: Field[]
11-
i18n: I18n
11+
i18n: I18n<any, any>
1212
schemaMap: FieldSchemaMap
1313
schemaPath: string
1414
validRelationships: string[]

packages/next/src/utilities/getNextRequestI18n.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { cookies, headers } from 'next/headers.js'
77
import { getRequestLanguage } from './getRequestLanguage.js'
88

99
/**
10-
* In the context of NextJS, this function initializes the i18n object for the current request.
10+
* In the context of Next.js, this function initializes the i18n object for the current request.
1111
*
1212
* It must be called on the server side, and within the lifecycle of a request since it relies on the request headers and cookies.
1313
*/

packages/next/src/utilities/initPage/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { I18nClient } from '@payloadcms/translations'
12
import type { InitPageResult, PayloadRequestWithData, VisibleEntities } from 'payload/types'
23

34
import { initI18n } from '@payloadcms/translations'
@@ -40,7 +41,7 @@ export const initPage = async ({
4041
const cookies = parseCookies(headers)
4142
const language = getRequestLanguage({ config: payload.config, cookies, headers })
4243

43-
const i18n = await initI18n({
44+
const i18n: I18nClient = await initI18n({
4445
config: i18nConfig,
4546
context: 'client',
4647
language,

packages/next/src/views/Version/RenderFieldsToDiff/fields/Select/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { I18n } from '@payloadcms/translations'
1+
import type { I18nClient } from '@payloadcms/translations'
22
import type { SelectFieldProps } from '@payloadcms/ui/fields/Select'
33
import type { MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
44
import type { OptionObject, SelectField } from 'payload/types'
@@ -35,7 +35,7 @@ const getOptionsToRender = (
3535

3636
const getTranslatedOptions = (
3737
options: (OptionObject | string)[] | OptionObject | string,
38-
i18n: I18n,
38+
i18n: I18nClient,
3939
): string => {
4040
if (Array.isArray(options)) {
4141
return options

packages/next/src/views/Version/RenderFieldsToDiff/fields/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { I18n } from '@payloadcms/translations'
1+
import type { I18nClient } from '@payloadcms/translations'
22
import type { FieldMap, MappedField } from '@payloadcms/ui/utilities/buildComponentMap'
33
import type { FieldPermissions } from 'payload/auth'
44
import type React from 'react'
@@ -13,7 +13,7 @@ export type Props = {
1313
disableGutter?: boolean
1414
field: MappedField
1515
fieldMap: FieldMap
16-
i18n: I18n
16+
i18n: I18nClient
1717
isRichText?: boolean
1818
locale?: string
1919
locales?: string[]

0 commit comments

Comments
 (0)