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で使っています。
- Product: https://spex.kikuta.dev/ja
- Chrome Web Store: https://chromewebstore.google.com/detail/gahnolkboklnjhnaahjdkingoejbepcc