Claude Media
ClaudeでXLIFFを翻訳する — タグとIDを保ったまま戻す手順

ClaudeでXLIFFを翻訳する — タグとIDを保ったまま戻す手順

CATツールが書き出したXLIFF 2.1のtargetだけをClaude Codeで埋め、ph・pc・IDを壊さずに戻す手順です。CLAUDE.mdの規約と検証スクリプトまで載せます。

XLIFFの翻訳をClaudeに任せるときの鉄則は、触るのは <target> の中身だけにすることです。<source>、<unit> や <segment> の id、インラインタグの id はそのまま残します。そうすれば、CATツールに取り込み直す段階で壊れる要素がなくなります。

この記事では、XLIFF 2.1の仕様に沿って「何を変えてよく、何を変えてはいけないか」を整理し、Claude Codeに渡す規約と、戻す前に走らせる検証スクリプトまでを一続きで示します。対象は、CATツールが書き出したXLIFFを機械翻訳や下訳の段階でClaudeに通したい翻訳担当者・ローカライズ担当のエンジニアです。

ClaudeがXLIFFで触ってよい場所はtargetだけ

XLIFF 2.1では、翻訳対象の文を <segment> の中の <source> が持ち、訳文は兄弟の <target> に入ります。<target> は省略可能で、空でも構いません。つまりClaudeの仕事は「<target> を作る、または中身を書き換える」ことに絞れます。

最小の構造は次のとおりです。

<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0"
       version="2.1" srcLang="en" trgLang="ja">
  <file id="f1">
    <unit id="u1">
      <segment>
        <source>Open the settings page.</source>
        <target>設定ページを開きます。</target>
      </segment>
    </unit>
  </file>
</xliff>

押さえたい決まりは3つあります。

  • <xliff> の trgLang は、<segment> か <ignorable> の子に <target> がある文書では必須です。srcLang と version は常に必須です
  • <target> の xml:lang を書くなら、trgLang と同じ値にします。<source> の xml:lang も srcLang と同じ値です
  • <unit> の id は必須で、同じ <file> の中で重複させられません

ほかに、<segment> と <segment> の間にある空白やコードは <ignorable> に入ります。<ignorable> も <source> と任意の <target> を持ちますが、翻訳対象の文ではないため、基本は触らない扱いにします。

インラインタグとIDはなぜ壊れやすいか

文中の <ph>(単独コード)、<pc>(開始と終了が対になったコード)といった要素が、原文の書式やプレースホルダーを表します。ここが翻訳中に最も壊れやすい部分です。日本語に訳すと語順が変わるため、タグを「それらしい位置」に移そうとして、IDを書き換えたり、閉じ忘れたりします。

たとえば %d を含む文は、仕様の例では次のように書かれます。

<unit id="1">
  <originalData>
    <data id="d1">%d</data>
  </originalData>
  <segment>
    <source>Number of entries: <ph id="1" dataRef="d1" /></source>
  </segment>
</unit>

<ph> は常に空要素で、実際の %d という文字列は <originalData> の <data> に保存されています。dataRef がその data の id を指す仕組みです。ですから訳文側で %d と直接書いてはいけません。<ph id="1" dataRef="d1"/> をそのまま置きます。

IDの規則は、検証の設計に直結します。

要素IDの一意性の範囲
<unit>IDの一意性の範囲同じ <file> の中
<data>IDの一意性の範囲同じ <unit> の中
<segment>、<ph>、<pc>、<sc>、<ec>、<mrk>、<sm>IDの一意性の範囲同じ <unit> の中

ただし例外があります。<target> の中のインライン要素は、対応する <source> のインライン要素と同じIDを重複して使うことが必須です(対応する要素が存在する場合)。これが「sourceと同じIDをtargetにも置く」という作法の根拠です。Claudeに渡す指示は、この1行に集約できます。

消してよいタグと消してはいけないタグ

訳文では、日本語の自然さのためにタグを落としたくなる場面があります。仕様はこれを canDelete 属性で制御します。既定値は yes で、no を指定したコードは削除できません。canCopy(複製可否)と canReorder(並べ替え可否)も同様に、既定は yes です。

targetを新しく作る場合のルールは、仕様の「既存のtargetがない場合」の節にあります。

  • 削除できないインラインコードは、すべてtargetに入れる
  • 並べ替えできないコードの順序は保つ
  • 削除できるコードは、targetに置いても置かなくても構わない

すでにtargetが入っている場合は、より厳しくなります。削除できないコードは、sourceにあるかどうかに関わらず残します。並べ替え不可のコードを新しく足すことはできません。

ここから分かる運用の線引きは次のとおりです。

  • 既定の ph・pc は、訳文で落としても仕様違反とは限らない。ただし多くの場合は落とすと書式が崩れるので、検証は警告として扱う
  • canDelete="no" のコードが欠けたら、エラーとして止める
  • sourceにないIDのコードをtargetに足したら、エラーとして止める

