jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

JavaScript Intl API — Dates, Numbers, Currency & Locale-Aware Formatting Without Hand-Rolling

Why not to hand-roll i18n formatting: Intl.NumberFormat, DateTimeFormat, PluralRules, Collator, Segmenter, performance, and timezone pitfalls.

You ship a pricing page and hard-code $1,234.56 {Bạn ship trang pricing và hard-code $1,234.56}. A German user sees a dollar sign where they expect 1.234,56 € {User Đức thấy ký hiệu dollar nơi họ mong 1.234,56 €}. You format a date as 12/27/2025 and a Vietnamese user reads it as day 12 of month 27 {Bạn format ngày 12/27/2025 và user Việt đọc thành ngày 12 tháng 27}. You pluralize with count === 1 ? 'item' : 'items' and Polish breaks {Bạn pluralize bằng count === 1 ? 'item' : 'items' và tiếng Ba Lan hỏng}. These are not edge cases — they are the default when you hand-roll locale formatting {Đây không phải edge case — chúng là mặc định khi bạn tự format locale}.

The browser ships a full Internationalization API under the Intl namespace {Trình duyệt ship sẵn Internationalization API dưới namespace Intl}. It encodes CLDR locale data — decimal separators, currency placement, calendar systems, plural rules, collation order — so you do not have to {Nó mã hóa dữ liệu locale CLDR — dấu thập phân, vị trí currency, hệ lịch, quy tắc số nhiều, thứ tự collation — để bạn không phải tự làm}. This post is for senior frontend engineers who need production-grade formatting without pulling a 200 KB library for every number {Bài này dành cho senior frontend engineer cần formatting production-grade mà không kéo thư viện 200 KB cho mỗi con số}.

Open the full demo {Mở demo đầy đủ}: /tools/js-intl-demo/.


Why you should never hand-roll formatting {Tại sao không bao giờ tự format}

Locale rules are not cosmetic preferences — they are grammatically and legally meaningful {Quy tắc locale không phải preference thẩm mỹ — chúng có ý nghĩa ngữ pháp và pháp lý}. Getting them wrong erodes trust on checkout flows, dashboards, and admin tools {Sai chúng làm mất niềm tin trên checkout, dashboard, và admin tool}.

ConcernHand-rolled trapWhat Intl handles
Decimal separatorAlways .. in en-US, , in de-DE, ٫ in ar-EG
GroupingAlways , every 3 digits1,234,567 vs 1.234.567 vs 12,34,567 (Indian lakhs)
Currency symbolPrefix $$1.00 (en-US) vs 1,00 $ (fr-FR) vs USD 1.00 (some locales)
PercentAppend %-12% vs -12 % vs -12٪ depending on locale
Date orderMM/DD/YYYY27/12/2025 (vi-VN), 2025/12/27 (ja-JP), non-Gregorian calendars
RTLIgnore directionArabic and Hebrew need symbol placement and bidirectional text
Pluralization=== 1 checkone, few, many, other categories per locale
Sort orderUnicode code pointsAccent-aware, case-aware, locale-specific (e.g. ä near a in German)

The toLocaleString() trap {Bẫy toLocaleString()}: Calling (1234.5).toLocaleString() without options uses the runtime default locale (often the browser/OS language), not your app’s chosen locale {Gọi (1234.5).toLocaleString() không có options dùng locale mặc định runtime (thường ngôn ngữ browser/OS), không phải locale app bạn chọn}. Worse: (1234.5).toLocaleString('de-DE') gives a decimal string but no currency symbol, no unit, no compact notation unless you pass a full options object — and behavior differs subtly across engines when options are incomplete {Tệ hơn: (1234.5).toLocaleString('de-DE') cho chuỗi decimal nhưng không có currency symbol, unit, compact notation trừ khi bạn truyền object options đầy đủ — và hành vi khác nhau giữa engine khi options thiếu}. Prefer explicit Intl.* constructors with cached instances {Ưu tiên constructor Intl.* rõ ràng với instance được cache}.

// ❌ Implicit locale, no control
price.toLocaleString();

// ❌ Locale passed but still no currency semantics
price.toLocaleString('de-DE');

// ✅ Explicit, reusable, testable
const eurFormatter = new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR',
});
eurFormatter.format(price);

