---
title: "Content localization"
description: "Turn on content locales and choose which fields are localized."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/next/payload/localization
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/next/llms.txt
---
> This page isn't translated yet. Showing English.


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](https://docs.systhema.app/sk/next/guides/multilingual.md)).

## Turning locales on

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](https://docs.systhema.app/sk/next/payload/localization/field-policy.md), 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](https://docs.systhema.app/sk/next/nextjs/locales.md), [`@systhemaui/next` — Locales](https://docs.systhema.app/sk/next/nextjs/locales.md), [Payload Admin translations](https://docs.systhema.app/sk/next/payload/localization/admin-translations.md), and — for turning localization on where content already exists — [Localizing an existing Payload site](https://docs.systhema.app/sk/next/payload/localization/migrating-existing-site.md).

## Which fields are localized (`localization`)

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

```ts
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](https://docs.systhema.app/sk/next/payload/localization/per-locale-publishing.md).

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](https://docs.systhema.app/sk/next/payload/localization/field-policy.md) — 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 translations

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](https://docs.systhema.app/sk/next/nextjs/locales.md#advanced-per-locale-form)) 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 options

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