feat: add browser extension localization runtime
This commit is contained in:
parent
fb4b41d367
commit
18bff9f352
|
|
@ -5,7 +5,7 @@
|
||||||
"description": "Verstak browser capture extension for Chromium and Firefox",
|
"description": "Verstak browser capture extension for Chromium and Firefox",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build-extension.js",
|
"build": "node scripts/build-extension.js",
|
||||||
"test": "node scripts/test-protocol.js && node scripts/test-popup-settings.js",
|
"test": "node scripts/test-protocol.js && node scripts/test-i18n.js && node scripts/test-popup-settings.js",
|
||||||
"sign:firefox": "./scripts/sign-firefox-xpi.sh",
|
"sign:firefox": "./scripts/sign-firefox-xpi.sh",
|
||||||
"release:firefox": "./scripts/release-firefox-xpi.sh"
|
"release:firefox": "./scripts/release-firefox-xpi.sh"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const i18n = require(path.join(root, 'shared', 'i18n.js'));
|
||||||
|
const en = JSON.parse(fs.readFileSync(path.join(root, 'shared', 'locales', 'en.json'), 'utf8'));
|
||||||
|
const ru = JSON.parse(fs.readFileSync(path.join(root, 'shared', 'locales', 'ru.json'), 'utf8'));
|
||||||
|
|
||||||
|
assert.strictEqual(i18n.normalizePreference(undefined), 'system');
|
||||||
|
assert.strictEqual(i18n.normalizePreference('de'), 'system');
|
||||||
|
assert.strictEqual(i18n.normalizePreference('system'), 'system');
|
||||||
|
assert.strictEqual(i18n.normalizePreference('en'), 'en');
|
||||||
|
assert.strictEqual(i18n.normalizePreference('ru'), 'ru');
|
||||||
|
|
||||||
|
assert.strictEqual(i18n.resolveLocale('system', 'ru'), 'ru');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('system', 'ru-RU'), 'ru');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('system', 'RU-ru'), 'ru');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('system', 'uk-UA'), 'en');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('system', 'en-US'), 'en');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('en', 'ru-RU'), 'en');
|
||||||
|
assert.strictEqual(i18n.resolveLocale('ru', 'en-US'), 'ru');
|
||||||
|
|
||||||
|
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'));
|
||||||
|
|
||||||
|
const tEn = i18n.createTranslator({ en, ru }, 'en');
|
||||||
|
const tRu = i18n.createTranslator({ en, ru }, 'ru');
|
||||||
|
assert.strictEqual(tRu('status.queued'), 'В очереди до запуска Верстака');
|
||||||
|
assert.strictEqual(tEn('error.value', { error: 'offline' }), 'Error: offline');
|
||||||
|
assert.strictEqual(tRu('missing', null, 'Fallback'), 'Fallback');
|
||||||
|
assert.strictEqual(tRu('missing.key'), 'missing.key');
|
||||||
|
|
||||||
|
i18n.loadCatalogs((locale) => Promise.resolve(locale === 'ru' ? ru : en))
|
||||||
|
.then((catalogs) => {
|
||||||
|
assert.strictEqual(catalogs.en, en);
|
||||||
|
assert.strictEqual(catalogs.ru, ru);
|
||||||
|
console.log('browser extension localization runtime tests passed');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var api = factory();
|
||||||
|
root.VerstakBrowserI18n = api;
|
||||||
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||||
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var PREFERENCES = { system: true, en: true, ru: true };
|
||||||
|
|
||||||
|
function normalizePreference(value) {
|
||||||
|
value = String(value || '').trim().toLowerCase();
|
||||||
|
return PREFERENCES[value] ? value : 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocale(preference, systemLocale) {
|
||||||
|
preference = normalizePreference(preference);
|
||||||
|
if (preference === 'en' || preference === 'ru') return preference;
|
||||||
|
systemLocale = String(systemLocale || '').trim().toLowerCase();
|
||||||
|
return systemLocale === 'ru' || systemLocale.indexOf('ru-') === 0 ? 'ru' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolate(message, params) {
|
||||||
|
if (!params) return message;
|
||||||
|
return message.replace(/\{([A-Za-z0-9_.-]+)\}/g, function (placeholder, name) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : placeholder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCatalogs(loadJSON) {
|
||||||
|
return Promise.all([loadJSON('en'), loadJSON('ru')]).then(function (catalogs) {
|
||||||
|
return { en: catalogs[0] || {}, ru: catalogs[1] || {} };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTranslator(catalogs, locale) {
|
||||||
|
catalogs = catalogs || {};
|
||||||
|
locale = locale === 'ru' ? 'ru' : 'en';
|
||||||
|
return function translate(key, params, fallback) {
|
||||||
|
var message = catalogs[locale] && catalogs[locale][key];
|
||||||
|
if (message == null && catalogs.en) message = catalogs.en[key];
|
||||||
|
if (message == null) message = fallback == null ? key : fallback;
|
||||||
|
return interpolate(String(message), params);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
normalizePreference: normalizePreference,
|
||||||
|
resolveLocale: resolveLocale,
|
||||||
|
loadCatalogs: loadCatalogs,
|
||||||
|
createTranslator: createTranslator
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"popup.subtitle": "Browser Inbox",
|
||||||
|
"label.receiver": "Receiver",
|
||||||
|
"label.pending": "Pending",
|
||||||
|
"label.url": "URL",
|
||||||
|
"label.file": "File",
|
||||||
|
"label.receiverUrl": "Receiver URL",
|
||||||
|
"label.pairingToken": "Pairing token",
|
||||||
|
"label.language": "Language",
|
||||||
|
"action.sendPage": "Send Page",
|
||||||
|
"action.sendFile": "Send File",
|
||||||
|
"action.retryPending": "Retry Pending",
|
||||||
|
"action.save": "Save",
|
||||||
|
"hint.contextMenu": "Selection and link captures are available from the page context menu.",
|
||||||
|
"receiver.online": "Online",
|
||||||
|
"receiver.offline": "Offline",
|
||||||
|
"receiver.unknown": "Unknown",
|
||||||
|
"language.system": "System",
|
||||||
|
"language.en": "English",
|
||||||
|
"language.ru": "Russian",
|
||||||
|
"status.sending": "Sending...",
|
||||||
|
"status.queued": "Queued until Verstak is available",
|
||||||
|
"status.done": "Done",
|
||||||
|
"status.readingFile": "Reading file...",
|
||||||
|
"status.saved": "Saved",
|
||||||
|
"error.chooseFile": "Choose a file first",
|
||||||
|
"error.fileTooLarge": "File is too large for browser capture",
|
||||||
|
"error.invalidReceiverUrl": "Receiver URL must start with http:// or https://",
|
||||||
|
"error.value": "Error: {error}",
|
||||||
|
"context.sendPage": "Send page to Verstak",
|
||||||
|
"context.sendSelection": "Send selection to Verstak",
|
||||||
|
"context.sendLink": "Send link to Verstak"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"popup.subtitle": "Входящие из браузера",
|
||||||
|
"label.receiver": "Приёмник",
|
||||||
|
"label.pending": "В очереди",
|
||||||
|
"label.url": "URL",
|
||||||
|
"label.file": "Файл",
|
||||||
|
"label.receiverUrl": "URL приёмника",
|
||||||
|
"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": "URL приёмника должен начинаться с http:// или https://",
|
||||||
|
"error.value": "Ошибка: {error}",
|
||||||
|
"context.sendPage": "Отправить страницу в Верстак",
|
||||||
|
"context.sendSelection": "Отправить выделение в Верстак",
|
||||||
|
"context.sendLink": "Отправить ссылку в Верстак"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue