Docs

This page isn't translated yet

Admin translations

Admin languages and translating custom schemas, components, hooks and endpoints.

On this page

Systhema's Admin translation layer is separate from Payload content localization. It ships reviewed built-in catalogs for English (en), Hungarian (hu), French (fr), Dutch (nl), Czech (cs), Slovak (sk), and Arabic (ar), and never changes localization.locales, document locale values, or your public-site copy.

systhema.config.ts locales ──> @systhemaui/next + next-intl ──> public website
                         └───> Payload config.localization ───> localized content

withSysthema translations ──> Payload config.i18n ───────────> Admin interface

These branches are independent. A site can publish English, French, and Dutch content while its Admin is English-only. Another site can publish only Hungarian content while editors choose any shipped Admin language. Frontend messages use next-intl; the helpers in this guide use Payload's TFunction. Configuring one branch never adds languages to the other.

withSysthema installs Payload's official language objects only for the selected Admin languages. Systhema's copy lives in the separate systhema namespace inside i18n.translations.

Define application copyLink to this section

Create a namespace for application-owned Admin copy. The base language defines the complete key shape; every other catalog must contain the same keys, placeholders, rich tags, and plural forms.

import {
  definePayloadAdminTranslations,
  withSysthema,
} from '@systhemaui/payload'

const adminCopy = definePayloadAdminTranslations({
  namespace: 'acme',
  baseLanguage: 'en',
  catalogs: {
    en: {
      collections: { campaign: 'Campaign', campaigns: 'Campaigns' },
      fields: {
        audience: 'Audience',
        audienceHint: 'Who should receive this campaign?',
        active: 'Active',
      },
      blocks: { banner: 'Campaign banner' },
      actions: { sent_one: '{{count}} message sent', sent_other: '{{count}} messages sent' },
      errors: { unavailable: 'Campaign service is unavailable' },
      accessibility: { dismissNotice: 'Dismiss campaign notice' },
    },
    hu: {
      collections: { campaign: 'Kampány', campaigns: 'Kampányok' },
      fields: {
        audience: 'Célközönség',
        audienceHint: 'Ki kapja meg ezt a kampányt?',
        active: 'Aktív',
      },
      blocks: { banner: 'Kampány banner' },
      actions: { sent_one: '{{count}} üzenet elküldve', sent_other: '{{count}} üzenet elküldve' },
      errors: { unavailable: 'A kampányszolgáltatás nem érhető el' },
      accessibility: { dismissNotice: 'Kampányértesítés bezárása' },
    },
  },
} as const)

export default withSysthema(config, {
  translations: { supported: ['en', 'hu'], fallback: 'en', catalogs: [adminCopy] },
})

Systhema supports only Admin languages whose full catalog has been reviewed: en, hu, fr, nl, cs, sk, and ar in this release. A code such as de is a configuration error even though Payload itself has German core translations. Raw config.i18n.supportedLanguages, translations, and fallbackLanguage must also stay within the explicitly selected set; Systhema rejects divergent raw configuration rather than silently exposing an unreviewed, partly English Admin.

Upgrading an existing project does not force this on you. While translations is unset, Systhema adopts whatever Admin languages your raw config.i18n already selects and simply installs its own systhema namespace for the ones it ships — a project that configured a Hungarian Admin the old way keeps booting untouched.

The single-owner contract starts the moment you set translations. From then on, raw config.i18n entries for languages outside translations.supported fail configuration deliberately, so move the Admin-language selection into the option in the same change. If both the raw config and the Systhema option declare a fallback, they must name the same language; remove the raw fallback so the canonical option has one owner.

Use the typed helpers anywhere Payload accepts a schema label or description. The same helpers work for collections, globals, blocks, and fields, including select options.

export const Campaigns = {
  slug: 'campaigns',
  labels: {
    singular: adminCopy.label('collections:campaign'),
    plural: adminCopy.label('collections:campaigns'),
  },
  fields: [
    {
      name: 'audience', type: 'select', label: adminCopy.label('fields:audience'),
      admin: { description: adminCopy.description('fields:audienceHint') },
      options: [{ label: adminCopy.staticLabel('fields:active'), value: 'active' }],
    },
  ],
}

export const campaignBlock = {
  slug: 'campaignBanner', labels: { singular: adminCopy.label('blocks:banner') }, fields: [],
}

staticLabel and staticDescription are only available for values without placeholders. They produce Payload's language map and are useful where a third-party schema or serialized client-prop API cannot call t.

Components, hooks, and accessibilityLink to this section

Client components import the client-only entry point. It contains no catalog JSON or server projection code.

'use client'
import { usePayloadAdminTranslations } from '@systhemaui/payload/translations/client'

export function CampaignNotice() {
  const { t } = usePayloadAdminTranslations<typeof adminCopy>('acme')
  return (
    <button aria-label={t('accessibility:dismissNotice')}>
      {t('actions:sent_other', { count: 12 })}
    </button>
  )
}

Import the definition as a type in a real client module so the JSON catalogs stay out of the browser graph: import type { adminCopy } from '@/payload/adminCopy'. The helper delegates values to Payload's translator, including interpolation, plurals, and formatting.

Use the same t value for visible buttons, placeholders, titles, loading states, and toast text; do not hard-code an English fallback in an Admin component.

Requests, endpoints, hooks, and validationLink to this section

Server code uses req.t so the message follows the active Admin language. This applies to endpoint responses, hook/validation errors, and custom Error messages rendered by Admin.

