國際化

Intl 物件是 ECMAScript 國際化 API 的名稱空間,它提供了廣泛的與區域設定和文化相關的資料和操作。

概述

Intl 物件非常注重用例。它為每個需要特定區域設定邏輯的用例提供一個單獨的物件。目前,它提供以下功能:

大多數 Intl API 都採用相似的設計(Intl.Locale 是唯一例外)。首先,使用所需的區域設定和選項構造一個例項。這定義了所需操作(格式化、排序、分段等)的一組規則。然後,當你在例項上呼叫方法時,例如 format()compare()segment(),該物件會將指定的規則應用於傳入的資料。

js
// 1. Construct a formatter object, specifying the locale and formatting options:
const price = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

// 2. Use the `format` method of the formatter object to format a number:
console.log(price.format(5.259)); // $5.26

建構函式的一般簽名是

js
new Intl.SomeObject(locales, options)
locales 可選

包含 BCP 47 語言標籤的字串或 Intl.Locale 例項,或此類區域設定識別符號的陣列。當傳入 undefined 或未指定支援的區域設定識別符號時,將使用執行時的預設區域設定。有關 locales 引數的一般形式和解釋,請參閱 Intl 主頁上的引數描述

options 可選

一個物件,包含自定義操作特定方面的屬性,這是理解如何使用每個 Intl 物件的關鍵。

區域設定資訊

區域設定是 Intl 所有行為的基礎。區域設定是一組約定,在 Intl API 中由 Intl.Locale 物件表示。所有接受語言標籤的 Intl 建構函式也接受 Intl.Locale 物件。

每個區域設定主要由四部分定義:languagescriptregion,有時還有一些variants。當它們按此順序透過 - 連線時,它們形成一個 BCP 47 語言標籤

  • 語言是區域設定中最重要的部分,並且是強制性的。當給定單一語言(如 enfr)時,有演算法可以推斷其餘資訊(參見 Intl.Locale.prototype.maximize())。
  • 然而,你通常也想指定區域,因為說相同語言的區域之間的約定可能差異很大。例如,美國的日期格式是 MM/DD/YYYY,而在英國是 DD/MM/YYYY,因此指定 en-USen-GB 很重要。
  • 你還可以指定指令碼。指令碼是書寫系統,或者用於轉錄語言的字元。實際上,指令碼通常是不必要的,因為特定區域使用的語言只用一種指令碼書寫。但是,也有例外,例如塞爾維亞語,可以用拉丁字母和西裡爾字母書寫(sr-Latnsr-Cyrl),或者中文,可以用簡體字和繁體字書寫(zh-Hanszh-Hant)。
  • 變體很少使用。通常,它們表示不同的拼寫;例如,德語有 19011996 拼寫變體,分別寫為 de-1901de-1996
js
// These two are equivalent when passed to other Intl APIs
const locale1 = new Intl.Locale("en-US");
const locale2 = new Intl.Locale("en-Latn-US");

console.log(locale1.language, locale1.script, locale1.region); // "en", undefined, "US"
console.log(locale2.language, locale2.script, locale2.region); // "en", "Latn", "US"

區域設定還包含該特定文化使用的一組約定。

用例屬性描述擴充套件子標籤
日期/時間格式化 calendar 用於將日期分組為年、月和周,併為其命名。例如,gregory 日期 "2022-01-01" 在 hebrew 日曆中變為 "28 Tevet 5782"。 ca
hourCycle 決定時間是以 12 小時制還是 24 小時制顯示,以及最小小時數是 0 還是 1。 hc
數字格式化,包括日期、時間、持續時間等。 numberingSystem 將數字轉換為特定於區域設定的表示法。常規的 0123456789 系統稱為 latn(拉丁)。通常,每個指令碼都有一個數字系統,它只是逐位翻譯,但有些指令碼有多個數字系統,有些可能不常用該指令碼書寫數字(例如,中文有自己的 hanidec 數字系統,但大多數文字使用標準 latn 系統),而另一些可能需要特殊的轉換演算法(例如羅馬數字 — roman)。 nu
排序 collation 定義通用排序演算法。例如,如果你使用德語 phonebk 排序,則 "ä" 被視為 "ae",並將在 "ad" 和 "af" 之間排序。 co
caseFirst 決定大寫字母還是小寫字母優先排序,或者是否忽略大小寫。 kf
numeric 決定數字是按數字排序還是按字串排序。例如,如果為 true,則 "10" 將排在 "2" 之後。 kn

在構造 Intl.Locale 或將語言標籤傳遞給其他 Intl 建構函式時,可以顯式指定這些屬性。有兩種方法:將其附加到語言標籤或將其指定為選項。

  • 要將其附加到語言標籤,你首先附加字串 -u(表示“Unicode 擴充套件”),然後是上面給出的擴充套件子標籤,然後是值。
  • 要將它們指定為選項,只需將上面給出的屬性名稱及其值新增到 options 物件中。

Intl.DateTimeFormat 為例,以下兩行都建立了一個用於格式化希伯來日曆中的日期的格式化程式:

js
const df1 = new Intl.DateTimeFormat("en-US-u-ca-hebrew");
const df2 = new Intl.DateTimeFormat("en-US", { calendar: "hebrew" });

