---
title: "Admin translations"
description: "Admin languages and translating custom schemas, components, hooks and endpoints."
url: https://docs.systhema.app/next/payload/localization/admin-translations
version: unreleased (main)
docs_index: https://docs.systhema.app/next/llms.txt
---

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.

```text
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 copy

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.

```ts
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.

```ts
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 accessibility

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

```tsx
'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 validation

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.

```ts
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 overrides

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`.

```ts
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 recipes

### One-language Admin, multilingual website

```ts
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 website

```ts
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 variables

```ts
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 gate

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

```ts
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 workflow

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 boundaries

- `@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](https://docs.systhema.app/next/nextjs/locales.md), [content localization](https://docs.systhema.app/next/payload/localization.md), [localizing an existing site](https://docs.systhema.app/next/payload/localization/migrating-existing-site.md), and the `translations` and `locales` [plugin options](https://docs.systhema.app/next/payload/plugin-options.md).
