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 オプションはフォーマットの詳細度を調整します。longshortnarrow の3つのスタイルが存在します。

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()メソッドは文字列の代わりにオブジェクトの配列を返します。各オブジェクトはフォーマットされたリストの一部を表します:

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インスタンスの作成にはオーバーヘッドがあります。フォーマッターを一度作成して再利用しましょう:

// 一度だけ作成
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// 何度も再利用
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("8文字以上");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("大文字");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("数字");
  }

  if (errors.length > 0) {
    return `パスワードには${formatter.format(errors)}が必要です。`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "パスワードには8文字以上、大文字、または数字が必要です。"

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

選択されたアイテムの表示

ユーザーが複数のアイテムを選択した場合、接続リストで選択内容をフォーマットします:

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

function getSelectionMessage(selectedFiles) {
  if (selectedFiles.length === 0) {
    return "ファイルが選択されていません";
  }

  if (selectedFiles.length === 1) {
    return `${selectedFiles[0]}が選択されました`;
  }

  return `${formatter.format(selectedFiles)}が選択されました`;
}

console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf、data.csv、およびnotes.txtが選択されました"

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

長いリストの処理

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

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 {
  // 古いブラウザ向けのフォールバック
  return items.join(", ");
}

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

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

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

避けるべき一般的なミス

新しいフォーマッターを繰り返し作成するとリソースが無駄になります:

// 非効率的
function display(items) {
  return new Intl.ListFormat("en").format(items);
}

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

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

// 他の言語で問題が発生
const text = items.join(", ");

// あらゆる言語で機能する
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

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

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

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

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

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

文章内で複数の項目を表示する際は常にIntl.ListFormatを使用してください。これにはナビゲーションのパンくずリスト、選択されたフィルター、バリデーションエラー、ユーザーリスト、カテゴリータグ、機能リストなどが含まれます。

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

このAPIは手動での文字列連結や結合パターンに取って代わります。ユーザー向けテキストに対してjoin(", ")を書く場合は常に、Intl.ListFormatがより良いロケールサポートを提供するかどうかを検討してください。