無法識別的屬性將被忽略,因此你可以對 Intl.NumberFormat 使用與上述相同的語法,但它不會與僅傳入 en-US 有任何不同,因為數字格式化不使用 calendar 屬性。

獲取這些區域設定約定的預設值很棘手。new Intl.Locale("en-US").calendar 返回 undefined,因為 Locale 物件只包含你傳遞給它的資訊。理論上,預設日曆取決於你使用日曆的 API,因此要獲取 Intl.DateTimeFormat 使用的 en-US 的預設日曆,你可以使用其 resolvedOptions() 方法。其他屬性也一樣。

js
const locale = new Intl.Locale("en-US");
console.log(locale.calendar); // undefined; it's not provided
console.log(new Intl.DateTimeFormat(locale).resolvedOptions().calendar); // "gregory"

Intl.Locale 物件同時做兩件事:它們表示已解析的 BCP 47 語言標籤(如上所示),並提供有關該區域設定的資訊。它的所有屬性,例如 calendar,都只從輸入中提取,而無需查詢任何資料來源以獲取預設值。另一方面,它有一組方法用於查詢有關區域設定的實際資訊。例如,getCalendars()getHourCycles()getNumberingSystems()getCollations() 方法補充了 calendarhourCyclenumberingSystemcollation 屬性,並且每個都返回該屬性的首選值陣列。

js
const locale = new Intl.Locale("ar-EG");
console.log(locale.getCalendars()); // ['gregory', 'coptic', 'islamic', 'islamic-civil', 'islamic-tbla']

Intl.Locale 例項還包含其他公開有用資訊的方法,例如 getTextInfo()getTimeZones()getWeekInfo()

確定區域設定

國際化有一個共同的關注點:我如何知道要使用哪個區域設定?

最明顯的答案是“使用者偏好”。瀏覽器透過 navigator.languages 屬性公開使用者的語言偏好。這是一個語言識別符號陣列,可以直接傳遞給格式化程式建構函式——稍後會詳細介紹。使用者可以在其瀏覽器設定中配置此列表。你也可以傳遞一個空陣列或 undefined,這兩種情況都會導致使用瀏覽器的預設區域設定。

js
const numberFormatter = new Intl.NumberFormat(navigator.languages);
console.log(numberFormatter.format(1234567.89));

const numberFormatter2 = new Intl.NumberFormat([]);

然而,這可能並不總是能提供最理想的結果。由 Intl 格式化程式格式化的字串僅佔你網站上顯示文字的一小部分;大多數本地化內容是由你(網站開發人員)提供的。例如,假設你的網站僅提供兩種語言:英語和法語。如果日本使用者訪問你的網站並期望以英語使用你的網站,當他們看到英語文字與日語數字和日期交織在一起時,他們會感到困惑!

通常,你不想使用瀏覽器的預設語言。相反,你想使用你的網站提供的其他內容相同的語言。假設你的網站有一個語言切換器,將使用者的選擇儲存在某個地方——你可以直接使用它。

js
// Suppose this can be changed by some site-wide control
const userSettings = {
  locale: "en-US",
  colorMode: "dark",
};
const numberFormatter = new Intl.NumberFormat(userSettings.locale);
console.log(numberFormatter.format(1234567.89));

如果你的網站有一個後端根據使用者的 Accept-Language 頭動態選擇語言並根據此傳送不同的 HTML,你也可以使用 HTML 元素的 HTMLElement.lang 屬性:new Intl.NumberFormat(document.documentElement.lang)

如果你的網站只提供一種語言,你也可以在程式碼中硬編碼區域設定:new Intl.NumberFormat("en-US")

如前所述,你還可以向建構函式傳遞一個區域設定陣列,表示一個回退選項列表。第一個使用 navigator.languages 的示例就是其中之一:如果第一個使用者配置的區域設定不支援特定操作,則會嘗試下一個,依此類推,直到找到執行時有資料支援的請求區域設定。你也可以手動執行此操作。在下面的示例中,我們以特異性遞減的順序指定了一系列區域設定,這些區域設定都代表香港華人可能理解的語言,因此格式化程式會選擇它支援的最具特異性的區域設定。

js
const numberFormatter = new Intl.NumberFormat([
  "yue-Hant",
  "zh-Hant-HK",
  "zh-Hant",
  "zh",
]);

沒有 API 可以列出所有支援的區域設定,但有幾種方法可以處理區域設定列表:

  • Intl.getCanonicalLocales():此函式接受區域設定識別符號列表並返回規範化區域設定識別符號列表。這對於理解每個 Intl 建構函式的規範化過程很有用。
  • 每個 Intl 物件上的 supportedLocalesOf() 靜態方法(例如 Intl.DateTimeFormat.supportedLocalesOf()):此方法接受與建構函式相同的引數(localesoptions),並返回與給定資料匹配的給定區域設定標籤的子集。這對於理解執行時支援特定操作的區域設定很有用,例如,顯示僅包含支援語言的語言切換器。

理解返回值

所有物件的第二個共同關注點是“方法返回什麼?”這是一個難以回答的問題,超出了返回值的結構或型別,因為沒有規範說明到底應該返回什麼。大多數情況下,方法的結果是一致的。然而,輸出可能因實現而異,即使在同一區域設定中也是如此——輸出差異是設計使然,並由規範允許。它也可能不是你所期望的。例如,format() 返回的字串可能使用不間斷空格或被雙向控制字元包圍。你不應該將任何 Intl 方法的結果與硬編碼常量進行比較;它們只應顯示給使用者。

