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}.
| Concern | Hand-rolled trap | What Intl handles |
|---|---|---|
| Decimal separator | Always . | . in en-US, , in de-DE, ٫ in ar-EG |
| Grouping | Always , every 3 digits | 1,234,567 vs 1.234.567 vs 12,34,567 (Indian lakhs) |
| Currency symbol | Prefix $ | $1.00 (en-US) vs 1,00 $ (fr-FR) vs USD 1.00 (some locales) |
| Percent | Append % | -12% vs -12 % vs -12٪ depending on locale |
| Date order | MM/DD/YYYY | 27/12/2025 (vi-VN), 2025/12/27 (ja-JP), non-Gregorian calendars |
| RTL | Ignore direction | Arabic and Hebrew need symbol placement and bidirectional text |
| Pluralization | === 1 check | one, few, many, other categories per locale |
| Sort order | Unicode code points | Accent-aware, case-aware, locale-specific (e.g. ä near a in German) |
The
toLocaleString()trap {BẫytoLocaleString()}: 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 explicitIntl.*constructors with cached instances {Ưu tiên constructorIntl.*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)
| Option | Use case |
|---|---|
minimumFractionDigits / maximumFractionDigits | Crypto (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
| Pitfall | Reality |
|---|---|
Date is always UTC internally | Display layer must set timeZone explicitly — default is runtime local zone |
| DST gaps and overlaps | 2025-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 throughIntl.DateTimeFormatorformatToParts{Luôn đi quaIntl.DateTimeFormathoặcformatToParts}.
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 withPluralRules{Format số bằngNumberFormat, rồi chọn template chuỗi bằngPluralRules}. Libraries like@formatjs/intlcompose both; nativeIntlgives you the primitives {Thư viện như@formatjs/intlcompose cả hai;Intlnative 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"]
sensitivity | Behavior |
|---|---|
'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 {"👨👩👧".length là 5 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 selection —
granularity: '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 chunking —
granularity: 'sentence'{Chunk TTS —granularity: '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}:
- Pre-create formatters for each supported locale at bootstrap {Pre-create formatter cho mỗi locale hỗ trợ lúc bootstrap}
- 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)} - 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}
- 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}
| Task | API |
|---|---|
| Price / invoice line item | Intl.NumberFormat with style: 'currency' |
| KPI / follower count | NumberFormat with notation: 'compact' |
| ”Last updated” timestamp | Intl.RelativeTimeFormat |
| Settings page date | Intl.DateTimeFormat with explicit timeZone |
| Filter tag sort | Intl.Collator |
| ”3 files selected” | Intl.PluralRules + catalog |
| Country dropdown label | Intl.DisplayNames type 'region' |
| Tweet character limit | Intl.Segmenter granularity 'grapheme' |
| Compatible browser list | Intl.ListFormat type 'disjunction' |
Test with real locales, not just
en-US{Test với locale thật, không chỉen-US}: Addde-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êmde-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}
- 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}.
- Use explicit
Intlconstructors with options — avoid baretoLocaleString()without a cached, configured instance {Dùng constructorIntlrõ ràng với options — tránhtoLocaleString()trần không có instance cache, cấu hình sẵn}. - 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}.
formatToPartsunlocks custom UI without fragile parsing {formatToPartsmở UI tùy biến không cần parse mong manh}.PluralRulesandCollatorfix bugs that only appear in non-English locales {PluralRulesvàCollatorsửa bug chỉ lộ ra ở locale không phải tiếng Anh}.Segmenteris the correct tool for length limits and truncation with emoji/CJK {Segmenterlà công cụ đúng cho giới hạn độ dài và truncate với emoji/CJK}.- Time zones belong in the formatter options, not in string hacks — plan for
Temporalfor arithmetic {Time zone thuộc formatter options, không phải hack chuỗi — lên kế hoạchTemporalcho 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}.