feat: localize browser extension context menus
This commit is contained in:
parent
18bff9f352
commit
766458d0a0
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"permissions": ["contextMenus", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
|
||||
"background": {
|
||||
"scripts": ["protocol.js", "api.js", "queue.js", "background.js"]
|
||||
"scripts": ["protocol.js", "api.js", "queue.js", "i18n.js", "background.js"]
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"description": "Verstak browser capture extension for Chromium and Firefox",
|
||||
"scripts": {
|
||||
"build": "node scripts/build-extension.js",
|
||||
"test": "node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js",
|
||||
"test": "node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js && node scripts/test-background-i18n.js",
|
||||
"sign:firefox": "./scripts/sign-firefox-xpi.sh",
|
||||
"release:firefox": "./scripts/release-firefox-xpi.sh"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env node
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const catalogs = {
|
||||
en: JSON.parse(fs.readFileSync(path.join(root, 'shared', 'locales', 'en.json'), 'utf8')),
|
||||
ru: JSON.parse(fs.readFileSync(path.join(root, 'shared', 'locales', 'ru.json'), 'utf8')),
|
||||
};
|
||||
const state = {
|
||||
settings: {
|
||||
receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures',
|
||||
receiverToken: 'paired-token',
|
||||
language: 'ru',
|
||||
},
|
||||
};
|
||||
let menuTitles = [];
|
||||
let installedListener;
|
||||
let messageListener;
|
||||
|
||||
const browser = {
|
||||
storage: {
|
||||
local: {
|
||||
get(key) {
|
||||
if (typeof key === 'string') return Promise.resolve({ [key]: state[key] });
|
||||
return Promise.resolve({ ...state });
|
||||
},
|
||||
set(patch) {
|
||||
Object.assign(state, patch || {});
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
},
|
||||
contextMenus: {
|
||||
removeAll(callback) {
|
||||
menuTitles = [];
|
||||
if (callback) callback();
|
||||
},
|
||||
create(item) {
|
||||
menuTitles.push(item.title);
|
||||
},
|
||||
onClicked: { addListener() {} },
|
||||
},
|
||||
runtime: {
|
||||
getURL(relativePath) { return `extension://${relativePath}`; },
|
||||
onInstalled: { addListener(listener) { installedListener = listener; } },
|
||||
onMessage: { addListener(listener) { messageListener = listener; } },
|
||||
},
|
||||
i18n: { getUILanguage() { return 'en-US'; } },
|
||||
tabs: { query() { return Promise.resolve([]); } },
|
||||
};
|
||||
|
||||
function fetchCatalog(url) {
|
||||
const locale = /\/ru\.json$/.test(url) ? 'ru' : 'en';
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(catalogs[locale]) });
|
||||
}
|
||||
|
||||
const context = vm.createContext({
|
||||
browser,
|
||||
console,
|
||||
Date,
|
||||
Math,
|
||||
Promise,
|
||||
URL,
|
||||
fetch: fetchCatalog,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
});
|
||||
context.globalThis = context;
|
||||
for (const file of ['protocol.js', 'api.js', 'queue.js', 'i18n.js', 'background.js']) {
|
||||
vm.runInContext(fs.readFileSync(path.join(root, 'shared', file), 'utf8'), context, { filename: file });
|
||||
}
|
||||
|
||||
async function flush() {
|
||||
for (let i = 0; i < 8; i += 1) await Promise.resolve();
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
return new Promise((resolve) => {
|
||||
messageListener(message, {}, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
installedListener();
|
||||
await flush();
|
||||
assert.deepStrictEqual(menuTitles, [
|
||||
'Отправить страницу в Верстак',
|
||||
'Отправить выделение в Верстак',
|
||||
'Отправить ссылку в Верстак',
|
||||
]);
|
||||
|
||||
const nextState = await sendMessage({
|
||||
type: 'verstak.capture',
|
||||
action: 'saveSettings',
|
||||
settings: {
|
||||
receiverUrl: state.settings.receiverUrl,
|
||||
receiverToken: state.settings.receiverToken,
|
||||
language: 'en',
|
||||
},
|
||||
});
|
||||
await flush();
|
||||
|
||||
assert.strictEqual(state.settings.receiverUrl, 'http://127.0.0.1:47731/api/browser-inbox/v1/captures');
|
||||
assert.strictEqual(state.settings.receiverToken, 'paired-token');
|
||||
assert.strictEqual(state.settings.language, 'en');
|
||||
assert.strictEqual(nextState.settings.language, 'en');
|
||||
assert.deepStrictEqual(menuTitles, [
|
||||
'Send page to Verstak',
|
||||
'Send selection to Verstak',
|
||||
'Send link to Verstak',
|
||||
]);
|
||||
console.log('browser extension background localization tests passed');
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -4,20 +4,49 @@
|
|||
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
||||
var protocol = globalThis.VerstakBrowser;
|
||||
var queue = new protocol.CaptureQueue(protocol.browserStorageAdapter(ext));
|
||||
var i18n = globalThis.VerstakBrowserI18n;
|
||||
var DEFAULT_SETTINGS = {
|
||||
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
|
||||
receiverToken: ''
|
||||
receiverToken: '',
|
||||
language: 'system'
|
||||
};
|
||||
var STATUS_KEY = 'verstak.status';
|
||||
var localeCatalogs = null;
|
||||
|
||||
function getSettings() {
|
||||
return ext.storage.local.get('settings').then(function (result) {
|
||||
return Object.assign({}, DEFAULT_SETTINGS, result && result.settings || {});
|
||||
var settings = Object.assign({}, DEFAULT_SETTINGS, result && result.settings || {});
|
||||
settings.language = i18n.normalizePreference(settings.language);
|
||||
return settings;
|
||||
});
|
||||
}
|
||||
|
||||
function saveSettings(settings) {
|
||||
return ext.storage.local.set({ settings: Object.assign({}, DEFAULT_SETTINGS, settings || {}) });
|
||||
settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
||||
settings.language = i18n.normalizePreference(settings.language);
|
||||
return ext.storage.local.set({ settings: settings });
|
||||
}
|
||||
|
||||
function loadLocaleCatalogs() {
|
||||
if (localeCatalogs) return localeCatalogs;
|
||||
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();
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.warn('[verstak] localization catalogs unavailable:', error);
|
||||
return { en: {}, ru: {} };
|
||||
});
|
||||
return localeCatalogs;
|
||||
}
|
||||
|
||||
function browserLocale() {
|
||||
try {
|
||||
return ext.i18n && ext.i18n.getUILanguage ? ext.i18n.getUILanguage() : 'en';
|
||||
} catch (_) {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(patch) {
|
||||
|
|
@ -105,11 +134,18 @@
|
|||
}
|
||||
|
||||
function setupContextMenus() {
|
||||
if (!ext.contextMenus) return;
|
||||
if (!ext.contextMenus) return Promise.resolve();
|
||||
return Promise.all([getSettings(), loadLocaleCatalogs()]).then(function (results) {
|
||||
var locale = i18n.resolveLocale(results[0].language, browserLocale());
|
||||
var t = i18n.createTranslator(results[1], locale);
|
||||
return new Promise(function (resolve) {
|
||||
ext.contextMenus.removeAll(function () {
|
||||
ext.contextMenus.create({ id: 'verstak-capture-page', title: 'Send page to Verstak', contexts: ['page'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-selection', title: 'Send selection to Verstak', contexts: ['selection'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-link', title: 'Send link to Verstak', contexts: ['link'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-page', title: t('context.sendPage', null, 'Send page to Verstak'), contexts: ['page'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-selection', title: t('context.sendSelection', null, 'Send selection to Verstak'), contexts: ['selection'] });
|
||||
ext.contextMenus.create({ id: 'verstak-capture-link', title: t('context.sendLink', null, 'Send link to Verstak'), contexts: ['link'] });
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +164,8 @@
|
|||
if (message.action === 'getState') return getState();
|
||||
if (message.action === 'saveSettings') {
|
||||
return saveSettings(message.settings).then(function () {
|
||||
return setupContextMenus();
|
||||
}).then(function () {
|
||||
return setStatus({ receiverReachable: null, lastResult: 'settings-saved', lastError: '' });
|
||||
}).then(getState);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue