From 713b95b5a8dc56a6510daa13f869f109d89e5734 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 14 Jul 2026 23:08:00 +0800 Subject: [PATCH] fix: localize activity event labels --- plugins/activity/frontend/src/index.js | 82 +++++++++++++------------- plugins/activity/locales/en.json | 37 ++++++++++-- plugins/activity/locales/ru.json | 33 ++++++++++- plugins/activity/plugin.json | 2 +- scripts/smoke-activity-plugin.js | 9 ++- 5 files changed, 112 insertions(+), 51 deletions(-) diff --git a/plugins/activity/frontend/src/index.js b/plugins/activity/frontend/src/index.js index 27b1e3f..5030482 100644 --- a/plugins/activity/frontend/src/index.js +++ b/plugins/activity/frontend/src/index.js @@ -37,19 +37,19 @@ 'activity.session.handled' ]; var EVENT_LABELS = { - 'workspace.selected': 'Workspace selected', - 'case.selected': 'Workspace selected', - 'file.opened': 'File opened', - 'file.changed': 'File changed', - 'note.saved': 'Note edited', - 'action.started': 'Work session detected', - 'browser.capture.received': 'Browser capture received', - 'browser.capture.page': 'Page captured', - 'browser.capture.selection': 'Selection captured', - 'browser.capture.link': 'Link captured', - 'browser.capture.file': 'File captured', - 'browser.capture.converted': 'Capture converted', - 'browser.activity.domain': 'Browser domain activity' + 'workspace.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' }, + 'case.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' }, + 'file.opened': { key: 'ui.event.fileOpened', fallback: 'File opened' }, + 'file.changed': { key: 'ui.event.fileChanged', fallback: 'File changed' }, + 'note.saved': { key: 'ui.event.noteEdited', fallback: 'Note edited' }, + 'action.started': { key: 'ui.event.workSessionDetected', fallback: 'Work session detected' }, + 'browser.capture.received': { key: 'ui.event.browserCaptureReceived', fallback: 'Browser capture received' }, + 'browser.capture.page': { key: 'ui.event.pageCaptured', fallback: 'Page captured' }, + 'browser.capture.selection': { key: 'ui.event.selectionCaptured', fallback: 'Selection captured' }, + 'browser.capture.link': { key: 'ui.event.linkCaptured', fallback: 'Link captured' }, + 'browser.capture.file': { key: 'ui.event.fileCaptured', fallback: 'File captured' }, + 'browser.capture.converted': { key: 'ui.event.captureConverted', fallback: 'Capture converted' }, + 'browser.activity.domain': { key: 'ui.event.browserDomainActivity', fallback: 'Browser domain activity' } }; var LOW_VALUE_EVENT_TYPES = { 'workspace.selected': true, @@ -412,25 +412,27 @@ }).slice(0, MAX_CANDIDATES); } - function humanEventType(type) { - return EVENT_LABELS[text(type).toLowerCase()] || ''; + function humanEventType(type, translate) { + var label = EVENT_LABELS[text(type).toLowerCase()]; + return label ? translate(label.key, null, label.fallback) : ''; } - function humanEventTitle(activity) { + function humanEventTitle(activity, translate) { var explicit = text(activity && activity.title).trim(); var type = text(activity && activity.type).trim(); - if (explicit && explicit.toLowerCase() !== type.toLowerCase()) return explicit; - return humanEventType(type) || text(activity && (activity.summary || activity.activityId)).trim() || 'Activity event'; + var label = humanEventType(type, translate); + if (explicit && explicit.toLowerCase() !== type.toLowerCase()) return label ? label + ' — ' + explicit : explicit; + return label || text(activity && (activity.summary || activity.activityId)).trim() || translate('ui.event.activity', null, 'Activity event'); } - function eventKind(activity) { + function eventKind(activity, translate) { var type = text(activity && activity.type).toLowerCase(); - if (type.indexOf('browser.capture') === 0) return 'Capture'; - if (type.indexOf('file.') === 0) return 'File'; - if (type.indexOf('note.') === 0) return 'Note'; - if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return 'Workspace'; - if (type.indexOf('action.') === 0) return 'Work'; - return 'Activity'; + if (type.indexOf('browser.capture') === 0) return translate('ui.kind.capture', null, 'Capture'); + if (type.indexOf('file.') === 0) return translate('ui.kind.file', null, 'File'); + if (type.indexOf('note.') === 0) return translate('ui.kind.note', null, 'Note'); + if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return translate('ui.kind.deal', null, 'Deal'); + if (type.indexOf('action.') === 0) return translate('ui.kind.work', null, 'Work'); + return translate('ui.kind.activity', null, 'Activity'); } function globalEventKeys(settings) { @@ -760,14 +762,10 @@ el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }), el('div', { className: 'activity-main' }, [ el('div', { className: 'activity-row-head' }, [ - el('span', { className: 'activity-type', textContent: eventKind(activity) }), - el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity) }) + el('span', { className: 'activity-type', textContent: eventKind(activity, tr) }), + el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity, tr) }) ]), - activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null, - el('details', { className: 'activity-details' }, [ - 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 : '')) }) - ]) + activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null ]) ])); }); @@ -824,19 +822,19 @@ el('div', {}, [ 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) }), - el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }), - el('div', { textContent: 'Activities: ' + candidate.activityCount }) + el('div', { textContent: tr('ui.candidateDeal', { deal: candidate.workspaceRootPath }, 'Deal: ' + candidate.workspaceRootPath) }), + el('div', { textContent: tr('ui.candidateTime', { time: candidateTimeRange(candidate) }, 'Time: ' + candidateTimeRange(candidate)) }), + el('div', { textContent: tr('ui.candidateDuration', { minutes: candidate.estimatedMinutes }, 'Estimated duration: ' + candidate.estimatedMinutes + ' min') }), + el('div', { textContent: tr('ui.candidateActivities', { count: candidate.activityCount }, 'Activities: ' + candidate.activityCount) }) ]), el('details', { className: 'activity-candidate-activities' }, [ - el('summary', { textContent: 'Included activities (' + candidate.activityCount + ')' }) + el('summary', { textContent: tr('ui.includedActivities', { count: candidate.activityCount }, 'Included activities (' + candidate.activityCount + ')') }) ].concat(candidate.activities.map(function (activity) { - return el('div', { className: 'activity-candidate-activity', textContent: activity.occurredAt + ' · ' + activity.type + ' · ' + activity.activityId }); + return el('div', { className: 'activity-candidate-activity', textContent: activity.occurredAt + ' · ' + humanEventTitle(activity, tr) }); }))) ]), el('div', { className: 'activity-candidate-actions' }, [ - el('div', { className: 'activity-candidate-duration', textContent: candidate.estimatedMinutes + ' min' }), + el('div', { className: 'activity-candidate-duration', textContent: tr('ui.minutes', { count: candidate.estimatedMinutes }, candidate.estimatedMinutes + ' min') }), 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); } }) ]) @@ -845,7 +843,7 @@ } function render() { - countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's'); + countEl.textContent = tr(events.length === 1 ? 'ui.eventCount.one' : 'ui.eventCount.many', { count: events.length }, events.length + ' event' + (events.length === 1 ? '' : 's')); clearBtn.disabled = events.length === 0; statusEl.textContent = statusText; statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : ''); @@ -917,7 +915,9 @@ if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe); }); })).then(function () { - statusText = scope.mode === 'global' ? 'Listening for all activity' : 'Listening for workspace activity'; + statusText = scope.mode === 'global' + ? tr('ui.listeningGlobal', null, 'Listening for all activity') + : tr('ui.listeningDeal', null, 'Listening for Deal activity'); statusClass = ''; }).catch(function (err) { reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err); diff --git a/plugins/activity/locales/en.json b/plugins/activity/locales/en.json index 05f4baa..1c1575b 100644 --- a/plugins/activity/locales/en.json +++ b/plugins/activity/locales/en.json @@ -1,6 +1,6 @@ { "manifest.name": "Activity", - "manifest.description": "Workspace-scoped activity log for public plugin events.", + "manifest.description": "Deal-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", @@ -10,8 +10,8 @@ "ui.workspaceTitle": "Activity · {workspace}", "ui.clear": "Clear", "ui.clearConfirmTitle": "Clear activity?", - "ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every case.", - "ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other cases remains.", + "ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every Deal.", + "ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other Deals remains.", "ui.confirmClear": "Clear activity", "ui.cancel": "Cancel", "ui.saveError": "Could not save activity. Please try again.", @@ -29,5 +29,34 @@ "ui.candidates": "Possible journal entries", "ui.candidate": "Possible journal entry", "ui.review": "Review", - "ui.dismiss": "Dismiss" + "ui.dismiss": "Dismiss", + "ui.event.workspaceSelected": "Deal selected", + "ui.event.fileOpened": "File opened", + "ui.event.fileChanged": "File changed", + "ui.event.noteEdited": "Note edited", + "ui.event.workSessionDetected": "Work session detected", + "ui.event.browserCaptureReceived": "Browser capture received", + "ui.event.pageCaptured": "Page captured", + "ui.event.selectionCaptured": "Selection captured", + "ui.event.linkCaptured": "Link captured", + "ui.event.fileCaptured": "File captured", + "ui.event.captureConverted": "Capture converted", + "ui.event.browserDomainActivity": "Browser domain activity", + "ui.event.activity": "Activity event", + "ui.kind.capture": "Capture", + "ui.kind.file": "File", + "ui.kind.note": "Note", + "ui.kind.deal": "Deal", + "ui.kind.work": "Work", + "ui.kind.activity": "Activity", + "ui.candidateDeal": "Deal: {deal}", + "ui.candidateTime": "Time: {time}", + "ui.candidateDuration": "Estimated duration: {minutes} min", + "ui.candidateActivities": "Activities: {count}", + "ui.includedActivities": "Included activities ({count})", + "ui.minutes": "{count} min", + "ui.eventCount.one": "{count} event", + "ui.eventCount.many": "{count} events", + "ui.listeningGlobal": "Listening for all activity", + "ui.listeningDeal": "Listening for Deal activity" } diff --git a/plugins/activity/locales/ru.json b/plugins/activity/locales/ru.json index e1d4f7d..d2b14e7 100644 --- a/plugins/activity/locales/ru.json +++ b/plugins/activity/locales/ru.json @@ -1,6 +1,6 @@ { "manifest.name": "Активность", - "manifest.description": "Журнал публичных событий плагинов в рабочем пространстве.", + "manifest.description": "Журнал публичных событий плагинов в Деле.", "contributions.views.verstak.activity.view.title": "Активность", "contributions.sidebarItems.verstak.activity.sidebar.title": "Активность", "contributions.workspaceItems.verstak.activity.workspace.title": "Активность", @@ -29,5 +29,34 @@ "ui.candidates": "Возможные записи журнала", "ui.candidate": "Возможная запись журнала", "ui.review": "Проверить", - "ui.dismiss": "Скрыть" + "ui.dismiss": "Скрыть", + "ui.event.workspaceSelected": "Выбрано Дело", + "ui.event.fileOpened": "Открыт файл", + "ui.event.fileChanged": "Изменён файл", + "ui.event.noteEdited": "Изменена заметка", + "ui.event.workSessionDetected": "Обнаружена рабочая сессия", + "ui.event.browserCaptureReceived": "Получено сохранённое из браузера", + "ui.event.pageCaptured": "Сохранена страница", + "ui.event.selectionCaptured": "Сохранено выделение", + "ui.event.linkCaptured": "Сохранена ссылка", + "ui.event.fileCaptured": "Сохранён файл", + "ui.event.captureConverted": "Сохранённое преобразовано", + "ui.event.browserDomainActivity": "Активность на домене в браузере", + "ui.event.activity": "Событие активности", + "ui.kind.capture": "Сохранённое", + "ui.kind.file": "Файл", + "ui.kind.note": "Заметка", + "ui.kind.deal": "Дело", + "ui.kind.work": "Работа", + "ui.kind.activity": "Активность", + "ui.candidateDeal": "Дело: {deal}", + "ui.candidateTime": "Время: {time}", + "ui.candidateDuration": "Оценка длительности: {minutes} мин.", + "ui.candidateActivities": "Активностей: {count}", + "ui.includedActivities": "Учтённые активности ({count})", + "ui.minutes": "{count} мин.", + "ui.eventCount.one": "Событий: {count}", + "ui.eventCount.many": "Событий: {count}", + "ui.listeningGlobal": "Учёт всей активности включён", + "ui.listeningDeal": "Учёт активности Дела включён" } diff --git a/plugins/activity/plugin.json b/plugins/activity/plugin.json index 917efd5..56443c0 100644 --- a/plugins/activity/plugin.json +++ b/plugins/activity/plugin.json @@ -4,7 +4,7 @@ "name": "Activity", "version": "0.1.0", "apiVersion": "0.1.0", - "description": "Workspace-scoped activity log for public plugin events.", + "description": "Deal-scoped activity log for public plugin events.", "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } }, "source": "official", "icon": "activity", diff --git a/scripts/smoke-activity-plugin.js b/scripts/smoke-activity-plugin.js index ff859b4..7e561dd 100644 --- a/scripts/smoke-activity-plugin.js +++ b/scripts/smoke-activity-plugin.js @@ -280,13 +280,16 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w } if (api.storedEvents(globalKey).length !== 0) throw new Error('workspace activity leaked into global storage'); if (!container.textContent.includes('Example Article')) throw new Error('browser capture title was not rendered'); - if (!container.textContent.includes('browser.capture.selection')) throw new Error('event type was not rendered'); - if (!container.textContent.includes('browser.capture.converted')) throw new Error('conversion event type was not rendered'); + if (!container.textContent.includes('Selection captured')) throw new Error('browser capture label was not rendered'); + if (!container.textContent.includes('Capture converted')) throw new Error('conversion label was not rendered'); + if (container.textContent.includes('browser.capture.selection') || container.textContent.includes('browser.capture.converted') || container.textContent.includes('verstak.browser-inbox')) { + throw new Error('Activity must not expose technical event or plugin identifiers in its UI'); + } if (!container.textContent.includes('Possible journal entries')) throw new Error('work session candidate section was not rendered'); if (container.textContent.includes('Project work on 2026-06-27')) throw new Error('candidate must not invent a worklog title'); const candidateNode = walk(container, (node) => node.getAttribute && node.getAttribute('data-work-session-candidate')); if (!candidateNode) throw new Error('work session candidate data attribute was not rendered'); - if (!candidateNode.textContent.includes('Workspace: Project')) throw new Error('candidate workspace was not rendered'); + if (!candidateNode.textContent.includes('Deal: Project')) throw new Error('candidate Deal was not rendered'); if (!candidateNode.textContent.includes('Estimated duration: 20 min')) throw new Error('candidate duration was not rendered'); if (!candidateNode.textContent.includes('Activities: 3')) throw new Error('candidate activity count was not rendered'); if (!walk(candidateNode, (node) => node.getAttribute && node.getAttribute('data-work-session-action') === 'review')) throw new Error('candidate review action was not rendered');