---
title: "Frontend locales"
description: "defineSysthemaLocales: prefix and domain routing, the advanced per-locale form, missing translations."
requested_language: sk
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/sk/nextjs/locales
version: unreleased (main)
docs_index: https://docs.systhema.app/sk/llms.txt
---
> This page isn't translated yet. Showing English.


Frontend locales are opt-in. You define them once in `systhema.config.ts` with `defineSysthemaLocales()` from `@systhemaui/core`, and `@systhemaui/next` (and `@systhemaui/payload`) pick them up from there. For how frontend locales relate to content localization and Admin languages, see [Multilingual sites](https://docs.systhema.app/sk/guides/multilingual.md).

## Defining locales

`@systhemaui/core` owns the framework-neutral locale definition. Define it once in `systhema.config.ts`; it has no Next.js, Payload, or `next-intl` dependency. Omit `locales` entirely to preserve the existing single-language behaviour. `systhema.config.ts` is the **sole** source of locale data — nothing is serialized into a generated file; `@systhemaui/next` and `@systhemaui/payload` both read this same in-memory config (via `getSysthemaConfigSync()`) at build/request time instead of consuming a copy.

```ts title="systhema.config.ts"
import { defineSysthemaLocales, type SysthemaConfig } from '@systhemaui/core'

export const locales = defineSysthemaLocales({
  supported: ['en', 'fr'],
  default: 'en',
  fallback: true,
})

const config: SysthemaConfig = { locales }
export default config
```

`defineSysthemaLocales()` validates and canonicalizes BCP 47 codes, rejects duplicates and a default/RTL locale outside `supported`, and freezes the returned serializable contract. Multi-locale routing defaults to `localePrefix: 'always'` (`/en/page`, `/fr/page`) with locale detection on. Set `routing: { localePrefix: 'as-needed' }` when the default locale should keep an unprefixed URL (`/page`, `/fr/page`). Cookie-only `never` routing is intentionally outside the statically rendered CMS contract because locales would not have unique public URLs. `rtl` is an optional subset of `supported`; use `getSysthemaLocaleDirection(locales, locale)` when setting `<html dir>`.

`fallback` (default `true`) is the content-fallback toggle. In a Payload project it is forwarded as `config.localization.fallback` — whether an unpopulated localized field renders the default locale's value. A raw `config.localization.fallback` that disagrees makes `withSysthema()` throw, so set it here, not there. Do not confuse it with `messages.fallback` (frontend UI catalogs, see [Messages and catalogs](https://docs.systhema.app/sk/nextjs/locales/messages.md)) or `translations.fallback` (the Admin interface language, see [Admin translations](https://docs.systhema.app/sk/payload/localization/admin-translations.md)) — three separate `fallback` knobs, three different scopes.

An exact-one-locale contract is normalized to unprefixed `as-needed` routing with locale detection disabled. It still establishes deterministic document language, messages, and formatting without pretending the site has a multilingual URL space.

## Prefix routing

The multi-locale default is an explicit `always` prefix, so a page such as `slug` is public at `/en/slug` and `/hu/slug`. To keep the default language unprefixed, opt into `as-needed`:

```ts
locales: defineSysthemaLocales({
  supported: ['en', 'hu'],
  default: 'en',
  routing: { localePrefix: 'as-needed' },
})
// /slug (English), /hu/slug (Hungarian)
```

## Domain routing

`routing.domains` optionally maps each supported locale to a public host. Host-only values normalize to HTTPS; explicit HTTP origins are accepted for loopback development. The map must cover every supported locale and use one protocol, and cannot contain credentials, a path, query, or fragment. A locale alone on its host is served unprefixed; two or more locales that share a host are path-prefixed (`isSysthemaLocalePrefixedOnHost` reports which), so you can freely mix dedicated domains and shared subfolders — e.g. `{ en: 'example.com', hu: 'example.hu', fr: 'example.eu', nl: 'example.eu' }` gives `example.com`, `example.hu`, and `example.eu/fr` + `example.eu/nl`. See [Public URL recipes](https://docs.systhema.app/sk/nextjs/locales/url-recipes.md).

```ts
const locales = defineSysthemaLocales({
  supported: ['en', 'hu'],
  default: 'en',
  routing: { domains: { en: 'example.com', hu: 'pelda.hu' } },
})
```

For a mapped locale, `getSysthemaLocaleOrigin(locales, locale)` returns its normalized origin and `getSysthemaPublicHref(locales, locale, pathname)` returns the absolute canonical URL. `getSysthemaLocalizedPathname()` is the public, path-only presentation helper; under domains it returns the unprefixed pathname for a locale alone on its host, and a locale-prefixed pathname when the locale shares its host (see `isSysthemaLocalePrefixedOnHost`). It is not an on-demand revalidation key. Use `getSysthemaInternalLocalePathname()` for internal routing and revalidation: it always produces `/<locale><pathname>` for an enabled locale contract, including exact-one-locale, `as-needed`, `always`, and domain routing. This keeps locale caches distinct even when their public paths are unprefixed. `getSysthemaAlternates()` uses absolute public URLs when origins are configured.

## Advanced per-locale form

`defineSysthemaLocales()` also accepts an **advanced** per-locale form — mutually exclusive with `supported`/`rtl`/`routing.domains` — for defining `domain`, `rtl`, `missing`, and `subfolder` per locale in one place instead of three separate top-level lists. Key order of `locales` defines the supported locale order:

```ts
const locales = defineSysthemaLocales({
  default: 'en',
  locales: {
    en: { domain: 'https://example.com' },
    hu: { domain: 'https://pelda.hu', missing: 'fallback' }, // serve EN content at the HU URL
    fr: { domain: 'https://example.eu', missing: 'redirect' }, // 307 to the EN equivalent URL
    nl: { domain: 'https://example.eu' }, // no override — inherits the contract's default (see below)
    ar: { domain: 'https://example.ae', rtl: true },
  },
  fallback: true,
  routing: { localePrefix: 'as-needed', localeDetection: true },
})
```

A `domain` set on some but not all locales still throws the existing all-or-nothing error — set it on every locale or none. `rtl`/`missing`/`subfolder` combine freely with either domain routing or plain path prefixing.

`missing` controls what happens when a page or post has no translation for a locale's URL. The default for every **non-default** locale is `'fallback'`: the requested locale's URL serves the **default locale's** content via Payload's display fallback — the requested locale's UI strings and `<html dir>` stay as requested, and only the untranslated fields borrow the default locale's value; already-translated fields still show their translation. Set `missing: 'redirect'` on a locale to 307-redirect that locale's URL to the default locale's equivalent URL instead. Set the contract-level `fallback: false` to flip the implicit default to `'notFound'` everywhere (today's strict 404) — one coherent strictness switch. An explicit per-locale `missing: 'notFound'` still overrides that switch for one locale even under `fallback: true` (any explicit value, including `'notFound'`, always wins over the implicit default). `missing` cannot be set on the default locale — it throws. Draft mode and Payload Preview/Live Preview always see a real 404 on a genuine miss; they never fall back or redirect to different content. A locale whose behavior resolves to `'fallback'` also prerenders its untranslated pages at the default locale's slug under its own prefix/domain (via display-fallback static-param fan-out); `'redirect'`/`'notFound'` locales keep the strict per-locale fan-out.

Read the effective behavior for a locale with `getSysthemaLocaleMissingBehavior(locales, locale): 'notFound' | 'fallback' | 'redirect'` — it returns `'notFound'` for the default locale or an unrecognized locale, the explicit per-locale override when one is set, and otherwise `'fallback'` (or `'notFound'` when the contract's `fallback` is `false`).

`subfolder` overrides the **public** URL path segment for a prefix-routed locale — it defaults to the locale code, so `ar: { subfolder: 'arabic' }` serves that locale at `/arabic/...` instead of `/ar/...`. It is valid for a pure path-prefixed locale and for a locale sharing a host with others in domain mode (see `isSysthemaLocalePrefixedOnHost` above); it throws on a dedicated-domain locale, which always serves unprefixed. A subfolder must be a single non-empty URL-safe segment, unique among the configured locales, and cannot equal another locale's own code (ambiguous with that locale). Internal routing — cache keys, the catch-all's internal segments, revalidation, static params — always uses the locale **code**; `subfolder` changes only what a browser sees. `getSysthemaLocalizedPathname()`, `getSysthemaAlternates()`, and `getSysthemaPublicHref()` all resolve the subfolder automatically; `getSysthemaInternalLocalePathname()` is unaffected. Once a locale has a `subfolder`, its bare `/<code>/...` path no longer matches — the subfolder replaces, not aliases, the code in public URLs.

The locale contract is intentionally independent from Payload Admin translations. Configuring frontend locales never selects Admin languages, and vice versa. `@systhemaui/next` ships reviewed frontend UI catalogs for `en`, `hu`, `fr`, `nl`, `cs`, `sk`, and `ar`. Register complete project catalogs for other locale codes with `messages.catalogs`, or deliberately opt into English UI fallback with `messages.fallback: 'en'`. Project namespaces are preserved alongside the `Systhema` namespace for next-intl. See [Messages and catalogs](https://docs.systhema.app/sk/nextjs/locales/messages.md) for catalog helpers and consumer component integration.

## The Next.js integration

`@systhemaui/next` includes and owns `next-intl`; consumers neither install nor import it directly, and no request-config module (`getRequestConfig`/`i18n/request.ts`) exists or is needed. Systhema's own components read frontend copy through `useSysthemaFrontendTranslator` (from `@systhemaui/next/messages/client`, backed by `SysthemaProvider`), never next-intl's client hooks — so no `NextIntlClientProvider` is rendered, and `next-intl` is used only for its middleware (routing) and navigation helpers, neither of which needs a request-config module. A consumer using next-intl directly for their own namespaces sets up their own provider in their own subtree. Locale APIs are intentionally excluded from the package root: use `@systhemaui/next/locale`, `/locale/proxy`, `/messages`, `/messages/client`, and `/gateway` so ordinary component imports cannot pull locale code. Without a locale value, `next.config.ts` and the route tree stay unchanged, and the application does not import `next-intl`.

Internally, the package is organised around two seams: `@systhemaui/next/config` for build-time contributions and `@systhemaui/next/gateway` for edge-time contributions. Locales contribute a config enhancer (injecting the routing contract into the edge bundle) and an edge handler (locale negotiation and redirects) to those seams. A future Systhema feature extends the same two seams rather than asking a consumer to add another file.

Declare locales only in the canonical `systhema.config.ts`. Turning locales on or off touches no shared file — `src/proxy.ts` and the catch-all `page.tsx` are byte-identical whether locales are configured or not. Instead:

- `next.config.ts` is wrapped with `withSysthema(nextConfig)` (`@systhemaui/next/config`) — a general-purpose wrapper that runs every enabled Systhema feature's enhancer against the resolved `systhema.config.ts`; today that's locales, the Server Action origin allow-list and the [legacy-polyfill drop](https://docs.systhema.app/sk/concepts/browser-support.md#legacy-polyfills), and future integrations hook in the same way with no further `next.config.ts` change. Its optional second argument holds the settings that are not read from `systhema.config.ts` (currently `dropLegacyPolyfills`); every one has a working default, so the one-argument call stays correct. It is baseline infrastructure: fresh scaffolds (the Next.js and Payload templates) ship it by default, and it reads `systhema.config.ts` at build time and, when `locales` is set, injects the compact locale-routing contract as a build-time `env` constant so the edge middleware can read it without touching the filesystem. It also appends the host of `NEXT_PUBLIC_SERVER_URL` to `experimental.serverActions.allowedOrigins`: Next rejects a Server Action whose `origin` host differs from `x-forwarded-host`, and behind the Systhema Gateway's Cloudflare Tunnel that header carries the tunnel hostname, which breaks the Payload Admin's block forms (stock fields, no swatches, a visible `_tier` select). Consumer entries are kept and a listed host is not repeated. With nothing configured and the variable unset, it returns `nextConfig` unchanged — a true no-op. Enabling locales adds the wrapper only when a project doesn't already have it; disabling locales leaves it in place.
- `src/proxy.ts` still just calls `systhemaGateway({ customAdminURL })` from `@systhemaui/next/gateway` (Payload starters import it from `@systhemaui/payload/gateway`, which re-exports the same function). The gateway reads that injected env constant itself and enables locale routing automatically when present — you never pass `locales`/`localeMiddleware` in by hand. See [Gateway and status codes](https://docs.systhema.app/sk/nextjs/gateway.md).
- `SysthemaPage` (from `@systhemaui/payload/next`) resolves the active locale from the incoming route segments internally, using the same config, so the Payload catch-all `page.tsx` needs no locale-aware wrapper either.

The only file that changes shape is the owned site shell, which converts from `app/(site)/template.tsx` to `app/(site)/shell.tsx` in Payload starters (see [Generated files and migration](https://docs.systhema.app/sk/nextjs/locales/generated-files.md)) — still outside `(systhema)`, written once and then owned by the project: customize fonts, branding, and header/footer props there freely; sync never overwrites it. Everything inside `(systhema)` stays Systhema-managed and freely regenerable.

```ts title="systhema.config.ts"
import { defineSysthemaLocales, type SysthemaConfig } from '@systhemaui/core'

const config: SysthemaConfig = {
  locales: defineSysthemaLocales({ supported: ['en', 'hu'], default: 'en' }),
}

export default config
```

## Missing translations

The advanced `defineSysthemaLocales({ locales: {...} })` form's per-locale `missing` setting controls what a locale's URL serves when a page or post has no translation. The default (`'fallback'`) serves the default locale's content at that URL via Payload's display fallback, keeping the requested locale's UI strings and `<html dir>`; `'redirect'` 307s to the default locale's equivalent URL instead; and the contract-level `fallback: false` flips the default to `'notFound'` (today's strict 404) everywhere. Draft mode and Payload Preview/Live Preview always show the real 404 on a genuine miss. See [Advanced per-locale form](#advanced-per-locale-form) for the full semantics and helper, and [Content localization](https://docs.systhema.app/sk/payload/localization.md) for what an editor experiences. 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.

## Related

- [Public URL recipes](https://docs.systhema.app/sk/nextjs/locales/url-recipes.md)
- [Messages and catalogs](https://docs.systhema.app/sk/nextjs/locales/messages.md)
- [Multilingual sites](https://docs.systhema.app/sk/guides/multilingual.md)
