intl

tape
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

FunctionDoes
locale(tag) -> Locale or IntlErrorA language tag read and matched to the data: "de-AT", "zh-TW", "fi_FI"
available() -> []const []const u8The 33 locales the data has
negotiate(requested, avail) -> ?stringThe 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
tape
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" });   // de

A 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.

FunctionDoes
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
tape
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.34

The rounding modes are JavaScript’s: HalfExpand (school rounding, the default), HalfEven, HalfTrunc, HalfCeil, HalfFloor, Ceil, Floor, Expand, Trunc.

Plurals

FunctionDoes
plural(l, n) -> PluralOne for 1 in English, Few for 3 in Russian
plural_ordinal(l, n) -> PluralEnglish: One for 1st, Two 2nd, Few 3rd, Other 4th
plural_range(l, a, b) -> PluralThe 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:

tape
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

FunctionMakes
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 IntlError50 km/h

Then set what you need, and format:

CallDoes
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_PartThe text in typed pieces (integer, group, decimal, currency …)
nf.format_range(&a, &b) -> string or IntlError3–5, €3.00 – €5.00
nf.plural(&d) -> PluralThe category of the number as this format shows it
tape
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 km

A 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

tape
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.05

date_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.

CallDoes
f.format(unix_ns)The instant as text; nanoseconds since 1970, as time.unix_nanos() gives
f.parts(unix_ns) -> Vec_PartTyped 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
en10/26/26 · Oct 26, 2026 · October 26, 2026 · Monday, October 26, 2026
de26.10.26 · 26.10.2026 · 26. Oktober 2026 · Montag, 26. Oktober 2026
fi26.10.2026 · 26.10.2026 · 26. lokakuuta 2026 · maanantai 26. lokakuuta 2026
ja2026/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.

tape
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.")
Skeletonenfija
EEE, EEEEMon, Mondayma, maanantai月, 月曜日
MMMd, MMMEdOct 26, Mon, Oct 2626.10., ma 26.10.10月26日, 10月26日(月)
yMMMd, yMdOct 26, 2026, 10/26/202626.10.20262026年10月26日, 2026/10/26
MMMMEEEEdMonday, October 26maanantai 26. lokakuuta10月26日月曜日
yMMMMOctober 2026lokakuu 20262026年10月
jm, jms1:05 PM, 1:05:09 PM13.05, 13.05.0913:05, 13:05:09
Hm, hm13:05, 1:05 PM13.05, 1.05 ip.13:05, 午後1:05
MMMMEEEEdjmMonday, October 26 at 1:05 PMmaanantai 26. lokakuuta klo 13.0510月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.

tape
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
Patternenfisvde
HH:mm13:0513:0513:0513:05
EEE, cccThu, Thuto, totors, torsDo., Do
EEEE, ccccThursday, Thursdaytorstaina, torstaitorsdag, torsdagDonnerstag, Donnerstag
d.M.29.10.29.10.29.10.29.10.
d MMM, LLL29 Oct, Oct29 loka, loka29 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

FunctionDoes
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) -> ?stringDisplayKind.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”
tape
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 { "?" });   // Finnland

Time zones

FunctionDoes
zone(name) -> ?ZoneAn 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) -> i64Its 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) -> boolOn daylight saving time then?
zone_names()All 450 zone names
tape
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 time

The 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:

tape
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:00

Errors

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: