JavaScriptでの「または」を使ったリストのフォーマット方法

Intl.ListFormatに選言タイプを使用して、あらゆる言語で代替案を正しくフォーマットする

はじめに

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

これらのリストは選択肢を示すために「または」を使用します。文字列連結で手動でフォーマットすると、他の言語では問題が生じます。なぜなら、異なる言語には異なる句読点のルール、「または」に相当する異なる単語、そしてカンマの配置に関する異なる慣習があるからです。選言型のIntl.ListFormat APIを使用すると、どの言語でも選択肢のリストを正しくフォーマットできます。

選言的リストとは

選言的リストは、通常一つのオプションが適用される代替案を提示します。「選言」という言葉は分離または代替を意味します。日本語では、選言的リストは接続詞として「または」を使用します:

const paymentMethods = ["クレジットカード", "デビットカード", "PayPal"];
// 期待される出力: "クレジットカード、デビットカード、またはPayPal"

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

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

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

英語話者は選言的リストを「A、B、またはC」と書き、項目間にカンマを入れ、最後の項目の前に「または」を置きます。このパターンは他の言語では機能しません:

// ハードコードされた英語パターン
const items = ["りんご", "オレンジ", "バナナ"];
const text = items.slice(0, -1).join(", ") + ", or " + items[items.length - 1];
// "りんご, オレンジ, or バナナ"

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

スペイン語では、「o」の前にカンマを使用しません:

期待される出力: "manzana, naranja o plátano"
英語パターンが生成する出力: "manzana, naranja, or plátano"

フランス語では、「ou」の前にカンマを使用しません:

期待される出力: "pomme, orange ou banane"
英語パターンが生成する出力: "pomme, orange, or banane"

ドイツ語では、「oder」の前にカンマを使用しません:

期待される出力: "Apfel, Orange oder Banane"
英語パターンが生成する出力: "Apfel, Orange, or Banane"

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

期待される出力: "りんご、オレンジ、またはバナナ"
英語パターンが生成する出力: "りんご、オレンジ、 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 オプションはフォーマットの詳細度を制御します。longshortnarrow の3つのスタイルが存在します。デフォルトは long スタイルです。

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つのスタイルすべてが同じ出力を生成します。他の言語ではより多くのバリエーションが見られます。ドイツ語では長いスタイルで「oder」を使用し、狭いスタイルでは省略形を使用することがあります。複数の丁寧さのレベルや長い接続詞を持つ言語では、その違いがより顕著になります。

狭いスタイルは通常、スペースを節約するためにスペースを削除したり、より短い接続詞を使用したりします。標準的なテキストには長いスタイル、適度にコンパクトな表示には短いスタイル、モバイルインターフェースやコンパクトな表などのスペースが限られている場合には狭いスタイルを使用してください。

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

各言語は独自の規則に従って選言的リストをフォーマットします。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; // 例:"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インスタンスの作成にはオーバーヘッドがあります。フォーマッターを一度作成して再利用しましょう:

// モジュールレベルで一度作成する
const disjunctionFormatter = new Intl.ListFormat("en", { type: "disjunction" });

// 複数の関数で再利用する
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 {
  // 古いブラウザ向けのフォールバック
  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を使用できます。

避けるべき一般的なミス

選言型(disjunction)の代わりに連言型(conjunction)を使用すると、誤った意味になります:

// 誤:すべてのメソッドが必要であることを示唆
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"

// 正:一つのメソッドを選択することを示唆
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"

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

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

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

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

// 他の言語で動作しない
const text = items.join(", ") + ", or other options";

// すべての言語で動作する
const formatter = new Intl.ListFormat(userLocale, { type: "disjunction" });
const allItems = [...items, "other options"];
const text = formatter.format(allItems);

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

// 防御的な実装
function formatPaymentMethods(methods) {
  if (methods.length === 0) {
    return "No payment methods available";
  }
  return formatter.format(methods);
}

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

選言的リストを使用するタイミング

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

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

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

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