fix: localize activity event labels

This commit is contained in:
mirivlad 2026-07-14 23:08:00 +08:00
parent b449e9c616
commit 713b95b5a8
5 changed files with 112 additions and 51 deletions

View File

@ -37,19 +37,19 @@
'activity.session.handled' 'activity.session.handled'
]; ];
var EVENT_LABELS = { var EVENT_LABELS = {
'workspace.selected': 'Workspace selected', 'workspace.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' },
'case.selected': 'Workspace selected', 'case.selected': { key: 'ui.event.workspaceSelected', fallback: 'Deal selected' },
'file.opened': 'File opened', 'file.opened': { key: 'ui.event.fileOpened', fallback: 'File opened' },
'file.changed': 'File changed', 'file.changed': { key: 'ui.event.fileChanged', fallback: 'File changed' },
'note.saved': 'Note edited', 'note.saved': { key: 'ui.event.noteEdited', fallback: 'Note edited' },
'action.started': 'Work session detected', 'action.started': { key: 'ui.event.workSessionDetected', fallback: 'Work session detected' },
'browser.capture.received': 'Browser capture received', 'browser.capture.received': { key: 'ui.event.browserCaptureReceived', fallback: 'Browser capture received' },
'browser.capture.page': 'Page captured', 'browser.capture.page': { key: 'ui.event.pageCaptured', fallback: 'Page captured' },
'browser.capture.selection': 'Selection captured', 'browser.capture.selection': { key: 'ui.event.selectionCaptured', fallback: 'Selection captured' },
'browser.capture.link': 'Link captured', 'browser.capture.link': { key: 'ui.event.linkCaptured', fallback: 'Link captured' },
'browser.capture.file': 'File captured', 'browser.capture.file': { key: 'ui.event.fileCaptured', fallback: 'File captured' },
'browser.capture.converted': 'Capture converted', 'browser.capture.converted': { key: 'ui.event.captureConverted', fallback: 'Capture converted' },
'browser.activity.domain': 'Browser domain activity' 'browser.activity.domain': { key: 'ui.event.browserDomainActivity', fallback: 'Browser domain activity' }
}; };
var LOW_VALUE_EVENT_TYPES = { var LOW_VALUE_EVENT_TYPES = {
'workspace.selected': true, 'workspace.selected': true,
@ -412,25 +412,27 @@
}).slice(0, MAX_CANDIDATES); }).slice(0, MAX_CANDIDATES);
} }
function humanEventType(type) { function humanEventType(type, translate) {
return EVENT_LABELS[text(type).toLowerCase()] || ''; 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 explicit = text(activity && activity.title).trim();
var type = text(activity && activity.type).trim(); var type = text(activity && activity.type).trim();
if (explicit && explicit.toLowerCase() !== type.toLowerCase()) return explicit; var label = humanEventType(type, translate);
return humanEventType(type) || text(activity && (activity.summary || activity.activityId)).trim() || 'Activity event'; 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(); var type = text(activity && activity.type).toLowerCase();
if (type.indexOf('browser.capture') === 0) return 'Capture'; if (type.indexOf('browser.capture') === 0) return translate('ui.kind.capture', null, 'Capture');
if (type.indexOf('file.') === 0) return 'File'; if (type.indexOf('file.') === 0) return translate('ui.kind.file', null, 'File');
if (type.indexOf('note.') === 0) return 'Note'; if (type.indexOf('note.') === 0) return translate('ui.kind.note', null, 'Note');
if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return 'Workspace'; if (type.indexOf('workspace') !== -1 || type.indexOf('case.') === 0) return translate('ui.kind.deal', null, 'Deal');
if (type.indexOf('action.') === 0) return 'Work'; if (type.indexOf('action.') === 0) return translate('ui.kind.work', null, 'Work');
return 'Activity'; return translate('ui.kind.activity', null, 'Activity');
} }
function globalEventKeys(settings) { function globalEventKeys(settings) {
@ -760,14 +762,10 @@
el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }), el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt) || '-' }),
el('div', { className: 'activity-main' }, [ el('div', { className: 'activity-main' }, [
el('div', { className: 'activity-row-head' }, [ el('div', { className: 'activity-row-head' }, [
el('span', { className: 'activity-type', textContent: eventKind(activity) }), el('span', { className: 'activity-type', textContent: eventKind(activity, tr) }),
el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity) }) el('span', { className: 'activity-title-text', textContent: humanEventTitle(activity, tr) })
]), ]),
activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null, 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 : '')) })
])
]) ])
])); ]));
}); });
@ -824,19 +822,19 @@
el('div', {}, [ el('div', {}, [
el('div', { className: 'activity-candidate-title', textContent: tr('ui.candidate', null, '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', { className: 'activity-candidate-facts' }, [
el('div', { textContent: 'Workspace: ' + candidate.workspaceRootPath }), el('div', { textContent: tr('ui.candidateDeal', { deal: candidate.workspaceRootPath }, 'Deal: ' + candidate.workspaceRootPath) }),
el('div', { textContent: 'Time: ' + candidateTimeRange(candidate) }), el('div', { textContent: tr('ui.candidateTime', { time: candidateTimeRange(candidate) }, 'Time: ' + candidateTimeRange(candidate)) }),
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }), el('div', { textContent: tr('ui.candidateDuration', { minutes: candidate.estimatedMinutes }, 'Estimated duration: ' + candidate.estimatedMinutes + ' min') }),
el('div', { textContent: 'Activities: ' + candidate.activityCount }) el('div', { textContent: tr('ui.candidateActivities', { count: candidate.activityCount }, 'Activities: ' + candidate.activityCount) })
]), ]),
el('details', { className: 'activity-candidate-activities' }, [ 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) { ].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-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': '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); } }) 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() { 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; clearBtn.disabled = events.length === 0;
statusEl.textContent = statusText; statusEl.textContent = statusText;
statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : ''); statusEl.className = 'activity-status' + (statusClass ? ' ' + statusClass : '');
@ -917,7 +915,9 @@
if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe); if (typeof unsubscribe === 'function') unsubscribers.push(unsubscribe);
}); });
})).then(function () { })).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 = ''; statusClass = '';
}).catch(function (err) { }).catch(function (err) {
reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err); reportError('ui.subscriptionsUnavailable', 'Activity updates are unavailable. Please try again.', err);

View File

@ -1,6 +1,6 @@
{ {
"manifest.name": "Activity", "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.views.verstak.activity.view.title": "Activity",
"contributions.sidebarItems.verstak.activity.sidebar.title": "Activity", "contributions.sidebarItems.verstak.activity.sidebar.title": "Activity",
"contributions.workspaceItems.verstak.activity.workspace.title": "Activity", "contributions.workspaceItems.verstak.activity.workspace.title": "Activity",
@ -10,8 +10,8 @@
"ui.workspaceTitle": "Activity · {workspace}", "ui.workspaceTitle": "Activity · {workspace}",
"ui.clear": "Clear", "ui.clear": "Clear",
"ui.clearConfirmTitle": "Clear activity?", "ui.clearConfirmTitle": "Clear activity?",
"ui.clearGlobalWarning": "This permanently deletes all recorded activity and journal suggestions in every case.", "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 cases remains.", "ui.clearWorkspaceWarning": "This permanently deletes recorded activity for {workspace}. Activity in other Deals remains.",
"ui.confirmClear": "Clear activity", "ui.confirmClear": "Clear activity",
"ui.cancel": "Cancel", "ui.cancel": "Cancel",
"ui.saveError": "Could not save activity. Please try again.", "ui.saveError": "Could not save activity. Please try again.",
@ -29,5 +29,34 @@
"ui.candidates": "Possible journal entries", "ui.candidates": "Possible journal entries",
"ui.candidate": "Possible journal entry", "ui.candidate": "Possible journal entry",
"ui.review": "Review", "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"
} }

View File

@ -1,6 +1,6 @@
{ {
"manifest.name": "Активность", "manifest.name": "Активность",
"manifest.description": "Журнал публичных событий плагинов в рабочем пространстве.", "manifest.description": "Журнал публичных событий плагинов в Деле.",
"contributions.views.verstak.activity.view.title": "Активность", "contributions.views.verstak.activity.view.title": "Активность",
"contributions.sidebarItems.verstak.activity.sidebar.title": "Активность", "contributions.sidebarItems.verstak.activity.sidebar.title": "Активность",
"contributions.workspaceItems.verstak.activity.workspace.title": "Активность", "contributions.workspaceItems.verstak.activity.workspace.title": "Активность",
@ -29,5 +29,34 @@
"ui.candidates": "Возможные записи журнала", "ui.candidates": "Возможные записи журнала",
"ui.candidate": "Возможная запись журнала", "ui.candidate": "Возможная запись журнала",
"ui.review": "Проверить", "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": "Учёт активности Дела включён"
} }

View File

@ -4,7 +4,7 @@
"name": "Activity", "name": "Activity",
"version": "0.1.0", "version": "0.1.0",
"apiVersion": "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" } }, "localization": { "defaultLocale": "en", "locales": { "en": "locales/en.json", "ru": "locales/ru.json" } },
"source": "official", "source": "official",
"icon": "activity", "icon": "activity",

View File

@ -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 (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('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('Selection captured')) throw new Error('browser capture label was not rendered');
if (!container.textContent.includes('browser.capture.converted')) throw new Error('conversion event type 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('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'); 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')); 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) 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('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 (!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'); if (!walk(candidateNode, (node) => node.getAttribute && node.getAttribute('data-work-session-action') === 'review')) throw new Error('candidate review action was not rendered');