チェック・トラブル対処Read this article in English

日本語の改行が変な位置に入る — 行頭に「、」「」」が来る不具合

行頭に「、」や「。」だけが来ている。閉じ括弧が前の文字なしで行頭に取り残されている。「っ」や「ょ」のような小書きのかなが行頭から始まっている。誤字でも誤訳でもなく、改行の位置そのものが、日本語の組版で禁止されている場所に入っています。

破られている規則には禁則処理という名前があり、機械的に検査できる程度に具体的です。見た目がほぼ同じ症状になる原因は3つあります。禁則の文字表を持たない描画系、文字表はあるが緩い設定で動いている描画系、原文に書き込まれた改行文字が翻訳後も残っている場合です。

原因より重要な性質が1つあります。この不具合が「再現しない」として閉じられる理由です。規則に違反するかどうかは、1行に入る文字数で変わります。同じ文が10文字では問題なく、12文字で崩れ、13文字で再び問題なくなります。

行頭に来てはいけない文字、行末に来てはいけない文字

禁則処理は2種類の規則として説明されがちですが、実際には3種類です。行頭禁則は、ある文字集合が行の先頭に来ることを禁止し、改行位置を手前にずらして前の行に留めます。行末禁則は、開き括弧が行の末尾に来ることを禁止します。囲むはずの文章から切り離されて見えるからです。正式な規定は、日本語文書の行組版を定めた日本産業規格 JIS X 4051 の行分割に関する箇条です。節末の表は代表例であって網羅ではなく、全角空白と中点を行頭禁則に含めるかは実装で分かれます。

この集合のうち括弧類は計算で求められ、小書きのかなは求められません。閉じ括弧類は Unicode の一般カテゴリが Close_Punctuation、開き括弧類は Open_Punctuation なので、文字表がなくても判別できます。ところが小書きの「っ」は U+3063、通常の「つ」は U+3064 で、どちらもカテゴリは Other_Letter、どちらも Alphabetic を持ち、ブラウザの正規表現から参照できる Unicode のプロパティにこの2つを区別できるものはありません。他のプロパティも助けになりません。Modifier_Letter と Extender が選ぶのは長音記号「ー」と繰返し記号「々」で、小書きのかなには一度も一致しません。通常のかなと同じ Other_Letter で Extender なしだからです。

つまり小書きのかなの禁則は、手で打ち込んだ明示的なコードポイントの一覧から来るしかありません。これが、「、」と「」」は完璧に処理する描画系が「っ」を行頭に許す理由です。報告するときにどの集合が影響を受けているかを書けば、設定の問題か文字表の欠落かが伝わります。単語単位の折り返しが日本語に流用できない理由は、CJKテキストの改行と折り返しで扱っています。

行頭禁則(行の先頭に来てはいけない) — 代表例
  句読点      、 U+3001   。 U+3002   , U+FF0C   . U+FF0E
  閉じ括弧類  」 U+300D   』 U+300F   ) U+FF09   〕 U+3015
              】 U+3011   } U+FF5D   〉 U+3009   》 U+300B
  中点類      ・ U+30FB   : U+FF1A   ; U+FF1B
  区切り約物  ! U+FF01   ? U+FF1F
  小書きのかな っ U+3063   ゃ U+3083   ゅ U+3085   ょ U+3087
              ぁ U+3041   ぃ U+3043   ぅ U+3045   ぇ U+3047
              ぉ U+3049   ゎ U+308E
  記号        ー U+30FC   々 U+3005   ゛ U+309B   ゜ U+309C

