kikuta@dev:~$cat ./notes/spex-wxt-i18n-runtime-override.md
·11 min·kikuta

Chrome拡張の言語と、操作対象アプリの言語を分ける

Spex自身のlocaleとGoogle Sheetsのhost localeを独立状態として扱い、runtimeの言語切替を設計する。

Google Sheets向けChrome拡張 Spex を日本語・英語対応しました。

拡張機能自身のi18nだけを考えるなら、やることは比較的分かりやすいです。

  • Popup
  • Settings
  • Tutorial
  • Toast
  • Shortcut Reference

の文字列を翻訳catalogへ移し、現在localeに応じて表示する。

SpexではWXTを使っているので、@wxt-dev/i18n を基盤にしました。

ただし、実装後に別の問題が出ました。

Spexを日本語表示にすると、英語版Google Sheets上の一部操作が壊れる。

一見すると変なバグです。

Spexの表示言語を変えただけなのに、なぜGoogle Sheetsのメニュー探索が変わるのでしょうか。

原因は、二つの「言語」を一つとして扱っていたことでした。

Chrome拡張には、自分のlocaleとは別にhost applicationのlocaleがある

SpexはGoogle Sheetsの上で動きます。

そのため、画面上には二種類のtextがあります。

Spex-owned text

Spex自身が表示するものです。

設定
チュートリアル
実行しました
この操作は利用できません

これはSpex localeで翻訳すべきです。

Google Sheets-owned text

操作対象のWebアプリが表示するものです。

挿入
行
上に 1 行挿入

英語UIなら、

Insert
Rows
Insert 1 row above

です。

これはSpexの表示localeとは関係ありません。

ユーザーは普通に、

Spex: 日本語
Google Sheets: English

という組み合わせを使えます。

逆もあります。

Spex: English
Google Sheets: 日本語

最初の実装では、この境界が十分に分かれていませんでした。

t("insertRowAbove") でhost menuを探し始めると壊れる

i18n対応を進めると、コード上に残っている日本語literalを消したくなります。

例えば、

findMenuItem("上に 1 行挿入")

を見ると、

findMenuItem(t("command.insertRowAbove"))

へ置き換えたくなります。

しかし、これは正しくありません。

t() が返すのはSpexの表示言語です。

Google Sheetsが英語表示なら、探すべきhost textは Insert 1 row above です。

つまり、

Spex locale
    !=
Google Sheets host locale

です。

ここを同じtranslation catalogへ入れると、UIの翻訳は正しくてもintegrationが壊れます。

host textを「翻訳文」ではなく「integration data」として分離した

最終的には、Google Sheetsのmenu labelをSpex i18nから切り離しました。

概念的には次のように扱います。

Spex UI catalog
  en.json
  ja.json

Google Sheets host integration labels
  semantic token
    -> EN host variants
    -> JA host variants

例えば insert.rowAbove というsemantic tokenに対して、

EN: Insert 1 row above
JA: 上に 1 行挿入

のようなhost-side候補を持ちます。

ここで重要なのは、

現在のSpex localeはどちらか

を見て候補を選ばないことです。

表示されているhost DOMに対して、既知のhost-localized variantを解決します。

access key表記も混ざっていた

Google Sheetsのmenu textは、単純な翻訳文字列だけではありません。

日本語UIでは、例えば次のような表記がありました。

行(R)►
上に 1 行挿入(R)
固定(R)►
1 行(O)

英語でもaccess key annotationが付く場合があります。

そのためhost text matcherでは、

固定(R)►

をsemantic比較用に、

固定

へnormalizeします。

一方で、

1 行(O)

1 は意味のある数字なので消してはいけません。

単純な正規表現で括弧を全部落とすのではなく、host UIのaccess-key annotationだけを除去する必要がありました。

sibling commandを取り違えない

locale-awareにすると、今度はmatchingを緩くしすぎる危険があります。

例えば、

Insert 1 row above
Insert 1 row below

は非常に似ています。

日本語でも、

上に 1 行挿入
下に 1 行挿入

です。

「rowを含むmenu item」のような雑なfallbackは危険です。

そのため、host matcherはsemantic tokenごとに、

  • exact
  • prefix
  • contains

のどの比較が許されるかを分けています。

さらにテストで、

rowAbove matcherがrowBelowへ一致しない
columnLeft matcherがcolumnRightへ一致しない
ascending matcherがdescendingへ一致しない

ことを固定します。

外部Webアプリを操作する拡張では、「見つからない」より「似た別操作を実行する」方が危険だからです。

2×2のlocale matrixでE2Eを作った

このバグは、日本語だけ・英語だけのテストでは見つかりません。

必要なのは、Spex localeとhost localeを別軸にすることです。

最低限、次の組み合わせがあります。

Spex EN / Sheets EN
Spex EN / Sheets JA
Spex JA / Sheets EN
Spex JA / Sheets JA

実際に追加したE2Eでは、特にcross-localeを確認しています。

host: English Sheets
Spex: 日本語
    ↓
英語host menuを解決
    ↓
Spex Toastは日本語

逆も確認します。

host: 日本語 Sheets
Spex: English
    ↓
日本語host menuを解決
    ↓
Spex ToastはEnglish

これが通ることで、

integration languageとpresentation languageを混ぜていない

ことをかなり強く確認できます。

WXT i18nは悪くなかった

この話を書くと、@wxt-dev/i18n の問題のように見えるかもしれません。

そうではありません。

WXT側のi18nは、拡張機能自身のlocalizationとして機能しています。

Spexではさらに、ユーザーが拡張機能内で

System
English
日本語

を手動選択できるようにしたかったため、runtime override layerも追加しています。

ただ、今回の本質的なバグはそこではありません。

Chrome拡張のlocaleと、操作対象Webアプリのlocaleは別domainだった。

これが本質です。

表示用identityもhost textへ依存させない

同じ理由で、Command identity自体も翻訳文字列にはしません。

例えば、

InsertRowAbove
ToggleBold
FreezeFirstRow

のようなlanguage-neutralなCommandIdを正本にします。

表示時だけ、

Insert row above
上に1行挿入

へ変換します。

そしてGoogle Sheets menuを探すときは、別のhost integration mappingを使います。

整理すると、3層あります。

Semantic identity
    ↓
Spex presentation label

Semantic identity
    ↓
Google Sheets host locator data

この二つは兄弟であって、片方から片方を生成しません。

一般化すると

これはGoogle Sheets固有ではないと思います。

例えば、

  • GmailへUIを足すextension
  • Jiraを操作するbrowser automation
  • Salesforceへ補助UIを足すtool
  • VS Code extensionが外部CLI outputを扱うケース

でも、

自分のアプリのlocale
外部システムのlocale

は別です。

自分の翻訳catalogを、そのままintegration locatorへ使うと、cross-locale環境で壊れます。

Spexでは日本語対応を入れたことで、その境界が表面化しました。

Spex

この設計はGoogle Sheets向けChrome拡張Spexで使っています。