fix: fully localize platform diagnostics

This commit is contained in:
mirivlad 2026-07-11 12:56:40 +08:00
parent 1bc5ecfd16
commit fe97c5ddcc
4 changed files with 300 additions and 109 deletions

View File

@ -73,10 +73,25 @@
function localized(tag, attrs, key, fallback) { function localized(tag, attrs, key, fallback) {
var node = el(tag, attrs, [tr(key, null, fallback)]); var node = el(tag, attrs, [tr(key, null, fallback)]);
localizedNodes.push({ node: node, key: key, fallback: fallback }); var item = { node: node, key: key, params: null, fallback: fallback };
node.__ptLocalized = item;
localizedNodes.push(item);
return node; return node;
} }
function setLocalized(node, key, params, fallback) {
var item = node.__ptLocalized;
if (!item) {
item = { node: node };
node.__ptLocalized = item;
localizedNodes.push(item);
}
item.key = key;
item.params = params || null;
item.fallback = fallback;
node.textContent = tr(key, params, fallback);
}
function trackCleanup(fn) { function trackCleanup(fn) {
if (typeof fn === 'function') { if (typeof fn === 'function') {
if (!Array.isArray(containerEl.__ptCleanup)) { if (!Array.isArray(containerEl.__ptCleanup)) {
@ -89,7 +104,7 @@
if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') {
trackCleanup(api.i18n.onDidChangeLocale(function () { trackCleanup(api.i18n.onDidChangeLocale(function () {
localizedNodes.forEach(function (item) { localizedNodes.forEach(function (item) {
item.node.textContent = tr(item.key, null, item.fallback); if (item.key) item.node.textContent = tr(item.key, item.params, item.fallback);
}); });
})); }));
} }
@ -112,44 +127,44 @@
var badgeRow = div('', [badge]); var badgeRow = div('', [badge]);
/* ── Real Plugin API bridge checks ─────────────────────────── */ /* ── Real Plugin API bridge checks ─────────────────────────── */
var savedValue = span('pt-list-value pt-saved-setting', 'Saved setting: loading...'); var savedValue = localized('span', { className: 'pt-list-value pt-saved-setting' }, 'ui.savedLoading', 'Saved setting: loading...');
var capabilityValue = span('pt-list-value pt-capability-result', 'Capabilities: loading...'); var capabilityValue = localized('span', { className: 'pt-list-value pt-capability-result' }, 'ui.capabilitiesLoading', 'Capabilities: loading...');
var commandValue = span('pt-list-value pt-command-result', 'Command: registering...'); var commandValue = localized('span', { className: 'pt-list-value pt-command-result' }, 'ui.commandRegistering', 'Command: registering...');
var eventValue = span('pt-list-value pt-event-result', 'Event: subscribing...'); var eventValue = localized('span', { className: 'pt-list-value pt-event-result' }, 'ui.eventSubscribing', 'Event: subscribing...');
var filesValue = span('pt-list-value pt-files-result', 'Files: running...'); var filesValue = localized('span', { className: 'pt-list-value pt-files-result' }, 'ui.filesRunning', 'Files: running...');
var filesErrorValue = span('pt-list-value pt-files-error-result', 'Files error path: checking...'); var filesErrorValue = localized('span', { className: 'pt-list-value pt-files-error-result' }, 'ui.filesPathChecking', 'Files error path: checking...');
var workbenchValue = span('pt-list-value pt-workbench-result', 'Workbench: ready'); var workbenchValue = localized('span', { className: 'pt-list-value pt-workbench-result' }, 'ui.workbenchReady', 'Workbench: ready');
function makeWorkbenchButton(className, label, request) { function makeWorkbenchButton(className, key, label, request) {
return el('button', { return el('button', {
className: 'btn btn-primary ' + className, className: 'btn btn-primary ' + className,
onClick: function () { onClick: function () {
workbenchValue.textContent = 'Workbench: opening...'; setLocalized(workbenchValue, 'ui.workbenchOpening', null, 'Workbench: opening...');
api.workbench.editResource(request) api.workbench.editResource(request)
.then(function (result) { .then(function (result) {
workbenchValue.textContent = 'Workbench: opened ' + result.request.path + ' with ' + (result.providerId || 'no-provider'); setLocalized(workbenchValue, 'ui.workbenchOpened', { path: result.request.path, provider: result.providerId || 'no-provider' }, 'Workbench: opened {path} with {provider}');
workbenchValue.setAttribute('data-workbench-status', result.status === 'opened' ? 'ok' : result.status); workbenchValue.setAttribute('data-workbench-status', result.status === 'opened' ? 'ok' : result.status);
}) })
.catch(function (err) { .catch(function (err) {
workbenchValue.textContent = 'Workbench error: ' + (err && err.message ? err.message : String(err)); setLocalized(workbenchValue, 'ui.workbenchError', { error: err && err.message ? err.message : String(err) }, 'Workbench error: {error}');
workbenchValue.setAttribute('data-workbench-status', 'error'); workbenchValue.setAttribute('data-workbench-status', 'error');
}); });
}, },
}, [label]); }, [localized('span', {}, key, label)]);
} }
var openTextWorkbenchButton = makeWorkbenchButton('pt-open-workbench-text', 'Open Text Diagnostic', { var openTextWorkbenchButton = makeWorkbenchButton('pt-open-workbench-text', 'ui.openTextDiagnostic', 'Open Text Diagnostic', {
kind: 'vault-file', kind: 'vault-file',
path: 'Docs/todo.txt', path: 'Docs/todo.txt',
extension: '.txt', extension: '.txt',
mime: 'text/plain', mime: 'text/plain',
context: { sourceView: 'files' }, context: { sourceView: 'files' },
}); });
var openMarkdownWorkbenchButton = makeWorkbenchButton('pt-open-workbench-markdown', 'Open Markdown Diagnostic', { var openMarkdownWorkbenchButton = makeWorkbenchButton('pt-open-workbench-markdown', 'ui.openMarkdownDiagnostic', 'Open Markdown Diagnostic', {
kind: 'vault-file', kind: 'vault-file',
path: 'Docs/readme.md', path: 'Docs/readme.md',
extension: '.md', extension: '.md',
context: { sourceView: 'files' }, context: { sourceView: 'files' },
}); });
var openNotesWorkbenchButton = makeWorkbenchButton('pt-open-workbench-notes', 'Open Notes Diagnostic', { var openNotesWorkbenchButton = makeWorkbenchButton('pt-open-workbench-notes', 'ui.openNotesDiagnostic', 'Open Notes Diagnostic', {
kind: 'vault-file', kind: 'vault-file',
path: 'Notes/example.md', path: 'Notes/example.md',
extension: '.md', extension: '.md',
@ -162,51 +177,51 @@
var settingInput = el('input', { var settingInput = el('input', {
className: 'pt-setting-input', className: 'pt-setting-input',
type: 'text', type: 'text',
'aria-label': 'Saved setting', 'aria-label': tr('ui.savedSetting', null, 'Saved setting'),
value: 'changed value', value: 'changed value',
}); });
var saveStatus = span('pt-list-value', ''); var saveStatus = span('pt-list-value', '');
var saveButton = el('button', { var saveButton = el('button', {
className: 'btn btn-primary pt-save-setting', className: 'btn btn-primary pt-save-setting',
onClick: function () { onClick: function () {
saveStatus.textContent = 'Saving...'; setLocalized(saveStatus, 'ui.saving', null, 'Saving...');
api.settings.write('savedText', settingInput.value) api.settings.write('savedText', settingInput.value)
.then(function () { .then(function () {
savedValue.textContent = 'Saved setting: ' + settingInput.value; setLocalized(savedValue, 'ui.savedSettingValue', { value: settingInput.value }, 'Saved setting: {value}');
saveStatus.textContent = 'Saved'; setLocalized(saveStatus, 'ui.saved', null, 'Saved');
}) })
.catch(function (err) { .catch(function (err) {
saveStatus.textContent = 'Error: ' + (err && err.message ? err.message : String(err)); setLocalized(saveStatus, 'ui.errorValue', { error: err && err.message ? err.message : String(err) }, 'Error: {error}');
}); });
}, },
}, ['Save Setting']); }, [localized('span', {}, 'ui.saveSetting', 'Save Setting')]);
api.settings.read('savedText') api.settings.read('savedText')
.then(function (value) { .then(function (value) {
var text = value || ''; var text = value || '';
settingInput.value = text || 'changed value'; settingInput.value = text || 'changed value';
savedValue.textContent = 'Saved setting: ' + text; setLocalized(savedValue, 'ui.savedSettingValue', { value: text }, 'Saved setting: {value}');
}) })
.catch(function (err) { .catch(function (err) {
savedValue.textContent = 'Settings error: ' + (err && err.message ? err.message : String(err)); setLocalized(savedValue, 'ui.settingsError', { error: err && err.message ? err.message : String(err) }, 'Settings error: {error}');
}); });
api.capabilities.list() api.capabilities.list()
.then(function (caps) { .then(function (caps) {
capabilityValue.textContent = 'Capabilities: ' + caps.length + ' available'; setLocalized(capabilityValue, 'ui.capabilitiesAvailable', { count: caps.length }, 'Capabilities: {count} available');
}) })
.catch(function (err) { .catch(function (err) {
capabilityValue.textContent = 'Capabilities error: ' + (err && err.message ? err.message : String(err)); setLocalized(capabilityValue, 'ui.capabilitiesError', { error: err && err.message ? err.message : String(err) }, 'Capabilities error: {error}');
}); });
api.capabilities.has('verstak/platform-test/v1') api.capabilities.has('verstak/platform-test/v1')
.then(function (available) { .then(function (available) {
badge.setAttribute('data-capability-status', available ? 'available' : 'missing'); badge.setAttribute('data-capability-status', available ? 'available' : 'missing');
badge.lastChild.textContent = 'Frontend Bundle Loaded | capability ' + (available ? 'available' : 'missing'); setLocalized(badge.lastChild, available ? 'ui.bundleCapabilityAvailable' : 'ui.bundleCapabilityMissing', null, available ? 'Frontend Bundle Loaded | capability available' : 'Frontend Bundle Loaded | capability missing');
}) })
.catch(function (err) { .catch(function (err) {
badge.setAttribute('data-capability-status', 'error'); badge.setAttribute('data-capability-status', 'error');
badge.lastChild.textContent = 'Capability error: ' + (err && err.message ? err.message : String(err)); setLocalized(badge.lastChild, 'ui.capabilityError', { error: err && err.message ? err.message : String(err) }, 'Capability error: {error}');
}); });
api.commands.register('verstak.platform-test.show-version', function () { api.commands.register('verstak.platform-test.show-version', function () {
@ -221,17 +236,17 @@
}) })
.then(function (result) { .then(function (result) {
badge.setAttribute('data-command-status', result.status || ''); badge.setAttribute('data-command-status', result.status || '');
commandValue.textContent = 'Command: ' + result.status + ' ' + result.result.version + ' from ' + result.result.source; setLocalized(commandValue, 'ui.commandResult', { status: result.status, version: result.result.version, source: result.result.source }, 'Command: {status} {version} from {source}');
}) })
.catch(function (err) { .catch(function (err) {
badge.setAttribute('data-command-status', 'error'); badge.setAttribute('data-command-status', 'error');
commandValue.textContent = 'Command error: ' + (err && err.message ? err.message : String(err)); setLocalized(commandValue, 'ui.commandError', { error: err && err.message ? err.message : String(err) }, 'Command error: {error}');
console.error('[platform-test] command bridge error:', err); console.error('[platform-test] command bridge error:', err);
}); });
api.events.subscribe('verstak.platform-test.echo', function (event) { api.events.subscribe('verstak.platform-test.echo', function (event) {
var message = event && event.payload ? event.payload.message : ''; var message = event && event.payload ? event.payload.message : '';
eventValue.textContent = 'Event: received ' + message; setLocalized(eventValue, 'ui.eventReceived', { message: message }, 'Event: received {message}');
eventValue.setAttribute('data-event-status', 'received'); eventValue.setAttribute('data-event-status', 'received');
}) })
.then(function (unsubscribe) { .then(function (unsubscribe) {
@ -239,7 +254,7 @@
return api.events.publish('verstak.platform-test.echo', { message: 'hello-event' }); return api.events.publish('verstak.platform-test.echo', { message: 'hello-event' });
}) })
.catch(function (err) { .catch(function (err) {
eventValue.textContent = 'Event error: ' + (err && err.message ? err.message : String(err)); setLocalized(eventValue, 'ui.eventError', { error: err && err.message ? err.message : String(err) }, 'Event error: {error}');
eventValue.setAttribute('data-event-status', 'error'); eventValue.setAttribute('data-event-status', 'error');
}); });
@ -268,27 +283,27 @@
return api.files.trash('PlatformTest/files-api-moved.txt'); return api.files.trash('PlatformTest/files-api-moved.txt');
}) })
.then(function () { .then(function () {
filesValue.textContent = 'Files: wrote/read/listed/moved/trashed'; setLocalized(filesValue, 'ui.filesPassed', null, 'Files: wrote/read/listed/moved/trashed');
filesValue.setAttribute('data-files-status', 'ok'); filesValue.setAttribute('data-files-status', 'ok');
}) })
.catch(function (err) { .catch(function (err) {
filesValue.textContent = 'Files error: ' + (err && err.message ? err.message : String(err)); setLocalized(filesValue, 'ui.filesError', { error: err && err.message ? err.message : String(err) }, 'Files error: {error}');
filesValue.setAttribute('data-files-status', 'error'); filesValue.setAttribute('data-files-status', 'error');
}); });
api.files.readText('.verstak/vault.json') api.files.readText('.verstak/vault.json')
.then(function () { .then(function () {
filesErrorValue.textContent = 'Files error path: unexpectedly allowed'; setLocalized(filesErrorValue, 'ui.filesPathAllowed', null, 'Files error path: unexpectedly allowed');
filesErrorValue.setAttribute('data-files-error-status', 'error'); filesErrorValue.setAttribute('data-files-error-status', 'error');
}) })
.catch(function (err) { .catch(function (err) {
var message = err && err.message ? err.message : String(err); var message = err && err.message ? err.message : String(err);
if (message.indexOf('reserved-path') === -1 && message.indexOf('.verstak') === -1) { if (message.indexOf('reserved-path') === -1 && message.indexOf('.verstak') === -1) {
filesErrorValue.textContent = 'Files error path: wrong error ' + message; setLocalized(filesErrorValue, 'ui.filesPathWrongError', { error: message }, 'Files error path: wrong error {error}');
filesErrorValue.setAttribute('data-files-error-status', 'error'); filesErrorValue.setAttribute('data-files-error-status', 'error');
return; return;
} }
filesErrorValue.textContent = 'Files error path: rejected reserved-path'; setLocalized(filesErrorValue, 'ui.filesPathRejected', null, 'Files error path: rejected reserved-path');
filesErrorValue.setAttribute('data-files-error-status', 'expected'); filesErrorValue.setAttribute('data-files-error-status', 'expected');
}); });
@ -296,11 +311,11 @@
localized('h3', { className: 'pt-card-title' }, 'ui.apiBridge', 'Real Plugin API Bridge'), localized('h3', { className: 'pt-card-title' }, 'ui.apiBridge', 'Real Plugin API Bridge'),
el('ul', { className: 'pt-list' }, [ el('ul', { className: 'pt-list' }, [
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Persisted setting'), localized('span', { className: 'pt-list-label' }, 'ui.persistedSetting', 'Persisted setting'),
savedValue, savedValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'New value'), localized('span', { className: 'pt-list-label' }, 'ui.newValue', 'New value'),
settingInput, settingInput,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
@ -308,27 +323,27 @@
saveStatus, saveStatus,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Capabilities'), localized('span', { className: 'pt-list-label' }, 'ui.capabilitiesLabel', 'Capabilities'),
capabilityValue, capabilityValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Command runtime'), localized('span', { className: 'pt-list-label' }, 'ui.commandRuntime', 'Command runtime'),
commandValue, commandValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Event runtime'), localized('span', { className: 'pt-list-label' }, 'ui.eventRuntime', 'Event runtime'),
eventValue, eventValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Files runtime'), localized('span', { className: 'pt-list-label' }, 'ui.filesRuntime', 'Files runtime'),
filesValue, filesValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Files reserved path'), localized('span', { className: 'pt-list-label' }, 'ui.filesReservedPath', 'Files reserved path'),
filesErrorValue, filesErrorValue,
]), ]),
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', 'Workbench routing'), localized('span', { className: 'pt-list-label' }, 'ui.workbenchRouting', 'Workbench routing'),
openTextWorkbenchButton, openTextWorkbenchButton,
openMarkdownWorkbenchButton, openMarkdownWorkbenchButton,
openNotesWorkbenchButton, openNotesWorkbenchButton,
@ -339,12 +354,12 @@
/* ── Test results summary ──────────────────────────────────── */ /* ── Test results summary ──────────────────────────────────── */
var testsData = [ var testsData = [
{ label: 'Plugin Registration', status: 'pass' }, { key: 'registration', label: 'Plugin Registration', status: 'pass' },
{ label: 'Capability: verstak/platform-test/v1', status: 'pass' }, { key: 'platformCapability', label: 'Capability: verstak/platform-test/v1', status: 'pass' },
{ label: 'Capability: verstak/diagnostics/v1', status: 'pass' }, { key: 'diagnosticsCapability', label: 'Capability: verstak/diagnostics/v1', status: 'pass' },
{ label: 'Capability: verstak/core/files/v1', status: 'pass' }, { key: 'filesCapability', label: 'Capability: verstak/core/files/v1', status: 'pass' },
{ label: 'Capability: verstak/core/workbench/v1', status: 'pass' }, { key: 'workbenchCapability', label: 'Capability: verstak/core/workbench/v1', status: 'pass' },
{ label: 'API Contract Compliance', status: 'pass' }, { key: 'apiContract', label: 'API Contract Compliance', status: 'pass' },
]; ];
var totalTests = testsData.length; var totalTests = testsData.length;
@ -353,19 +368,19 @@
var summaryRow = div('pt-test-summary', [ var summaryRow = div('pt-test-summary', [
div('pt-test-stat', [ div('pt-test-stat', [
span('pt-test-stat-value pt-pass', String(passedTests)), span('pt-test-stat-value pt-pass', String(passedTests)),
span('pt-test-stat-label', 'Passed'), localized('span', { className: 'pt-test-stat-label' }, 'ui.passed', 'Passed'),
]), ]),
div('pt-test-stat', [ div('pt-test-stat', [
span('pt-test-stat-value pt-fail', String(totalTests - passedTests)), span('pt-test-stat-value pt-fail', String(totalTests - passedTests)),
span('pt-test-stat-label', 'Failed'), localized('span', { className: 'pt-test-stat-label' }, 'ui.failed', 'Failed'),
]), ]),
div('pt-test-stat', [ div('pt-test-stat', [
span('pt-test-stat-value', String(totalTests)), span('pt-test-stat-value', String(totalTests)),
span('pt-test-stat-label', 'Total'), localized('span', { className: 'pt-test-stat-label' }, 'ui.total', 'Total'),
]), ]),
div('pt-test-stat', [ div('pt-test-stat', [
span('pt-test-stat-value pt-pass', '100%'), span('pt-test-stat-value pt-pass', '100%'),
span('pt-test-stat-label', 'Success Rate'), localized('span', { className: 'pt-test-stat-label' }, 'ui.successRate', 'Success Rate'),
]), ]),
]); ]);
@ -373,8 +388,8 @@
testsData.forEach(function (t) { testsData.forEach(function (t) {
var dot = el('span', { className: 'pt-cap-dot pt-cap-dot-ok' }); var dot = el('span', { className: 'pt-cap-dot pt-cap-dot-ok' });
var item = el('li', { className: 'pt-list-item' }, [ var item = el('li', { className: 'pt-list-item' }, [
el('span', { className: 'pt-list-label' }, [dot, ' ', t.label]), el('span', { className: 'pt-list-label' }, [dot, ' ', localized('span', {}, 'ui.test.' + t.key, t.label)]),
span('pt-list-value pt-pass', t.status === 'pass' ? '✓ PASS' : '✗ FAIL'), localized('span', { className: 'pt-list-value pt-pass' }, t.status === 'pass' ? 'ui.pass' : 'ui.fail', t.status === 'pass' ? '✓ PASS' : '✗ FAIL'),
]); ]);
testsList.appendChild(item); testsList.appendChild(item);
}); });
@ -399,7 +414,7 @@
var dot = el('span', { var dot = el('span', {
className: 'pt-cap-dot pt-cap-dot-missing', className: 'pt-cap-dot pt-cap-dot-missing',
}); });
var statusText = span('pt-list-value', 'Checking...'); var statusText = localized('span', { className: 'pt-list-value' }, 'ui.checking', 'Checking...');
var item = el('li', { className: 'pt-list-item' }, [ var item = el('li', { className: 'pt-list-item' }, [
el('span', { className: 'pt-list-label' }, [dot, ' ', cap.label]), el('span', { className: 'pt-list-label' }, [dot, ' ', cap.label]),
statusText, statusText,
@ -408,11 +423,11 @@
api.capabilities.has(cap.id) api.capabilities.has(cap.id)
.then(function (available) { .then(function (available) {
dot.className = 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing'); dot.className = 'pt-cap-dot ' + (available ? 'pt-cap-dot-ok' : 'pt-cap-dot-missing');
statusText.textContent = available ? '✓ Available' : '— Unavailable'; setLocalized(statusText, available ? 'ui.available' : 'ui.unavailable', null, available ? '✓ Available' : '— Unavailable');
}) })
.catch(function () { .catch(function () {
dot.className = 'pt-cap-dot pt-cap-dot-missing'; dot.className = 'pt-cap-dot pt-cap-dot-missing';
statusText.textContent = 'Error'; setLocalized(statusText, 'ui.error', null, 'Error');
}); });
}); });
@ -424,16 +439,16 @@
/* ── Plugin info ───────────────────────────────────────────── */ /* ── Plugin info ───────────────────────────────────────────── */
var infoList = el('ul', { className: 'pt-list' }); var infoList = el('ul', { className: 'pt-list' });
var infoItems = [ var infoItems = [
{ label: 'Plugin ID', value: api.pluginId }, { key: 'pluginId', label: 'Plugin ID', value: api.pluginId },
{ label: 'Bundle Status', value: 'Loaded ✓' }, { key: 'bundleStatus', label: 'Bundle Status', value: tr('ui.loaded', null, 'Loaded ✓') },
{ label: 'Registration Scheme', value: 'VerstakPluginRegister' }, { key: 'registration', label: 'Registration Scheme', value: 'VerstakPluginRegister' },
{ label: 'Components', value: 'DiagnosticsPanel, PlatformTestSettings' }, { key: 'components', label: 'Components', value: 'DiagnosticsPanel, PlatformTestSettings' },
{ label: 'Container', value: containerEl.tagName.toLowerCase() + (containerEl.id ? '#' + containerEl.id : '') }, { key: 'container', label: 'Container', value: containerEl.tagName.toLowerCase() + (containerEl.id ? '#' + containerEl.id : '') },
]; ];
infoItems.forEach(function (item) { infoItems.forEach(function (item) {
infoList.appendChild( infoList.appendChild(
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', item.label), localized('span', { className: 'pt-list-label' }, 'ui.info.' + item.key, item.label),
span('pt-list-value', item.value), span('pt-list-value', item.value),
]) ])
); );
@ -469,7 +484,7 @@
apiStatusList.appendChild( apiStatusList.appendChild(
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
el('span', { className: 'pt-list-label' }, [dot, ' ', chk.label]), el('span', { className: 'pt-list-label' }, [dot, ' ', chk.label]),
span('pt-list-value', chk.ok ? '✓ Ready' : '✗ Missing'), localized('span', { className: 'pt-list-value' }, chk.ok ? 'ui.ready' : 'ui.missing', chk.ok ? '✓ Ready' : '✗ Missing'),
]) ])
); );
}); });
@ -506,7 +521,7 @@
function renderMouseLog() { function renderMouseLog() {
if (mouseLog.length === 0) { if (mouseLog.length === 0) {
mouseLogContainer.textContent = '(no events captured)'; setLocalized(mouseLogContainer, 'ui.noMouseEvents', null, '(no events captured)');
mouseCountSpan.textContent = '0'; mouseCountSpan.textContent = '0';
return; return;
} }
@ -516,6 +531,7 @@
' defaultPrevented=' + e.defaultPrevented + ' defaultPrevented=' + e.defaultPrevented +
' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : ''); ' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : '');
}); });
if (mouseLogContainer.__ptLocalized) mouseLogContainer.__ptLocalized.key = '';
mouseLogContainer.textContent = lines.join('\n'); mouseLogContainer.textContent = lines.join('\n');
mouseCountSpan.textContent = String(mouseLog.length); mouseCountSpan.textContent = String(mouseLog.length);
mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight; mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight;
@ -545,7 +561,7 @@
window.addEventListener(type, handler, true); window.addEventListener(type, handler, true);
mouseHandlers.push({ type: type, handler: handler }); mouseHandlers.push({ type: type, handler: handler });
}); });
startStopBtn.textContent = '■ Stop Capture'; setLocalized(startStopBtn, 'ui.stopCapture', null, '■ Stop Capture');
startStopBtn.setAttribute('data-mouse-capturing', 'true'); startStopBtn.setAttribute('data-mouse-capturing', 'true');
renderMouseLog(); renderMouseLog();
} }
@ -557,35 +573,35 @@
window.removeEventListener(h.type, h.handler, true); window.removeEventListener(h.type, h.handler, true);
}); });
mouseHandlers = []; mouseHandlers = [];
startStopBtn.textContent = '▶ Start Capture'; setLocalized(startStopBtn, 'ui.startCapture', null, '▶ Start Capture');
startStopBtn.setAttribute('data-mouse-capturing', 'false'); startStopBtn.setAttribute('data-mouse-capturing', 'false');
} }
var startStopBtn = el('button', { var startStopBtn = localized('button', {
className: 'btn btn-primary', className: 'btn btn-primary',
style: { marginRight: '0.5rem' }, style: { marginRight: '0.5rem' },
onClick: function () { onClick: function () {
if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); } if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); }
}, },
}, ['▶ Start Capture']); }, 'ui.startCapture', '▶ Start Capture');
var clearBtn = el('button', { var clearBtn = localized('button', {
className: 'btn btn-secondary', className: 'btn btn-secondary',
style: { marginRight: '0.5rem' }, style: { marginRight: '0.5rem' },
onClick: function () { onClick: function () {
mouseLog = []; mouseLog = [];
renderMouseLog(); renderMouseLog();
}, },
}, ['Clear']); }, 'ui.clear', 'Clear');
var copyBtn = el('button', { var copyBtn = localized('button', {
className: 'btn btn-secondary', className: 'btn btn-secondary',
onClick: function () { onClick: function () {
var json = JSON.stringify(mouseLog, null, 2); var json = JSON.stringify(mouseLog, null, 2);
if (navigator.clipboard) { if (navigator.clipboard) {
navigator.clipboard.writeText(json).then(function () { navigator.clipboard.writeText(json).then(function () {
copyBtn.textContent = '✓ Copied!'; setLocalized(copyBtn, 'ui.copied', null, '✓ Copied!');
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); setTimeout(function () { setLocalized(copyBtn, 'ui.copyJson', null, 'Copy JSON'); }, 1500);
}); });
} else { } else {
var ta = document.createElement('textarea'); var ta = document.createElement('textarea');
@ -594,24 +610,22 @@
ta.select(); ta.select();
document.execCommand('copy'); document.execCommand('copy');
document.body.removeChild(ta); document.body.removeChild(ta);
copyBtn.textContent = '✓ Copied!'; setLocalized(copyBtn, 'ui.copied', null, '✓ Copied!');
setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); setTimeout(function () { setLocalized(copyBtn, 'ui.copyJson', null, 'Copy JSON'); }, 1500);
} }
}, },
}, ['Copy JSON']); }, 'ui.copyJson', 'Copy JSON');
trackCleanup(function () { stopMouseCapture(); }); trackCleanup(function () { stopMouseCapture(); });
var mouseCard = div('pt-card', [ var mouseCard = div('pt-card', [
localized('h3', { className: 'pt-card-title' }, 'ui.mouseInspector', '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' } }, [ localized('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, 'ui.mouseHint', 'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.'),
'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.',
]),
el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [ el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [
startStopBtn, startStopBtn,
clearBtn, clearBtn,
copyBtn, copyBtn,
span('pt-list-label', ' Events:'), localized('span', { className: 'pt-list-label' }, 'ui.eventsCount', 'Events:'),
mouseCountSpan, mouseCountSpan,
]), ]),
mouseLogContainer, mouseLogContainer,
@ -722,17 +736,13 @@
/* ── Info card ──────────────────────────────────────────────── */ /* ── Info card ──────────────────────────────────────────────── */
var infoCard = div('pt-card', [ var infoCard = div('pt-card', [
el('p', { style: { margin: '0', color: '#a0a0b8', fontSize: '0.85rem' } }, [ localized('p', { style: { margin: '0', color: '#a0a0b8', fontSize: '0.85rem' } }, 'ui.settingsInfo', 'Settings panel loaded from the plugin frontend bundle through the VerstakPluginRegister contract.'),
'Settings panel loaded from plugin frontend bundle via ',
el('code', { style: { background: '#1a1a2e', padding: '0.1rem 0.3rem', borderRadius: '3px', color: '#4ecca3' } }, ['VerstakPluginRegister']),
' contract.',
]),
]); ]);
/* ── Counter section ────────────────────────────────────────── */ /* ── Counter section ────────────────────────────────────────── */
var counterDisplay = div('pt-counter', [ var counterDisplay = div('pt-counter', [
el('span', { className: 'pt-counter-value' }, [String(counterState.value)]), el('span', { className: 'pt-counter-value' }, [String(counterState.value)]),
span('pt-counter-label', 'clicks (session only, no persistence)'), localized('span', { className: 'pt-counter-label' }, 'ui.counterClicks', 'clicks (session only, no persistence)'),
]); ]);
var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () { var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () {
@ -758,24 +768,22 @@
localized('h3', { className: 'pt-card-title' }, 'ui.counter', 'Interactive Counter (Local State)'), localized('h3', { className: 'pt-card-title' }, 'ui.counter', 'Interactive Counter (Local State)'),
counterDisplay, counterDisplay,
btnGroup, btnGroup,
el('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [ localized('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, 'ui.counterHint', 'This counter is a local demo. State is not persisted — refreshing resets it.'),
'This counter is a local demo. State is not persisted — refreshing resets it.',
]),
]); ]);
/* ── Settings stub ─────────────────────────────────────────── */ /* ── Settings stub ─────────────────────────────────────────── */
var settingsDemoList = el('ul', { className: 'pt-list' }); var settingsDemoList = el('ul', { className: 'pt-list' });
var settingsItems = [ var settingsItems = [
{ label: 'Auto-run on load', value: 'true' }, { key: 'autoRun', label: 'Auto-run on load', valueKey: 'true', value: 'true' },
{ label: 'Verbose logging', value: 'false' }, { key: 'verbose', label: 'Verbose logging', valueKey: 'false', value: 'false' },
{ label: 'Theme override', value: 'inherit' }, { key: 'theme', label: 'Theme override', valueKey: 'inherit', value: 'inherit' },
{ label: 'Notifications', value: 'enabled' }, { key: 'notifications', label: 'Notifications', valueKey: 'enabled', value: 'enabled' },
]; ];
settingsItems.forEach(function (s) { settingsItems.forEach(function (s) {
settingsDemoList.appendChild( settingsDemoList.appendChild(
el('li', { className: 'pt-list-item' }, [ el('li', { className: 'pt-list-item' }, [
span('pt-list-label', s.label), localized('span', { className: 'pt-list-label' }, 'ui.setting.' + s.key, s.label),
span('pt-list-value', s.value), localized('span', { className: 'pt-list-value' }, 'ui.value.' + s.valueKey, s.value),
]) ])
); );
}); });
@ -783,9 +791,7 @@
var settingsCard = div('pt-card', [ var settingsCard = div('pt-card', [
localized('h3', { className: 'pt-card-title' }, 'ui.demoSettings', 'Plugin Settings (Demo)'), localized('h3', { className: 'pt-card-title' }, 'ui.demoSettings', 'Plugin Settings (Demo)'),
settingsDemoList, settingsDemoList,
el('p', { style: { marginTop: '0.5rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [ localized('p', { style: { marginTop: '0.5rem', color: '#6c6c8a', fontSize: '0.7rem' } }, 'ui.settingsHint', 'Use api.settings.read() / api.settings.write() for persisted settings.'),
'Use api.settings.read() / api.settings.write() for persisted settings.',
]),
]); ]);
/* ── Assemble ──────────────────────────────────────────────── */ /* ── Assemble ──────────────────────────────────────────────── */

View File

@ -21,5 +21,91 @@
"ui.decrement": " Decrement", "ui.decrement": " Decrement",
"ui.reset": "Reset", "ui.reset": "Reset",
"ui.counter": "Interactive Counter", "ui.counter": "Interactive Counter",
"ui.demoSettings": "Demo Settings" "ui.demoSettings": "Demo Settings",
"ui.savedLoading": "Saved setting: loading...",
"ui.capabilitiesLoading": "Capabilities: loading...",
"ui.commandRegistering": "Command: registering...",
"ui.eventSubscribing": "Event: subscribing...",
"ui.filesRunning": "Files: running...",
"ui.filesPathChecking": "Files error path: checking...",
"ui.workbenchReady": "Workbench: ready",
"ui.workbenchOpening": "Workbench: opening...",
"ui.workbenchOpened": "Workbench: opened {path} with {provider}",
"ui.workbenchError": "Workbench error: {error}",
"ui.openTextDiagnostic": "Open Text Diagnostic",
"ui.openMarkdownDiagnostic": "Open Markdown Diagnostic",
"ui.openNotesDiagnostic": "Open Notes Diagnostic",
"ui.savedSetting": "Saved setting",
"ui.saving": "Saving...",
"ui.savedSettingValue": "Saved setting: {value}",
"ui.saved": "Saved",
"ui.errorValue": "Error: {error}",
"ui.saveSetting": "Save Setting",
"ui.settingsError": "Settings error: {error}",
"ui.capabilitiesAvailable": "Capabilities: {count} available",
"ui.capabilitiesError": "Capabilities error: {error}",
"ui.bundleCapabilityAvailable": "Frontend Bundle Loaded | capability available",
"ui.bundleCapabilityMissing": "Frontend Bundle Loaded | capability missing",
"ui.capabilityError": "Capability error: {error}",
"ui.commandResult": "Command: {status} {version} from {source}",
"ui.commandError": "Command error: {error}",
"ui.eventReceived": "Event: received {message}",
"ui.eventError": "Event error: {error}",
"ui.filesPassed": "Files: wrote/read/listed/moved/trashed",
"ui.filesError": "Files error: {error}",
"ui.filesPathAllowed": "Files error path: unexpectedly allowed",
"ui.filesPathWrongError": "Files error path: wrong error {error}",
"ui.filesPathRejected": "Files error path: rejected reserved-path",
"ui.persistedSetting": "Persisted setting",
"ui.newValue": "New value",
"ui.capabilitiesLabel": "Capabilities",
"ui.commandRuntime": "Command runtime",
"ui.eventRuntime": "Event runtime",
"ui.filesRuntime": "Files runtime",
"ui.filesReservedPath": "Files reserved path",
"ui.workbenchRouting": "Workbench routing",
"ui.passed": "Passed",
"ui.failed": "Failed",
"ui.total": "Total",
"ui.successRate": "Success Rate",
"ui.checking": "Checking...",
"ui.available": "✓ Available",
"ui.unavailable": "— Unavailable",
"ui.error": "Error",
"ui.ready": "✓ Ready",
"ui.missing": "✗ Missing",
"ui.loaded": "Loaded ✓",
"ui.info.pluginId": "Plugin ID",
"ui.info.bundleStatus": "Bundle Status",
"ui.info.registration": "Registration Scheme",
"ui.info.components": "Components",
"ui.info.container": "Container",
"ui.test.registration": "Plugin Registration",
"ui.test.platformCapability": "Capability: verstak/platform-test/v1",
"ui.test.diagnosticsCapability": "Capability: verstak/diagnostics/v1",
"ui.test.filesCapability": "Capability: verstak/core/files/v1",
"ui.test.workbenchCapability": "Capability: verstak/core/workbench/v1",
"ui.test.apiContract": "API Contract Compliance",
"ui.pass": "✓ PASS",
"ui.fail": "✗ FAIL",
"ui.noMouseEvents": "(no events captured)",
"ui.stopCapture": "■ Stop Capture",
"ui.startCapture": "▶ Start Capture",
"ui.clear": "Clear",
"ui.copyJson": "Copy JSON",
"ui.copied": "✓ Copied!",
"ui.mouseHint": "Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.",
"ui.eventsCount": "Events:",
"ui.settingsInfo": "Settings panel loaded from the plugin frontend bundle through the VerstakPluginRegister contract.",
"ui.counterClicks": "clicks (session only, no persistence)",
"ui.counterHint": "This counter is a local demo. State is not persisted — refreshing resets it.",
"ui.setting.autoRun": "Auto-run on load",
"ui.setting.verbose": "Verbose logging",
"ui.setting.theme": "Theme override",
"ui.setting.notifications": "Notifications",
"ui.value.true": "true",
"ui.value.false": "false",
"ui.value.inherit": "inherit",
"ui.value.enabled": "enabled",
"ui.settingsHint": "Use api.settings.read() / api.settings.write() for persisted settings."
} }

