From d6c6ca2cc5abea56951d37b5e3a17990135d0f17 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 14 Jul 2026 22:00:24 +0800 Subject: [PATCH] fix: localize popup operation errors --- scripts/test-i18n.js | 2 +- scripts/test-popup-settings.js | 33 +++++++++++++++++++++++++++++++-- shared/locales/en.json | 5 ++++- shared/locales/ru.json | 5 ++++- shared/popup/popup.js | 13 +++++++++---- 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/scripts/test-i18n.js b/scripts/test-i18n.js index 5ddcc47..59a354a 100644 --- a/scripts/test-i18n.js +++ b/scripts/test-i18n.js @@ -29,7 +29,7 @@ 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(tEn('error.sendCapture'), 'Could not send the capture. Please try again.'); assert.strictEqual(tRu('missing', null, 'Fallback'), 'Fallback'); assert.strictEqual(tRu('missing.key'), 'missing.key'); diff --git a/scripts/test-popup-settings.js b/scripts/test-popup-settings.js index 57f034f..490a513 100644 --- a/scripts/test-popup-settings.js +++ b/scripts/test-popup-settings.js @@ -65,6 +65,8 @@ const elements = {}; }); let savedSettings = null; +let nextRequestError = null; +const technicalWarnings = []; const initialState = { settings: { receiverUrl: 'http://127.0.0.1:47731/api/browser-inbox/v1/captures', @@ -80,6 +82,11 @@ const browser = { runtime: { getURL(relativePath) { return `extension://${relativePath}`; }, sendMessage(message) { + if (nextRequestError) { + const error = nextRequestError; + nextRequestError = null; + return Promise.reject(new Error(error)); + } if (message.action === 'getState') return Promise.resolve(initialState); if (message.action === 'saveSettings') { savedSettings = message.settings; @@ -107,10 +114,24 @@ function fetchCatalog(url) { } const i18nPath = path.join(__dirname, '..', 'shared', 'i18n.js'); const popupPath = path.join(__dirname, '..', 'shared', 'popup', 'popup.js'); -const context = vm.createContext({ browser, console, document, Promise, btoa, fetch: fetchCatalog, navigator: { language: 'en-US' } }); +const popupSource = fs.readFileSync(popupPath, 'utf8'); +assert.equal(/setStatus\(\s*(?:error\b|err\b|String\()/.test(popupSource), false); +const context = vm.createContext({ + browser, + console: { + error: console.error, + log: console.log, + warn(...args) { technicalWarnings.push(args.map(String).join(' ')); }, + }, + document, + Promise, + btoa, + fetch: fetchCatalog, + navigator: { language: 'en-US' }, +}); context.globalThis = context; vm.runInContext(fs.readFileSync(i18nPath, 'utf8'), context, { filename: i18nPath }); -vm.runInContext(fs.readFileSync(popupPath, 'utf8'), context, { filename: popupPath }); +vm.runInContext(popupSource, context, { filename: popupPath }); async function flush() { for (let i = 0; i < 16; i += 1) await Promise.resolve(); @@ -156,6 +177,14 @@ async function flush() { assert.strictEqual(savedSettings.language, 'en'); assert.strictEqual(savedSettings.passiveActivityEnabled, true); assert.deepStrictEqual(Array.from(savedSettings.passiveActivityExcludedDomains), ['youtube.com', 'x.com']); + + nextRequestError = '[plugin:verstak.browser-inbox] captures.create failed: receiver unavailable'; + elements['capture-page'].click(); + await flush(); + assert.strictEqual(elements.status.textContent, 'Could not send the capture. Please try again.'); + assert.equal(elements.status.textContent.includes('[plugin:'), false); + assert.ok(technicalWarnings.some((message) => message.includes('captures.create failed'))); + console.log('browser extension popup localization/settings tests passed'); })().catch((error) => { console.error(error); diff --git a/shared/locales/en.json b/shared/locales/en.json index d5b6ee7..3b697a9 100644 --- a/shared/locales/en.json +++ b/shared/locales/en.json @@ -30,7 +30,10 @@ "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}", + "error.loadState": "Could not load the extension state. Please try again.", + "error.saveSettings": "Could not save settings. Please try again.", + "error.sendCapture": "Could not send the capture. Please try again.", + "error.readFile": "Could not read the file. Choose it again.", "context.sendPage": "Send page to Verstak", "context.sendSelection": "Send selection to Verstak", "context.sendLink": "Send link to Verstak" diff --git a/shared/locales/ru.json b/shared/locales/ru.json index 9f010a1..a39c4ab 100644 --- a/shared/locales/ru.json +++ b/shared/locales/ru.json @@ -30,7 +30,10 @@ "error.chooseFile": "Сначала выберите файл", "error.fileTooLarge": "Файл слишком велик для отправки из браузера", "error.invalidReceiverUrl": "URL приёмника должен начинаться с http:// или https://", - "error.value": "Ошибка: {error}", + "error.loadState": "Не удалось загрузить данные расширения. Повторите попытку.", + "error.saveSettings": "Не удалось сохранить настройки. Повторите попытку.", + "error.sendCapture": "Не удалось отправить материал. Повторите попытку.", + "error.readFile": "Не удалось прочитать файл. Выберите его ещё раз.", "context.sendPage": "Отправить страницу в Верстак", "context.sendSelection": "Отправить выделение в Верстак", "context.sendLink": "Отправить ссылку в Верстак" diff --git a/shared/popup/popup.js b/shared/popup/popup.js index 8eeff6b..386ff90 100644 --- a/shared/popup/popup.js +++ b/shared/popup/popup.js @@ -118,6 +118,11 @@ statusEl.textContent = text; } + function reportError(key, fallback, error) { + console.warn('[verstak.popup] request failed:', error); + setStatus(t(key, null, fallback)); + } + function request(message) { return Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) { if (result && result.error) throw new Error(result.error); @@ -148,7 +153,7 @@ applyLocale(state.settings && state.settings.language || 'system'); return state; }).catch(function (error) { - setStatus(error && error.message ? error.message : String(error)); + reportError('error.loadState', 'Could not load the extension state. Please try again.', error); }); } @@ -174,7 +179,7 @@ if (successMessage) setStatus(t('status.saved', null, 'Saved')); return state; }).catch(function (error) { - setStatus(error && error.message ? error.message : String(error)); + reportError('error.saveSettings', 'Could not save settings. Please try again.', error); }); } @@ -188,7 +193,7 @@ setStatus(t('status.done', null, 'Done')); } }).catch(function (error) { - setStatus(error && error.message ? error.message : String(error)); + reportError('error.sendCapture', 'Could not send the capture. Please try again.', error); }); } @@ -218,7 +223,7 @@ fileDataBase64: arrayBufferToBase64(results[0]) }); }).catch(function (error) { - setStatus(error && error.message ? error.message : String(error)); + reportError('error.readFile', 'Could not read the file. Choose it again.', error); }); });