フォーマット済み出力をスタイリング用に分割する方法

formatToParts()を使用してフォーマット済み出力の個々のコンポーネントにアクセスし、カスタムスタイリングを適用する

はじめに

JavaScriptフォーマッターのformat()メソッドは「$1,234.56」や「January 15, 2025」のような完全な文字列を返します。これは単純な表示には適していますが、個々の部分に異なるスタイルを適用することはできません。通貨記号を太字にしたり、月名を異なる色にしたり、特定の要素にカスタムマークアップを適用したりすることができません。

JavaScriptはこの問題を解決するためにformatToParts()メソッドを提供しています。単一の文字列を返す代わりに、フォーマットされた出力の各部分を表すオブジェクトの配列を返します。各部分にはcurrencymonthelementなどの型と、実際の文字列を含む値があります。これらの部分を処理して、カスタムスタイルを適用したり、複雑なレイアウトを構築したり、フォーマットされたコンテンツをリッチなユーザーインターフェースに統合したりすることができます。

formatToParts()メソッドは、NumberFormatDateTimeFormatListFormatRelativeTimeFormatDurationFormatなど、複数のIntlフォーマッターで利用できます。これにより、JavaScriptのすべての国際化フォーマットで一貫したパターンとなっています。

フォーマットされた文字列にスタイルを適用することが難しい理由

「$1,234.56」のようなフォーマットされた文字列を受け取った場合、通貨記号がどこで終わり、数字がどこから始まるかを簡単に識別することはできません。ロケールによって記号の位置が異なります。ロケールによっては異なる区切り文字を使用します。これらの文字列を確実に解析するには、Intl APIですでに実装されているフォーマットルールを複製する複雑なロジックが必要です。

通貨記号を異なる色で表示するダッシュボードを考えてみましょう。format()を使用する場合、以下のことが必要になります:

  1. どの文字が通貨記号であるかを検出する
  2. 記号と数字の間のスペースを考慮する
  3. ロケール間で異なる記号の位置に対応する
  4. 数字を壊さないように文字列を慎重に解析する

このアプローチは脆弱でエラーが発生しやすいです。ロケールのフォーマットルールが変更されると、解析ロジックが破綻します。

同じ問題は日付、リスト、その他のフォーマットされた出力にも存在します。ロケール固有のフォーマットルールを再実装せずに、フォーマットされた文字列を確実に解析してコンポーネントを識別することはできません。

formatToParts()メソッドはコンポーネントを個別に提供することでこの問題を解消します。ロケールに関係なく、どの部分が何であるかを正確に示す構造化されたデータを受け取ることができます。

formatToParts の仕組み

formatToParts() メソッドは戻り値を除いて format() と同じように動作します。同じオプションでフォーマッターを作成し、format() の代わりに formatToParts() を呼び出します。

このメソッドはオブジェクトの配列を返します。各オブジェクトには2つのプロパティがあります:

  • type:その部分が表すものを識別します(currencymonthliteral など)
  • value:その部分の実際の文字列を含みます

各部分はフォーマットされた出力と同じ順序で表示されます。すべての値を結合することでこれを確認できます。結果は format() を呼び出した場合とまったく同じ出力になります。

このパターンは formatToParts() をサポートするすべてのフォーマッターで一貫しています。部分のタイプはフォーマッターによって異なりますが、構造は常に同じです。

フォーマットされた数値を部分に分割する

NumberFormat フォーマッターは、フォーマットされた数値を分解するための formatToParts() を提供します。これは基本的な数値、通貨、パーセンテージ、その他の数値スタイルに対応しています。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = formatter.formatToParts(1234.56);
console.log(parts);

これは以下のようなオブジェクトの配列を出力します:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

各オブジェクトはその部分が表すものを識別し、その値を提供します。currency タイプは通貨記号を表します。integer タイプは整数部分の数字を表します。group タイプは桁区切り記号を表します。decimal タイプは小数点を表します。fraction タイプは小数点以下の数字を表します。

部分がフォーマットされた出力と一致することを確認できます:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// 出力: "$1,234.56"

連結された部分は format() を呼び出した場合とまったく同じ出力を生成します。

フォーマットされた数値の通貨記号のスタイリング

formatToParts()の主な使用例は、異なるコンポーネントに異なるスタイルを適用することです。パーツ配列を処理して、特定のタイプをHTML要素でラップすることができます。

通貨記号を太字にする例:

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

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

console.log(html);
// 出力: "<strong>$</strong>1,234.56"

このアプローチはあらゆるマークアップ言語で機能します。パーツ配列を処理することで、HTML、JSX、またはその他の形式を生成できます。

