diff --git a/plugins/activity/frontend/src/index.js b/plugins/activity/frontend/src/index.js index 0ab63a4..a720189 100644 --- a/plugins/activity/frontend/src/index.js +++ b/plugins/activity/frontend/src/index.js @@ -409,19 +409,23 @@ var candidateSourceEvents = []; var candidates = []; var dismissedByWorkspace = {}; - var statusText = 'Loading activity...'; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + var statusText = tr('ui.loading', null, 'Loading activity...'); var statusClass = ''; var disposed = false; var unsubscribers = []; var toolbar = el('div', { className: 'activity-toolbar' }); - var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? 'Activity' : 'Activity · ' + scope.label }); + var titleEl = el('span', { className: 'activity-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label) }); var countEl = el('span', { className: 'activity-count' }); var statusEl = el('span', { className: 'activity-status' }); var clearBtn = el('button', { className: 'activity-btn danger', 'data-activity-action': 'clear', - textContent: 'Clear', + textContent: tr('ui.clear', null, 'Clear'), onClick: function () { if (scope.mode === 'global') { clearGlobal().then(render); @@ -489,7 +493,7 @@ ? events.filter(function (item) { return !item._storageKey || item._storageKey === GLOBAL_KEY; }) : events; return api.settings.write(scope.key, storageEvents(toStore)).then(persistCandidateCaches).catch(function (err) { - statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err)); + statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save activity: ' + (err && err.message ? err.message : String(err))); statusClass = 'error'; }); } @@ -513,10 +517,10 @@ return api.settings.write(key, []); })); }).then(function () { - statusText = 'Activity cleared'; + statusText = tr('ui.cleared', null, 'Activity cleared'); statusClass = ''; }).catch(function (err) { - statusText = 'Could not clear activity: ' + (err && err.message ? err.message : String(err)); + statusText = tr('ui.clearError', { error: err && err.message ? err.message : String(err) }, 'Could not clear activity: ' + (err && err.message ? err.message : String(err))); statusClass = 'error'; }); } @@ -525,8 +529,8 @@ listEl.innerHTML = ''; if (events.length === 0) { listEl.appendChild(el('div', { className: 'activity-empty' }, [ - el('div', { className: 'activity-empty-title', textContent: 'No activity events yet' }), - el('div', { textContent: 'File changes, browser captures, and conversions will appear here.' }) + el('div', { className: 'activity-empty-title', textContent: tr('ui.empty', null, 'No activity events yet') }), + el('div', { textContent: tr('ui.emptyHint', null, 'File changes, browser captures, and conversions will appear here.') }) ])); return; } @@ -543,8 +547,8 @@ ]), activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null, el('details', { className: 'activity-details' }, [ - el('summary', {}, ['Details']), - el('div', { className: 'activity-source', textContent: 'Event: ' + activity.type + (activity.sourcePluginId ? ' · Source: ' + activity.sourcePluginId : '') }) + el('summary', {}, [tr('ui.details', null, 'Details')]), + el('div', { className: 'activity-source', textContent: tr('ui.eventSource', { event: activity.type, source: activity.sourcePluginId ? ' · ' + activity.sourcePluginId : '' }, 'Event: ' + activity.type + (activity.sourcePluginId ? ' · Source: ' + activity.sourcePluginId : '')) }) ]) ]) ])); @@ -574,7 +578,7 @@ persistDismissals(candidate.workspaceRootPath), persistCandidateCache(candidate.workspaceRootPath) ]).then(function () { - statusText = 'Candidate dismissed'; + statusText = tr('ui.dismissed', null, 'Candidate dismissed'); statusClass = ''; }).catch(function (err) { statusText = 'Could not dismiss candidate: ' + (err && err.message ? err.message : String(err)); @@ -590,14 +594,14 @@ } if (typeof candidatesEl.removeAttribute === 'function') candidatesEl.removeAttribute('hidden'); else delete candidatesEl.attributes.hidden; - candidatesEl.appendChild(el('div', { className: 'activity-candidates-title', textContent: 'Possible journal entries' })); + candidatesEl.appendChild(el('div', { className: 'activity-candidates-title', textContent: tr('ui.candidates', null, 'Possible journal entries') })); candidates.forEach(function (candidate) { candidatesEl.appendChild(el('div', { className: 'activity-candidate', 'data-work-session-candidate': candidate.candidateId }, [ el('div', {}, [ - el('div', { className: 'activity-candidate-title', textContent: 'Possible journal entry' }), + el('div', { className: 'activity-candidate-title', textContent: tr('ui.candidate', null, 'Possible journal entry') }), el('div', { className: 'activity-candidate-facts' }, [ el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }), el('div', { textContent: 'Time: ' + candidateTimeRange(candidate) }), @@ -612,8 +616,8 @@ ]), el('div', { className: 'activity-candidate-actions' }, [ el('div', { className: 'activity-candidate-duration', textContent: candidate.estimatedMinutes + ' min' }), - el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: 'Review', onClick: function () { reviewCandidate(candidate); } }), - el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: 'Dismiss', onClick: function () { dismissCandidate(candidate); } }) + el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'review', textContent: tr('ui.review', null, 'Review'), onClick: function () { reviewCandidate(candidate); } }), + el('button', { className: 'activity-btn', type: 'button', 'data-work-session-action': 'dismiss', textContent: tr('ui.dismiss', null, 'Dismiss'), onClick: function () { dismissCandidate(candidate); } }) ]) ])); }); @@ -696,6 +700,13 @@ }).then(function () { if (!disposed) render(); }); + if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + api.i18n.onDidChangeLocale(function () { + titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Activity') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Activity · ' + scope.label); + clearBtn.textContent = tr('ui.clear', null, 'Clear'); + render(); + }); + } containerEl.__activityUnmount = function () { disposed = true; diff --git a/plugins/activity/locales/en.json b/plugins/activity/locales/en.json new file mode 100644 index 0000000..55068c7 --- /dev/null +++ b/plugins/activity/locales/en.json @@ -0,0 +1,24 @@ +{ + "manifest.name": "Activity", + "manifest.description": "Workspace-scoped activity log for public plugin events.", + "contributions.views.verstak.activity.view.title": "Activity", + "contributions.sidebarItems.verstak.activity.sidebar.title": "Activity", + "contributions.workspaceItems.verstak.activity.workspace.title": "Activity", + "contributions.commands.verstak.activity.suggestWorklog.title": "List Possible Journal Entries", + "ui.loading": "Loading activity...", + "ui.title": "Activity", + "ui.workspaceTitle": "Activity · {workspace}", + "ui.clear": "Clear", + "ui.saveError": "Could not save activity: {error}", + "ui.cleared": "Activity cleared", + "ui.clearError": "Could not clear activity: {error}", + "ui.empty": "No activity events yet", + "ui.emptyHint": "File changes, browser captures, and conversions will appear here.", + "ui.details": "Details", + "ui.eventSource": "Event: {event}{source}", + "ui.dismissed": "Candidate dismissed", + "ui.candidates": "Possible journal entries", + "ui.candidate": "Possible journal entry", + "ui.review": "Review", + "ui.dismiss": "Dismiss" +} diff --git a/plugins/activity/locales/ru.json b/plugins/activity/locales/ru.json new file mode 100644 index 0000000..bc300a6 --- /dev/null +++ b/plugins/activity/locales/ru.json @@ -0,0 +1,24 @@ +{ + "manifest.name": "Активность", + "manifest.description": "Журнал публичных событий плагинов в рабочем пространстве.", + "contributions.views.verstak.activity.view.title": "Активность", + "contributions.sidebarItems.verstak.activity.sidebar.title": "Активность", + "contributions.workspaceItems.verstak.activity.workspace.title": "Активность", + "contributions.commands.verstak.activity.suggestWorklog.title": "Показать возможные записи журнала", + "ui.loading": "Загрузка активности...", + "ui.title": "Активность", + "ui.workspaceTitle": "Активность · {workspace}", + "ui.clear": "Очистить", + "ui.saveError": "Не удалось сохранить активность: {error}", + "ui.cleared": "Активность очищена", + "ui.clearError": "Не удалось очистить активность: {error}", + "ui.empty": "Событий активности пока нет", + "ui.emptyHint": "Здесь появятся изменения файлов, материалы из браузера и преобразования.", + "ui.details": "Подробнее", + "ui.eventSource": "Событие: {event}{source}", + "ui.dismissed": "Кандидат скрыт", + "ui.candidates": "Возможные записи журнала", + "ui.candidate": "Возможная запись журнала", + "ui.review": "Проверить", + "ui.dismiss": "Скрыть" +} diff --git a/plugins/activity/plugin.json b/plugins/activity/plugin.json index a03314f..917efd5 100644 --- a/plugins/activity/plugin.json +++ b/plugins/activity/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Workspace-scoped activity log for public plugin events.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "activity", "provides": [ diff --git a/plugins/browser-inbox/frontend/src/index.js b/plugins/browser-inbox/frontend/src/index.js index e6ab2c4..afa36c6 100644 --- a/plugins/browser-inbox/frontend/src/index.js +++ b/plugins/browser-inbox/frontend/src/index.js @@ -354,9 +354,14 @@ var statusFilter = 'all'; var workspaceFilter = ''; var searchQuery = ''; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + statusText = tr('ui.connecting', null, 'Connecting to receiver events...'); var toolbar = el('div', { className: 'browser-inbox-toolbar' }); - var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? 'Browser Inbox' : 'Browser Inbox · ' + scope.label }); + var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label) }); var countEl = el('span', { className: 'browser-inbox-count' }); var statusEl = el('span', { className: 'browser-inbox-status' }); var filtersEl = el('div', { className: 'browser-inbox-filters' }); @@ -370,10 +375,10 @@ render(); } }, [ - el('option', { value: 'all', textContent: 'All captures' }), - el('option', { value: 'unassigned', textContent: 'Unassigned' }), - el('option', { value: 'unprocessed', textContent: 'Unprocessed' }), - el('option', { value: 'processed', textContent: 'Processed' }) + el('option', { value: 'all', textContent: tr('ui.allCaptures', null, 'All captures') }), + el('option', { value: 'unassigned', textContent: tr('ui.unassigned', null, 'Unassigned') }), + el('option', { value: 'unprocessed', textContent: tr('ui.unprocessed', null, 'Unprocessed') }), + el('option', { value: 'processed', textContent: tr('ui.processed', null, 'Processed') }) ]); var workspaceFilterEl = el('select', { className: 'browser-inbox-select', @@ -388,7 +393,7 @@ var searchInput = el('input', { className: 'browser-inbox-input', type: 'search', - placeholder: 'Search captures', + placeholder: tr('ui.search', null, 'Search captures'), 'data-browser-inbox-filter': 'search', 'aria-label': 'Search captures', onInput: function (event) { @@ -402,7 +407,7 @@ var clearBtn = el('button', { className: 'browser-inbox-btn danger', 'data-browser-inbox-action': 'clear', - textContent: 'Clear', + textContent: tr('ui.clear', null, 'Clear'), onClick: function () { clearScope().then(render); } @@ -413,7 +418,7 @@ if (scope.mode === 'global') { filtersEl.appendChild(workspaceFilterEl); } else { - filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: 'Assigned to this workspace' })); + filtersEl.appendChild(el('span', { className: 'browser-inbox-count', textContent: tr('ui.assignedHere', null, 'Assigned to this workspace') })); } filtersEl.appendChild(searchInput); toolbar.appendChild(filtersEl); @@ -748,25 +753,25 @@ detailEl.innerHTML = ''; var capture = selectedCapture(); if (!capture) { - detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: 'Select a capture to inspect it.' })); + detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: tr('ui.selectCapture', null, 'Select a capture to inspect it.') })); return; } selectedId = capture.captureId; detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture) })); detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [ - el('div', { className: 'browser-inbox-meta-label', textContent: 'Kind' }), + el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.kind', null, 'Kind') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.kind }), el('div', { className: 'browser-inbox-meta-label', textContent: 'URL' }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }), - el('div', { className: 'browser-inbox-meta-label', textContent: 'Domain' }), + el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.domain', null, 'Domain') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }), - el('div', { className: 'browser-inbox-meta-label', textContent: 'Captured' }), + el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.captured', null, 'Captured') }), el('div', { className: 'browser-inbox-meta-value', textContent: formatDate(capture.capturedAt) || '-' }), el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }), - el('div', { className: 'browser-inbox-meta-label', textContent: 'Workspace' }), + el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.workspace', null, 'Workspace') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.workspaceRootPath || 'Unassigned' }), - el('div', { className: 'browser-inbox-meta-label', textContent: 'Status' }), + el('div', { className: 'browser-inbox-meta-label', textContent: tr('ui.status', null, 'Status') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed ? 'Processed' : 'Unprocessed' }) ])); var assignmentSelect = el('select', { @@ -819,7 +824,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-note', - textContent: 'Create Note', + textContent: tr('ui.createNote', null, 'Create Note'), onClick: function () { createNoteFromCapture(capture); } @@ -828,7 +833,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-link', - textContent: 'Create Link', + textContent: tr('ui.createLink', null, 'Create Link'), onClick: function () { createLinkFromCapture(capture); } @@ -838,7 +843,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-file', - textContent: 'Create File', + textContent: tr('ui.createFile', null, 'Create File'), onClick: function () { createFileFromCapture(capture); } @@ -848,7 +853,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn danger', 'data-browser-inbox-action': 'remove', - textContent: 'Delete', + textContent: tr('ui.delete', null, 'Delete'), onClick: function () { removeCapture(capture.captureId); } @@ -941,6 +946,14 @@ }).then(function () { if (!disposed) render(); }); + if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + api.i18n.onDidChangeLocale(function () { + titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label); + searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures')); + clearBtn.textContent = tr('ui.clear', null, 'Clear'); + render(); + }); + } containerEl.__browserInboxUnmount = function () { disposed = true; @@ -967,6 +980,11 @@ containerEl.innerHTML = ''; containerEl.className = 'browser-inbox-settings'; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + var receiverURLInput = el('input', { className: 'browser-inbox-settings-input', type: 'text', @@ -988,27 +1006,27 @@ className: 'browser-inbox-btn', type: 'button', 'data-browser-inbox-settings-action': 'copy-url', - textContent: 'Copy URL' + textContent: tr('ui.copyUrl', null, 'Copy URL') }); var copyTokenButton = el('button', { className: 'browser-inbox-btn', type: 'button', 'data-browser-inbox-settings-action': 'copy-token', - textContent: 'Copy Token' + textContent: tr('ui.copyToken', null, 'Copy Token') }); var rotateTokenButton = el('button', { className: 'browser-inbox-btn danger', type: 'button', 'data-browser-inbox-settings-action': 'rotate-token', - textContent: 'Rotate Token' + textContent: tr('ui.rotateToken', null, 'Rotate Token') }); containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [ - el('label', { className: 'browser-inbox-settings-label', textContent: 'Receiver URL' }), + el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.receiverUrl', null, 'Receiver URL') }), receiverURLInput ])); containerEl.appendChild(el('div', { className: 'browser-inbox-settings-field' }, [ - el('label', { className: 'browser-inbox-settings-label', textContent: 'Pairing Token' }), + el('label', { className: 'browser-inbox-settings-label', textContent: tr('ui.pairingToken', null, 'Pairing Token') }), receiverTokenInput ])); containerEl.appendChild(el('div', { className: 'browser-inbox-settings-actions' }, [ @@ -1090,6 +1108,13 @@ }); loadPairing(); + if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + api.i18n.onDidChangeLocale(function () { + copyURLButton.textContent = tr('ui.copyUrl', null, 'Copy URL'); + copyTokenButton.textContent = tr('ui.copyToken', null, 'Copy Token'); + rotateTokenButton.textContent = tr('ui.rotateToken', null, 'Rotate Token'); + }); + } }, unmount: function (containerEl) { if (containerEl) containerEl.innerHTML = ''; diff --git a/plugins/browser-inbox/locales/en.json b/plugins/browser-inbox/locales/en.json new file mode 100644 index 0000000..303cc04 --- /dev/null +++ b/plugins/browser-inbox/locales/en.json @@ -0,0 +1,33 @@ +{ + "manifest.name": "Browser Inbox", + "manifest.description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.", + "contributions.views.verstak.browser-inbox.view.title": "Browser Inbox", + "contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Browser Inbox", + "contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Browser Inbox", + "contributions.settingsPanels.verstak.browser-inbox.settings.title": "Browser Inbox", + "ui.connecting": "Connecting to receiver events...", + "ui.title": "Browser Inbox", + "ui.workspaceTitle": "Browser Inbox · {workspace}", + "ui.allCaptures": "All captures", + "ui.unassigned": "Unassigned", + "ui.unprocessed": "Unprocessed", + "ui.processed": "Processed", + "ui.search": "Search captures", + "ui.clear": "Clear", + "ui.assignedHere": "Assigned to this workspace", + "ui.selectCapture": "Select a capture to inspect it.", + "ui.kind": "Kind", + "ui.domain": "Domain", + "ui.captured": "Captured", + "ui.workspace": "Workspace", + "ui.status": "Status", + "ui.createNote": "Create Note", + "ui.createLink": "Create Link", + "ui.createFile": "Create File", + "ui.delete": "Delete", + "ui.copyUrl": "Copy URL", + "ui.copyToken": "Copy Token", + "ui.rotateToken": "Rotate Token", + "ui.receiverUrl": "Receiver URL", + "ui.pairingToken": "Pairing Token" +} diff --git a/plugins/browser-inbox/locales/ru.json b/plugins/browser-inbox/locales/ru.json new file mode 100644 index 0000000..4c497b3 --- /dev/null +++ b/plugins/browser-inbox/locales/ru.json @@ -0,0 +1,33 @@ +{ + "manifest.name": "Входящие из браузера", + "manifest.description": "Общая очередь материалов из браузера с явным назначением рабочего пространства через локальный протокол приёма.", + "contributions.views.verstak.browser-inbox.view.title": "Входящие из браузера", + "contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Входящие из браузера", + "contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Входящие из браузера", + "contributions.settingsPanels.verstak.browser-inbox.settings.title": "Входящие из браузера", + "ui.connecting": "Подключение к событиям приёмника...", + "ui.title": "Входящие из браузера", + "ui.workspaceTitle": "Входящие из браузера · {workspace}", + "ui.allCaptures": "Все материалы", + "ui.unassigned": "Не назначено", + "ui.unprocessed": "Не обработано", + "ui.processed": "Обработано", + "ui.search": "Поиск материалов", + "ui.clear": "Очистить", + "ui.assignedHere": "Назначено этому рабочему пространству", + "ui.selectCapture": "Выберите материал для просмотра.", + "ui.kind": "Тип", + "ui.domain": "Домен", + "ui.captured": "Получено", + "ui.workspace": "Рабочее пространство", + "ui.status": "Состояние", + "ui.createNote": "Создать заметку", + "ui.createLink": "Создать ссылку", + "ui.createFile": "Создать файл", + "ui.delete": "Удалить", + "ui.copyUrl": "Копировать URL", + "ui.copyToken": "Копировать токен", + "ui.rotateToken": "Сменить токен", + "ui.receiverUrl": "URL приёмника", + "ui.pairingToken": "Токен сопряжения" +} diff --git a/plugins/browser-inbox/plugin.json b/plugins/browser-inbox/plugin.json index a6997c7..adc6586 100644 --- a/plugins/browser-inbox/plugin.json +++ b/plugins/browser-inbox/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "inbox", "provides": [ diff --git a/plugins/default-editor/frontend/src/index.js b/plugins/default-editor/frontend/src/index.js index ddcbcc1..47831c7 100644 --- a/plugins/default-editor/frontend/src/index.js +++ b/plugins/default-editor/frontend/src/index.js @@ -291,6 +291,10 @@ var linesEl = null; var previewEl = null; var secretLinksAvailable = false; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } containerEl.setAttribute('data-editor-mode', editorMode); containerEl.setAttribute('data-resource-path', resourcePath); @@ -298,13 +302,13 @@ var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]); var contextLabel = el('span', { className: 'de-toolbar-context', title: resourcePath }, [resourcePath || fileName(resourcePath)]); - var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, ['notes context']) : null; + var notesBadge = editorMode === 'notes-markdown' ? el('span', { className: 'de-notes-badge', 'data-notes-badge': '' }, [tr('ui.notesContext', null, 'notes context')]) : null; var spacer = el('span', { className: 'de-toolbar-spacer' }); - var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, ['Edit']) : null; - var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, ['Preview']) : null; - var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, ['Split']) : null; - var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, ['Reload']); - var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, ['Save']); + var editBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'edit' }, [tr('ui.edit', null, 'Edit')]) : null; + var previewBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'preview' }, [tr('ui.preview', null, 'Preview')]) : null; + var splitBtn = isMarkdown ? el('button', { className: 'de-toolbar-btn', 'data-editor-mode-button': 'split' }, [tr('ui.split', null, 'Split')]) : null; + var reloadBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'reload' }, [tr('ui.reload', null, 'Reload')]); + var saveBtn = el('button', { className: 'de-toolbar-btn', 'data-editor-action': 'save' }, [tr('ui.save', null, 'Save')]); var statusEl = el('span', { className: 'de-status', 'data-save-state': '' }); var toolbarChildren = [modeLabel, contextLabel]; if (notesBadge) toolbarChildren.push(notesBadge); @@ -336,7 +340,7 @@ containerEl.appendChild(editorWrap); if (editorMode === 'notes-markdown') { - containerEl.appendChild(el('div', { className: 'de-notes-info' }, ['Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.'])); + containerEl.appendChild(el('div', { className: 'de-notes-info' }, [tr('ui.notesInfo', null, 'Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.')])); } function updateLineNumbers() { @@ -349,16 +353,16 @@ function updateStatus() { if (saveState === 'saving') { - statusEl.textContent = 'Saving...'; + statusEl.textContent = tr('ui.saving', null, 'Saving...'); statusEl.className = 'de-status saving'; } else if (saveState === 'error') { - statusEl.textContent = 'Error saving'; + statusEl.textContent = tr('ui.saveError', null, 'Error saving'); statusEl.className = 'de-status error'; } else if (dirty) { - statusEl.textContent = 'Modified'; + statusEl.textContent = tr('ui.modified', null, 'Modified'); statusEl.className = 'de-status dirty'; } else if (lastSavedAt) { - statusEl.textContent = saveState === 'saved' ? 'Saved ' + lastSavedAt : 'Saved'; + statusEl.textContent = saveState === 'saved' ? tr('ui.savedAt', { time: lastSavedAt }, 'Saved ' + lastSavedAt) : tr('ui.saved', null, 'Saved'); statusEl.className = 'de-status saved'; } else { statusEl.textContent = ''; @@ -475,9 +479,9 @@ } function reloadFromDisk() { - if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return; + if (dirty && !window.confirm(tr('ui.discardConfirm', null, 'Discard unsaved changes and reload from disk?'))) return; editorWrap.innerHTML = ''; - editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...'])); + editorWrap.appendChild(el('div', { className: 'de-loading' }, [tr('ui.loading', null, 'Loading...')])); var readPromise = api.files.readText(resourcePath); readPromise.then(function (content) { if (disposed) return; @@ -490,7 +494,7 @@ if (disposed) return; editorWrap.innerHTML = ''; editorWrap.appendChild(el('div', { className: 'de-error' }, [ - el('div', {}, ['Failed to load file']), + el('div', {}, [tr('ui.loadFailed', null, 'Failed to load file')]), el('div', { className: 'de-error-msg' }, [(err && err.message) ? err.message : String(err)]) ])); }); @@ -534,6 +538,17 @@ loadSecretProviderAvailability(); reloadFromDisk(); + var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function' + ? api.i18n.onDidChangeLocale(function () { + if (notesBadge) notesBadge.textContent = tr('ui.notesContext', null, 'notes context'); + if (editBtn) editBtn.textContent = tr('ui.edit', null, 'Edit'); + if (previewBtn) previewBtn.textContent = tr('ui.preview', null, 'Preview'); + if (splitBtn) splitBtn.textContent = tr('ui.split', null, 'Split'); + reloadBtn.textContent = tr('ui.reload', null, 'Reload'); + saveBtn.textContent = tr('ui.save', null, 'Save'); + updateStatus(); + }) + : null; containerEl.addEventListener('click', function (event) { var secretLink = event.target.closest('.secret-link'); @@ -582,6 +597,7 @@ containerEl.__deCleanup = function () { disposed = true; + if (typeof localeUnsubscribe === 'function') localeUnsubscribe(); if (saveTimer) clearTimeout(saveTimer); }; }, diff --git a/plugins/default-editor/locales/en.json b/plugins/default-editor/locales/en.json new file mode 100644 index 0000000..4729b54 --- /dev/null +++ b/plugins/default-editor/locales/en.json @@ -0,0 +1,22 @@ +{ + "manifest.name": "Default Editor", + "manifest.description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.", + "contributions.openProviders.verstak.default-editor.text.title": "Default Text Editor", + "contributions.openProviders.verstak.default-editor.markdown.title": "Default Markdown Editor", + "contributions.openProviders.verstak.default-editor.notes-markdown.title": "Default Notes Markdown Editor", + "ui.notesContext": "notes context", + "ui.edit": "Edit", + "ui.preview": "Preview", + "ui.split": "Split", + "ui.reload": "Reload", + "ui.save": "Save", + "ui.notesInfo": "Notes context active. Note actions, backlinks, and graph tools are reserved for the future Notes plugin.", + "ui.saving": "Saving...", + "ui.saveError": "Error saving", + "ui.modified": "Modified", + "ui.savedAt": "Saved {time}", + "ui.saved": "Saved", + "ui.discardConfirm": "Discard unsaved changes and reload from disk?", + "ui.loading": "Loading...", + "ui.loadFailed": "Failed to load file" +} diff --git a/plugins/default-editor/locales/ru.json b/plugins/default-editor/locales/ru.json new file mode 100644 index 0000000..984a52c --- /dev/null +++ b/plugins/default-editor/locales/ru.json @@ -0,0 +1,22 @@ +{ + "manifest.name": "Стандартный редактор", + "manifest.description": "Встроенный редактор и просмотрщик текста и Markdown для Верстака.", + "contributions.openProviders.verstak.default-editor.text.title": "Стандартный текстовый редактор", + "contributions.openProviders.verstak.default-editor.markdown.title": "Стандартный редактор Markdown", + "contributions.openProviders.verstak.default-editor.notes-markdown.title": "Стандартный редактор Markdown-заметок", + "ui.notesContext": "контекст заметки", + "ui.edit": "Редактировать", + "ui.preview": "Просмотр", + "ui.split": "Разделить", + "ui.reload": "Перезагрузить", + "ui.save": "Сохранить", + "ui.notesInfo": "Активен контекст заметки. Действия, обратные ссылки и граф будут развиваться в плагине заметок.", + "ui.saving": "Сохранение...", + "ui.saveError": "Ошибка сохранения", + "ui.modified": "Изменён", + "ui.savedAt": "Сохранено {time}", + "ui.saved": "Сохранено", + "ui.discardConfirm": "Отменить несохранённые изменения и перечитать файл с диска?", + "ui.loading": "Загрузка...", + "ui.loadFailed": "Не удалось загрузить файл" +} diff --git a/plugins/default-editor/plugin.json b/plugins/default-editor/plugin.json index 72b678f..1e106cb 100644 --- a/plugins/default-editor/plugin.json +++ b/plugins/default-editor/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "edit", "provides": [ diff --git a/plugins/file-preview/frontend/src/index.js b/plugins/file-preview/frontend/src/index.js index 7110ec6..7ac52c5 100644 --- a/plugins/file-preview/frontend/src/index.js +++ b/plugins/file-preview/frontend/src/index.js @@ -108,6 +108,10 @@ var request = props && props.request || {}; var path = request.path || ''; var ext = extension(path, request.extension); + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } containerEl.innerHTML = ''; containerEl.className = 'fp-root'; containerEl.setAttribute('data-plugin-id', 'verstak.file-preview'); @@ -116,19 +120,19 @@ var openButton = el('button', { className: 'fp-btn', 'data-action': 'open-external', - textContent: 'Open External', + textContent: tr('ui.openExternal', null, 'Open External'), onClick: function () { if (api.files.openExternal) api.files.openExternal(path).catch(function (err) { console.error('[file-preview] openExternal:', err); }); } }); containerEl.appendChild(el('div', { className: 'fp-toolbar' }, [ - el('span', { className: 'fp-mode' }, ['Preview']), + el('span', { className: 'fp-mode' }, [tr('ui.preview', null, 'Preview')]), el('span', { className: 'fp-path' }, [path]), el('span', { className: 'fp-spacer' }), openButton ])); - var body = el('div', { className: 'fp-loading' }, ['Loading...']); + var body = el('div', { className: 'fp-loading' }, [tr('ui.loading', null, 'Loading...')]); containerEl.appendChild(body); api.files.metadata(path).then(function (meta) { @@ -140,8 +144,13 @@ }); }).catch(function (err) { body.className = 'fp-error'; - body.textContent = 'Preview error: ' + (err && err.message ? err.message : String(err)); + body.textContent = tr('ui.error', { error: err && err.message ? err.message : String(err) }, 'Preview error: ' + (err && err.message ? err.message : String(err))); }); + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + api.i18n.onDidChangeLocale(function () { + openButton.textContent = tr('ui.openExternal', null, 'Open External'); + }); + } }, unmount: function (containerEl) { containerEl.innerHTML = ''; diff --git a/plugins/file-preview/locales/en.json b/plugins/file-preview/locales/en.json new file mode 100644 index 0000000..d291af8 --- /dev/null +++ b/plugins/file-preview/locales/en.json @@ -0,0 +1,9 @@ +{ + "manifest.name": "File Preview", + "manifest.description": "Read-only inline image preview provider for vault files.", + "contributions.openProviders.verstak.file-preview.image.title": "Image Preview", + "ui.openExternal": "Open External", + "ui.preview": "Preview", + "ui.loading": "Loading...", + "ui.error": "Preview error: {error}" +} diff --git a/plugins/file-preview/locales/ru.json b/plugins/file-preview/locales/ru.json new file mode 100644 index 0000000..0f1596e --- /dev/null +++ b/plugins/file-preview/locales/ru.json @@ -0,0 +1,9 @@ +{ + "manifest.name": "Просмотр файлов", + "manifest.description": "Встроенный просмотр изображений из хранилища без редактирования.", + "contributions.openProviders.verstak.file-preview.image.title": "Просмотр изображения", + "ui.openExternal": "Открыть во внешнем приложении", + "ui.preview": "Просмотр", + "ui.loading": "Загрузка...", + "ui.error": "Ошибка просмотра: {error}" +} diff --git a/plugins/file-preview/plugin.json b/plugins/file-preview/plugin.json index 2a4bb84..5c865de 100644 --- a/plugins/file-preview/plugin.json +++ b/plugins/file-preview/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Read-only inline image preview provider for vault files.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "eye", "provides": [ diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 84d2d2b..ae39fcf 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -369,6 +369,11 @@ } var navigatingHistory = false; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + function scopedPath(local) { local = cleanPath(local); return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local; @@ -390,26 +395,26 @@ var toolbar = el('div', { className: 'files-toolbar' }); var breadcrumb = el('div', { className: 'files-breadcrumb' }); - var backBtn = iconButton('back', 'Back', 'back', goBack); - var forwardBtn = iconButton('forward', 'Forward', 'forward', goForward); - var upBtn = iconButton('up', 'Up', 'up', goUp); - var refreshBtn = iconButton('refresh', 'Refresh', 'refresh', loadEntries); - var newFolderBtn = iconButton('new-folder', 'New folder', 'folderAdd', function () { startCreate('folder'); }); - var newMdBtn = iconButton('new-markdown', 'New markdown file', 'markdownAdd', function () { startCreate('markdown'); }); - var newTextBtn = iconButton('new-text', 'New text file', 'textAdd', function () { startCreate('text'); }); - var openBtn = iconButton('open', 'Open', 'open', function () { openEntry(selectedEntry()); }); - var renameBtn = iconButton('rename', 'Rename', 'rename', function () { beginRename(); }); - var trashBtn = iconButton('trash', 'Move to trash', 'trash', function () { trashEntry(); }); - var cutBtn = iconButton('cut', 'Cut', 'cut', function () { cutSelection(); }); - var copyBtn = iconButton('copy', 'Copy', 'copy', function () { copySelection(); }); - var pasteBtn = iconButton('paste', 'Paste', 'paste', function () { pasteEntry(); }); - var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' }); + var backBtn = iconButton('back', tr('ui.back', null, 'Back'), 'back', goBack); + var forwardBtn = iconButton('forward', tr('ui.forward', null, 'Forward'), 'forward', goForward); + var upBtn = iconButton('up', tr('ui.up', null, 'Up'), 'up', goUp); + var refreshBtn = iconButton('refresh', tr('ui.refresh', null, 'Refresh'), 'refresh', loadEntries); + var newFolderBtn = iconButton('new-folder', tr('ui.newFolder', null, 'New folder'), 'folderAdd', function () { startCreate('folder'); }); + var newMdBtn = iconButton('new-markdown', tr('ui.newMarkdown', null, 'New markdown file'), 'markdownAdd', function () { startCreate('markdown'); }); + var newTextBtn = iconButton('new-text', tr('ui.newText', null, 'New text file'), 'textAdd', function () { startCreate('text'); }); + var openBtn = iconButton('open', tr('ui.open', null, 'Open'), 'open', function () { openEntry(selectedEntry()); }); + var renameBtn = iconButton('rename', tr('ui.rename', null, 'Rename'), 'rename', function () { beginRename(); }); + var trashBtn = iconButton('trash', tr('ui.trash', null, 'Move to trash'), 'trash', function () { trashEntry(); }); + var cutBtn = iconButton('cut', tr('ui.cut', null, 'Cut'), 'cut', function () { cutSelection(); }); + var copyBtn = iconButton('copy', tr('ui.copy', null, 'Copy'), 'copy', function () { copySelection(); }); + var pasteBtn = iconButton('paste', tr('ui.paste', null, 'Paste'), 'paste', function () { pasteEntry(); }); + var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: tr('ui.filter', null, 'Filter current folder') }); var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [ - el('option', { value: 'folder-name' }, ['Folders + name']), - el('option', { value: 'name-asc' }, ['Name']), - el('option', { value: 'type' }, ['Type']), - el('option', { value: 'modified-desc' }, ['Modified']), - el('option', { value: 'size-desc' }, ['Size']) + el('option', { value: 'folder-name' }, [tr('ui.sort.foldersName', null, 'Folders + name')]), + el('option', { value: 'name-asc' }, [tr('ui.column.name', null, 'Name')]), + el('option', { value: 'type' }, [tr('ui.column.type', null, 'Type')]), + el('option', { value: 'modified-desc' }, [tr('ui.column.modified', null, 'Modified')]), + el('option', { value: 'size-desc' }, [tr('ui.column.size', null, 'Size')]) ]); trashBtn.classList.add('danger'); toolbar.appendChild(breadcrumb); @@ -430,8 +435,8 @@ var createField = el('div', { className: 'files-field-stack' }); var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' }); var createError = el('div', { className: 'files-panel-error', 'data-files-create-error': '', role: 'alert' }); - var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, ['Create']); - var createCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); + var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, [tr('ui.create', null, 'Create')]); + var createCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]); createField.appendChild(createInput); createField.appendChild(createError); createPanel.appendChild(createField); @@ -443,8 +448,8 @@ var renameField = el('div', { className: 'files-field-stack' }); var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' }); var renameError = el('div', { className: 'files-panel-error', 'data-files-rename-error': '', role: 'alert' }); - var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, ['Rename']); - var renameCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); + var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, [tr('ui.rename', null, 'Rename')]); + var renameCancel = el('button', { className: 'files-toolbar-btn' }, [tr('ui.cancel', null, 'Cancel')]); renameField.appendChild(renameInput); renameField.appendChild(renameError); renamePanel.appendChild(renameField); @@ -598,27 +603,27 @@ function renderEmptyFolderState() { return el('div', { className: 'files-empty' }, [ - el('div', { className: 'files-empty-title' }, ['Empty folder']), + el('div', { className: 'files-empty-title' }, [tr('ui.emptyFolder', null, 'Empty folder')]), el('div', { className: 'files-empty-actions' }, [ - emptyCreateAction('new-folder', 'New folder', 'folder', 'folderAdd'), - emptyCreateAction('new-markdown', 'New markdown file', 'markdown', 'markdownAdd'), - emptyCreateAction('new-text', 'New text file', 'text', 'textAdd') + emptyCreateAction('new-folder', tr('ui.newFolder', null, 'New folder'), 'folder', 'folderAdd'), + emptyCreateAction('new-markdown', tr('ui.newMarkdown', null, 'New markdown file'), 'markdown', 'markdownAdd'), + emptyCreateAction('new-text', tr('ui.newText', null, 'New text file'), 'text', 'textAdd') ]) ]); } function renderNoMatchesState() { return el('div', { className: 'files-empty' }, [ - el('div', { className: 'files-empty-title' }, ['No matches']), + el('div', { className: 'files-empty-title' }, [tr('ui.noMatches', null, 'No matches')]), el('div', { className: 'files-empty-actions' }, [ el('button', { className: 'files-empty-btn', 'data-files-empty-action': 'clear-filter', 'data-files-icon': 'refresh', type: 'button', - title: 'Clear filter', - 'aria-label': 'Clear filter', - innerHTML: svgIcon(ACTION_ICONS.refresh) + 'Clear filter', + title: tr('ui.clearFilter', null, 'Clear filter'), + 'aria-label': tr('ui.clearFilter', null, 'Clear filter'), + innerHTML: svgIcon(ACTION_ICONS.refresh) + '' + tr('ui.clearFilter', null, 'Clear filter') + '', onClick: function () { filterText = ''; filterInput.value = ''; @@ -633,11 +638,11 @@ function renderList() { listContainer.innerHTML = ''; var header = el('div', { className: 'files-header' }, [ - el('span', {}, ['Name']), - el('span', {}, ['Type']), - el('span', {}, ['Size']), - el('span', {}, ['Modified']), - el('span', {}, ['Actions']) + el('span', {}, [tr('ui.column.name', null, 'Name')]), + el('span', {}, [tr('ui.column.type', null, 'Type')]), + el('span', {}, [tr('ui.column.size', null, 'Size')]), + el('span', {}, [tr('ui.column.modified', null, 'Modified')]), + el('span', {}, [tr('ui.column.actions', null, 'Actions')]) ]); listContainer.appendChild(header); @@ -687,9 +692,9 @@ el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]), el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]), el('div', { className: 'files-row-actions' }, [ - iconButton('row-open', 'Open', 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'), - iconButton('row-rename', 'Rename', 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'), - iconButton('row-trash', 'Move to trash', 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn danger') + iconButton('row-open', tr('ui.open', null, 'Open'), 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'), + iconButton('row-rename', tr('ui.rename', null, 'Rename'), 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'), + iconButton('row-trash', tr('ui.trash', null, 'Move to trash'), 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn danger') ]) ]); listContainer.appendChild(row); @@ -701,7 +706,7 @@ selectedPaths = {}; lastClickedPath = ''; listContainer.innerHTML = ''; - listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...'])); + listContainer.appendChild(el('div', { className: 'files-loading' }, [tr('ui.loading', null, 'Loading...')])); updateBreadcrumb(); api.files.list(scopedPath(currentPath)).then(function (result) { if (disposed) return; @@ -711,7 +716,7 @@ if (disposed) return; listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'files-error' }, [ - el('div', {}, ['Failed to load files']), + el('div', {}, [tr('ui.loadFailed', null, 'Failed to load files')]), el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)]) ])); }); @@ -1478,6 +1483,29 @@ loadContributionActions(); loadEntries(); + var localeUnsubscribe = null; + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + localeUnsubscribe = api.i18n.onDidChangeLocale(function () { + [ + [backBtn, 'ui.back', 'Back'], [forwardBtn, 'ui.forward', 'Forward'], [upBtn, 'ui.up', 'Up'], + [refreshBtn, 'ui.refresh', 'Refresh'], [newFolderBtn, 'ui.newFolder', 'New folder'], + [newMdBtn, 'ui.newMarkdown', 'New markdown file'], [newTextBtn, 'ui.newText', 'New text file'], + [openBtn, 'ui.open', 'Open'], [renameBtn, 'ui.rename', 'Rename'], [trashBtn, 'ui.trash', 'Move to trash'], + [cutBtn, 'ui.cut', 'Cut'], [copyBtn, 'ui.copy', 'Copy'], [pasteBtn, 'ui.paste', 'Paste'] + ].forEach(function (item) { + var label = tr(item[1], null, item[2]); + item[0].setAttribute('title', label); + item[0].setAttribute('aria-label', label); + }); + filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter current folder')); + createConfirm.textContent = tr('ui.create', null, 'Create'); + createCancel.textContent = tr('ui.cancel', null, 'Cancel'); + renameConfirm.textContent = tr('ui.rename', null, 'Rename'); + renameCancel.textContent = tr('ui.cancel', null, 'Cancel'); + renderList(); + }); + } + var fileChangedUnsubscribe = null; if (api.events && typeof api.events.subscribe === 'function') { api.events.subscribe('file.changed', function (event) { @@ -1492,6 +1520,7 @@ containerEl.__filesCleanup = function () { disposed = true; + if (typeof localeUnsubscribe === 'function') localeUnsubscribe(); if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe(); document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onDocKeydown); diff --git a/plugins/files/locales/en.json b/plugins/files/locales/en.json new file mode 100644 index 0000000..dc8ff8a --- /dev/null +++ b/plugins/files/locales/en.json @@ -0,0 +1,32 @@ +{ + "manifest.name": "Files", + "manifest.description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", + "contributions.workspaceItems.verstak.files.workspace.title": "Files", + "ui.back": "Back", + "ui.forward": "Forward", + "ui.up": "Up", + "ui.refresh": "Refresh", + "ui.newFolder": "New folder", + "ui.newMarkdown": "New markdown file", + "ui.newText": "New text file", + "ui.open": "Open", + "ui.rename": "Rename", + "ui.trash": "Move to trash", + "ui.cut": "Cut", + "ui.copy": "Copy", + "ui.paste": "Paste", + "ui.filter": "Filter current folder", + "ui.sort.foldersName": "Folders + name", + "ui.column.name": "Name", + "ui.column.type": "Type", + "ui.column.modified": "Modified", + "ui.column.size": "Size", + "ui.column.actions": "Actions", + "ui.create": "Create", + "ui.cancel": "Cancel", + "ui.emptyFolder": "Empty folder", + "ui.noMatches": "No matches", + "ui.clearFilter": "Clear filter", + "ui.loading": "Loading...", + "ui.loadFailed": "Failed to load files" +} diff --git a/plugins/files/locales/ru.json b/plugins/files/locales/ru.json new file mode 100644 index 0000000..15dea45 --- /dev/null +++ b/plugins/files/locales/ru.json @@ -0,0 +1,32 @@ +{ + "manifest.name": "Файлы", + "manifest.description": "Файловый менеджер рабочего пространства с созданием, переименованием, корзиной, фильтрацией и сортировкой.", + "contributions.workspaceItems.verstak.files.workspace.title": "Файлы", + "ui.back": "Назад", + "ui.forward": "Вперёд", + "ui.up": "На уровень выше", + "ui.refresh": "Обновить", + "ui.newFolder": "Новая папка", + "ui.newMarkdown": "Новый Markdown-файл", + "ui.newText": "Новый текстовый файл", + "ui.open": "Открыть", + "ui.rename": "Переименовать", + "ui.trash": "Переместить в корзину", + "ui.cut": "Вырезать", + "ui.copy": "Копировать", + "ui.paste": "Вставить", + "ui.filter": "Фильтр текущей папки", + "ui.sort.foldersName": "Сначала папки, затем по имени", + "ui.column.name": "Имя", + "ui.column.type": "Тип", + "ui.column.modified": "Изменён", + "ui.column.size": "Размер", + "ui.column.actions": "Действия", + "ui.create": "Создать", + "ui.cancel": "Отмена", + "ui.emptyFolder": "Папка пуста", + "ui.noMatches": "Совпадений нет", + "ui.clearFilter": "Сбросить фильтр", + "ui.loading": "Загрузка...", + "ui.loadFailed": "Не удалось загрузить файлы" +} diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json index f6dc4d9..bc47b51 100644 --- a/plugins/files/plugin.json +++ b/plugins/files/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Workspace-scoped vault file explorer with create, rename, trash, filtering, sorting, and Workbench openResource integration.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "folder", "provides": [ diff --git a/plugins/journal/frontend/src/index.js b/plugins/journal/frontend/src/index.js index 5802556..83917f8 100644 --- a/plugins/journal/frontend/src/index.js +++ b/plugins/journal/frontend/src/index.js @@ -273,18 +273,22 @@ var scope = scopeFromProps(props || {}); var entries = []; - var statusText = 'Loading journal...'; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + var statusText = tr('ui.loading', null, 'Loading journal...'); var statusClass = ''; var modalHost = el('div', { className: 'journal-modal-host', hidden: 'hidden' }); var toolbar = el('div', { className: 'journal-toolbar' }); - var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? 'Journal' : 'Journal · ' + scope.label }); + var titleEl = el('span', { className: 'journal-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label) }); var countEl = el('span', { className: 'journal-count' }); var statusEl = el('span', { className: 'journal-status' }); var addBtn = el('button', { className: 'journal-btn primary', 'data-journal-action': 'add', - innerHTML: iconSvg('add') + 'Add', + innerHTML: iconSvg('add') + '' + tr('ui.add', null, 'Add') + '', onClick: function () { showEntryModal(); } }); toolbar.appendChild(titleEl); @@ -302,7 +306,7 @@ if (scope.mode !== 'workspace') return Promise.resolve(); if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve(); return api.settings.write(scope.key, storageEntries(entries)).catch(function (err) { - statusText = 'Could not save journal: ' + (err && err.message ? err.message : String(err)); + statusText = tr('ui.saveError', { error: err && err.message ? err.message : String(err) }, 'Could not save journal: ' + (err && err.message ? err.message : String(err))); statusClass = 'error'; }); } @@ -316,19 +320,19 @@ all = all.concat(normalizeEntries((settings || {})[key], key)); }); entries = sortEntries(all); - statusText = 'Aggregating worklogs'; + statusText = tr('ui.aggregating', null, 'Aggregating worklogs'); statusClass = ''; }).catch(function (err) { - statusText = 'Could not load journal: ' + (err && err.message ? err.message : String(err)); + statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err))); statusClass = 'error'; }); } return api.settings.read(scope.key).then(function (stored) { entries = sortEntries(normalizeEntries(stored, scope.key)); - statusText = 'Ready'; + statusText = tr('ui.ready', null, 'Ready'); statusClass = ''; }).catch(function (err) { - statusText = 'Could not load journal: ' + (err && err.message ? err.message : String(err)); + statusText = tr('ui.loadError', { error: err && err.message ? err.message : String(err) }, 'Could not load journal: ' + (err && err.message ? err.message : String(err))); statusClass = 'error'; }); } @@ -344,8 +348,8 @@ var reviewingCandidate = !editing && !!candidate; var reviewingTodo = !editing && !!completedTodo; var dateInput = el('input', { className: 'journal-input', type: 'date', value: editing ? existingEntry.date : (reviewingCandidate ? candidateDate(candidate.startedAt) : (reviewingTodo ? candidateDate(completedTodo.completedAt) : today())), 'data-journal-input': 'date' }); - var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: 'Work item', value: editing ? existingEntry.title : (reviewingTodo ? completedTodo.title : ''), 'data-journal-input': 'title' }); - var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: 'Body', 'data-journal-input': 'summary' }); + var titleInput = el('input', { className: 'journal-input', type: 'text', placeholder: tr('ui.workItem', null, 'Work item'), value: editing ? existingEntry.title : (reviewingTodo ? completedTodo.title : ''), 'data-journal-input': 'title' }); + var summaryInput = el('textarea', { className: 'journal-input textarea', placeholder: tr('ui.body', null, 'Body'), 'data-journal-input': 'summary' }); summaryInput.value = editing ? existingEntry.summary : (reviewingTodo ? completedTodo.description : ''); var minutesInput = el('input', { className: 'journal-input', type: 'number', min: '0', step: '1', value: editing ? existingEntry.minutes : (reviewingCandidate ? candidate.estimatedMinutes : (reviewingTodo ? '0' : '30')), 'data-journal-input': 'minutes' }); var billableInput = el('input', { type: 'checkbox', 'data-journal-input': 'billable' }); @@ -397,20 +401,20 @@ if (event.target === event.currentTarget) closeEntryModal(); } }, [ el('div', { className: 'journal-modal' }, [ - el('div', { className: 'journal-modal-title', textContent: editing ? 'Edit journal entry' : (reviewingCandidate ? 'Review possible journal entry' : (reviewingTodo ? 'Create journal entry from completed todo' : 'Add journal entry')) }), + el('div', { className: 'journal-modal-title', textContent: editing ? tr('ui.editEntry', null, 'Edit journal entry') : (reviewingCandidate ? tr('ui.reviewCandidate', null, 'Review possible journal entry') : (reviewingTodo ? tr('ui.fromTodo', null, 'Create journal entry from completed todo') : tr('ui.addEntry', null, 'Add journal entry'))) }), candidateContext, todoContext, el('div', { className: 'journal-modal-grid' }, [ - el('label', { className: 'journal-field' }, ['Date', dateInput]), - el('label', { className: 'journal-field' }, ['Minutes', minutesInput]), - el('label', { className: 'journal-field wide' }, ['Title', titleInput]), - el('label', { className: 'journal-field wide' }, ['Body', summaryInput]), - el('label', { className: 'journal-billable' }, [billableInput, 'Billable']) + el('label', { className: 'journal-field' }, [tr('ui.date', null, 'Date'), dateInput]), + el('label', { className: 'journal-field' }, [tr('ui.minutes', null, 'Minutes'), minutesInput]), + el('label', { className: 'journal-field wide' }, [tr('ui.fieldTitle', null, 'Title'), titleInput]), + el('label', { className: 'journal-field wide' }, [tr('ui.body', null, 'Body'), summaryInput]), + el('label', { className: 'journal-billable' }, [billableInput, tr('ui.billable', null, 'Billable')]) ]), candidateActivities, el('div', { className: 'journal-modal-actions' }, [ - el('button', { className: 'journal-btn ghost', type: 'button', textContent: 'Cancel', onClick: closeEntryModal }), - el('button', { className: 'journal-btn primary', type: 'button', 'data-journal-action': 'save-entry', textContent: editing ? 'Save changes' : 'Add entry', onClick: saveEntry }) + el('button', { className: 'journal-btn ghost', type: 'button', textContent: tr('ui.cancel', null, 'Cancel'), onClick: closeEntryModal }), + el('button', { className: 'journal-btn primary', type: 'button', 'data-journal-action': 'save-entry', textContent: editing ? tr('ui.saveChanges', null, 'Save changes') : tr('ui.addEntryShort', null, 'Add entry'), onClick: saveEntry }) ]) ]) ])); @@ -421,7 +425,7 @@ if (scope.mode !== 'workspace') return; var title = text(formValue && formValue.title).trim(); if (!title) { - statusText = 'Title is required'; + statusText = tr('ui.titleRequired', null, 'Title is required'); statusClass = 'error'; render(); return; @@ -469,7 +473,7 @@ function deleteEntry(entry) { if (scope.mode !== 'workspace' || !entry) return; entries = entries.filter(function (item) { return item.entryId !== entry.entryId; }); - statusText = 'Entry deleted'; + statusText = tr('ui.deleted', null, 'Entry deleted'); statusClass = ''; persist().then(render); } @@ -477,7 +481,7 @@ function renderList() { listEl.innerHTML = ''; if (!entries.length) { - listEl.appendChild(el('div', { className: 'journal-empty', textContent: scope.mode === 'global' ? 'No worklog entries yet.' : 'No worklog entries yet.' })); + listEl.appendChild(el('div', { className: 'journal-empty', textContent: tr('ui.empty', null, 'No worklog entries yet.') })); return; } entries.forEach(function (entry) { @@ -493,8 +497,8 @@ ]), el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' }), scope.mode === 'workspace' ? el('div', { className: 'journal-row-actions' }, [ - el('button', { className: 'journal-icon-btn', type: 'button', title: 'Edit', 'aria-label': 'Edit', 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }), - el('button', { className: 'journal-icon-btn danger', type: 'button', title: 'Delete', 'aria-label': 'Delete', 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } }) + el('button', { className: 'journal-icon-btn', type: 'button', title: tr('ui.edit', null, 'Edit'), 'aria-label': tr('ui.edit', null, 'Edit'), 'data-journal-action': 'edit', innerHTML: iconSvg('edit'), onClick: function () { showEntryModal(entry); } }), + el('button', { className: 'journal-icon-btn danger', type: 'button', title: tr('ui.delete', null, 'Delete'), 'aria-label': tr('ui.delete', null, 'Delete'), 'data-journal-action': 'delete', innerHTML: iconSvg('trash'), onClick: function () { deleteEntry(entry); } }) ]) : null ])); }); @@ -516,6 +520,13 @@ if (candidate) showEntryModal(null, candidate); else if (completedTodo) showEntryModal(null, null, completedTodo); }); + if (api && api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + api.i18n.onDidChangeLocale(function () { + titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Journal') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Journal · ' + scope.label); + addBtn.innerHTML = iconSvg('add') + '' + tr('ui.add', null, 'Add') + ''; + render(); + }); + } }; JournalView.unmount = function (containerEl) { diff --git a/plugins/journal/locales/en.json b/plugins/journal/locales/en.json new file mode 100644 index 0000000..629c5cc --- /dev/null +++ b/plugins/journal/locales/en.json @@ -0,0 +1,33 @@ +{ + "manifest.name": "Journal", + "manifest.description": "Workspace-scoped journal with user-authored entries and optional Activity links.", + "contributions.views.verstak.journal.view.title": "Journal", + "contributions.sidebarItems.verstak.journal.sidebar.title": "Journal", + "contributions.workspaceItems.verstak.journal.workspace.title": "Journal", + "ui.loading": "Loading journal...", + "ui.title": "Journal", + "ui.workspaceTitle": "Journal · {workspace}", + "ui.add": "Add", + "ui.saveError": "Could not save journal: {error}", + "ui.aggregating": "Aggregating worklogs", + "ui.loadError": "Could not load journal: {error}", + "ui.ready": "Ready", + "ui.workItem": "Work item", + "ui.body": "Body", + "ui.editEntry": "Edit journal entry", + "ui.reviewCandidate": "Review possible journal entry", + "ui.fromTodo": "Create journal entry from completed todo", + "ui.addEntry": "Add journal entry", + "ui.date": "Date", + "ui.minutes": "Minutes", + "ui.fieldTitle": "Title", + "ui.billable": "Billable", + "ui.cancel": "Cancel", + "ui.saveChanges": "Save changes", + "ui.addEntryShort": "Add entry", + "ui.titleRequired": "Title is required", + "ui.deleted": "Entry deleted", + "ui.empty": "No worklog entries yet.", + "ui.edit": "Edit", + "ui.delete": "Delete" +} diff --git a/plugins/journal/locales/ru.json b/plugins/journal/locales/ru.json new file mode 100644 index 0000000..12dd97a --- /dev/null +++ b/plugins/journal/locales/ru.json @@ -0,0 +1,33 @@ +{ + "manifest.name": "Журнал", + "manifest.description": "Журнал рабочего пространства с ручными записями и необязательными связями с активностью.", + "contributions.views.verstak.journal.view.title": "Журнал", + "contributions.sidebarItems.verstak.journal.sidebar.title": "Журнал", + "contributions.workspaceItems.verstak.journal.workspace.title": "Журнал", + "ui.loading": "Загрузка журнала...", + "ui.title": "Журнал", + "ui.workspaceTitle": "Журнал · {workspace}", + "ui.add": "Добавить", + "ui.saveError": "Не удалось сохранить журнал: {error}", + "ui.aggregating": "Сбор записей о работе", + "ui.loadError": "Не удалось загрузить журнал: {error}", + "ui.ready": "Готово", + "ui.workItem": "Выполненная работа", + "ui.body": "Описание", + "ui.editEntry": "Изменить запись журнала", + "ui.reviewCandidate": "Проверить возможную запись журнала", + "ui.fromTodo": "Создать запись журнала из выполненной задачи", + "ui.addEntry": "Добавить запись журнала", + "ui.date": "Дата", + "ui.minutes": "Минуты", + "ui.fieldTitle": "Название", + "ui.billable": "Оплачиваемая работа", + "ui.cancel": "Отмена", + "ui.saveChanges": "Сохранить изменения", + "ui.addEntryShort": "Добавить запись", + "ui.titleRequired": "Введите название", + "ui.deleted": "Запись удалена", + "ui.empty": "Записей о работе пока нет.", + "ui.edit": "Изменить", + "ui.delete": "Удалить" +} diff --git a/plugins/journal/plugin.json b/plugins/journal/plugin.json index 9364abe..e4f0580 100644 --- a/plugins/journal/plugin.json +++ b/plugins/journal/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Workspace-scoped journal with user-authored entries and optional Activity links.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "book-open", "provides": [ diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js index 909298c..f1feb15 100644 --- a/plugins/notes/frontend/src/index.js +++ b/plugins/notes/frontend/src/index.js @@ -183,6 +183,11 @@ var sortMode = 'title-asc'; var renameTarget = null; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + function notesParent() { return workspaceRoot || ''; } @@ -262,8 +267,8 @@ // ─── UI Elements ──────────────────────────────────────── var toolbar = el('div', { className: 'notes-toolbar' }); - var createBtn = el('button', { className: 'notes-btn primary', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' }); - var filterInput = el('input', { className: 'notes-filter', 'data-notes-filter': '', placeholder: 'Filter notes' }); + var createBtn = el('button', { className: 'notes-btn primary', 'data-action': 'create', innerHTML: iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note') }); + var filterInput = el('input', { className: 'notes-filter', 'data-notes-filter': '', placeholder: tr('ui.filter', null, 'Filter notes') }); var sortSelect = el('select', { className: 'notes-sort', 'data-notes-sort': '' }, [ el('option', { value: 'title-asc' }, ['A-Z']), el('option', { value: 'title-desc' }, ['Z-A']) @@ -276,25 +281,25 @@ toolbar.appendChild(statusEl); containerEl.appendChild(toolbar); - var titleBar = el('div', { className: 'notes-title-bar' }, ['Notes in ' + workspaceName]); + var titleBar = el('div', { className: 'notes-title-bar' }, [tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName)]); containerEl.appendChild(titleBar); var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' }); containerEl.appendChild(listContainer); var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } }); - var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: 'Note title' }); - var createConfirm = el('button', { className: 'notes-btn', textContent: 'Create' }); - var createCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' }); + var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: tr('ui.noteTitle', null, 'Note title') }); + var createConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.create', null, 'Create') }); + var createCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') }); createPanel.appendChild(createInput); createPanel.appendChild(createConfirm); createPanel.appendChild(createCancel); containerEl.appendChild(createPanel); var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } }); - var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: 'New title' }); - var renameConfirm = el('button', { className: 'notes-btn', textContent: 'Rename' }); - var renameCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' }); + var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: tr('ui.newTitle', null, 'New title') }); + var renameConfirm = el('button', { className: 'notes-btn', textContent: tr('ui.rename', null, 'Rename') }); + var renameCancel = el('button', { className: 'notes-btn', textContent: tr('ui.cancel', null, 'Cancel') }); renamePanel.appendChild(renameInput); renamePanel.appendChild(renameConfirm); renamePanel.appendChild(renameCancel); @@ -321,7 +326,7 @@ function loadNotes() { listContainer.innerHTML = ''; - listContainer.appendChild(el('div', { className: 'notes-empty' }, ['Loading...'])); + listContainer.appendChild(el('div', { className: 'notes-empty' }, [tr('ui.loading', null, 'Loading...')])); var parent = notesParent(); listNotes(parent).then(function (result) { @@ -392,36 +397,36 @@ function renderList() { listContainer.innerHTML = ''; if (!notes || notes.length === 0) { - renderEmpty('No notes yet'); + renderEmpty(tr('ui.empty', null, 'No notes yet')); return; } var shown = visibleNotes(); if (shown.length === 0) { - renderEmpty('No matching notes', 'Clear the filter to show all notes'); + renderEmpty(tr('ui.noMatches', null, 'No matching notes'), tr('ui.clearFilterHint', null, 'Clear the filter to show all notes')); return; } shown.forEach(function (note) { var actionButtons = [ el('button', { className: 'notes-item-btn', - title: 'Open', - 'aria-label': 'Open', + title: tr('ui.open', null, 'Open'), + 'aria-label': tr('ui.open', null, 'Open'), 'data-note-action': 'open', innerHTML: iconSvg('open'), onClick: function (e) { e.stopPropagation(); openNote(note); } }), el('button', { className: 'notes-item-btn', - title: 'Rename', - 'aria-label': 'Rename', + title: tr('ui.rename', null, 'Rename'), + 'aria-label': tr('ui.rename', null, 'Rename'), 'data-note-action': 'rename', innerHTML: iconSvg('rename'), onClick: function (e) { e.stopPropagation(); beginRename(note); } }), el('button', { className: 'notes-item-btn', - title: 'Move to Trash', - 'aria-label': 'Move to Trash', + title: tr('ui.trash', null, 'Move to Trash'), + 'aria-label': tr('ui.trash', null, 'Move to Trash'), 'data-note-action': 'trash', innerHTML: iconSvg('trash'), onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); } @@ -457,7 +462,7 @@ listContainer.appendChild(el('div', { className: 'notes-empty' }, [ el('div', { innerHTML: iconSvg('note') }), el('div', {}, [msg]), - el('div', { className: 'notes-empty-hint' }, [hint || 'Click "New Note" to create one']) + el('div', { className: 'notes-empty-hint' }, [hint || tr('ui.emptyHint', null, 'Click "New Note" to create one')]) ])); } @@ -499,7 +504,7 @@ function confirmCreate() { var title = createInput.value.trim(); if (!title) return; - setStatus('Creating note...', 'loading'); + setStatus(tr('ui.creating', null, 'Creating note...'), 'loading'); var parent = notesParent(); createNote(parent, title).then(function (data) { if (disposed) return; @@ -509,7 +514,7 @@ return; } hideCreate(); - setStatus('Note created', 'success'); + setStatus(tr('ui.created', null, 'Note created'), 'success'); loadNotes(); // Open the newly created note if (data.path) { @@ -551,7 +556,7 @@ if (!renameTarget) return; var newTitle = renameInput.value.trim(); if (!newTitle) return; - setStatus('Renaming...', 'loading'); + setStatus(tr('ui.renaming', null, 'Renaming...'), 'loading'); renameNote(renameTarget.path, newTitle).then(function (data) { if (disposed) return; data = data || {}; @@ -560,7 +565,7 @@ return; } hideRename(); - setStatus('Note renamed', 'success'); + setStatus(tr('ui.renamed', null, 'Note renamed'), 'success'); loadNotes(); }).catch(function (err) { setStatus('Error: ' + (err.message || err), 'error'); @@ -573,11 +578,11 @@ if (!note) return; showTrashModal(note).then(function (confirmed) { if (!confirmed || disposed) return; - setStatus('Moving note to trash...', 'loading'); + setStatus(tr('ui.trashing', null, 'Moving note to trash...'), 'loading'); api.files.trash(note.path).then(function () { if (disposed) return; if (selectedPath === note.path) selectedPath = ''; - setStatus('Note moved to trash', 'success'); + setStatus(tr('ui.trashed', null, 'Note moved to trash'), 'success'); loadNotes(); }).catch(function (err) { setStatus('Error: ' + (err.message || err), 'error'); @@ -590,11 +595,11 @@ function showConflictModal(title, existingPath, focusTarget) { var overlay = el('div', { className: 'notes-modal-overlay' }); var modal = el('div', { className: 'notes-modal' }, [ - el('div', { className: 'notes-modal-title' }, ['Name Conflict']), + el('div', { className: 'notes-modal-title' }, [tr('ui.conflictTitle', null, 'Name Conflict')]), el('div', { className: 'notes-modal-msg' }, [ - 'A note with the title "' + title + '" already exists.', - existingPath ? ' Existing file: ' + existingPath + '.' : '', - ' Please choose a different title.' + tr('ui.conflictMessage', { title: title }, 'A note with the title "' + title + '" already exists.'), + existingPath ? tr('ui.existingFile', { path: existingPath }, ' Existing file: ' + existingPath + '.') : '', + tr('ui.chooseDifferent', null, ' Please choose a different title.') ].join('')), el('div', { className: 'notes-modal-actions' }, [ el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); (focusTarget || createInput).focus(); } }) @@ -612,13 +617,13 @@ resolve(value); } var modal = el('div', { className: 'notes-modal' }, [ - el('div', { className: 'notes-modal-title' }, ['Move Note to Trash']), + el('div', { className: 'notes-modal-title' }, [tr('ui.trashTitle', null, 'Move Note to Trash')]), el('div', { className: 'notes-modal-msg' }, [ - 'Move "' + (note.title || fileName(note.path)) + '" to trash?' + tr('ui.trashConfirm', { title: note.title || fileName(note.path) }, 'Move "' + (note.title || fileName(note.path)) + '" to trash?') ]), el('div', { className: 'notes-modal-actions' }, [ - el('button', { className: 'notes-modal-btn cancel', textContent: 'Cancel', onClick: function () { close(false); } }), - el('button', { className: 'notes-modal-btn danger', 'data-notes-confirm-trash': '', textContent: 'Move to Trash', onClick: function () { close(true); } }) + el('button', { className: 'notes-modal-btn cancel', textContent: tr('ui.cancel', null, 'Cancel'), onClick: function () { close(false); } }), + el('button', { className: 'notes-modal-btn danger', 'data-notes-confirm-trash': '', textContent: tr('ui.trash', null, 'Move to Trash'), onClick: function () { close(true); } }) ]) ]); overlay.appendChild(modal); @@ -655,6 +660,22 @@ loadContributionActions(); loadNotes(); + var localeUnsubscribe = null; + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + localeUnsubscribe = api.i18n.onDidChangeLocale(function () { + createBtn.innerHTML = iconSvg('add') + ' ' + tr('ui.newNote', null, 'New Note'); + filterInput.setAttribute('placeholder', tr('ui.filter', null, 'Filter notes')); + titleBar.textContent = tr('ui.title', { workspace: workspaceName }, 'Notes in ' + workspaceName); + createInput.setAttribute('placeholder', tr('ui.noteTitle', null, 'Note title')); + renameInput.setAttribute('placeholder', tr('ui.newTitle', null, 'New title')); + createConfirm.textContent = tr('ui.create', null, 'Create'); + createCancel.textContent = tr('ui.cancel', null, 'Cancel'); + renameConfirm.textContent = tr('ui.rename', null, 'Rename'); + renameCancel.textContent = tr('ui.cancel', null, 'Cancel'); + renderList(); + }); + } + var fileChangedUnsubscribe = null; if (api.events && typeof api.events.subscribe === 'function') { api.events.subscribe('file.changed', function (event) { @@ -669,6 +690,7 @@ containerEl.__notesCleanup = function () { disposed = true; + if (typeof localeUnsubscribe === 'function') localeUnsubscribe(); if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe(); }; }, diff --git a/plugins/notes/locales/en.json b/plugins/notes/locales/en.json new file mode 100644 index 0000000..f941530 --- /dev/null +++ b/plugins/notes/locales/en.json @@ -0,0 +1,32 @@ +{ + "manifest.name": "Notes", + "manifest.description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.", + "contributions.workspaceItems.verstak.notes.workspace.title": "Notes", + "ui.newNote": "New Note", + "ui.filter": "Filter notes", + "ui.title": "Notes in {workspace}", + "ui.noteTitle": "Note title", + "ui.create": "Create", + "ui.cancel": "Cancel", + "ui.newTitle": "New title", + "ui.rename": "Rename", + "ui.loading": "Loading...", + "ui.empty": "No notes yet", + "ui.noMatches": "No matching notes", + "ui.clearFilterHint": "Clear the filter to show all notes", + "ui.open": "Open", + "ui.trash": "Move to Trash", + "ui.emptyHint": "Click \"New Note\" to create one", + "ui.creating": "Creating note...", + "ui.created": "Note created", + "ui.renaming": "Renaming...", + "ui.renamed": "Note renamed", + "ui.trashing": "Moving note to trash...", + "ui.trashed": "Note moved to trash", + "ui.conflictTitle": "Name Conflict", + "ui.conflictMessage": "A note with the title \"{title}\" already exists.", + "ui.existingFile": " Existing file: {path}.", + "ui.chooseDifferent": " Please choose a different title.", + "ui.trashTitle": "Move Note to Trash", + "ui.trashConfirm": "Move \"{title}\" to trash?" +} diff --git a/plugins/notes/locales/ru.json b/plugins/notes/locales/ru.json new file mode 100644 index 0000000..3987a83 --- /dev/null +++ b/plugins/notes/locales/ru.json @@ -0,0 +1,32 @@ +{ + "manifest.name": "Заметки", + "manifest.description": "Менеджер Markdown-заметок рабочего пространства с созданием, переименованием и корзиной.", + "contributions.workspaceItems.verstak.notes.workspace.title": "Заметки", + "ui.newNote": "Новая заметка", + "ui.filter": "Фильтр заметок", + "ui.title": "Заметки · {workspace}", + "ui.noteTitle": "Название заметки", + "ui.create": "Создать", + "ui.cancel": "Отмена", + "ui.newTitle": "Новое название", + "ui.rename": "Переименовать", + "ui.loading": "Загрузка...", + "ui.empty": "Заметок пока нет", + "ui.noMatches": "Подходящих заметок нет", + "ui.clearFilterHint": "Сбросьте фильтр, чтобы показать все заметки", + "ui.open": "Открыть", + "ui.trash": "Переместить в корзину", + "ui.emptyHint": "Нажмите «Новая заметка», чтобы создать первую", + "ui.creating": "Создание заметки...", + "ui.created": "Заметка создана", + "ui.renaming": "Переименование...", + "ui.renamed": "Заметка переименована", + "ui.trashing": "Перемещение заметки в корзину...", + "ui.trashed": "Заметка перемещена в корзину", + "ui.conflictTitle": "Конфликт названия", + "ui.conflictMessage": "Заметка с названием «{title}» уже существует.", + "ui.existingFile": " Существующий файл: {path}.", + "ui.chooseDifferent": " Выберите другое название.", + "ui.trashTitle": "Переместить заметку в корзину", + "ui.trashConfirm": "Переместить «{title}» в корзину?" +} diff --git a/plugins/notes/plugin.json b/plugins/notes/plugin.json index dc3b381..2a366e6 100644 --- a/plugins/notes/plugin.json +++ b/plugins/notes/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "edit", "provides": [ diff --git a/plugins/platform-test/frontend/src/index.js b/plugins/platform-test/frontend/src/index.js index 594ce07..61d7b8d 100644 --- a/plugins/platform-test/frontend/src/index.js +++ b/plugins/platform-test/frontend/src/index.js @@ -63,6 +63,11 @@ containerEl.innerHTML = ''; containerEl.className = 'pt-root'; + + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } containerEl.__ptCleanup = []; function trackCleanup(fn) { @@ -79,7 +84,7 @@ var header = div('pt-header', [ span('pt-icon', '◉'), div('pt-title-group', [ - el('h2', { className: 'pt-plugin-name' }, ['Platform Diagnostics']), + el('h2', { className: 'pt-plugin-name' }, [tr('ui.diagnostics', null, 'Platform Diagnostics')]), el('p', { className: 'pt-plugin-id' }, [api.pluginId]), ]), span('pt-version', 'v' + (props && props.version ? props.version : '0.1.0')), @@ -88,7 +93,7 @@ /* ── Status badge ──────────────────────────────────────────── */ var badge = div('pt-badge pt-badge-success', [ el('span', {}, ['✅']), - el('span', {}, ['Frontend Bundle Loaded']), + el('span', {}, [tr('ui.bundleLoaded', null, 'Frontend Bundle Loaded')]), ]); var badgeRow = div('', [badge]); @@ -274,7 +279,7 @@ }); var bridgeCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Real Plugin API Bridge']), + el('h3', { className: 'pt-card-title' }, [tr('ui.apiBridge', null, 'Real Plugin API Bridge')]), el('ul', { className: 'pt-list' }, [ el('li', { className: 'pt-list-item' }, [ span('pt-list-label', 'Persisted setting'), @@ -361,7 +366,7 @@ }); var testsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Test Results']), + el('h3', { className: 'pt-card-title' }, [tr('ui.testResults', null, 'Test Results')]), summaryRow, testsList, ]); @@ -398,7 +403,7 @@ }); var capsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Registered Capabilities']), + el('h3', { className: 'pt-card-title' }, [tr('ui.capabilities', null, 'Registered Capabilities')]), capList, ]); @@ -421,7 +426,7 @@ }); var infoCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Plugin Info']), + el('h3', { className: 'pt-card-title' }, [tr('ui.pluginInfo', null, 'Plugin Info')]), infoList, ]); @@ -456,7 +461,7 @@ }); var apiCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Host API Methods']), + el('h3', { className: 'pt-card-title' }, [tr('ui.hostMethods', null, 'Host API Methods')]), apiStatusList, ]); @@ -584,7 +589,7 @@ trackCleanup(function () { stopMouseCapture(); }); var mouseCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']), + el('h3', { className: 'pt-card-title' }, [tr('ui.mouseInspector', null, '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.', ]), @@ -667,6 +672,11 @@ containerEl.innerHTML = ''; containerEl.className = 'pt-root'; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + /* ── Counter state (local, not persisted) ──────────────────── */ var counterState = { value: 0 }; @@ -674,7 +684,7 @@ var header = div('pt-header', [ span('pt-icon', '⚙️'), div('pt-title-group', [ - el('h2', { className: 'pt-plugin-name' }, ['Platform Test Settings']), + el('h2', { className: 'pt-plugin-name' }, [tr('ui.settings', null, 'Platform Test Settings')]), el('p', { className: 'pt-plugin-id' }, [api.pluginId]), ]), ]); @@ -697,24 +707,24 @@ var incrementBtn = el('button', { className: 'btn btn-primary', onClick: function () { counterState.value += 1; counterDisplay.firstChild.textContent = String(counterState.value); - }}, ['+ Increment']); + }}, [tr('ui.increment', null, '+ 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); - }}, ['− Decrement']); + }}, [tr('ui.decrement', null, '− Decrement')]); var resetBtn = el('button', { className: 'btn btn-secondary', onClick: function () { counterState.value = 0; counterDisplay.firstChild.textContent = '0'; - }}, ['↺ Reset']); + }}, [tr('ui.reset', null, '↺ 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' }, ['Interactive Counter (Local State)']), + el('h3', { className: 'pt-card-title' }, [tr('ui.counter', null, 'Interactive Counter (Local State)')]), counterDisplay, btnGroup, el('p', { style: { marginTop: '0.75rem', color: '#6c6c8a', fontSize: '0.7rem' } }, [ @@ -740,7 +750,7 @@ }); var settingsCard = div('pt-card', [ - el('h3', { className: 'pt-card-title' }, ['Plugin Settings (Demo)']), + el('h3', { className: 'pt-card-title' }, [tr('ui.demoSettings', null, '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.', diff --git a/plugins/platform-test/locales/en.json b/plugins/platform-test/locales/en.json new file mode 100644 index 0000000..b52d061 --- /dev/null +++ b/plugins/platform-test/locales/en.json @@ -0,0 +1,25 @@ +{ + "manifest.name": "Platform Test", + "manifest.description": "Runtime test plugin for verifying the Verstak platform: manifest loading, capability registration, contribution injection, event bus, and UI rendering.", + "contributions.views.verstak.platform-test.diagnostics.title": "Platform Diagnostics", + "contributions.commands.verstak.platform-test.run-tests.title": "Run Platform Tests", + "contributions.commands.verstak.platform-test.show-version.title": "Show Version Info", + "contributions.sidebarItems.verstak.platform-test.sidebar.title": "Platform Test", + "contributions.statusBarItems.verstak.platform-test.status.label": "[OK] All Tests Pass", + "contributions.settingsPanels.verstak.platform-test.settings.title": "Platform Test Settings", + "contributions.openProviders.verstak.platform-test.markdown-diagnostic.title": "Platform Test Markdown Diagnostic", + "ui.diagnostics": "Platform Diagnostics", + "ui.bundleLoaded": "Plugin Bundle Loaded", + "ui.apiBridge": "API Bridge", + "ui.testResults": "Test Results", + "ui.capabilities": "Granted Capabilities", + "ui.pluginInfo": "Plugin Info", + "ui.hostMethods": "Host Methods", + "ui.mouseInspector": "Mouse Event Inspector", + "ui.settings": "Platform Test Settings", + "ui.increment": "+ Increment", + "ui.decrement": "− Decrement", + "ui.reset": "Reset", + "ui.counter": "Interactive Counter", + "ui.demoSettings": "Demo Settings" +} diff --git a/plugins/platform-test/locales/ru.json b/plugins/platform-test/locales/ru.json new file mode 100644 index 0000000..31d3116 --- /dev/null +++ b/plugins/platform-test/locales/ru.json @@ -0,0 +1,25 @@ +{ + "manifest.name": "Тест платформы", + "manifest.description": "Диагностический плагин для проверки загрузки manifest, capabilities, contributions, шины событий и интерфейса Верстака.", + "contributions.views.verstak.platform-test.diagnostics.title": "Диагностика платформы", + "contributions.commands.verstak.platform-test.run-tests.title": "Запустить тесты платформы", + "contributions.commands.verstak.platform-test.show-version.title": "Показать сведения о версии", + "contributions.sidebarItems.verstak.platform-test.sidebar.title": "Тест платформы", + "contributions.statusBarItems.verstak.platform-test.status.label": "[OK] Все тесты пройдены", + "contributions.settingsPanels.verstak.platform-test.settings.title": "Настройки теста платформы", + "contributions.openProviders.verstak.platform-test.markdown-diagnostic.title": "Диагностика Markdown платформы", + "ui.diagnostics": "Диагностика платформы", + "ui.bundleLoaded": "Пакет плагина загружен", + "ui.apiBridge": "Мост API", + "ui.testResults": "Результаты тестов", + "ui.capabilities": "Предоставленные возможности", + "ui.pluginInfo": "Сведения о плагине", + "ui.hostMethods": "Методы хоста", + "ui.mouseInspector": "Инспектор событий мыши", + "ui.settings": "Настройки теста платформы", + "ui.increment": "+ Увеличить", + "ui.decrement": "− Уменьшить", + "ui.reset": "Сбросить", + "ui.counter": "Интерактивный счётчик", + "ui.demoSettings": "Демонстрационные настройки" +} diff --git a/plugins/platform-test/plugin.json b/plugins/platform-test/plugin.json index 90e0f7b..6648827 100644 --- a/plugins/platform-test/plugin.json +++ b/plugins/platform-test/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Runtime test plugin for verifying the Verstak platform: manifest loading, capability registration, contribution injection, event bus, and UI rendering.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "flask", "provides": [ diff --git a/plugins/search/frontend/src/index.js b/plugins/search/frontend/src/index.js index 797ba62..20f64a5 100644 --- a/plugins/search/frontend/src/index.js +++ b/plugins/search/frontend/src/index.js @@ -315,6 +315,11 @@ var statusEl = null; var alertEl = null; var resultsEl = null; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } + state.status = tr('ui.minChars', null, 'Enter at least 2 characters.'); function ensureLayout() { if (input) return; @@ -325,7 +330,7 @@ input = el('input', { className: 'search-input', type: 'search', - placeholder: 'Search files, folders, text', + placeholder: tr('ui.placeholder', null, 'Search files, folders, text'), value: state.query, 'data-search-input': 'query', onInput: function (event) { @@ -341,7 +346,7 @@ containerEl.appendChild(el('div', { className: 'search-toolbar' }, [ input, button, - el('span', { className: 'search-scope', title: rootPath || 'Vault' }, [rootPath || 'Vault']) + el('span', { className: 'search-scope', title: rootPath || tr('ui.vault', null, 'Vault') }, [rootPath || tr('ui.vault', null, 'Vault')]) ])); statusEl = el('div', { className: 'search-status' }); @@ -357,7 +362,7 @@ if (document.activeElement !== input && input.value !== state.query) { input.value = state.query; } - button.textContent = state.searching ? 'Searching...' : 'Search'; + button.textContent = state.searching ? tr('ui.searching', null, 'Searching...') : tr('ui.search', null, 'Search'); button.disabled = !!state.searching; statusEl.className = 'search-status' + (state.error ? ' error' : ''); statusEl.textContent = state.error || state.status; @@ -365,7 +370,7 @@ if (state.providerErrors && state.providerErrors.length) { if (typeof alertEl.removeAttribute === 'function') alertEl.removeAttribute('hidden'); alertEl.appendChild(el('details', {}, [ - el('summary', {}, ['Some plugin search providers did not respond']), + el('summary', {}, [tr('ui.providersFailed', null, 'Some plugin search providers did not respond')]), el('div', {}, [state.providerErrors.join('; ')]) ])); } else if (typeof alertEl.setAttribute === 'function') { @@ -373,7 +378,7 @@ } resultsEl.innerHTML = ''; if (!state.results.length) { - resultsEl.appendChild(el('div', { className: 'search-empty' }, [state.searching ? 'Searching...' : 'No results'])); + resultsEl.appendChild(el('div', { className: 'search-empty' }, [state.searching ? tr('ui.searching', null, 'Searching...') : tr('ui.noResults', null, 'No results')])); return; } state.results.forEach(function (result) { @@ -391,7 +396,7 @@ ]), result.openable ? el('button', { className: 'search-btn search-open-btn', - textContent: 'Open', + textContent: tr('ui.open', null, 'Open'), 'data-search-open': result.path, onClick: function () { api.workbench.openResource({ @@ -415,7 +420,7 @@ if (query.length < 2) { state.searching = false; state.results = []; - state.status = 'Enter at least 2 characters.'; + state.status = tr('ui.minChars', null, 'Enter at least 2 characters.'); state.error = ''; state.providerErrors = []; render(); @@ -432,7 +437,7 @@ state.query = String(state.query || '').trim(); if (state.query.length < 2) { state.results = []; - state.status = 'Enter at least 2 characters.'; + state.status = tr('ui.minChars', null, 'Enter at least 2 characters.'); state.error = ''; state.providerErrors = []; render(); @@ -441,7 +446,7 @@ state.searching = true; state.error = ''; state.providerErrors = []; - state.status = 'Searching...'; + state.status = tr('ui.searching', null, 'Searching...'); var seq = searchSeq + 1; searchSeq = seq; render(); @@ -451,7 +456,7 @@ var external = await runExternalProviders(api, rootPath, state.query, MAX_RESULTS - results.length); if (seq !== searchSeq) return; state.results = results.concat(external.results); - state.status = state.results.length + ' result' + (state.results.length === 1 ? '' : 's'); + state.status = tr('ui.count', { count: state.results.length }, state.results.length + ' result' + (state.results.length === 1 ? '' : 's')); state.providerErrors = external.errors; } catch (err) { if (seq !== searchSeq) return; @@ -507,6 +512,12 @@ setupIntegrations(); render(); + if (api.i18n && typeof api.i18n.onDidChangeLocale === 'function') { + cleanupFns.push(api.i18n.onDidChangeLocale(function () { + if (input) input.setAttribute('placeholder', tr('ui.placeholder', null, 'Search files, folders, text')); + render(); + })); + } containerEl.__verstakSearchCleanup = function () { if (searchTimer) clearTimeout(searchTimer); searchSeq += 1; diff --git a/plugins/search/locales/en.json b/plugins/search/locales/en.json new file mode 100644 index 0000000..e5e50a6 --- /dev/null +++ b/plugins/search/locales/en.json @@ -0,0 +1,16 @@ +{ + "manifest.name": "Search", + "manifest.description": "Workspace-scoped vault text search provider.", + "contributions.workspaceItems.verstak.search.workspace.title": "Search", + "contributions.commands.verstak.search.searchVaultText.title": "Search Vault Text", + "contributions.searchProviders.verstak.search.vault-text.label": "Vault Text Search", + "ui.minChars": "Enter at least 2 characters.", + "ui.placeholder": "Search files, folders, text", + "ui.vault": "Vault", + "ui.searching": "Searching...", + "ui.search": "Search", + "ui.providersFailed": "Some plugin search providers did not respond", + "ui.noResults": "No results", + "ui.open": "Open", + "ui.count": "{count} result(s)" +} diff --git a/plugins/search/locales/ru.json b/plugins/search/locales/ru.json new file mode 100644 index 0000000..5417d72 --- /dev/null +++ b/plugins/search/locales/ru.json @@ -0,0 +1,16 @@ +{ + "manifest.name": "Поиск", + "manifest.description": "Поиск текста в хранилище в пределах рабочего пространства.", + "contributions.workspaceItems.verstak.search.workspace.title": "Поиск", + "contributions.commands.verstak.search.searchVaultText.title": "Искать текст в хранилище", + "contributions.searchProviders.verstak.search.vault-text.label": "Поиск по тексту хранилища", + "ui.minChars": "Введите не менее 2 символов.", + "ui.placeholder": "Поиск файлов, папок и текста", + "ui.vault": "Хранилище", + "ui.searching": "Поиск...", + "ui.search": "Искать", + "ui.providersFailed": "Некоторые поставщики поиска не ответили", + "ui.noResults": "Ничего не найдено", + "ui.open": "Открыть", + "ui.count": "Результатов: {count}" +} diff --git a/plugins/search/plugin.json b/plugins/search/plugin.json index a319dcb..84c6458 100644 --- a/plugins/search/plugin.json +++ b/plugins/search/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Workspace-scoped vault text search provider.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "search", "provides": [ diff --git a/plugins/secrets/frontend/src/index.js b/plugins/secrets/frontend/src/index.js index 49eefee..af08c68 100644 --- a/plugins/secrets/frontend/src/index.js +++ b/plugins/secrets/frontend/src/index.js @@ -150,6 +150,10 @@ var unlocked = false; var statusText = ''; var statusError = false; + function tr(key, params, fallback) { + if (api && api.i18n && typeof api.i18n.t === 'function') return api.i18n.t(key, params, fallback); + return fallback || key; + } function setStatus(message, isError) { statusText = message || ''; @@ -162,13 +166,13 @@ className: 'secrets-input', type: 'password', 'data-secret-master-password': '', - placeholder: 'Master password' + placeholder: tr('ui.masterPassword', null, 'Master password') }); var confirmInput = initialized ? null : el('input', { className: 'secrets-input', type: 'password', 'data-secret-master-password-confirm': '', - placeholder: 'Repeat master password' + placeholder: tr('ui.repeatPassword', null, 'Repeat master password') }); var unlockBtn = el('button', { className: 'secrets-btn primary', @@ -176,7 +180,7 @@ 'data-secret-unlock': '', onClick: function () { if (!initialized && passwordInput.value !== confirmInput.value) { - setStatus('Master passwords do not match', true); + setStatus(tr('ui.passwordMismatch', null, 'Master passwords do not match'), true); return; } unlockBtn.disabled = true; @@ -189,17 +193,17 @@ setStatus((err && err.message) ? err.message : String(err), true); }); } - }, ['Unlock']); - if (!initialized) unlockBtn.textContent = 'Create master password'; + }, [tr('ui.unlock', null, 'Unlock')]); + if (!initialized) unlockBtn.textContent = tr('ui.createMaster', null, 'Create master password'); var rows = [ el('div', { className: 'secrets-row' }, [ - el('label', { className: 'secrets-label' }, ['Password']), + el('label', { className: 'secrets-label' }, [tr('ui.password', null, 'Password')]), passwordInput ]) ]; if (confirmInput) { rows.push(el('div', { className: 'secrets-row' }, [ - el('label', { className: 'secrets-label' }, ['Repeat']), + el('label', { className: 'secrets-label' }, [tr('ui.repeat', null, 'Repeat')]), confirmInput ])); } @@ -209,12 +213,12 @@ containerEl.appendChild(el('div', { className: 'secrets-root' }, [ el('div', { className: 'secrets-panel' }, [ el('div', { className: 'secrets-toolbar' }, [ - el('span', { className: 'secrets-title' }, ['Secrets']) + el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]) ]) ]), el('div', { className: 'secrets-main' }, [ el('div', { className: 'secrets-card' }, [ - el('h2', {}, [initialized ? 'Unlock secrets' : 'Create master password']), + el('h2', {}, [initialized ? tr('ui.unlockSecrets', null, 'Unlock secrets') : tr('ui.createMaster', null, 'Create master password')]), el('div', { className: 'secrets-form' }, rows) ]) ]) @@ -224,14 +228,14 @@ function renderList() { var children = [ el('div', { className: 'secrets-toolbar' }, [ - el('span', { className: 'secrets-title' }, ['Secrets']), + el('span', { className: 'secrets-title' }, [tr('ui.title', null, 'Secrets')]), el('span', { className: 'secrets-count' }, [String(records.length)]), el('span', { className: 'secrets-spacer' }), - el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, ['New']) + el('button', { className: 'secrets-btn', type: 'button', onClick: showNewSecret }, [tr('ui.new', null, 'New')]) ]) ]; if (!records.length) { - children.push(el('div', { className: 'secrets-empty' }, ['No secrets'])); + children.push(el('div', { className: 'secrets-empty' }, [tr('ui.empty', null, 'No secrets')])); return children; } groupRecords(records).forEach(function (group) { @@ -255,17 +259,17 @@ function renderSelected() { if (!selectedRecord) return el('div', { className: 'secrets-card' }, [ - el('h2', {}, ['Select a secret']) + el('h2', {}, [tr('ui.select', null, 'Select a secret')]) ]); return el('div', { className: 'secrets-card' }, [ el('h2', {}, [selectedRecord.title || selectedRecord.id]), el('table', { className: 'secrets-table' }, [ el('tbody', {}, [ - fieldRow('Group', scopeLabel(selectedRecord)), + fieldRow(tr('ui.group', null, 'Group'), scopeLabel(selectedRecord)), fieldRow('ID', selectedRecord.id), - fieldRow('Username', selectedRecord.username || ''), - fieldRow('Password', selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'), - fieldRow('Updated', selectedRecord.updatedAt || '') + fieldRow(tr('ui.username', null, 'Username'), selectedRecord.username || ''), + fieldRow(tr('ui.password', null, 'Password'), selectedValue ? selectedValue : '••••••••••••', selectedValue ? '' : 'secrets-hidden-value'), + fieldRow(tr('ui.updated', null, 'Updated'), selectedRecord.updatedAt || '') ]) ]), el('div', { className: 'secrets-actions' }, [ @@ -274,19 +278,19 @@ type: 'button', 'data-secret-copy-link': selectedRecord.id, onClick: function () { copySecretLink(selectedRecord.id); } - }, ['Copy secret link']), + }, [tr('ui.copyLink', null, 'Copy secret link')]), el('button', { className: 'secrets-btn', type: 'button', 'data-secret-edit': selectedRecord.id, onClick: function () { showEditSecret(); } - }, ['Edit']), + }, [tr('ui.edit', null, 'Edit')]), el('button', { className: 'secrets-btn danger', type: 'button', 'data-secret-delete': selectedRecord.id, onClick: function () { deleteSecret(selectedRecord.id); } - }, ['Delete']) + }, [tr('ui.delete', null, 'Delete')]) ]), el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText]) ]); @@ -301,28 +305,28 @@ function renderSecretForm(existing) { var isEdit = !!existing; - var title = el('input', { className: 'secrets-input', type: 'text', 'data-secret-title': '', placeholder: 'Title' }); + var title = el('input', { className: 'secrets-input', type: 'text', 'data-secret-title': '', placeholder: tr('ui.fieldTitle', null, 'Title') }); title.value = existing ? text(existing.title) : ''; var id = el('input', { className: 'secrets-input', type: 'text', placeholder: 'stable.id' }); id.value = existing ? text(existing.id) : ''; id.disabled = isEdit; - var username = el('input', { className: 'secrets-input', type: 'text', placeholder: 'optional username' }); + var username = el('input', { className: 'secrets-input', type: 'text', placeholder: tr('ui.optionalUsername', null, 'optional username') }); username.value = existing ? text(existing.username) : ''; - var value = el('textarea', { className: 'secrets-textarea', 'data-secret-value': '', placeholder: 'Secret value' }); + var value = el('textarea', { className: 'secrets-textarea', 'data-secret-value': '', placeholder: tr('ui.secretValue', null, 'Secret value') }); value.value = isEdit ? selectedValue : ''; var scope = el('select', { className: 'secrets-select' }, [ - el('option', { value: ScopeGlobal }, ['Global']), - el('option', { value: ScopeWorkspace }, [workspaceRoot || 'Workspace']) + el('option', { value: ScopeGlobal }, [tr('ui.global', null, 'Global')]), + el('option', { value: ScopeWorkspace }, [workspaceRoot || tr('ui.workspace', null, 'Workspace')]) ]); scope.value = existing && existing.scope && existing.scope.kind ? existing.scope.kind : (workspaceRoot ? ScopeWorkspace : ScopeGlobal); return el('div', { className: 'secrets-card' }, [ - el('h2', {}, [isEdit ? 'Edit secret' : 'New secret']), + el('h2', {}, [isEdit ? tr('ui.editSecret', null, 'Edit secret') : tr('ui.newSecret', null, 'New secret')]), el('div', { className: 'secrets-form' }, [ - el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Title']), title]), + el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.fieldTitle', null, 'Title')]), title]), el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['ID']), id]), - el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Username']), username]), - el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Scope']), scope]), - el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, ['Value']), value]), + el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.username', null, 'Username')]), username]), + el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.scope', null, 'Scope')]), scope]), + el('div', { className: 'secrets-row' }, [el('label', { className: 'secrets-label' }, [tr('ui.value', null, 'Value')]), value]), el('div', { className: 'secrets-actions' }, [ el('button', { className: 'secrets-btn primary', @@ -345,8 +349,8 @@ setStatus((err && err.message) ? err.message : String(err), true); }); } - }, ['Save']), - el('button', { className: 'secrets-btn', type: 'button', onClick: function () { mode = 'selected'; render(); } }, ['Cancel']) + }, [tr('ui.save', null, 'Save')]), + el('button', { className: 'secrets-btn', type: 'button', onClick: function () { mode = 'selected'; render(); } }, [tr('ui.cancel', null, 'Cancel')]) ]), el('div', { className: statusError ? 'secrets-status error' : 'secrets-status' }, [statusText]) ]) @@ -451,8 +455,13 @@ renderLocked(); }); + var localeUnsubscribe = api.i18n && typeof api.i18n.onDidChangeLocale === 'function' + ? api.i18n.onDidChangeLocale(render) + : null; + containerEl.__secretsCleanup = function () { disposed = true; + if (typeof localeUnsubscribe === 'function') localeUnsubscribe(); }; }, diff --git a/plugins/secrets/locales/en.json b/plugins/secrets/locales/en.json new file mode 100644 index 0000000..352516e --- /dev/null +++ b/plugins/secrets/locales/en.json @@ -0,0 +1,36 @@ +{ + "manifest.name": "Secrets", + "manifest.description": "Encrypted global and workspace-scoped secret manager.", + "contributions.openProviders.verstak.secrets.secret.title": "Secrets", + "contributions.settingsPanels.verstak.secrets.settings.title": "Secrets", + "contributions.workspaceItems.verstak.secrets.workspace.title": "Secrets", + "ui.masterPassword": "Master password", + "ui.repeatPassword": "Repeat master password", + "ui.passwordMismatch": "Master passwords do not match", + "ui.unlock": "Unlock", + "ui.createMaster": "Create master password", + "ui.password": "Password", + "ui.repeat": "Repeat", + "ui.title": "Secrets", + "ui.unlockSecrets": "Unlock secrets", + "ui.new": "New", + "ui.empty": "No secrets", + "ui.select": "Select a secret", + "ui.group": "Group", + "ui.username": "Username", + "ui.updated": "Updated", + "ui.copyLink": "Copy secret link", + "ui.edit": "Edit", + "ui.delete": "Delete", + "ui.fieldTitle": "Title", + "ui.optionalUsername": "optional username", + "ui.secretValue": "Secret value", + "ui.global": "Global", + "ui.workspace": "Workspace", + "ui.editSecret": "Edit secret", + "ui.newSecret": "New secret", + "ui.scope": "Scope", + "ui.value": "Value", + "ui.save": "Save", + "ui.cancel": "Cancel" +} diff --git a/plugins/secrets/locales/ru.json b/plugins/secrets/locales/ru.json new file mode 100644 index 0000000..ab19dda --- /dev/null +++ b/plugins/secrets/locales/ru.json @@ -0,0 +1,36 @@ +{ + "manifest.name": "Секреты", + "manifest.description": "Зашифрованное хранилище общих секретов и секретов рабочих пространств.", + "contributions.openProviders.verstak.secrets.secret.title": "Секреты", + "contributions.settingsPanels.verstak.secrets.settings.title": "Секреты", + "contributions.workspaceItems.verstak.secrets.workspace.title": "Секреты", + "ui.masterPassword": "Мастер-пароль", + "ui.repeatPassword": "Повторите мастер-пароль", + "ui.passwordMismatch": "Мастер-пароли не совпадают", + "ui.unlock": "Разблокировать", + "ui.createMaster": "Создать мастер-пароль", + "ui.password": "Пароль", + "ui.repeat": "Повтор", + "ui.title": "Секреты", + "ui.unlockSecrets": "Разблокировать секреты", + "ui.new": "Новый", + "ui.empty": "Секретов нет", + "ui.select": "Выберите секрет", + "ui.group": "Группа", + "ui.username": "Имя пользователя", + "ui.updated": "Обновлён", + "ui.copyLink": "Копировать ссылку на секрет", + "ui.edit": "Изменить", + "ui.delete": "Удалить", + "ui.fieldTitle": "Название", + "ui.optionalUsername": "необязательное имя пользователя", + "ui.secretValue": "Значение секрета", + "ui.global": "Общий", + "ui.workspace": "Рабочее пространство", + "ui.editSecret": "Изменить секрет", + "ui.newSecret": "Новый секрет", + "ui.scope": "Область", + "ui.value": "Значение", + "ui.save": "Сохранить", + "ui.cancel": "Отмена" +} diff --git a/plugins/secrets/plugin.json b/plugins/secrets/plugin.json index 400eaec..c07c632 100644 --- a/plugins/secrets/plugin.json +++ b/plugins/secrets/plugin.json @@ -5,6 +5,7 @@ "version": "0.1.0", "apiVersion": "0.1.0", "description": "Encrypted global and workspace-scoped secret manager.", + "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "key-round", "provides": [ diff --git a/plugins/sync/frontend/src/SyncSettings.svelte b/plugins/sync/frontend/src/SyncSettings.svelte index 3634f31..5e47601 100644 --- a/plugins/sync/frontend/src/SyncSettings.svelte +++ b/plugins/sync/frontend/src/SyncSettings.svelte @@ -1,4 +1,6 @@
Synchronize your vault across devices.
+{tr('ui.description', null, 'Synchronize your vault across devices.')}
{#if errorMsg}