當然,這個答案似乎不能令人滿意,因為大多數開發人員確實希望控制輸出的外觀——至少,你不希望你的使用者被無意義的輸出弄糊塗。如果你確實想進行測試,無論是自動化測試還是手動測試,這裡有一些指導方針:

  • 測試你的使用者可能使用的所有區域設定。如果你有一組固定的支援區域設定(例如透過語言切換器),這會更容易。如果你使用使用者偏好的任何區域設定,你可以為你的使用者選擇一些常見的區域設定,但請記住使用者看到的內容可能會有所不同。你通常可以透過測試執行器的配置或模擬 Intl 建構函式來模擬使用者偏好。
  • 在多個 JavaScript 引擎上進行測試。Intl API 由 JavaScript 引擎直接實現,因此例如,你應該期望 Node.js 和 Chrome(都使用 V8)具有相同的輸出,而 Firefox(使用 SpiderMonkey)可能具有不同的輸出。儘管所有引擎都可能使用 CLDR 資料,但它們通常以不同的方式對其進行後處理。某些瀏覽器構建設定(例如,為了減小安裝大小)也可能會影響支援的區域設定和選項。
  • 不要假設輸出。這意味著你不應該手動編寫輸出,例如 expect(result).toBe("foo")。相反,請使用快照測試或從測試執行的輸出中複製字串值。

格式化資料

Intl 的主要用例是輸出表示結構化資料的特定於區域設定的文字。這類似於翻譯軟體,但它不是讓你翻譯任意文字,而是獲取日期、數字和列表等資料,並根據特定於區域設定的規則對其進行格式化。

Intl.DateTimeFormatIntl.DurationFormatIntl.ListFormatIntl.NumberFormatIntl.RelativeTimeFormat 物件各自格式化一種資料。每個例項提供兩種方法:

  • format():接受一段資料並使用區域設定和選項確定的格式規則返回一個字串。
  • formatToParts():接受相同的資料並返回相同的字串,但分解為多個部分,每個部分都是一個包含 typevalue 的物件。這對於更高階的用例很有用,例如將格式化文字與其他文字交錯。

例如,下面是 Intl.NumberFormat 物件的典型用法:

js
// 1. Construct a formatter object, specifying the locale and formatting options:
const price = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});

// 2. Use the `format` method of the formatter object to format a number:
console.log(price.format(5.259)); // $5.26

// Or, use the `formatToParts` method to get the formatted number
// broken down into parts:
console.table(price.formatToParts(5.259));
// |   | type       | value |
// | 0 | "currency" | "$"   |
// | 1 | "integer"  | "5"   |
// | 2 | "decimal"  | "."   |
// | 3 | "fraction" | "26"  |

你並不總是必須構造格式化程式物件來格式化字串。對於隨意使用,你也可以直接在資料上呼叫 toLocaleString() 方法,將區域設定和選項作為引數傳遞。toLocaleString() 方法由 Temporal.PlainDate.prototype.toLocaleString()Temporal.Duration.prototype.toLocaleString()Number.prototype.toLocaleString() 等實現。請閱讀你正在格式化的資料文件,以檢視它是否支援 toLocaleString(),以及它對應的格式化程式選項。

js
console.log(
  (5.259).toLocaleString("en-US", {
    style: "currency",
    currency: "USD",
  }),
); // $5.26

請注意,toLocaleString() 可能比使用格式化程式物件效率低,因為每次呼叫 toLocaleString 時,它都必須在龐大的本地化字串資料庫中執行搜尋。當使用相同的引數多次呼叫該方法時,最好建立一個格式化程式物件並使用其 format() 方法,因為格式化程式物件會記住傳遞給它的引數,並可能決定快取資料庫的一部分,因此未來的 format 呼叫可以在更受限制的上下文中搜索本地化字串。

日期和時間格式化

Intl.DateTimeFormat 格式化日期和時間,以及日期和時間範圍。DateTimeFormat 物件接受以下形式之一的日期/時間輸入:DateTemporal.PlainDateTimeTemporal.PlainTimeTemporal.PlainDateTemporal.PlainYearMonthTemporal.PlainMonthDay

注意:你不能直接傳遞 Temporal.ZonedDateTime 物件,因為時區已在該物件中固定。你應該使用 Temporal.ZonedDateTime.prototype.toLocaleString() 或先將其轉換為 Temporal.PlainDateTime 物件。

本地化日期和時間格式化的常見用例如下:

  • 以其他日曆系統輸出相同的日期和時間,例如伊斯蘭、希伯來或中國日曆。
  • 輸出相同的實際時間(瞬間),但以其他時區。
  • 選擇性地輸出日期和時間的某些元件,例如只輸出年和月,以及它們的特定表示形式(例如“Thursday”或“Thu”)。
  • 根據特定於區域設定的約定輸出日期,例如美國是 MM/DD/YYYY,英國是 DD/MM/YYYY,日本是 YYYY/MM/DD。
  • 根據特定於區域設定的約定輸出時間,例如 12 小時制或 24 小時制。

