feat: pass i18n through field label and description functions (#11802)

Passes the `i18n` arg through field label and description functions.
This is to avoid using custom components when simply needing to
translate a `StaticLabel` object, such as collection labels.

Here's an example:

```ts
{
  labels: {
    singular: {
      en: 'My Collection'
    }
  },
  fields: [
   // ...
   {
     type: 'collapsible',
     label: ({ i18n }) => `Translate this: ${getTranslation(collectionConfig.labels.singular, i18n)}`
     // ...
    }
  ]
}
```
This commit is contained in:
Jacob Fletcher
2025-03-20 13:43:17 -04:00
committed by GitHub
parent 032c424244
commit 31211e9755
12 changed files with 29 additions and 24 deletions

View File

@@ -172,7 +172,7 @@ export async function VersionsView(props: DocumentViewServerProps) {
const pluralLabel = collectionConfig?.labels?.plural const pluralLabel = collectionConfig?.labels?.plural
? typeof collectionConfig.labels.plural === 'function' ? typeof collectionConfig.labels.plural === 'function'
? collectionConfig.labels.plural({ t }) ? collectionConfig.labels.plural({ i18n, t })
: collectionConfig.labels.plural : collectionConfig.labels.plural
: globalConfig?.label : globalConfig?.label

View File

@@ -1,9 +1,9 @@
import type { TFunction } from '@payloadcms/translations' import type { I18nClient, TFunction } from '@payloadcms/translations'
import type { Field } from '../../fields/config/types.js' import type { Field } from '../../fields/config/types.js'
import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js' import type { ClientFieldWithOptionalType, ServerComponentProps } from './Field.js'
export type DescriptionFunction = ({ t }: { t: TFunction }) => string export type DescriptionFunction = (args: { i18n: I18nClient; t: TFunction }) => string
export type FieldDescriptionClientComponent< export type FieldDescriptionClientComponent<
TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType, TFieldClient extends ClientFieldWithOptionalType = ClientFieldWithOptionalType,

View File

@@ -215,11 +215,11 @@ export const createClientCollectionConfig = ({
clientCollection.labels = { clientCollection.labels = {
plural: plural:
typeof collection.labels.plural === 'function' typeof collection.labels.plural === 'function'
? collection.labels.plural({ t: i18n.t }) ? collection.labels.plural({ i18n, t: i18n.t })
: collection.labels.plural, : collection.labels.plural,
singular: singular:
typeof collection.labels.singular === 'function' typeof collection.labels.singular === 'function'
? collection.labels.singular({ t: i18n.t }) ? collection.labels.singular({ i18n, t: i18n.t })
: collection.labels.singular, : collection.labels.singular,
} }
break break

View File

@@ -508,9 +508,8 @@ export type LocalizationConfig = Prettify<
LocalizationConfigWithLabels | LocalizationConfigWithNoLabels LocalizationConfigWithLabels | LocalizationConfigWithNoLabels
> >
export type LabelFunction<TTranslationKeys = DefaultTranslationKeys> = ({ export type LabelFunction<TTranslationKeys = DefaultTranslationKeys> = (args: {
t, i18n: I18nClient
}: {
t: TFunction<TTranslationKeys> t: TFunction<TTranslationKeys>
}) => string }) => string

View File

@@ -140,12 +140,12 @@ export const createClientBlocks = ({
if (block.labels.singular) { if (block.labels.singular) {
if (typeof block.labels.singular === 'function') { if (typeof block.labels.singular === 'function') {
clientBlock.labels.singular = block.labels.singular({ t: i18n.t }) clientBlock.labels.singular = block.labels.singular({ i18n, t: i18n.t })
} else { } else {
clientBlock.labels.singular = block.labels.singular clientBlock.labels.singular = block.labels.singular
} }
if (typeof block.labels.plural === 'function') { if (typeof block.labels.plural === 'function') {
clientBlock.labels.plural = block.labels.plural({ t: i18n.t }) clientBlock.labels.plural = block.labels.plural({ i18n, t: i18n.t })
} else { } else {
clientBlock.labels.plural = block.labels.plural clientBlock.labels.plural = block.labels.plural
} }
@@ -224,7 +224,7 @@ export const createClientField = ({
//@ts-expect-error - would need to type narrow //@ts-expect-error - would need to type narrow
if (typeof incomingField.label === 'function') { if (typeof incomingField.label === 'function') {
//@ts-expect-error - would need to type narrow //@ts-expect-error - would need to type narrow
clientField.label = incomingField.label({ t: i18n.t }) clientField.label = incomingField.label({ i18n, t: i18n.t })
} else { } else {
//@ts-expect-error - would need to type narrow //@ts-expect-error - would need to type narrow
clientField.label = incomingField.label clientField.label = incomingField.label
@@ -246,12 +246,12 @@ export const createClientField = ({
if (incomingField.labels.singular) { if (incomingField.labels.singular) {
if (typeof incomingField.labels.singular === 'function') { if (typeof incomingField.labels.singular === 'function') {
field.labels.singular = incomingField.labels.singular({ t: i18n.t }) field.labels.singular = incomingField.labels.singular({ i18n, t: i18n.t })
} else { } else {
field.labels.singular = incomingField.labels.singular field.labels.singular = incomingField.labels.singular
} }
if (typeof incomingField.labels.plural === 'function') { if (typeof incomingField.labels.plural === 'function') {
field.labels.plural = incomingField.labels.plural({ t: i18n.t }) field.labels.plural = incomingField.labels.plural({ i18n, t: i18n.t })
} else { } else {
field.labels.plural = incomingField.labels.plural field.labels.plural = incomingField.labels.plural
} }
@@ -287,12 +287,12 @@ export const createClientField = ({
if (incomingField.labels.singular) { if (incomingField.labels.singular) {
if (typeof incomingField.labels.singular === 'function') { if (typeof incomingField.labels.singular === 'function') {
field.labels.singular = incomingField.labels.singular({ t: i18n.t }) field.labels.singular = incomingField.labels.singular({ i18n, t: i18n.t })
} else { } else {
field.labels.singular = incomingField.labels.singular field.labels.singular = incomingField.labels.singular
} }
if (typeof incomingField.labels.plural === 'function') { if (typeof incomingField.labels.plural === 'function') {
field.labels.plural = incomingField.labels.plural({ t: i18n.t }) field.labels.plural = incomingField.labels.plural({ i18n, t: i18n.t })
} else { } else {
field.labels.plural = incomingField.labels.plural field.labels.plural = incomingField.labels.plural
} }
@@ -345,7 +345,7 @@ export const createClientField = ({
} }
field.options[i] = { field.options[i] = {
label: option.label({ t: i18n.t }), label: option.label({ i18n, t: i18n.t }),
value: option.value, value: option.value,
} }
} }
@@ -409,7 +409,7 @@ export const createClientField = ({
case 'description': case 'description':
if ('description' in tab.admin) { if ('description' in tab.admin) {
if (typeof tab.admin?.description === 'function') { if (typeof tab.admin?.description === 'function') {
clientTab.admin.description = tab.admin.description({ t: i18n.t }) clientTab.admin.description = tab.admin.description({ i18n, t: i18n.t })
} else { } else {
clientTab.admin.description = tab.admin.description clientTab.admin.description = tab.admin.description
} }

View File

@@ -447,6 +447,7 @@ export type OptionObject = {
label: OptionLabel label: OptionLabel
value: string value: string
} }
export type Option = OptionObject | string export type Option = OptionObject | string
export type FieldGraphQLType = { export type FieldGraphQLType = {

View File

@@ -105,7 +105,7 @@ export const createClientGlobalConfig = ({
break break
case 'label': case 'label':
clientGlobal.label = clientGlobal.label =
typeof global.label === 'function' ? global.label({ t: i18n.t }) : global.label typeof global.label === 'function' ? global.label({ i18n, t: i18n.t }) : global.label
break break
default: { default: {
clientGlobal[key] = global[key] clientGlobal[key] = global[key]

View File

@@ -5,7 +5,7 @@ import type { LabelFunction, StaticLabel } from '../config/types.js'
export const getTranslatedLabel = (label: LabelFunction | StaticLabel, i18n?: I18n): string => { export const getTranslatedLabel = (label: LabelFunction | StaticLabel, i18n?: I18n): string => {
if (typeof label === 'function') { if (typeof label === 'function') {
return label({ t: i18n.t }) return label({ i18n, t: i18n.t })
} }
if (typeof label === 'object') { if (typeof label === 'object') {

View File

@@ -1,10 +1,10 @@
import type { JSX } from 'react' import type { JSX } from 'react'
import type { I18n, TFunction } from '../types.js' import type { I18n, I18nClient, TFunction } from '../types.js'
type LabelType = type LabelType =
| (() => JSX.Element) | (() => JSX.Element)
| (({ t }: { t: TFunction }) => string) | ((args: { i18n: I18nClient; t: TFunction }) => string)
| JSX.Element | JSX.Element
| Record<string, string> | Record<string, string>
| string | string
@@ -35,7 +35,9 @@ export const getTranslation = <T extends LabelType>(
} }
if (typeof label === 'function') { if (typeof label === 'function') {
return label({ t: i18n.t }) as unknown as T extends JSX.Element ? JSX.Element : string return label({ i18n: undefined as any, t: i18n.t }) as unknown as T extends JSX.Element
? JSX.Element
: string
} }
// If it's a React Element or string, then we should just pass it through // If it's a React Element or string, then we should just pass it through

View File

@@ -98,7 +98,7 @@ const TabsFieldComponent: TabsFieldClientComponent = (props) => {
const activeTabStaticDescription = const activeTabStaticDescription =
typeof activeTabDescription === 'function' typeof activeTabDescription === 'function'
? activeTabDescription({ t: i18n.t }) ? activeTabDescription({ i18n, t: i18n.t })
: activeTabDescription : activeTabDescription
const hasVisibleTabs = tabStates.some(({ passesCondition }) => passesCondition) const hasVisibleTabs = tabStates.some(({ passesCondition }) => passesCondition)

View File

@@ -244,6 +244,7 @@ export const renderField: RenderFieldMethod = ({
fieldState.customComponents.Description = ( fieldState.customComponents.Description = (
<FieldDescription <FieldDescription
description={fieldConfig.admin?.description({ description={fieldConfig.admin?.description({
i18n: req.i18n,
t: req.i18n.t, t: req.i18n.t,
})} })}
path={path} path={path}

View File

@@ -53,7 +53,9 @@ export function groupNavItems(
: entityToGroup.entity.label : entityToGroup.entity.label
const label = const label =
typeof labelOrFunction === 'function' ? labelOrFunction({ t: i18n.t }) : labelOrFunction typeof labelOrFunction === 'function'
? labelOrFunction({ i18n, t: i18n.t })
: labelOrFunction
if (entityToGroup.entity.admin.group) { if (entityToGroup.entity.admin.group) {
const existingGroup = groups.find( const existingGroup = groups.find(