Files
atelier/domains/i18n/locale-resources.md
T
Jon Chery 9ebc9c8868 docs(milestone): complete v0.3 — GitOps+Operators/AI-ML/i18n/Compliance
---ci---
project: atelier
phase: 6
milestone: v0.3
status: complete
requirements:
  covered: [ATELIER-60..91]
  partial: []
---/ci---
2026-08-05 03:45:38 +00:00

138 lines
7.2 KiB
Markdown

# Locale Resources — Derived Rules
> Derives from `domains/i18n/first-principles.md`. Covers P1 (Source
> Language is a Locale), P2 (Locale Identifiers Standardized), P3
> (Resources External, Not Inline), P4 (Plural/Gender Parameterized),
> and P10 (Translation Reversible and Versioned). Referenced by
> `formatting.md` (strings the formatter resolves) and
> `rtl-bidi.md` (the `dir` the resource layer carries).
## Resources are the Boundary (P3 Resources are External, Not Inline)
- User-facing strings live in locale resource files, addressed by
key. Code references a key; the resource layer resolves the key to
the active locale. The source language is itself a locale
(`en-US`), not a fallback baked into code.
- String concatenation in code (`"Welcome, " + name + "!"`) is the
cardinal violation: it bakes in source-language word order and
breaks for every locale with different grammar. Replace every
concatenation with a parameterized message:
`t("welcome", { name })`.
- The resource is the single place a string lives. Editing a string
in code instead of the resource is a locality violation (C4): the
string and its locale consequences now live apart.
## Resource File Formats
| Format | Shape | When | Notes |
|--------|-------|------|-------|
| `.po` / `.pot` | gettext; msgid → msgstr, plural headers | Server-side, GNU ecosystem, PHP/Python/C | Mature tooling (`xgettext`, `msgmerge`); supports plural categories via header |
| JSON (flat or namespaced) | `{ "key": "value" }` per locale | JS/web, i18next, FormatJS | Simple, machine-readable, but no native plural support — wrap with ICU MessageFormat |
| Fluent `.ftl` | Mozilla FTL; asymmetric, resolver-driven | Browser-grade l10n, asymmetric translations | One message can resolve differently per locale without code changes; supports attributes, selectors |
| ICU Resource Bundle | ICU binary/text resources | ICU-native, JVM, C++ | Tightest integration with ICU formatting/CLDR; steeper tooling |
- None is advocated over the others. The choice is ecosystem fit,
not correctness. All four satisfy P3/P4 when used as the boundary.
- A custom format (a hand-rolled `.csv` of strings) is a violation:
it is unsupported by standard tooling, has no plural grammar, and
cannot compose with `formatting.md`'s ICU layer.
## Key Naming and Namespaces (P2 Locale Identifiers Standardized)
- Locale identifiers are BCP 47 tags (`en-US`, `ar-EG`, `zh-Hans-CN`).
No ad-hoc codes. The resource file is named for its locale:
`en-US.json`, `ar-EG.po`, `ftl/ar-EG/main.ftl`.
- Message keys are stable, semantic, and structured — not prose.
`checkout.cart.item_count` not `"You have 3 items in your cart"`.
A key that is the source string (`t("You have items")`) breaks the
moment the source copy is edited; the key must outlive the copy.
- Namespaces segment by surface (`checkout.*`, `errors.*`, `onboarding.*`)
so that a locale can be loaded incrementally and so that key
collisions across surfaces are impossible. A flat namespace with
thousands of keys is a C2 (Clarity) violation waiting to happen.
## Fallback Chains
- The fallback chain is explicit: requested locale → language-only
(`en` from `en-GB`) → default locale → key itself (last resort).
The default locale is declared once, not re-derived in every call
site.
- A missing key in the requested locale falling back silently to the
source locale is a P3 violation: the user is silently shown the
developer's locale, which is not the locale they asked for. Missing
keys must be observable (see `testing-i18n.md`).
- Fallback is a property of the resource layer, not of individual
components. A component that re-implements fallback is duplicating
a contract (C6 Composability violation).
## Plural and Gender in Resources (P4 Plural/Gender Parameterized)
- Plural variants live in the resource, selected by the formatter,
parameterized by the count. The code passes the count; the resource
carries the variants; the formatter picks the right one per the
locale's CLDR plural rules.
```
// JSON + ICU MessageFormat (FormatJS / i18next)
{
"cart.item_count": "{count, plural, one {# item} other {# items}}"
}
// ar-EG.json — six plural categories per CLDR
{
"cart.item_count": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}}"
}
```
- `if (n == 1)` branching in code is a violation regardless of
language. Arabic has six plural categories; Russian has three;
English has two. A two-branch `if` encodes exactly one locale's
rules and is wrong for every other.
## Extraction Tooling (P1, P3)
- Strings are extracted mechanically (e.g. `xgettext`, `i18next-
parser`, FormatJS babel plugin), not by hand-tagging. Mechanical
extraction produces a `.pot` template that translators work from;
the template is regenerated on every build.
- A string that cannot be extracted (built at runtime from
fragments) is a P3 violation: it is invisible to the pipeline. If
the extractor cannot see it, neither can the translator.
- The extracted template is versioned (`P10`): the diff between
templates is the change in translatable surface. A template that
is not committed is a contract that is not reviewable.
## Versioning and Rollback (P10 Translation Reversible and Versioned)
- Resource files are committed to git alongside code. A bad
translation is a `git revert` of the resource, not a hot-patch over
the translator's work. Every locale resource has history,
provenance (which translator / which service produced which
commit), and a rollback path.
- A locale resource that is generated by a translation service and
committed without review is a P10 violation: the resource is
versioned but the provenance is opaque. Review the diff the same
way you review a code diff.
## What Violates Locale-Resource Discipline
| Violation | Principle |
|-----------|-----------|
| `t("You have " + n + " items")` concatenation | P3 Resources are External, Not Inline |
| A custom `.csv` string store instead of a standard format | P3 Resources are External, Not Inline |
| Locale file named `english.json` not `en-US.json` | P2 Locale Identifiers are Standardized |
| `if (n == 1) { t("item") } else { t("items") }` in code | P4 Plural and Gender are Parameterized |
| A key equal to the source string (`t("Welcome back")`) | P2 / P10 — keys must outlive copy |
| Silent fallback to the source locale with no signal | P3 Resources are External, Not Inline |
| A runtime-built string the extractor cannot see | P3 Resources are External, Not Inline |
| Resource files committed by a bot with no human review | P10 Translation is Reversible and Versioned |
## Relationship to Other Domains
- `domains/uiux/copywriting.md` — copy lives in resources; UI
microcopy is the source content the resource layer carries.
- `domains/api/error-responses.md` — API error messages are locale-
resource keys resolved at the boundary, not inline strings.
- `domains/data/schema-design.md` — locale identifiers and resource
shapes are a data contract; treat them as schema.
- `domains/i18n/formatting.md` — the formatter resolves the
parameterized message this layer produces.