要決定格式化字串的外觀,你首先選擇日曆(它影響年、月、周和日的計算)和時區(它影響精確時間以及可能的日期)。這是透過上述 calendar 選項(或區域設定識別符號中的 -ca- 擴充套件鍵)和 timeZone 選項完成的。

  • Date 物件表示使用者時區和 ISO 8601 日曆中的唯一瞬間(由 Date.prototype.getHours()Date.prototype.getMonth() 等方法報告)。它們透過保留瞬間轉換為給定的 calendartimeZone,因此日期和時間元件可能會更改。
  • 各種 Temporal 物件已經內建了日曆,因此 calendar 選項必須與物件的日曆一致——除非日期的日曆是 "iso8601",在這種情況下它會轉換為請求的 calendar。這些物件沒有時區,因此它們直接在給定的 timeZone 中顯示,無需轉換。

在這裡,我們演示 calendartimeZone 配置的組合如何導致同一瞬間的不同表示。

js
// Assume that the local time zone is UTC
const targetDate = new Date(2022, 0, 1); // 2022-01-01 midnight in the local time zone
const results = [];

for (const calendar of ["gregory", "hebrew"]) {
  for (const timeZone of ["America/New_York", "Asia/Tokyo"]) {
    const df = new Intl.DateTimeFormat("en-US", {
      calendar,
      timeZone,
      // More on these later
      dateStyle: "full",
      timeStyle: "full",
    });
    results.push({ calendar, timeZone, output: df.format(targetDate) });
  }
}

console.table(results);

輸出如下所示

calendar timeZone output
'gregory' 'America/New_York' 'Friday, December 31, 2021 at 7:00:00 PM Eastern Standard Time'
'gregory' 'Asia/Tokyo' 'Saturday, January 1, 2022 at 9:00:00 AM Japan Standard Time'
'hebrew' 'America/New_York' 'Friday, 27 Tevet 5782 at 7:00:00 PM Eastern Standard Time'
'hebrew' 'Asia/Tokyo' 'Saturday, 28 Tevet 5782 at 9:00:00 AM Japan Standard Time'

日期/時間由以下元件組成:weekdayerayearmonthdaydayPeriodhourminutesecondfractionalSecondDigitstimeZoneName。你的下一個決定是輸出中包含哪些元件,以及它們應該採用什麼形式。你有兩種選擇:

  • 你可以手動配置每個元件,使用與元件同名的選項。只有你指定的元件將以指定的形式包含在輸出中。
  • 你可以使用快捷方式 dateStyletimeStyle,它們是預定義的一組元件。它們根據區域設定擴充套件為一組元件選項。

你應該選擇這兩種方法之一,因為它們是互斥的。同時使用兩種方法將導致錯誤。

從根本上說,在請求元件組合後,DateTimeFormat 物件會查詢與請求元件匹配的“模板”,因此它只需要逐一填充值。並非所有元件組合都有預定義的模板。DateTimeFormat 有一個 formatMatcher 選項,它決定如何透過使元件比請求的更長或更短,或透過省略或新增元件來進行協商。這非常技術化,因此你應該閱讀 Intl.DateTimeFormat() 參考以更好地理解它如何處理此問題。

在這裡,我們演示了一些常見的元件格式化方式:

js
const df1 = new Intl.DateTimeFormat("en-US", {
  // Include all components (usually)
  dateStyle: "full",
  timeStyle: "full",
});

const df2 = new Intl.DateTimeFormat("en-US", {
  // Display the calendar date
  era: "short",
  year: "numeric",
  month: "long",
  day: "numeric",
});

const df3 = new Intl.DateTimeFormat("en-US", {
  // Display a time like on a digital clock
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  timeZoneName: "shortOffset",
});

const targetDate = new Date(2022, 0, 1, 12, 34, 56); // 2022-01-01 12:34:56 in the local time zone
console.log(df1.format(targetDate));
// Saturday, January 1, 2022 at 12:34:56 PM Coordinated Universal Time
// January 1, 2022 AD
// 12:34:56 PM GMT

還有其他自定義選項。例如,你可以使用 hourCycle 選項以 12 小時制或 24 小時制顯示時間,並將午夜/中午顯示為 12:00 或 0:00。你還可以使用 numberingSystem 選項以其他數字系統顯示任何數字。

除了 format() 之外,還有第二個重要方法 formatRange(),它格式化日期或時間範圍。它接受兩個相同型別的日期時間,分別格式化它們,用範圍分隔符(如 en-dash)連線它們,並刪除重複的公共部分。

js
const springBreak = {
  start: new Date(2023, 2, 10),
  end: new Date(2023, 2, 26),
};

const df = new Intl.DateTimeFormat("en-US", { dateStyle: "long" });
console.log(df.formatRange(springBreak.start, springBreak.end));
// March 10 – 26, 2023

數字格式化

數字格式化使用 Intl.NumberFormat 物件完成。NumberFormat 物件接受數字、字串或 BigInt 形式的輸入。傳遞字串或 BigInt 而不是數字允許你格式化太大或太小而無法精確表示為 JavaScript 數字的數字。

本地化數字格式化的常見用例如下:

  • 以其他數字系統(指令碼)輸出數字,例如中文、阿拉伯文或羅馬數字。
  • 以特定於區域設定的約定輸出數字,例如小數符號(英語中是“.”,但在許多歐洲文化中是“,”),或數字分組(英語中是 3 位,但在其他文化中可能是 4 或 2 位,並且可能使用“,”、“ ”或“.”)。
  • 以指數表示法輸出數字,例如“3.7 million”或“2 thousand”。
  • 將數字輸出為貨幣,應用特定的貨幣符號和舍入規則。例如,在美國小於一美分或在日本小於一日元的貨幣值可能沒有顯示意義。
  • 將數字輸出為百分比,應用特定於區域設定的轉換和格式化規則。
  • 輸出帶有單位的數字,例如“米”或“升”,帶有翻譯的單位名稱。

要決定格式化字串的外觀,你首先選擇數字系統(它影響用於數字的字元)。數字系統的用途已在 區域設定資訊 中討論。你需要決定的另一個選項是 style,它設定數字所代表的上下文,並可能影響其他選項的預設值。它是 "decimal""percent""currency""unit" 之一。如果你想格式化貨幣,那麼你還需要提供 currency 選項。如果你想格式化單位,那麼你還需要提供 unit 選項。

js
const results = [];
for (const options of [
  { style: "decimal" }, // Format the number as a dimensionless decimal
  { style: "percent" }, // Format the number as a percentage; it is multiplied by 100
  { style: "currency", currency: "USD" }, // Format the number as a US dollar amount
  { style: "unit", unit: "meter" }, // Format the number as a length in meters
]) {
  const nf = new Intl.NumberFormat("en-US", options);
  results.push({ style: options.style, output: nf.format(1234567.89) });
}
console.table(results);

輸出如下:

style output
'decimal' '1,234,567.89'
'percent' '123,456,789%'
'currency' '$1,234,567.89'
'unit' '1,234,567.89 m'

下一組選項都指定數字部分應該是什麼樣子。首先,你可能希望以更具可讀性的方式表示極大的值。你可以將 notation 選項設定為 "scientific""engineering",兩者都使用 1.23e+6 表示法。區別在於後者使用 3 的倍數作為指數,使 尾數e 符號之前的部分)保持在 1 到 1000 之間,而前者可以使用任何整數作為指數,使尾數保持在 1 到 10 之間。你還可以將 notation 設定為 "compact" 以使用更易於人類閱讀的表示法。

js
const results = [];
for (const options of [
  { notation: "scientific" },
  { notation: "engineering" },
  { notation: "compact", compactDisplay: "short" }, // "short" is default
  { notation: "compact", compactDisplay: "long" },
]) {
  const nf = new Intl.NumberFormat("en-US", options);
  results.push({
    notation: options.compactDisplay
      ? `${options.notation}-${options.compactDisplay}`
      : options.notation,
    output: nf.format(12000),
  });
}
console.table(results);

輸出如下:

notation output
'scientific' '1.2E4'
'engineering' '12E3'
'compact-short' '12K'
'compact-long' '12 thousand'

然後,你可能希望對數字進行舍入(如果你指定了 notation,則只對尾數部分),這樣你就不會顯示太長的數字。這些是數字選項,包括:

  • minimumIntegerDigits
  • minimumFractionDigits
  • maximumFractionDigits
  • minimumSignificantDigits
  • maximumSignificantDigits
  • roundingPriority
  • roundingIncrement
  • roundingMode

這些選項的精確互動相當複雜,不值得在此處介紹。你應該閱讀 數字選項 參考以獲取更多詳細資訊。儘管如此,一般想法是直接的:我們首先找到我們想要保留的小數位數,然後根據最後一位的值向上或向下舍入多餘的小數位數。

js
const results = [];
for (const options of [
  { minimumFractionDigits: 4, maximumFractionDigits: 4 },
  { minimumSignificantDigits: 4, maximumSignificantDigits: 4 },
  { minimumFractionDigits: 0, maximumFractionDigits: 0, roundingMode: "floor" },
  {
    minimumFractionDigits: 0,
    maximumFractionDigits: 0,
    roundingMode: "floor",
    roundingIncrement: 10,
  },
]) {
  const nf = new Intl.NumberFormat("en-US", options);
  results.push({
    options,
    output: nf.format(1234.56789),
  });
}
console.table(results);

輸出如下所示

options output
{ minimumFractionDigits: 4, maximumFractionDigits: 4 } '1,234.5679'
{ minimumSignificantDigits: 4, maximumSignificantDigits: 4 } '1,235'
{ minimumFractionDigits: 0, maximumFractionDigits: 0, roundingMode: "floor" } '1,234'
{ minimumFractionDigits: 0, maximumFractionDigits: 0, roundingMode: "floor", roundingIncrement: 10 } '1,230'

還有其他自定義選項。例如,你可以使用 useGroupingsignDisplay 選項來自定義是否以及如何顯示分組分隔符(例如“1,234,567.89”中的“,”)和符號。但是,請注意,用於分組分隔符、小數點和符號的字元是特定於區域設定的,因此你不能直接自定義它們。

除了 format() 之外,還有第二個重要方法 formatRange(),它格式化數字範圍。它接受兩個數字表示形式,分別格式化它們,用範圍分隔符(如 en-dash)連線它們,並可能刪除重複的公共部分。

