Convert a Gregorian Date to a Japanese Era Date
To convert a Gregorian date into a Japanese-era date, attach the japanese calendar to a Temporal.PlainDate with .withCalendar('japanese'), then read its .era (reiwa, heisei, showa) and .eraYear properties — or format the whole thing with Intl.DateTimeFormat using calendar: 'japanese'. This page is part of Calendar Systems and Era Handling.
Why This Scenario Is Tricky
The Japanese imperial calendar counts years from the start of the current emperor's era (元号, gengō), not from a fixed epoch. "2019" is not a single Japanese year: January through April 30 was Heisei 31, and May 1 onward was Reiwa 1 (年号 written as 令和元年, "Reiwa first year"). The era changed mid-year, so any naive gregorianYear - offset formula is wrong for dates straddling that boundary.
Legacy Date cannot help here at all: it has no era concept and assumes the proleptic Gregorian calendar. You would have to hard-code the era transition table yourself — and keep it current whenever a new emperor accedes. The Temporal API and Intl both defer to the CLDR/ICU era data shipped with the runtime, so the boundaries stay correct without bespoke tables.
The diagram below shows the most recent era transitions on the Gregorian timeline and where the mid-year Heisei→Reiwa boundary falls.
Minimal Working Solution
The shortest correct conversion reads the era fields straight off a japanese-calendar PlainDate:
import { Temporal } from '@js-temporal/polyfill';
// Start from an ISO/Gregorian date, then re-interpret it in the Japanese calendar
const jp = Temporal.PlainDate.from('2024-05-01').withCalendar('japanese');
console.log(jp.era); // 'reiwa' — CLDR era id, not the Gregorian year
console.log(jp.eraYear); // 6 → Reiwa 6
console.log(jp.year); // 2024 — the ISO year is always still available
.withCalendar() does not move the day; it re-labels the same instant in a different calendar system. The underlying ISO date is unchanged, so .year still returns 2024 while .eraYear returns the era-relative count.
Full Production Version
A real converter should accept either an ISO string or a PlainDate, validate input, and return both the structured era data and a localized display string. Caching the Intl.DateTimeFormat instance matters because constructing one is expensive.
import { Temporal } from '@js-temporal/polyfill';
interface JapaneseEraDate {
era: string; // 'reiwa' | 'heisei' | 'showa' | …
eraYear: number; // year within the era (1-based)
isoYear: number; // ISO/Gregorian year
display: string; // localized, e.g. '令和6年5月1日'
}
// Construct the formatter once and reuse it — building it per call is costly
const jaFormatter = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
era: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC', // PlainDate has no time/zone; pin UTC to avoid host-zone day shifts
});
export function toJapaneseEra(input: string | Temporal.PlainDate): JapaneseEraDate {
const iso =
typeof input === 'string' ? Temporal.PlainDate.from(input) : input;
if (iso.calendarId !== 'iso8601' && iso.calendarId !== 'gregory') {
throw new RangeError(`Expected an ISO/Gregorian date, got ${iso.calendarId}`);
}
const jp = iso.withCalendar('japanese');
// Intl.format() takes a Date, not a Temporal value — build a UTC-midnight Date
// so the calendar day cannot drift backward in a negative-offset host zone
const asDate = new Date(Date.UTC(iso.year, iso.month - 1, iso.day));
return {
era: jp.era ?? 'unknown',
eraYear: jp.eraYear ?? jp.year,
isoYear: iso.year,
display: jaFormatter.format(asDate),
};
}
console.log(toJapaneseEra('2024-05-01'));
// { era: 'reiwa', eraYear: 6, isoYear: 2024, display: '令和6年5月1日' }
For an English rendering, swap the locale and the era/month token widths:
const enFormatter = new Intl.DateTimeFormat('en-US-u-ca-japanese', {
era: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC', // same UTC pin — keep the displayed day stable across servers
});
const may1 = Temporal.PlainDate.from('2024-05-01');
const asDate = new Date(Date.UTC(may1.year, may1.month - 1, may1.day));
console.log(enFormatter.format(asDate)); // 'May 1, 6 Reiwa'
Verifying the Mid-Year Era Boundary
The whole point of using calendar data instead of arithmetic is the 2019 Heisei→Reiwa switch on May 1. This assertion block proves both sides of that boundary resolve correctly:
import { Temporal } from '@js-temporal/polyfill';
const lastHeisei = Temporal.PlainDate.from('2019-04-30').withCalendar('japanese');
const firstReiwa = Temporal.PlainDate.from('2019-05-01').withCalendar('japanese');
// 30 April 2019 is the final day of Heisei (Heisei 31)
console.assert(lastHeisei.era === 'heisei', 'Apr 30 2019 should be Heisei');
console.assert(lastHeisei.eraYear === 31, 'Apr 30 2019 should be Heisei 31');
// 1 May 2019 is the first day of Reiwa (Reiwa 1 — 元年, the "gan-nen" first year)
console.assert(firstReiwa.era === 'reiwa', 'May 1 2019 should be Reiwa');
console.assert(firstReiwa.eraYear === 1, 'May 1 2019 should be Reiwa 1');
// Both dates share the same ISO year despite different eras
console.assert(lastHeisei.year === firstReiwa.year, 'Both are ISO year 2019');
Common Pitfalls
- Computing the era from the Gregorian year alone.
2019 - 1988 = 31(Heisei) is correct for January but wrong from May onward. Wrong:const eraYear = isoYear - 1988. Right:iso.withCalendar('japanese').eraYear, which respects the mid-year boundary. - Forgetting the
timeZonewhen formatting aPlainDate. WithouttimeZone: 'UTC',Intl.DateTimeFormat.format()interprets the synthesizedDatein the host zone and can roll a midnight date back one day west of UTC. Wrong:new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { era: 'long' }). Right: includetimeZone: 'UTC'. - Treating
.eraas a display label.jp.erareturns a CLDR id like'reiwa', not 令和. Wrong:el.textContent = jp.era. Right: letIntl.DateTimeFormatproduce the localized era text withera: 'long'orera: 'short'. - Re-building the formatter inside a loop. Wrong:
rows.map(r => new Intl.DateTimeFormat(...).format(r)). Right: construct one formatter outside the loop and reuse it.
Frequently Asked Questions
Why does .eraYear return 1 instead of 2019 for May 1, 2019?
Because the Japanese calendar counts years within the current era, not from a fixed epoch. May 1, 2019 is the first day of Reiwa, so .eraYear is 1 (written 令和元年). The ISO year 2019 is still available on the separate .year property.
Will Temporal know about a future era before the runtime is updated?
No. Era boundaries come from the CLDR/ICU data bundled with the JavaScript engine (or the @js-temporal/polyfill build). A new imperial era is only recognized once that data is updated, so keep your runtime and polyfill current for forward-dated calculations.