なお、<pc> を <sc>/<ec> のペアに変換するなど、仕様が許す編集もあります。しかしClaudeに任せる作業としては、タグの種類を変える自由を与えないほうが検証が単純になります。規約には「タグの種類を変更しない」と書いておきます。

CLAUDE.mdにXLIFFのルールを置く

ここからがClaude側の具体策です。Claude Codeは、プロジェクトのルートに置いた CLAUDE.md を毎セッションの開始時に読みます。コーディング規約などを書いておく場所ですが、翻訳作業のルールを置いても同じように働きます。

作業フォルダーに次のような CLAUDE.md を置きます。ここでの文面は、仕様の節を踏まえて筆者が組み立てた例です。

# XLIFF翻訳ルール
 
- 対象は XLIFF 2.1。編集するのは <target> の内容だけ
- <source>、<originalData>、<unit>/<segment> の id は絶対に変更しない
- <ph>、<pc>、<sc>、<ec>、<mrk> は source と同じ id・同じ属性で target に置く
- <data> に入っている文字列(%d、{name} など)は、訳文に直接書かない
- canDelete="no" のタグは必ず target に残す
- translate="no" の unit は target を作らない
- 訳せない場合は <target> を作らず、unit の id を報告する
- 訳し終えたら python3 check_xliff.py 原本.xlf 出力.xlf を実行し、
  ERROR が 0 になるまで直す
- 言語は srcLang=en、trgLang=ja。用語は glossary.csv を優先する

最後から2行目が肝です。検証コマンドを規約に書き、通るまで直させると、タグの取りこぼしをClaude自身が検出して修正する反復になります。人間が差分を目で追う量が減ります。

用語集の使い方は、英文プレスリリースを用語集で翻訳する手順で扱っています。XLIFFでも考え方は同じで、glossary.csv を作業フォルダーに置いて規約から参照させます。

検証スクリプトで戻す前にタグとIDを照合する

原本と出力の2つのXLIFFを比べて、source の改変、IDの過不足、canDelete="no" の欠落を検出するスクリプトです。標準ライブラリだけで動きます。

check_xliff.py の全文
import sys
import xml.etree.ElementTree as ET
 
NS = {"x": "urn:oasis:names:tc:xliff:document:2.0"}
INLINE = {"ph", "pc", "sc", "ec", "mrk", "sm", "em"}
 
 
def local(tag):
    return tag.rsplit("}", 1)[-1]
 
 
def codes(node):
    return {(local(e.tag), e.get("id") or e.get("startRef")):
            e.get("canDelete", "yes")
            for e in node.iter() if local(e.tag) in INLINE}
 
 
def dump(el):
    el.tail = None
    return ET.tostring(el)
 
 
def segments(path):
    root = ET.parse(path).getroot()
    out = {}
    for unit in root.iter("{%s}unit" % NS["x"]):
        for i, seg in enumerate(unit.findall("x:segment", NS)):
            out[(unit.get("id"), i)] = seg
    return root, out
 
 
orig_root, orig = segments(sys.argv[1])
new_root, new = segments(sys.argv[2])
errors, warnings = [], []
 
if set(orig) != set(new):
    errors.append("unit/segment の構成が変わっています: %s"
                  % sorted(set(orig) ^ set(new)))
for key in sorted(set(orig) & set(new)):
    o, n = orig[key], new[key]
    if dump(o.find("x:source", NS)) != dump(n.find("x:source", NS)):
        errors.append("%s: source が書き換わっています" % (key,))
    tgt = n.find("x:target", NS)
    if tgt is None:
        warnings.append("%s: target がありません" % (key,))
        continue
    if not "".join(tgt.itertext()).strip():
        warnings.append("%s: target が空です" % (key,))
    src_codes = codes(o.find("x:source", NS))
    tgt_codes = codes(tgt)
    for c, can_delete in src_codes.items():
        if c not in tgt_codes:
            (errors if can_delete == "no" else warnings).append(
                "%s: %s が target にありません(canDelete=%s)"
                % (key, c, can_delete))
    for c in tgt_codes:
        if c not in src_codes:
            errors.append("%s: source に無い %s が target にあります"
                          % (key, c))
 
for m in errors:
    print("ERROR", m)
for m in warnings:
    print("WARN ", m)
sys.exit(1 if errors else 0)

このスクリプトは、ID照合を (要素名, id) の組で行います。<ec> は startRef で対応する開始コードを指すため、id がなければ startRef で代用しています。名前空間つきの要素を local() で外す点も、XLIFF 2.xを扱うときの定番の処理です。

手元で、%d を表す <ph canDelete="no"> と <pc> を含む1つの <unit> のサンプルを用意して流すと、次のような出力になります。<pc> を訳文から落とした場合は警告、<ph> を落とした場合は終了コード1のエラーです。