行末禁則(行の末尾に来てはいけない) — 代表例
  開き括弧類  「 U+300C   『 U+300E   ( U+FF08   〔 U+3014
              【 U+3010   { U+FF5B   〈 U+3008   《 U+300A

分離禁止(この組は分断しない)
  …… (U+2026 を2つ)   ‥‥ (U+2025 を2つ)   ―― (U+2015 を2つ)

同じ文が、ある幅では正しく別の幅では崩れる

実験はこうです。日本語の文字列を1行あたり固定文字数で折り返し、先頭の文字が行頭禁則に含まれる行を検出し、幅8から24まで繰り返す。結果が節末の表です。

眺めるべきはサンプルCです。ごく普通のシステムメッセージで、禁則処理をまったく行わなくても17通りの幅のうち15通りで正しく表示されます。崩れるのは2通りだけです。サンプルBも13通りで問題がなく、幅12で崩れます。目視で問題がなかったことは弱い証拠にすぎず、再現できない違反報告は幅の違いであることが多い、ということです。

実効的な幅は、どこかで設定する数値でもありません。ボックスの幅を字送り幅で割った結果なので、フォントの代替やポイントサイズの変更で、ゲーム中のすべての改行位置が静かにずれます。枠に何文字入るかの計算は日本語と英語のピクセルフォント、フォント代替で出る別の症状は豆腐(未収録文字の四角)で扱っています。

禁則処理なしの素朴な固定幅折り返し、幅8〜24

A  43文字  「セーブデータを上書きしますか?」とっさに判断できないので、もう一度確認してください。
   違反する幅 8, 9, 14, 15, 16, 18, 21     17通りのうち10通りは問題なし
     幅14 -> 4行目が 。 で始まる
     幅15 -> 2行目が ? で始まる
     幅16 -> 2行目が 」 で始まる

B  32文字  ここは危険だ、先に進むなら回復アイテムを持っていったほうがいい。
   違反する幅 8, 12, 21, 24                17通りのうち13通りは問題なし
     幅12 -> ここは危険だ、先に進むな / ら回復アイテムを持ってい / ったほうがいい。
             3行目が っ で始まる
     幅13 -> 問題なし

C  32文字  所持金が足りません。装備を売却してから、もう一度お試しください。
   違反する幅 9 と 19 だけ                 17通りのうち15通りは問題なし

そもそも描画系は日本語のどこで改行できるのか

日本語にはスペースがないから折り返しには手がかりがない、と考えたくなります。実際は逆です。Unicode の改行モデルでは大半の漢字やかなの間で改行が許されるので、日本語の文字列はほぼすべての文字境界に改行候補を持っています。禁則処理は、そこから禁止されたものを引き算する工程です。妙な位置で改行する描画系は、候補を見つけられなかったのではなく除外できなかったのです。

単語の境界は改行候補とは別の話です。日本語の単語分割は辞書に基づき、ブラウザはその機能をそのまま公開しています。上書き確認のメッセージに単語粒度の分割をかけた結果を節末に示します。

その出力では辞書が「とっさ」をひとまとまりに保ち、内部の「っ」は境界に現れません。この位置だけで改行すれば、任意の文字間で改行するよりはるかに自然になります。ただしこれは単語分割であって改行処理ではありません。正しさは禁則の規則から来ます。

Intl.Segmenter("ja", { granularity: "word" })
  入力   セーブデータを上書きしますか?        (15文字)
  出力   セーブ / データ / を / 上書き / し / ます / か / ?

  入力   「セーブデータを上書きしますか?」とっさに判断できない。
  出力   「 / セーブ / データ / を / 上書き / し / ます / か / ? / 」
         / とっさ / に / 判断 / でき / ない / 。

英語 "Do you want to overwrite the save data?"
  空白で分割  Do / you / want / to / overwrite / the / save / data?

実装ごとに変えるべき設定

Unity の TextMesh Pro では、禁則の文字集合はコードに埋め込まれた挙動ではなくプロジェクトのアセットです。設定に改行用の文字ファイルが2つあり、一方は行末に残してはいけない文字、他方は行頭に来てはいけない文字を列挙します。ファイルが不完全だと「一部だけ崩れる」現象の原因になります。項目名やファイルの場所は変わるので、編集の前に公式の TextMesh Pro ドキュメントの設定に関する項で現行の名称を確認してください。

Unreal Engine は国際化対応のために ICU(International Components for Unicode)を同梱しており、テキストレイアウトは ICU の改行処理で改行位置を決めます。改行がおかしいなら、改行アルゴリズムより先に、テキストのカルチャやロケールの設定と独自実装のレイアウトを疑ってください。自分のバージョンの設定は、公式の Unreal のローカライズに関するドキュメントで確認します。

RPG Maker MV・MZ は状況が違います。既定のメッセージウィンドウは自動で折り返さないので、改行は人が入れるものであり、設定するアルゴリズムが存在しません。禁則処理は執筆側と翻訳取り込み側の責任です。改行位置を作り直さずに訳文へ差し替えるスクリプトは、機械的に違反を生み出します。プラグインで自動折り返しを足しているなら禁則の挙動はそのプラグインのものなので、前提にせず確認してください。周辺の制約はRPG Makerのローカライズガイドにまとめてあります。

CSS では line-break プロパティで、初期値は auto です。仕様は auto を、UA が制限の集合を決めてよく、短い行にはより緩い規則を使ってもよいものと定めています。明示的な値のうち、小書きのかなと長音記号の前での改行を許すのは loose だけで、normal も strict も禁止します。閉じ括弧と句読点は、これらの値のどれでも行頭に来られません。改行を無条件に許すのは別値の anywhere だけです。つまり行頭の「」」は描画やマークアップの不具合、行頭の「っ」は loose の指定か、auto が緩い規則を選んだかで、狭いボックスほど起きます。auto に任せず明示的に固定してください。strict が最も厳格で、この集合については normal も同等です。定義は CSS Text 仕様の改行の節にあります。

.jp-text {
  /* 初期値の auto に任せず固定する。auto は短い行で緩めてよい */
  line-break: strict;      /* 小書きのかな・ー の前で改行しない(normal も同じ) */
  word-break: normal;      /* keep-all は約物のない連続があふれる */
  overflow-wrap: normal;   /* break-word はここでは効かない */
  white-space: normal;     /* pre / pre-wrap は原文の改行を残す */
}

原文に埋め込まれた改行を測る

3つ目の原因は、原語のレイアウトで見た目を整えるために文字列に打ち込まれた改行文字です。翻訳を経てもそのまま残ります。改行文字が絶対位置である一方、折り返しはボックスに対する相対的な処理なので、何かが変わった瞬間に両者はずれます。節末の測定がそれです。

手動改行を入れた前提の幅では両者は同一で、改行文字は無害に見えます。1文字狭くなった途端、その改行はもう「入る限界」ではなくなり、残りが単独の行にこぼれます。幅9では1文字、幅8では2文字の取り残された行です。狭いほうのどの幅でも行数が3から4に増えており、これが、手動改行が高さ固定のウィンドウからテキストをあふれさせる仕組みです。

したがって、翻訳対象の文字列からは改行文字を外し、禁則処理を有効にしたレイアウトに折り返させます。2行構成のタイトルなど本当に改行が必要なら、言語ごとのデータとして持ちます。制約が「収まること」なら、翻訳者には文字数の上限として渡し、改行はレイアウトに任せてください。その長さの変動は英語から日本語へのゲームローカライズで扱っています。

テキスト  危険だ、ここから先は一人で行くしかないぞ。   (21文字)
          手動改行を「ここから先は」の後(10文字目)に置く

ボックス幅 10   手動改行なし: 3行      手動改行あり: 3行
ボックス幅  9   手動改行なし: 3行      手動改行あり: 4行
                  危険だ、ここから先 / は / 一人で行くしかない / ぞ。
ボックス幅  8   手動改行なし: 3行      手動改行あり: 4行
                  危険だ、ここから / 先は / 一人で行くしかな / いぞ。
ボックス幅  7   手動改行なし: 3行      手動改行あり: 4行
                  危険だ、ここか / ら先は / 一人で行くしか / ないぞ。

実行できる検査と、実機でしか分からないこと

検出そのものは、折り返し済みの行に対する正規表現1本です。行頭禁則の集合から文字クラスを作り、描画された各行を判定します。重要な制約は、必要なのが折り返し済みの行であって元の文字列ではない点です。この検査は折り返し後の行を書き出すデバッグ表示か、スクリーンショットの比較に置きます。

静的な検査が届かないのは実行時の差し込みです。プレイヤー名やアイテム名、数値がテンプレートに入ると文字列の長さが変わり、それ以降の改行位置がすべて変わります。3文字の名前では問題のないメッセージが、6文字の名前では違反することがあります。テンプレートは差し込み例ではなく、想定される最長の値で試してください。この作業の位置づけはローカライズQA(LQA)とは何かで説明しています。

  • 折り返し済みの行に検査をかける。最も狭いボックス、次に出荷UIが使う各幅で
  • 3つの集合を別々に確認する。句読点、閉じ括弧類、小書きのかなと長音記号 — 前2つが通っても3つ目については何も言えない
  • 開き括弧が行末に残っていないかも見る。同じ不具合の裏返しになる
  • 想定される最長のプレイヤー名・アイテム名・数値を差し込んでから判定する
  • フォント・サイズ・ボックス幅を変えたら、出荷するフォントで対象プラットフォームごとに再検査する

関連記事