JavaScriptでorを使ったリストをフォーマットする方法

Intl.ListFormatとdisjunctionタイプを使用して、あらゆる言語で選択肢を正しくフォーマットする

はじめに

アプリケーションは、ユーザーに選択肢や代替案を提示することがよくあります。ファイルアップロードコンポーネントは「PNG、JPEG、またはSVG」ファイルを受け入れます。支払いフォームでは、支払い方法として「クレジットカード、デビットカード、またはPayPal」を許可します。エラーメッセージは、認証の失敗を解決するために「ユーザー名、パスワード、またはメールアドレス」の修正を提案します。

これらのリストは「または」を使用して選択肢を示します。文字列連結を使用して手動でフォーマットすると、他の言語では機能しません。なぜなら、言語によって句読点のルール、「または」を表す単語、カンマの配置規則が異なるためです。Intl.ListFormat APIとdisjunctionタイプを使用すると、これらの選択肢リストをあらゆる言語で正しくフォーマットできます。

選言的リストとは

選言的リストは、通常1つのオプションが適用される選択肢を提示します。「disjunction」という単語は、分離または選択肢を意味します。英語では、選言的リストは接続詞として「or」を使用します。

const paymentMethods = ["credit card", "debit card", "PayPal"];
// Desired output: "credit card, debit card, or PayPal"

これは、すべての項目が一緒に適用されることを示すために「and」を使用する連言的リストとは異なります。選言的リストは選択を伝え、連言的リストは組み合わせを伝えます。

選言的リストの一般的な使用例には、支払いオプション、ファイル形式の制限、トラブルシューティングの提案、検索フィルターの選択肢、およびユーザーが複数の可能性から1つのオプションを選択するあらゆるインターフェースが含まれます。

手動フォーマットが失敗する理由

英語話者は選言的リストを「A、B、またはC」として記述し、項目間にカンマを使用し、最後の項目の前に「or」を使用します。このパターンは他の言語では機能しません。

// Hardcoded English pattern
const items = ["apple", "orange", "banana"];
const text = items.slice(0, -1).join(", ") + ", or " + items[items.length - 1];
// "apple, orange, or banana"

このコードは、スペイン語、フランス語、ドイツ語、およびその他のほとんどの言語で誤った出力を生成します。各言語には、選言的リストに対する独自のフォーマットルールがあります。

スペイン語では、カンマなしで「o」を使用します:

Expected: "manzana, naranja o plátano"
English pattern produces: "manzana, naranja, or plátano"

フランス語では、カンマなしで「ou」を使用します:

Expected: "pomme, orange ou banane"
English pattern produces: "pomme, orange, or banane"

ドイツ語では、カンマなしで「oder」を使用します:

Expected: "Apfel, Orange oder Banane"
English pattern produces: "Apfel, Orange, or Banane"

日本語では、異なる句読点とともに助詞「か」を使用します:

Expected: "りんご、オレンジ、またはバナナ"
English pattern produces: "りんご、オレンジ、 or バナナ"

これらの違いは、単純な単語の置き換えを超えています。句読点の配置、スペースのルール、文法的な助詞はすべて言語によって異なります。手動の文字列連結では、この複雑さに対応できません。

選言タイプでIntl.ListFormatを使用する

Intl.ListFormat APIは、言語固有のルールに従ってリストをフォーマットします。typeオプションを"disjunction"に設定して、選択肢のリストをフォーマットします:

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

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

フォーマッターは任意の配列長を処理します:

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

console.log(formatter.format([]));
// ""

console.log(formatter.format(["credit card"]));
// "credit card"

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

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

APIは、各ケースに対して正しい句読点と接続詞を自動的に適用します。

選言スタイルを理解する

styleオプションは、フォーマットの冗長性を制御します。3つのスタイルが存在します:longshortnarrowlongスタイルがデフォルトです。

const items = ["email", "phone", "SMS"];

const long = new Intl.ListFormat("en", {
  type: "disjunction",
  style: "long"
});
console.log(long.format(items));
// "email, phone, or SMS"

const short = new Intl.ListFormat("en", {
  type: "disjunction",
  style: "short"
});
console.log(short.format(items));
// "email, phone, or SMS"

const narrow = new Intl.ListFormat("en", {
  type: "disjunction",
  style: "narrow"
});
console.log(narrow.format(items));
// "email, phone, or SMS"

英語では、3つのスタイルすべてが選言リストに対して同一の出力を生成します。他の言語では、より多くのバリエーションが見られます。ドイツ語では、longスタイルで「oder」を使用し、narrowスタイルでは省略形を使用する場合があります。複数の敬語レベルや長い接続詞を持つ言語では、違いがより顕著になります。

narrowスタイルは通常、スペースを削除するか、より短い接続詞を使用して、制約のあるレイアウトでスペースを節約します。標準テキストにはlongスタイル、適度にコンパクトな表示にはshortスタイル、モバイルインターフェースやコンパクトなテーブルなどの厳しいスペース制約にはnarrowスタイルを使用してください。

選言リストが異なる言語でどのように表示されるか

各言語は、それぞれの慣習に従って選言リストをフォーマットします。Intl.ListFormatはこれらの違いを自動的に処理します。

英語では「or」とともにカンマを使用します。

const en = new Intl.ListFormat("en", { type: "disjunction" });
console.log(en.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG, or SVG"

スペイン語では「o」とともにカンマを使用し、最後の接続詞の前にはカンマを付けません。

const es = new Intl.ListFormat("es", { type: "disjunction" });
console.log(es.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG o SVG"

フランス語では「ou」とともにカンマを使用し、最後の接続詞の前にはカンマを付けません。

const fr = new Intl.ListFormat("fr", { type: "disjunction" });
console.log(fr.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG ou SVG"

ドイツ語では「oder」とともにカンマを使用し、最後の接続詞の前にはカンマを付けません。

const de = new Intl.ListFormat("de", { type: "disjunction" });
console.log(de.format(["PNG", "JPEG", "SVG"]));
// "PNG, JPEG oder SVG"

日本語では異なる句読点と助詞を使用します。

const ja = new Intl.ListFormat("ja", { type: "disjunction" });
console.log(ja.format(["PNG", "JPEG", "SVG"]));
// "PNG、JPEG、またはSVG"

中国語では中国語の句読点を使用します。

const zh = new Intl.ListFormat("zh", { type: "disjunction" });
console.log(zh.format(["PNG", "JPEG", "SVG"]));
// "PNG、JPEG或SVG"

これらの例は、APIが各言語の文法および句読点の慣習にどのように適応するかを示しています。適切なロケールを指定すれば、同じコードがすべての言語で機能します。

支払いオプションのフォーマット

支払いフォームでは複数の支払い方法の選択肢を提示します。選言リストを使用してフォーマットします。

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

function getPaymentMessage(methods) {
  if (methods.length === 0) {
    return "No payment methods available";
  }

  return `Pay with ${formatter.format(methods)}.`;
}

const methods = ["credit card", "debit card", "PayPal", "Apple Pay"];
console.log(getPaymentMessage(methods));
// "Pay with credit card, debit card, PayPal, or Apple Pay."

国際的なアプリケーションの場合は、ユーザーのロケールを渡します。

const userLocale = navigator.language; // e.g., "fr-FR"
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });

function getPaymentMessage(methods) {
  if (methods.length === 0) {
    return "No payment methods available";
  }

  return `Pay with ${formatter.format(methods)}.`;
}

このアプローチは、チェックアウトフロー、支払い方法セレクター、およびユーザーが支払い方法を選択するあらゆるインターフェースで機能します。

ファイルアップロード制限のフォーマット

ファイルアップロードコンポーネントは、システムが受け入れるファイルタイプを指定します。

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

function getAcceptedFormatsMessage(formats) {
  if (formats.length === 0) {
    return "No file formats accepted";
  }

  if (formats.length === 1) {
    return `Accepted format: ${formats[0]}`;
  }

  return `Accepted formats: ${formatter.format(formats)}`;
}

const imageFormats = ["PNG", "JPEG", "SVG", "WebP"];
console.log(getAcceptedFormatsMessage(imageFormats));
// "Accepted formats: PNG, JPEG, SVG, or WebP"

const documentFormats = ["PDF", "DOCX"];
console.log(getAcceptedFormatsMessage(documentFormats));
// "Accepted formats: PDF or DOCX"

このパターンは、画像アップロード、ドキュメント送信、およびフォーマット制限のあるあらゆるファイル入力で機能します。

トラブルシューティング提案のフォーマット

エラーメッセージは、問題を解決するための複数の方法を提案することがよくあります。これらの提案を選言的リストとして提示します。

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

function getAuthenticationError(missingFields) {
  if (missingFields.length === 0) {
    return "Authentication failed";
  }

  return `Please check your ${formatter.format(missingFields)} and try again.`;
}

console.log(getAuthenticationError(["username", "password"]));
// "Please check your username or password and try again."

console.log(getAuthenticationError(["email", "username", "password"]));
// "Please check your email, username, or password and try again."

選言的リストは、ユーザーが必ずしもすべてのフィールドではなく、言及されたフィールドのいずれかを修正する必要があることを明確にします。

検索フィルターの選択肢のフォーマット

検索インターフェースはアクティブなフィルターを表示します。フィルターが選択肢を提示する場合は、選言的リストを使用します。

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

function getFilterSummary(filters) {
  if (filters.length === 0) {
    return "No filters applied";
  }

  if (filters.length === 1) {
    return `Showing results for: ${filters[0]}`;
  }

  return `Showing results for: ${formatter.format(filters)}`;
}

const categories = ["Electronics", "Books", "Clothing"];
console.log(getFilterSummary(categories));
// "Showing results for: Electronics, Books, or Clothing"

これは、カテゴリフィルター、タグ選択、および選択された値が組み合わせではなく選択肢を表すあらゆるフィルターインターフェースで機能します。

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

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

// Create once at module level
const disjunctionFormatter = new Intl.ListFormat("en", { type: "disjunction" });

// Reuse in multiple functions
function formatPaymentMethods(methods) {
  return disjunctionFormatter.format(methods);
}

function formatFileTypes(types) {
  return disjunctionFormatter.format(types);
}

function formatErrorSuggestions(suggestions) {
  return disjunctionFormatter.format(suggestions);
}

複数のロケールをサポートするアプリケーションの場合は、フォーマッターをキャッシュに保存します。

const formatters = new Map();

function getDisjunctionFormatter(locale) {
  if (!formatters.has(locale)) {
    formatters.set(
      locale,
      new Intl.ListFormat(locale, { type: "disjunction" })
    );
  }
  return formatters.get(locale);
}

const formatter = getDisjunctionFormatter("en");
console.log(formatter.format(["A", "B", "C"]));
// "A, B, or C"

このパターンは、アプリケーション全体で複数のロケールをサポートしながら、初期化コストを削減します。

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

formatToParts()メソッドは、フォーマットされたリストの各部分を表すオブジェクトの配列を返します。これにより、カスタムスタイリングが可能になります。

const formatter = new Intl.ListFormat("en", { type: "disjunction" });
const parts = formatter.formatToParts(["PNG", "JPEG", "SVG"]);

console.log(parts);
// [
//   { type: "element", value: "PNG" },
//   { type: "literal", value: ", " },
//   { type: "element", value: "JPEG" },
//   { type: "literal", value: ", or " },
//   { type: "element", value: "SVG" }
// ]

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

これを使用して、要素とリテラルに異なるスタイルを適用します。

const formatter = new Intl.ListFormat("en", { type: "disjunction" });
const formats = ["PNG", "JPEG", "SVG"];

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

console.log(html);
// "<code>PNG</code>, <code>JPEG</code>, or <code>SVG</code>"

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

ブラウザサポートと互換性

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

APIを使用する前にサポートを確認します。

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en", { type: "disjunction" });
  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");
}

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

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

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