python3 check_xliff.py in.xlf bad.xlf; echo "exit=$?"
WARN  ('u1', 0): ('pc', '2') が target にありません(canDelete=yes)
exit=0

警告だけなら終了コードは0です。pc が意図的に落とされたのか、書き忘れなのかは、警告を見て人間が判断します。

claude -pで下訳を回す

作業フォルダーに原本のXLIFF、CLAUDE.md、check_xliff.py、用語集を置いたら、Claude Codeを起動して依頼します。ドキュメントには、claude -p でCIに翻訳を組み込む例として claude -p "translate new strings into French and raise a PR for review" が載っています。同じ形で、XLIFFの翻訳を依頼できます。

claude -p "app_en-ja.xlf の target が空の segment を翻訳して、\
app_en-ja.translated.xlf として保存してください。\
CLAUDE.md の規約に従い、最後に検証スクリプトを実行してください"

大きなファイルは、いきなり全体を流さず、数十ユニットに切った試験版で出力を見てから本番に進みます。元のファイルは上書きせず、別名で保存させるのが安全です。検証スクリプトは原本と出力の2つを比べる設計なので、原本が残っている必要があります。

初回は対話セッションで、Claudeが書き換える差分を見ながら進めるのもよい方法です。ルールが足りないと分かった時点で CLAUDE.md に1行足せば、次のセッションからも効きます。

stateとtranslateをどう扱うか

<segment> の state は、initial、translated、reviewed、final の4段階で、既定は initial です。translated 以降は、segmentに <target> があることが前提です。<target> がないのに state を initial 以外にしてはいけません。さらに、state を更新するなら subState も更新するか削除することが求められます。

Claudeが下訳を入れたsegmentの state を何にするかは、ワークフロー次第です。人間のレビューを経る前提なら、translated にとどめ、reviewed や final にはしません。規約には「stateはtranslatedまで」と書いておけば、レビュー済みと誤認されることを防げます。

translate="no" は、<file>、<group>、<unit>、<mrk>、<sm> に付けられる属性で、翻訳しない範囲を示します。<unit> に付いていれば、既定でその下の <segment> もすべて翻訳対象外です。製品名やコマンドのように訳さない語を <mrk translate="no"> で包む使い方もあります。規約の「translate=noのunitはtargetを作らない」は、この指定への対応です。

空白の扱いは xml:space が決めます。<data> の中は常に preserve で、<unit> で preserve と指定されているなら、<source> と <target> のインライン内容でも空白を保持する必要があります。インデントを整形するツールで再保存すると、この空白が変わることがあります。出力は整形せず、検証後にそのままCATツールへ渡します。

よくあるつまずき

つまずきは、症状から原因をたどると速いです。

落とし穴

target にsourceと違うIDのタグを置いてしまう

訳文を整えるうちに、<ph id="1"> の隣に id="3" のタグが新しく現れることがあります。<target> のインライン要素はsource側と同じIDを使うのが仕様の決まりなので、検証スクリプトはこれをエラーとして止めます。修正は、sourceにある id に差し替えるだけです。

落とし穴

segmentを勝手に分割・結合する

文が長いからと、1つの <segment> を2つに割る編集は避けます。仕様では、分割できるのは canResegment が yes と解決されるsegmentに限られます。sourceとtargetの対応が崩れるため、仕様は、言語的な対応を保証できないならtargetを捨ててsourceを翻訳し直す選択も検討に値するとしています。規約には「segmentの数と順序を変えない」と書いておけば足ります。

もう一つ、trgLang の指定漏れがあります。原本がtargetを1つも持たない文書では、trgLang が付いていないことがあります。Claudeがtargetを足した時点で、この属性は必須になります。検証スクリプトに trgLang の有無を調べる行を足しておくと、取り込み時のエラーを避けられます。

また、取り込み側のツールが何を受け付けるかは、各CATツールの仕様に依存します。仕様は、MergerがExtractorと同等の処理能力と元形式の知識を持つことを前提としており、別のツールが書き出した文書を戻せることまでは保証していません。最初の1ファイルで、実際にインポートが通ることを確かめてから本番に進みます。

まとめ

XLIFFの翻訳をClaudeに任せる設計は、「編集してよい範囲を <target> に絞る」「規約に書く」「機械で照合する」の3点で決まります。仕様の決まりのうち、<target> のインライン要素がsourceと同じIDを使うことと、canDelete="no" のコードを残すことの2つを検証スクリプトに落とせば、取り込み直しで壊れる典型的な原因をほぼ入口で止められます。

文体や精度の確認は、別の仕組みで補います。誤訳の検出には訳し戻しによる検証ワークフローが使えます。すでにLILTなどの翻訳基盤を使っているなら、ClaudeとLILTの連携と使い分けを比べるとよいでしょう。

この記事を共有:XはてブLinkedIn