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

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

はじめに

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

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);
// Output: "$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);
// Output: "<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);
// Output: "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);
// Output: "<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);
// Output: "<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);
// Output: "<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);
// Output: "<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);

これはオブジェクトの配列を出力します:

{/* CODE_PLACEHOLDER_abd978769d38434511f86f02f249689b */

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

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);
// Output: "<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>"

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

ロケール固有のパーツ順序を理解する

パーツ配列は、ロケール固有のフォーマットルールを自動的に維持します。異なるロケールでは、コンポーネントを異なる順序で配置し、異なるフォーマットを使用しますが、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: "€" }
// ]

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

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

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));
// Output: "<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として表示されます

これらの型を理解することで、あらゆるフォーマッター出力を正しく処理するスタイリングコードを記述できます。