Intl.ListFormat API

配列をロケールに対応した読みやすいリストに変換

はじめに

複数の項目をユーザーに表示する際、開発者は配列をカンマで結合し、最後の項目の前に「and」を追加することがよくあります。

const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", and " + users[users.length - 1];
// "Alice, Bob, and Charlie"

このアプローチは英語の句読点ルールをハードコーディングしており、他の言語では機能しません。日本語では異なる助詞を使用し、ドイツ語では異なる空白ルールがあり、中国語では異なる区切り文字を使用します。Intl.ListFormat APIは、各ロケールの規則に従ってリストをフォーマットすることで、この問題を解決します。

Intl.ListFormatの機能

Intl.ListFormatは、配列を任意の言語の文法および句読点ルールに従った人間が読みやすいリストに変換します。すべての言語に共通する3種類のリストを処理します。

  • 接続リストは「and」を使用して項目を接続します(「A、B、C」)
  • 選言リストは「or」を使用して選択肢を提示します(「A、B、またはC」)
  • 単位リストは接続詞なしで測定値をフォーマットします(「5フィート2インチ」)

このAPIは、句読点から語の選択、空白まで、各言語がこれらのリストタイプをどのようにフォーマットするかを認識しています。

基本的な使用方法

ロケールとオプションを指定してフォーマッターを作成し、配列を指定してformat()を呼び出します。

const formatter = new Intl.ListFormat("en", {
  type: "conjunction",
  style: "long"
});

const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"

フォーマッターは、エッジケースを含む任意の長さの配列を処理します。

formatter.format([]);              // ""
formatter.format(["bread"]);       // "bread"
formatter.format(["bread", "milk"]); // "bread and milk"

リストタイプによる接続詞の制御

typeオプションは、フォーマットされたリストに表示される接続詞を決定します。

接続リスト

すべての項目が一緒に適用されるリストにはtype: "conjunction"を使用します。これがデフォルトのタイプです。

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS, and JavaScript"

一般的な用途には、選択された項目の表示、機能の列挙、すべて適用される複数の値の表示などがあります。

選言リスト

選択肢や選択肢を提示するリストにはtype: "disjunction"を使用します。

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"

これは、オプションリスト、複数の解決策を含むエラーメッセージ、およびユーザーが1つの項目を選択するあらゆるコンテキストで表示されます。

単位リスト

接続詞なしで表示すべき測定値や技術的な値にはtype: "unit"を使用します。

const formatter = new Intl.ListFormat("en", { type: "unit" });

console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"

単位リストは、測定値、技術仕様、および複合値に使用できます。

リストスタイルで冗長性を制御

styleオプションは、フォーマットの冗長性を調整します。3つのスタイルがあります:longshortnarrow

const items = ["Monday", "Wednesday", "Friday"];

const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday, and Friday"

const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday, and Friday"

const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"

英語では、ほとんどのリストでlongshortは同じ出力を生成します。narrowスタイルは接続詞を省略します。他の言語では、特に選言リストにおいて、スタイル間でより多くのバリエーションが見られます。

言語によるリストフォーマットの違い

各言語には独自のリストフォーマット規則があります。Intl.ListFormatはこれらの違いを自動的に処理します。

英語はカンマ、スペース、接続詞を使用します。

