ファイル形式・標準規格Read this article in English

XLIFF とは — 1.2 と 2.0 の違いと、ツールとの往復で壊れる箇所

XLIFF は、翻訳対象のテキストをシステム間で受け渡すための XML 形式です。文字列を書き出し、翻訳者や翻訳支援ツールに訳文を入れてもらい、自分のファイルに統合して戻します。OASIS の標準規格で、触る前に知るべき実務的な事実は「実際に渡されるのは 2008年の 1.2 か 2014年の 2.0 のどちらか」ということです。この2つは差が大きく、一方を読めるツールが他方も読めるとは限りません。2.x 系の後続版として 2.1 と 2.2 も OASIS 標準になっていますが、ツールの対応は実質 1.2 と 2.0 に集まっています。

以降で、同じ2つの文字列を両バージョンで書いた完全なファイル、事故になるバージョン差、往復で失われる箇所、渡すときに文書で伝えることを順に見ます。掲載した XML はコマンドラインの XML パーサで整形式を確認し、壊れた例も実際にエラーになることを確認しています。

最小の完全な XLIFF を 1.2 と 2.0 で並べる

断片では自分のインポータを検証できません。最初に壊れるのはルート要素・名前空間・言語属性で、どれも断片には写らないからです。

  • ルート要素はどちらも xliff。ただし名前空間の末尾が document:1.2 か document:2.0 かで分かれ、version 属性は名前空間と一致していなければならない。片方だけを見るインポータが「非対応ファイル」エラーの定番。
  • 翻訳単位は 1.2 では body の中の trans-unit、2.0 では file 直下の unit。テキストはさらに1段下の segment にある。どちらも source と target をペアで持ち、source はツール側の約束として書き換えない。突き合わせるのはこのペア。
  • trans-unit と unit の id は、ファイルと自分のデータをつなぐキー。行番号ではなく自分の文字列キーを入れる。行番号のままだと、原文に1行挿入しただけで以降の全部が番号ずれを起こす。
dialogue.1.2.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
  <file original="dialogue.json" source-language="en" target-language="ja" datatype="plaintext">
    <body>
      <trans-unit id="tutorial.open_inventory">
        <source>Press <ph id="1">%s</ph> to open your inventory.</source>
        <target state="translated"><ph id="1">%s</ph> を押してインベントリを開きます。</target>
        <note from="developer">%s is a key name. Do not translate or reorder.</note>
      </trans-unit>
      <trans-unit id="combat.weapon_broke">
        <source>Your <g id="2" ctype="bold">Iron Sword</g> broke.</source>
        <target state="needs-review-translation"><g id="2" ctype="bold">鉄の剣</g>が壊れました。</target>
      </trans-unit>
    </body>
  </file>
</xliff>

dialogue.2.0.xlf
<?xml version="1.0" encoding="UTF-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ja">
  <file id="dialogue" original="dialogue.json">
    <unit id="tutorial.open_inventory">
      <notes>
        <note category="developer">The placeholder is a key name. Do not translate or reorder.</note>
      </notes>
      <originalData>
        <data id="d1">%s</data>
      </originalData>
      <segment state="translated">
        <source>Press <ph id="1" dataRef="d1"/> to open your inventory.</source>
        <target><ph id="1" dataRef="d1"/> を押してインベントリを開きます。</target>
      </segment>
    </unit>
    <unit id="combat.weapon_broke">
      <segment state="initial">
        <source>Your <pc id="2">Iron Sword</pc> broke.</source>
        <target><pc id="2">鉄の剣</pc>が壊れました。</target>
      </segment>
    </unit>
  </file>
</xliff>

1.2 と 2.0 の違い — インポータが読み間違える箇所

1.2 から 2.0 への変更は文法の小手直しではありません。セグメントのモデル自体が違い、雑な変換処理が情報を落とす場所と重なります。

上の 2.0 の source のテキスト内容を XML ツールに尋ねると、プレースホルダを含まない文字列が返ります。1.2 では文字列の一部として返ります。だからセグメントを平文として扱うもの(スペルチェック、機械翻訳、置換)は 1.2 のプレースホルダを壊せますが、2.0 には届きません。1.2 と 2.0 のどちらでも出せるなら、2.0 を選ぶ一番強い理由がこれです。

  • state の置き場所。1.2 では target 要素、2.0 では segment にある。target を見に行くインポータは、2.0 のファイルから state を1つも読めない。1.2 仕様の target 要素の節と 2.0 仕様の segment の節で位置を確認する。
  • state の値の体系。2.0 は initial・translated・reviewed・final の4値と、ツール独自の細分化用の subState。1.2 は進行度と作業理由が混ざった長いリストで、new・needs-translation・needs-review-translation・translated・signed-off・final などがあり、trans-unit に別途 approved 属性を持つ。1.2 の複数の値は 2.0 に対応物がなく initial に潰れるので、対応づける前に 1.2 仕様の state 属性の定義を読む。
  • 元のコード文字列の置き場所。1.2 では元のマークアップを ph の内容として、翻訳対象テキストの中に置ける。2.0 では unit の originalData に外出しされ、インライン要素は dataRef で参照する。
  • インラインタグ。1.2 は範囲に g、単独プレースホルダに x、インラインコードに ph、2つに分かれたコードに bpt と ept。2.0 は範囲に pc、両端が離れる範囲や他と交差する範囲に sc と ec、単独プレースホルダに ph。
  • 注記。1.2 は note 要素を直接付け、書いた人を from 属性で示す。2.0 は notes 要素で包み、category で分類する。