View File

@ -21,5 +21,91 @@
"ui.decrement": " Уменьшить", "ui.decrement": " Уменьшить",
"ui.reset": "Сбросить", "ui.reset": "Сбросить",
"ui.counter": "Интерактивный счётчик", "ui.counter": "Интерактивный счётчик",
"ui.demoSettings": "Демонстрационные настройки" "ui.demoSettings": "Демонстрационные настройки",
"ui.savedLoading": "Загрузка сохранённой настройки...",
"ui.capabilitiesLoading": "Загрузка возможностей...",
"ui.commandRegistering": "Регистрация команды...",
"ui.eventSubscribing": "Подписка на событие...",
"ui.filesRunning": "Проверка файлов...",
"ui.filesPathChecking": "Проверка запрещённого пути...",
"ui.workbenchReady": "Workbench готов",
"ui.workbenchOpening": "Открытие Workbench...",
"ui.workbenchOpened": "Workbench открыл {path} через {provider}",
"ui.workbenchError": "Ошибка Workbench: {error}",
"ui.openTextDiagnostic": "Открыть текстовую диагностику",
"ui.openMarkdownDiagnostic": "Открыть диагностику Markdown",
"ui.openNotesDiagnostic": "Открыть диагностику заметок",
"ui.savedSetting": "Сохранённая настройка",
"ui.saving": "Сохранение...",
"ui.savedSettingValue": "Сохранённая настройка: {value}",
"ui.saved": "Сохранено",
"ui.errorValue": "Ошибка: {error}",
"ui.saveSetting": "Сохранить настройку",
"ui.settingsError": "Ошибка настроек: {error}",
"ui.capabilitiesAvailable": "Доступно возможностей: {count}",
"ui.capabilitiesError": "Ошибка возможностей: {error}",
"ui.bundleCapabilityAvailable": "Frontend bundle загружен | capability доступна",
"ui.bundleCapabilityMissing": "Frontend bundle загружен | capability отсутствует",
"ui.capabilityError": "Ошибка capability: {error}",
"ui.commandResult": "Команда: {status} {version}, источник: {source}",
"ui.commandError": "Ошибка команды: {error}",
"ui.eventReceived": "Получено событие: {message}",
"ui.eventError": "Ошибка события: {error}",
"ui.filesPassed": "Файлы: запись/чтение/список/перемещение/удаление выполнены",
"ui.filesError": "Ошибка файлов: {error}",
"ui.filesPathAllowed": "Ошибка: запрещённый путь неожиданно разрешён",
"ui.filesPathWrongError": "Неверная ошибка запрещённого пути: {error}",
"ui.filesPathRejected": "Зарезервированный путь отклонён",
"ui.persistedSetting": "Сохранённая настройка",
"ui.newValue": "Новое значение",
"ui.capabilitiesLabel": "Возможности",
"ui.commandRuntime": "Выполнение команд",
"ui.eventRuntime": "Обработка событий",
"ui.filesRuntime": "Работа с файлами",
"ui.filesReservedPath": "Зарезервированный путь",
"ui.workbenchRouting": "Маршрутизация Workbench",
"ui.passed": "Пройдено",
"ui.failed": "Ошибок",
"ui.total": "Всего",
"ui.successRate": "Успешность",
"ui.checking": "Проверка...",
"ui.available": "✓ Доступно",
"ui.unavailable": "— Недоступно",
"ui.error": "Ошибка",
"ui.ready": "✓ Готово",
"ui.missing": "✗ Отсутствует",
"ui.loaded": "Загружено ✓",
"ui.info.pluginId": "ID плагина",
"ui.info.bundleStatus": "Состояние bundle",
"ui.info.registration": "Схема регистрации",
"ui.info.components": "Компоненты",
"ui.info.container": "Контейнер",
"ui.test.registration": "Регистрация плагина",
"ui.test.platformCapability": "Capability: verstak/platform-test/v1",
"ui.test.diagnosticsCapability": "Capability: verstak/diagnostics/v1",
"ui.test.filesCapability": "Capability: verstak/core/files/v1",
"ui.test.workbenchCapability": "Capability: verstak/core/workbench/v1",
"ui.test.apiContract": "Соответствие контракту API",
"ui.pass": "✓ ПРОЙДЕНО",
"ui.fail": "✗ ОШИБКА",
"ui.noMouseEvents": "(события не зафиксированы)",
"ui.stopCapture": "■ Остановить запись",
"ui.startCapture": "▶ Начать запись",
"ui.clear": "Очистить",
"ui.copyJson": "Копировать JSON",
"ui.copied": "✓ Скопировано!",
"ui.mouseHint": "Записывает все события мыши и указателя в окне. Нажмите кнопки назад/вперёд, чтобы увидеть события WebKitGTK.",
"ui.eventsCount": "События:",
"ui.settingsInfo": "Панель настроек загружена из frontend bundle плагина через контракт VerstakPluginRegister.",
"ui.counterClicks": "нажатий (только в текущем сеансе)",
"ui.counterHint": "Это локальный демонстрационный счётчик. После обновления его значение сбрасывается.",
"ui.setting.autoRun": "Автозапуск при загрузке",
"ui.setting.verbose": "Подробный журнал",
"ui.setting.theme": "Переопределение темы",
"ui.setting.notifications": "Уведомления",
"ui.value.true": "да",
"ui.value.false": "нет",
"ui.value.inherit": "наследовать",
"ui.value.enabled": "включены",
"ui.settingsHint": "Для постоянных настроек используйте api.settings.read() / api.settings.write()."
} }

View File

@ -6,6 +6,10 @@ const vm = require('vm');
const root = path.resolve(__dirname, '..'); const root = path.resolve(__dirname, '..');
const bundlePath = path.join(root, 'plugins', 'platform-test', 'frontend', 'src', 'index.js'); const bundlePath = path.join(root, 'plugins', 'platform-test', 'frontend', 'src', 'index.js');
const source = fs.readFileSync(bundlePath, 'utf8'); const source = fs.readFileSync(bundlePath, 'utf8');
const platformCatalogs = {
en: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'platform-test', 'locales', 'en.json'), 'utf8')),
ru: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'platform-test', 'locales', 'ru.json'), 'utf8')),
};
class FakeNode { class FakeNode {
constructor(tagName) { constructor(tagName) {
@ -46,6 +50,10 @@ function findByClass(node, className) {
return null; return null;
} }
function allText(node) {
return [String(node.textContent || '')].concat((node.children || []).map(allText)).join(' ');
}
function makeDocument() { function makeDocument() {
return { return {
head: new FakeNode('head'), head: new FakeNode('head'),
@ -88,10 +96,7 @@ const api = {
i18n: { i18n: {
locale: 'en', locale: 'en',
listeners: new Set(), listeners: new Set(),
messages: { messages: platformCatalogs,
en: { 'ui.diagnostics': 'Platform Diagnostics', 'ui.settings': 'Platform Test Settings' },
ru: { 'ui.diagnostics': 'Диагностика платформы', 'ui.settings': 'Настройки теста платформы' },
},
getLocale() { return this.locale; }, getLocale() { return this.locale; },
t(key, params, fallback) { return this.messages[this.locale][key] || fallback || key; }, t(key, params, fallback) { return this.messages[this.locale][key] || fallback || key; },
onDidChangeLocale(listener) { onDidChangeLocale(listener) {
@ -212,6 +217,10 @@ const api = {
if (findByClass(container, 'pt-plugin-name')?.textContent !== 'Диагностика платформы') { if (findByClass(container, 'pt-plugin-name')?.textContent !== 'Диагностика платформы') {
throw new Error('DiagnosticsPanel did not update after locale change'); throw new Error('DiagnosticsPanel did not update after locale change');
} }
const diagnosticsText = allText(container);
if (!diagnosticsText.includes('Сохранённая настройка') || !diagnosticsText.includes('Пройдено')) {
throw new Error('DiagnosticsPanel left primary diagnostic chrome untranslated');
}
} }
if (name === 'PlatformTestSettings') { if (name === 'PlatformTestSettings') {
const counter = findByClass(container, 'pt-counter-value'); const counter = findByClass(container, 'pt-counter-value');
@ -223,6 +232,10 @@ const api = {
if (counter.textContent !== '3') { if (counter.textContent !== '3') {
throw new Error('PlatformTestSettings lost counter state after locale change'); throw new Error('PlatformTestSettings lost counter state after locale change');
} }
const settingsText = allText(container);
if (!settingsText.includes('Панель настроек загружена') || !settingsText.includes('нажатий')) {
throw new Error('PlatformTestSettings left primary settings copy untranslated');
}
} }
if (typeof component.unmount === 'function') { if (typeof component.unmount === 'function') {
component.unmount(container); component.unmount(container);