11 KiB
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, orru; missing/invalid values normalize tosystem. ruandru-*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, andlocales/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)returningPromise<{en,ru}>. -
Produces:
globalThis.VerstakBrowserI18n.createTranslator(catalogs, locale)returningt(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:
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:
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 testand verify GREEN
Set:
"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
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.languageand context-menu recreation aftersaveSettings. -
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:
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:
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.jsbefore background code in both manifests -
Chromium service worker remains bundled by the build concatenation order.
-
Firefox
background.scriptsbecomesprotocol.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.
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.languagefrom background and the shared translator. -
Produces: immediate popup translation plus a
saveSettingsmessage 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:
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:
<label id="language-label" for="language-select">Language</label>
<select id="language-select">
<option value="system">System</option>
<option value="en">English</option>
<option value="ru">Русский</option>
</select>
- 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
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/chromiumanddist/firefoxextension 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:
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
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
git add scripts/build-extension.js README.md scripts/test-i18n.js
git commit -m "build: package browser extension locale catalogs"