Unicode Technology Workshop 2026 · Nancy, France

ICU4X in Practice:
Lightweight and Portable i18n

Shane Carr — Chair, Unicode ICU4X-TC
October 20–23, 2026 · Ensad Nancy / ANRT

ICU4X Logo

About the Speaker

Shane Carr

Shane Carr (@sffc)

  • Chair, Unicode ICU4X Technical Committee (ICU4X-TC)
  • Convener, Ecma TC39-TG2 (JavaScript Intl / ECMA-402)
  • Internationalization Engineering @ Google

Follow along online:
Slides & Tutorials: icu4x.unicode.org

Today's Session (~90 Minutes)

1. What Is i18n? What Is ICU4X?

~20 Minutes

  • The i18n stack & real-world i18n problems
  • Why scaling i18n across devices & languages is hard
  • ICU4X architecture, clients, and benchmarks

2. Hands-On Tutorial

~40 Minutes

  • Coding Exercise (~25 min): Build your first app in Rust or JavaScript
  • Data Pipeline Walkthrough (~15 min): Custom language packs and data slicing

3. What's New in ICU4X

~20 Minutes

  • ICU4X 2.2 & 2.3 releases
  • Chrome & Firefox Temporal
  • New Unicode-driven segmenter & ML models
  • Tiered locale display names, calendars, date ranges & currency

Part 1: What Is i18n?

Internationalization in the wild and across the software stack

I18N = Internationalization

I N T E R N A T I O N A L I Z A T I O N
⬇
I 18 N

Designing software so it can support users in any language, script, region, and cultural convention.

i18n in the Wild

YouTube in Spanish
Spanish: 1,9 M likes · 346 M de visualizaciones · hace 4 meses
YouTube in Japanese
Japanese: 191万 likes · 3.4億 回視聴 · 4 か月前

Examples of i18n Operations (October 20, 2026)

Today's Date Across Locales & Calendars

Locale / Calendar Formatted Output
en-US (Gregorian) Oct 20, 2026
fr-FR (Gregorian) 20 oct. 2026
he-u-ca-hebrew ט׳ בחשוון תשפ״ז (9 Heshvan 5787)
ar-SA-u-ca-islamic-umalqura ٩ جمادى الأولى ١٤٤٨ هـ
zh-u-ca-chinese 2026丙午年九月十一
ja-u-ca-japanese 令和8年10月20日

Date Ranges (UTW 2026: Oct 20–23)

  • en-US: Oct 20 – 23, 2026
  • fr-FR: 20–23 oct. 2026
  • ja-JP: 2026/10/20~2026/10/23

Locale Display Names (fr-CA)

  • en: Canadian French (Dialect) / French (Canada)
  • fr: français canadien
  • zh-Hant: 法文(加拿大)

Word Segmentation (No Spaces Between Words)

  • Japanese: 国際|化|ライブラリ|を|使う
  • Thai: การ|ทำให้|เป็น|สากล

More Problems that i18n Libraries Solve

  • BCP-47 Locale Manipulation & Likely Subtags
    pl-u-ca-gregory-hc-h12 · zh-TW → zh-Hant-TW
  • Number, Currency & Unit Formatting
    CHF 12'345.67 (de-CH) · ¥10万 (ja-JP) · 5 km / 3.1 mi
  • Date, Time & Time Zone Formatting
    令和8年10月20日火曜日 14:30 中央ヨーロッパ夏時間
  • Date Range & Relative Time Formatting
    20–23 oct. 2026 · in 3 days / dans 3 jours
  • Locale-Sensitive Collation (Sorting)
    ä > z in Swedish (sv), ä < b in German (de)
  • Plural Rules & List Formatting
    zero, one, two, few, many, other · A, B, or C
  • Display Names of Languages, Scripts & Regions
    Simplified Mandarin Chinese · United Kingdom / UK
  • Text Segmentation (Grapheme, Word, Line, Sentence)
    ユニコード|は|、|符号|化|文字|集合

