動作原理¶
このページの内容は、ライブラリを使うために必須ではありません。使い方は チュートリアルとガイドで説明しています。このページは その代わりに、ライブラリを第一原理から組み立て直します。t-stringが実際には 何であるか、msgidがそこからどう導かれるか、翻訳を有効にする条件は何か、そして 実装がその検査すべてのコストを1マイクロ秒の何分の一かに抑える方法です。 興味がある場合、コントリビュートしたい場合、 この規約を自分で実装する予定がある場合にお読みください。
t-stringの正体¶
f-stringはstrを生成し、しかも即座に生成します。関数が受け取る時点で値は
補間済みで、文は固まっています。t-string(PEP 750)は同じ構文と、式の同じ
即時評価を持ちますが、生成する型が異なります。
>>> name = "Ada"
>>> f"Hello {name}!"
'Hello Ada!'
>>> t"Hello {name}!"
Template(strings=('Hello ', '!'), interpolations=(Interpolation('Ada', 'name', None, ''),))
このTemplateオブジェクトは、カタログpipelineが必要とする部品を、分離した
まま保持しています。
>>> template = t"Total: {amount:,.2f}"
>>> template.strings
('Total: ', '')
>>> template.interpolations[0].expression
'amount'
>>> template.interpolations[0].value
1234.5
>>> template.interpolations[0].format_spec
',.2f'
strings— 補間の周囲にあるリテラルテキスト。ソース順です。- 各補間について:ソーステキストとしての式(
'amount')、評価済みの 値(1234.5)、そして変換指定(!r)とフォーマット指定 (,.2f)。適用されるのではなく、別々に運ばれます。
このライブラリが行うすべては、この構造の規律ある消費です。i18nが必要とする 唯一の分離 — 静的テキストと値の分離 — は言語がすでに行っています。だから このライブラリは、あなたのソースコードを解析することも、文の中のどこに値が あるかを推測することもありません。残るのは3つの決定です。この構造がどう カタログkeyになるか、そのkeyの翻訳は何を言ってよいか、そして両者がどう 再び合成されるか。
templateからmsgidへ¶
msgid — カタログの索引となるkey — は、templateの静的な部分だけから導出
されます。stringsとinterpolationsをソース順に辿り、各リテラル部分の
波括弧をエスケープし({は{{になります)、各補間について{name}トークンを
1つ出力します。nameは、前後の空白を取り除いた式のテキストです。
t"Total: {amount:,.2f}"からは次のようになります。
strings ('Total: ', '')
interpolations expression 'amount' conversion None format_spec ',.2f'
msgid 'Total: {amount}'
このルールの各部分には理由があります。
- 式は単純な名前でなければなりません —
str.isidentifier()が真で、 Pythonキーワードではないこと。t"Hello {user.name}"は呼び出し箇所で拒否 されます。msgidはkeyです。どの実行でも、どの抽出でも同一でなければ ならず、翻訳者が読むものでもあります。だからプレースホルダーは安定した 意味のある語であるべきで、カタログを式言語へ誘うコード片であっては なりません。 - 変換指定とフォーマット指定は決してmsgidに入りません。 翻訳者が
:,.2fを読まされるべきではなく、どの翻訳もそれを変更できるべきでは ありません。知っておく価値のある系があります。コード内の:,.2fを:,.0fへ絞ってもmsgidは1つも変わらないため、どの言語の翻訳も無効に なりません。カタログのkeyが追跡するのは文が何を言っているかであり、 値がどう書式化されるかではありません。 - 繰り返される名前は、その書式も厳密に繰り返さなければなりません。
t"{x:.2f} vs {x:.3f}"は拒否されます。両方の出現が同じ{x}トークンへ 畳まれるため、レンダリングがどちらの書式を使うべきかを、msgidがもはや 語れなくなるからです。 - 空のmsgidは決して検索されません。 gettextが空のmsgidをカタログ自身の
メタデータヘッダーに予約しているためです。
t""は、カタログに触れずに""としてレンダリングされます。
このページが省略したエッジケースを含む完全なルールは、 SPEC §2に あります。
翻訳が言ってよいこと¶
カタログから返ってきたpatternは、string.Formatter — str.formatが使うのと
同じparser — で解析されます。この文法は発明ではなく、意図的な借用です。
このライブラリが受け付けるpatternは、より広いecosystemがすでに理解している
patternです。その上で、2つの検査が適用されます。
形: すべてのfieldは素の{name}でなければなりません。変換指定や
フォーマット指定 — 明示的に空の{name:}を含む — は拒否され、位置field
({0}、{})や空白で囲まれた名前({ name })も拒否されます。最後のものは
見かけ以上に重要です。str.formatもGNUのmsgfmtも{ name }を拒否するため、
ここで受け付ければ、chain内の他のどのツールも検証できないカタログが生まれて
しまいます。
名前: patternのプレースホルダー集合が、ソースのそれと比較されます。 単数のメッセージでは、ソースのすべての名前が必須であり、それ以外は何も 許可されません。複数形のメッセージでは、2つの枝がマージされます。
- 許可 = 両方の枝の名前の和集合
- 必須 = 両方の枝の名前の積集合
したがってt"One file" / t"{n} files"に対しては、名前nはどちらの形の
翻訳でも許可されますが、どちらでも必須ではありません。この非対称性こそが、
対象言語の複数形体系がソース言語と異なることを許します。日本語は両方の枝を、
おそらく{n}を使う1つの形で翻訳します。英語より形の多い言語は、英語に{n}が
ない形でも{n}を必要とするかもしれません。
これは仮定の話ではありません。このサイト自身のchromeカタログは複数形メッセージ
Built {n} localized page / Built {n} localized pages — 英語の2つの枝 —
を持っており、サイトの各言語版はこの1つのメッセージを、1つの形から6つの形まで
さまざまに翻訳しています。
そのうち9つの言語版を、形の順に
| カタログ | 形の数 | 翻訳(形の順) |
|---|---|---|
| 日本語 | 1 | ローカライズ済みページを{n}件ビルドしました |
| トルコ語 | 2 | {n} yerelleştirilmiş sayfa oluşturuldu — 2回、まったく同一:トルコ語の名詞は数詞の後でも単数のままです |
| イタリア語 | 2 | Generata {n} pagina localizzata · Generate {n} pagine localizzate — 分詞が性と数に一致します |
| ラトビア語 | 3 | Izveidota {n} lokalizēta lapa · Izveidotas {n} lokalizētas lapas · Izveidots {n} lokalizētu lapu — 3番目の形はゼロ専用です |
| ロシア語 | 3 | Собрана {n} локализованная страница · Собраны {n} локализованные страницы · Собрано {n} локализованных страниц |
| ポーランド語 | 3 | Zbudowano {n} zlokalizowaną stronę · Zbudowano {n} zlokalizowane strony · Zbudowano {n} zlokalizowanych stron |
| スロベニア語 | 4 | Zgrajena {n} lokalizirana stran · Zgrajeni {n} lokalizirani strani · Zgrajene {n} lokalizirane strani · Zgrajenih {n} lokaliziranih strani — 2番目はちょうど2件を表す双数です |
| アイルランド語 | 5 | Tógadh {n} leathanach logánaithe · Tógadh {n} leathanaigh logánaithe — 1件、2件、3〜6件、7〜10件、それ以外。語幹は交替しますが、leathanachはlで始まり、アイルランド語のどの変異もlには表記されないため、いくつかの形が一致します |
| アラビア語 | 6 | 中でも、ちょうど1件を表すتم إنشاء صفحة مترجمة واحدة ({n})と、少数を表すتم إنشاء {n} صفحات مترجمة |
各行は、このリポジトリのi18n/*/LC_MESSAGES/site.poに実在するエントリであり、
リリースのたびに多言語ビルドで描画されます。さらに、テストが
この表をそれらのカタログに固定しているため、両者が乖離することはありません。
この範囲の内側では、並べ替えと繰り返しは意図的に制約されていません。どちらも
実在する言語で文法的に必要であり、出現回数を制限すれば、安全上の利点なしに
正しい翻訳を拒否することになります。翻訳は依然として何も評価できません。
評価経路が存在しないからです。プレースホルダーは、templateの計算済みの値から
名前で検索されるだけで、eval、getattr、str.format自体に渡されることは
決してありません。
レンダリング¶
検証済みのpatternをレンダリングするのは、そのchunkを辿る歩みです。各リテラル
部分を出力し、各プレースホルダーについては、補間が捕捉した値にソース側の
変換指定とフォーマット指定を適用します — format(convert(value,
conversion), format_spec)。その際、2つの保証が守られます。
- それぞれの値は、1回のレンダリングにつき最大1回だけ書式化されます。
翻訳がプレースホルダーを繰り返す場合でも同様です。繰り返しが変えるのは
結果が挿入される回数であり、あなたの
__format__が実行される回数では ありません。 - 複数形では、プレースホルダーは自分を定義した枝を読みます。 両方の枝に
存在する名前は、ソース言語が選択する枝(
n == 1ならsingular、それ 以外はplural)が捕捉した値を読みます。枝に固有の名前は常に自分の枝を 読みます。対象言語の複数形規則によって別の形で使えるようになった場合でも 同じです。
レンダリング時に検証が失敗した場合、応答はpatternの提供者によって分かれます。
カタログから出てきたpatternは劣化します。警告を1件ログに残してソース
テキストをレンダリングし、壊れたカタログがアプリケーションを停止させないと
いうgettextの契約を守ります(ガイドが両方のモードを示します)。
呼び出し側が直接渡したpattern — CompiledTemplate.render — は常に例外を
送出します。劣化の戻り先となるソーステキストが存在しないからです。lenientさ
はカタログ検索のためにあり、引数のためにはありません。
診断も設計の一部¶
プレースホルダーのエラーは、多くの場合programmerではなく翻訳者の前に現れ、
しかも問題が目に見えないファイルの中でのことがよくあります。エディタでまさに
その文字が見えている人に{name} is missingと言っても行き止まりです。そこで
メッセージは3つのルールで組み立てられます。
- 目に見えない文字を含む名前 — 入力メソッドが生成したno-break spaceや
zero-width space — は、その文字をcode pointへ置き換えて、その位置のまま
表示します:
{<U+00A0>name}。読者に必要なのはどこかを見ることです。 - 文字が文字体系を混在させる名前、つまりhomoglyphの場合は、2回表示
します。1回は読める形で、1回はescapeして。Cyrillicの
аを含む{nаme}は 印刷上{name}と区別できず、escapeした形(nаme)だけが両者を見分けられる 綴りだからです。 - それ以外はすべて書かれたとおりに表示します。
{名前}や{café}は 普通の名前です。escapeすれば、読者は何が意図されたのかを見つけられなく なります。
同じ原理で、存在するように見える「欠落した」プレースホルダーは、その不在の
理由まで説明されます。East Asianの入力メソッドによる全角の波括弧、エスケープ
の往復で生じた{{name}}の二重化、波括弧の外にある名前。
翻訳者向けに書かれた
失敗メッセージの一覧表が、これらの
メッセージをそのまま示しています。
ホットパス¶
上記のすべては、アプリケーションがレンダリングする翻訳済み文字列1つごとに 起こります。そのため実装は1つの考えを軸に組まれています。検証は決して スキップされない。だからキャッシュすべきは検証である。
flowchart LR
T["t-string"] --> S{"構造は<br>既出か?"}
S -- "ヒット" --> G["キャッシュ済みmsgidで<br>カタログを検索"]
S -- "ミス" --> D["msgidを導出し<br>planをキャッシュ"] --> G
G --> V{"patternは<br>既出か?"}
V -- "ヒット" --> R["レンダリング"]
V -- "ミス" --> C["検証し<br>結果をキャッシュ"] --> R
キャッシュは3つ、段階ごとに1つです。
- 呼び出し箇所の構造ごとのplan。 templateの
stringstuple — interpreterがすでに構築したオブジェクト — がキャッシュのkeyなので、検索は 何も割り当てません。ヒット時にも、各補間の式・変換指定・フォーマット指定は 記録済みのものと比較されます。リテラルテキストは同じでも書式が異なる2つの 呼び出し箇所(t"{x:.2f}"とt"{x:.3f}")が衝突してはならず、その比較が、 interpreterが無償で渡してくれるkeyを使う対価です。 - patternごとの判定。 カタログがあるpatternで初めて答えたとき、それは 解析・検証され、結果 — コンパイル済みのrender plan、または無効という 記録 — がplan上に保持されます。以降、そのメッセージのレンダリングは dictionary検索1回でそこへ到達します。無効なpatternも記憶されます。壊れた カタログエントリがレンダリングごとではなく一度だけ警告する理由です。
- 複数形ペアごとのマージ済みplan。 和集合・積集合を保持し、枝の計算が 呼び出しごとではなくメッセージごとに1回で済むようにします。
すべてのキャッシュは有界で、補間された値を保持するものはありません。静的な
構造とpatternのテキストだけです。
benchmarks/runtime.py
によるCPython 3.14.6・macOS 26・arm64ノートPCでの計測結果は、1 fieldの
メッセージで、t-string自体の構築を含めておよそ0.4 µs。何も検査しない素の
gettext(...).format(...)の約2.7倍です。これは1台のマシンの数字にすぎません。
スクリプトはヘッダーに自身のインタプリタとplatformを出力するので、どの比率も
自分のものとして扱う前に、実際にデプロイするハードウェアで走らせてください。
core.py
冒頭の解説に、この結果の背後にある個々の計測が記録されています。
再実装する¶
上記のどれもこの実装に固有のものではありません。規約は仕様v1として文書化されており、 その機械可読な適合性テストスイートを使えば、抽出器、 IDEのplugin、別言語での実装が、このページで説明したすべての規則に対して自身を 検証できます。この実装自身も自分のテストでこのスイートを実行しており、それが このページ、仕様、コードが静かに乖離しないことを保っています。