js
const heightRange = {
  min: 1.63,
  max: 1.95,
};

const nf = new Intl.NumberFormat("en-US", { style: "unit", unit: "meter" });
console.log(nf.formatRange(heightRange.min, heightRange.max));
// 1.63–1.95 m

列表格式化

你可能已經編寫了這樣的程式碼:

js
const fruits = ["apple", "banana", "cherry"];
console.log(`I like ${fruits.join(", ")}.`);
// I like apple, banana, cherry.

此程式碼未國際化。在某些語言中,列表分隔符不是逗號。在大多數語言(包括英語)中,你需要在最後一個專案之前使用連詞。但即使手動新增“and”也無法在所有講英語的人中都正確,因為英語中存在關於 牛津逗號 的爭議:“apple, banana, and cherry”與“apple, banana and cherry”。

Intl.ListFormat 物件解決了這個問題。它接受一個字串陣列,並以特定於區域設定的方式連線它們,使得結果表示連詞(and)、析取詞(or)或單位列表。

js
const fruits = ["apple", "banana", "cherry"];
const lf = new Intl.ListFormat("en-US", { style: "long", type: "conjunction" });
console.log(`I like ${lf.format(fruits)}.`);
// I like apple, banana, and cherry.

const lf = new Intl.ListFormat("en-US", { style: "long", type: "disjunction" });
console.log(`I can give you ${lf.format(fruits)}.`);
// I can give you apple, banana, or cherry.

檢視 Intl.ListFormat() 以獲取更多示例和選項。

相對時間格式化

Intl.RelativeTimeFormat 格式化時間差。RelativeTimeFormat 物件以兩個引數的形式接受相對時間:一個數字(帶任何符號)和一個時間單位,例如 "day""hour""minute"

它同時做了幾件事:

  • 它本地化並複數化時間單位,例如“1 day”與“2 days”,類似於數字格式化。
  • 它為過去和未來的時間選擇合適的短語,例如“in 1 day”與“1 day ago”。
  • 它可能會為某些時間單位選擇特殊短語,例如“1 day ago”與“yesterday”。
js
const rtf = new Intl.RelativeTimeFormat("en-US", { numeric: "auto" });
console.log(rtf.format(1, "day")); // tomorrow
console.log(rtf.format(2, "day")); // in 2 days
console.log(rtf.format(-1, "hour")); // 1 hour ago

檢視 Intl.RelativeTimeFormat() 以獲取更多示例和選項。

持續時間格式化

Intl.DurationFormat 提供持續時間格式化,例如“3 hours, 4 minutes, 5 seconds”。它不是一個具有自己格式化程式的原始操作:它內部使用 Intl.NumberFormatIntl.ListFormat 來格式化每個持續時間元件,然後用列表分隔符連線它們。DurationFormat 物件接受 Temporal.Duration 物件形式的持續時間,或者具有相同屬性的普通物件。

除了自定義數字系統之外,持續時間格式化選項還決定是否顯示每個元件以及它們的長度。

js
console.log(
  new Intl.DurationFormat("en-US", {
    style: "long",
  }).format({ hours: 3, minutes: 4, seconds: 5 }),
);
// 3 hours, 4 minutes, and 5 seconds

檢視 Intl.DurationFormat() 以獲取更多示例和選項。

排序

Intl.Collator 物件用於比較和排序字串。它接受兩個字串並返回一個數字,指示它們的相對順序,其方式與 Array.prototype.sort 方法的 compareFn 引數相同。

你不應該使用 JavaScript 運算子(如 ===>)來比較面向使用者的字串,原因有很多:

  • 不相關的拼寫變體:例如,在英語中,“naïve”和“naive”只是同一個詞的替代拼寫,應該被視為相等。
  • 忽略大小寫:通常,在比較字串時,你希望忽略大小寫。例如,“apple”和“Apple”應該被視為相等。
  • Unicode 碼點順序沒有意義:比較運算子(如 >)按 Unicode 碼點順序進行比較,這與字典中字元的順序不同。例如,“ï”在碼點順序中排在“z”之後,但你希望它在字典中排在“i”旁邊。
  • Unicode 規範化:同一個字元在 Unicode 中可能有多種表示形式。例如,“ñ”可以表示為單個字元,也可以表示為“n”後跟一個組合波浪號。(參見 String.prototype.normalize()。)這些應該被視為相等。
  • 數字比較:字串中的數字應按數字而不是字串進行比較。例如,你希望“test-10”排在“test-2”之後。

排序有兩種不同的用例:排序搜尋。排序是指你有一個字串列表,並希望根據某種規則對其進行排序。搜尋是指你有一個字串列表,並希望找到與查詢匹配的字串。在搜尋時,你應該只關注比較結果是否為零(相等),而不是結果的符號。

即使在同一個區域設定中,也有許多不同的排序方式。例如,德語中有兩種不同的排序順序:電話簿字典。電話簿排序強調發音——就像“ä”、“ö”等在排序前被擴充套件為“ae”、“oe”等。

js
const names = ["Hochberg", "Hönigswald", "Holzman"];

const germanPhonebook = new Intl.Collator("de-DE-u-co-phonebk");

