---
type: Axis
title: i18n (localization axis)
description: The internationalization/locale architecture axis — how the system adapts text + formats to locale (single-locale → translatable → full-i18n → per-tenant locale). A system-singleton axis whose commitment surface is the persistent-data schema (+ resource bundles). DEEP (RFC #75 5A.4 / P1 #851 → promoted 4.5.0a6 #856); worked example is the deliberately single-locale (English-only prose + swimlane UI) posture and the externalize-every-hardcoded-string climb it has chosen not to pay.
tags: [axis, architecture, i18n, l10n, locale, translation, rfc-75, deep]
timestamp: 2026-06-18T00:00:00Z
---

# I18n (localization axis)

> **DEEP axis (P1 #851 → 4.5.0a6 #856).** Full §6 depth with a worked example
> from the skill's own history — the skill is deliberately single-locale (skill
> prose, the swimlane UI, every operator-facing string is English-only, inline,
> never externalized into a bundle), and the worked example names exactly what
> the translatable climb would cost (hunting every hardcoded string across skill
> prose + the SolidJS UI) and why the single-operator-tool scope makes that cost
> unjustified.

## Concept

I18n is the **localization** axis (RFC #75 §3): how the system adapts text and
formats to a locale. The ladder runs single-locale (one hardcoded language +
formats) → translatable (strings are externalized into resource bundles; a
locale selects a bundle) → full-i18n (locale-aware formatting of dates, numbers,
plurals, collation, bidi text — not just string swap) → per-tenant/per-principal
locale (each tenant or principal carries its own locale, resolved per request).
Its **commitment surface is the persistent-data schema** (§4) — locale columns,
translated content, resource bundles — plus the externalized-strings refactor.
Its **scope is system-singleton** (§5).

## Why it is an axis (discriminator test — §2)

1. **Ordered tiers** — each rung adapts more of the system to locale.
2. **Cross-cutting bundle** — a rung dictates string externalization, the bundle
   format, locale-aware formatting calls, content schemas (translated columns),
   and the locale tests.
3. **Asymmetric migration cost** — going from single-locale to translatable
   means hunting every hardcoded string in the codebase; per-tenant locale means
   threading a locale through every request + storing it.
4. **Requirement-keyed selection** — falsifiable probes pick the rung.

## Schema view (the §6 entry — validated by `axis_catalog`)

```yaml
id: i18n
kind: architecture-axis
scope: system-singleton
selection:
  probes:
    - "Is one fixed language + format set acceptable (internal/single-market tool)? → single-locale."
    - "Must the UI be available in other languages (string translation)? → translatable."
    - "Must dates/numbers/plurals/collation/bidi adapt per locale, not just strings? → full-i18n."
    - "Must each tenant or principal see its OWN locale, resolved per request? → per-tenant locale."
    - "Are user-visible strings hardcoded in code? → you are at single-locale by accident; externalizing them is the climb."
composes_with:
  - "per-tenant locale = i18n × tenancy (or × principal)"
applies_when: []
tiers:
  - tier: single-locale
    commitment_delta:
      surface: persistent-data-schema
      delta: "one hardcoded language + number/date format throughout; strings live inline in code/templates."
    admin_tool: "none."
    enforcement:
      - "the single locale is consistent; no locale parameter exists."
    tests:
      - "output is in the one locale's language + formats."
    forecloses: "any other language/format without code edits."
    migration_cost_to_next: "high — externalize every user-visible string into resource bundles."
  - tier: translatable
    commitment_delta:
      surface: persistent-data-schema
      delta: "strings are externalized into per-locale resource bundles; a selected locale swaps the bundle. Formatting may still be fixed."
    admin_tool: "translation-bundle editor + missing-key report."
    enforcement:
      - "no user-visible string is hardcoded; every string resolves through a bundle key."
    tests:
      - "selecting a locale swaps all strings; a missing key is reported, not silently blank."
    forecloses: "inline strings; everything routes through bundles."
    migration_cost_to_next: "medium — make dates/numbers/plurals/collation locale-aware (not just strings)."
  - tier: full-i18n
    commitment_delta:
      surface: persistent-data-schema
      delta: "locale-aware formatting of dates, numbers, currency, plural rules, collation, and bidirectional text — the full locale model, not just string swap."
    admin_tool: "locale-formatting preview; plural-rule + collation config."
    enforcement:
      - "all formatting goes through locale-aware APIs; plurals/bidi/collation respect the active locale."
    tests:
      - "a date/number/plural/sort renders correctly across several locales including a bidi + non-Latin one."
    forecloses: "string-only localization; formatting is now locale-driven everywhere."
    migration_cost_to_next: "medium — resolve a distinct locale per tenant/principal per request."
  - tier: per-tenant-locale
    commitment_delta:
      surface: persistent-data-schema
      delta: "each tenant/principal carries a stored locale resolved per request; one deployment serves many locales simultaneously."
    admin_tool: "per-principal/tenant locale setting + override."
    enforcement:
      - "every request resolves the acting locale from the principal/tenant; output uses it end to end."
    tests:
      - "two concurrent requests from different-locale principals each get their own locale."
    forecloses: "a single process-wide locale."
    migration_cost_to_next: "n/a — top of the ladder."
examples:
  - "The skill sits at SINGLE-LOCALE, and unusually for this axis that is the *correct* rung, not a deficiency to fix. Every operator-facing surface — the skill's own prose tree (SKILL.md and its TOC children), the SolidJS swimlane UI's labels and the elapsed/journey copy, the MCP tool descriptions, and all CHANGELOG/docs text — is English-only and inline; no string has ever been pulled into a per-locale resource bundle, and there is no locale parameter anywhere in the request path. The de-escalation probe ('is one fixed language acceptable for a single-operator tool?') answers yes: the skill is a single-operator development instrument, so the audience is one developer who reads English, and the TRANSLATABLE climb's cost is concretely high here — it means hunting every hardcoded string across two codebases (Python skill prose + the rules_js/SolidJS UI bundle) and routing each through a bundle key, with a missing-key report to stop silent blanks. FULL-I18N (locale-aware date/number/plural/collation, which would touch the swimlane's elapsed-time and timestamp rendering) and PER-TENANT-LOCALE (resolving a locale per principal) are both unreachable-by-design because the principal axis is singular and the tenancy axis is single — there is no second principal or tenant to carry a distinct locale. This axis is the clearest case in the catalog of a deliberate, justified floor."
```

## Selection procedure (§8)

Bidirectional check; chosen once. Recorded in the system architecture record.

## Composition (§5)

I18n × tenancy (or × principal) = **per-tenant locale**. Rule authored in 5A.3.
