fix: localize overview activity labels
This commit is contained in:
parent
2da414fb3b
commit
a0262b97b3
|
|
@ -248,7 +248,7 @@ test.describe('UX Overview workspace flow', () => {
|
|||
await expect(overview.locator('[data-overview-summary="attention"]')).toContainText('3');
|
||||
const attention = overview.locator('[data-overview-section="attention"]');
|
||||
await expect(attention).toContainText('Possible journal entry');
|
||||
await expect(attention).toContainText('Workspace: Project · 10 min · 2 activities');
|
||||
await expect(attention).toContainText('Deal: Project · 10 min · 2 activities');
|
||||
await attention.locator('.overview-attention-row', { hasText: 'Possible journal entry' }).getByRole('button', { name: 'Review candidate' }).click();
|
||||
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('.journal-root [data-journal-candidate]')).toContainText('Workspace: Project');
|
||||
|
|
@ -295,4 +295,28 @@ test.describe('UX Overview workspace flow', () => {
|
|||
await expect(recent).toContainText('Added journal entry "Write project summary"');
|
||||
await expect(recent).not.toContainText('Changed file "draft.md"');
|
||||
});
|
||||
|
||||
test('Overview localizes activity labels without exposing internal event names', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
await window.go.api.App.WritePluginSettings('verstak.activity', {
|
||||
'events:workspace:Project': [{
|
||||
activityId: 'overview-russian-note',
|
||||
occurredAt: '2026-06-30T08:25:00.000Z',
|
||||
type: 'note.saved',
|
||||
title: 'Локализация',
|
||||
summary: 'Project/Notes/Localization.md',
|
||||
workspaceRootPath: 'Project',
|
||||
}],
|
||||
});
|
||||
});
|
||||
await page.locator('[data-settings-menu-button]').click();
|
||||
await page.locator('[data-settings-language="ru"]').click();
|
||||
|
||||
const overview = page.locator('[data-overview-root]');
|
||||
await overview.locator('[data-overview-action="refresh"]').click();
|
||||
const recent = overview.locator('[data-overview-section="recent"]');
|
||||
await expect(recent).toContainText('Изменена заметка «Локализация»');
|
||||
await expect(recent).not.toContainText('note.saved');
|
||||
await expect(recent).not.toContainText('Edited note');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -236,6 +236,42 @@ export default {
|
|||
'overview.recentChangesHint': 'Latest meaningful activity in this Deal.',
|
||||
'overview.event.workspaceOpened': 'Opened Deal',
|
||||
'overview.event.activity': 'Deal activity',
|
||||
'overview.event.noteEdited': 'Edited note "{name}"',
|
||||
'overview.event.noteOpened': 'Opened note "{name}"',
|
||||
'overview.event.noteCreated': 'Created note "{name}"',
|
||||
'overview.event.fileOpened': 'Opened file "{name}"',
|
||||
'overview.event.fileChanged': 'Changed file "{name}"',
|
||||
'overview.event.fileCreated': 'Created file "{name}"',
|
||||
'overview.event.fileRemoved': 'Removed file "{name}"',
|
||||
'overview.event.capturePage': 'Captured page "{name}"',
|
||||
'overview.event.captureSelection': 'Captured selection "{name}"',
|
||||
'overview.event.captureLink': 'Captured link "{name}"',
|
||||
'overview.event.captureFile': 'Captured file "{name}"',
|
||||
'overview.event.captureConverted': 'Converted capture "{name}"',
|
||||
'overview.event.capture': 'Captured {kind} "{title}"',
|
||||
'overview.event.journalAdded': 'Added journal entry "{name}"',
|
||||
'overview.event.workSessionDetected': 'Work session detected',
|
||||
'overview.event.todo': 'Todo "{title}"',
|
||||
'overview.event.reviewCapture': 'Review capture "{title}"',
|
||||
'overview.event.continueJournal': 'Continue journal entry "{title}"',
|
||||
'overview.event.possibleJournalEntry': 'Possible journal entry',
|
||||
'overview.captureKind.page': 'page',
|
||||
'overview.captureKind.selection': 'selection',
|
||||
'overview.captureKind.link': 'link',
|
||||
'overview.captureKind.file': 'file',
|
||||
'overview.captureKind.item': 'item',
|
||||
'overview.todo.reminderDue': 'Reminder due',
|
||||
'overview.todo.overdue': 'Overdue',
|
||||
'overview.todo.dueSoon': 'Due soon',
|
||||
'overview.todo.due': 'Due {date}',
|
||||
'overview.candidateMeta': 'Deal: {deal} · {minutes} min · {activities} activities',
|
||||
'overview.minutes': '{count} min',
|
||||
'overview.browserCapture': 'Browser capture',
|
||||
'overview.untitledTodo': 'Untitled todo',
|
||||
'overview.untitledCapture': 'Untitled capture',
|
||||
'overview.untitledJournal': 'Journal entry',
|
||||
'overview.unknownDeal': 'Unknown Deal',
|
||||
'overview.item': 'item',
|
||||
'overview.overviewNote': 'Deal overview note',
|
||||
'overview.recentFilter': 'Recent changes filter',
|
||||
'overview.loadingRecent': 'Loading recent changes...',
|
||||
|
|
|
|||
|
|
@ -236,6 +236,42 @@ export default {
|
|||
'overview.recentChangesHint': 'Последняя важная активность в этом Деле.',
|
||||
'overview.event.workspaceOpened': 'Открыто Дело',
|
||||
'overview.event.activity': 'Активность Дела',
|
||||
'overview.event.noteEdited': 'Изменена заметка «{name}»',
|
||||
'overview.event.noteOpened': 'Открыта заметка «{name}»',
|
||||
'overview.event.noteCreated': 'Создана заметка «{name}»',
|
||||
'overview.event.fileOpened': 'Открыт файл «{name}»',
|
||||
'overview.event.fileChanged': 'Изменён файл «{name}»',
|
||||
'overview.event.fileCreated': 'Создан файл «{name}»',
|
||||
'overview.event.fileRemoved': 'Удалён файл «{name}»',
|
||||
'overview.event.capturePage': 'Сохранена страница «{name}»',
|
||||
'overview.event.captureSelection': 'Сохранено выделение «{name}»',
|
||||
'overview.event.captureLink': 'Сохранена ссылка «{name}»',
|
||||
'overview.event.captureFile': 'Сохранён файл «{name}»',
|
||||
'overview.event.captureConverted': 'Сохранённое преобразовано: «{name}»',
|
||||
'overview.event.capture': 'Сохранено: {kind} «{title}»',
|
||||
'overview.event.journalAdded': 'Добавлена запись в журнал «{name}»',
|
||||
'overview.event.workSessionDetected': 'Обнаружена рабочая сессия',
|
||||
'overview.event.todo': 'Задача «{title}»',
|
||||
'overview.event.reviewCapture': 'Разобрать сохранённое «{title}»',
|
||||
'overview.event.continueJournal': 'Продолжить запись журнала «{title}»',
|
||||
'overview.event.possibleJournalEntry': 'Возможная запись в журнал',
|
||||
'overview.captureKind.page': 'страница',
|
||||
'overview.captureKind.selection': 'выделение',
|
||||
'overview.captureKind.link': 'ссылка',
|
||||
'overview.captureKind.file': 'файл',
|
||||
'overview.captureKind.item': 'элемент',
|
||||
'overview.todo.reminderDue': 'Пора напомнить',
|
||||
'overview.todo.overdue': 'Просрочено',
|
||||
'overview.todo.dueSoon': 'Скоро срок',
|
||||
'overview.todo.due': 'Срок: {date}',
|
||||
'overview.candidateMeta': 'Дело: {deal} · {minutes} мин. · активностей: {activities}',
|
||||
'overview.minutes': '{count} мин.',
|
||||
'overview.browserCapture': 'Сохранённое из браузера',
|
||||
'overview.untitledTodo': 'Задача без названия',
|
||||
'overview.untitledCapture': 'Сохранённое без названия',
|
||||
'overview.untitledJournal': 'Запись журнала',
|
||||
'overview.unknownDeal': 'Неизвестное Дело',
|
||||
'overview.item': 'элемент',
|
||||
'overview.overviewNote': 'Обзорная заметка Дела',
|
||||
'overview.recentFilter': 'Фильтр недавних изменений',
|
||||
'overview.loadingRecent': 'Загрузка недавних изменений...',
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@
|
|||
}
|
||||
|
||||
function todoTitle(item) {
|
||||
return String(item?.title || item?.name || 'Untitled todo').trim() || 'Untitled todo';
|
||||
return String(item?.title || item?.name || tr('overview.untitledTodo')).trim() || tr('overview.untitledTodo');
|
||||
}
|
||||
|
||||
function todoAttentionState(item) {
|
||||
|
|
@ -203,9 +203,9 @@
|
|||
const now = Date.now();
|
||||
const dueAt = todoDateMs(item?.dueAt);
|
||||
const reminderAt = todoDateMs(item?.reminderAt);
|
||||
if (reminderAt && reminderAt <= now) return 'Reminder due';
|
||||
if (dueAt && dueAt < now) return 'Overdue';
|
||||
if (dueAt && dueAt <= now + 3 * 24 * 60 * 60 * 1000) return 'Due soon';
|
||||
if (reminderAt && reminderAt <= now) return tr('overview.todo.reminderDue');
|
||||
if (dueAt && dueAt < now) return tr('overview.todo.overdue');
|
||||
if (dueAt && dueAt <= now + 3 * 24 * 60 * 60 * 1000) return tr('overview.todo.dueSoon');
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -238,17 +238,12 @@
|
|||
return fileName(path).replace(/\.(md|markdown|txt)$/i, '').replace(/_/g, ' ') || 'Untitled';
|
||||
}
|
||||
|
||||
function quoted(value) {
|
||||
const clean = String(value || '').trim();
|
||||
return clean ? `"${clean}"` : '';
|
||||
}
|
||||
|
||||
function entityName(item) {
|
||||
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {};
|
||||
const path = payload.path || payload.notePath || item?.path || item?.relativePath || looksLikePath(item?.summary);
|
||||
const title = String(item?.title || payload.title || '').trim();
|
||||
if (path && (!title || /^(saved note|file opened|file changed|activity event)$/i.test(title))) return titleFromPath(path);
|
||||
return title || titleFromPath(path) || String(item?.summary || item?.activityId || 'item');
|
||||
return title || titleFromPath(path) || String(item?.summary || item?.activityId || tr('overview.item'));
|
||||
}
|
||||
|
||||
function looksLikePath(value) {
|
||||
|
|
@ -258,11 +253,11 @@
|
|||
}
|
||||
|
||||
function captureTitle(capture) {
|
||||
return capture?.title || capture?.fileName || capture?.url || capture?.captureId || 'Untitled capture';
|
||||
return capture?.title || capture?.fileName || capture?.url || capture?.captureId || tr('overview.untitledCapture');
|
||||
}
|
||||
|
||||
function journalTitle(entry) {
|
||||
return entry?.title || entry?.summary || entry?.date || entry?.entryId || 'Journal entry';
|
||||
return entry?.title || entry?.summary || entry?.date || entry?.entryId || tr('overview.untitledJournal');
|
||||
}
|
||||
|
||||
function itemTimeLabel(item) {
|
||||
|
|
@ -312,24 +307,33 @@
|
|||
function activityTitle(item) {
|
||||
const type = String(item?.type || '').toLowerCase();
|
||||
const name = entityName(item);
|
||||
if (type === 'note.saved' || type === 'note.edited') return `Edited note ${quoted(name)}`;
|
||||
if (type === 'note.opened') return `Opened note ${quoted(name)}`;
|
||||
if (type === 'note.created') return `Created note ${quoted(name)}`;
|
||||
if (type === 'file.opened') return `Opened file ${quoted(name)}`;
|
||||
if (type === 'file.changed') return `Changed file ${quoted(name)}`;
|
||||
if (type === 'file.created') return `Created file ${quoted(name)}`;
|
||||
if (type === 'file.deleted' || type === 'file.trashed') return `Removed file ${quoted(name)}`;
|
||||
if (type === 'browser.capture.page') return `Captured page ${quoted(name)}`;
|
||||
if (type === 'browser.capture.selection') return `Captured selection ${quoted(name)}`;
|
||||
if (type === 'browser.capture.link') return `Captured link ${quoted(name)}`;
|
||||
if (type === 'browser.capture.file') return `Captured file ${quoted(name)}`;
|
||||
if (type === 'browser.capture.converted') return `Converted capture ${quoted(name)}`;
|
||||
if (type === 'journal.entry.added' || type === 'worklog.entry.added') return `Added journal entry ${quoted(name)}`;
|
||||
if (type === 'action.started') return 'Work session detected';
|
||||
if (type === 'note.saved' || type === 'note.edited') return tr('overview.event.noteEdited', { name });
|
||||
if (type === 'note.opened') return tr('overview.event.noteOpened', { name });
|
||||
if (type === 'note.created') return tr('overview.event.noteCreated', { name });
|
||||
if (type === 'file.opened') return tr('overview.event.fileOpened', { name });
|
||||
if (type === 'file.changed') return tr('overview.event.fileChanged', { name });
|
||||
if (type === 'file.created') return tr('overview.event.fileCreated', { name });
|
||||
if (type === 'file.deleted' || type === 'file.trashed') return tr('overview.event.fileRemoved', { name });
|
||||
if (type === 'browser.capture.page') return tr('overview.event.capturePage', { name });
|
||||
if (type === 'browser.capture.selection') return tr('overview.event.captureSelection', { name });
|
||||
if (type === 'browser.capture.link') return tr('overview.event.captureLink', { name });
|
||||
if (type === 'browser.capture.file') return tr('overview.event.captureFile', { name });
|
||||
if (type === 'browser.capture.converted') return tr('overview.event.captureConverted', { name });
|
||||
if (type === 'journal.entry.added' || type === 'worklog.entry.added') return tr('overview.event.journalAdded', { name });
|
||||
if (type === 'action.started') return tr('overview.event.workSessionDetected');
|
||||
if (type === 'workspace.opened') return tr('overview.event.workspaceOpened');
|
||||
return item?.title || item?.summary || tr('overview.event.activity');
|
||||
}
|
||||
|
||||
function captureKindLabel(capture) {
|
||||
const kind = String(capture?.kind || '').toLowerCase();
|
||||
if (kind === 'page') return tr('overview.captureKind.page');
|
||||
if (kind === 'selection') return tr('overview.captureKind.selection');
|
||||
if (kind === 'link') return tr('overview.captureKind.link');
|
||||
if (kind === 'file') return tr('overview.captureKind.file');
|
||||
return tr('overview.captureKind.item');
|
||||
}
|
||||
|
||||
function actionForCategory(category) {
|
||||
if (category === 'notes') return { kind: 'notes', label: tr('overview.openNotes') };
|
||||
if (category === 'files') return { kind: 'files', label: tr('overview.openFiles') };
|
||||
|
|
@ -348,7 +352,7 @@
|
|||
id: item.activityId || `${item.type}:${timeValue(item)}`,
|
||||
category,
|
||||
title: activityTitle(item),
|
||||
meta: `${itemTimeLabel(item)}${item.sourcePluginId ? ' · ' + item.sourcePluginId.replace('verstak.', '') : ''}`,
|
||||
meta: itemTimeLabel(item),
|
||||
time: timeValue(item),
|
||||
absolute: absoluteTime(timeValue(item)),
|
||||
actionKind: action.kind,
|
||||
|
|
@ -358,8 +362,8 @@
|
|||
const captureItems = captureRows.map(item => ({
|
||||
id: item.captureId || `capture:${timeValue(item)}`,
|
||||
category: 'captures',
|
||||
title: `Captured ${item.kind || 'item'} ${quoted(captureTitle(item))}`,
|
||||
meta: `${itemTimeLabel(item)} · ${item.domain || item.url || 'Browser capture'}`,
|
||||
title: tr('overview.event.capture', { kind: captureKindLabel(item), title: captureTitle(item) }),
|
||||
meta: `${itemTimeLabel(item)} · ${item.domain || item.url || tr('overview.browserCapture')}`,
|
||||
time: timeValue(item),
|
||||
absolute: absoluteTime(timeValue(item)),
|
||||
actionKind: 'browser-inbox',
|
||||
|
|
@ -368,8 +372,8 @@
|
|||
const journalItems = journalRows.map(item => ({
|
||||
id: item.entryId || `journal:${timeValue(item)}`,
|
||||
category: 'journal',
|
||||
title: `Added journal entry ${quoted(journalTitle(item))}`,
|
||||
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + item.minutes + ' min' : ''}`,
|
||||
title: tr('overview.event.journalAdded', { name: journalTitle(item) }),
|
||||
meta: `${itemTimeLabel(item)}${item.minutes ? ' · ' + tr('overview.minutes', { count: item.minutes }) : ''}`,
|
||||
time: timeValue(item),
|
||||
absolute: absoluteTime(timeValue(item)),
|
||||
actionKind: 'journal',
|
||||
|
|
@ -409,8 +413,8 @@
|
|||
const todoCandidates = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).map(item => ({
|
||||
id: item.id || `todo:${todoTitle(item)}`,
|
||||
category: 'todo',
|
||||
title: `Todo ${quoted(todoTitle(item))}`,
|
||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||
title: tr('overview.event.todo', { title: todoTitle(item) }),
|
||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · ${tr('overview.todo.due', { date: item.dueAt })}` : ''}`,
|
||||
time: todoTimeValue(item),
|
||||
absolute: absoluteTime(todoTimeValue(item)),
|
||||
actionKind: 'todo',
|
||||
|
|
@ -419,8 +423,8 @@
|
|||
const captureCandidates = sortByTime(captureRows).map(item => ({
|
||||
id: item.captureId || `capture:${timeValue(item)}`,
|
||||
category: 'captures',
|
||||
title: `Review capture ${quoted(captureTitle(item))}`,
|
||||
meta: `${itemTimeLabel(item)} · ${item.domain || item.kind || 'Browser capture'}`,
|
||||
title: tr('overview.event.reviewCapture', { title: captureTitle(item) }),
|
||||
meta: `${itemTimeLabel(item)} · ${item.domain || captureKindLabel(item) || tr('overview.browserCapture')}`,
|
||||
time: timeValue(item),
|
||||
absolute: absoluteTime(timeValue(item)),
|
||||
actionKind: 'browser-inbox',
|
||||
|
|
@ -435,7 +439,7 @@
|
|||
const journalCandidates = sortByTime(journalRows).map(item => ({
|
||||
id: item.entryId || `journal:${timeValue(item)}`,
|
||||
category: 'journal',
|
||||
title: `Continue journal entry ${quoted(journalTitle(item))}`,
|
||||
title: tr('overview.event.continueJournal', { title: journalTitle(item) }),
|
||||
meta: itemTimeLabel(item),
|
||||
time: timeValue(item),
|
||||
absolute: absoluteTime(timeValue(item)),
|
||||
|
|
@ -449,14 +453,18 @@
|
|||
const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
|
||||
id: item.captureId || `capture:${timeValue(item)}`,
|
||||
title: captureTitle(item),
|
||||
meta: `${item.kind || 'capture'} · ${itemTimeLabel(item)}`,
|
||||
meta: `${captureKindLabel(item)} · ${itemTimeLabel(item)}`,
|
||||
actionKind: 'browser-inbox',
|
||||
actionLabel: tr('overview.reviewInbox'),
|
||||
}));
|
||||
const candidateItems = sortByTime(candidates).slice(0, 4).map(item => ({
|
||||
id: item.candidateId || `work-session:${timeValue(item)}`,
|
||||
title: 'Possible journal entry',
|
||||
meta: `Workspace: ${item.workspaceRootPath || workspaceRootPath || 'Unknown'} · ${item.estimatedMinutes || 0} min · ${item.activityCount || (item.activityIds || []).length || 0} activities`,
|
||||
title: tr('overview.event.possibleJournalEntry'),
|
||||
meta: tr('overview.candidateMeta', {
|
||||
deal: item.workspaceRootPath || workspaceRootPath || tr('overview.unknownDeal'),
|
||||
minutes: item.estimatedMinutes || 0,
|
||||
activities: item.activityCount || (item.activityIds || []).length || 0,
|
||||
}),
|
||||
actionKind: 'journal',
|
||||
actionLabel: tr('overview.reviewCandidate'),
|
||||
toolRequest: { type: 'work-session-candidate', candidate: item },
|
||||
|
|
@ -464,7 +472,7 @@
|
|||
const todoItems = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).slice(0, 4).map(item => ({
|
||||
id: item.id || `todo:${todoTitle(item)}`,
|
||||
title: todoTitle(item),
|
||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||
meta: `${todoAttentionState(item)}${item.dueAt ? ` · ${tr('overview.todo.due', { date: item.dueAt })}` : ''}`,
|
||||
actionKind: 'todo',
|
||||
actionLabel: tr('overview.openTodos'),
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue