🎄

Intlの全体像と基本となるIF(#1)

2024/12/01に公開

この記事は「1 人 Intl Advent Calendar 2024」の 1 日目の記事です。

アドベントカレンダーの初回である今回はアドベントカレンダー自体の説明、 JS の国際化 API「Intl」の概要・仕様的な位置付け、基本となる構成要素・IF などを紹介します。

このアドベントカレンダーについてと構成

このアドベントカレンダーは、JavaScript (ECMAScript) の国際化 API「Intl」について紹介するものです。2024 年末時点での Intl 仕様と提案されているプロポーザルを幅広く紹介することを目指しています。また、Intl に関連する他の仕様についても適宜紹介します。

機能やドメインに関連する話題をまとめて紹介するため、「既存仕様 → 関連するプロポーザル」の順で繰り返し説明していく予定です。基本的な機能だけを知りたい方は、適宜読み飛ばしてください。

前提となる知識・用語

このアドベントカレンダーでは、題材の性質上、JavaScript の仕様や仕様策定に関わる用語が頻繁に出てきます。ある程度 JavaScript の仕様に興味がある方には既知の知識かもしれませんが、初めて触れる方は以下の記事を読んで前提知識を持っておくと読みやすくなります。

https://jsprimer.net/basic/ecmascript/

以降の文書・記事では「ECMAScript」、「Stage」といった用語を使用しますが、「この用語は何だっけ?」と思った場合は、こちらの記事などで確認すると良いでしょう。

Intl とは

Intl は ECMAScript (JavaScript[1]) に存在する名前空間です。Intl.〇〇 のように、Intl 配下の各 API は「国際化」に関連する機能を提供しています。

国際化 API の範囲

「国際化機能」と言われても、具体的に何を指すのか分かりにくいかもしれません。Intl の仕様書の序文では、その適用範囲について次のように述べています。

This Standard defines the application programming interface for ECMAScript objects that support programs that need to adapt to the linguistic and cultural conventions used by different human languages and countries. (from ECMA-402, 10th edition)

簡単に訳すと、「さまざまな国や言語・文化によって異なる部分や処理をサポートする機能をまとめた API」と言えるでしょう。

具体的には、Intl は以下のような国際化機能を提供しています。

  • 日時のフォーマット
  • 数値のフォーマット
  • 複数形や序数など、各言語の数に対する文法上の分類判別
  • 見た目上の一文字 (書記素) や単語、文レベルの分割
  • ロケールの解決
  • 言語に依存した文字列の比較

これらの機能は言語や文化、地域によって異なり、規則性もないため、自前でサポートするのは困難です。Intl はこれらの機能を JavaScript の標準でサポートするために生まれた API です。

Intl の利点

Intl の最大の利点は、主要ブラウザや Node.js などのランタイムで言語機能としてサポートされていることです。具体的には次のようなメリットがあります。

  • サードパーティのライブラリを必要としない
    • JavaScript の組み込みオブジェクトとして利用できるため、専用のライブラリを追加する必要がありません。これにより、専用ライブラリを使用している場合はバンドルサイズの削減が期待できます。また、ライブラリ独自の記法に依存しないため、コードの再利用性も高まります。
  • 後方互換性がある程度保証される
    • JavaScript の一部として、後方互換性を考慮しながらアップデートされるため、安心して実装できます。これは、破壊的な API 変更の可能性があるライブラリとは異なるメリットです。

仕様の位置付けと策定

Intl は JavaScript の API として利用できますが、仕様の位置付けは他の JavaScript 標準仕様 (ECMAScript) と少し異なります。

より厳密に言うと、Intl は「ECMAScript を拡張する形で標準化された ECMAScript の国際化 API の総称」です。

このように、Intl は ECMAScript の拡張仕様であるため、一般的な ECMAScript の仕様とは異なる仕様番号[2]で管理されています。一般的な ECMAScript の仕様が ECMA-262 という仕様番号であるのに対し、Intl は ECMA-402 という仕様番号で管理されています。この ECMA-262 と ECMA-402 を合わせた集合が、一般的に「JavaScript の仕様」として理解されていると言っていいでしょう。

Intl の仕様について調べるときの注意点

単に「仕様番号が分かれているだけ」であれば特に問題はありませんが、ECMA-262 の拡張仕様という位置付け上、仕様書や議事録などのリソースが一般的な ECMAScript の仕様 (ECMA-262) と分かれていることがあります。そのため、Intl の仕様を詳しく調べたり、策定中のプロポーザルを追いたい場合は、この点を理解しておく必要があります。それぞれ具体的に見ていきましょう。

まず、仕様番号が異なるため、仕様書も ECMA-262 とは別に記述され、異なる URL で公開されています。

https://tc39.es/ecma402/

また、Intl (ECMA-402) の仕様は TC39 の中でも専門の Task Group (TC39 の中の 5 つある分科会)[3] で議論されることが多く、ECMAScript 全体のミーティングとは別に、ECMA-402 の (ほぼ) 月次ミーティングの議事録が残されています。

https://github.com/tc39/ecma402/tree/main/meetings

もちろん、ECMAScript 全体のミーティングでも ECMA-402 のプロポーザルについて話されることはありますが、プロポーザルの詳細や最新情報を調べたい場合は、こちらも参照すると良いでしょう。

さらに細かい違いですが、提案されているプロポーザルの一覧も ECMA-262 に対するものとは別のパスで管理されているため、探す際には注意が必要です。

https://github.com/tc39/proposals/blob/HEAD/ecma402/README.md

このように、Intl について調べる際には、他の ECMAScript と微妙にリソースが分かれていることがあるので、注意しながら調べるようにしましょう。

豆知識 : Intl の簡単な歴史

Intl の歴史は意外と長く、最初にその仕様が提案されたのは 2010 年 9 月の TC39 ミーティングまで遡ります。当時は今以上に初歩的な書式化機能しかなく、文字列のソートに至ってはブラウザ間 (JS エンジン間) で挙動が異なる状態でした。

その後、翌年の 2011 年 10 月に最初の Draft 仕様書が完成し、さらに 1 年間の仕様改善や実装作業を経て、2012 年 2 月に ECMA-402 の 1st Edition が公開されました。当時は今と比べて機能も少なく、サポートしているブラウザも少なかったため、主要 3 ブラウザの実装が揃うのはさらに 4 年後の 2016 年まで待つこととなりました。

Intl API の全体像と基本的なインターフェース

次に、Intl API の全体像と基本的なインターフェースについて見ていきます。

コンストラクタプロパティとメソッド

Intl は多くの機能をコンストラクタプロパティとして提供しており、2024 年 12 月現在、9 つのコンストラクタプロパティがあります。

  • Intl.Collator()
  • Intl.DateTimeFormat()
  • Intl.DisplayNames()
  • Intl.ListFormat()
  • Intl.Locale()
  • Intl.NumberFormat()
  • Intl.PluralRules()
  • Intl.RelativeTimeFormat()
  • Intl.Segmenter()

また、ロケールや Intl 自体の機能に関するメタ情報を返すメソッドが 2 つ存在します。

  • Intl.getCanonicalLocales()
  • Intl.supportedValuesOf()

これらのメソッドについては、5 日目の記事で詳しく解説します。

コンストラクタの基本的なインターフェース

Intl の主要な機能はコンストラクタプロパティとして提供されているため、使用する際はそれぞれのコンストラクタプロパティを初期化 (new) して利用します。初期化時の引数はオプションの違いこそありますが、どのコンストラクタプロパティも比較的共通した形式を持っています。

具体的に、Intl.Locale を除くすべてのコンストラクタプロパティは以下の 2 つの引数を受け取ります。

  1. ロケール識別子または Intl.Locale オブジェクト、あるいはそれらの配列
  2. 初期化時のオプション
// Intl.DateTimeFormat での例
const formatter = new Intl.DateTimeFormat("ja-JP", { calendar: "japanese" });

1. ロケール識別子または Intl.Locale オブジェクト、あるいはそれらの配列

1 つ目の引数は、ロケール識別子または Intl.Locale オブジェクトを受け取ります。

ロケール識別子とは、以下のようなロケールを表す文字列のことで、言語タグとも呼ばれます。

  • ja-JP
  • en-US

ロケール識別子自体は ECMAScript とは別に、IETF の BCP47 という仕様群によって定義されています。BCP47 については 4 日目の記事で詳しく解説しますが、ここでは言語や地域を表す決まった文字列と考えてください。

もう 1 つの Intl.Locale オブジェクトは、Intl が用意している「ロケール情報をより簡単に操作するため」のオブジェクトです。表せる情報の範囲ではロケール識別子で問題ありませんが、ロケール識別子が持つ情報を解析・操作しやすいインターフェースを提供しています。この Intl.Locale オブジェクトについては、2 日目3 日目の記事で解説します。また、配列の場合の挙動についても 4 日目の記事で触れます。

2. 初期化時のオプション

2 つ目の引数では、各機能の挙動を制御するためのオプションを受け取ります。具体的な機能ごとの例は以下の通りです。

  • 日時のフォーマット → 年月日の細かい表記法、24 時間表記かなど
  • 数値のフォーマット → 桁数、単位、区切り文字を使うかなど
  • 分ち書き → 単位は「文字」か「単語」か「文」など

唯一、localeMatcher というプロパティはすべてのコンストラクタプロパティで受け取ります。この localeMatcher オプションについては 4 日目の記事で解説します。

コンストラクタプロパティを new して使う

このように、Intl の機能はコンストラクタプロパティで提供されており、それらのコンストラクタを new して得られたインスタンスで書式化や判別などを行います。

例えば、Intl.DateTimeFormatIntl.NumberFormat() では format() メソッドが、Intl.Segmenter() では segment() メソッドが用意されており、各インスタンスに国際化機能を使うためのメソッドが備わっています。

// Intl.DateTimeFormat の例
const formatter = new Intl.DateTimeFormat("ja-JP", { calendar: "japanese" });
// 実際にフォーマットする
formatter.format(new Date("2024-12-01")); // R6/12/1

このように、Intl の使い方は「まずロケールと挙動のオプションを指定してインスタンスを作成し、そのインスタンスを利用する」という点がポイントになります。

なぜ一度インスタンスを作るのか(想像を含む)

ここからは仕様策定の議論をしっかり追ったわけではないので、筆者の想像を含みますが、Intl は「作成したインスタンスをアプリケーション全体で共有する前提で設計されている」と思っています。作成したインスタンスを共有することによるメリットは主に以下の 2 つです。

  • ロジックが複雑で作成コストが高いインスタンスの場合、使い回せたほうが良い
  • 任意のロケールにおいて、アプリケーション内での書式・挙動は統一されていることが望ましい

1 つ目の理由ですが、Intl の各機能は国際化のために多くの情報を参照することが多く、ロジックも複雑になりがちなので、インスタンス生成のコストが高いです。これは Intl 自体を策定する際に、統一された標準 API を持つメリットとしても言及されています。

Reuse of objects that are more expensive to create, like collators.
Intl の元になった EcmaScript i18n API strawman Proposal より

そのため、パフォーマンスのためにもこれらの機能はアプリケーション内で使い回されることが望ましく、使い回しやすいようなデザインになっていると考えられます。

また、2 つ目の理由として、任意のロケールにおいてアプリケーション内での書式・挙動は統一されていることが望ましいということが挙げられます。実際、表示される場所によって数値や日時の表記が異なっていると少なからず違和感を感じるはずです。一方で、毎回フォーマットや判別のたびにロケールやオプションを渡していると、同じ値を指定するのも面倒ですし、指定をミスしただけで表記ゆれが発生してしまいます。

このように、パフォーマンスに影響が出づらく、かつ表記や挙動をアプリケーション内で統一しやすくするためにも、Intl は「ロケールやオプションを指定するのは初期化時だけ」「あとは初期化したインスタンスを使い回す(限り挙動は統一される)」といった設計になっているのではないでしょうか。

以上の点を踏まえて、Intl の機能を使う際は、なるべく初期化したインスタンス(またはそれらをラップしたもの)をアプリケーション全体で共有することを意識すると良いでしょう。

次回予告

今回は Intl についての概要と、仕様の位置付け、基本となるインターフェースなどについて解説しました。次回 2 日目はロケール識別子(言語タグ)とその仕様 BCP47 について解説していきます。

参考文献

脚注
  1. JavaScript の仕様に言及する場合は原則 ECMAScript の名称を使用します。 ↩︎

  2. ECMA International では策定している仕様ごとに番号がついています。 ↩︎

  3. Intl などの国際化 API について話し合うのは TG2 です。 ↩︎

Discussion