Intl.NumberFormat — numbers, currency, percent, units, compact {Intl.NumberFormat — số, currency, percent, unit, compact}

Intl.NumberFormat is the workhorse for any numeric display: prices, percentages, file sizes, distances, KPI deltas {Intl.NumberFormat là công cụ chính cho mọi hiển thị số: giá, phần trăm, dung lượng file, khoảng cách, KPI delta}.

Core styles

const locale = 'de-DE';
const value = 1234567.89;

// Decimal — grouping + separator follow locale
new Intl.NumberFormat(locale).format(value);
// → "1.234.567,89"

// Currency — symbol placement + fraction digits from ISO 4217
new Intl.NumberFormat(locale, {
  style: 'currency',
  currency: 'EUR',
}).format(value);
// → "1.234.567,89 €"

// Percent — value is multiplied by 100 internally
new Intl.NumberFormat(locale, { style: 'percent' }).format(0.875);
// → "87,5 %"

// Unit — length, mass, temperature, digital storage
new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long',
}).format(42);
// → "42 kilometers"

Compact notation for dashboards

Compact notation (notation: 'compact') produces 1.2M, 1,2 Mio., 120万 — essential for dense UI where full grouping overflows {Compact notation (notation: 'compact') tạo 1.2M, 1,2 Mio., 120万 — cần thiết cho UI dày nơi grouping đầy đủ tràn}.

new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'short',
  maximumFractionDigits: 1,
}).format(1_234_567);
// → "1.2M"

formatToParts — build custom UI without string parsing

When you need to style the currency symbol separately (superscript cents, color-coded sign), never regex-parse the formatted string {Khi cần style currency symbol riêng (cent superscript, dấu màu), không bao giờ regex-parse chuỗi đã format}. Use formatToParts() {Dùng formatToParts()}:

const parts = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
}).formatToParts(-1234.56);

// [
//   { type: 'minusSign', value: '-' },
//   { type: 'currency', value: '$' },
//   { type: 'integer', value: '1,234' },
//   { type: 'decimal', value: '.' },
//   { type: 'fraction', value: '56' },
// ]

Map type to DOM nodes. This survives locale changes without brittle string splits {Map type sang DOM node. Cách này sống sót khi đổi locale mà không cần split chuỗi mong manh}.

formatRange — price spans and date-adjacent numbers

const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
fmt.formatRange(19.99, 29.99);
// → "$19.99 – $29.99" (en-dash inserted by locale rules)
OptionUse case
minimumFractionDigits / maximumFractionDigitsCrypto (8 decimals) vs JPY (0 decimals)
signDisplay: 'exceptZero'Show + on positive deltas in trading UI
roundingMode: 'floor'Always round down prices (legal requirement in some jurisdictions)
currencyDisplay: 'code'Show USD instead of $ in multi-currency admin panels

Intl.DateTimeFormat — dates, times, time zones, calendars {Intl.DateTimeFormat — ngày, giờ, time zone, lịch}

Dates are the second most hand-rolled disaster after numbers {Date là thảm họa tự format phổ biến thứ hai sau số}. Intl.DateTimeFormat handles date order, month names, 12h vs 24h, and time zone conversion from a single Date or timestamp {Intl.DateTimeFormat xử lý thứ tự ngày, tên tháng, 12h vs 24h, và chuyển time zone từ một Date hoặc timestamp}.

const instant = new Date('2025-12-27T15:30:00.000Z');

// Preset styles — let the locale decide field order and separators
new Intl.DateTimeFormat('en-US', {
  dateStyle: 'full',
  timeStyle: 'short',
  timeZone: 'America/New_York',
}).format(instant);
// → "Saturday, December 27, 2025 at 10:30 AM"

new Intl.DateTimeFormat('ja-JP', {
  dateStyle: 'long',
  timeStyle: 'medium',
  timeZone: 'Asia/Tokyo',
}).format(instant);
// → "2025年12月28日 0:30:00" (next calendar day in JST)

Granular field control

When preset styles are too coarse, specify individual fields {Khi preset style quá thô, chỉ định từng field}:

new Intl.DateTimeFormat('vi-VN', {
  weekday: 'short',
  day: 'numeric',
  month: 'long',
  year: 'numeric',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'Asia/Ho_Chi_Minh',
}).format(instant);

formatToParts for custom date pickers

