Тема
Войти Регистрация

I18N

_Status: being written in the current phase. See REWORK-STATE.md §3 for the authoritative progress list; this file is filled in as the corresponding stream lands._

R3 REOPENED — owner directive 2026-11-02 (ADR-073). The 2026-10-25

freeze is lifted: waves 8+ land the remaining locales, one per round, until

full coverage. The live set is web/i18n/_active.json (ADR-071) and grows

with every landing; the i18n gate target remains **full coverage = the

active set**. The landing recipe below is operative again. (History: the

set stood frozen at 49/66 from 2026-10-25 to 2026-11-02 — see

docs/P22-PLAN.md §R3 execution records.)

Coverage vs activation (ADR-071) — read this before landing a locale

Two different things have been conflated once already, and it cost a user-visible defect (s48-01). They are now separate, separately gated concepts.

Coverage is a build-time property of the catalogs: _platform.<code>.json covers every key of _base.en.json. php scripts/build_i18n.php --check reports it as full coverage: N/66 locales, and docs/I18N-STATUS.md is generated from the same computation. Coverage alone makes a locale translatable, not reachable.

Activation is a runtime property: can a visitor actually switch to it? The live set is

(SUPPORTED_LOCALES ∪ web/i18n/_active.json) − DISABLED_LOCALES

computed in exactly one place, Iya\Core\ActiveLocales, and read by every consumer that used to read the .env list on its own:

ConsumerWhat it gates
Http\Middleware\LocaleResolver::supports()?lang=, the lang cookie, the session value, POST /lang/<code>, /xx/ path prefixes, Accept-Language negotiation
Http\HostResolverlanguage subdomains (uz.abuseipdb.ru)
Web\View::languages()the language switcher AND the hreflang alternates
Web\Controllers\AccountController::updatePreferences()which locale a user may SAVE as a preference

web/i18n/_active.json is generated by scripts/build_i18n.php (never by hand) and lists, sorted, every locale at 100% platform coverage. It is deterministic — no timestamp — so --check byte-compares it and a stale committed copy reddens the i18n gate. A partial --locale= build neither rewrites nor is judged against it.

SUPPORTED_LOCALES is therefore an override, not the list:

  • add a code there to force-enable a locale before its catalog is complete

(early access, staged rollout);

  • add a code to DISABLED_LOCALES to force-disable a complete one without a

redeploy (translation quality incident, legal hold);

  • en is always live — it is the fallback catalog, and it survives even an

explicit disable;

  • .env.example must list every active locale (LocaleActivationTest asserts

parity in both directions), so a rebuilt environment activates the landed waves by construction.

ActiveLocales is fail-closed: a missing or corrupt _active.json yields an empty complete-set (i.e. the env list alone), never "every catalog on disk", and only bare lowercase [a-z]{2,3} codes are accepted from the artifact, so a hand-edited file cannot inject a route segment.

The switcher

partials/lang-switcher.php offers the live set as switchable POST /lang/<code> forms and renders every other catalog as a disabled entry (aria-disabled="true", a <span>, no form) under the existing alerts.kind_soon label. Offering a locale the resolver rejects is precisely what made s48-01 silent: the click looked like it worked, answered 303, and changed nothing. Note that a new key must not be added to _base.en.json for this or any other UI tweak — doing so resets full coverage for all 48 landed locales (RESILIENCE-PLAYBOOK §6.5 p.23); compose from existing keys.

Definition of delivered

A locale is delivered when **POST /lang/<code> sets the lang cookie AND the following GET / renders <html lang="<code>"> with its own catalog copy.** Nothing shorter counts. That is asserted three ways, because one of them once wasn't:

  • tests/unit/Core/LocaleActivationTest.php — the artifact equals an independent

recomputation from disk; every complete catalog is live even though the suite boots a 9-code SUPPORTED_LOCALES (the defect scenario, under test); the resolver agrees with ActiveLocales; .env.example parity both ways; the override works in both directions; corrupt/hostile artifacts fail closed.

  • tests/integration/HttpSmokeTest.php — the switcher offers exactly the

live set (no dead clicks, no dark locales), not-live catalogs are never form targets, every hreflang alternate is live, and each landed wave locale renders its own <html lang> and home.hero_title through ?lang=<code>.

  • var-deploy/s49_live_prod_check.py (agent tooling, staged) — the same contract

against the live prod over HTTPS, 34 checks, with a fresh cookie jar per locale, controls (de/en) and fail-closed probes (ca/hy/ta).

Landing a new locale — the activation part is now free

Follow ADR-070 (all-or-nothing per locale, one landing commit at 1784/1784). After php scripts/build_i18n.php the new code appears in _active.json, the gate line moves 48/66 → 49/66, the switcher starts offering it, uz-style subdomains resolve, and users can save it as a preference — with **no .env edit**. Still required: bump the single locale pin (tests/integration/WaitlistTest.php), run the targeted i18n tests, the control suite, and the live prod check after the deploy.

Версия 2.2.0 · AbuseIPDB.RU · webmaster@abuseipdb.ru · source: docs/I18N.md