JavaScript

JavaScriptのIntl.Segmenter.segment():絵文字を壊さず文字数を数える

この記事でわかること

Intl.Segmenter.segment()で絵文字や結合文字を分断せずに数え、短縮する方法を解説。引数・戻り値、ブラウザーとNode.jsでの実行手順、期待出力と互換性の制約を学べます。

入力欄の文字数表示やタイトルの短縮では、利用者が認識する「1文字」とJavaScriptが数える単位の違いが問題になります。絵文字や濁点を含む文字を単純に切り出すと、途中で分断されることがあります。

Intl.Segmenter.prototype.segment()は、指定した単位で文字列を分節するメソッドです。この記事では書記素クラスタ単位の分節に絞り、文字数のカウントと、文字のまとまりを保った短縮を実装します。

目的:見た目の文字のまとまりを扱う

JavaScriptの文字列のlengthはUTF-16コード単位の数です。例えば"😊".lengthは2ですが、利用者には通常1つの絵文字として見えます。

文字列をスプレッド構文で配列にするとコードポイント単位になります。ただし、複数のコードポイントからできた文字のまとまりまでは保てません。例えば"e\u0301"は、英字と結合アクセントからできた文字列です。コードポイントは2つですが、通常はアクセント付きの1文字として表示されます。

そこでgranularityに"grapheme"を指定します。書記素クラスタは、利用者が1文字として認識するまとまりを扱うための単位です。ただし、描画幅やフォントの字形の個数を測るものではありません。分節の基本的な考え方はMDNのコンストラクター解説で確認できます。

動作環境と対応バージョン

2026年10月7日時点で確認した公式資料では、Intl.Segmenterは2024年4月に主要ブラウザーエンジンで利用可能になったBaseline 2024の機能です。対応開始バージョンはChrome 87、Edge 87、Firefox 125、Safari 14.1です。古いブラウザーやアプリ内のWebViewでは、実行前に機能の有無を確認してください。web.devの対応状況

Node.jsでも利用できます。本記事の実行対象は、公式配布のNode.js 22系または24系とします。これは記事の対象環境であり、APIが初めて導入されたバージョンを示すものではありません。ブラウザー専用のDOMには依存せず、同じJavaScriptコードを両環境で実行できます。

Node.jsの国際化機能はICUを利用します。標準では完全なICUデータを含みますが、国際化を無効にした独自ビルドではIntl自体が存在しない場合があります。限定的なICU構成ではロケールの対応にも注意が必要です。Node.jsの国際化サポート

引数と戻り値を理解する

最初にnew Intl.Segmenter("ja", { granularity: "grapheme" })で分節器を作り、そのインスタンスのsegment(input)を呼びます。

  • コンストラクターのlocalesはロケールの指定です。ここでは日本語を表す"ja"を使います。
  • options.granularityは分節単位です。既定値は"grapheme"で、ほかに"word"と"sentence"があります。
  • segment(input)のinputは分節対象の文字列です。
  • 戻り値は反復可能なSegmentsオブジェクトです。配列や文字数そのものではありません。

Segmentsはfor...ofで順番に取り出せます。各要素のsegmentは分節された文字列、indexは元の文字列内の開始位置、inputは元の文字列全体です。indexは書記素の通し番号ではなく、UTF-16コード単位で数えた位置です。MDNのsegment()解説

配列に変換する場合

Array.from(segmenter.segment(text), item => item.segment)と書けば、分節された文字列だけの配列になります。その配列のlengthが書記素クラスタの数です。空文字列の分節結果には要素がないため、配列の長さは0になります。

Segmentsが反復可能なオブジェクトであることはMDNのSegments解説にも記載されています。

自己完結したコードを実行する

次のコードは外部ライブラリーや通信を必要としません。結合アクセントと絵文字を含む文字列を数え、先頭2書記素を残して短縮します。各例は即時実行関数で囲んでいるため、ブラウザーのコンソールで繰り返し実行しても変数の再宣言を避けられます。

JavaScript
(() => {
  try {
    if (
      typeof Intl === "undefined" ||
      typeof Intl.Segmenter !== "function"
    ) {
      throw new Error("Intl.Segmenterに対応した環境が必要です");
    }

    const segmenter = new Intl.Segmenter("ja", {
      granularity: "grapheme"
    });

    function toGraphemes(text) {
      if (typeof text !== "string") {
        throw new TypeError("textは文字列にしてください");
      }
      return Array.from(
        segmenter.segment(text),
        item => item.segment
      );
    }

    function shorten(text, max) {
      if (!Number.isInteger(max) || max < 0) {
        throw new RangeError("maxは0以上の整数にしてください");
      }
      const parts = toGraphemes(text);
      if (parts.length <= max) {
        return text;
      }
      return parts.slice(0, max).join("") + "…";
    }

    const text = "Ae\u0301😊B";
    const parts = toGraphemes(text);

    console.log(`UTF-16単位数: ${text.length}`);
    console.log(`コードポイント数: ${[...text].length}`);
    console.log(`書記素数: ${parts.length}`);
    console.log(`分節: ${JSON.stringify(parts)}`);
    console.log(`短縮: ${shorten(text, 2)}`);
    console.log(`空文字の書記素数: ${toGraphemes("").length}`);

    for (const item of segmenter.segment(text)) {
      console.log(`開始位置 ${item.index}: ${item.segment}`);
    }
  } catch (error) {
    console.error(
      error instanceof Error ? error.message : String(error)
    );
  }
})();