Same pattern as numbers — decompose into { type, value } parts for a headless calendar component {Cùng pattern với số — tách thành part \{ type, value \} cho calendar component headless}.

const parts = new Intl.DateTimeFormat('en-US', {
  dateStyle: 'medium',
}).formatToParts(instant);

const byType = Object.fromEntries(parts.map((p) => [p.type, p.value]));
// byType.day, byType.month, byType.year — safe to bind to inputs

Time zone pitfalls

PitfallReality
Date is always UTC internallyDisplay layer must set timeZone explicitly — default is runtime local zone
DST gaps and overlaps2025-03-09T02:30:00 in America/New_York may not exist; Intl resolves per spec
”Store as UTC, display local”Correct pattern: persist ISO UTC, format with user’s timeZone option
Server renders “today”Server time zone ≠ user time zone — hydrate or pass user’s IANA zone from profile

Never use string concatenation for ISO dates in UI {Không bao giờ nối chuỗi ISO date cho UI}: date.getMonth() + 1 + '/' + date.getDate() ignores locale order and fails for RTL locales {date.getMonth() + 1 + '/' + date.getDate() bỏ qua thứ tự locale và fail với locale RTL}. Always go through Intl.DateTimeFormat or formatToParts {Luôn đi qua Intl.DateTimeFormat hoặc formatToParts}.


Intl.RelativeTimeFormat — “2 days ago” done correctly {Intl.RelativeTimeFormat — “2 days ago” đúng cách}

Relative timestamps appear in notifications, activity feeds, and comment threads {Timestamp tương đối xuất hiện trong notification, activity feed, và comment thread}. Do not template "${n} days ago" — grammar varies (word order, plural agreement, “yesterday” shortcuts) {Đừng template "${n} days ago" — ngữ pháp khác nhau (thứ tự từ, số nhiều, shortcut “yesterday”)}.

const rtf = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' });

rtf.format(-1, 'day');   // → "yesterday"
rtf.format(-2, 'day');   // → "2 days ago"
rtf.format(3, 'hour');   // → "in 3 hours"

numeric: 'auto' lets the locale collapse -1 day to “yesterday” when appropriate {numeric: 'auto' cho locale gom -1 day thành “yesterday” khi phù hợp}. Wrap it in a helper that computes the delta unit from two timestamps {Bọc trong helper tính delta unit từ hai timestamp}:

const DIVISORS = [
  [60, 'second'],
  [60, 'minute'],
  [24, 'hour'],
  [7, 'day'],
  [4.345, 'week'],
  [12, 'month'],
  [Infinity, 'year'],
];

function relativeTime(from, to, locale) {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  let delta = (to - from) / 1000;

  for (const [amount, unit] of DIVISORS) {
    if (Math.abs(delta) < amount) {
      return rtf.format(Math.round(delta), unit);
    }
    delta /= amount;
  }
}

Intl.ListFormat — conjunctions and Oxford commas {Intl.ListFormat — liên từ và Oxford comma}

Building "A, B, and C" manually breaks for locales that use different conjunctions, separators, or no Oxford comma {Tự build "A, B, and C" hỏng với locale dùng liên từ, separator, hoặc không có Oxford comma khác}.

const items = ['Chrome', 'Firefox', 'Safari'];

new Intl.ListFormat('en-US', { type: 'conjunction' }).format(items);
// → "Chrome, Firefox, and Safari"

new Intl.ListFormat('de-DE', { type: 'conjunction' }).format(items);
// → "Chrome, Firefox und Safari"

new Intl.ListFormat('en-US', { type: 'disjunction' }).format(items);
// → "Chrome, Firefox, or Safari"

Use type: 'unit' for dimensions like 10 ft × 8 ft × 6 ft {Dùng type: 'unit' cho kích thước như 10 ft × 8 ft × 6 ft}. Pair with Intl.DisplayNames when list items are enum codes (country codes, language codes) rather than pre-translated strings {Kết hợp Intl.DisplayNames khi item là enum code (mã quốc gia, mã ngôn ngữ) thay vì chuỗi đã dịch}.


Intl.PluralRules — beyond count === 1 {Intl.PluralRules — vượt count === 1}

English has two plural forms in practice (one, other) but many locales have three to six {Tiếng Anh có hai dạng số nhiều (one, other) nhưng nhiều locale có ba đến sáu}. Polish distinguishes one, few, many, other. Arabic has zero, one, two, few, many, other {Tiếng Ba Lan phân biệt one, few, many, other. Tiếng Ả Rập có zero, one, two, few, many, other}.

