Files
atelier/domains/i18n/formatting.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

6.3 KiB
Raw Blame History

Formatting — Derived Rules

Derives from domains/i18n/first-principles.md. Covers P2 (Locale Identifiers Standardized), P4 (Plural/Gender Parameterized), and P5 (Formatting is Locale-Aware). Referenced by locale-resources.md (the formatter resolves the messages) and testing-i18n.md (the formatted output is what snapshots assert).

Formatting is Locale-Aware (P5 Formatting is Locale-Aware)

  • Dates, times, numbers, currencies, units, and relative time are formatted via ICU / CLDR / the JavaScript Intl API — never hand-rolled. CLDR is the source of truth for locale data; Intl is the runtime that exposes it.
  • A hand-rolled formatter encodes one locale's conventions and silently produces wrong output for every other locale. The canonical failure is date format: mm/dd/yyyy (US) vs dd/mm/yyyy (most of the world) vs yyyy-mm-dd (ISO, sortable). Picking one and calling it done is a correctness violation in every locale it is wrong for.

BCP 47 Tags Drive Formatting (P2 Locale Identifiers Standardized)

  • Every formatter takes a BCP 47 locale tag. The tag is the contract between the resource layer and the formatting layer: the same tag that selects the resource selects the formatter.
  • A locale tag that is not BCP 47 cannot be resolved by Intl, ICU, or CLDR — the formatter returns the runtime default, which is the developer's locale, not the user's. This is why P2 is a prerequisite of P5: you cannot format for a locale you cannot name.

The Intl Surface (ICU/CLDR in the Browser and Node)

API Formats Notes
Intl.DateTimeFormat Dates, times, date+time, time zones Calendar (buddhist, hebrew, islamic), numbering system (arab, hanidec) via locale tag extensions
Intl.NumberFormat Numbers, currencies, units, percent Notation (compact, scientific), grouping, sign display
Intl.RelativeTimeFormat "3 days ago", "in 2 months" Locale-specific phrasing; numeric vs auto
Intl.PluralRules Plural category for a count one, few, many, other, zero, two per CLDR — the engine ICU MessageFormat uses
Intl.ListFormat "a, b, and c" Conjunction / disjunction / unit lists, locale-specific separators
Intl.Collator Locale-aware string sorting Strength (base, accent, case); numeric collation
  • All of these are built on ICU/CLDR; they are the runtime baseline. Use them. A moment.js-style hand-rolled format string ("MM/DD/YYYY") is a relic of the pre-Intl era and a P5 violation in any locale-aware code path.

Dates and Times

// Correct — Intl, locale-aware
new Intl.DateTimeFormat("ar-EG", {
  dateStyle: "full",
  timeStyle: "short",
}).format(new Date());
// "الأربعاء، ٧ نوفمبر ٢٠٢٤، ٣:١٥ م"

// Wrong — hand-rolled, source-locale only
const d = new Date();
const s = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
// "11/7/2024" — meaningless in most locales
  • Time zones are not locales. A locale tells you how to format a timestamp; a time zone tells you what instant it refers to. Do not derive one from the other (ar-EG is not a time zone). Format with the user's locale; render in the user's time zone; store in UTC.

Numbers, Currencies, Units

new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" })
  .format(1234.56); // "1.234,56 €"

new Intl.NumberFormat("ar-EG", { style: "currency", currency: "EGP" })
  .format(1234.56); // "١٬٢٣٤٫٥٦ ج.م."

new Intl.NumberFormat("en-US", { style: "unit", unit: "kilometer-per-hour" })
  .format(100); // "100 km/h"
  • The currency code (EUR, EGP, USD) is ISO 4217; the locale determines the symbol, grouping, and placement. A hand-rolled "$" + amount is wrong for de-DE (symbol, grouping, placement all differ).

Plural Rules (P4 Plural/Gender Parameterized)

  • Intl.PluralRules returns the CLDR plural category for a count in a given locale. ICU MessageFormat uses this category to select the variant from the resource (locale-resources.md).

  • Never branch on the raw count in code. The count goes to the formatter; the formatter consults PluralRules for the locale; the resource carries the variant for that category.

    // ICU MessageFormat (FormatJS)
    new Intl.MessageFormat(
      "{count, plural, one {# item} other {# items}}",
      "en-US"
    ).format({ count: 1 }); // "1 item"
    
    // ar-EG — six categories; the code is identical, only the
    // resource differs.
    

Gender and Select

  • ICU MessageFormat also supports {gender, select, male {...} female {...} other {...}} for gendered agreement and {case, select, ...} for general disjunction. These live in the resource, not in code branches.
  • A switch (gender) in code that picks a string is the same violation as if (n == 1): it encodes one locale's grammar in code and breaks for every locale with different agreement rules.

What Violates Formatting Discipline

Violation Principle
getMonth() + 1 + "/" + getDay() hand-rolled date P5 Formatting is Locale-Aware
"$" + amount hand-rolled currency P5 Formatting is Locale-Aware
if (n === 1) "item" else "items" plural branch P4 Plural and Gender are Parameterized
moment("MM/DD/YYYY") format string in locale-aware code P5 Formatting is Locale-Aware
Deriving time zone from locale tag P2 Locale Identifiers are Standardized
A non-BCP-47 tag passed to Intl (silently falls back) P2 Locale Identifiers are Standardized
switch (gender) selecting strings in code P4 Plural and Gender are Parameterized
Storing timestamps in local time, not UTC P5 Formatting is Locale-Aware

Relationship to Other Domains

  • domains/api/error-responses.md — API error messages are formatted for the requesting locale; the error code is stable, the message is locale-formatted.
  • domains/data/schema-design.md — locale identifiers, currency codes, and time zones are data contracts; treat them as schema (en-US, EUR, UTC), not free text.
  • domains/i18n/locale-resources.md — the resource layer carries the parameterized messages this formatter resolves.
  • domains/testing/fixtures.md — formatted output per locale is the fixture; snapshot tests assert against it.