往復で壊れる箇所 — パーサが拾うものと、黙って通るもの

整形式の確認は安い関門です。戻ってきたファイルに XML パーサをかけるだけで、ある種類の破損はインポータに届く前に止まります。意図的に壊した3つのファイルは、別々のエラーになりました。

3番目は誤解しやすい点です。Shift_JIS と宣言して Shift_JIS で保存したファイルは問題なく解析でき、正しく読み戻せました。壊れるのは宣言とバイト列の不一致で、日本語環境向けのエディタが、開くつもりのなかったファイルを上書き保存したときに起きます。

より厄介なのは、パーサが黙って通す失敗です。

  • 原文を CDATA で囲む。整形式なのに、インラインタグの意味を無効化する。包まれた文字列のマークアップは普通の文字になるので、翻訳者は生のタグを見ることになり、打ち間違えられる。パーサで確かめると、CDATA の source はマークアップをテキスト内容の一部として返した。
  • 知らないツール独自の名前空間。ツールは内部のセグメントキーなどを自分の名前空間で付けてくる。仕様に従う読み取り側は無視してよい。つまり自分の往復処理が、相手が次工程で必要とするデータを黙って落とせる。自分が入れたのではない拡張の上に何も組まない。
  • id が安定しない。実行ごとに番号を振り直す書き出し処理を、形式自体は禁止していない。id が変わると前回版との突き合わせは全滅し、戻ってきたファイルは別の行に統合される。原文が変わっていないのに id が変わったら落ちるテストを足す。
  • 空白。XML の文字データは書かれたまま保持されるので、source の先頭の空白や末尾の改行は文字列の一部。正規化するツールもしないツールもある。空白に意味があるなら xml:space を preserve にし、そのうえで値が往復を生き延びたかを確認する。
  • インラインタグの順序と個数。入れ子が成立していれば、範囲をプレースホルダの向こうへ動かしても、ペアの片方の id を落としても整形式として通る。target と source のインライン id の集合を segment ごとに突き合わせる検査は、自分のインポータに書くもの。
$ xmllint --noout broken-amp.xlf
broken-amp.xlf:6: parser error : xmlParseEntityRef: no name
        <source>Save & Quit</source>
                      ^

$ xmllint --noout broken-halfpair.xlf
broken-halfpair.xlf:7: parser error : Opening and ending tag mismatch: pc line 7 and target
        <target><pc id="2">鉄の剣が壊れました。</target>
  ... four further mismatch errors, ending in "Premature end of data in tag xliff line 2"

$ xmllint --noout broken-cp932.xlf
broken-cp932.xlf:13: parser error : Input is not proper UTF-8, indicate encoding !
Bytes: 0x82 0xF0 0x89 0x9F
  ... followed by the offending line and a caret marker

Unity・Unreal・スプレッドシートとの関係

エンジン側の事情は統一されていません。Unity の Localization パッケージは String Table に対して XLIFF のインポートとエクスポートができます。パッケージマニュアルの Localization Tables のインポート・エクスポートの項に CSV と並んで記載があり、扱えるバージョンもそこにあります。自分のリリースのマニュアルで確認してください。Unreal Engine は交換形式に XLIFF を使わず、Localization Dashboard 経由で Portable Object 形式を読み書きします。XLIFF しか送ってこない相手だと自分の側に変換工程が増えます。何が残り何が落ちるかは PO ファイル形式の解説 にあります。

スプレッドシート運用が常に間違いなのではありません。両端を自分が握り、文字列数が少なく、編集する人がそれで困らないなら勝ちます。負けるのは、インラインマークアップ・セグメントごとの進行状態とコンテキストを運ぶ必要が出た瞬間で、セルにはその置き場所がありません。今がスプレッドシートなら、次に安いのは 更新に耐える Unity の CSV 運用 です。XLIFF が配管の手間に見合うのは、外部の相手が翻訳を担う段から。なお XLIFF が運ぶのは案件そのもので、案件をまたいで再利用する翻訳メモリや用語集は別形式です(翻訳メモリの TMX と用語集の TBX)。

翻訳を依頼するときに文書で伝えること

XLIFF の事故の多くは形式ではなく取り決めの問題です。次の答えと、受け取らない条件(宣言と実体が食い違う文字コード、id が変わったファイル)を、ファイルと一緒に文書として渡してください。

残りの受け渡し物(ビルド、スクリーンショット、用語集)は、どの形式を選んでも同じで、一覧は ローカライズキットの作り方 にあります。XLIFF が運ぶのは文字列だけです。

  • 自分が出すバージョンと名前空間、そして戻しとして受け取るバージョン。2.0 か 1.2 かを明示する。相手のツールが黙って変換することがある。締切前に知りたい事実。
  • 言語タグを正しい形で。置き場所は 1.2 が file 要素の source-language と target-language、2.0 がルート要素の srcLang と trgLang で、srcLang は必須。区切りはアンダースコアではなくハイフンで、大小文字は正規化される。ja-JP はタグだが ja_JP はタグではなく、pt-br は pt-BR、zh-hans は zh-Hans を意味する。副タグをどこまで書くかは BCP 47 言語タグの読み方 にある。
  • どんなプレースホルダがあり、実行時に何になるか。書式を全部列挙し、並べ替えてよいかも書く。
  • 文字数制限とコンテキストは、その unit の note に書く。どの画面か、誰のセリフか、ボタンのラベルか本文か、を1文添えるだけで曖昧さの大半が消える。

関連記事