多語系應用程式常把「介面翻譯」和「日期、數字處理」當成同一件事。這在多數語系看似可行,但切換到泰文文化時,預設日曆可能讓年份採用佛曆表示。若機器可讀日期也跟著請求文化解析,查詢區間、匯入資料與排程參數就可能得到語意錯誤但格式合法的值。本文以 .NET 10 與可重現測試說明如何把兩種責任分開。

核心結論:翻譯文化與資料文化必須分離

.NET CurrentCulture 泰文佛曆日期問題的根本解法,是讓 CurrentUICulture 負責資源翻譯,讓 CurrentCulture 或明確指定的格式提供者負責機器日期與數字;資料邊界一律使用固定格式及 InvariantCulture。

CurrentCulture 影響文化敏感的日期與數字格式化,也會影響沒有明確指定格式提供者的解析操作。CurrentUICulture 的主要責任則是協助資源管理器選擇對應語言資源。兩者可以相同,但多語系網站不應假設它們永遠必須相同。使用者想看到泰文介面,不等於資料庫查詢條件、API 交換格式或批次檔案中的年份也要改成佛曆。

建議把資料分成兩條路徑。顯示給人的完整日期可以依使用者文化呈現,並清楚標示曆法;傳送給程式、寫入結構化欄位或作為查詢條件的日期,則採 ISO 風格固定格式,解析時明確指定 InvariantCulture。這種邊界設計不依賴執行緒當下狀態,測試也能直接觀察輸入與輸出。

為什麼泰文文化會讓年份看起來多了 543

泰文文化的日期格式可使用 ThaiBuddhistCalendar,因此同一個代表西元二〇二六年的 DateTime,以 th-TH 的 yyyy 格式輸出時會顯示二五六九;瞬間本身沒有移動,改變的是文化化的年份表示。

這個差異容易被誤判成時區錯誤。時區處理的是瞬間與地方時間之間的換算,曆法處理的是同一日期如何命名年、月、日。若程式內的 DateTime 是二〇二六年三月十一日,以泰國佛曆格式顯示二五六九年是格式化規則,不代表物件被加了五百四十三年。然而,若程式再用相同文化把字串「2026-03-11」當成佛曆輸入解析,得到的實際西元年份便可能遠早於預期,這就不只是顯示差異。

風險最大的情境是輸入字串長得完全合理,解析函式也回報成功。程式因此不會拋出例外,只會拿錯誤年份去篩選資料,最後得到空集合。監控若只看例外率,這種錯誤很容易漏掉。正確的檢查方式必須同時記錄文化名稱、日曆型別、原始輸入、解析後年份及採用的格式提供者,但公開記錄時不應包含使用者資料。

來回格式化為何會掩蓋日期錯誤

若字串先按某文化解析,再按同一文化格式化,年份偏移可能在輸入與輸出兩端互相抵銷,畫面仍呈現原字串;只有檢查解析後物件的 Year、查詢結果與固定文化輸出,才會看到真正差異。

例如,輸入看起來是四位數年份,解析後再用相同日曆輸出,也可能得到相同的四位數文字。開發者若只比較輸入與畫面,就會錯把對稱轉換當成正確性證據。真正要問的是:解析後的時間值是否代表預期的公曆日期?傳入資料查詢前,年份是否落在允許範圍?輸出到另一個服務時,對方是否依相同曆法解讀?

因此測試要跨越文化邊界,而不是只測同文化往返。至少保留一個 DateTime.Year 斷言、一個 InvariantCulture 格式輸出斷言,以及一個使用固定格式重新解析的斷言。若輸入規格只允許公曆年月日,應把格式及文化寫進程式碼,不應期待執行緒文化剛好符合規格。

設計文化隔離邊界

最穩健的設計是把介面文字、給人看的日期、機器交換日期分成三層;第一層跟隨 CurrentUICulture,第二層依產品規則選擇文化,第三層固定使用可明確解析的格式與 InvariantCulture。

第一層是資源翻譯,例如按鈕、欄位名稱與說明文字。設定 CurrentUICulture 後,資源管理器可選擇泰文資源。第二層是人類顯示內容,這裡可以尊重地區慣例,但產品需決定是否同時顯示公曆年份、時區或曆法標籤。第三層是資料契約,包括 URL 參數、JSON 日期字串、CSV 欄位、訊息內容與資料庫查詢條件。這一層要避免模糊格式。

對只含日期的欄位,可使用 yyyy-MM-dd 並以 DateOnly.TryParseExact 或 DateTime.TryParseExact 搭配固定文化。對代表瞬間的欄位,應選擇包含時區偏移的標準格式,並在資料模型中區分地方日期與時間點。本文聚焦曆法隔離,不把時區問題混在同一個修補裡,因為兩者需要不同測試條件。

也不要在每個控制器或服務隨手設定文化。建立單一文化設定入口,限制誰能改動執行脈絡,再讓資料邊界方法一律要求格式提供者。如此即使未來加入其他具有不同日曆或數字符號的文化,核心資料仍維持穩定。

可重現驗證:用 .NET 10 測試泰文佛曆與固定文化

