Skip to content

Latest commit

 

History

History
263 lines (196 loc) · 10.5 KB

i18n.md

File metadata and controls

263 lines (196 loc) · 10.5 KB

Language Pack — UI を 6 言語で出す仕組み

locales/<言語ID>.json を 1 枚置くだけで UI がその言語になる。同梱は 英語 → 日本語 → 简体中文 → 한국어 → Português (Brasil) → Español の 6 つで、 この並びがそのまま選択画面の並び順になる。

zaivern-code/
├── locales/
│   ├── en.json      ← 基準言語 (訳が欠けたらここへ落ちる)
│   ├── ja.json      ← 原文言語 (ID → 日本語。既存コードとの橋渡しに使う)
│   ├── zh-CN.json
│   ├── ko.json
│   ├── pt-BR.json
│   └── es.json

中身は安定 ID をキーにした平の JSON。

// locales/en.json
{
  "app.new_session": "New Session",
  "app.settings": "Settings",
  "agent.send": "Send",
  "agent.stop": "Stop",
  "agent.approve": "Approve",
  "agent.approve_all": "Approve All",
  "agent.broadcast": "Broadcast to All Agents"
}

Rust 側は ID を渡すだけ。

ui.button(tr("agent.approve"));

1. 使う側 — 言語を変える

入口 場所
🌐 メニュー ツールバー右側 (🎨 の隣)。狭いときは ⋯ の中
メニューバー 表示 → 表示言語
コマンドパレット ⌘⇧P → 「表示言語」
設定画面 外観 → 表示言語
プラグイン画面 english-mode / japanese-mode / chinese-mode / korean-mode / portuguese-mode / spanish-mode の有効/無効
設定ファイル ~/.zaivern/config.toml の ui_language
CLI zai lang (一覧) / zai lang set <id> (切替) / zai lang install <id> (導入)

再起動は要らない。 選んだ次のフレームから全部のラベルが入れ替わる。

言語パックは同時に 1 つだけ有効になる。 どの入口から変えても ui_language とプラグインの有効/無効が必ず揃うので、「プラグイン画面では english-mode が有効なのに画面は韓国語」という食い違いは起きない。 1 つも有効にしていない状態が「自動」。

既定は「自動」

ui_language = "auto" は OS の言語に従う。

OS 見るもの
Linux / macOS (端末から起動) LC_ALL → LC_MESSAGES → LANG → LANGUAGE
macOS (Finder / Dock から起動) 上が空なので defaults read -g AppleLocale を裏のスレッドで引く
Windows GetUserDefaultLocaleName

判定できた言語の辞書が無ければ英語、OS から何も取れなければ日本語 (このアプリの原文言語) にする。「入れただけで画面が別の言語になる」を 起こさないための保険で、macOS で defaults の結果が遅れて届いたときだけ、 そのフレームで 1 度だけ引き直す (毎フレームの費用は atomic 1 回)。

zh_TW は zh-CN へ、pt_PT は pt-BR へ寄る (同梱がそれしか無いあいだ)。


2. 増やす側 — zai lang で入れる / 作る

GitHub から入れる

Zaivern Language Pack
        ↓  (GitHub リポジトリの locales/)
  zai lang install
        ↓
  ~/.zaivern/locales/
zai lang list --remote        # 配布元にある言語を見る (✓ = もう入っている)
zai lang install zh-CN        # ~/.zaivern/locales/zh-CN.json へ入れる
zai lang install ko
zai lang set zh-CN            # config.toml の ui_language を書き換える
zai lang list                 # いまの言語と、選べる言語
zai lang remove ko            # 入れたファイルを消す (同梱は消せない)

配布元は既定でこのビルドの配布元 (install.sh から読む)。 決め打ちしていないので、誰でも自分のリポジトリで配れる:

zai lang install fr --from someone/zaivern-lang-fr
ZAIVERN_LANG_REPO=my-org/lang-packs zai lang list --remote

install は置く前に検査する (fail-closed):

断る条件 理由
JSON として読めない / 最上位がオブジェクトでない 壊れた辞書を置かない
空 入れても何も起きない
{name} が基準と食い違う 実行時に穴が開く ({path} が消えた案内など)
既にある (かつ --force 無し) 手で直した訳を黙って消さない

書き込みは一時ファイル + rename。途中で切れても半端なファイルが残らない。 訳が足りないぶんは英語へ落ちるので、部分訳のパックでも入れて使える (何件足りないかは入れたときに出る)。

自分で作る

アプリを再ビルドしなくてよい。 置くだけで一覧に並ぶ。

zai lang export fr            # ~/.zaivern/locales/fr.json に雛形を書き出す
$EDITOR ~/.zaivern/locales/fr.json
zai lang check fr             # 過不足・プレースホルダを検査 (合わなければ終了コード 1)
zai lang set fr

GUI からも同じことができる (⌘⇧P → 「表示言語: 翻訳の雛形を書き出す」)。

置き場は 2 か所。先に来たほうが勝つ。

  1. ~/.zaivern/locales/<id>.json
  2. ~/.config/zaivern/locales/<id>.json (OS が返す設定ディレクトリ。 Windows なら %APPDATA%\zaivern\locales)

