All lessons

Lesson 08·Unit 3 · The Intl toolbox·Calendar systems & week conventions·Lv 201·~8 min

Calendar systems, eras & the shape of the week

Thailand's official year is 2569. Japan's paperwork says Reiwa 8. A Saudi contract may be dated 1448. The Gregorian calendar is one calendar among many that CLDR ships, and the week doesn't start on the same day everywhere either.

Intl.DateTimeFormat-u-ca-getWeekInfo

By the end

  • ·Render one date in another calendar with a -u-ca- extension, and ask resolvedOptions() which one a locale picked.
  • ·Explain why one day carries four different year numbers (2026, Reiwa 8, 2569 BE, 1448 AH), and why an era resets the count.
  • ·Read a locale's week shape from Intl.Locale instead of hardcoding it, and say why "W1" means two different weeks.

The problem

Date code tends to hardcode three assumptions: the year number is universal, the era is ancient history, and the week runs Monday-to-Sunday with a Saturday–Sunday weekend. All three are locale data. A birth-year dropdown that only offers Gregorian years, a “this year” filter that compares against 2026, or a booking grid that shades the wrong weekend each ships a real bug in Bangkok, Tokyo, Riyadh, or Tel Aviv.

How it works

The year number is an output, not a fact

Most date code treats the year as a constant that locales decorate: the same 2026 everywhere, with the day and month shuffled around it. A calendar system sits a level below that. It decides what turns a moment into a year, month, and day at all, and locales disagree about it. Take July 29, 2026. Thailand's official Buddhist calendar calls that year 2569. The Umm al-Qura calendar used in Saudi Arabia says 1448, the Hebrew calendar 5786, and the Japanese one Reiwa 8.

BCP 47 carries the choice in the -u-ca- extension: en-u-ca-hebrew means English text, Hebrew calendar. When the tag has no extension, CLDR data picks the default for the region. That is how plain th-TH resolves to buddhist and fa-IR to persian with no code changes on your side. The design point of putting it in the tag is that the choice travels with the request: a server, a database export, and a client that see the same tag produce the same rendering.

The default itself is data, and data gets revised. Older CLDR releases defaulted ar-SA to the Umm al-Qura calendar. Current ones default it to gregory, reflecting Saudi Arabia's 2016 switch to Gregorian dates for civil purposes. Two browsers on your users' desks can disagree about this today. That is why the demo computes its locale table in your runtime instead of shipping a screenshot. resolvedOptions().calendar tells you what happened, and Intl.supportedValuesOf("calendar") lists what the runtime ships (18 systems in Node 22).

How it works

Offset, astronomy, or decree

Calendars differ in how much arithmetic you can safely do on them. The Buddhist and Minguo calendars are pure offsets on Gregorian months: add 543 for Thailand, subtract 1911 for Taiwan, and the month structure never moves. The lunar and lunisolar families are not offsets at all. An Islamic calendar year runs 354 or 355 days, so every Hijri date lands about 10 to 11 days earlier on the Gregorian calendar each year. The Umm al-Qura variant follows a table published in Saudi Arabia rather than a formula you can inline. The Hebrew calendar keeps lunar months aligned with solar seasons by inserting an entire leap month, Adar I, in 7 years of every 19.

The Japanese calendar adds a third mechanism: the era, a named span that resets the year counter by decree. January 7, 1989 was Shōwa 64, and the next morning was Heisei 1. April 30, 2019 was Heisei 31, the next day was Reiwa 1, and Shōwa 64 itself lasted seven days. A government announces an era name. No formula computes it. Reiwa became public one month before it took effect. That is why hardcoded era tables are a time bomb, and calendar data channels (CLDR, ICU, OS updates) are the fix.

The transferable rule: only the offset calendars are safe to compute by hand, and even there you should not, because Intl.DateTimeFormat with -u-ca- already knows all three mechanisms. Store ISO 8601 internally, convert at the edge.

const d = new Date(Date.UTC(2026, 6, 29));
for (const ca of ["gregory", "japanese", "buddhist", "islamic-umalqura"]) {
  console.log(ca, new Intl.DateTimeFormat("en-u-ca-" + ca,
    { dateStyle: "full", timeZone: "UTC" }).format(d));
}
// gregory           Wednesday, July 29, 2026
// japanese          Wednesday, July 29, 8 Reiwa
// buddhist          Wednesday, July 29, 2569 BE
// islamic-umalqura  Wednesday, Safar 15, 1448 AH
One instant, four year counters (Node 22, ICU 78).

How it works

The week has a shape, and it is not yours

