diff --git a/plugins/browser-inbox/frontend/src/index.js b/plugins/browser-inbox/frontend/src/index.js index afa36c6..2091422 100644 --- a/plugins/browser-inbox/frontend/src/index.js +++ b/plugins/browser-inbox/frontend/src/index.js @@ -1059,7 +1059,7 @@ function loadPairing() { setBusy(true); - setStatus('Loading...', false); + setStatus(tr('ui.loading', null, 'Loading...'), false); return Promise.resolve().then(function () { return pairingAPI().pairing(); }).then(function (pairing) { @@ -1075,11 +1075,11 @@ function copyValue(value, label) { if (!value) return; if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { - setStatus('Clipboard unavailable', true); + setStatus(tr('ui.clipboardUnavailable', null, 'Clipboard unavailable'), true); return; } navigator.clipboard.writeText(value).then(function () { - setStatus(label + ' copied', false); + setStatus(tr('ui.copied', { label: label }, '{label} copied'), false); }).catch(function (err) { setStatus(text(err && err.message ? err.message : err), true); }); @@ -1092,14 +1092,14 @@ copyValue(receiverTokenInput.value, 'Token'); }); rotateTokenButton.addEventListener('click', function () { - if (typeof window.confirm === 'function' && !window.confirm('Rotate pairing token?')) return; + if (typeof window.confirm === 'function' && !window.confirm(tr('ui.rotateConfirm', null, 'Rotate pairing token?'))) return; setBusy(true); - setStatus('Rotating...', false); + setStatus(tr('ui.rotating', null, 'Rotating...'), false); Promise.resolve().then(function () { return pairingAPI().rotateToken(); }).then(function (pairing) { applyPairing(pairing); - setStatus('Token rotated', false); + setStatus(tr('ui.tokenRotated', null, 'Token rotated'), false); }).catch(function (err) { setStatus(text(err && err.message ? err.message : err), true); }).then(function () { diff --git a/plugins/browser-inbox/locales/en.json b/plugins/browser-inbox/locales/en.json index 303cc04..84456c6 100644 --- a/plugins/browser-inbox/locales/en.json +++ b/plugins/browser-inbox/locales/en.json @@ -29,5 +29,11 @@ "ui.copyToken": "Copy Token", "ui.rotateToken": "Rotate Token", "ui.receiverUrl": "Receiver URL", - "ui.pairingToken": "Pairing Token" + "ui.pairingToken": "Pairing Token", + "ui.loading": "Loading...", + "ui.clipboardUnavailable": "Clipboard unavailable", + "ui.copied": "{label} copied", + "ui.rotateConfirm": "Rotate pairing token?", + "ui.rotating": "Rotating...", + "ui.tokenRotated": "Token rotated" } diff --git a/plugins/browser-inbox/locales/ru.json b/plugins/browser-inbox/locales/ru.json index 4c497b3..3cb6de2 100644 --- a/plugins/browser-inbox/locales/ru.json +++ b/plugins/browser-inbox/locales/ru.json @@ -29,5 +29,11 @@ "ui.copyToken": "Копировать токен", "ui.rotateToken": "Сменить токен", "ui.receiverUrl": "URL приёмника", - "ui.pairingToken": "Токен сопряжения" + "ui.pairingToken": "Токен сопряжения", + "ui.loading": "Загрузка...", + "ui.clipboardUnavailable": "Буфер обмена недоступен", + "ui.copied": "{label} скопирован", + "ui.rotateConfirm": "Сменить токен сопряжения?", + "ui.rotating": "Смена токена...", + "ui.tokenRotated": "Токен изменён" } diff --git a/plugins/journal/frontend/src/index.js b/plugins/journal/frontend/src/index.js index 83917f8..30452f6 100644 --- a/plugins/journal/frontend/src/index.js +++ b/plugins/journal/frontend/src/index.js @@ -505,7 +505,11 @@ } function render() { - countEl.textContent = entries.length + ' entr' + (entries.length === 1 ? 'y' : 'ies'); + countEl.textContent = tr( + entries.length === 1 ? 'ui.entryCount.one' : 'ui.entryCount.many', + { count: entries.length }, + entries.length === 1 ? '{count} entry' : '{count} entries' + ); statusEl.textContent = statusText; statusEl.className = 'journal-status' + (statusClass ? ' ' + statusClass : ''); addBtn.disabled = scope.mode !== 'workspace'; diff --git a/plugins/journal/locales/en.json b/plugins/journal/locales/en.json index 629c5cc..4c74be4 100644 --- a/plugins/journal/locales/en.json +++ b/plugins/journal/locales/en.json @@ -29,5 +29,7 @@ "ui.deleted": "Entry deleted", "ui.empty": "No worklog entries yet.", "ui.edit": "Edit", - "ui.delete": "Delete" + "ui.delete": "Delete", + "ui.entryCount.one": "{count} entry", + "ui.entryCount.many": "{count} entries" } diff --git a/plugins/journal/locales/ru.json b/plugins/journal/locales/ru.json index 12dd97a..6f955bf 100644 --- a/plugins/journal/locales/ru.json +++ b/plugins/journal/locales/ru.json @@ -29,5 +29,7 @@ "ui.deleted": "Запись удалена", "ui.empty": "Записей о работе пока нет.", "ui.edit": "Изменить", - "ui.delete": "Удалить" + "ui.delete": "Удалить", + "ui.entryCount.one": "{count} запись", + "ui.entryCount.many": "Записей: {count}" } diff --git a/plugins/platform-test/frontend/src/index.js b/plugins/platform-test/frontend/src/index.js index 61d7b8d..4fb0204 100644 --- a/plugins/platform-test/frontend/src/index.js +++ b/plugins/platform-test/frontend/src/index.js @@ -69,6 +69,13 @@ return fallback || key; } containerEl.__ptCleanup = []; + var localizedNodes = []; + + function localized(tag, attrs, key, fallback) { + var node = el(tag, attrs, [tr(key, null, fallback)]); + localizedNodes.push({ node: node, key: key, fallback: fallback }); + return node; + } function trackCleanup(fn) { if (typeof fn === 'function') { @@ -79,12 +86,19 @@ containerEl.__ptCleanup.push(fn); } } + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + trackCleanup(api.i18n.onDidChangeLocale(function () { + localizedNodes.forEach(function (item) { + item.node.textContent = tr(item.key, null, item.fallback); + }); + })); + } /* ── Header ─────────────────────────────────────────────────── */ var header = div('pt-header', [ span('pt-icon', '◉'), div('pt-title-group', [ - el('h2', { className: 'pt-plugin-name' }, [tr('ui.diagnostics', null, 'Platform Diagnostics')]), + localized('h2', { className: 'pt-plugin-name' }, 'ui.diagnostics', 'Platform Diagnostics'), el('p', { className: 'pt-plugin-id' }, [api.pluginId]), ]), span('pt-version', 'v' + (props && props.version ? props.version : '0.1.0')), @@ -93,7 +107,7 @@ /* ── Status badge ──────────────────────────────────────────── */ var badge = div('pt-badge pt-badge-success', [ el('span', {}, ['✅']), - el('span', {}, [tr('ui.bundleLoaded', null, 'Frontend Bundle Loaded')]), + localized('span', {}, 'ui.bundleLoaded', 'Frontend Bundle Loaded'), ]); var badgeRow = div('', [badge]); @@ -279,7 +293,7 @@ }); var bridgeCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.apiBridge', null, 'Real Plugin API Bridge')]), + localized('h3', { className: 'pt-card-title' }, 'ui.apiBridge', 'Real Plugin API Bridge'), el('ul', { className: 'pt-list' }, [ el('li', { className: 'pt-list-item' }, [ span('pt-list-label', 'Persisted setting'), @@ -366,7 +380,7 @@ }); var testsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.testResults', null, 'Test Results')]), + localized('h3', { className: 'pt-card-title' }, 'ui.testResults', 'Test Results'), summaryRow, testsList, ]); @@ -403,7 +417,7 @@ }); var capsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.capabilities', null, 'Registered Capabilities')]), + localized('h3', { className: 'pt-card-title' }, 'ui.capabilities', 'Registered Capabilities'), capList, ]); @@ -426,7 +440,7 @@ }); var infoCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.pluginInfo', null, 'Plugin Info')]), + localized('h3', { className: 'pt-card-title' }, 'ui.pluginInfo', 'Plugin Info'), infoList, ]); @@ -461,7 +475,7 @@ }); var apiCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.hostMethods', null, 'Host API Methods')]), + localized('h3', { className: 'pt-card-title' }, 'ui.hostMethods', 'Host API Methods'), apiStatusList, ]); @@ -589,7 +603,7 @@ trackCleanup(function () { stopMouseCapture(); }); var mouseCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.mouseInspector', null, 'Mouse Event Inspector')]), + localized('h3', { className: 'pt-card-title' }, 'ui.mouseInspector', 'Mouse Event Inspector'), el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [ 'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.', ]), @@ -676,6 +690,23 @@ if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); return fallback || key; } + var localizedNodes = []; + var unsubscribeLocale = null; + + function localized(tag, attrs, key, fallback) { + var node = el(tag, attrs, [tr(key, null, fallback)]); + localizedNodes.push({ node: node, key: key, fallback: fallback }); + return node; + } + + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + unsubscribeLocale = api.i18n.onDidChangeLocale(function () { + localizedNodes.forEach(function (item) { + item.node.textContent = tr(item.key, null, item.fallback); + }); + }); + containerEl.__ptSettingsCleanup = unsubscribeLocale; + } /* ── Counter state (local, not persisted) ──────────────────── */ var counterState = { value: 0 }; @@ -684,7 +715,7 @@ var header = div('pt-header', [ span('pt-icon', '⚙️'), div('pt-title-group', [ - el('h2', { className: 'pt-plugin-name' }, [tr('ui.settings', null, 'Platform Test Settings')]), + localized('h2', { className: 'pt-plugin-name' }, 'ui.settings', 'Platform Test Settings'), el('p', { className: 'pt-plugin-id' }, [api.pluginId]), ]), ]); @@ -707,24 +738,24 @@ var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () { counterState.value += 1; counterDisplay.firstChild.textContent = String(counterState.value); - }}, [tr('ui.increment', null, '+ Increment')]); + }}, [localized('span', {}, 'ui.increment', '+ Increment')]); var decrementBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = Math.max(0, counterState.value - 1); counterDisplay.firstChild.textContent = String(counterState.value); - }}, [tr('ui.decrement', null, '− Decrement')]); + }}, [localized('span', {}, 'ui.decrement', '− Decrement')]); var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = 0; counterDisplay.firstChild.textContent = '0'; - }}, [tr('ui.reset', null, '↺ Reset')]); + }}, [localized('span', {}, 'ui.reset', '↺ Reset')]); var btnGroup = el('div', { style: { display: 'flex', gap: '0.5rem' } }, [ incrementBtn, decrementBtn, resetBtn, ]); var counterCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.counter', null, 'Interactive Counter (Local State)')]), + localized('h3', { className: 'pt-card-title' }, 'ui.counter', 'Interactive Counter (Local State)'), counterDisplay, btnGroup, el('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [ @@ -750,7 +781,7 @@ }); var settingsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, [tr('ui.demoSettings', null, 'Plugin Settings (Demo)')]), + localized('h3', { className: 'pt-card-title' }, 'ui.demoSettings', 'Plugin Settings (Demo)'), settingsDemoList, el('p', { style: { marginTop: '0.5rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [ 'Use api.settings.read() / api.settings.write() for persisted settings.', @@ -765,8 +796,10 @@ }, unmount: function (containerEl) { + if (typeof containerEl.__ptSettingsCleanup === 'function') containerEl.__ptSettingsCleanup(); containerEl.innerHTML = ''; containerEl.className = ''; + delete containerEl.__ptSettingsCleanup; }, }; diff --git a/plugins/secrets/frontend/src/index.js b/plugins/secrets/frontend/src/index.js index af08c68..df17129 100644 --- a/plugins/secrets/frontend/src/index.js +++ b/plugins/secrets/frontend/src/index.js @@ -423,7 +423,7 @@ } function deleteSecret(id) { - if (!id || !window.confirm('Delete this secret?')) return; + if (!id || !window.confirm(tr('ui.deleteConfirm', null, 'Delete this secret?'))) return; api.secrets.delete(id).then(function () { selectedID = ''; selectedRecord = null; @@ -437,7 +437,7 @@ function copySecretLink(id) { api.secrets.copyLink(id).then(function (link) { return writeClipboard(api, link).then(function () { - setStatus('Secret link copied', false); + setStatus(tr('ui.linkCopied', null, 'Secret link copied'), false); }); }).catch(function (err) { setStatus((err && err.message) ? err.message : String(err), true); diff --git a/plugins/secrets/locales/en.json b/plugins/secrets/locales/en.json index 352516e..1e4b70c 100644 --- a/plugins/secrets/locales/en.json +++ b/plugins/secrets/locales/en.json @@ -32,5 +32,7 @@ "ui.scope": "Scope", "ui.value": "Value", "ui.save": "Save", - "ui.cancel": "Cancel" + "ui.cancel": "Cancel", + "ui.deleteConfirm": "Delete this secret?", + "ui.linkCopied": "Secret link copied" } diff --git a/plugins/secrets/locales/ru.json b/plugins/secrets/locales/ru.json index ab19dda..1a34bf5 100644 --- a/plugins/secrets/locales/ru.json +++ b/plugins/secrets/locales/ru.json @@ -32,5 +32,7 @@ "ui.scope": "Область", "ui.value": "Значение", "ui.save": "Сохранить", - "ui.cancel": "Отмена" + "ui.cancel": "Отмена", + "ui.deleteConfirm": "Удалить этот секрет?", + "ui.linkCopied": "Ссылка на секрет скопирована" } diff --git a/scripts/smoke-platform-frontend.js b/scripts/smoke-platform-frontend.js index d6ef2ff..fe17301 100644 --- a/scripts/smoke-platform-frontend.js +++ b/scripts/smoke-platform-frontend.js @@ -37,6 +37,15 @@ class FakeNode { addEventListener() {} } +function findByClass(node, className) { + if (String(node.className || '').split(/\s+/).includes(className)) return node; + for (const child of node.children || []) { + const found = findByClass(child, className); + if (found) return found; + } + return null; +} + function makeDocument() { return { head: new FakeNode('head'), @@ -76,6 +85,25 @@ if (!components) { const api = { pluginId: 'verstak.platform-test', + i18n: { + locale: 'en', + listeners: new Set(), + messages: { + en: { 'ui.diagnostics': 'Platform Diagnostics', 'ui.settings': 'Platform Test Settings' }, + ru: { 'ui.diagnostics': 'Диагностика платформы', 'ui.settings': 'Настройки теста платформы' }, + }, + getLocale() { return this.locale; }, + t(key, params, fallback) { return this.messages[this.locale][key] || fallback || key; }, + onDidChangeLocale(listener) { + this.listeners.add(listener); + listener(this.locale); + return () => this.listeners.delete(listener); + }, + setLocale(locale) { + this.locale = locale; + this.listeners.forEach((listener) => listener(locale)); + }, + }, settings: { read: async () => 'initial value', write: async () => undefined, @@ -179,9 +207,27 @@ const api = { if (container.children.length === 0) { throw new Error(`${name} mounted no DOM nodes`); } + if (name === 'DiagnosticsPanel') { + api.i18n.setLocale('ru'); + if (findByClass(container, 'pt-plugin-name')?.textContent !== 'Диагностика платформы') { + throw new Error('DiagnosticsPanel did not update after locale change'); + } + } + if (name === 'PlatformTestSettings') { + const counter = findByClass(container, 'pt-counter-value'); + counter.textContent = '3'; + api.i18n.setLocale('ru'); + if (findByClass(container, 'pt-plugin-name')?.textContent !== 'Настройки теста платформы') { + throw new Error('PlatformTestSettings did not update after locale change'); + } + if (counter.textContent !== '3') { + throw new Error('PlatformTestSettings lost counter state after locale change'); + } + } if (typeof component.unmount === 'function') { component.unmount(container); } + api.i18n.setLocale('en'); } console.log('platform-test frontend smoke passed');