const pr = new Intl.PluralRules('pl-PL');

pr.select(1);   // → "one"
pr.select(2);   // → "few"
pr.select(5);   // → "many"
pr.select(22);  // → "few"

Wire it to your message catalog {Nối với message catalog}:

const messages = {
  en: { one: '{n} item', other: '{n} items' },
  pl: { one: '{n} rzecz', few: '{n} rzeczy', many: '{n} rzeczy', other: '{n} rzeczy' },
};

function pluralize(locale, count, messagesForLocale) {
  const category = new Intl.PluralRules(locale).select(count);
  const template = messagesForLocale[category] ?? messagesForLocale.other;
  return template.replace('{n}', String(count));
}

Do not conflate plural rules with number formatting {Đừng nhầm plural rules với number formatting}: Format the number with NumberFormat, then pick the string template with PluralRules {Format số bằng NumberFormat, rồi chọn template chuỗi bằng PluralRules}. Libraries like @formatjs/intl compose both; native Intl gives you the primitives {Thư viện như @formatjs/intl compose cả hai; Intl native cho bạn primitive}.


Intl.Collator — locale-aware sort vs naive .sort() {Intl.Collator — sort theo locale vs .sort() ngây thơ}

JavaScript’s default Array.prototype.sort() compares UTF-16 code units — Z before a, accents far from base letters {Array.prototype.sort() mặc định so sánh UTF-16 code unit — Z trước a, dấu xa chữ cơ sở}. For any user-facing sorted list (names, cities, tags), use Intl.Collator {Với mọi danh sách sort hiển thị user (tên, thành phố, tag), dùng Intl.Collator}.

const names = ['Zürich', 'apple', 'Äpfel', 'éclair', 'Zebra'];

// ❌ Code point order — wrong for humans
names.slice().sort();
// → ["Zebra", "Zürich", "apple", "Äpfel", "éclair"]

// ✅ Locale-aware
const collator = new Intl.Collator('de-DE', { sensitivity: 'base' });
names.slice().sort(collator.compare);
// → ["apple", "Äpfel", "éclair", "Zebra", "Zürich"]
sensitivityBehavior
'base'Ignore accents and case — a = ä = A
'accent'Distinguish accents, ignore case
'case'Distinguish case, ignore accents
'variant'Default — distinguish everything including kana variants

Reuse the collator — construction is expensive (same theme as formatters below) {Reuse collator — khởi tạo tốn kém (cùng chủ đề với formatter bên dưới)}. Create one per locale at app init or store in a Map {Tạo một instance per locale lúc init app hoặc lưu trong Map}.


Intl.Segmenter — grapheme, word, and sentence boundaries {Intl.Segmenter — ranh giới grapheme, word, sentence}

String length and slicing are locale-sensitive when emoji, combining marks, or CJK are involved {Độ dài và slice chuỗi nhạy locale khi có emoji, dấu kết hợp, hoặc CJK}. "👨‍👩‍👧".length is 5 in JavaScript (UTF-16 code units) but one grapheme cluster visually {"👨‍👩‍👧".length5 trong JavaScript (UTF-16 code unit) nhưng một grapheme cluster về mặt hiển thị}.

const segmenter = new Intl.Segmenter('en-US', { granularity: 'grapheme' });
const segments = [...segmenter.segment('👨‍👩‍👧‍👦 family')];

segments.map((s) => s.segment);
// → ["👨‍👩‍👧‍👦", " ", "f", "a", "m", "i", "l", "y"]
// First entry is ONE user-perceived character

Use cases on real products {Use case trên sản phẩm thật}:

  • Character counters for social posts — count graphemes, not .length {Bộ đếm ký tự cho post mạng xã hội — đếm grapheme, không phải .length}
  • Truncation with ellipsis — slice at grapheme boundary to avoid splitting emoji {Truncate với ellipsis — cắt tại ranh giới grapheme để không split emoji}
  • Double-click word selectiongranularity: 'word' for CJK and European languages {Double-click chọn từgranularity: 'word' cho CJK và ngôn ngữ châu Âu}
  • Text-to-speech chunkinggranularity: 'sentence' {Chunk TTSgranularity: 'sentence'}