小数部分を異なるスタイルにする例:

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2
});

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 出力: "1,234<span class="text-gray-500">.50</span>"

このパターンは、小数部分が小さく表示されたり、薄く表示されたりする価格表示でよく使用されます。

フォーマットされた日付をパーツに分割する

DateTimeFormatフォーマッタは、フォーマットされた日付と時刻を分解するためのformatToParts()を提供します。

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
console.log(parts);

これは以下のようなオブジェクトの配列を出力します:

[
  { type: "month", value: "January" },
  { type: "literal", value: " " },
  { type: "day", value: "15" },
  { type: "literal", value: ", " },
  { type: "year", value: "2025" }
]

monthタイプは月名または数字を表します。dayタイプは月の日を表します。yearタイプは年を表します。literalタイプはフォーマッタによって挿入されるスペース、句読点、またはその他のテキストを表します。

フォーマットされた日付の月名のスタイリング

数値と同じパターンを使用して、日付コンポーネントにカスタムスタイルを適用できます。

月名を太字にする例:

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);
const html = parts
  .map(part => {
    if (part.type === "month") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 出力: "<strong>January</strong> 15, 2025"

複数の日付コンポーネントのスタイリング:

const formatter = new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2025, 0, 15);
const parts = formatter.formatToParts(date);

const html = parts
  .map(part => {
    switch (part.type) {
      case "weekday":
        return `<span class="font-bold">${part.value}</span>`;
      case "month":
        return `<span class="text-blue-600">${part.value}</span>`;
      case "year":
        return `<span class="text-gray-500">${part.value}</span>`;
      default:
        return part.value;
    }
  })
  .join("");

console.log(html);
// 出力: "<span class="font-bold">Wednesday</span>, <span class="text-blue-600">January</span> 15, <span class="text-gray-500">2025</span>"

この粒度の高い制御により、各コンポーネントに対して正確なスタイリングが可能になります。

フォーマットされたリストをパーツに分割する

ListFormatフォーマッターは、フォーマットされたリストを分解するためのformatToParts()を提供します。

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
console.log(parts);

これは以下のようなオブジェクトの配列を出力します:

[
  { type: "element", value: "apples" },
  { type: "literal", value: ", " },
  { type: "element", value: "oranges" },
  { type: "literal", value: ", and " },
  { type: "element", value: "bananas" }
]

elementタイプはリスト内の各アイテムを表します。literalタイプはフォーマッターによって追加された区切り文字や接続詞を表します。

リストアイテムを個別にスタイリングする

同じパターンを使用してリスト要素にカスタムスタイリングを適用できます。

リストアイテムを太字にする:

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);
const html = parts
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 出力: "<strong>apples</strong>, <strong>oranges</strong>, and <strong>bananas</strong>"

特定のリストアイテムをスタイリングする:

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

const items = ["apples", "oranges", "bananas"];
const parts = formatter.formatToParts(items);

let itemIndex = 0;
const html = parts
  .map(part => {
    if (part.type === "element") {
      const currentIndex = itemIndex++;
      if (currentIndex === 0) {
        return `<span class="text-green-600">${part.value}</span>`;
      }
      return part.value;
    }
    return part.value;
  })
  .join("");

console.log(html);
// 出力: "<span class="text-green-600">apples</span>, oranges, and bananas"

このアプローチにより、ロケール固有のフォーマットを適切に維持しながら、特定のアイテムを強調表示することができます。

フォーマットされた相対時間を部分に分割する

RelativeTimeFormatフォーマッタは、相対時間表現を分解するためのformatToParts()を提供します。

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "auto"
});

const parts = formatter.formatToParts(-1, "day");
console.log(parts);

これは以下のようなオブジェクトの配列を出力します:

[
  { type: "literal", value: "yesterday" }
]

数値による相対時間の場合:

const formatter = new Intl.RelativeTimeFormat("en-US", {
  numeric: "always"
});

const parts = formatter.formatToParts(-3, "day");
console.log(parts);
// [
//   { type: "integer", value: "3" },
//   { type: "literal", value: " days ago" }
// ]

integerタイプは数値を表します。literalタイプは相対時間の単位と方向を表します。

フォーマットされた期間を部分に分割する

DurationFormat フォーマッタは、フォーマットされた期間を分解するための formatToParts() を提供します。

const formatter = new Intl.DurationFormat("en-US", {
  style: "long"
});

const parts = formatter.formatToParts({
  hours: 2,
  minutes: 30,
  seconds: 15
});
console.log(parts);

これは以下のようなオブジェクトの配列を出力します:

