diff --git a/plugins/browser-inbox/frontend/src/index.js b/plugins/browser-inbox/frontend/src/index.js index be1699d..d42513e 100644 --- a/plugins/browser-inbox/frontend/src/index.js +++ b/plugins/browser-inbox/frontend/src/index.js @@ -1,5 +1,5 @@ /* =========================================================== - Browser Inbox Plugin — Verstak v2 Frontend Bundle + Browser Plugin — Verstak v2 Frontend Bundle Contract: window.VerstakPluginRegister(id, { components }) =========================================================== */ @@ -146,7 +146,7 @@ function scopeFromProps(props) { var workspaceRoot = workspaceFromProps(props); if (!workspaceRoot) { - return { mode: 'global', key: GLOBAL_KEY, label: 'All workspaces', workspaceRoot: '' }; + return { mode: 'global', key: GLOBAL_KEY, label: '', workspaceRoot: '' }; } return { mode: 'workspace', @@ -161,13 +161,13 @@ return value === 'selection' || value === 'link' || value === 'file' || value === 'page' ? value : 'page'; } - function displayTitle(capture) { + function displayTitle(capture, fallbackTitle) { if (capture && capture.kind === 'file' && capture.fileName) return capture.fileName; - return capture.title || capture.url || capture.captureId || 'Untitled capture'; + return capture.title || capture.url || capture.captureId || fallbackTitle || ''; } - function noteTitle(capture) { - return text((capture && (capture.title || capture.domain || capture.captureId)) || 'Browser Capture').trim() || 'Browser Capture'; + function noteTitle(capture, fallbackTitle) { + return text((capture && (capture.title || capture.domain || capture.captureId)) || fallbackTitle).trim() || text(fallbackTitle).trim(); } function safeNoteFilename(title) { @@ -199,8 +199,8 @@ return base; } - function captureToMarkdown(capture) { - var title = noteTitle(capture); + function captureToMarkdown(capture, fallbackTitle) { + var title = noteTitle(capture, fallbackTitle); var lines = ['# ' + title, '']; if (capture && capture.url) lines.push('Source: ' + capture.url); if (capture && capture.capturedAt) lines.push('Captured: ' + capture.capturedAt); @@ -363,7 +363,7 @@ var scope = scopeFromProps(props || {}); var captures = []; var selectedId = ''; - var statusText = 'Connecting to receiver events...'; + var statusText = ''; var statusClass = ''; var disposed = false; var unsubscribers = []; @@ -377,6 +377,18 @@ return fallback || key; } + function localizedItemCount(count) { + var locale = api && api.i18n && typeof api.i18n.getLocale === 'function' ? api.i18n.getLocale() : 'en'; + if (locale === 'ru') { + var mod10 = count % 10; + var mod100 = count % 100; + if (mod10 === 1 && mod100 !== 11) return tr('ui.items.one', { count: count }, count + ' материал'); + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return tr('ui.items.few', { count: count }, count + ' материала'); + return tr('ui.items.many', { count: count }, count + ' материалов'); + } + return tr(count === 1 ? 'ui.items.one' : 'ui.items.other', { count: count }, count + (count === 1 ? ' item' : ' items')); + } + function reportError(key, fallback, err) { if (typeof console !== 'undefined' && typeof console.warn === 'function') { console.warn('[verstak.browser-inbox] ' + key, err); @@ -389,14 +401,14 @@ 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' ? tr('ui.title', null, 'Browser Inbox') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser Inbox · ' + scope.label) }); + var titleEl = el('span', { className: 'browser-inbox-title', textContent: scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + 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' }); var statusFilterEl = el('select', { className: 'browser-inbox-select', 'data-browser-inbox-filter': 'status', - 'aria-label': 'Capture status filter', + 'aria-label': tr('ui.statusFilter', null, 'Material status filter'), onChange: function (event) { statusFilter = text(event && event.target && event.target.value) || 'all'; selectedId = ''; @@ -412,7 +424,7 @@ var workspaceFilterEl = el('select', { className: 'browser-inbox-select', 'data-browser-inbox-filter': 'workspace', - 'aria-label': 'Workspace filter', + 'aria-label': tr('ui.workspaceFilter', null, 'Deal filter'), onChange: function (event) { workspaceFilter = cleanWorkspace(event && event.target && event.target.value); selectedId = ''; @@ -424,7 +436,7 @@ type: 'search', placeholder: tr('ui.search', null, 'Search captures'), 'data-browser-inbox-filter': 'search', - 'aria-label': 'Search captures', + 'aria-label': tr('ui.search', null, 'Search captures'), onInput: function (event) { searchQuery = text(event && event.target && event.target.value).trim().toLowerCase(); selectedId = ''; @@ -482,7 +494,7 @@ function renderWorkspaceFilterOptions() { if (scope.mode !== 'global') return; workspaceFilterEl.innerHTML = ''; - workspaceFilterEl.appendChild(option('', 'All workspaces')); + workspaceFilterEl.appendChild(option('', tr('ui.allDeals', null, 'All Deals'))); workspaceRoots().forEach(function (root) { workspaceFilterEl.appendChild(option(root, root)); }); @@ -506,7 +518,7 @@ function publishMutation(action, payload, verify, verifySettings) { if (!api || !api.events || typeof api.events.publish !== 'function') { - reportError('ui.saveError', 'Could not update the browser inbox. Please try again.'); + reportError('ui.saveError', 'Could not update browser materials. Please try again.'); return Promise.resolve(false); } return api.events.publish(MUTATION_EVENT, Object.assign({ action: action }, payload || {})).then(function () { @@ -524,7 +536,7 @@ } return true; }).catch(function (err) { - reportError('ui.saveError', 'Could not update the browser inbox. Please try again.', err); + reportError('ui.saveError', 'Could not update browser materials. Please try again.', err); return false; }); } @@ -551,7 +563,9 @@ var ids = scope.mode === 'global' ? captures.map(function (capture) { return capture.captureId; }) : captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).map(function (capture) { return capture.captureId; }); - return archiveCaptures(ids, scope.mode === 'global' ? 'Inbox archived' : 'Workspace captures archived'); + return archiveCaptures(ids, scope.mode === 'global' + ? tr('ui.inboxArchived', null, 'Inbox archived') + : tr('ui.workspaceCapturesArchived', null, 'Deal materials archived')); } function selectedCapture() { @@ -569,10 +583,8 @@ }); if (existing) return Promise.resolve(); selectedId = capture.captureId; - statusText = 'Capture received'; + statusText = tr('ui.captureReceived', null, 'Material received'); statusClass = ''; - statusText = 'Could not load received capture from storage'; - statusClass = 'error'; render(); return Promise.resolve(); } @@ -598,14 +610,16 @@ }).then(function (saved) { if (!saved) return; if (workspaceRoot && workspaceOptions.indexOf(workspaceRoot) === -1) workspaceOptions.push(workspaceRoot); - statusText = workspaceRoot ? 'Capture assigned to ' + workspaceRoot : 'Capture is unassigned'; + statusText = workspaceRoot + ? tr('ui.assignedToWorkspace', { workspace: workspaceRoot }, 'Material assigned to ' + workspaceRoot) + : tr('ui.captureUnassigned', null, 'Material is unassigned'); statusClass = ''; render(); }); } function archiveCapture(captureId) { - return archiveCaptures([captureId], 'Capture archived'); + return archiveCaptures([captureId], tr('ui.captureArchived', null, 'Material archived')); } function restoreCapture(captureId) { @@ -615,7 +629,7 @@ }); }).then(function (saved) { if (!saved) return; - statusText = 'Capture restored to Inbox'; + statusText = tr('ui.captureRestored', null, 'Material restored to Inbox'); statusClass = ''; render(); }); @@ -627,7 +641,7 @@ }).then(function (saved) { if (!saved) return; if (selectedId === captureId) selectedId = ''; - statusText = 'Capture permanently deleted'; + statusText = tr('ui.captureDeleted', null, 'Material permanently deleted'); statusClass = ''; render(); }); @@ -643,7 +657,9 @@ }); }).then(function (saved) { if (!saved) return; - statusText = processed ? 'Capture marked processed' : 'Capture marked unprocessed'; + statusText = processed + ? tr('ui.captureProcessed', null, 'Material marked processed') + : tr('ui.captureUnprocessed', null, 'Material marked unprocessed'); statusClass = ''; render(); }); @@ -655,12 +671,12 @@ reportError('ui.createNoteError', 'Could not create the note. Please try again.'); return Promise.resolve(); } - var title = noteTitle(capture); + var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')); var notePath = capture.workspaceRootPath + '/Notes/' + safeNoteFilename(title); - statusText = 'Creating note...'; + statusText = tr('ui.creatingNote', null, 'Creating note...'); statusClass = ''; render(); - return api.files.writeText(notePath, captureToMarkdown(capture), { + return api.files.writeText(notePath, captureToMarkdown(capture, tr('ui.untitledCapture', null, 'Untitled material')), { createIfMissing: true, overwrite: false }).then(function () { @@ -677,7 +693,7 @@ } return undefined; }).then(function () { - statusText = 'Created note: ' + notePath; + statusText = tr('ui.noteCreated', { path: notePath }, 'Note created: ' + notePath); statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { @@ -691,8 +707,8 @@ reportError('ui.createLinkError', 'Could not create the link. Please try again.'); return Promise.resolve(); } - var title = noteTitle(capture); - statusText = 'Creating link...'; + var title = noteTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')); + statusText = tr('ui.creatingLink', null, 'Creating link...'); statusClass = ''; render(); function writeLink(number) { @@ -725,7 +741,7 @@ } return linkPath; }).then(function () { - statusText = 'Created link'; + statusText = tr('ui.linkCreated', null, 'Link created'); statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { @@ -748,7 +764,7 @@ } var fileName = safeFileFilename(capture.fileName); var filePath = capture.workspaceRootPath + '/Files/' + fileName; - statusText = 'Creating file...'; + statusText = tr('ui.creatingFile', null, 'Creating file...'); statusClass = ''; render(); var writeOptions = { @@ -775,7 +791,7 @@ } return undefined; }).then(function () { - statusText = 'Created file: ' + filePath; + statusText = tr('ui.fileCreated', { path: filePath }, 'File created: ' + filePath); statusClass = ''; return archiveCapture(capture.captureId); }).catch(function (err) { @@ -788,8 +804,8 @@ var visible = visibleCaptures(); if (visible.length === 0) { var emptyText = captures.length === 0 - ? 'No browser captures yet. Keep this view open, then send a page, selection, or link from the extension.' - : 'No captures match the current filters.'; + ? tr('ui.empty', null, 'No browser materials yet. Send a page, selection, or link from the extension.') + : tr('ui.emptyFiltered', null, 'No materials match the current filters.'); listEl.appendChild(el('div', { className: 'browser-inbox-empty', textContent: emptyText })); return; } @@ -805,20 +821,22 @@ }, [ el('div', { className: 'browser-inbox-row-head' }, [ el('span', { className: 'browser-inbox-kind', textContent: capture.kind }), - el('span', { className: 'browser-inbox-row-title', textContent: displayTitle(capture) }) + el('span', { className: 'browser-inbox-row-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) }) ]), el('div', { className: 'browser-inbox-row-url', textContent: capture.url || capture.domain || capture.captureId }) ]); row.appendChild(el('div', { className: 'browser-inbox-row-meta' }, [ el('span', { className: 'browser-inbox-badge' + (workspaceRoot ? '' : ' unassigned'), - textContent: workspaceRoot || 'Unassigned' + textContent: workspaceRoot || tr('ui.unassigned', null, 'Unassigned') }), el('span', { className: 'browser-inbox-badge' + (capture.processed ? ' processed' : ''), - textContent: capture.processed ? 'Processed' : 'Unprocessed' + textContent: capture.processed + ? tr('ui.processed', null, 'Processed') + : tr('ui.unprocessed', null, 'Unprocessed') }), - capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: 'Archived' }) : null + capture.globalState === 'archived' ? el('span', { className: 'browser-inbox-badge', textContent: tr('ui.archive', null, 'Archive') }) : null ])); if (capture.text) { row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text })); @@ -835,45 +853,47 @@ return; } selectedId = capture.captureId; - detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture) })); + detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: displayTitle(capture, tr('ui.untitledCapture', null, 'Untitled material')) })); detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [ 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-label', textContent: tr('ui.url', null, 'URL') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }), 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: 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-label', textContent: tr('ui.browser', null, 'Browser') }), el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' }), 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-value', textContent: capture.workspaceRootPath || tr('ui.unassigned', null, 'Unassigned') }), 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' }) + el('div', { className: 'browser-inbox-meta-value', textContent: capture.processed + ? tr('ui.processed', null, 'Processed') + : tr('ui.unprocessed', null, 'Unprocessed') }) ])); var assignmentSelect = el('select', { className: 'browser-inbox-select', 'data-browser-inbox-assignment': capture.captureId, - 'aria-label': 'Assign capture workspace', + 'aria-label': tr('ui.assignment', null, 'Assign to Deal'), onChange: function (event) { assignWorkspace(capture.captureId, event && event.target && event.target.value); } }); - assignmentSelect.appendChild(option('', 'Unassigned')); + assignmentSelect.appendChild(option('', tr('ui.unassigned', null, 'Unassigned'))); workspaceRoots().forEach(function (workspaceRoot) { assignmentSelect.appendChild(option(workspaceRoot, workspaceRoot)); }); assignmentSelect.value = capture.workspaceRootPath || ''; var assignmentControls = [ - el('span', { className: 'browser-inbox-meta-label', textContent: 'Assign workspace' }), + el('span', { className: 'browser-inbox-meta-label', textContent: tr('ui.assignment', null, 'Assign to Deal') }), assignmentSelect ]; if (capture.workspaceRootPath) { assignmentControls.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'clear-assignment', - textContent: 'Clear assignment', + textContent: tr('ui.clearAssignment', null, 'Clear assignment'), onClick: function () { assignWorkspace(capture.captureId, ''); } @@ -881,7 +901,7 @@ } detailEl.appendChild(el('div', { className: 'browser-inbox-assignment' }, assignmentControls)); if (!capture.workspaceRootPath) { - detailEl.appendChild(el('div', { className: 'browser-inbox-detail-note', textContent: 'Assign a workspace before creating a note, link, or file.' })); + detailEl.appendChild(el('div', { className: 'browser-inbox-detail-note', textContent: tr('ui.assignBeforeCreation', null, 'Assign a Deal before creating a note, link, or file.') })); } if (capture.text) { detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.text })); @@ -893,7 +913,9 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'toggle-processed', - textContent: capture.processed ? 'Mark Unprocessed' : 'Mark Processed', + textContent: capture.processed + ? tr('ui.markUnprocessed', null, 'Mark unprocessed') + : tr('ui.markProcessed', null, 'Mark processed'), onClick: function () { setProcessed(capture.captureId, !capture.processed); } @@ -902,7 +924,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'open-link', - textContent: 'Open link', + textContent: tr('ui.openLink', null, 'Open link'), onClick: function () { openCaptureURL(capture); } @@ -942,7 +964,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'restore', - textContent: 'Restore to Inbox', + textContent: tr('ui.restore', null, 'Restore to Inbox'), onClick: function () { restoreCapture(capture.captureId); } @@ -951,7 +973,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'archive', - textContent: 'Archive', + textContent: tr('ui.archive', null, 'Archive'), onClick: function () { archiveCapture(capture.captureId); } @@ -960,7 +982,7 @@ actionButtons.push(el('button', { className: 'browser-inbox-btn danger', 'data-browser-inbox-action': 'delete-permanently', - textContent: 'Delete permanently', + textContent: tr('ui.deletePermanently', null, 'Delete permanently'), onClick: function () { permanentlyDeleteCapture(capture.captureId); } @@ -972,8 +994,11 @@ var visibleCount = visibleCaptures().length; var total = captures.length; countEl.textContent = visibleCount === total - ? total + ' item' + (total === 1 ? '' : 's') - : visibleCount + ' of ' + total + ' items'; + ? localizedItemCount(total) + : tr('ui.items.filtered', { + visible: localizedItemCount(visibleCount), + total: localizedItemCount(total) + }, localizedItemCount(visibleCount) + ' of ' + localizedItemCount(total)); var scopeCount = scope.mode === 'global' ? total : captures.filter(function (capture) { return capture.workspaceRootPath === scope.workspaceRoot; }).length; @@ -1026,7 +1051,7 @@ }); }); }).catch(function (err) { - reportError('ui.loadError', 'Could not load the browser inbox. Please try again.', err); + reportError('ui.loadError', 'Could not load browser materials. Please try again.', err); }); } @@ -1066,13 +1091,13 @@ }).then(function (saved) { if (!saved) return; selectedId = received.captureId; - statusText = 'Capture received'; + statusText = tr('ui.captureReceived', null, 'Material received'); statusClass = ''; render(); }); } selectedId = received.captureId; - statusText = 'Capture received'; + statusText = tr('ui.captureReceived', null, 'Material received'); statusClass = ''; render(); return undefined; @@ -1081,7 +1106,9 @@ if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe); }); })).then(function () { - statusText = scope.mode === 'global' ? 'Receiver ready for all workspaces' : 'Receiver ready for workspace'; + statusText = scope.mode === 'global' + ? tr('ui.receiverReadyAll', null, 'Receiver ready for all Deals') + : tr('ui.receiverReadyWorkspace', null, 'Receiver ready for this Deal'); statusClass = ''; }).catch(function (err) { reportError('ui.receiverError', 'The browser receiver is unavailable. Please try again.', err); @@ -1098,8 +1125,11 @@ }); 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); + titleEl.textContent = scope.mode === 'global' ? tr('ui.title', null, 'Browser') : tr('ui.workspaceTitle', { workspace: scope.label }, 'Browser · ' + scope.label); searchInput.setAttribute('placeholder', tr('ui.search', null, 'Search captures')); + statusFilterEl.setAttribute('aria-label', tr('ui.statusFilter', null, 'Material status filter')); + workspaceFilterEl.setAttribute('aria-label', tr('ui.workspaceFilter', null, 'Deal filter')); + searchInput.setAttribute('aria-label', tr('ui.search', null, 'Search captures')); clearBtn.textContent = tr('ui.clear', null, 'Clear'); render(); }); diff --git a/plugins/browser-inbox/locales/en.json b/plugins/browser-inbox/locales/en.json index 4e4d1d6..ab37842 100644 --- a/plugins/browser-inbox/locales/en.json +++ b/plugins/browser-inbox/locales/en.json @@ -1,25 +1,26 @@ { - "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", + "manifest.name": "Browser", + "manifest.description": "Global browser materials with explicit Deal assignment delivered through the local receiver event protocol.", + "contributions.views.verstak.browser-inbox.view.title": "Browser", + "contributions.sidebarItems.verstak.browser-inbox.sidebar.title": "Browser", + "contributions.workspaceItems.verstak.browser-inbox.workspace.title": "Browser", + "contributions.settingsPanels.verstak.browser-inbox.settings.title": "Browser", "ui.connecting": "Connecting to receiver events...", - "ui.title": "Browser Inbox", - "ui.workspaceTitle": "Browser Inbox · {workspace}", + "ui.title": "Browser", + "ui.workspaceTitle": "Browser · {workspace}", "ui.allCaptures": "All captures", + "ui.allDeals": "All Deals", "ui.unassigned": "Unassigned", "ui.unprocessed": "Unprocessed", "ui.processed": "Processed", "ui.search": "Search captures", "ui.clear": "Clear", - "ui.assignedHere": "Assigned to this workspace", + "ui.assignedHere": "Assigned to this Deal", "ui.selectCapture": "Select a capture to inspect it.", "ui.kind": "Kind", "ui.domain": "Domain", "ui.captured": "Captured", - "ui.workspace": "Workspace", + "ui.workspace": "Deal", "ui.status": "Status", "ui.createNote": "Create Note", "ui.createLink": "Create Link", @@ -36,14 +37,54 @@ "ui.rotateConfirm": "Rotate pairing token?", "ui.rotating": "Rotating...", "ui.tokenRotated": "Token rotated", - "ui.saveError": "Could not update the browser inbox. Please try again.", + "ui.saveError": "Could not update browser materials. Please try again.", "ui.createNoteError": "Could not create the note. Please try again.", "ui.createLinkError": "Could not create the link. Please try again.", "ui.openLinkError": "Could not open the link. Please try again.", "ui.createFileError": "Could not create the file. Please try again.", - "ui.loadError": "Could not load the browser inbox. Please try again.", + "ui.loadError": "Could not load browser materials. Please try again.", "ui.receiverError": "The browser receiver is unavailable. Please try again.", "ui.pairingLoadError": "Could not load browser connection settings. Please try again.", "ui.clipboardError": "Could not copy to the clipboard. Please try again.", - "ui.tokenRotateError": "Could not rotate the pairing token. Please try again." + "ui.tokenRotateError": "Could not rotate the pairing token. Please try again.", + "ui.statusFilter": "Material status filter", + "ui.workspaceFilter": "Deal filter", + "ui.assignment": "Assign to Deal", + "ui.clearAssignment": "Clear assignment", + "ui.empty": "No browser materials yet. Send a page, selection, or link from the extension.", + "ui.emptyFiltered": "No materials match the current filters.", + "ui.untitledCapture": "Untitled material", + "ui.url": "URL", + "ui.browser": "Browser", + "ui.assignBeforeCreation": "Assign a Deal before creating a note, link, or file.", + "ui.markProcessed": "Mark processed", + "ui.markUnprocessed": "Mark unprocessed", + "ui.openLink": "Open link", + "ui.restore": "Restore to Inbox", + "ui.archive": "Archive", + "ui.deletePermanently": "Delete permanently", + "ui.captureReceived": "Material received", + "ui.captureLoadError": "Could not load the received material.", + "ui.inboxArchived": "Inbox archived", + "ui.workspaceCapturesArchived": "Deal materials archived", + "ui.assignedToWorkspace": "Material assigned to {workspace}", + "ui.captureUnassigned": "Material is unassigned", + "ui.captureArchived": "Material archived", + "ui.captureRestored": "Material restored to Inbox", + "ui.captureDeleted": "Material permanently deleted", + "ui.captureProcessed": "Material marked processed", + "ui.captureUnprocessed": "Material marked unprocessed", + "ui.creatingNote": "Creating note...", + "ui.noteCreated": "Note created: {path}", + "ui.creatingLink": "Creating link...", + "ui.linkCreated": "Link created", + "ui.creatingFile": "Creating file...", + "ui.fileCreated": "File created: {path}", + "ui.receiverReadyAll": "Receiver ready for all Deals", + "ui.receiverReadyWorkspace": "Receiver ready for this Deal", + "ui.items.one": "{count} item", + "ui.items.few": "{count} items", + "ui.items.many": "{count} items", + "ui.items.other": "{count} items", + "ui.items.filtered": "{visible} of {total}" } diff --git a/plugins/browser-inbox/locales/ru.json b/plugins/browser-inbox/locales/ru.json index 7f0abbd..7ab9143 100644 --- a/plugins/browser-inbox/locales/ru.json +++ b/plugins/browser-inbox/locales/ru.json @@ -1,25 +1,26 @@ { - "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": "Входящие из браузера", + "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.title": "Браузер", + "ui.workspaceTitle": "Браузер · {workspace}", "ui.allCaptures": "Все материалы", + "ui.allDeals": "Все Дела", "ui.unassigned": "Не назначено", "ui.unprocessed": "Не обработано", "ui.processed": "Обработано", "ui.search": "Поиск материалов", "ui.clear": "Очистить", - "ui.assignedHere": "Назначено этому рабочему пространству", + "ui.assignedHere": "Назначено этому Делу", "ui.selectCapture": "Выберите материал для просмотра.", "ui.kind": "Тип", "ui.domain": "Домен", "ui.captured": "Получено", - "ui.workspace": "Рабочее пространство", + "ui.workspace": "Дело", "ui.status": "Состояние", "ui.createNote": "Создать заметку", "ui.createLink": "Создать ссылку", @@ -36,14 +37,54 @@ "ui.rotateConfirm": "Сменить токен сопряжения?", "ui.rotating": "Смена токена...", "ui.tokenRotated": "Токен изменён", - "ui.saveError": "Не удалось обновить входящие из браузера. Повторите попытку.", + "ui.saveError": "Не удалось обновить материалы браузера. Повторите попытку.", "ui.createNoteError": "Не удалось создать заметку. Повторите попытку.", "ui.createLinkError": "Не удалось создать ссылку. Повторите попытку.", "ui.openLinkError": "Не удалось открыть ссылку. Повторите попытку.", "ui.createFileError": "Не удалось создать файл. Повторите попытку.", - "ui.loadError": "Не удалось загрузить входящие из браузера. Повторите попытку.", + "ui.loadError": "Не удалось загрузить материалы браузера. Повторите попытку.", "ui.receiverError": "Приёмник браузера недоступен. Повторите попытку.", "ui.pairingLoadError": "Не удалось загрузить параметры подключения браузера. Повторите попытку.", "ui.clipboardError": "Не удалось скопировать в буфер обмена. Повторите попытку.", - "ui.tokenRotateError": "Не удалось сменить токен сопряжения. Повторите попытку." + "ui.tokenRotateError": "Не удалось сменить токен сопряжения. Повторите попытку.", + "ui.statusFilter": "Фильтр состояния материала", + "ui.workspaceFilter": "Фильтр Дела", + "ui.assignment": "Назначить Делу", + "ui.clearAssignment": "Снять назначение", + "ui.empty": "Пока нет материалов из браузера. Отправьте страницу, выделенный текст или ссылку с помощью расширения.", + "ui.emptyFiltered": "Нет материалов по текущим фильтрам.", + "ui.untitledCapture": "Материал без названия", + "ui.url": "URL", + "ui.browser": "Браузер", + "ui.assignBeforeCreation": "Назначьте Дело перед созданием заметки, ссылки или файла.", + "ui.markProcessed": "Отметить обработанным", + "ui.markUnprocessed": "Отметить необработанным", + "ui.openLink": "Открыть ссылку", + "ui.restore": "Вернуть во входящие", + "ui.archive": "Архивировать", + "ui.deletePermanently": "Удалить навсегда", + "ui.captureReceived": "Материал получен", + "ui.captureLoadError": "Не удалось загрузить полученный материал.", + "ui.inboxArchived": "Входящие архивированы", + "ui.workspaceCapturesArchived": "Материалы Дела архивированы", + "ui.assignedToWorkspace": "Материал назначен Делу «{workspace}»", + "ui.captureUnassigned": "Материал не назначен", + "ui.captureArchived": "Материал архивирован", + "ui.captureRestored": "Материал возвращён во входящие", + "ui.captureDeleted": "Материал удалён навсегда", + "ui.captureProcessed": "Материал отмечен обработанным", + "ui.captureUnprocessed": "Материал отмечен необработанным", + "ui.creatingNote": "Создание заметки...", + "ui.noteCreated": "Заметка создана: {path}", + "ui.creatingLink": "Создание ссылки...", + "ui.linkCreated": "Ссылка создана", + "ui.creatingFile": "Создание файла...", + "ui.fileCreated": "Файл создан: {path}", + "ui.receiverReadyAll": "Приёмник готов для всех Дел", + "ui.receiverReadyWorkspace": "Приёмник готов для этого Дела", + "ui.items.one": "{count} материал", + "ui.items.few": "{count} материала", + "ui.items.many": "{count} материалов", + "ui.items.other": "{count} материала", + "ui.items.filtered": "{visible} из {total}" } diff --git a/plugins/browser-inbox/plugin.json b/plugins/browser-inbox/plugin.json index adc6586..8e76534 100644 --- a/plugins/browser-inbox/plugin.json +++ b/plugins/browser-inbox/plugin.json @@ -1,10 +1,10 @@ { "schemaVersion": 1, "id": "verstak.browser-inbox", - "name": "Browser Inbox", + "name": "Browser", "version": "0.1.0", "apiVersion": "0.1.0", - "description": "Global browser capture queue with explicit workspace assignment delivered through the local receiver event protocol.", + "description": "Global browser materials with explicit Deal assignment delivered through the local receiver event protocol.", "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "inbox", @@ -29,7 +29,7 @@ "views": [ { "id": "verstak.browser-inbox.view", - "title": "Browser Inbox", + "title": "Browser", "icon": "inbox", "component": "BrowserInboxView" } @@ -37,7 +37,7 @@ "sidebarItems": [ { "id": "verstak.browser-inbox.sidebar", - "title": "Browser Inbox", + "title": "Browser", "icon": "inbox", "view": "verstak.browser-inbox.view", "position": 30 @@ -46,7 +46,7 @@ "workspaceItems": [ { "id": "verstak.browser-inbox.workspace", - "title": "Browser Inbox", + "title": "Browser", "icon": "inbox", "component": "BrowserInboxView" } @@ -54,7 +54,7 @@ "settingsPanels": [ { "id": "verstak.browser-inbox.settings", - "title": "Browser Inbox", + "title": "Browser", "component": "BrowserInboxSettings" } ] diff --git a/scripts/smoke-browser-inbox-plugin.js b/scripts/smoke-browser-inbox-plugin.js index a0bfb01..ffca561 100644 --- a/scripts/smoke-browser-inbox-plugin.js +++ b/scripts/smoke-browser-inbox-plugin.js @@ -6,6 +6,10 @@ const vm = require('vm'); const root = path.resolve(__dirname, '..'); const sourcePath = path.join(root, 'plugins', 'browser-inbox', 'frontend', 'src', 'index.js'); const source = fs.readFileSync(sourcePath, 'utf8'); +const catalogs = { + en: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'browser-inbox', 'locales', 'en.json'), 'utf8')), + ru: JSON.parse(fs.readFileSync(path.join(root, 'plugins', 'browser-inbox', 'locales', 'ru.json'), 'utf8')), +}; const technicalErrors = []; class FakeNode { @@ -145,7 +149,7 @@ function loadComponent(document) { return component; } -function makeApi(initialSettings = {}) { +function makeApi(initialSettings = {}, locale = 'en') { const settings = { ...initialSettings }; const handlers = {}; const unsubscribed = []; @@ -162,6 +166,13 @@ function makeApi(initialSettings = {}) { receiverToken: 'initial-browser-token', }; let nextWriteError = null; + function translate(key, params, fallback) { + let value = catalogs[locale]?.[key] || catalogs.en[key] || fallback || key; + Object.entries(params || {}).forEach(([name, replacement]) => { + value = value.replace(new RegExp(`\\{${name}\\}`, 'g'), String(replacement)); + }); + return value; + } function backendCaptures() { const keys = ['captures:global', 'captures', ...Object.keys(settings).filter((key) => key.startsWith('captures:workspace:'))]; const seen = new Set(); @@ -214,6 +225,11 @@ function makeApi(initialSettings = {}) { fileByteWrites, openedURLs, publishedEvents, + i18n: { + getLocale: () => locale, + t: translate, + onDidChangeLocale: () => () => {}, + }, failNextWrite(message) { nextWriteError = new Error(message || 'write failed'); }, @@ -307,6 +323,18 @@ async function mountSettingsWithApi(api, document = makeDocument()) { } styledView.component.unmount && styledView.component.unmount(styledView.container); + const russianView = await mountWithApi(makeApi({}, 'ru'), {}); + if (!russianView.container.textContent.includes('Браузер')) { + throw new Error('Browser Inbox does not use the localized Browser title'); + } + if (!russianView.container.textContent.includes('Все Дела')) { + throw new Error('Browser Inbox does not use the localized Deal filter'); + } + if (!russianView.container.textContent.includes('Пока нет материалов из браузера')) { + throw new Error('Browser Inbox empty state is not localized'); + } + russianView.component.unmount && russianView.component.unmount(russianView.container); + const api = makeApi(); const settingsView = await mountSettingsWithApi(makeApi()); const receiverURLInput = walk(settingsView.container, (node) => node.getAttribute && node.getAttribute('data-browser-inbox-pairing-url') === '');