// as if sorting ["Hochberg", "Hoenigswald", "Holzman"]:
console.log(names.sort(germanPhonebook.compare));
// ['Hochberg', 'Hönigswald', 'Holzman']

一些德語單詞會帶額外的變音符號,因此在詞典中,忽略變音符號排序是明智的(除非排序的單詞因變音符號而異:schonschön 之前)。

js
const germanDictionary = new Intl.Collator("de-DE-u-co-dict");

// as if sorting ["Hochberg", "Honigswald", "Holzman"]:
console.log(names.sort(germanDictionary.compare).join(", "));
// "Hochberg, Holzman, Hönigswald"

複數規則

Intl.PluralRules 物件對於選擇單詞的正確複數形式很有用。它不會自動為你複數化單詞(例如,你不能傳入“apple”並期望返回“apples”),但它會根據數字告訴你使用哪種複數形式。你可能已經這樣做了:

js
function formatMessage(n) {
  return `You have ${n} ${n === 1 ? "apple" : "apples"}.`;
}

但這很難推廣到所有語言,尤其是那些有許多複數形式的語言。你可以檢視 Intl.PluralRules 以獲取複數規則的一般介紹。在這裡,我們只演示一些常見的用例。

js
const prCard = new Intl.PluralRules("en-US");
const prOrd = new Intl.PluralRules("en-US", { type: "ordinal" });

const englishOrdinalSuffixes = {
  one: "st",
  two: "nd",
  few: "rd",
  other: "th",
};

const catPlural = {
  one: "cat",
  other: "cats",
};

function formatMessage(n1, n2) {
  return `The ${n1}${englishOrdinalSuffixes[prOrd.select(n1)]} U.S. president had ${n2} ${catPlural[prCard.select(n2)]}.`;
}

console.log(formatMessage(42, 1)); // The 42nd U.S. president had 1 cat.
console.log(formatMessage(45, 0)); // The 45th U.S. president had 0 cats.

分段

Intl.Segmenter 物件對於將字串分段很有用。如果沒有 Intl,你已經能夠按 UTF-16 碼元和 Unicode 碼點 拆分字串:

js
const str = "🇺🇸🇨🇳🇷🇺🇬🇧🇫🇷";
console.log(str.split(""));
// Array(20) ['\uD83C', '\uDDFA', '\uD83C', ...]
console.log([...str]);
// Array(10) ['🇺', '🇸', '🇨', '🇳', '🇷', '🇺', '🇬', '🇧', '🇫', '🇷']

但正如你所看到的,Unicode 碼點與人類使用者感知到的離散字元不同。這通常發生在表情符號中,單個表情符號可以由多個碼點表示。當用戶與文字互動時,字素是他們可以操作的最小文字單位,例如刪除或選擇。Segmenter 物件支援字素級分段,這對於計算字元、測量文字寬度等很有用。它接受一個字串並返回一個可迭代的 segments 物件,其中每個元素都有一個 segment 屬性,表示分段的文字。

js
const segmenter = new Intl.Segmenter("en-US", { granularity: "grapheme" });
console.log([...segmenter.segment("🇺🇸🇨🇳🇷🇺🇬🇧🇫🇷")].map((s) => s.segment));
// ['🇺🇸', '🇨🇳', '🇷🇺', '🇬🇧', '🇫🇷']

分段器還可以進行更高級別的分段,包括單詞級和句子級拆分。這些用例必然是特定於語言的。例如,以下是一個非常糟糕的單詞計數實現:

js
const wordCount = (str) => str.split(/\s+/).length;
console.log(wordCount("Hello, world!")); // 2

這存在幾個問題:並非所有語言都使用空格來分隔單詞,並非所有空格都分隔單詞,並且並非所有單詞都由空格分隔。為了解決這個問題,使用 Segmenter 並設定 granularity: "word"。結果是輸入字串,被分割成單詞和非單詞段。如果你正在計數單詞,你應該透過檢查每個段的 isWordLike 屬性來過濾掉非單詞。

js
const segmenter = new Intl.Segmenter("en-US", { granularity: "word" });
const str = "It can even split non-space-separated words";
console.table([...segmenter.segment(str)]);
// ┌─────────────┬───────┬────────────┐
// │ segment     │ index │ isWordLike │
// ├─────────────┼───────┼────────────┤
// │ 'It'        │ 0     │ true       │
// │ ' '         │ 2     │ false      │
// │ 'can'       │ 3     │ true       │
// │ ' '         │ 6     │ false      │
// │ 'even'      │ 7     │ true       │
// │ ' '         │ 11    │ false      │
// │ 'split'     │ 12    │ true       │
// │ ' '         │ 17    │ false      │
// │ 'non'       │ 18    │ true       │
// │ '-'         │ 21    │ false      │
// │ 'space'     │ 22    │ true       │
// │ '-'         │ 27    │ false      │
// │ 'separated' │ 28    │ true       │
// │ ' '         │ 37    │ false      │
// │ 'words'     │ 38    │ true       │
// └─────────────┴───────┴────────────┘

console.log(
  [...segmenter.segment(str)].filter((s) => s.isWordLike).map((s) => s.segment),
);
// ['It', 'can', 'even', 'split', 'non', 'space', 'separated', 'words']

單詞分段也適用於基於字元的語言。例如,在中文中,幾個字元可以代表一個單詞,但它們之間沒有空格。分段器實現與瀏覽器內建單詞分段相同的行為,由雙擊單詞觸發。