同梱と同じ ID を書けば同梱の訳を上書きできる。1 行だけ直したいときは、 その 1 行だけの JSON を置けばよい (残りは同梱のまま)。

プラグインとして配る

plugin.toml に [language] を書けば .zvplug 1 個で配れる。

[plugin]
name = "french-mode"
version = "1.0.0"
default_enabled = false      # 入れただけで言語が変わるのは驚きなので false

[language]
id = "fr"
name = "Français"
locales = "locales"          # このディレクトリ直下に fr.json を置く

locales を省略すると「同梱言語を選ぶだけ」のプラグインになる (japanese-mode などがこの形。同じ訳を 2 か所に持たせない)。


3. 直す側 — 訳漏れを見つける

# 画面に出るのに辞書へ載っていない tr("…") を探す (見つかったら終了コード 1)
zai lang missing

#   翻訳へ回せる形 (シャード JSON) で書き出す
zai lang missing src /tmp/delta.json

# 訳を 6 枚へ取り込む (プレースホルダが合わない訳は採らない)
zai lang apply /tmp/delta-translated.json

# 6 枚が噛み合っているか (番人テストと同じ判定)
zai lang check ja

label() -> &'static str の表のように tr(&x) 経由で届く文字列は静的には 辿れない。そちらは実行時に集める。

ZAIVERN_I18N_TRACE=1 zai .
# 画面をひととおり触ってから
# ⌘⇧P → 「表示言語: 訳が無い文字列を書き出す」

集めるのは環境変数を付けたときだけ。既定では tr() は OnceLock を 1 回読むだけで、訳漏れの記録に費用を払わない (設計原則 3)。


4. 仕組み — なぜ 3000 箇所を書き換えずに済んだか

このリポジトリの tr() 呼び出しは、もともと日本語の原文そのものを 渡していた (3300 箇所)。

ui.button(tr("承認"));      // 既存
ui.button(tr("agent.approve"));  // これから書くコード

locales/ja.json は ID → 日本語原文 なので、その逆引き (日本語原文 → ID) を 1 枚作れば、呼び出し側を 1 行も書き換えずに 全部の文字列が 6 言語になる。tr() の引き先はこの順:

  1. ID → 訳 (新しいコード)
  2. 日本語原文 → 訳 (既存の 3300 箇所。原文言語のときは作らない)
  3. 言語プラグインの旧形式辞書 ([language] dict の TOML。後方互換)
  4. 渡された文字列そのもの

4 があるので、訳が 1 つ欠けても UI は必ず何かを表示する。壊れない。

新しいコードは ID を使うこと。原文を直しても訳が生き残る。

番人

テスト 守るもの
locale::tests::全同梱言語のキー集合が一致する 6 枚の鍵が完全に同じ
locale::tests::訳は空でなくプレースホルダも一致する {name} が全言語で同じ集合
locale::tests::ソースのtrリテラルはすべて同梱辞書から引ける 画面に日本語が残らない
locale::tests::利用者が示した正準idは必ずある 説明に出す ID の名前が変わらない
locale::tests::同梱の全言語は基準と過不足なく噛み合う zai lang check と同じ判定
locale::tests::同じ原文を持つidは訳も一致する 逆引きの勝者が変わっても表示が変わらない

落ちたら直すのは locales/*.json であってテストではない。 tr("新しい文言") を足したら、zai lang missing で拾って 6 言語ぶん訳を入れる (zai lang missing src /tmp/d.json → 訳す → zai lang apply /tmp/d.json)。

道具は Rust の zai に入っている (zai i18n … は zai lang … の別名)。走査もエスケープ解除も src/locale.rs の scan_source_literals / unescape_rust 1 か所だけで、 番人テストと zai i18n が同じ実装を使う (2 つ持つとずれて、番人が 通っているのに画面に日本語が残る、という嘘が出る)。


5. スマホ (リモート) 側

PC で選んだ言語がそのままスマホの画面にも効く。

配信直前に i18n::export_prefix("remote.") で remote.* だけを取り出し、 window.ZVI18N = {…} としてページへ差し込む。JS 側は T('remote.save', '💾 保存') と data-i18n 属性で差し替える。 第 2 引数は日本語の原文なので、辞書が届かなくても画面は壊れない。

<html lang> も選択中の言語 ID になる (音声入力の認識言語もこれに追従する)。 JSON は </script> と U+2028/2029 を退避してから埋めるので、訳文に何が 入っていてもページの構文は壊れない。


6. 置き場所と優先順位のまとめ

選択:  ui_language ("auto" | 言語ID)  ← 🌐 / 設定 / プラグイン / config.toml が全部ここへ集まる
辞書:  同梱 locales/<id>.json
       ← ~/.config/zaivern/locales/<id>.json で上書き
       ← ~/.zaivern/locales/<id>.json で上書き (いちばん強い)
       ← プラグインの [language] locales でも足せる
落ち先: <選択した言語> → (地域変種があれば同語の同梱変種) → en → 渡された文字列そのもの