Internationalization (i18n)#
Provides a zero-dependency translator, file-based catalogs, automatic locale resolution, and localization of framework-standard messages. It is opt-in via configuration: if you set none of the i18n-related environment variables, it stays single-locale (English) at no cost, and every framework string is byte-identical to when i18n is not used.
Configuration#
Controlled via environment variables (all optional):
APP_LOCALE=en # default / active locale (default: en)
APP_LOCALES=en,ja # supported locales (default: APP_LOCALE only)
LOCALE_COOKIE=locale # cookie name for the cookie source (default: locale)
LOCALE_PATH_PREFIX=true # opt-out of /{locale}/... routing (default: true)Translator and LocaleResolver are always registered in the container (autowire, public, Services & DI). However, locale switching (cookie / Accept-Language / path-prefix resolution) only takes effect when APP_LOCALES lists two or more locales. When it is unset (or single-locale), every request resolves to APP_LOCALE and no path rewriting happens at all — even a /en/* route works exactly as when i18n is not used. LOCALE_PATH_PREFIX=false merely disables /{locale}/... routing in a multi-locale app (cookie / Accept-Language stay active); it cannot enable prefix routing in a single-locale app (there is nothing to distinguish).
APP_LOCALE is the default active locale, not the framework's fallback. The built-in relayer.* messages always fall back to English (the complete bundled catalog), so an untranslated key in a framework string is never exposed as-is.
Locale resolution priority#
For each request, LocaleResolver determines the locale in the following priority order (top is highest):
- URL path prefix —
/{locale}/...when the first segment is a
supported locale. It is the only source that rewrites the path the router matches, and /ja/about and /about both reach the same src/Pages/about/page.psx (Routing & Pages).
- Session — consulted only when a session is already started.
Starting a session just to determine the locale would emit a Set-Cookie on every request and break the CDN cache for anonymous pages, so the resolver does not do that. In a logged-in flow that already has a session, the stored _locale is honored.
- Cookie —
LOCALE_COOKIE(CDN-safe, no session needed). Accept-Language— negotiated by q-value against the supported list.- Default —
APP_LOCALE.
Matching is by primary subtag (ja-JP matches a supported ja), and the resolved value is the canonical spelling from APP_LOCALES. The determined locale is also reflected in <html lang="…"> and can be obtained via $request->locale().
Defer fragments and path-prefix routing. The subrequest of
<X defer />is fetched at a root-absolute/_defer/{name}URL that does not include/{locale}(usePHP pins it to the root). So the parent page's path prefix is not propagated, and the fragment's locale is resolved from the cookie /Accept-Language/ default, not from the URL path. If you switch locale only via the/{locale}/…prefix and want defer fragments in the same language too, also setLOCALE_COOKIE(or rely onAccept-Language). The cookie is CDN-safe and is the intended carrier for this case.
Translating your own content#
Place a PHP catalog at <projectRoot>/translations/{locale}.php. It may be flat or nested, and is merged (overriding) into the framework's catalog:
// translations/ja.php
return [
'home.title' => 'ようこそ',
'cart.items' => '{count}点|{count}点', // pipe = plural
'user' => ['greeting' => 'こんにちは、{name}さん'],
];Translator can be injected into any page, layout, or service:
use Polidog\Relayer\I18n\Translator;
return static fn (Translator $t) => h('h1', [], $t->trans('home.title'));
// placeholder: $t->trans('user.greeting', ['name' => $name])
// plural: $t->transChoice('cart.items', $count)trans(string $key, array $params = [], ?string $locale = null)—
replaces {name} placeholders with $params.
transChoice(string $key, int $count, array $params = [], ?string $locale = null)—
selects a form from a pipe-separated one|other message using a simplified CLDR rule (English-style one/other; Japanese, Chinese, Korean, etc. are single-form).
- An unknown key falls back to the key itself (after placeholder
substitution) — visible but not fatal.
Localizing framework-standard messages#
Validation messages and HTTP error reason phrases (HTML error pages, and the JSON {"error": …} body of API Routes) are resolved in the relayer.* namespace of the same catalog, and en and ja are bundled. Because validation schemas are built outside the container, they reach the current translator via a process-wide ambient holder (Polidog\Relayer\I18n\Translators) that AppRouter sets on every request. A custom message passed to refine() / required('…') is always passed through as-is. CLI output (relayer …, CLI Commands) is English only.
Change history (1)
- Created