Three more values come with the calendar: which day the week starts on, which days are the weekend, and how many days of January a week needs before it counts as week 1. Intl.Locale is where you ask. One wrinkle is a lesson in reading runtimes. The current spelling is getWeekInfo(), but older engines, Node 22's V8 among them, expose the same data as a weekInfo property, so defensive code checks for both. Node reports minimalDays while current Chrome omits it. That is why the table below prints a dash when your engine stays quiet. Days run 1 for Monday through 7 for Sunday.

The values move more than people expect. The US starts its grid on Sunday with a Saturday-Sunday weekend. Germany starts Monday. Saudi Arabia and Israel also start Sunday, but their weekend is Friday and Saturday. A booking UI that shades Saturday-Sunday as "the weekend" is wrong in both markets in different directions: it greys out a working day and leaves a rest day bookable.

The quiet one is minimalDays, the week-numbering rule. The ISO 8601 convention used across most of Europe wants week 1 to hold at least 4 days of the new year. The US convention accepts 1. January 1, 2027 falls on a Friday. An American report calls that week "week 1 of 2027" while an ISO report files the same days under week 53 of 2026. Two dashboards can sit a full week apart on what "W1" means, and both are right for their locale.

See it yourself

Try these, in order

Each step triggers a specific failure you should recognize on sight.

  1. 1
    In One day, eight year counters, set Date (Gregorian) to 2019-05-01. The japanese row flips to 1 Reiwa while gregory still reads 2019. Step back one day to 2019-04-30 and the same row reads 31 Heisei: the year counter reset mid-week, by decree.
  2. 2
    Set the date to 2026-06-16. The islamic-umalqura row reads Muharram 1, 1448: a new year in the middle of June. Set the same date in 2027 and the Hijri year has already rolled to 1449, because a 354-day year drifts about 10 to 11 days against Gregorian every cycle.
  3. 3
    Read the ar-SA row in What your browser ships, then open this page in another browser if you have one handy. The default calendar is CLDR data, not a constant: current runtimes report gregory for ar-SA, older ones report islamic-umalqura. resolvedOptions().calendar is how code finds out instead of assuming.
  4. 4
    In The shape of the week, compare the ar-SA and he-IL rows against en-US. All three start their week on Sunday, but the shaded weekend is Friday and Saturday. A calendar grid that hardcodes a Saturday-Sunday weekend ships wrong in Riyadh and Tel Aviv in different directions.
  5. 5
    Read the Min. days column for en-US and de-DE. In an engine that reports it you see 1 versus 4. Current Chrome shows a dash because its getWeekInfo() omits the value. The number is the week-numbering rule: the US calls the week containing January 1 week 1, while the ISO rule (minimalDays 4) can file the first days of January under the previous year's week 53. "W1" in a report header is ambiguous until you name the rule.
  6. 6
    In What your browser ships, type am-ET into the locale field and press Add locale. A new row appears with the calendar your engine resolves for Amharic in Ethiopia, rendered in Ge'ez script. Any well-formed BCP 47 tag works, and a tag with no data still resolves through fallback. This table is your runtime's CLDR snapshot, live.

One day, eight year counters

Pick a date. Every row is the same day, formatted by Intl.DateTimeFormat with a different -u-ca- calendar. Only the first row agrees with your filing system about what year it is.

CalendarYear readsFull date (en)
gregoryThe ISO / trade default. Not the world's only civil calendar.2026Wednesday, July 29, 2026
japaneseGregorian months, but the year counter resets per emperor.8 ReiwaWednesday, July 29, 8 Reiwa
buddhistThailand's official calendar. Gregorian + 543.2569 BEWednesday, July 29, 2569 BE
islamic-umalquraSaudi Arabia's calendar. Lunar: a year is 354–355 days.1448 AHWednesday, Safar 15, 1448 AH
hebrewLunisolar: a leap year inserts a whole month (Adar I).5786Wednesday, 15 Av 5786
persianIran and Afghanistan. Solar, new year at the March equinox.1405 APWednesday, Mordad 7, 1405 AP
rocTaiwan's official era: year 1 = 1912.115 MinguoWednesday, July 29, 115 Minguo
indianIndia's civil Śaka calendar, published alongside Gregorian.1948 ŚakaWednesday, Sravana 7, 1948 Śaka

The full list comes from Intl.supportedValuesOf("calendar").

What your browser ships

No -u-ca- extension here: each row asks resolvedOptions().calendar which calendar the plain locale tag gets by default. The demo computes this table in your browser on purpose. The ar-SA default changed from islamic-umalqura to gregory in recent CLDR releases, so two browsers can disagree about it today.

Locale tagDefault calendarYour browser renders
en-US
th-TH
fa-IR
ar-SA
ja-JP
ja-JP-u-ca-japanese

Thailand runs on the Buddhist calendar by default: a th-TH user reads this year as 2569 BE. Iran resolves to the Persian calendar. Users can also override the calendar per-device, so treat the default as a starting point, not a fact about the user. A tag your engine has no data for still resolves: it falls back, and the row shows what the fallback ships.

An era boundary, frame by frame

The Japanese calendar restarts its year counter when an emperor accedes. Both modern transitions, rendered by -u-ca-japanese: Shōwa 64 lasted seven days, and April 30 → May 1, 2019 took the year from Heisei 31 to Reiwa 1 mid-week.

Last day of Shōwa · 1989-01-07

January 7, 64 Shōwa

昭和64年1月7日

First day of Heisei · 1989-01-08

January 8, 1 Heisei

平成元年1月8日

Last day of Heisei · 2019-04-30

April 30, 31 Heisei

平成31年4月30日

First day of Reiwa · 2019-05-01

May 1, 1 Reiwa

令和元年5月1日

Walk across a boundary yourself

May 1, 1 Reiwa

令和元年5月1日

May 1, 2019 · boundary day

The 2019 transition was announced one month in advance. Unicode 12.1.0 shipped with exactly one new character for it: U+32FF ㋿, the square Reiwa era sign.

The shape of the week

Which day a calendar grid starts on and which days are the weekend are CLDR data, read here from Intl.Locale week info in your browser. Shaded cells are that locale's weekend. The first column is that locale's first day of the week.

Reading week data…

Min. days is the week-numbering rule: how many days of January a week must contain to count as week 1. Most of Europe says 4 (the ISO 8601 rule), the US says 1. January 1, 2027 falls on a Friday. A US calendar files it in week 1, an ISO calendar in the last week of 2026. “Week 1” in a report header is ambiguous until you name the rule. A dash in that column means your engine's week info omits the value (current Chrome does). The CLDR data behind it exists either way.

Things you almost certainly believe that are wrong

  • Everyone agrees what year it is. A Thai government form says 2569 BE. A Saudi contract may be dated 1448 AH. A Japanese tax office says 8 Reiwa.
  • A year is 365 days, give or take a leap day. An Islamic calendar year is 354 or 355 days, so every Hijri date lands about 10–11 days earlier on the Gregorian calendar each year. A Hebrew leap year inserts an entire extra month.
  • Eras are ancient history. Reiwa 1 began May 1, 2019. Japanese public paperwork uses era years today, and birth-year dropdowns that only offer Gregorian years are a localization bug there.
  • The weekend is Saturday and Sunday. Saudi Arabia and Israel rest Friday–Saturday. A booking UI that shades Sat–Sun as “the weekend” is wrong in both markets.
  • You can convert between calendars with arithmetic. Buddhist is Gregorian + 543. But Umm al-Qura month lengths follow a published Saudi table, and Hebrew leap months follow a 19-year cycle. Ask the calendar API. Do not do date math.

If you remember one thing

The year number, the era, and the shape of the week are all locale data. Store ISO 8601, format through the calendar the locale resolves, and never do arithmetic on a formatted year.

What to do about it

  • Store dates in ISO 8601 / proleptic Gregorian internally, and convert at the display edge with Intl.DateTimeFormat. The locale (or an explicit -u-ca- extension) picks the calendar; your storage format never changes.
  • Never do arithmetic on a formatted year. “Gregorian + 543” works for Thailand and for nothing else: Hijri months follow a published table and Japanese eras change by accession.
  • Read the week's shape from Intl.Locale week info (getWeekInfo(), or the older weekInfo property) instead of hardcoding first day and weekend.
  • Treat era tables as data with an update path. The Reiwa era was announced one month before it took effect; systems with hardcoded era lists needed emergency patches, systems reading CLDR/ICU data needed a data update.

Use this with

Stakeholders

EngineeringProductQA

Moments

  • ·Designing date pickers, calendars, or scheduling features
  • ·Entering markets with a non-Gregorian civil calendar (TH, JP, SA, IR, IL, TW)
  • ·Defining “week 1” for reports and analytics

Field note

The Reiwa transition was a live test for the whole industry. Japan announced the era name on April 1, 2019, and it took effect on May 1, 2019, thirty days later. In that window Unicode shipped release 12.1.0 with exactly one new character, U+32FF, the square Reiwa era sign, so receipts and forms could keep the single-glyph era abbreviation convention. Teams that read era data from CLDR/ICU took an ordinary data update. Teams with hardcoded era tables took an emergency release.

Unicode 12.1.0 (the single-character release)

Quick check

3 questions · pass at 2+

  1. Question 1/3

    A user in Bangkok submits a form dated year 2569. What happened?

  2. Question 2/3

    Your date library ships a hardcoded table of Japanese eras. Why is that a time bomb rather than a fact?

  3. Question 3/3

    Your booking calendar renders Monday-first columns and shades Saturday and Sunday as the weekend. Which markets file bugs, and what is the fix?

Words you'll hear

Where to read more

Related lessons