Docs
Systhema Design (opens in new tab)
Unreleased

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.

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.