The i18n Stack

  • Unicode
    Unicode: Foundational algorithms, specifications (UAX #14, UAX #29, UTS #35, …), character set & properties
  • CLDR
    CLDR: Locale data for hundreds of languages, plus specs for LDML, MessageFormat, person names
  • ICU
    ICU: Libraries that implement the standard algorithms in Unicode with the locale data in CLDR
  • Platform API
    Platform API: Making i18n easy to use from applications (e.g., JavaScript Intl / ECMA-402, android.icu)
  • Application
    Application: Localized user experiences built on top of the platform API or directly on the i18n library

Challenges with Scaling i18n

Why traditional monolithic i18n architectures hit a wall

Challenge 1: Quadratic Growth of Locale Data

  • Every CLDR release adds more locales and deeper coverage.
  • Total locale data grows quadratically:
    Locales × Components
  • Monolithic data bundles grow significantly every release.
CLDR Growth Chart
Each line is one CLDR release: locales ranked by coverage (horizontal) × coverage % (vertical)

Challenge 2: Bundling Every Language Doesn't Scale

  • Language coverage is essential for digital inclusion.
  • Yet statically bundling 1,000s of languages into every app binary doesn't scale.
  • 10 KB/language × 1,000 languages = +10 MB per download.
Cumulative installs by number of default languages
Past ~70 bundled languages, the extra download size costs more installs than the new languages gain

Challenge 3: Devices Have Different Requirements

Wearables

Wearables & MCU

Kilobytes of RAM/Flash, no full OS, battery-critical

Smartphones

Mobile Apps

Strict app download size & startup latency budgets

Laptops & Web

Web & Desktop

Download bundle size & cross-platform consistency

Data Centers

Data Centers

High concurrency, low memory per request, 100s of locales

Challenge 4: New Programming Languages Every Year

PYPL Programming Languages
Source: PYPL PopularitY of Programming Language index (pypl.github.io)
  • Maintaining ICU4C (C/C++) and ICU4J (Java) in sync for 25+ years already takes immense effort.
  • Rust, Dart/Flutter, JS/WASM, Swift, Kotlin, Python, Go need idiomatic APIs without rewriting Unicode & CLDR from scratch.

Challenge 5: Privacy & On-Device Processing

Why not "i18n-as-a-service"? Because i18n touches every keystroke:
  • 🔒 User privacy: Messages, contacts, and queries never leave the device.
  • ⚡ 60fps UI loop: Every scroll, resize, or keystroke segments and measures text.
  • ✈️ Offline-first: Instant response times with zero network round-trips.

Imagine sending something like this to a central service:

中|ワ|況|写|イノナ|開|億|が|ゃ|へ|者|43|俳|寺|式|7|沢|暮|ル|材|年|る|あん|移|酔|むぎ|え|す|写|情|主|逃|69|引|ぎ|う|ぱ|何|81|昇|フネイマ|本|因|ぱ|不|真|え|断|候|ら|。|吉|群|コクチ|賀|伝|ツエ|別|写|ユリキ|施|見|ら|力|1|取|目|れ|づ|ぱ|ぞ|然|野|テ|倍|展|ぴ|る|て|け|京|答|週|ネコ|入|整|料|勉|ら|げ|。

Wish List for a Scalable i18n Library

1. Pay Only for What You Use

Dead-code & data elimination for unused components

2. Add Extra Locales on Demand

Dynamic language packs from disk or over the network

3. Run on All Types of Devices

From no_std wearables & MCUs to browsers and cloud servers

4. Write Once, Use Across Languages

One shared implementation across ecosystems via zero-cost FFI

5. 100% On-Device Processing

Fast startup, tiny memory footprint, private, and offline-ready

Enter ICU4X

Modular, portable, and memory-safe internationalization

What Is ICU4X?

Portable by Design
no_std to cloud, with C, C++, JS/WASM, and Dart bindings via Diplomat.

Lightweight
Compile-time dead-code elimination & data slicing (50–90% smaller than ICU4C).

Secure & Fast
Memory-safe Rust with zero-copy data (ZeroVec) and zero-alloc formatting (Writeable).

ICU4X Logo

Who Are We? (ICU4X-TC)

Unicode Consortium

Unicode Consortium

Official Technical Committee (ICU4X-TC) under the Unicode Consortium

Google Mozilla

Industry Backing

Founded and actively developed by engineers at Google and Mozilla

🌍

Open-Source Community

40+ contributors in the past year across industry, open source, and GSoC students

The Journey to ICU4X (2019–2026)

2019

Problems identified. Multiple solutions prototyped.

2020

Started work in Unicode. Early data provider & zero-copy infra.

2022

ICU4X 1.0 released with stable Rust & FFI guarantees.

2023

Shipped in Firefox (icu_segmenter) & Pixel Watch (no_std).

2025

ICU4X 2.0 released; shipped in Android & Firefox Temporal.

2026

ICU4X 2.2 & 2.3 released; ECMA-262 Temporal ships in Chrome!
6 Years at Unicode! 🎉

Example ICU4X Clients (1/3)

CanvasKit

CanvasKit

via WebAssembly

Pixel Watch

Google Pixel Watch

via no_std Rust

Data Centers

Data Centers

via Rust & C++

More ICU4X Clients (2/3)

Firefox

Firefox

Text Segmentation (icu_segmenter) & Temporal Calendars (icu_calendar)

Chrome

Chrome (V8)

ECMA-262 Temporal (temporal_rs + icu_calendar + zoneinfo64)

Android

Android & Gboard

Bionic C Library & Gboard Case Mapping (icu_casemap, 82 KiB static)

Rustc

Rust Compiler

rustc diagnostic lists & locale fallback (icu_list & icu_locale_core)

ICU4X Across the Open-Source Ecosystem (3/3)

Text Shaping, Layout & UI

  • HarfRust — Pure-Rust HarfBuzz
  • Parley — Powers Slint, Dioxus, Bevy
  • Typst — Typesetting & bibliography
  • Servo — Browser layout engine

Engines, Compilers & AI

  • Ruff & ty — Python linter & type checker
  • OpenAI Codex CLI — Token & number formatting
  • Boa, Kiesel & Nova — JS engines (Intl & Temporal)
  • text-splitter — LLM/RAG text chunking

App Frameworks & SDKs

  • Dart & Flutter (intl4x) — Official pub.dev SDK
  • leptos_i18n — Rust SSR/WASM web apps
  • resvg / usvg — Pure-Rust SVG <text>
Ubiquitous via Rust url & idna (UTS #46 — 100M+ monthly downloads):
Pydantic v2 · uv · Deno · Reqwest · Next.js / SWC · Cloudflare quiche · Wasmtime · rust-analyzer · Zed · Tauri

ICU4X in Many Programming Languages

Rust App
Direct Crate Dependency
Dart / Flutter App
Dart Native Assets (record_use)
C++ App
Zero-Cost C++ Headers (RAII)
JavaScript / TS App
WebAssembly + TS Wrappers
No FFI layer:
plain Cargo dependency
Diplomat Code Generator & ICU4X C ABI (icu_capi) — Single source of truth for all FFI bindings
ICU4X Core Rust Crates
icu_datetime · icu_calendar · icu_segmenter · icu_locale · icu_collator · icu_decimal · icu_list · icu_plurals · icu_properties · …

Benchmarks: ICU4X is Fast and Tiny

DateTimeFormatter Performance
100k Medium Dates (sec, lower is better)
2.0s 1.5s 1.0s 0.5s 0.0s 1.83s ICU4C 76 0.69s ICU4X 2.0 Blob 0.20s ICU4X 2.0 Baked
2.6×–9.1× faster than ICU4C
DateTimeFormatter Heap Allocations
100k Medium Dates (total bytes allocated, log scale!)
1E+10 1E+9 1E+8 1E+7 1E+6 Notice the logarithmic scale! 3.86 GB ICU4C 76 2.40 MB ICU4X 2.0 Blob 2.40 MB ICU4X 2.0 Baked
~1,600× less heap memory allocated
Basic Number Format: Code Size
Compiled Code Size (kB, lower is better)
800 600 400 200 0 781.8 kB ICU4C 26.7 kB ICU4X
96.6% smaller code footprint (~29×)

Part 2A: Hands-On Tutorial

Building Your First App Using ICU4X (~25 Minutes)

How This Tutorial Will Work

💻 Get Your Laptops Out!

You can complete the hands-on exercise in either:

  1. Rust — Local command-line application (cargo new)
  2. JavaScript — Browser UI in CodePen (zero local setup required!)

🔗 Open the Tutorial Page

icu4x.unicode.org/2_3/tutorials/date-picker

  • Step-by-step instructions for both languages, section by section
  • Each section has a Rust part and a JavaScript part — follow whichever you chose

What We Will Build & APIs We Will Use

🦀 In Rust

A CLI app that takes a locale and formats today's date & time

  • APIs:
    • icu::locale::Locale
    • icu::datetime::DateTimeFormatter
    • icu::datetime::fieldsets::YMD & YMDT
    • icu::calendar::Date, icu::time::{DateTime, Time}
  • Docs: docs.rs/icu/2.3

🌐 In JavaScript

A web UI with a date/time picker and interactive locale selector

Hands-On Coding Exercise (~25 Minutes)

Checkpoints

  1. Set up your project (Rust cargo add icu@2.3 or open the JS CodePen template)
  2. Parse & canonicalize a Locale
    Try DE-CH or ES-419 → watch it normalize to de-CH and es-419!
  3. Format a Date with YMD::medium()
    Test en, en-GB, fr, th, ja-u-ca-japanese, en-u-ca-hebrew
  4. Upgrade to DateTime (YMDT)

Tutorial Part 1: Recap

1. Portable Ergonomics

Consistent, type-safe APIs across Rust, JS/WASM, C++, and Dart.

2. Standards Compliant

BCP-47 canonicalization, semantic skeletons (YMD, YMDT), and Unicode extensions (-u-ca-hebrew).

3. Ready-to-Use Data

Ships with compiled_data covering hundreds of locales out of the box.

Part 2B: Managing Data in ICU4X

Walkthrough & Demo: Custom Data Packs and Static Slicing (~15 Minutes)

Why Custom Data? The Chakma (ccp) Example

  • Try passing ccp (Chakma, spoken in Bangladesh and India) into DateTimeFormatter:
    • Input locale: ccp
    • Output: 2026 M10 20 (Root fallback!)
  • Why? Default compiled_data includes hundreds of high-coverage locales, not every long-tail locale.
  • Walkthrough: icu4x.unicode.org/2_3/tutorials/date-picker-data

What We'll Walk Through Next:

  1. DataProvider & DataMarker under the hood
  2. Generating ccp.blob with icu4x-datagen
  3. Loading ccp.blob dynamically at runtime
  4. Shrinking ccp.blob: 5.4 MB → 13.7 KB → 4.7 KB!

The DataProvider Trait

  • DataProvider<M: DataMarker> underlies all ICU4X data loading.
  • Each DataMarker is one atomic unit of data (e.g., DatetimeNamesMonthGregorianV1).
  • Every _unstable constructor declares its exact required markers as Rust trait bounds:
// impl<FSet> DateTimeFormatter<FSet>
pub fn try_new_unstable<P>(
    provider: &P,
    prefs: DateTimeFormatterPreferences,
    field_set: FSet,
) -> Result<Self, DateTimeFormatterLoadError>
where
    P: DataProvider<DatetimeNamesMonthGregorianV1>
     + DataProvider<DatetimePatternsDateGregorianV1>
     + DataProvider<DecimalSymbolsV1>
     + /* ... */;
Unused data markers are statically dead-code eliminated by the compiler!

Concrete Data Providers & Adapters

Concrete Providers

  • Baked (icu_provider_baked): Compiled into Rust const tables; zero deserialization cost.
  • Blob (icu_provider_blob): Portable binary pack loaded from files, HTTP, or memory.

Provider Adapters (icu_provider_adapters)

  • Locale Fallback: Parent inheritance (fr-CA → fr → und)
  • Forking Adapter: Try Baked defaults first, fall back to ccp.blob!
DateTimeFormatter
LocaleFallbackProvider
fr-CA → fr → und
ForkByMarkerProvider
try A, then B
Baked
compiled_data
Blob
ccp.blob

Step 1: Generating a Data Pack with icu4x-datagen

  • Install the CLI tool (or use icu_provider_export in build.rs):
cargo install icu4x-datagen
  • Export CLDR data for Chakma (ccp) into a binary blob:
icu4x-datagen \
  --markers all \
  --locales ccp \
  --format blob \
  --out ccp.blob

Step 2: Loading ccp.blob at Runtime

In Rust (try_new_with_buffer_provider)

// cargo add icu --features serde
// cargo add icu_provider_blob --features alloc
use icu_provider_blob::BlobDataProvider;

let blob = std::fs::read("ccp.blob")?;
let provider =
    BlobDataProvider::try_new_from_blob(
        blob.into_boxed_slice()
    )?;

let dtf = DateTimeFormatter::try_new_with_buffer_provider(
    &provider,
    locale!("ccp").into(),
    YMD::medium(),
)?;

In JavaScript (DataProvider.fromByteSlice)

import { DataProvider, DateFormatter, Locale } from "icu";

const response = await fetch("./ccp.blob");
const bytes = new Uint8Array(
  await response.arrayBuffer()
);
const provider = DataProvider.fromByteSlice(bytes);

const df = DateFormatter.createYmdWithProvider(
  provider,
  Locale.fromString("ccp"),
  /* options */
);
Output for ccp: 𑄸𑄶 𑄃𑄧𑄇𑄴𑄑𑄮𑄝𑄧𑄢𑄴, 𑄸𑄶𑄸𑄼 (Native Chakma month name & digits!)

Step 3: Slimming Data with --markers-for-bin

  • --markers all exported every component in ICU4X (5.4 MB).
  • Point icu4x-datagen at the compiled binary (.exe or .wasm) to extract only used markers:
icu4x-datagen \
  --markers-for-bin target/release/tutorial \
  --locales ccp \
  --format blob \
  --overwrite \
  --out ccp.blob
5,448,603 bytes (5.4 MB)   ➔   13,711 bytes (13.7 KB)!   (99.7% reduction)

Step 4: Narrower APIs = Even Smaller Data

  • DateTimeFormatter supports all 15+ calendars (13.7 KB).
  • For Gregorian-only apps, use FixedCalendarDateTimeFormatter<Gregorian, ...>:
use icu::calendar::Gregorian;
use icu::datetime::FixedCalendarDateTimeFormatter;

let dtf = FixedCalendarDateTimeFormatter::<Gregorian, _>::try_new_with_buffer_provider(
    &provider, locale.into(), YMD::medium()
)?;
println!("{}", dtf.format(&iso_date.to_calendar(Gregorian))); // input must be Gregorian
  • --markers-for-bin now detects only 5 Gregorian & decimal markers:
13,711 bytes (13.7 KB)   ➔   4,711 bytes (4.7 KB)!

Data Pipeline Recap

1. Dynamic Language Packs

Lean compiled defaults + on-demand BlobDataProvider language packs.

2. Automatic Marker Slicing

--markers-for-bin inspects your .exe or .wasm to extract only used DataMarkers.

3. Type-Driven Footprint

Specific types (FixedCalendarDateTimeFormatter, try_new_tiny) shrink both code and data.

Part 3: What's New in ICU4X

2025–2026 Project Update & Call for Feedback (~20 Minutes)

Releases & Unstable Feature Disclaimer

🚀 Recent Releases

  • ICU4X 2.2 (Apr 2026) — CLDR 48.2, TZDB 2026a, typed calendars
  • ICU4X 2.3 (Aug 2026) — Display names, date ranges, new segmenter
  • ICU4X 2.4 (In Dev) — CLDR 49, Unicode 18, TZDB 2026e

🚧 Feedback Wanted on unstable APIs!

  • Available today behind the unstable Cargo feature (or icu_experimental).
  • Built for real-world testing—try them out and send feedback before stabilization!

ECMA-262 Temporal Shipped in Chrome & Firefox!

Chrome (V8) & Boa (Chrome 144, Jan 2026!)

  • JS Temporal → V8 C++ → temporal_rs (via Diplomat FFI)
  • icu_calendar: 15+ calendars, zero heap allocations
  • zoneinfo64: Zero-copy Rust parser sharing ICU4C's zoneinfo64.res

Firefox (SpiderMonkey) (Firefox 139, May 2025)

  • JS Temporal → SpiderMonkey C++
  • icu_calendar (via Diplomat FFI) for all calendar arithmetic & non-Gregorian calendars
  • ICU4C for time zones
📄 Deep dive: Shane's EuroRust 2026 talk — Shipping Temporal: How the Rust Ecosystem Powers Date/Time on the Web

Dart & Flutter Native Assets (record_use)

  • Dart build hooks (build.dart & link.dart) integrate with record_use.
  • Compiler records which ICU4X FFI symbols are reachable by the app.
  • Link hook strips unused C ABI symbols and caches native binaries.

Impact for Flutter & Dart Apps

  • Zero manual build setup: dart run / flutter build fetches or builds ICU4X automatically.
  • Call-graph tree-shaking: Ship only the ICU4X components your Dart code calls.

New Segmenter Implementations (icu_segmenter) (1/2)

Unicode-Data-Driven Rule Segmenter by Robert Bastian (*_neo_*) unstable

Compiles upstream Unicode state machines directly into a compact DFA (no hand-maintained TOML rules!):

1. Direct from UCD

Parses upstream ucd/auxiliary/*Break*.txt state machines directly via unicodeset_parse.

2. Zero-Cost Tailorings

Compiles CLDR tailorings and CSS line-break options (lb=loose/strict, lw=phrase) into symbol map overrides.

3. Single-Pass Scripts

Fuses Thai, Lao, Khmer, Myanmar, and CJ directly into DFA symbols—no secondary lookup!

  • Plus Unicode 17 Line Breaking (*_17_* constructors) unstable.

Experimental ML Word Segmenters (icu_segmenter) (2/2)

RAdaBoost (Radical-Informed AdaBoost)

  • Unihan IRG radicals + AdaBoost trees for Chinese (radaboost) & Thai (thadaboost).
  • Generalizes to out-of-vocabulary Han characters in a tiny footprint.

Convolutional Neural Network (CNN)

  • Dilated CNN for Thai word segmentation (CnnSegmenter).
  • Explores accuracy vs. latency trade-offs alongside LSTM and dictionary models.
🚧 Prototype status: Available in components/segmenter/examples/ (not yet in the main crate API).

Locale Display Names (1/2): Styles, Lengths & Fallback

  • icu_locale::names unstable (+ C++, JS & Dart FFI): Language, Region, Script & Variant display names.
  • Configurable display (Dialect, Standard, Menu) and length (Medium, Short, Long):
Display + Length en-GB az zh-Hans
Dialect + Medium (default) "British English" "Azerbaijani" "Simplified Chinese"
Dialect + Short "UK English" "Azeri" "Simplified Chinese"
Dialect + Long "British English" "Azerbaijani" "Simplified Mandarin Chinese"
Standard + Medium "English (United Kingdom)" "Azerbaijani" "Chinese (Simplified)"
Menu + Short "English (UK)" "Azeri" "Chinese, Mandarin (Simplified)"
Graceful Fallback (TryWriteable): Unknown subtags fall back to raw codes with Part::ERROR — it-Qabc-150 ➔ "Italian (Qabc, Europe)".

Locale Display Names (2/2): Tiny vs. Light vs. Heavy Tiers

  • Mapped from CLDR per-path coverage levels so you only pay for the names you need:
1. Tiny (try_new_tiny) core + basic
Own language (fr, en), official regions (FR, CA, BE, CH), own script (Latn).
2. Light (try_new_light) + moderate
~70 major languages & dialects, all regions (IN, …), major scripts (Arab, Cyrl, Hans).
3. Heavy (try_new_heavy) + modern
~130+ languages (az, scn, br), 49+ scripts (Deva, Xsux), all BCP-47 variants.
Tier (fr locale) fr-CA zh-Hans hi-Deva-IN
Tiny (try_new_tiny) ✅ "français (Canada)" ❌ "zh (Hans)" ❌ "hi (Deva, IN)"
Light (try_new_light) ✅ "français canadien" ✅ "chinois simplifié" ❌ "hindi (Deva, Inde)"
Heavy (try_new_heavy) ✅ "français canadien" ✅ "chinois simplifié" ✅ "hindi (dévanagari, Inde)"

New & Improved Calendars (icu_calendar 2.2 & 2.3)

Type-Safe & Temporal-Ready

  • Typed Lunisolar Months:
    Month::new(5) vs. Month::leap(5) (no more "M05L" strings!)
  • Field Bag Construction (Date::try_from_fields):
    ECMA-262 Temporal Overflow (Constrain / Reject) & missing-field strategies
  • Cross-Calendar Arithmetic:
    try_add & try_until across all 15+ calendars (75% faster diffing)

Smarter Resolution & Smaller Data

  • Locale-Aware Default Calendars:
    fa ➔ Persian · th ➔ Buddhist (via likely subtags)
  • First-Class Julian Calendar:
    AnyCalendar::Julian for historical dates
  • −20 KB Japanese Era Data:
    Compact era start dates & zero-copy struct layout

DateTime & Currency Formatting Upgrades

DateTime (icu_datetime)

  • Date Ranges (DateRangeFormatter) unstable:
    Oct 20 – 23, 2026 (collapses if start == end)
  • Unambiguous Yearless Dates 2.4:
    Medium MD in ja uses 10月20日 (MMM) instead of 10/20
  • Flexible Day Periods (B):
    "in the morning" · "at night"
  • Dynamic Skeletons (DateTimeFieldBag) 2.4 unstable:
    Runtime UTS #35 skeleton parsing

Currency (icu_experimental::currency) unstable

  • Standard & ISO Code: $12,345.67 · USD 12,345.67
  • Compact & Accounting: $12K · ($12,345.67)
  • Locale Sign Placement: CHF-12'345.67 (de-CH)
  • Number-Only (try_new_no_currency):
    Currency-aware decimals without symbol (12,345.67 for USD, 12,346 for JPY)

Top Contributors Since UTW 2025 (1/2)

Robert Bastian
Robert Bastian
323 Commits
Segmenter V2, RSCD, calendars, CLDR 49
Shane Carr
Shane Carr
173 Commits
Display names & tiers, field bag, ZeroTrie
Manish Goregaokar
Manish Goregaokar
148 Commits
Date ranges, calendar math, relative time
Younies Mahmoud
Younies Mahmoud
30 Commits
Currency formatting (compact, accounting)
Pratush Shyam Gupt
Pratush Shyam Gupt
12 Commits
Plural ranges, properties, ZeroTrie
Henri Sivonen
Henri Sivonen
10 Commits
Collation, normalization, UTS #46
Moritz
Moritz
8 Commits
Dart record_use & FFI provenance
rusat
rusat
7 Commits
ISO 8601 durations, calendar/collation tests
OpNUB
OpNUB
6 Commits
RAdaBoost & CNN segmenters, Unihan

And Many More Contributors! (2/2)

3–5 Commits

  • Parth Agrawal (5)
  • Sidney Cammeresi (5)
  • Kamthorn Krairaksa (4)
  • Xuanwo (4)
  • Ben Beasley (3)
  • Georg Semmler (3)
  • Makoto Kato (3)
  • Mohammed Afreed baig (3)
  • RS (3)
  • cloud-machine-111 (3)
  • may (3)

2 Commits

  • Alan Liu
  • AudaciousAxiom
  • Chaganti Reddy
  • Ishaan Kesarwani
  • Rizzen
  • Swarnit
  • robofinch

1 Commit

  • aumetra
  • Bryce Berger
  • Caleb Koo
  • Charley Peng
  • Eric Sheppard
  • Fuqiao Xue
  • Hamza Jadid
  • Hans Elias J.
  • Jayant
  • José Julián Espina
  • Kevin Ness
  • Kushida
  • Linus Groh
  • Martin Chrástek
  • Milad Fa
  • moulins
  • Nora Papale
  • WaterWhisperer
  • xtqqczze
  • yqaty

Call to Action & Q&A

  • 🌐 Documentation, Tutorials & Live Demo
    icu4x.unicode.org · icu4x.unicode.org/2_3/demo

  • 🧪 Try Our unstable APIs & Send Feedback!

    • icu_locale::names (Tiny / Light / Heavy display names)
    • icu_segmenter V2 (_neo_ Unicode state machine segmenter)
    • icu_datetime (DateRangeFormatter; DateTimeFieldBag coming in 2.4)
    • icu_experimental::currency
  • 🤝 Get Involved!
    We love mentoring new contributors—reach out at shane@unicode.org or on GitHub Discussions.

icu4x-announce QR Code
Subscribe to icu4x-announce
Low-traffic release announcements

console.assert(new Intl.NumberFormat("es-ES", { notation: "compact" }).format(1_900_000) === "1,9\u00a0M"); console.assert(new Intl.NumberFormat("es-ES", { notation: "compact" }).format(346_000_000) === "346\u00a0M"); console.assert(new Intl.RelativeTimeFormat("es", { numeric: "always" }).format(-4, "month") === "hace 4 meses"); console.assert(new Intl.NumberFormat("ja-JP", { notation: "compact" }).format(1_910_000) === "191万"); console.assert(new Intl.NumberFormat("ja-JP", { notation: "compact", maximumFractionDigits: 1 }).format(340_000_000) === "3.4億"); console.assert(new Intl.RelativeTimeFormat("ja", { numeric: "always" }).format(-4, "month") === "4 か月前");

const d = new Date("2026-10-20T00:00:00Z"); console.assert(new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "Oct 20, 2026"); console.assert(new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "20 oct. 2026"); console.assert(new Intl.DateTimeFormat("he-u-ca-hebrew", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "ט׳ בחשוון תשפ״ז"); console.assert(new Intl.DateTimeFormat("en-u-ca-hebrew", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "9 Heshvan 5787"); console.assert(new Intl.DateTimeFormat("ar-SA-u-ca-islamic-umalqura", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "٩ جمادى الأولى ١٤٤٨ هـ"); console.assert(new Intl.DateTimeFormat("zh-u-ca-chinese", { dateStyle: "long", timeZone: "UTC" }).format(d) === "2026丙午年九月十一"); console.assert(new Intl.DateTimeFormat("ja-u-ca-japanese", { dateStyle: "medium", timeZone: "UTC" }).format(d) === "令和8年10月20日"); const d1 = new Date("2026-10-20T00:00:00Z"), d2 = new Date("2026-10-23T00:00:00Z"); console.assert(new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" }).formatRange(d1, d2) === "Oct 20\u2009–\u200923, 2026"); console.assert(new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeZone: "UTC" }).formatRange(d1, d2) === "20–23 oct. 2026"); console.assert(new Intl.DateTimeFormat("ja-JP", { dateStyle: "medium", timeZone: "UTC" }).formatRange(d1, d2) === "2026/10/20~2026/10/23"); console.assert(new Intl.DisplayNames("en", { type: "language", languageDisplay: "dialect" }).of("fr-CA") === "Canadian French"); console.assert(new Intl.DisplayNames("en", { type: "language", languageDisplay: "standard" }).of("fr-CA") === "French (Canada)"); console.assert(new Intl.DisplayNames("fr", { type: "language", languageDisplay: "dialect" }).of("fr-CA") === "français canadien"); console.assert(new Intl.DisplayNames("zh-Hant", { type: "language" }).of("fr-CA") === "法文(加拿大)"); console.assert([...new Intl.Segmenter("ja", { granularity: "word" }).segment("国際化ライブラリを使う")].map(s => s.segment).join("|") === "国際|化|ライブラリ|を|使う"); console.assert([...new Intl.Segmenter("th", { granularity: "word" }).segment("การทำให้เป็นสากล")].map(s => s.segment).join("|") === "การ|ทำให้|เป็น|สากล");

console.assert(new Intl.Locale("zh-TW").maximize().toString() === "zh-Hant-TW"); console.assert(new Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" }).format(12345.67) === "CHF\u00a012'345.67"); console.assert(new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY", notation: "compact" }).format(100000) === "¥10万"); console.assert(new Intl.NumberFormat("en-US", { style: "unit", unit: "kilometer", unitDisplay: "short" }).format(5) === "5 km"); console.assert(new Intl.NumberFormat("en-US", { style: "unit", unit: "mile", unitDisplay: "short" }).format(3.1) === "3.1 mi"); console.assert(new Intl.DateTimeFormat("ja-u-ca-japanese", { era: "long", year: "numeric", month: "long", day: "numeric", weekday: "long", hour: "2-digit", minute: "2-digit", timeZoneName: "long", timeZone: "Europe/Paris" }).format(new Date("2026-10-20T14:30:00+02:00")) === "令和8年10月20日火曜日 14:30 中央ヨーロッパ夏時間"); console.assert(new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeZone: "UTC" }).formatRange(new Date("2026-10-20T00:00:00Z"), new Date("2026-10-23T00:00:00Z")) === "20–23 oct. 2026"); console.assert(new Intl.RelativeTimeFormat("en").format(3, "day") === "in 3 days"); console.assert(new Intl.RelativeTimeFormat("fr").format(3, "day") === "dans 3 jours"); console.assert(new Intl.Collator("sv").compare("ä", "z") > 0); console.assert(new Intl.Collator("de").compare("ä", "b") < 0); console.assert(new Intl.ListFormat("en", { type: "disjunction" }).format(["A", "B", "C"]) === "A, B, or C"); console.assert(new Intl.DisplayNames("en", { type: "region", style: "long" }).of("GB") === "United Kingdom"); console.assert(new Intl.DisplayNames("en", { type: "region", style: "short" }).of("GB") === "UK"); // ICU4X: // LanguageIdentifierDisplayName::try_new_long_light(locale!("en").into(), langid!("zh-Hans"), Default::default()) => "Simplified Mandarin Chinese" // WordSegmenter::new_auto(Default::default()).segment_str("ユニコードは、符号化文字集合") => "ユニコード|は|、|符号|化|文字|集合"

s1 = "中ワ況写イノナ開億がゃへ者43俳寺式7沢暮ル材年るあん移酔むぎえす写情主逃69引ぎうぱ何81昇フネイマ本因ぱ不真え断候ら。吉群コクチ賀伝ツエ別写ユリキ施見ら力1取目れづぱぞ然野テ倍展ぴるてけ京答週ネコ入整料勉らげ。"; s2 = "中|ワ|況|写|イノナ|開|億|が|ゃ|へ|者|43|俳|寺|式|7|沢|暮|ル|材|年|る|あん|移|酔|むぎ|え|す|写|情|主|逃|69|引|ぎ|う|ぱ|何|81|昇|フネイマ|本|因|ぱ|不|真|え|断|候|ら|。|吉|群|コクチ|賀|伝|ツエ|別|写|ユリキ|施|見|ら|力|1|取|目|れ|づ|ぱ|ぞ|然|野|テ|倍|展|ぴ|る|て|け|京|答|週|ネコ|入|整|料|勉|ら|げ|。"; console.assert([...new Intl.Segmenter("ja", { granularity: "word" }).segment(s1)].map((v) => v.segment).join("|") === s2);

Curved milestone callout arrow from 2020 to 2026

Chart 1: DateTimeFormatter Performance (sec, lower is better)

Chart 2: DateTimeFormatter Memory Use (log scale bytes, lower is better)

Chart 3: Basic Number Format: Code Size (kB, lower is better)

console.assert(new Intl.Locale("DE-CH").toString() === "de-CH"); console.assert(new Intl.Locale("ES-419").toString() === "es-419"); console.assert(new Intl.DateTimeFormat("ja-u-ca-japanese", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }).format(new Date("2026-10-20T14:30:00Z")) === "令和8年10月20日 14:30"); // Rust solution: use icu::calendar::Date; use icu::datetime::{fieldsets::YMD, DateTimeFormatter}; use icu::locale::Locale; use icu::time::{DateTime, Time}; let locale: Locale = "ja-u-ca-japanese".parse()?; let dtf = DateTimeFormatter::try_new( locale.into(), YMD::medium().with_time_hm(), // YMDT )?; let dt = DateTime { date: Date::try_new_iso(2026, 10, 20)?, time: Time::try_new(14, 30, 0, 0)?, }; println!("{}", dtf.format(&dt)); // => 令和8年10月20日 14:30

ICU4X compiled_data root fallback: DateTimeFormatter::try_new(locale!("ccp").into(), YMD::medium()).unwrap().format(&Date::try_new_iso(2026, 10, 20).unwrap()) // => "2026 M10 20" https://github.com/unicode-org/icu4x/blob/main/tutorials/data-provider-runtime.md

console.assert(new Intl.DateTimeFormat("ccp", { dateStyle: "medium", timeZone: "UTC" }).format(new Date("2026-10-20T00:00:00Z")) === "𑄸𑄶 𑄃𑄧𑄇𑄴𑄑𑄮𑄝𑄧𑄢𑄴, 𑄸𑄶𑄸𑄼");

console.assert(["en-GB", "az", "zh-Hans"].map(c => new Intl.DisplayNames("en", { type: "language", languageDisplay: "dialect", style: "long" }).of(c)).join(" | ") === "British English | Azerbaijani | Simplified Chinese"); console.assert(["en-GB", "az", "zh-Hans"].map(c => new Intl.DisplayNames("en", { type: "language", languageDisplay: "dialect", style: "short" }).of(c)).join(" | ") === "UK English | Azeri | Simplified Chinese"); console.assert(["en-GB", "az", "zh-Hans"].map(c => new Intl.DisplayNames("en", { type: "language", languageDisplay: "standard", style: "long" }).of(c)).join(" | ") === "English (United Kingdom) | Azerbaijani | Chinese (Simplified)"); // ICU4X Long, Menu & TryWriteable fallback tests: // https://github.com/unicode-org/icu4x/blob/main/components/locale/src/names/language.rs#L57-L181 // https://github.com/unicode-org/icu4x/blob/main/components/locale/src/names/language.rs#L2080-L2117

console.assert(["fr-CA", "zh-Hans", "hi-Deva-IN"].map(c => new Intl.DisplayNames("fr", { type: "language", languageDisplay: "dialect" }).of(c)).join(" | ") === "français canadien | chinois simplifié | hindi (dévanagari, Inde)"); // ICU4X Tiny / Light / Heavy tiers (locale!("fr") with [langid!("fr-CA"), langid!("zh-Hans"), langid!("hi-Deva-IN")]): // - try_new_tiny: [Ok("français (Canada)"), Err("zh (Hans)"), Err("hi (Deva, IN)")] // - try_new_light: [Ok("français canadien"), Ok("chinois simplifié"), Err("hindi (Deva, Inde)")] // - try_new_heavy: [Ok("français canadien"), Ok("chinois simplifié"), Ok("hindi (dévanagari, Inde)")] // https://github.com/unicode-org/icu4x/blob/main/components/locale/src/names/language.rs#L429-L460

console.assert(new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" }).formatRange(new Date("2026-10-20T00:00:00Z"), new Date("2026-10-23T00:00:00Z")) === "Oct 20\u2009–\u200923, 2026"); console.assert(new Intl.DateTimeFormat("en-US", { dayPeriod: "long", timeZone: "UTC" }).format(new Date("2026-10-20T09:00:00Z")) === "in the morning"); console.assert(new Intl.DateTimeFormat("en-US", { dayPeriod: "long", timeZone: "UTC" }).format(new Date("2026-10-20T23:00:00Z")) === "at night"); console.assert(new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(12345.67) === "$12,345.67"); console.assert(new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", currencyDisplay: "code" }).format(12345.67) === "USD\u00a012,345.67"); console.assert(new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", notation: "compact" }).format(12345.67) === "$12K"); console.assert(new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", currencySign: "accounting" }).format(-12345.67) === "($12,345.67)"); console.assert(new Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" }).format(-12345.67) === "CHF-12'345.67"); // ICU4X currency tests (including try_new_no_currency): // https://github.com/unicode-org/icu4x/blob/main/components/experimental/src/dimension/currency/format.rs