js
const segmenter = new Intl.Segmenter("zh-Hans", { granularity: "word" });
console.log([...segmenter.segment("我是這篇文件的作者")].map((s) => s.segment));
// ['我是', '這', '篇', '文', '檔', '的', '作者']

句子分段同樣複雜。例如,在英語中,有許多標點符號可以標記句子的結尾(“.”、“!”、“?” 等等)。

js
const segmenter = new Intl.Segmenter("en-US", { granularity: "sentence" });
console.log(
  [...segmenter.segment("I ate a sandwich. Then I went to bed.")].map(
    (s) => s.segment,
  ),
);
// ['I ate a sandwich. ', 'Then I went to bed.']

請注意,分段器不會刪除任何字元。它只是將字串拆分成段,每個段都是一個句子。然後你可以刪除標點符號。此外,分段器的當前實現不支援句子分段抑制(防止在“Mr.”或“Approx.”等句號後出現句子中斷),但正在進行這方面的工作。

顯示名稱

在介紹了這麼多選項和行為之後,你可能會想如何將它們呈現給使用者。Intl 提供了兩個用於構建使用者介面的有用 API:Intl.supportedValuesOf()Intl.DisplayNames

Intl.supportedValuesOf() 函式返回給定選項支援的值陣列。例如,你可以使用它來填充受支援日曆的下拉列表,使用者可以從中選擇顯示日期。

js
const supportedCal = Intl.supportedValuesOf("calendar");
console.log(supportedCal);
// ['buddhist', 'chinese', 'coptic', 'dangi', ...]

但通常,這些識別符號對使用者不友好。例如,你可能希望以使用者的語言顯示日曆,或將其縮寫展開。為此,Intl.DisplayNames 物件非常有用。它類似於格式化程式,但它不是基於模板的。相反,它是一個從語言無關識別符號到本地化名稱的直接對映。它支援格式化語言、區域、指令碼(BCP 47 語言標籤的三個子欄位)、貨幣、日曆和日期時間欄位。

嘗試下面的演示:

html
<select id="lang"></select>
<select id="calendar"></select>
<output id="output"></output>
js
const langSelect = document.getElementById("lang");
const calSelect = document.getElementById("calendar");
const fieldset = document.querySelector("fieldset");
const output = document.getElementById("output");

// A few examples
const langs = [
  "en-US",
  "zh-Hans-CN",
  "ja-JP",
  "ar-EG",
  "ru-RU",
  "es-ES",
  "fr-FR",
  "de-DE",
  "hi-IN",
  "pt-BR",
  "bn-BD",
  "he-IL",
];
const calendars = Intl.supportedValuesOf("calendar");

for (const lang of langs) {
  const option = document.createElement("option");
  option.value = lang;
  option.textContent = new Intl.DisplayNames(lang, { type: "language" }).of(
    lang,
  );
  langSelect.appendChild(option);
}

function renderCalSelect() {
  const lang = langSelect.value;
  calSelect.innerHTML = "";
  const dn = new Intl.DisplayNames(lang, { type: "calendar" });
  const preferredCalendars = new Intl.Locale(lang).getCalendars?.() ?? [
    "gregory",
  ];
  for (const cal of [
    ...preferredCalendars,
    ...calendars.filter((c) => !preferredCalendars.includes(c)),
  ]) {
    const option = document.createElement("option");
    option.value = cal;
    option.textContent = dn.of(cal);
    calSelect.appendChild(option);
  }
}

function renderFieldInputs() {
  const lang = langSelect.value;
  fieldset.querySelectorAll("label").forEach((label) => label.remove());
  const dn = new Intl.DisplayNames(lang, { type: "dateTimeField" });
  for (const field of fields) {
    const label = document.createElement("label");
    label.textContent = dn.of(field);
    const input = document.createElement("input");
    input.type = "checkbox";
    input.value = field;
    label.appendChild(input);
    fieldset.appendChild(label);
  }
}

function renderTime() {
  const lang = langSelect.value;
  const cal = calSelect.value;
  const df = new Intl.DateTimeFormat(lang, {
    calendar: cal,
    dateStyle: "full",
    timeStyle: "full",
  });
  const now = new Date();
  const dn = new Intl.DisplayNames(lang, { type: "dateTimeField" });
  output.innerHTML = "";
  for (const component of df.formatToParts(now)) {
    const text = document.createElement("span");
    text.textContent = component.value;
    if (
      ![
        "era",
        "year",
        "quarter",
        "month",
        "weekOfYear",
        "weekday",
        "day",
        "dayPeriod",
        "hour",
        "minute",
        "second",
        "timeZoneName",
      ].includes(component.type)
    ) {
      output.appendChild(text);
      continue;
    }
    const title = dn.of(component.type);
    const field = document.createElement("ruby");
    field.appendChild(text);
    const rt = document.createElement("rt");
    rt.textContent = title;
    field.appendChild(rt);
    output.appendChild(field);
  }
}

renderCalSelect();
renderTime();
langSelect.addEventListener("change", renderCalSelect);
langSelect.addEventListener("change", renderTime);
calSelect.addEventListener("change", renderTime);
setInterval(renderTime, 500);