---
title: "Admin translations"
description: "Admin languages and translating custom schemas, components, hooks and endpoints."
url: https://docs.systhema.app/payload/localization/admin-translations
version: unreleased (main)
docs_index: https://docs.systhema.app/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.

Payload 3.85 does not expose translatable Live Preview breakpoint labels: its public config accepts only strings, its provider adds `Responsive` internally, and the toolbar renders those labels without `t()`. Systhema therefore does not rewrite breakpoint names through DOM observation or fork Payload's toolbar. All other Systhema-owned Preview and Live Preview copy follows the Admin locale; breakpoint names will become translatable when Payload exposes a translation-aware label contract.