export const endpoint = {
  path: '/campaigns/send', method: 'post',
  handler: async (req) => {
    if (!req.user) return Response.json({ message: adminCopy.translate(req.t, 'errors:unavailable') }, { status: 403 })
    return Response.json({
      message: adminCopy.translate(req.t, 'actions:sent_other', { count: 12 }),
    })
  },
}

export const validateAudience = ({ req, value }: { req: { t: Parameters<typeof adminCopy.translate>[0] }; value?: string }) =>
  value ? true : adminCopy.translate(req.t, 'errors:unavailable')

For server-only utility code with an I18n object, use adminCopy.get(i18n, 'fields:audience'). It accepts the same third argument for interpolation and plural variables. Payload server components that receive i18n can call that helper directly; request handlers and hooks should prefer req.t so the active Admin language is authoritative.

Registration and overridesLink to this section

Register as many consumer namespaces as needed through catalogs. Raw config.i18n.translations for a selected language remains intact. Merge order is: registered consumer catalogs, raw consumer translations, then translations.overrides for the reserved systhema namespace. You cannot register a consumer namespace named systhema.

withSysthema(config, {
  translations: {
    supported: ['en', 'hu'],
    catalogs: [adminCopy],
    overrides: { hu: { common: { save: 'Mentés most' } } },
  },
})

Only the selected Admin languages receive Systhema's official Payload Language objects. This list is the Admin interface language set — it is not the website's content locales. Even where the two use the same code (fr here and in locales), they stay independent opt-ins: place website locales in Systhema's locale configuration, not here.

Common recipesLink to this section

One-language Admin, multilingual websiteLink to this section

withSysthema(config, { translations: { supported: ['en'], fallback: 'en' } })

Configure the website's en, fr, and nl locales in systhema.config.ts. No Admin catalog is needed for French or Dutch.

Bilingual Admin, one-language websiteLink to this section

withSysthema(config, {
  translations: { supported: ['en', 'hu'], fallback: 'hu', catalogs: [adminCopy] },
})

Leave frontend locales disabled. The application keeps its current route structure and does not load next-intl, while editors can still choose English or Hungarian in Payload.

Custom validation with translated variablesLink to this section

const validateLimit = (
  value: unknown[],
  { req }: { req: { t: Parameters<typeof adminCopy.translate>[0] } },
) =>
  value.length <= 3 ||
  adminCopy.translate(req.t, 'errors:tooMany', { count: value.length, maximum: 3 })

Type safety as a review gateLink to this section

Keep catalog definitions as const. Misspelled keys then fail during typechecking:

adminCopy.label('fields:audience')
// @ts-expect-error There is no such base-catalog key.
adminCopy.label('fields:audence')

Runtime validation adds exact language parity, non-empty leaves, matching {{variables}}, rich tags, and plural variants.

Translator workflowLink to this section

  1. Add the English key and its counterpart in every shipped language in the same change.
  2. Preserve {{placeholder}}, ICU plural suffixes (_one, _other), and rich tags such as <0>.
  3. Run the Payload translation tests; validation rejects missing, extra, empty, placeholder, and plural mismatches before configuration is projected.
  4. Keep API field names, enum values, provider IDs, AI prompt target-language instructions, and machine/log messages as protocol data — not Admin catalog entries.
  5. Give a translator exactly one language file. English is the structural source of truth; never add or remove keys in a single translation.

Package boundariesLink to this section

  • @systhemaui/payload/translations is server-safe and exports definitions, validation, projection, catalogs, schema helpers, and types.
  • @systhemaui/payload/translations/client is client-only and exports hooks. It imports neither JSON catalog nor Payload config projection code.
  • @systhemaui/payload/translations/languages/<code> re-exports Payload's official language object for each shipped Admin language. Consumers normally let withSysthema install them.
  • @systhemaui/next owns frontend next-intl integration. Do not use Payload Admin helpers for public-site copy.

Systhema-owned UI should use systhemaAdminTranslations on the server and useSysthemaAdminTranslations in a client component. Consumer code should use its own namespace definition, which prevents accidental key collisions and makes ownership clear.

Systhema ships an @payloadcms/ui patch that separates the resolved field and block schema cache by Admin language. Without it, the first editor's language can remain in native block pickers and fields after a language switch, even while the surrounding interface changes correctly. The patch also translates the default Mobile, Tablet and Responsive Live Preview labels. Consumer-authored breakpoint labels remain unchanged.

New Payload projects receive the UI and Lexical patches through systhema create; existing projects receive them through systhema upgrade or systhema doctor --fix. Run pnpm install after a doctor fix and restart the application to clear the old in-memory schema cache. An upgrade refreshes Systhema-owned patch files when their contents change within the same Payload range. Custom patch paths stay consumer-owned; combine the changes manually if you maintain your own patch for either package.

The Lexical patch translates nested block validation messages while retaining block identifiers, field paths and nested errors. The shipped catalogs cover all seven Admin languages; array and blocks fields declare explicit singular and plural row labels so Payload does not generate English add-row labels from field names. Block picker images are decorative because the visible translated block label already names them.

The Lexical handle buttons also read the active Admin translator for “Add block” and “Drag to move”, including in Payload's compiled client entry. Framework-neutral React primitives still accept translated text through their documented label props; the Payload and Next adapters supply native translations.

Related: frontend locales, content localization, localizing an existing site, and the translations and locales plugin options.