[
  { type: "integer", value: "2" },
  { type: "literal", value: " hours, " },
  { type: "integer", value: "30" },
  { type: "literal", value: " minutes, " },
  { type: "integer", value: "15" },
  { type: "literal", value: " seconds" }
]

integer タイプは数値を表します。literal タイプは単位名と区切り文字を表します。

フォーマットされた部分からHTMLを構築する

部分を処理し、スタイルルールを一貫して適用する再利用可能な関数を作成できます。

function formatWithStyles(parts, styleMap) {
  return parts
    .map(part => {
      const style = styleMap[part.type];
      if (style) {
        return `<span class="${style}">${part.value}</span>`;
      }
      return part.value;
    })
    .join("");
}

const numberFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

const parts = numberFormatter.formatToParts(1234.56);
const html = formatWithStyles(parts, {
  currency: "font-bold text-gray-700",
  integer: "text-2xl",
  fraction: "text-sm text-gray-500"
});

console.log(html);
// 出力: "<span class="font-bold text-gray-700">$</span><span class="text-2xl">1</span>,<span class="text-2xl">234</span>.<span class="text-sm text-gray-500">56</span>"

このパターンはスタイルルールをフォーマットロジックから分離し、保守と再利用を容易にします。

ロケール固有の部分の順序を理解する

parts配列は自動的にロケール固有のフォーマットルールを維持します。異なるロケールでは、コンポーネントの配置順序や使用するフォーマットが異なりますが、formatToParts()はこれらの違いを処理します。

const usdFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

ドイツ語のフォーマットでは、通貨記号は数字の後にスペースを入れて配置されます。グループ区切り文字はピリオドで、小数点区切り文字はカンマです。スタイリングコードはロケールに関係なく同じ方法でparts配列を処理し、フォーマットは自動的に適応します。

アクセシブルなフォーマット表示の作成

formatToParts()を使用して、フォーマット済み出力にアクセシビリティ属性を追加できます。これにより、スクリーンリーダーが値を正確に読み上げることができます。

const formatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
});

function formatAccessible(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessible(1234.56));
// 出力: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

これにより、スクリーンリーダーがフォーマットされた表示値と、適切なコンテキストを持つ基礎となる数値の両方を読み上げることが保証されます。

formatToPartsとフレームワークコンポーネントの組み合わせ

Reactのようなモダンなフレームワークでは、formatToParts()を使用して効率的にコンポーネントを構築できます。

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

このコンポーネントは、あらゆるロケールと通貨に対して適切なフォーマットを維持しながら、異なる部分に異なるスタイルを適用します。

formatToPartsを使用するタイミング

カスタマイズなしの単純なフォーマット済み文字列が必要な場合は、format()を使用してください。これはほとんどの表示シナリオで一般的なケースです。

以下の場合はformatToParts()を使用してください:

  • フォーマット済み出力の異なる部分に異なるスタイルを適用する
  • フォーマット済みコンテンツでHTMLまたはJSXを構築する
  • 特定のコンポーネントに属性またはメタデータを追加する
  • フォーマット済み出力を複雑なレイアウトに統合する
  • フォーマット済み出力をプログラムで処理する
  • 細かい制御が必要なカスタムビジュアルデザインを作成する

formatToParts()メソッドは、単一の文字列ではなくオブジェクトの配列を作成するため、format()よりもわずかにオーバーヘッドが大きくなります。この差は一般的なアプリケーションでは無視できますが、1秒間に何千もの値をフォーマットする場合は、format()の方がパフォーマンスが良くなります。

ほとんどのアプリケーションでは、パフォーマンスの懸念よりもスタイリングのニーズに基づいて選択してください。出力をカスタマイズする必要がない場合は、format()を使用してください。カスタムスタイリングやマークアップが必要な場合は、formatToParts()を使用してください。

フォーマッター間で共通する部分タイプ

フォーマッターによって異なる部分タイプが生成されますが、複数のフォーマッターに共通するタイプもあります:

  • literal:スペース、句読点、またはフォーマットによって追加されるその他のテキスト。日付、数値、リスト、期間に表示されます。
  • integer:整数の桁。数値、相対時間、期間に表示されます。
  • decimal:小数点区切り。数値に表示されます。
  • fraction:小数点以下の桁。数値に表示されます。

フォーマッター固有のタイプには以下が含まれます:

  • 数値:currencygrouppercentSignminusSignplusSignunitcompactexponentInteger
  • 日付:weekdayerayearmonthdayhourminuteseconddayPeriodtimeZoneName
  • リスト:element
  • 相対時間:数値はintegerとして、テキストはliteralとして表示されます

これらのタイプを理解することで、どのフォーマッター出力も正しく処理するスタイリングコードを書くことができます。