Browser support is broad in 2025–2026 (Chrome 87+, Firefox 125+, Safari 16.4+). Feature-detect with 'Segmenter' in Intl and fall back to a library for legacy browsers if needed {Hỗ trợ trình duyệt rộng 2025–2026 (Chrome 87+, Firefox 125+, Safari 16.4+). Feature-detect với 'Segmenter' in Intl và fallback thư viện cho browser cũ nếu cần}.


Intl.DisplayNames — human-readable enum labels {Intl.DisplayNames — nhãn enum đọc được}

When your API returns region: 'VN' or language: 'vi', do not maintain a static JSON map of 200 country names {Khi API trả region: 'VN' hoặc language: 'vi', đừng duy trì JSON map tĩnh 200 tên quốc gia}.

const regions = new Intl.DisplayNames(['vi-VN'], { type: 'region' });
regions.of('VN'); // → "Việt Nam"

const languages = new Intl.DisplayNames(['en-US'], { type: 'language' });
languages.of('vi'); // → "Vietnamese"

const scripts = new Intl.DisplayNames(['en-US'], { type: 'script' });
scripts.of('Latn'); // → "Latin"

Types include region, language, script, currency, calendar, dateTimeField, keyValue (for Unicode key/value pairs) {Type gồm region, language, script, currency, calendar, dateTimeField, keyValue (cho cặp key/value Unicode)}. Combine with ListFormat for "English, Vietnamese, and Japanese" from ISO codes {Kết hợp ListFormat cho "English, Vietnamese, and Japanese" từ mã ISO}.


Performance — reuse formatter instances {Performance — reuse formatter instance}

Creating an Intl.NumberFormat or Intl.DateTimeFormat is orders of magnitude slower than calling .format() on an existing instance {Tạo Intl.NumberFormat hoặc Intl.DateTimeFormat chậm hơn nhiều bậc so với gọi .format() trên instance có sẵn}. Each construction loads and parses locale data internally {Mỗi lần khởi tạo load và parse dữ liệu locale bên trong}.

// ❌ Inside a render loop — creates formatter every row
rows.map((row) =>
  new Intl.NumberFormat(locale, { style: 'currency', currency: 'USD' }).format(row.price)
);

// ✅ Module-level or memoized cache
const formatterCache = new Map();

function getNumberFormat(locale, options) {
  const key = locale + JSON.stringify(options);
  if (!formatterCache.has(key)) {
    formatterCache.set(key, new Intl.NumberFormat(locale, options));
  }
  return formatterCache.get(key);
}

Benchmarks in V8 typically show ~100× slower construction vs format for NumberFormat {Benchmark trên V8 thường cho ~100× chậm hơn construction vs format với NumberFormat}. Patterns that work in production {Pattern hoạt động trên production}:

  1. Pre-create formatters for each supported locale at bootstrap {Pre-create formatter cho mỗi locale hỗ trợ lúc bootstrap}
  2. Memoize by (locale, optionsKey) when options vary (currency toggle, date style picker) {Memoize theo (locale, optionsKey) khi options thay đổi (toggle currency, date style picker)}
  3. Store collators and plural rules the same way — they are equally expensive to construct {Lưu collator và plural rules cùng cách — chúng tốn kém khởi tạo tương đương}
  4. SSR: create formatters once per request locale on the server, not per data row in a loop {SSR: tạo formatter một lần per request locale trên server, không phải per data row trong loop}

Time zones and the road to Temporal {Time zone và con đường tới Temporal}

Date is a legacy object: mutable, month is 0-indexed, no time zone awareness in the object itself, and parsing Date.parse('02/03/2024') is implementation-defined {Date là object legacy: mutable, tháng 0-indexed, không nhận biết time zone trong object, và parse Date.parse('02/03/2024') phụ thuộc implementation}. Intl.DateTimeFormat solves the display layer but not arithmetic (add 3 months, end of quarter in Tokyo) {Intl.DateTimeFormat giải display nhưng không giải số học (cộng 3 tháng, cuối quý ở Tokyo)}.

Temporal (Stage 3 as of 2025–2026) is the successor: Temporal.Instant, Temporal.ZonedDateTime, Temporal.PlainDate — immutable, explicit time zones, unambiguous parsing {Temporal (Stage 3 tính đến 2025–2026) là kế thời: Temporal.Instant, Temporal.ZonedDateTime, Temporal.PlainDate — immutable, time zone rõ ràng, parse không mơ hồ}.