下列測試已在隔離的本機主控台專案實際執行,SDK 為 .NET 10.0.400;它不連線外部資料、不修改既有應用程式,只驗證日曆型別、年份格式、固定格式解析及 UI 文化分離。

  1. 建立空白目錄,確認 dotnet --version 顯示 10.0.400,再執行 dotnet new console --framework net10.0 --force。
  2. 將下列程式存成 Program.cs;程式只建立記憶體物件並以斷言判定結果。
  3. 執行 dotnet run --configuration Release;全部斷言通過時,最後一行必須顯示 PASS,任何文化行為不同則以非零結束。
using System.Globalization;

static void Assert(bool condition, string message)
{
    if (!condition) throw new InvalidOperationException(message);
}

var thai = CultureInfo.GetCultureInfo("th-TH");
var date = new DateTime(2026, 3, 11);
Assert(thai.DateTimeFormat.Calendar is ThaiBuddhistCalendar,
    "Expected ThaiBuddhistCalendar.");
Assert(date.ToString("yyyy", thai) == "2569",
    "Expected Buddhist Era year 2569.");

var parsed = DateTime.ParseExact(
    "2026-03-11", "yyyy-MM-dd",
    CultureInfo.InvariantCulture, DateTimeStyles.None);
Assert(parsed.Year == 2026, "Expected Gregorian year 2026.");

CultureInfo.CurrentUICulture = thai;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Assert(date.ToString("yyyy-MM-dd") == "2026-03-11",
    "Machine date must remain Gregorian.");
Console.WriteLine("PASS: all culture-isolation checks succeeded.");

本次真實結果為:th-TH 的日曆型別是 ThaiBuddhistCalendar;西元二〇二六格式化為佛曆二五六九;固定文化解析後 Year 為二〇二六;介面文化設為泰文而格式文化固定時,機器日期仍為 2026-03-11。這些結果只代表列出的 SDK、作業系統與測試條件,不應包裝成跨版本效能或可靠度數字。

ASP.NET Core 中的落地方式

在 ASP.NET Core 中,請先決定每個輸入欄位是本地化文字還是固定資料契約,再選擇繫結與驗證策略;不要只靠全域請求文化,因為它無法替每一種日期語意做出正確決定。

介面資源可以跟隨請求語言,但查詢模型中的機器日期可保留為字串,進入應用服務前以固定格式解析,失敗時回傳明確驗證訊息。另一種方式是建立專用值物件或模型繫結器,明文定義格式。無論採何種方式,測試都要包含泰文 UI 文化,並斷言解析後的公曆年份,而不是只斷言 HTTP 請求成功。

public static bool TryParseMachineDate(string input, out DateOnly value)
{
    return DateOnly.TryParseExact(
        input,
        "yyyy-MM-dd",
        CultureInfo.InvariantCulture,
        DateTimeStyles.None,
        out value);
}

顯示層若需要泰文日期,可以在最後一刻明確傳入 th-TH。這樣同一個領域值在內部不受請求文化改變,只有呈現文字依使用者選擇變化。若產品規格要求公曆泰文顯示,則建立新的 CultureInfo 實例並依官方支援方式選擇公曆,而不是修改共享的唯讀文化物件。

測試矩陣與失敗判定

文化相關測試至少要涵蓋預設文化、泰文文化與固定文化,並分別測試單向格式化、單向解析及跨文化資料交換;通過條件必須檢查領域值,不能只看畫面文字或沒有例外。

建議矩陣包含一般日期、月底、閏日與無效輸入。對固定格式欄位,2026-03-11 應解析為公曆二〇二六年;多餘空白、不同分隔符及不存在日期應依契約拒絕。對本地化顯示,泰文文化可呈現佛曆年,但重新送回伺服器時不應把顯示文字當作固定資料欄位。把「顯示測試」與「契約測試」分開,失敗時才知道是資源、曆法、格式還是輸入驗證出錯。

每次升級 .NET 或全球化資料來源後,重新執行同一組斷言。若 th-TH 的預設日曆、格式結果或可用日曆集合改變,測試應明確失敗並要求人工審查,而不是靜默接受新行為。這也是固定版本與可觀察結果的重要性。

上線前檢查清單

修正文化問題不能只改一行全域設定;必須盤點資料邊界、驗證既有格式及補齊跨文化回歸測試,才能避免同類錯誤在匯入、查詢或背景工作中以另一種形式再次出現。

  • UI 資源只依賴 CurrentUICulture,不拿它決定機器資料格式。
  • 每個日期欄位都有明確語意:地方日期、當地顯示文字或帶偏移的時間點。
  • 固定日期字串採指定格式並明確傳入 InvariantCulture。
  • 測試直接斷言解析後的年份、月份與日期,而非只比較往返字串。
  • 泰文案例同時檢查 Calendar 型別與格式化年份。
  • 錯誤輸入會得到可觀察的驗證失敗,不會轉成預設日期後繼續執行。
  • 診斷資訊只記錄文化與格式決策,不記錄使用者輸入內容。

結論很直接:多語系不只是翻譯問題,也是資料契約問題。把 CurrentUICulture、CurrentCulture 與 InvariantCulture 的責任切開,再用固定版本的隔離測試驗證,就能讓泰文介面維持在地化,同時保住日期查詢與資料交換的公曆語意。