1138 字
6 分鐘

JavaScript 日期差一天?用 Intl.DateTimeFormat 分開語系與時區

API 回傳 2026-10-19T16:30:00Z,台灣頁面卻顯示 10 月 19 日。若你用 toISOString().slice(0, 10) 取得日期,拿到的是 UTC 日期;同一個時間點在台北已經是 10 月 20 日。

zh-TW 選擇文字格式,Asia/Taipei 才選擇時區。 想讓伺服器與瀏覽器按照相同地區顯示時間,兩端都要明確指定時區,而不是依賴各自的系統預設。

先問輸入代表什麼#

這三種資料不該走同一條轉換路徑:

輸入資料意義處理方向
2026-10-19T16:30:00Z已確定的時間點轉成 Date,再依顯示時區格式化
2026-10-20生日、帳單日等純日期保留日期語意,不任意套用顯示時區
2026-10-20T09:00:00未附時區的日期時間先決定它屬於哪個地區,再轉成時間點

Date.parse 的規則區分只有日期與沒有 offset 的日期時間:標準日期字串按 UTC 解讀,沒有時區的日期時間則依執行環境的本地時區。伺服器和使用者電腦的設定不同,就可能在解析階段已經得到不同時間點。

因此,API 若要交換確定的事件時間,可約定傳帶 Z/offset 的標準格式或 timestamp;純日期欄位則留在日期型別的契約裡。不要為了統一顯示,替所有生日資料補上午夜 Z。

顯示給台灣讀者時明確指定 timeZone#

Intl.DateTimeFormat會按照選項格式化既有時間點,不會改變 Date 內的 timestamp:

const instant = new Date('2026-10-19T16:30:00Z');
const formatter = new Intl.DateTimeFormat('zh-TW', {
timeZone: 'Asia/Taipei',
dateStyle: 'medium',
timeStyle: 'short',
});
console.log(formatter.format(instant));
console.log(instant.toISOString()); // 仍是 2026-10-19T16:30:00.000Z

SSR 也要用相同選項。如果伺服器用 UTC 預設、瀏覽器用使用者時區,兩端就可能顯示不同日。需要個人化時區時,先決定由伺服器取得使用者設定,或等瀏覽器接手後再顯示;不要靠隱藏 hydration 警告解決資料契約差異。

日期鍵不要拆本地化顯示字串#

畫面可能使用斜線、空白或在地化數字。若需求是依「台灣當天」分組,可用 formatToParts() 按欄位取值,避免假設 format() 的字串永遠是固定順序。

下面只接受已建立且有效的 Date;上游字串格式、真實日曆日期的嚴格驗證要在輸入邊界另外處理。範例以西元四位數年份的業務資料為範圍。

function dateKeyAt(instant, timeZone) {
if (!(instant instanceof Date) || !Number.isFinite(instant.getTime())) {
throw new TypeError('需要有效的 Date 時間點');
}
const formatter = new Intl.DateTimeFormat('en', {
timeZone,
calendar: 'gregory',
numberingSystem: 'latn',
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const parts = Object.fromEntries(
formatter.formatToParts(instant)
.filter(({ type }) => ['year', 'month', 'day'].includes(type))
.map(({ type, value }) => [type, value]),
);
return `${parts.year}-${parts.month}-${parts.day}`;
}
const instant = new Date('2026-10-19T16:30:00Z');
if (dateKeyAt(instant, 'Asia/Taipei') !== '2026-10-20') {
throw new Error('台北日期錯誤');
}
if (dateKeyAt(instant, 'UTC') !== '2026-10-19') {
throw new Error('UTC 日期錯誤');
}

calendar 與 numberingSystem 讓這個資料鍵明確使用西元與拉丁數字;它和中文畫面的顯示格式可以分開。若使用不同 iframe realm 的 Date 或 Temporal 型別,要先在邊界正規化,這個簡化 helper 沒有涵蓋那些型別。

夏令時間不能固定加減幾小時#

台灣資料可能只顯示台北時間,但跨國使用者會遇到夏令時間。以下兩個 UTC 時間點不同,卻都落在紐約當地的凌晨 1<30>;要用時區名稱補充顯示,不能只保存「1<30>」當作唯一時間:

const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZoneName: 'shortOffset',
});
const a = new Date('2026-11-01T05:30:00Z');
const b = new Date('2026-11-01T06:30:00Z');
console.log(formatter.format(a)); // 此環境:01:30 GMT-4
console.log(formatter.format(b)); // 此環境:01:30 GMT-5
if (a.getTime() === b.getTime()) throw new Error('應為不同時間點');

這是針對時區資料庫規則的示例,並非宣稱未來已發生某個事件。未來排程若定義成「當地每天 9 點」,還要保存地區時區與排程規則;Intl 負責格式化,不會替你排程或把模糊的牆上時間反解成唯一 timestamp。

本文程式區塊於 2026-10-20 在 Node.js 24.13.0 執行,日期鍵另在 TZ=UTC、TZ=Asia/Taipei、TZ=America/Los_Angeles 驗證。這證明明確時區不依賴上述預設值,不代表所有瀏覽器的標點、縮寫與時區資料庫版本完全相同。測試宜比較 formatToParts() 的欄位與 timestamp,不把整段在地化輸出當成固定 API 格式。

需要產生行事曆事件時,可接著檢查日曆連結的開始/結束時間與 TZID;日期顯示正確,不代表匯出的事件時區也正確。

參考資料:

MDN:Intl.DateTimeFormat

MDN:DateTimeFormat 選項

MDN:formatToParts()

MDN:Date.parse()

JavaScript 日期差一天?用 Intl.DateTimeFormat 分開語系與時區
https://laplusda.com/posts/javascript-intl-datetimeformat-timezone-date/
作者
Zero
發佈於
2026-10-20
許可協議
CC BY-NC-SA 4.0