ブラウザーでの実行方法

対応ブラウザーで通常のWebページを開き、開発者ツールのConsoleを選びます。上のJavaScriptコード全体を入力して実行してください。HTMLファイルやサーバーの準備は不要です。絵文字の外見はOSやフォントによって変わることがあります。

Node.jsでの実行方法

対象バージョンのNode.jsを用意し、上のコードをUTF-8のsegment-demo.cjsとして保存します。ターミナルで保存先のディレクトリーに移動し、次のコマンドを実行してください。requireやimportは不要です。

Shell
node --version
node segment-demo.cjs

期待出力

対応環境では、次の内容が出力される想定です。アクセント付き文字は英字と結合アクセントの組み合わせなので、表示上は1文字でも内部には2つのコードポイントがあります。以下は公式仕様に基づく期待出力であり、この記事の作成時に実機実行した結果ではありません。

CODE
UTF-16単位数: 6
コードポイント数: 5
書記素数: 4
分節: ["A","é","😊","B"]
短縮: Aé…
空文字の書記素数: 0
開始位置 0: A
開始位置 1: é
開始位置 3: 😊
開始位置 5: B

shorten()のmaxは、省略記号を除いて残す書記素数です。そのためmaxが2なら、短縮結果は省略記号を含めて3書記素になります。省略記号込みで上限を決めたい設計では、その分の枠を予約する必要があります。

失敗例と例外処理

コードポイント単位でも分断が起きる

次の例は、そのまま単独で実行できます。スプレッド構文で配列化して先頭1要素だけ残すと、結合アクセントが落ちます。書記素単位で取り出すと文字のまとまりが保たれます。

JavaScript
(() => {
  const text = "e\u0301";
  console.log([...text].slice(0, 1).join(""));

  if (
    typeof Intl !== "undefined" &&
    typeof Intl.Segmenter === "function"
  ) {
    const segmenter = new Intl.Segmenter("ja", {
      granularity: "grapheme"
    });
    const parts = Array.from(
      segmenter.segment(text),
      item => item.segment
    );
    console.log(parts[0]);
  } else {
    console.log("Intl.Segmenterは未対応です");
  }
})();

対応環境での期待出力は次のとおりです。

CODE
e
é

不正な設定と入力を区別する

granularityに"character"などの無効な値を指定すると、コンストラクターはRangeErrorを投げます。また、Intl.Segmenterの生成にはnewが必要で、省略するとTypeErrorになります。MDNの例外の説明

主例のtoGraphemes()では、入力が文字列かどうかも検査しています。このTypeErrorは自作関数の入力ルールです。API自体の例外条件と混同しないでください。

処理は同期的です。Promiseを返さないためawaitは不要で、同期例外はtry...catchで扱います。未対応環境では、意味の異なる数え方へ黙って切り替えるより、対応環境を案内するほうが文字数の規則を維持できます。

活用事例と設計上の制約

プロフィール名の文字数表示、通知文の短縮、絵文字を含む一覧タイトルのプレビューに利用できます。主例では同じ分節器を複数回使い、カウントと短縮が同じ単位になるようにしています。

ただし、書記素数はUTF-8のバイト数でも、画面上の横幅でもありません。データベースの容量制限やレイアウト幅の判定には、それぞれ別の基準が必要です。また、分節によって文字列が正規化されるわけではなく、分節結果には元の文字列の表現が保たれます。

UnicodeやICUの更新により、新しい文字の扱いなどが実行環境間で変わる可能性もあります。ブラウザーのカウントを入力の補助に使い、保存時の最終判定をサーバー側で行う設計にすると、制限の適用場所が明確になります。

改善案として、運用では結合アクセント、家族の絵文字、国旗、肌の色を指定した絵文字を確認用データに加えてください。単純な英数字だけでは、分断に関する不具合を見つけにくいためです。

主例は理解しやすさを優先して全分節を配列化しています。長い文章を大量に扱う場合は、カウントをfor...ofで行い、短縮では必要な範囲まで反復する方法を検討できます。最初は提示コードで数え方を確認し、処理量が問題になった段階で配列の確保を減らすと進めやすくなります。

公式参考資料

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →