diff --git a/web/src/lib/journal.test.ts b/web/src/lib/journal.test.ts new file mode 100644 index 000000000..5d198a067 --- /dev/null +++ b/web/src/lib/journal.test.ts @@ -0,0 +1,44 @@ +// Every journal date is a Seoul time ("2026-05-14T01:30:00+09:00"), and the +// pages are built wherever the build runs. On 27 October a build in UTC printed +// 60 of the 520 dates on /journal a day early: headache, stopped, +// i-hope-it-works-out and negative were written between midnight and 9 a.m. in +// Seoul, which is still the day before in UTC. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { formatJournalDate, getAllJournalPosts, type JournalLang } from "./journal"; + +// UTC is a build server, Los Angeles is nine hours further behind it, and +// Kiritimati (UTC+14) is the one that runs ahead of Seoul. +const ZONES = ["UTC", "America/Los_Angeles", "Pacific/Kiritimati", "Asia/Seoul"]; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe("formatJournalDate", () => { + it.each(ZONES)("prints the Seoul day when the process runs in %s", (zone) => { + vi.stubEnv("TZ", zone); + + expect(formatJournalDate("2026-05-14T01:30:00+09:00", "ko")).toBe("2026년 5월 14일"); + expect(formatJournalDate("2026-05-14T01:30:00+09:00", "en")).toBe("May 14, 2026"); + expect(formatJournalDate("2026-05-23T07:00:00+09:00", "en")).toBe("May 23, 2026"); + expect(formatJournalDate("2026-10-04T23:30:00+09:00", "ko")).toBe("2026년 10월 4일"); + }); + + it("gives every post the day written in its frontmatter", () => { + vi.stubEnv("TZ", "UTC"); + const wrong: string[] = []; + + for (const lang of ["ko", "en"] satisfies JournalLang[]) { + for (const post of getAllJournalPosts({ lang, includeDrafts: true })) { + const day = post.date.match(/^(\d{4})-(\d{2})-(\d{2})T[^+]*\+09:00$/); + if (!day) continue; + const want = formatJournalDate(`${day[1]}-${day[2]}-${day[3]}T12:00:00+09:00`, lang); + const shown = formatJournalDate(post.date, lang); + if (shown !== want) wrong.push(`${post.slug} (${lang}): ${shown}, written ${want}`); + } + } + + expect(wrong).toEqual([]); + }); +}); diff --git a/web/src/lib/journal.ts b/web/src/lib/journal.ts index 191c088c1..6f0ec59e1 100644 --- a/web/src/lib/journal.ts +++ b/web/src/lib/journal.ts @@ -128,10 +128,13 @@ export function getAdjacentJournalPosts(slug: string, lang: JournalLang = "ko") }; } +// The dates are written in Seoul (+09:00). Without a zone the day is the build +// machine's, and a post written at 01:30 in Seoul is the day before in UTC. export function formatJournalDate(date: string, lang: JournalLang = "ko") { return new Intl.DateTimeFormat(lang === "ko" ? "ko-KR" : "en", { month: "long", day: "numeric", year: "numeric", + timeZone: "Asia/Seoul", }).format(new Date(date)); }