選言型の代わりに連言型を使用すると、誤った意味になります。

// Wrong: suggests all methods required
const wrong = new Intl.ListFormat("en", { type: "conjunction" });
console.log(`Pay with ${wrong.format(["credit card", "debit card"])}`);
// "Pay with credit card and debit card"

// Correct: suggests choosing one method
const correct = new Intl.ListFormat("en", { type: "disjunction" });
console.log(`Pay with ${correct.format(["credit card", "debit card"])}`);
// "Pay with credit card or debit card"

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

// Inefficient
function formatOptions(options) {
  return new Intl.ListFormat("en", { type: "disjunction" }).format(options);
}

// Efficient
const formatter = new Intl.ListFormat("en", { type: "disjunction" });
function formatOptions(options) {
  return formatter.format(options);
}

文字列に「or」をハードコーディングすると、ローカライゼーションができなくなります。

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

// Works across languages
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
const allItems = [...items, "other options"];
const text = formatter.format(allItems);

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

// Defensive
function formatPaymentMethods(methods) {
  if (methods.length === 0) {
    return "No payment methods available";
  }
  return formatter.format(methods);
}

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

選言リストを使用する場合

選言リストは、通常1つのオプションが適用される代替案や選択肢を提示する場合に使用します。これには、支払い方法の選択、ファイル形式の制限、認証エラーの提案、検索フィルターオプション、アカウントタイプの選択などが含まれます。

すべての項目を一緒に適用する必要がある場合は、選言リストを使用しないでください。代わりに連言リストを使用してください。たとえば、「名前、メール、パスワードが必要です」は、1つだけではなくすべてのフィールドを提供する必要があるため、連言を使用します。

選択の意味を持たない中立的な列挙には、選言リストを使用しないでください。測定値や技術仕様は、通常、選言や連言ではなく単位リストを使用します。

このAPIは、代替案のための手動文字列連結パターンを置き換えます。ユーザー向けテキストで項目を「or」で結合するコードを記述する場合は、選言型のIntl.ListFormatがより優れたロケールサポートを提供するかどうかを検討してください。