intl
import intl from "intl";
let de: intl.Locale = intl.locale("de-DE") or { return 1; };
let price: intl.Dec = intl.parse_dec("1234.5") or { return 1; };
let eur: intl.NumberFormat = intl.currency_format(de, "EUR") or { return 1; };
io.println(eur.format(&price)); // 1.234,50 €intl writes numbers, prices, dates, times and lists the way people in a
locale expect: 1,234.5 in English is 1.234,5 in German and 1 234,5
in French; a date is 10/26/26 in the US, 26/10/2026 in Britain,
26.10.2026 in Finland. It’s what JavaScript’s Intl does, with the
data of the Unicode CLDR (48.2.3) for 33 locales:
en en-GB en-IN en-AU en-CA de de-AT de-CH fi sv nb
da fr fr-CA es es-MX it nl pt pt-PT pl ru ja ko
zh zh-Hant ar he th hi tr cs sr
and the IANA time zone database (2026e) for zones by name.
intl only computes: it doesn’t read the clock or the environment. Give
it the instant (time.unix_nanos()) and the locale (locale.get(), your
settings). The data is about 4 MB, embedded in programs that import
intl and in no others.
Locales
| Function | Does |
|---|---|
locale(tag) -> Locale or IntlError | A language tag read and matched to the data: "de-AT", "zh-TW", "fi_FI" |
available() -> []const []const u8 | The 33 locales the data has |
negotiate(requested, avail) -> ?string | The first of avail that serves one of requested, in order |
l.language(), l.script(), l.region() | "zh", "Hant", "TW" ("" when absent) |
l.maximize(), l.minimize() | en ↔ en-Latn-US, zh-TW ↔ zh-Hant-TW |
l.base_name(), l.data_tag() | The tag without extensions; the data locale serving it |
let l: intl.Locale = intl.locale("EN-us") or { return 1; };
io.println(l.tag); // en-US
io.println(l.data_tag()); // en
let wanted: [2][]const u8 = ["fr-CH", "de"];
let mine: [2][]const u8 = ["en", "de"];
io.println(intl.negotiate(wanted[0..2], mine[0..2]) or { "en" }); // deA tag is made canonical: case (en-US), old codes (iw is he, in is
id), extension values. A locale the data doesn’t have is served by its
language (de-LU by de, fr-BE by fr). locale fails with
IntlError.InvalidTag for a tag that isn’t one, and IntlError.NoData
for a language the data doesn’t have.
Decimals
Numbers go to intl as Dec, an exact decimal of any length: 0.1 + 0.2 is 0.3, and a price is never off by a binary fraction.
| Function | Does |
|---|---|
parse_dec(text) -> Dec or IntlError | "1234.50", "-1e-7", ".5" |
dec_from_i64(n), dec_from_f64(x) | From numbers (0.1 is exactly 0.1, as it prints) |
d.to_string() | "1234.5", written out |
d.add(&e), d.sub(&e), d.neg(), d.cmp(&e) | Exact arithmetic and order |
d.round(n, mode), d.round_significant(n, mode) | Rounded to n fraction or significant digits |
let a: intl.Dec = intl.parse_dec("2.345") or { return 1; };
io.println(a.round(2).to_string()); // 2.35
io.println(a.round(2, intl.RoundingMode.HalfEven).to_string()); // 2.34The rounding modes are JavaScript’s: HalfExpand (school rounding, the
default), HalfEven, HalfTrunc, HalfCeil, HalfFloor, Ceil,
Floor, Expand, Trunc.
Plurals
| Function | Does |
|---|---|
plural(l, n) -> Plural | One for 1 in English, Few for 3 in Russian |
plural_ordinal(l, n) -> Plural | English: One for 1st, Two 2nd, Few 3rd, Other 4th |
plural_range(l, a, b) -> Plural | The category of “1–2” |
Plural is Zero, One, Two, Few, Many or Other. Languages have
different numbers of forms: English two, Russian four, Arabic six:
let ru: intl.Locale = intl.locale("ru") or { return 1; };
for (n in [1, 3, 5, 21]) {
io.println("{n}: {intl.plural(ru, intl.dec_from_i64(n))}"); // one, few, many, one
}Choose a message by the category; with @t translations, see
locale: Plural forms.
Numbers
| Function | Makes |
|---|---|
number_format(l) | 1,234.5 |
percent_format(l) | 25% for 0.25 |
currency_format(l, "EUR") -> NumberFormat or IntlError | €1,234.50, 1.234,50 € |
unit_format(l, "kilometer-per-hour") -> NumberFormat or IntlError | 50 km/h |
Then set what you need, and format:
| Call | Does |
|---|---|
nf.notation(n) | Standard, Scientific (1.235E3), Engineering, Compact (1.2K, 3.4M) |
nf.compact_display(w) | Short 1.2K or Long 1.2 thousand |
nf.currency_display(d) | Symbol $, NarrowSymbol, Code USD, Name US dollars |
nf.accounting(true) | ($1.00) for negative amounts where the locale writes them so |
nf.unit_display(w) | Short 12 km, Narrow 12km, Long 12 kilometers |
nf.fraction_digits(min, max) | Digits after the point |
nf.significant_digits(min, max) | Significant digits |
nf.integer_digits(min) | Zeros before: 007 |
nf.rounding(mode) | A RoundingMode |
nf.grouping(g) | Auto, Always, Min2, Off |
nf.sign_display(s) | Auto, Always (+5), Never, ExceptZero, Negative |
nf.format(&d), nf.format_i64(n), nf.format_f64(x) | The text |
nf.parts(&d) -> Vec_Part | The text in typed pieces (integer, group, decimal, currency …) |
nf.format_range(&a, &b) -> string or IntlError | 3–5, €3.00 – €5.00 |
nf.plural(&d) -> Plural | The category of the number as this format shows it |
let en: intl.Locale = intl.locale("en") or { return 1; };
var big: intl.NumberFormat = intl.number_format(en);
big.notation(intl.Notation.Compact);
io.println(big.format_i64(1234567)); // 1.2M
let fi: intl.Locale = intl.locale("fi") or { return 1; };
let km: intl.NumberFormat = intl.unit_format(fi, "kilometer") or { return 1; };
io.println(km.format_f64(12.5)); // 12,5 kmA currency gets its own number of decimals (EUR 2, JPY 0). Arabic
and other locales with their own digits write them by default where CLDR
says so; ask for others with -u-nu- in the tag ("en-u-nu-arab").
Dates and times
let f: intl.DateTimeFormat = intl.date_time_format(fi, intl.Style.Full, intl.Style.Short, "Europe/Helsinki") or {
return 1;
};
io.println(f.format(time.unix_nanos())); // maanantai 26. lokakuuta 2026 klo 15.05date_time_format(l, date, time, zone) takes a style for the date and
one for the time: Full, Long, Medium, Short, or None (not
both). The zone is an IANA name ("America/New_York", any case) or an
offset ("+05:30"); IntlError.UnknownZone for anything else. The hour
cycle is the locale’s: 3:05 PM in the US, 15.05 in Finland.
| Call | Does |
|---|---|
f.format(unix_ns) | The instant as text; nanoseconds since 1970, as time.unix_nanos() gives |
f.parts(unix_ns) -> Vec_Part | Typed pieces: weekday, day, month, year, hour, minute, dayPeriod, literal … |
f.format_range(start_ns, end_ns) | Jan 10 – 20, 2026, 10:00 – 11:30 AM |
f.pattern() | The CLDR pattern used: MMM d, y |
| Short, medium, long, full dates | |
|---|---|
en | 10/26/26 · Oct 26, 2026 · October 26, 2026 · Monday, October 26, 2026 |
de | 26.10.26 · 26.10.2026 · 26. Oktober 2026 · Montag, 26. Oktober 2026 |
fi | 26.10.2026 · 26.10.2026 · 26. lokakuuta 2026 · maanantai 26. lokakuuta 2026 |
ja | 2026/10/26 · 2026/10/26 · 2026年10月26日 · 2026年10月26日月曜日 |
By skeleton
When a style is not the shape you want (a chat list’s “ma” for a message
from this week, “26. lokakuuta” without the year), name the fields with a
CLDR skeleton: UTS 35’s pattern letters in any order, each as long as the
width you want. The locale decides the order, the punctuation and the
words, as ICU’s DateTimePatternGenerator and Apple’s
dateFormat(fromTemplate:) do.
let wd: intl.DateTimeFormat = intl.date_time_format_skeleton(fi, "EEE", "Europe/Helsinki") or { return 1; };
io.println(wd.format(time.unix_nanos())); // ma (en "Mon", sv "mån", de "Mo")
let day: intl.DateTimeFormat = intl.date_time_format_skeleton(de, "MMMd", "Europe/Berlin") or { return 1; };
io.println(day.format(time.unix_nanos())); // 26. Okt. (en "Oct 26", fi "26.10.")| Skeleton | en | fi | ja |
|---|---|---|---|
EEE, EEEE | Mon, Monday | ma, maanantai | 月, 月曜日 |
MMMd, MMMEd | Oct 26, Mon, Oct 26 | 26.10., ma 26.10. | 10月26日, 10月26日(月) |
yMMMd, yMd | Oct 26, 2026, 10/26/2026 | 26.10.2026 | 2026年10月26日, 2026/10/26 |
MMMMEEEEd | Monday, October 26 | maanantai 26. lokakuuta | 10月26日月曜日 |
yMMMM | October 2026 | lokakuu 2026 | 2026年10月 |
jm, jms | 1:05 PM, 1:05:09 PM | 13.05, 13.05.09 | 13:05, 13:05:09 |
Hm, hm | 13:05, 1:05 PM | 13.05, 1.05 ip. | 13:05, 午後1:05 |
MMMMEEEEdjm | Monday, October 26 at 1:05 PM | maanantai 26. lokakuuta klo 13.05 | 10月26日月曜日 13:05 |
The letters: G era; y year; M month (M 1, MM 01, MMM Oct,
MMMM October, MMMMM O); E weekday (EEE Mon, EEEE Monday,
EEEEE M); d day; j the hour in the locale’s clock (h 12-hour, H
24-hour, J the locale’s without AM/PM); m minute; s second; S
fractions of a second; z, v, O the zone (as GMT+2). The pattern
the locale has for the skeleton is used, else its nearest one with the
fields made as wide as asked; a date and a time are joined as the locale
joins them (“klo”, “at”, “um”). Letters for weeks, quarters and the
week-based year (w, W, Q, Y …), a field twice, or anything but
letters is IntlError.InvalidOption. The format is a DateTimeFormat
like any other: parts, format_range (MMMd: “Jan 10 – 20”, across a
year “Jan 10, 2026 – Jan 1, 2027”; a skeleton with both a date and a time
writes its ranges whole: “Jan 10, 10:00 AM – Jan 12, 9:00 AM”) and
pattern (EEE in Finnish is ccc).
By pattern
When you want the fields in your own order with your own punctuation (a
design’s “Do. 29.10.”, a log’s yyyy-MM-dd'T'HH:mm:ss.SSSXXX), give a
CLDR pattern (ICU’s SimpleDateFormat letters). It is used exactly as
written: the order and the literals stay, and only the fields are the
locale’s words and digits.
let p: intl.DateTimeFormat = intl.date_time_format_pattern(de, "EEE d.M. 'um' HH:mm", "Europe/Berlin") or { return 1; };
io.println(p.format(time.unix_nanos())); // Do. 29.10. um 14:05| Pattern | en | fi | sv | de |
|---|---|---|---|---|
HH:mm | 13:05 | 13:05 | 13:05 | 13:05 |
EEE, ccc | Thu, Thu | to, to | tors, tors | Do., Do |
EEEE, cccc | Thursday, Thursday | torstaina, torstai | torsdag, torsdag | Donnerstag, Donnerstag |
d.M. | 29.10. | 29.10. | 29.10. | 29.10. |
d MMM, LLL | 29 Oct, Oct | 29 loka, loka | 29 okt., okt. | 29 Okt., Okt |
E and M are the forms used inside a date (German “Do.”, Finnish
“torstaina”, “lokakuuta”); c and L the stand-alone forms (“Do”,
“torstai”, “lokakuu”). The hour is as written: HH is 24-hour and h
12-hour in every locale, nothing is adjusted to the locale’s clock.
Quoted text is literal ('klo' HH.mm “klo 13.05”), and '' is a quote
(h 'o''clock' a “1 o’clock PM”). a is AM/PM, b adds noon and
midnight, B the locale’s periods (“in the afternoon”); G the era.
Zones: z, v, O as GMT+2 (long GMT+02:00); X, x and Z the
ISO 8601 offsets (X “Z” at UTC, XXX “+02:00”, Z “+0200”); VV the
zone’s name (Europe/Helsinki). Digits are the locale’s numbering
system’s (ar-u-nu-arab “٢٩ أكتوبر ٢٠٢٦ ١٣:٠٥”; the ISO offsets stay ASCII).
An empty pattern, an unclosed quote, a letter for weeks, quarters, the
week-based year or the day of the year (w, Q, Y, D …), or a
length no letter has (MMMMMM) is IntlError.InvalidOption; a zone it
can’t find IntlError.UnknownZone.
pattern() returns the pattern as given. format_range uses the
locale’s interval formats when the pattern is the locale’s own for its
fields (en MMM d: “Jan 10 – 20”, fi d.M.: “10.–20.1.”), so a
range reads as the format does; any other pattern writes both ends whole
in it (“10 Jan – 20 Jan” for en d MMM), without a year it doesn’t
show.
Only the Gregorian calendar is drawn (Thai dates are in the Gregorian
years, not the Buddhist ones JavaScript uses by default), and a zone’s name
is written as an offset (GMT+2).
Lists, names and relative times
| Function | Does |
|---|---|
list(l, items, kind, width) | ListKind.And “a, b, and c”, Or “a, b, or c”, Unit “3 ft, 7 in”; Width.Long, Short, Narrow |
display_name(l, kind, code) -> ?string | DisplayKind.Language “Canadian French”, Region “Finland”, Script “Cyrillic”, Currency “Euro” |
relative_time(l, value, unit, auto, width) | “in 3 days”, “2 hours ago”; with auto “yesterday”, “tomorrow” |
let items: [3][]const u8 = ["omenat", "appelsiinit", "päärynät"];
io.println(intl.list(fi, items[0..3], intl.ListKind.And)); // omenat, appelsiinit ja päärynät
io.println(intl.relative_time(de, -3, intl.RelativeUnit.Day)); // vor 3 Tagen
io.println(intl.relative_time(fi, 1, intl.RelativeUnit.Day, true)); // huomenna
io.println(intl.display_name(de, intl.DisplayKind.Region, "FI") or { "?" }); // FinnlandTime zones
| Function | Does |
|---|---|
zone(name) -> ?Zone | An IANA zone by name, any case: "Europe/Helsinki", "asia/calcutta" |
z.name() | Its name (an old name gives the current one: Asia/Kolkata) |
z.offset_at(unix_s) -> i64 | Its offset from UTC in seconds at an instant (seconds since 1970) |
z.abbreviation_at(unix_s) | EET, EEST, EST, IST |
z.is_dst_at(unix_s) -> bool | On daylight saving time then? |
zone_names() | All 450 zone names |
let hel: intl.Zone = intl.zone("Europe/Helsinki") or { return 1; };
io.println("{hel.offset_at(1774745999)}"); // 7200: 2026-03-29 00:59:59 UTC, winter
io.println("{hel.offset_at(1774746000)}"); // 10800: an hour later, summer timeThe offset is right for any instant, past or future: each zone’s history
of changes, then its current rules. With time, it gives a
Zoned to print or compute with:
let t: i64 = time.unix_nanos();
let here: time.Zoned = time.zoned(t, hel.offset_at(t / 1000000000));
io.println(here.to_iso8601()); // 2026-10-26T15:05:09+02:00Errors
IntlError: InvalidTag, NoData, InvalidNumber, UnknownZone,
UnknownCurrency, UnknownUnit, InvalidOption; each prints a
description ("{err}").
Notes
- Pure: the same results in every program, VM and native, on every platform.
- Other locales, other calendars and zone names like “Eastern European Time” aren’t there yet.
Last modified: