Docs
Next

Content localization

Turn on content locales and choose which fields are localized.

On this page

Content localization stores page, post and settings content per locale. It is one of three independent switches; frontend locales and Admin languages are the other two (see Multilingual sites).

Turning locales onLink to this section

Two independent, off-by-default axes. locales is the same value declared in systhema.config.ts (defineSysthemaLocales) — pass systhemaConfig.locales here so frontend routing and CMS content locales share one source of truth; it localizes Systhema-owned fields (page title/slug/content, header/footer, uploads alt, and template content). WHICH fields exactly is the separate localization option — see the content localization field policy, whose policy: 'all' localizes every field of every Systhema-owned schema instead of a fixed name allowlist. Do not add a raw config.localization block that diverges — the plugin rejects two sources of truth. translations is the separate Admin interface language (reviewed built-ins en, hu, fr, nl, cs, sk, ar, with fallback), which never changes content locales or public copy; omit it to keep the Admin English-only.

Enabling locales generates no glue files on either side. The one-time migration (run pnpm sync and confirm when prompted; --migrate-locales pre-approves it for automation) only wraps next.config.ts with withSysthema(nextConfig) (fresh scaffolds ship this general-purpose wrapper by default, so on those projects this step is already satisfied), adds this locales: systhemaConfig.locales line to payload.config.ts, and converts the owned site shell from app/(site)/template.tsx to app/(site)/shell.tsx (receiving locale as a prop from a managed boundary layout inside (systhema) — consumer code never lives inside (systhema); a managed not-found.tsx beside that layout re-exports your app/(site)/not-found.tsx so a thrown notFound() renders the 404 page inside the locale-resolved site shell instead of as a bare unstyled fragment). src/proxy.ts and the catch-all page.tsx are untouched — they read the config/env at runtime instead of importing a generated seam. Full references: Frontend locales, @systhemaui/next — Locales, Payload Admin translations, and — for turning localization on where content already exists — Localizing an existing Payload site.

Which fields are localized (localization)Link to this section

locales turns content localization on; localization decides which fields it reaches.

localization: {
  policy: 'all',              // 'legacy' (default) | 'all'
  shared: ['pages.default.hero.aspectRatio'],
  sharedRows: ['pages.default.gallery.items'],
  localized: ['pages.caseStudy.strapline'],
  localizeStatus: false,      // per-locale publishing, on by default with `locales`
}

'legacy' — the default — localizes fields whose NAME appears on a fixed allowlist. 'all' inverts it: every field of every Systhema-owned schema is localized except a documented exception list (submission keys, upload identity, access control, structural keys, and records of events). The default stays 'legacy' so that upgrading the package never changes an existing project's database shape on its own; a multi-locale project that has not declared a policy gets a one-time startup notice and a systhema doctor warning.

localizeStatus is a separate switch on the same option, and it decides something else: whether the publication state itself is per-locale. With it on, an editor can publish a page in hu while en stays a draft, on Pages, Posts and Components. Unlike policy it is an opt-out. It is on by default whenever locales is configured, and a no-op without it. It moves _status out of its shared column, so an existing site needs the data move first. systhema upgrade applies it, systhema migrate migrate-localize-status runs it standalone, and on a project with a Payload migration directory it also generates the migration file production applies. See per-locale publishing.

Preview the difference with pnpm payload systhema-localization-report before changing anything. Switching to 'all' on a site that already holds content is a schema change and needs backfillSysthemaLocalization in a Payload migration — deploying the code without it takes the site down, because production runs with Drizzle push disabled. The whole procedure, the exception list with a reason per entry, the arrays-and-blocks decision, and measured cost figures are in the content localization field policy — which the package also ships at node_modules/@systhemaui/payload/docs/localization-policy.md, the path the startup notice and the doctor hint print.

Missing translationsLink to this section

Publishing a page or post in the default locale alone is enough to make every configured locale's URL for it resolve. A locale whose missing behavior is 'fallback' — the default for every non-default locale, unless the contract's top-level fallback is set to false — serves the default locale's content at that locale's URL via Payload's own display fallback: the visited locale's UI strings and <html dir> stay as requested, and only the fields an editor hasn't translated yet borrow the default locale's value. Translate the page later and the translation takes over automatically, with no republish or config change. Set missing: 'redirect' on a locale (in the advanced locales: {...} form of defineSysthemaLocales, see Frontend locales) to 307-redirect that locale's URL to the default locale's equivalent URL instead of serving fallback content; set missing: 'notFound' on a locale to restore today's strict 404 for it specifically; or set the contract-level fallback: false to make 'notFound' the default everywhere (an explicit per-locale missing still overrides that switch). Fallback-behavior locales also prerender their untranslated pages at the default locale's slug under their own prefix/domain at build time, so a freshly added locale isn't 404ing on untranslated pages until content catches up. Draft mode and Payload Preview/Live Preview always show the real 404 on a genuine miss — an editor drafting or previewing unpublished/untranslated content never silently sees fallback or redirect behavior. SEO note: hreflang/alternate URLs are generated independently of missing and stay pure URL math — a 'redirect'-behavior locale's alternate can point at a URL that itself redirects, while the default 'fallback' behavior keeps every alternate a real 200 page, which is the SEO-safe choice for a locale still catching up on translations.

Form optionsLink to this section

One schema detail changes shape when locales is on, because a single shared value cannot serve several languages:

  • Form options. A radio/select/checkbox option normally carries one Option field that is both the visible text and the value stored on every submission. That cannot be translated — rewriting it would rewrite the stored answers — so a localized project gets the pair back: a Label (localized, what the visitor reads) beside a Value (shared, what is submitted and stored). The label is optional; options authored before you enabled locales have none and keep rendering their value, so nothing breaks on the way in. Fill in the label per locale to translate the list. Related, and not limited to localized projects: the lock beside a page/post/category/tag URL path means "keep this path synced to the title", and it now only regenerates the path when you actually edit the title. Opening the editor never rewrites a path you chose by hand, seeded through the API, or authored per locale. (The lock itself is a single shared flag, not a per-locale one — a title edit only ever rewrites the path of the locale you are editing.)