Normative description of the data the Intl implementation reads: where it comes from, the embedded format, and how to add a locale. A port must be able to reproduce the format from this text.
Sources and pins
| Input | Version | Pinned by |
|---|---|---|
CLDR JSON (cldr-48.0.0-json-full.zip) | 48.0 | SHA-256 in tools/locale-data/pin.txt |
CLDR common (cldr-common-48.zip: uca/allkeys_CLDR.txt, collation/*.xml) | 48 | SHA-256 and the official SHA-512 in pin.txt |
tzdata (tzdata2025b.tar.gz, only zone.tab) | 2025b | SHA-256 in pin.txt |
UCD files (grapheme, word, sentence break properties, emoji-data.txt, DerivedCoreProperties.txt, PropList.txt, Blocks.txt, DerivedGeneralCategory.txt) | 17.0.0 | SHA-256 in pin.txt |
CLDR 48.0 and Unicode 17.0 are what the reference Node releases (v22.22.1 and v24.22.1) report as process.versions.cldr / process.versions.unicode, so Intl output compared against them carries no data-version noise. Raw inputs are fetched by tools/locale-data/fetch.sh (checksum-verified, never committed). The licence for all of it is the Unicode License v3 (see THIRD-PARTY-NOTICES.md).
Regenerating
tools/locale-data/fetch.sh # -> $ONEECMA_LOCALE_DATA or ~/oneecma/locale-data dotnet run --project tools/LocaleDataGen -- \ --cldr <root>/cldr/48.0 --ucd <root>/ucd/17.0.0 --tzdata <dir holding zone.tab> \ --out src/RecursiveAI.OneEcma/Intl/Data/Generated
The output is a pure function of the pinned inputs: two runs give identical bytes (checked with diff -r). The .bin files are committed and embedded in the engine assembly as manifest resources named oneecma-data/<file>.
Files
| File | Content | Bytes |
|---|---|---|
supplemental.bin | tree (collation tailoring rule text also covers the root search collation): likely subtags, aliases, parent locales, default content, currency data (digits, regions), week data, time data, calendar preferences, numbering systems, metazone mapping, plural ranges, day period rules, territory containment, right-to-left scripts, BCP 47 keyword data (calendar, collation, currency, measure, number, timezone including IANA aliases), collation tailoring rule text, list of stored locales | 373,342 |
locale-<id>.bin | tree per stored locale, delta against its parent | und 31,520; en 148,874; en-001 15,736; en-GB 4,192; es 125,352; es-419 17,439; es-US 13,955; fr 133,642; de 130,050; pt 125,568 |
plurals.bin | cardinal and ordinal plural rule ASTs for every CLDR language | 6,968 |
collation.bin | CLDR root collation table, contractions, implicit-weight ranges, decimal digit zeros | 282,838 |
breaks.bin | UAX #29 property ranges | 17,607 |
normalization.bin | UAX #15 combining classes, one-step decomposition mappings, composition exclusions | 35,464 |
Total 1,445,121 bytes. The largest parts are likely subtags (222 KB of the supplemental tree), the root collation table (283 KB), and per language the units (about 35 KB) and currency names (about 31 KB). en-US is CLDR default content of en and pt-BR of pt, so they have no blob of their own; en-GB, es-US store only their differences.
Container
All integers are little endian. A varint is unsigned LEB128, at most 5 bytes. Every blob starts with 8 bytes: OEDT, format version (1), kind (1 tree, 2 plurals, 3 collation, 4 breaks, 5 normalization), two zero bytes. A reader rejects any other version.
Tree blobs (kind 1)
After the 8 byte header, five u32: stringCount, tableOffset, dataOffset, nodesOffset, rootOffset (the last relative to nodesOffset; the others absolute). Then:
- String table at
tableOffset:stringCount + 1u32 offsets into the string data (the last is the data length). AtdataOffset: the UTF-8 strings, sorted by byte order, so a reader binary-searches a key without decoding. Keys and values share the table. - Nodes from
nodesOffset. A node starts with a varint(count << 2) | tag. Tag 2 is a string andcountis its string id. Tag 0 is an object:countkey ids (varints, ascending) thencountchild offsets (varints, relative tonodesOffset). Tag 1 is an array:countchild offsets. Children precede their parent and identical subtrees are stored once, so offsets may be shared. - JSON numbers and booleans are stored as their text; JSON objects keep their keys, and properties that only carry prose (
_description,_since,_reason,_iso4217,_comment,_references) are dropped.
Inheritance. A locale blob holds only members whose value differs from its parent's resolved data (CLDR JSON full packages are already resolved against the locale's own parents, so the generator diffs resolved against resolved). Each locale blob has a member @parent naming its parent locale (empty for und). Parents follow CLDR parentLocales, else drop the last subtag: en-GB → en-001 → en → und; es-US → es-419 → es → und. A lookup walks the chain from the most specific blob until some blob has the path. Arrays and strings are replaced whole, never merged. The value ∅∅∅ is CLDR's explicit "nothing" marker and is stored as is.
Paths. Locale trees have the members numbers (symbols, decimal/percent/scientific/currency/rational formats, compact patterns; Latin digits only), currencies, gregorian (months, days, quarters, day periods, eras, date/time/date-time formats, availableFormats, appendItems, intervalFormats), dateFields (fields and relative-time patterns), timeZoneNames (zone, metazone and region formats and names), languages, scripts, territories, variants, displayNames (keys, types such as calendars, code patterns, locale display pattern), listPatterns, layout, units (all CLDR units in long/short/narrow plus duration unit patterns, which also serve DurationFormat). The members below each keep their CLDR JSON names and structure.
Plurals (kind 2)
Two sections, cardinal then ordinal. Each: varint language count, then per language (sorted): varint-length UTF-8 id, a byte rule count, and per rule a byte category (0 zero, 1 one, 2 two, 3 few, 4 many, 5 other; other is implied and never stored), a varint AST byte length and the AST. The AST of a rule (UTS #35 Part 3): varint or count; per or term a varint and count; per relation a byte operand (0 n, 1 i, 2 v, 3 w, 4 f, 5 t, 6 c, 7 e), a varint modulus (0 none), a byte negated flag (!=), a varint range count and per range varint low and high (equal for a single value). Rules are tried in category order. Only = and != relations exist in CLDR 48; the generator fails on anything else. Samples are dropped.
Collation (kind 3)
The CLDR root collation (allkeys_CLDR.txt, UCA 17.0.0, default alternate handling non-ignorable). Layout: varint-length version string; implicit classes (varint count; each u16 base, byte shift, byte mode, varint origin); implicit runs (varint count; each varint gap since previous end, varint length-1, byte class; class 0 covers every code point not in a run); single-code-point entries (varint count; each varint code point delta, byte CE count, CEs); contractions (varint count; each byte key length, varint code points, byte CE count, CEs). A CE is varint primary, varint secondary, byte tertiary with bit 7 set for variable (*) elements. After the contractions come the decimal digits for numeric collation: varint count, then varint code point deltas of each digit zero (every general category Nd run is ten digits long, so only its zero is stored). A reader of an older blob that ends after the contractions has no digits.
Implicit weights (UTS #10 §10.1.3, Table 16) for code points without an entry: AAAA = base + (cp >> shift) (shift 0 means no addition); BBBB = ((mode 0 ? cp - origin : cp & 0x7FFF) | 0x8000); the pair is [AAAA.0020.0002][BBBB.0000.0000]. Classes: Tangut and Tangut Supplement (FB00), Tangut Components and Supplement (FB01), Nushu (FB02), Khitan Small Script (FB03) (assigned code points only), core Han = Unified_Ideograph in the CJK Unified Ideographs or CJK Compatibility Ideographs blocks (FB40 + cp>>15), other Unified_Ideograph (FB80 + cp>>15), everything else (FBC0 + cp>>15).
The reader's MatchLongest expects NFD text and matches contiguous contractions only; discontiguous contraction matching, variable weighting options and the Collator options (caseFirst, numeric, sensitivity, ...) belong to the Collator (docs/text-services.md).
Tailoring. The root table is the default collation of en, fr, de and pt. Spanish (standard) adds &N<ñ<<<Ñ. The rule text of every non-alternate, non-draft <collation> of en, es, fr, de and pt, and the search collation of the root (for example es traditional, de phonebook) is stored in supplemental.bin under collationTailoring/<language>/<type> exactly as CLDR writes it (decomposed, comments removed). The Collator applies it with the UTS #35 rule syntax; the data layer does not.
Deviation. CLDR orders Han in the root by radical-stroke ([radical ...] data in FractionalUCA). The shipped table has the UCA code point order for Han (the implicit weights above). The Node differential must exclude Han ordering until that is added.
Normalization (kind 5)
After the header: varint range count and per range varint gap since the previous end, varint length-1, byte canonical combining class (only non-zero classes; code points not listed have class 0); varint mapping count and per mapping: varint code point delta, byte flags (bit 0: compatibility mapping, bit 1: Full_Composition_Exclusion), byte length, varint code points. Mappings are the single-step values of UnicodeData.txt field 5 with the <tag> removed; Hangul syllables are left to the algorithm of Unicode section 3.12. The reader expands every mapping recursively, once, when it loads, and derives the primary composites: canonical mappings of two code points that are not composition-excluded.
Breaks (kind 4)
Varint property count; per property: UTF-8 name (Grapheme_Cluster_Break, Word_Break, Sentence_Break, Extended_Pictographic, Indic_Conjunct_Break), varint value count and the value names (index 0 is the default for unlisted code points), varint range count and per range: varint gap since the previous end, varint length-1, byte value index. Adjacent ranges with the same value are merged.
Time zone data
Date-time formatting needs the display names of time zones and the country of each zone. supplemental.bin carries, besides the CLDR metazone mapping:
zoneRegions: CLDR canonical zone id to the ISO 3166 region of thezone.tabline that names it or one of its aliases (tzdata 2025b, public domain;fetch.shextractszone.tabfrom the pinned archive).windowsZones: IANA zone id to Windows zone id (every id in the type lists of CLDR 48windowsZones.json; the first mapZone naming an id wins). A host on Windows without ICU only knows zones by Windows id, so the BCL time-zone source retries an unknown IANA id through this table (an alias goes through its canonical id first). An id that neither lookup resolves is unknown: Intl throws a RangeError and Date local time throws one too, never reading it as UTC.primaryZones: region to the zone CLDR names as that region's primary zone (a zone whose location name is the bare country name).metaZoneInfo: per zone, a list of periodsusesMetazonewith_mzoneand optional_fromand_to(yyyy-MM-dd HH:mm, UTC; a missing_fromis 1970-01-01).metaZoneMap: the reference ("golden") zone of each metazone per territory,001for the world.
The data holds no offsets: the services ask an ITimeZoneSource for the UTC offset and daylight flag at an instant (see docs/intl.md).
Empty markers. A child locale that blocks a name its parent has (CLDR spells this with the inheritance marker, e.g. en-GB short names that are absent from en-001 although present in en) is stored as an explicit empty string inside timeZoneNames, so that the delta against the parent does not make the parent's name reappear. A reader treats an empty string as "no name" and falls back to the localized GMT format.
Adding a locale
- Add its id (and any intermediate parent the CLDR chain needs) to
Localesintools/LocaleDataGen/Program.cs. The id must exist in the CLDR JSONfullpackages; region locales whose data is the language's default content need no entry. - For a Latin-script locale with a different collation, add its language to
CollationLanguages; for plural data nothing is needed (all languages ship). - Add the tag to
CldrData.SupportedLocales, run the generator, and add probes totests/OneEcma.Tests/LocaleDataTests.cswith expected values from the reference Node.
Another numbering system or calendar is a generator change: DropNonLatinNumberSystems and the ca-* sources.
What is not in the data
- Territory-to-time-zone lists (
Intl.Locale.prototype.getTimeZones) need the tz database, which CLDR does not carry; the IANA zone names and aliases come from the BCP 47 timezone data (_iana,_alias), not from a tz release. - Language matching data (
languageMatching), unit conversion and preference data, person names, transforms, annotations and non-Gregorian calendars.
Reader
RecursiveAI.OneEcma.Intl.Data (internal): CldrData (entry point; GetLocale, TryResolveLocale, Supplemental, Plurals, Collation, Breaks), DataTree (path lookups across the inheritance chain), TreeBlob, PluralData / PluralOperands, CollationData, BreakData, NormalizationData. Loading is lazy and cached; every object is immutable and thread-safe. No reflection other than reading the manifest resource.