const en = new Intl.ListFormat("en");
console.log(en.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris, and London"

ドイツ語は同じカンマ構造を使用しますが、接続詞が異なります。

const de = new Intl.ListFormat("de");
console.log(de.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris und London"

日本語は異なる区切り文字と助詞を使用します。

const ja = new Intl.ListFormat("ja");
console.log(ja.format(["東京", "パリ", "ロンドン"]));
// "東京、パリ、ロンドン"

中国語は完全に異なる句読点を使用します。

const zh = new Intl.ListFormat("zh");
console.log(zh.format(["东京", "巴黎", "伦敦"]));
// "东京、巴黎和伦敦"

これらの違いは句読点だけでなく、スペーシング規則、接続詞の配置、文法的助詞にまで及びます。単一のアプローチをハードコーディングすると、他の言語では機能しなくなります。

カスタムレンダリングのためのformatToPartsの使用

formatToParts()メソッドは、文字列の代わりにオブジェクトの配列を返します。各オブジェクトは、フォーマットされたリストの1つの部分を表します。

const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);

console.log(parts);
// [
//   { type: "element", value: "red" },
//   { type: "literal", value: ", " },
//   { type: "element", value: "green" },
//   { type: "literal", value: ", and " },
//   { type: "element", value: "blue" }
// ]

各部分にはtypevalueがあります。typeは、リスト項目の場合は"element"、フォーマット用の句読点や接続詞の場合は"literal"のいずれかです。

この構造により、要素とリテラルに異なるスタイルを適用するカスタムレンダリングが可能になります。

const formatter = new Intl.ListFormat("en");
const items = ["Alice", "Bob", "Charlie"];

const html = formatter.formatToParts(items)
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// "<strong>Alice</strong>, <strong>Bob</strong>, and <strong>Charlie</strong>"

このアプローチは、実際のリスト項目にカスタムプレゼンテーションを適用しながら、ロケールに適した句読点を維持します。

パフォーマンスのためのフォーマッターの再利用

Intl.ListFormatインスタンスの作成にはオーバーヘッドがあります。フォーマッターを一度作成して再利用してください。

// Create once
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// Reuse many times
function displayUsers(users) {
  return listFormatter.format(users.map(u => u.name));
}

function displayTags(tags) {
  return listFormatter.format(tags);
}

複数のロケールを持つアプリケーションの場合、フォーマッターをマップに保存します。

const formatters = new Map();

function getListFormatter(locale, options) {
  const key = `${locale}-${options.type}-${options.style}`;
  if (!formatters.has(key)) {
    formatters.set(key, new Intl.ListFormat(locale, options));
  }
  return formatters.get(key);
}

const formatter = getListFormatter("en", { type: "conjunction", style: "long" });
console.log(formatter.format(["a", "b", "c"]));

このパターンは、複数のロケールと設定をサポートしながら、繰り返しの初期化コストを削減します。

エラーメッセージのフォーマット

フォーム検証では、複数のエラーが発生することがよくあります。選択肢を提示するために、選言リストでフォーマットします。

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

function validatePassword(password) {
  const errors = [];

  if (password.length < 8) {
    errors.push("at least 8 characters");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("an uppercase letter");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("a number");
  }

  if (errors.length > 0) {
    return `Password must contain ${formatter.format(errors)}.`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "Password must contain at least 8 characters, an uppercase letter, or a number."

選言リストは、ユーザーがこれらの問題のいずれかを修正する必要があることを明確にし、フォーマットは各ロケールの規則に適応します。

選択された項目の表示

ユーザーが複数の項目を選択した場合、連言リストで選択内容をフォーマットします。

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function getSelectionMessage(selectedFiles) {
  if (selectedFiles.length === 0) {
    return "No files selected";
  }

  if (selectedFiles.length === 1) {
    return `${selectedFiles[0]} selected`;
  }

  return `${formatter.format(selectedFiles)} selected`;
}

console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv, and notes.txt selected"

このパターンは、ファイル選択、フィルター選択、カテゴリー選択、およびあらゆる複数選択インターフェースに適用できます。

長いリストの処理

多数の項目を含むリストの場合、フォーマット前に切り詰めることを検討してください。

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function formatUserList(users) {
  if (users.length <= 3) {
    return formatter.format(users);
  }

  const visible = users.slice(0, 2);
  const remaining = users.length - 2;

  return `${formatter.format(visible)}, and ${remaining} others`;
}

console.log(formatUserList(["Alice", "Bob", "Charlie", "David", "Eve"]));
// "Alice, Bob, and 3 others"

これにより、合計数を示しながら可読性を維持します。正確なしきい値は、インターフェースの制約によって異なります。

ブラウザサポートとフォールバック

Intl.ListFormatは、2021年4月以降のすべてのモダンブラウザで動作します。サポートには、Chrome 72以降、Firefox 78以降、Safari 14.1以降、Edge 79以降が含まれます。

機能検出でサポートを確認します。

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en");
  return formatter.format(items);
} else {
  // Fallback for older browsers
  return items.join(", ");
}

より広範な互換性を確保するには、@formatjs/intl-listformatのようなポリフィルを使用してください。必要な環境にのみインストールします。

if (typeof Intl.ListFormat === "undefined") {
  await import("@formatjs/intl-listformat/polyfill");
}

現在のブラウザサポート状況を考慮すると、ほとんどのアプリケーションはポリフィルなしでIntl.ListFormatを直接使用できます。

避けるべき一般的な間違い

フォーマッターを繰り返し新規作成することはリソースの無駄です。

// Inefficient
function display(items) {
  return new Intl.ListFormat("en").format(items);
}

// Efficient
const formatter = new Intl.ListFormat("en");
function display(items) {
  return formatter.format(items);
}

ユーザー向けテキストにarray.join()を使用すると、ローカライゼーションの問題が発生します。

// Breaks in other languages
const text = items.join(", ");

// Works across languages
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

英語の接続詞ルールが普遍的に適用されると仮定すると、他のロケールで誤った出力が生成されます。常にユーザーのロケールをコンストラクタに渡してください。

空の配列を処理しないと、予期しない出力が発生する可能性があります。

// Defensive
function formatItems(items) {
  if (items.length === 0) {
    return "No items";
  }
  return formatter.format(items);
}

format([])は空文字列を返しますが、明示的な空状態の処理によりユーザーエクスペリエンスが向上します。

Intl.ListFormatを使用するタイミング

文章内で複数の項目を表示する場合は常にIntl.ListFormatを使用してください。これには、ナビゲーションブレッドクラム、選択されたフィルター、検証エラー、ユーザーリスト、カテゴリタグ、機能リストが含まれます。

テーブルやオプションメニューなどの構造化データ表示には使用しないでください。これらのコンポーネントには、文章リストルールの範囲外に独自のフォーマット要件があります。

このAPIは、手動の文字列連結および結合パターンを置き換えます。ユーザー向けテキストにjoin(", ")を記述する場合は常に、Intl.ListFormatがより優れたロケールサポートを提供するかどうかを検討してください。