Status: implemented
Implemented — this change has shipped.
RFC 0001: Locale negotiation
Implemented 2026-09-01 at protocol 1.13.0 (§III.1a), exactly as revised below. Refinements recorded: the renderer's refusal is a typed
UnsupportedLocaleError(codeLOC-01, mirroring the validator);formatCountstays locale-agnostic (it never carried separators);short/medium/longdate styles remain Intl-based conveniences and are explicitly non-conformance surfaces; and two adjacent fixes rode along — the implementation-manifest schema's capability enum had drifted from theViewerCapabilitytype (missingintegrity,refinement,capability-verify) and the reference manifest never claimedcapability-verifyafter RFC 0011.
Revised 2026-09-01 against everything implemented since the April draft. The mechanism survives — the file declares its locale, readers support it or refuse, no silent fallback — with these changes: the error code becomes
LOC-01in a registeredLOCfamily (the draft'sBLOCKING-LOCALE-001predates RFC 0030's §III.6a family registry and reads like ablocking_flagscousin, which it is not); the display-only boundary is now normative and exhaustive — locale touches chat/summary/report renders and nothing else: canonical JSON content, the CSV interchange renders, UW Lite's canonical form, digests, receipts, and the calc engine all stay locale-free, andCalcEvaluationContext.locale(which the engine provably never reads) is pinned locale-invariant; non-en-USformatting MUST come from the curated rules registry, never runtimeIntl— ICU varies across runtimes and versions, and "deterministic per locale" cannot rest on whatever ICU shipped this morning (en-USkeeps its existing code path byte-identical, protecting every baseline); the conformance home is a namedconformance/locale/suite (thecapital-stack/lease-up/capabilityprecedent) rather than fixtures folded into tier-1; and both April unresolved questions are resolved — dates render per-locale in display, always ISO inside JSON, and currency-code disambiguation is explicitly deferred to a future RFC rather than smuggled in here.
Summary
v1 freezes every numeric and date format to en-US so that two conforming implementations cannot disagree about how 1234.5 or 2026-04-15 renders. This RFC proposes a minimal, additive locale-negotiation surface: a file declares the locale it was authored in (locale frontmatter, default en-US), an implementation declares the locales it supports (supported_locales in its manifest), and a reader facing a locale it does not support refuses (LOC-01) rather than silently falling back. Rendering remains deterministic per locale via a curated rules table; cross-locale conversion is explicitly out of scope.
Motivation
SupportedLocaleinprotocol.tsis'en-US'literally — the only allowed value;NumberFormatRules.locale/DateFormatRules.localeare therefore vestigial. The type was always the v2 hook (Protocol §XV lists this RFC as the future work that opens it).- Format spec Part III display rules are hard-coded to US conventions (
$,1,000.00). - Adopters in the EU, UK, and Asia must fork the renderer to get their locale's display rules — an entire class of adopters self-excluded from conformance.
The cost of getting locale wrong is non-determinism — the same file rendered by two conforming tools showing different numbers. Negotiation (declare, support-or-refuse) is what lets adopters opt in without breaking that.
Proposed change
The display-only boundary (normative)
locale governs human display renders only: chat, summary, report HTML/PDF, and display strings in view-model/badge surfaces. Everything machines read stays locale-free, exhaustively:
- Canonical JSON content — numbers are JSON numbers, dates ISO-8601, rates fractions. Unchanged by this RFC and by any locale.
- CSV renders (
formatPercentCsv,formatNumberCsv, the export views) — CSV is interchange, not display; separators stay canonical. - UW Lite — its canonical form is a byte-level contract (canonicalization + digests); Lite stays locale-frozen for 1.x.
- Digests, signatures, receipts — cover canonical bytes; untouched.
- The calc engine — locale-invariant.
CalcEvaluationContext.localeexists and the engine never reads it; this RFC widens the type and pins the behavior: evaluation MUST be identical whatever locale a context declares. A conformance case pins this.
Format spec
New frontmatter key and one normative rule in Part III:
locale: en-US # default when absent; one of the SupportedLocale valuesA conforming implementation MUST support
en-USand MAY support additional registered locales. When rendering a display surface for a file whoselocaleit does not support, an implementation MUST emitLOC-01(error) and MUST NOT fall back to a different locale — two readers disagreeing about what1.234,56means is the failure this rule exists to prevent. Parsing, validation, editing, and calc of such a file are unaffected: the content is canonical and locale-free.
Part III's number/date rules become a per-locale table; en-US keeps its current rules verbatim. First wave (each row fully stated in the spec, each with a conformance fixture): en-US, en-GB, de-DE, fr-FR, ja-JP, zh-CN. New locales land via small additive RFC amendments to the table.
Dates: display renders use the locale's convention (15.04.2026 for de-DE, 2026/04/15 for ja-JP); JSON content stays ISO-8601 always (April's open question, resolved as leaned).
Protocol spec
SupportedLocalewidens to the six-value union.ImplementationManifestgainssupported_locales?: SupportedLocale[](absent =['en-US'], so every existing manifest keeps meaning what it meant).implementation-manifest.schema.jsonupdated in lockstep.- New
LOCvalidator family registered in §III.6a (VALIDATOR_CODE_FAMILIES), owned byvalidate:
| Code | Severity | Trigger |
|---|---|---|
LOC-01 | error | The file declares a locale this implementation does not list in supported_locales (or an unregistered tag). Refusal to render displays, never to parse. |
ModuleManifest.requires_locales?: SupportedLocale[](additive, optional) for modules that hard-code locale-specific conventions.
Library
format-rules.ts(new) —BUILTIN_FORMAT_RULES, a curatedRecord<SupportedLocale, NumberFormatRules & DateFormatRules>registry: decimal separator, grouping separator, currency symbol + placement, percent spacing, date pattern. Formatting for non-en-USlocales is implemented from these rules directly — neverIntl/ICU — because runtime ICU differences are exactly the non-determinism this spec refuses.en-USkeeps its existing implementation and byte-identical output (every chat/summary baseline in the corpus stays green).format.ts—formatCurrency,formatPercent,formatRatio,formatCount,formatDate,formatValueacceptlocalein their existing options objects (additive; default'en-US'). CSV formatters deliberately do NOT.renderer.ts— chat/summary readfrontmatter.localeand thread it through display formatting.validator.ts—LOC-01when a declared locale is outside the implementation's support (the reference implementation supports the full first wave, so its ownLOC-01fires only on unregistered tags).- Round-trip: the
localekey is ordinary frontmatter — Tier-2 byte preservation already covers it; it is not immutable (changing it reflows displays, not content).
The change is additive — files without locale behave exactly as today. Protocol minor bump; format stays 1.1 (one new optional frontmatter key plus Part III's table refactor, additive at 1.x).
Compatibility analysis
- Existing
.uw.mdfiles — no breakage; absentlocale=en-US. - Tier-1 readers — an
en-US-only reader (the manifest default) works on every existing file and refuses display renders of ade-DEfile withLOC-01, which is the designed behavior. - Tier-2 editors — round-trip preservation already covers the key.
- Tier-3 calc hosts — no behavior change, now pinned by fixture.
- Tier-4 agent hosts — SHOULD surface the file's locale in agent context so generated narrative matches; agents still write canonical numbers.
- UW Lite / receipts / signing / envelope digests — untouched by construction (the display-only boundary).
- Modules —
requires_localesis additive and optional.
No deprecation path. Additive.
Conformance impact
New named suite conformance/locale/:
render-de-DE/— the shared deal rendered underde-DE: chat/summary baselines with1.234.567 €-style figures and15.04.2026dates.- One render fixture per remaining non-
en-USfirst-wave locale (same deal, per-locale baseline). unsupported-locale-refuses/—locale: xx-XX→LOC-01, and the document still parses, validates, and calcs.calc-locale-invariant/— identical calc results under every registered locale in the evaluation context.csv-stays-canonical/— the CSV render of thede-DEfile is byte-identical to theen-USfile's.
Existing fixtures pass unchanged (implicit en-US).
Reference implementation
packages/uwmd-core/src/protocol.ts— widenSupportedLocale,supported_localesonImplementationManifest,LOCfamily row,LOC-01remediation.packages/uwmd-core/src/format-rules.ts(new) —BUILTIN_FORMAT_RULES.packages/uwmd-core/src/format.ts—localein the options objects; registry-driven dispatch for non-en-US.packages/uwmd-core/src/renderer.ts— threadfrontmatter.locale.packages/uwmd-core/src/validator.ts—LOC-01.spec/UW_FORMAT_SPEC_v1.md— Part III per-locale table + the frontmatter key;spec/UW_PROTOCOL_v1.md— manifest + family registration;spec/schemas/implementation-manifest.schema.jsonin lockstep.- Protocol version: minor bump.
- Test plan: unit tests per locale per formatter kind from the registry rows; the render/refusal/invariance fixtures above.
Alternatives considered
- Per-section locale override (
localein_meta). Rejected — internally inconsistent files, renderer state complexity; the real use case is per-deal. - Runtime locale switching at render time. Rejected — breaks determinism; the file declares, readers support or refuse.
- Full ICU locale tag space (
SupportedLocale = string). Rejected — uncertifiable surface; the closed enum grows by small additive RFCs. - Runtime
Intlfor the new locales. Rejected in this revision — ICU output varies across Node versions and platforms, which would make conformance baselines flap. A curated table the spec states verbatim is small (six rows), auditable, and deterministic.
Deferred (explicitly not this RFC)
- Currency-code disambiguation — a deal in Mexico authored in
en-USwith peso amounts needs acurrency_codeon monetary values before any symbol logic can be honest. That is a data-model question, not a display-negotiation one; it gets its own RFC when an adopter needs it. Until thenen-USrenders$exactly as today. - Cross-locale conversion — out of scope by design, unchanged from the April draft.
- Localized validation/remediation message text — codes are the contract; message language stays English for 1.x.
Prior art
- CommonMark punts display entirely; not an option — determinism including display is the value proposition.
- JSON Schema
format: date— wire format defined, display punted; we do the same for storage and make display the negotiated surface. - ICU locale data — the source the curated rows are checked against, deliberately not a runtime dependency.
- HTTP
Accept-Language— the closest shape: negotiation, evaluated here at parse/render time rather than request time.