落とし穴¶
このサイトは35言語に翻訳されており、そのすべてが、このドキュメントの教える ループを回して作られたものです。業界の基準からすれば小さなコーパスですが、 それでも、i18nを見た目より難しくしている罠のほとんどを踏むには十分でした。
以下の各節は、ここで実際に起きたこと、その時それがどう見えたか、そして ライブラリがあなたの代わりに検査する範囲とあなたの判断に委ねられる範囲との 境界がどこにあるか、です。
変数名の変更が文を翻訳し直しにする¶
msgidはカタログのkeyであり、補間される名前はその内側にあります。定数を1つ
モジュールスコープへ移し、Pythonのスタイルが求めるとおりに大文字化したこと —
authorからAUTHORへ — が、Copyright © 2026 {author} · MIT Licenseを、
どのカタログも見たことのないメッセージへ変えました。読者に見える変化を何も
もたらさない改名のために、この行の翻訳はすべての言語でfuzzyのサイクルを
もう一巡することになっていたはずです。
ライブラリはこれを止めません。どちらの綴りも妥当なプレースホルダー名です。 ライブラリがするのは、その名前を守る価値のあるものにすることです。補間は 素の名前でなければならないため、 カタログのkeyに入るのは翻訳者が読める単語であって、式ではありません。
逆の場合は構造上安全です。変換と書式指定はmsgidの一部ではないため、
{amount:,.2f}を{amount:,.0f}に締めてもkeyは変わらず、どこの翻訳も
無効になりません。
nplurals=2は2つの異なる文字列を意味しない¶
トルコ語、ハンガリー語、ペルシア語、ベンガル語はいずれも複数形を2つ宣言し、
その4言語すべてで、個数付きメッセージの2つの形が正当に同じ文字列になります。
数詞の後でも名詞は単数のままなので、{n} sayfaは1ページでも10ページでも
正しいのです。重複を「修正」するレビュアーは翻訳を壊します。
逆向きの間違いも同じくらい起こりやすいものです。ラトビア語の3番目の形は
ゼロ専用に存在し、スロベニア語の2番目はちょうど2件を表す双数、
ルーマニア語の最後の形は、最初の2つが持ってはならない語deを必要とします。
これらの枠を単数形と複数形で埋めれば、誰も試さない個数でだけ誤っている
カタログができあがります。
さらに悪いことに、枠の順序は意味的ではありません。ウェールズ語は5つの形を、
msgstr[0]が一般形、msgstr[1]が単数形になるように並べています。見た目どおりの
順に埋めると、個数を伴わないメッセージが必ず見に行く場所へ単数形が置かれます。
ライブラリはこれを一切引き受けません。そしてそれこそが要点です。対象言語の 複数形規則はその言語自身のカタログヘッダーにあり、和集合/積集合の規則は、 翻訳がソースより多くの形を持つことも少なく持つことも許します。ライブラリが 検査するのは、言語を知らずに検査できる唯一のこと — どの形も必要な プレースホルダーを保っているか、だけです。
2つの形が同一なのには理由がある¶
アイルランド語には複数形が5つあり、このサイトのビルドレポートではそのうち
いくつかが同じ綴りになっています。これはコピー&ペーストのミスではありません。
leathanachはlで始まり、アイルランド語の数詞が引き起こす2種類の語頭変異は
どちらもlには表記されないのです。それでも各形は実際に働いています。語幹は
leathanachとleathanaighの間で交替し、10を超える個数では単数形に戻ります。
ただ、「ページ」を意味する名詞ではその対比が表に出ないだけです。
重複した形を疑わしいとして報告する検査はすべて、正しいアイルランド語を 報告します。これをレビューできるのは、その言語を知る人間だけです。
メッセージは1つの個数としか一致できない¶
このサイトのビルドレポートは、何ページ描画したか、どれだけ時間がかかったかを
伝えます。これを「Rendered {n} pages in {seconds} seconds」と書くのは無害に
見えて、翻訳できません。gettextは1つの個数から1つの形を選び、その個数はnです。
secondsという語は、複数形の機構が決して見ることのない数と一致しなければ
ならなくなります。
解決策は、2つ目の量を語ではなく単位記号にすることです。そして単位記号自体も
ローカライズの対象です。このサイトのカタログはs、с、ث、שנ׳、mpを
持っており、フランス語、スペイン語、スウェーデン語の組版は、英語と違って記号の
前にスペースを求めます。そのどれもライブラリの管轄ではありません。しかし、
メッセージが2つの一致を必要としていると気付くことは管轄であり、そのための
唯一の道具は、メッセージを別の書き方にすることです。
英語の文の編集は外国語の文法の編集になる¶
ホームページはかつて「all ten language editions」と書いていました。数字が古く なり続けるので削ったという、英語では1語の編集が、複数の主語を単数に変えました。 スペイン語、イタリア語、ポルトガル語、ロシア語、ウクライナ語、ギリシャ語、 オランダ語、ヘブライ語はいずれも動詞を一致させ直す必要があり、いくつかは分詞も 変える必要がありました。
英語では些細に読めるソース編集も、下流では些細ではありません。pybabel updateが
することであるfuzzyマーキングは、各翻訳者にそれへ気付く機会を与える仕組みです。
目に見えない違いはあらゆるコピー&ペーストを生き延びる¶
ガイドは(nаme)を含む診断を引用しています。これは意図的なescapeです。そこで
名指しされている文字が、どんな読者にもラテン文字と見分けのつかないCyrillicのа
だからです。このサイトの翻訳者たちは、そのescapeを実際の文字へ5回、5つの
異なる言語で変換しました。そのたびに、正しく見えて誤っているページができました。
これはライブラリが実際に捕まえるもので、診断が今の形をしている理由でもあります。 文字が複数の書記体系にまたがるプレースホルダーは 2回報告されます。1回は読める形で、 1回はescapeして。escapeした形だけが両者を見分けられる綴りだからです。波括弧の 内側のno-break spaceが符号位置で印字されるのも同じ理由です。カタログのcheckerは、 そのメッセージが出荷される前に拒否します。
空でないことは翻訳済みを意味しない¶
msgidをmsgstrへコピーして足場を組んだカタログは、素朴な検査をすべて通過します。 空のものはなく、fuzzyもなく、メッセージ集合は完全に一致します。このサイトのある 言語版は、数時間その状態で出荷されました。別の言語版の8ページも同様で、そちらは 英語ソースとバイト単位で同一のコピーでした。両者のコードブロックを比較する検査は 通ってしまいます。同じファイルなのですから。
どちらも翻訳ライブラリに見えるものではありません。そしてどちらも安く検査
できますが、すべてのエントリがソースと異なることを要求する方法ではだめです。
OK、製品名、人名、頭字語、コード識別子は、いずれもそれ自身へ翻訳されます。
それを禁じる検査は、偽陽性を永遠に出し続けます。
代わりに、カタログ全体やページ全体にわたって一致率を測り、外れ値だけを 人間に回します。このサイトのテストはまさにそれをしています。各言語版の散文行を 英語ソースと比較し、一致が25%を超えたら失敗させるのです。偽物の言語版は87% でした。本物の翻訳はどれも4〜8%に収まります。これはURLや引用されたプログラム 出力のように、正当に一致してしまう行の小さな裾野です。2つの母集団は十分に 離れているので、閾値は精密である必要がありません。
翻訳されるものはカタログだけではない¶
ここで起きた2つの失敗は、gettextとは無関係でした。
見出しを翻訳すると、そこから生成されるアンカーが変わるため、その節への ページ横断リンクがすべて壊れます。しかも、その言語でだけ、静かに。このサイトは すべての見出しに英語のアンカーを固定しており、テストが期待されるアンカーの一覧を 英語ページから導出します。
そしてサイトジェネレーターは68言語のインターフェース翻訳を同梱していますが、 そこにスワヒリ語とアイルランド語は含まれません。それが無い場合、ビルドは英語へ degradeせず、テンプレートのincludeが失敗して、その言語版はそもそもビルドできません。 このリポジトリ自身のファイル2つは、その穴を埋めるために存在しています。
あなたの道具にもバグはある¶
このドキュメントが古いカタログを検出するために勧めているCIステップ
pybabel update --checkは、pgettextやnpgettextを使うプロジェクトでは
その仕事を果たせません。Babel 2.18.0では、msgctxtを持つカタログをすべて、
毎回、古いと報告します。比較はCatalog.is_identicalを通ります。これは各
メッセージを、格納に使われたキーで引きます — そしてコンテキスト付きメッセージの
そのキーは(id, context)の組であり、Catalog.getはそれを受け付けません。参照は
何も返さず、カタログ同士が等しいと判定されることは決してありません。
>>> from babel.messages.catalog import Catalog
>>> c = Catalog(locale="ja")
>>> c.add("Guide", "ガイド", context="navigation")
<Message 'Guide' (flags: [])>
>>> c.is_identical(c)
False
これはここで実際に使おうとして見つかり、上流へ報告されました。代わりに置く検査は 実運用のページにあります。
一般的な教訓は、居心地の悪いほうです。常に赤いゲートは、ゲートが無いより 悪いのです。チームがそれを切ってしまうからです。落ちることを信頼する前に、 そのCI検査が実際に通り得ることを確かめてください。
このライブラリの目的を一言で¶
このページのほとんどは、どんな道具も肩代わりできない判断です。道具にできるのは、 翻訳がそれ自身の翻訳する文の構造を変えられないと保証すること — 値を落とすことも、 でっち上げることも、書式を変えることも、あなたのオブジェクトへ手を伸ばすことも できないと保証し、しかもそれを、直さなければならない人が行動に移せる一文で 言えることです。それがこのライブラリの約束のすべてであり、このサイトの残りは、 それをどう守っているかの説明です。