diff --git a/docs/superpowers/plans/2026-07-11-browser-extension-localization.md b/docs/superpowers/plans/2026-07-11-browser-extension-localization.md new file mode 100644 index 0000000..e4668cf --- /dev/null +++ b/docs/superpowers/plans/2026-07-11-browser-extension-localization.md @@ -0,0 +1,328 @@ +# Browser Extension Localization Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add persisted `System / English / Russian` language selection to the browser extension, localizing popup chrome and context menus in Chromium and Firefox. + +**Architecture:** A framework-independent `shared/i18n.js` resolves preferences and loads extension-owned JSON catalogs. Popup and background both consume it; background owns persistence and context-menu recreation, while popup performs immediate DOM translation and sends settings updates. + +**Tech Stack:** WebExtension APIs, plain ES5-compatible JavaScript, JSON catalogs, Node `assert`/`vm` tests, existing Node build script. + +## Global Constraints + +- Store only `system`, `en`, or `ru`; missing/invalid values normalize to `system`. +- `ru` and `ru-*` browser UI locales resolve to Russian; all other system locales resolve to English. +- Keep the extension language independent from desktop settings. +- Add no runtime dependency and no manifest-localization scope. +- Never translate receiver URLs, tokens, file names, user content, or raw receiver error text. +- Preserve receiver URL/token values when language changes. +- Both browser distributions must contain `i18n.js`, `locales/en.json`, and `locales/ru.json`. + +--- + +### Task 1: Shared Locale Runtime and Catalog Contract + +**Files:** + +- Create: `shared/i18n.js` +- Create: `shared/locales/en.json` +- Create: `shared/locales/ru.json` +- Create: `scripts/test-i18n.js` +- Modify: `package.json` + +**Interfaces:** + +- Produces: `globalThis.VerstakBrowserI18n.normalizePreference(value)`. +- Produces: `globalThis.VerstakBrowserI18n.resolveLocale(preference, systemLocale)`. +- Produces: `globalThis.VerstakBrowserI18n.loadCatalogs(loadJSON)` returning `Promise<{en,ru}>`. +- Produces: `globalThis.VerstakBrowserI18n.createTranslator(catalogs, locale)` returning `t(key, params, fallback)`. + +- [ ] **Step 1: Write the failing runtime/catalog test** + +Create `scripts/test-i18n.js` that loads `shared/i18n.js` in `vm`, reads both JSON catalogs, and asserts: + +```js +assert.strictEqual(i18n.normalizePreference('de'), 'system'); +assert.strictEqual(i18n.resolveLocale('system', 'ru-RU'), 'ru'); +assert.strictEqual(i18n.resolveLocale('system', 'uk-UA'), 'en'); +assert.strictEqual(i18n.resolveLocale('en', 'ru-RU'), 'en'); +assert.deepStrictEqual(Object.keys(ru).sort(), Object.keys(en).sort()); +assert.ok(Object.values(en).every((value) => typeof value === 'string')); +assert.ok(Object.values(ru).every((value) => typeof value === 'string')); +assert.strictEqual(i18n.createTranslator({ en, ru }, 'ru')('status.queued'), 'В очереди до запуска Верстака'); +assert.strictEqual(i18n.createTranslator({ en, ru }, 'en')('error.value', { error: 'offline' }), 'Error: offline'); +``` + +- [ ] **Step 2: Run the test and confirm RED** + +Run: `node scripts/test-i18n.js` + +Expected: FAIL because `shared/i18n.js` and catalogs do not exist. + +- [ ] **Step 3: Implement the minimal shared runtime** + +Implement an IIFE exporting the four functions. `loadCatalogs(loadJSON)` must call `loadJSON('en')` and `loadJSON('ru')`; translation lookup order is selected catalog, English catalog, explicit fallback, key. Interpolation replaces `{name}` only when `params` owns that property. + +Catalogs must define identical keys for: + +```text +popup.subtitle +label.receiver +label.pending +label.url +label.file +label.receiverUrl +label.pairingToken +label.language +action.sendPage +action.sendFile +action.retryPending +action.save +hint.contextMenu +receiver.online +receiver.offline +receiver.unknown +language.system +language.en +language.ru +status.sending +status.queued +status.done +status.readingFile +status.saved +error.chooseFile +error.fileTooLarge +error.invalidReceiverUrl +error.value +context.sendPage +context.sendSelection +context.sendLink +``` + +- [ ] **Step 4: Add the test to `npm test` and verify GREEN** + +Set: + +```json +"test": "node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js" +``` + +Run: `node scripts/test-i18n.js` + +Expected: `browser extension localization runtime tests passed`. + +- [ ] **Step 5: Commit the shared contract** + +```bash +git add shared/i18n.js shared/locales scripts/test-i18n.js package.json +git commit -m "feat: add browser extension localization runtime" +``` + +--- + +### Task 2: Persisted Language and Localized Context Menus + +**Files:** + +- Modify: `shared/background.js` +- Modify: `chromium/manifest.json` +- Modify: `firefox/manifest.json` +- Create: `scripts/test-background-i18n.js` + +**Interfaces:** + +- Consumes: `VerstakBrowserI18n.loadCatalogs`, `normalizePreference`, `resolveLocale`, `createTranslator`. +- Produces: persisted `settings.language` and context-menu recreation after `saveSettings`. + +- [ ] **Step 1: Write the failing background test** + +Load `protocol.js`, `queue.js`, `i18n.js`, and `background.js` into `vm` with fake storage/context-menu APIs. Seed `settings.language = 'ru'`, trigger `runtime.onInstalled`, and assert: + +```js +assert.deepStrictEqual(menuTitles, [ + 'Отправить страницу в Верстак', + 'Отправить выделение в Верстак', + 'Отправить ссылку в Верстак', +]); +``` + +Send a `saveSettings` message with `{ receiverUrl, receiverToken, language: 'en' }`, then assert stored receiver fields are unchanged and menu titles become English. + +- [ ] **Step 2: Run the test and confirm RED** + +Run: `node scripts/test-background-i18n.js` + +Expected: FAIL because background defaults contain no language and menu titles are literal English. + +- [ ] **Step 3: Implement background localization** + +Add `language: 'system'` to `DEFAULT_SETTINGS`. Normalize it in `getSettings`/`saveSettings`. Load catalogs with: + +```js +function loadLocaleCatalogs() { + return localeCatalogs || i18n.loadCatalogs(function (locale) { + return fetch(ext.runtime.getURL('locales/' + locale + '.json')).then(function (response) { + if (!response.ok) throw new Error('catalog load failed: ' + locale); + return response.json(); + }); + }); +} +``` + +Make `setupContextMenus()` resolve settings plus catalogs, choose +`ext.i18n.getUILanguage()` with English fallback, and create the three menu +titles via `t('context.sendPage')`, `t('context.sendSelection')`, and +`t('context.sendLink')`. Call it after language settings are saved. + +- [ ] **Step 4: Load `i18n.js` before background code in both manifests** + +- Chromium service worker remains bundled by the build concatenation order. +- Firefox `background.scripts` becomes `protocol.js`, `api.js`, `queue.js`, `i18n.js`, `background.js`. + +Also append `node scripts/test-background-i18n.js` to the `test` script in +`package.json` now that the test file exists. + +- [ ] **Step 5: Verify and commit** + +Run: `node scripts/test-background-i18n.js` + +Expected: `browser extension background localization tests passed`. + +```bash +git add shared/background.js chromium/manifest.json firefox/manifest.json scripts/test-background-i18n.js +git commit -m "feat: localize browser extension context menus" +``` + +--- + +### Task 3: Popup Language Selector and Live Translation + +**Files:** + +- Modify: `shared/popup/popup.html` +- Modify: `shared/popup/popup.js` +- Modify: `shared/popup/popup.css` +- Modify: `scripts/test-popup-settings.js` + +**Interfaces:** + +- Consumes: state `settings.language` from background and the shared translator. +- Produces: immediate popup translation plus a `saveSettings` message containing receiver URL, token, and language. + +- [ ] **Step 1: Extend the popup test and confirm RED** + +Add fake elements for every localized ID plus `language-select`, mock catalog fetches, `browser.i18n.getUILanguage()`, and `document.documentElement.lang`. Assert initial `system` with `ru-RU` renders Russian. Change select value to `en`, dispatch `change`, flush promises, then assert: + +```js +assert.strictEqual(elements['capture-page'].textContent, 'Send Page'); +assert.strictEqual(elements['receiver-state'].textContent, 'Unknown'); +assert.strictEqual(document.documentElement.lang, 'en'); +assert.strictEqual(savedSettings.language, 'en'); +assert.strictEqual(savedSettings.receiverUrl, initialState.settings.receiverUrl); +assert.strictEqual(savedSettings.receiverToken, initialState.settings.receiverToken); +``` + +- [ ] **Step 2: Add popup markup** + +Load `../i18n.js` before `popup.js`, give all static user-visible elements stable IDs, and add: + +```html + + +``` + +- [ ] **Step 3: Implement asynchronous popup initialization and live switch** + +Load both catalogs through `ext.runtime.getURL`, resolve the locale from saved +preference plus browser UI language, and apply all static text through one +`applyLocale(preference)` function. `render(state)` localizes +online/offline/unknown. Known statuses and validations use catalog keys; raw +error messages are wrapped only when needed by `error.value`. + +The select `change` handler must call `applyLocale` before awaiting background +storage, then send all three settings fields. The existing Save button also +sends the selected language. + +- [ ] **Step 4: Style the select consistently and verify GREEN** + +Share the existing input box model/colors with `select` and keep the popup +minimum width unchanged. + +Run: `node scripts/test-popup-settings.js` + +Expected: `browser extension popup localization/settings tests passed`. + +- [ ] **Step 5: Commit** + +```bash +git add shared/popup scripts/test-popup-settings.js +git commit -m "feat: add browser extension language selector" +``` + +--- + +### Task 4: Distribution Packaging, Documentation, and Full Verification + +**Files:** + +- Modify: `scripts/build-extension.js` +- Modify: `README.md` +- Test: `scripts/test-i18n.js` +- Test: `scripts/test-background-i18n.js` +- Test: `scripts/test-popup-settings.js` + +**Interfaces:** + +- Consumes: shared runtime/catalogs and updated manifests. +- Produces: complete `dist/chromium` and `dist/firefox` extension directories. + +- [ ] **Step 1: Add failing build-content assertions** + +Extend `scripts/test-i18n.js` to run after build when `dist` exists and assert +both targets contain: + +```text +i18n.js +locales/en.json +locales/ru.json +``` + +- [ ] **Step 2: Update the build** + +For Chromium, concatenate `shared/i18n.js` before `shared/background.js` and +also copy it as `dist/chromium/i18n.js` for popup use. For Firefox, copy +`shared/i18n.js` beside background scripts. Copy both JSON catalogs to each +target's `locales/` directory. + +- [ ] **Step 3: Document the selector** + +Add a README section stating that popup settings provide persisted +`System / English / Russian`, that System uses browser UI language, and that +extension/desktop preferences are independent. + +- [ ] **Step 4: Run complete verification** + +```bash +npm test +npm run build +npm test +git diff --check +``` + +Expected: + +- protocol, runtime, popup, and background tests pass; +- both browser distributions build; +- post-build content assertions pass; +- no whitespace errors. + +- [ ] **Step 5: Commit packaging and docs** + +```bash +git add scripts/build-extension.js README.md scripts/test-i18n.js +git commit -m "build: package browser extension locale catalogs" +```