// Temporal (where available — polyfill or native in some runtimes)
const zdt = Temporal.ZonedDateTime.from('2025-12-27T15:30:00+07:00[Asia/Ho_Chi_Minh]');
zdt.add({ days: 1 }).toLocaleString('vi-VN');

Current status (2025–2026): shipping natively in some JavaScript runtimes; browsers rely on @js-temporal/polyfill or gradual rollout {Trạng thái hiện tại (2025–2026): ship native trên một số runtime JavaScript; browser dùng @js-temporal/polyfill hoặc rollout dần}. Practical strategy today: persist UTC ISO strings, format with Intl.DateTimeFormat + timeZone, and isolate date math in tested utilities until Temporal is baseline {Chiến lược thực tế hôm nay: lưu chuỗi ISO UTC, format bằng Intl.DateTimeFormat + timeZone, và cô lập date math trong utility đã test cho đến khi Temporal là baseline}.


Production checklist {Checklist production}

TaskAPI
Price / invoice line itemIntl.NumberFormat with style: 'currency'
KPI / follower countNumberFormat with notation: 'compact'
”Last updated” timestampIntl.RelativeTimeFormat
Settings page dateIntl.DateTimeFormat with explicit timeZone
Filter tag sortIntl.Collator
”3 files selected”Intl.PluralRules + catalog
Country dropdown labelIntl.DisplayNames type 'region'
Tweet character limitIntl.Segmenter granularity 'grapheme'
Compatible browser listIntl.ListFormat type 'disjunction'

Test with real locales, not just en-US {Test với locale thật, không chỉ en-US}: Add de-DE, ar-EG, ja-JP, and at least one locale with complex plurals (pl-PL, ar-EG) to your visual regression or Storybook matrix {Thêm de-DE, ar-EG, ja-JP, và ít nhất một locale plural phức tạp (pl-PL, ar-EG) vào visual regression hoặc Storybook matrix}. Switching locale should re-create cached formatters, not mutate existing ones {Đổi locale nên tạo lại formatter cache, không mutate instance cũ}.


Key takeaways {Điểm chính}

  1. Never hand-roll separators, currency placement, date order, or plural templates — locale rules are data, not string templates {Không tự format separator, vị trí currency, thứ tự ngày, hoặc template plural — quy tắc locale là data, không phải string template}.
  2. Use explicit Intl constructors with options — avoid bare toLocaleString() without a cached, configured instance {Dùng constructor Intl rõ ràng với options — tránh toLocaleString() trần không có instance cache, cấu hình sẵn}.
  3. Reuse instances — construction is expensive; memoize by locale + options key {Reuse instance — khởi tạo tốn kém; memoize theo locale + options key}.
  4. formatToParts unlocks custom UI without fragile parsing {formatToParts mở UI tùy biến không cần parse mong manh}.
  5. PluralRules and Collator fix bugs that only appear in non-English locales {PluralRulesCollator sửa bug chỉ lộ ra ở locale không phải tiếng Anh}.
  6. Segmenter is the correct tool for length limits and truncation with emoji/CJK {Segmenter là công cụ đúng cho giới hạn độ dài và truncate với emoji/CJK}.
  7. Time zones belong in the formatter options, not in string hacks — plan for Temporal for arithmetic {Time zone thuộc formatter options, không phải hack chuỗi — lên kế hoạch Temporal cho số học}.

The interactive demo above runs entirely in your browser — switch locale and compare en-US, de-DE, vi-VN, ja-JP, and ar-EG side by side {Demo tương tác trên chạy hoàn toàn trong browser — đổi locale và so sánh en-US, de-DE, vi-VN, ja-JP, và ar-EG cạnh nhau}. For most frontend apps, native Intl covers 90% of formatting needs with zero bundle cost {Với hầu hết frontend app, Intl native cover 90% nhu cầu formatting với zero bundle cost}. Reach for @formatjs/intl or Temporal polyfills when you need message interpolation, time-zone arithmetic, or runtime locale loading from CDN {Chọn @formatjs/intl hoặc polyfill Temporal khi cần message interpolation, số học time zone, hoặc load locale runtime từ CDN}.