diff --git a/frontend/src/lib/locales/de/workspace.js b/frontend/src/lib/locales/de/workspace.js
index 3ff6025de..7ed84c51c 100644
--- a/frontend/src/lib/locales/de/workspace.js
+++ b/frontend/src/lib/locales/de/workspace.js
@@ -260,6 +260,14 @@ export default {
copyWorkItem: 'Vorgang kopieren',
unwatchWorkItem: 'Vorgang nicht mehr beobachten',
watchWorkItem: 'Vorgang beobachten',
+ watchers: 'Beobachter',
+ watchersEmpty: 'Noch niemand beobachtet diesen Vorgang.',
+ watcherAdd: 'Beobachter hinzufügen',
+ watcherRemove: 'Beobachter entfernen',
+ watcherYou: '(du)',
+ watcherPickUser: 'Benutzer zum Hinzufügen auswählen',
+ watchersAddFailed: 'Beobachter konnte nicht hinzugefügt werden',
+ watchersRemoveFailed: 'Beobachter konnte nicht entfernt werden',
noSubIssueTypes: 'Keine Untereintragstypen verfügbar',
cannotCreateChildItems: 'Für diese Hierarchieebene können keine untergeordneten Vorgänge erstellt werden.',
workItems: 'Vorgänge',
diff --git a/frontend/src/lib/locales/en/workspace.js b/frontend/src/lib/locales/en/workspace.js
index 265ec3acd..4c539b36e 100644
--- a/frontend/src/lib/locales/en/workspace.js
+++ b/frontend/src/lib/locales/en/workspace.js
@@ -445,6 +445,14 @@ export default {
copyWorkItem: 'Copy Work Item',
unwatchWorkItem: 'Unwatch Work Item',
watchWorkItem: 'Watch Work Item',
+ watchers: 'Watchers',
+ watchersEmpty: 'Nobody is watching this item yet.',
+ watcherAdd: 'Add watcher',
+ watcherRemove: 'Remove watcher',
+ watcherYou: '(you)',
+ watcherPickUser: 'Select a user to add',
+ watchersAddFailed: 'Failed to add watcher',
+ watchersRemoveFailed: 'Failed to remove watcher',
noSubIssueTypes: 'No sub-issue types available',
cannotCreateChildItems: 'Cannot create child work items for this item level.',
diff --git a/frontend/src/lib/locales/fr/workspace.js b/frontend/src/lib/locales/fr/workspace.js
index d50df32d2..ac6636a15 100644
--- a/frontend/src/lib/locales/fr/workspace.js
+++ b/frontend/src/lib/locales/fr/workspace.js
@@ -329,6 +329,14 @@ export default {
copyWorkItem: 'Copier l’élément de travail',
unwatchWorkItem: 'Ne plus suivre l’élément de travail',
watchWorkItem: 'Suivre l’élément de travail',
+ watchers: 'Observateurs',
+ watchersEmpty: 'Personne ne suit encore cet élément.',
+ watcherAdd: 'Ajouter un observateur',
+ watcherRemove: 'Retirer l’observateur',
+ watcherYou: '(vous)',
+ watcherPickUser: 'Sélectionner un utilisateur à ajouter',
+ watchersAddFailed: 'Impossible d’ajouter l’observateur',
+ watchersRemoveFailed: 'Impossible de retirer l’observateur',
noSubIssueTypes: 'Aucun type de sous-demande disponible',
cannotCreateChildItems: 'Impossible de créer des éléments de travail enfants pour ce niveau d’élément.',
diff --git a/frontend/src/lib/locales/ru/workspace.js b/frontend/src/lib/locales/ru/workspace.js
index 850d3895e..894b0c66a 100644
--- a/frontend/src/lib/locales/ru/workspace.js
+++ b/frontend/src/lib/locales/ru/workspace.js
@@ -48,7 +48,7 @@ export default {
timeTracking: 'Учёт времени', details: 'Сведения', agentLog: 'Журнал агента', agentLogEmpty: 'Для этого элемента ещё не было запусков агента', agentLogTriggeredBy: 'запустил', agentLogWaiting: 'Ожидание результата…', agentRunSingular: 'запуск', agentRunPlural: 'запуски', agentRerunLabel: 'Запустить агента снова', agentRerunStarting: 'Запуск…', agentRerunBusy: 'Выполняется запуск', agentRerunTitle: 'Снова запустить агента для этого элемента', agentRerunFailed: 'Не удалось начать новый запуск', agentReviewFlagTitle: 'Требуется проверка человеком', agentReviewFlagBody: 'Агент неверно использовал инструмент и не смог исправиться — результат может быть ненадёжным.',
created: 'Создан', lastUpdated: 'Последнее обновление', healthOverview: 'Состояние элемента', healthOverviewDescription: 'Признаки на основе недавней активности, срока и истории статусов.', activityHealthCompleted: 'Завершено', activityHealthUnknown: 'Неизвестно', activityHealthStale: 'Нет активности', activityHealthActive: 'Активно', activityMonitoringComplete: 'Отслеживание активности завершено', activityToday: 'Активность сегодня', activityIdleDays: 'Без активности: {count} дн.', activityIdleDays_one: 'Без активности {count} день', activityIdleDays_few: 'Без активности {count} дня', activityIdleDays_many: 'Без активности {count} дней', activityIdleDays_other: 'Без активности {count} дня', lastActivityAt: 'Последняя активность: {date}', activityUnavailable: 'Время последней активности неизвестно', dueHealthCompleted: 'Завершено', dueHealthUnscheduled: 'Не запланировано', dueHealthOverdue: 'Просрочено', dueHealthToday: 'Срок сегодня', dueHealthSoon: 'Срок скоро', dueHealthScheduled: 'Запланировано', workCompleted: 'Работа завершена', completedOn: 'Завершено {date}', dueOn: 'Срок: {date}', dueDateUnavailable: 'Срок не указан', timeInStatus: 'Время в статусе', unknown: 'Неизвестно', statusSince: 'В этом статусе с {date}', statusHistoryUnavailable: 'Данные о времени в статусах недоступны', statusDurationsDescription: 'Общее календарное время, накопленное в каждом статусе.', statusDurationsLoading: 'Расчёт времени в статусах…', statusDurationsLoadError: 'Не удалось рассчитать время в статусах.', statusDurationsRetry: 'Повторить', statusDurationsEmpty: 'Истории статусов пока нет.', currentStatus: 'Текущий', durationLessThanMinute: '< 1 мин.', timeline: 'Хронология', by: 'от', workItemInformation: 'Сведения о рабочем элементе', id: 'ID', type: 'Тип', workItem: 'Рабочий элемент', noProjectConfigured: 'Проект учёта времени не настроен', setDefaultProject: 'Чтобы записывать время, задайте проект по умолчанию в настройках рабочего пространства или элемента', timeEntries: 'Записи времени', startTimer: 'Запустить таймер', logTime: 'Записать время', startTimerTitle: 'Начать учитывать время для этого элемента', logTimeTitle: 'Вручную записать время работы над элементом', noTimeLogged: 'Время пока не записано', logged: 'записано', loggedOf: 'записано из', estimated: 'оценено', includeChildItems: 'Учитывать дочерние элементы', rollupTruncated: 'Учитываются только первые {max} дочерних элементов', rollupItemCount: 'Сумма по элементам: {count}', rollupItemCount_one: 'Сумма по {count} элементу', rollupItemCount_few: 'Сумма по {count} элементам', rollupItemCount_many: 'Сумма по {count} элементам', rollupItemCount_other: 'Сумма по {count} элемента',
workItemDetails: 'Сведения о рабочем элементе', fullDetails: 'Все сведения', errorLoadingWorkItem: 'Не удалось загрузить рабочий элемент', workItemNotFound: 'Рабочий элемент не найден', timerBusy: 'Таймер занят', timerSyncingMessage: 'Таймер синхронизируется. Подождите и повторите попытку.', timerAlreadyRunning: 'Таймер уже запущен', stopTimerFirst: 'Остановите текущий таймер перед запуском нового.', workingOn: 'Работа над «{title}»', failedToStartTimer: 'Не удалось запустить таймер', failedToSaveTimeEntry: 'Не удалось сохранить запись времени', failedToDeleteTimeEntry: 'Не удалось удалить запись времени', deleteTimeEntry: 'Удалить запись времени', deleteTimeEntryConfirm: 'Удалить эту запись времени? Это действие нельзя отменить.', noDescription: 'Без описания', itemCopiedAs: 'Рабочий элемент скопирован как {key}', clickToViewCopied: 'Нажмите, чтобы открыть копию', failedToCopy: 'Не удалось скопировать элемент', moveWorkspaceMenu: 'Переместить в рабочее пространство', moveWorkspaceTitle: 'Переместить «{title}»', moveWorkspaceSubtitle: 'Выберите рабочее пространство и проверьте, как будут перенесены поля.', moveWorkspaceDestination: 'Целевое рабочее пространство', moveWorkspaceDestinationPlaceholder: 'Выберите рабочее пространство…', moveWorkspaceLoadingPreview: 'Подготовка предпросмотра перемещения…', moveWorkspaceNewKeyHint: 'Следующий свободный номер в целевом пространстве будет назначен при подтверждении.', moveWorkspaceChildrenDetached: 'Дочерние элементы станут корневыми в исходном пространстве: {count}', moveWorkspaceDeadKeyWarning: 'Ключ {key} перестанет работать и никогда не будет использован повторно.', moveWorkspaceNoPriority: 'Без приоритета', moveWorkspaceConfirm: 'Переместить элемент', moveWorkspaceCancel: 'Отмена', moveWorkspaceMoving: 'Перемещение…', moveWorkspaceSuccess: 'Элемент перемещён как {key}', moveWorkspaceLoadError: 'Не удалось загрузить целевые рабочие пространства', moveWorkspacePreviewError: 'Не удалось создать предпросмотр перемещения', moveWorkspaceError: 'Не удалось переместить элемент', mergeMenu: 'Объединить дубликат с этим', mergeTitle: 'Объединить дубликат с этой заявкой', mergeDuplicateLabel: 'Дублирующая заявка', mergeHelp: 'Найдите дубликат заявки. Его комментарии, вложения и переписка переедут сюда, а на дубликате появится ссылка на эту заявку.', mergeDifferentRequester: 'У этих заявок разные заявители. Комментарии дубликата переместятся сюда, но станут приватными; файлы, переписка и участники останутся в дубликате.', mergeSelfError: 'Заявку нельзя объединить с самой собой', mergeConfirm: 'Объединить', mergeWorking: 'Объединение…', mergeFailed: 'Не удалось объединить дубликат', mergeSuccess: 'Дубликат объединён с этой заявкой', splitMenu: 'Выделить в подзадачу', splitTitle: 'Выделить в подзадачу', splitNewTitleLabel: 'Заголовок новой подзадачи', splitNewTitlePlaceholder: 'О чём это продолжение?', splitSelectComments: 'Выберите комментарии для переноса', splitNoComments: 'Нет комментариев для выделения', splitTitleRequired: 'Укажите заголовок новой подзадачи', splitSelectionRequired: 'Выберите хотя бы один комментарий', splitConfirm: 'Создать подзадачу', splitWorking: 'Создание…', splitFailed: 'Не удалось выделить подзадачу', splitSuccess: 'Подзадача создана', splitLoadError: 'Не удалось загрузить комментарии', mergedBanner: 'Эта заявка была объединена с другой заявкой.', mergedBannerOpen: 'Открыть основную заявку', deleteWorkItem: 'Удалить рабочий элемент', confirmDeleteItem: 'Удалить «{title}»? Это действие нельзя отменить.', failedToDelete: 'Не удалось удалить элемент',
- deleteItemWithChildren: 'Удалить элемент с дочерними', itemHasChildren: 'У элемента есть дочерние элементы: {count}.', itemHasChildrenSingular: 'У элемента есть 1 дочерний элемент.', deleteAllOption: 'Удалить всё ({count} элементов)', deleteAllDescription: 'Безвозвратно удалить этот элемент и всех его потомков', reparentOption: 'Переместить дочерние элементы', reparentDescription: 'Переместить дочерние элементы на уровень выше, затем удалить только этот элемент', typeToConfirm: 'Введите «{title}», чтобы подтвердить удаление', confirmationPlaceholder: 'Введите название для подтверждения…', deleteAllItems: 'Удалить все элементы', reparentAndDelete: 'Переместить дочерние и удалить', reparentFailed: 'Не удалось переместить дочерние элементы', cascadeDeleteFailed: 'Не удалось удалить дерево элементов', deletedItemsCount: 'Удалено элементов: {count}', reparentedAndDeleted: 'Дочерние элементы перемещены, элемент удалён', selectNewParent: 'Куда переместить дочерние элементы', selectNewParentPlaceholder: 'Выберите родительский элемент…', makeRootItem: 'Сделать корневыми элементами (без родителя)', reparentLevelHint: 'Показаны только элементы того же уровня иерархии', noOtherItemsAtLevel: 'На этом уровне нет других элементов — выберите «Сделать корневыми элементами» или вариант выше', reparentToGrandparent: 'Дочерние элементы будут перемещены к родителю удаляемого элемента', childrenWillBecomeRoot: 'Дочерние элементы станут корневыми', failedToUpdateWatchStatus: 'Не удалось изменить статус отслеживания', copyWorkItem: 'Копировать рабочий элемент', unwatchWorkItem: 'Перестать отслеживать', watchWorkItem: 'Отслеживать рабочий элемент', noSubIssueTypes: 'Нет доступных типов подзадач', cannotCreateChildItems: 'Для элемента этого уровня нельзя создавать дочерние рабочие элементы.',
+ deleteItemWithChildren: 'Удалить элемент с дочерними', itemHasChildren: 'У элемента есть дочерние элементы: {count}.', itemHasChildrenSingular: 'У элемента есть 1 дочерний элемент.', deleteAllOption: 'Удалить всё ({count} элементов)', deleteAllDescription: 'Безвозвратно удалить этот элемент и всех его потомков', reparentOption: 'Переместить дочерние элементы', reparentDescription: 'Переместить дочерние элементы на уровень выше, затем удалить только этот элемент', typeToConfirm: 'Введите «{title}», чтобы подтвердить удаление', confirmationPlaceholder: 'Введите название для подтверждения…', deleteAllItems: 'Удалить все элементы', reparentAndDelete: 'Переместить дочерние и удалить', reparentFailed: 'Не удалось переместить дочерние элементы', cascadeDeleteFailed: 'Не удалось удалить дерево элементов', deletedItemsCount: 'Удалено элементов: {count}', reparentedAndDeleted: 'Дочерние элементы перемещены, элемент удалён', selectNewParent: 'Куда переместить дочерние элементы', selectNewParentPlaceholder: 'Выберите родительский элемент…', makeRootItem: 'Сделать корневыми элементами (без родителя)', reparentLevelHint: 'Показаны только элементы того же уровня иерархии', noOtherItemsAtLevel: 'На этом уровне нет других элементов — выберите «Сделать корневыми элементами» или вариант выше', reparentToGrandparent: 'Дочерние элементы будут перемещены к родителю удаляемого элемента', childrenWillBecomeRoot: 'Дочерние элементы станут корневыми', failedToUpdateWatchStatus: 'Не удалось изменить статус отслеживания', copyWorkItem: 'Копировать рабочий элемент', unwatchWorkItem: 'Перестать отслеживать', watchWorkItem: 'Отслеживать рабочий элемент', watchers: 'Наблюдатели', watchersEmpty: 'Этот элемент пока никто не отслеживает.', watcherAdd: 'Добавить наблюдателя', watcherRemove: 'Удалить наблюдателя', watcherYou: '(вы)', watcherPickUser: 'Выберите пользователя', watchersAddFailed: 'Не удалось добавить наблюдателя', watchersRemoveFailed: 'Не удалось удалить наблюдателя', noSubIssueTypes: 'Нет доступных типов подзадач', cannotCreateChildItems: 'Для элемента этого уровня нельзя создавать дочерние рабочие элементы.',
workItems: 'Рабочие элементы', linkedTo: 'связан с', goToLinkedWorkItem: 'Перейти к связанному элементу', goTo: 'Перейти к «{title}»', noParent: 'Без родителя', setParent: 'Задать родителя', changeParent: 'Изменить родителя', searchForParentItem: 'Поиск родительского элемента…', showingItemsFromLevel: 'Показаны только элементы уровня {level}', oneLevelAbove: 'на уровень выше {name}', searchParentAcrossWorkspaces: 'Искать родительский элемент во всех пространствах', removeParent: 'Удалить родителя', noItemsAtLevel: 'На уровне иерархии {level} элементы не найдены', failedToUpdateParent: 'Не удалось изменить родителя', failedToRemoveParent: 'Не удалось удалить родителя', clickToCopyKey: 'Нажмите, чтобы скопировать ключ', hierarchyMismatchTitle: 'Элемент больше не соответствует своему родителю', hierarchyMismatchDescription: 'Тип {type} настроен на уровне {currentLevel}, а его существующий родитель также находится на уровне {parentLevel}. Связь сохранена, но для смены родителя теперь требуется родитель уровня {requiredLevel}. Выберите тип элемента уровня {compatibleLevel}, чтобы работать с текущим уровнем родителя.', chooseCompatibleType: 'Выбрать совместимый тип', parentPickerMismatchTitle: 'Текущая конфигурация иерархии отличается от сохранённой связи', parentPickerMismatchDescription: 'Тип {type} теперь принимает родителей уровня {requiredLevel}. Существующий родитель находится на уровне {currentParentLevel}; чтобы выбрать другого родителя этого уровня, сначала используйте тип элемента уровня {compatibleLevel}.', compatibleTypeHint: 'Тип уровня {compatibleLevel} соответствует текущему родителю уровня {parentLevel}. При смене типа существующие связи сохраняются.', hierarchyLevelLabel: 'Уровень иерархии {level}', genericSubtaskLevelLabel: 'Обычная подзадача · любой уровень родителя', genericSubtaskParentHint: 'Обычные подзадачи можно перемещать под любой стандартный элемент иерархии.', fitsCurrentParent: 'Соответствует текущему родителю',
enterDescription: 'Введите описание…', clickToEditDescription: 'Нажмите, чтобы изменить описание', clickToAddDescription: 'Нажмите, чтобы добавить описание', noDescriptionProvided: 'Описание не указано — нажмите, чтобы добавить', addLink: 'Добавить связь', createChild: 'Создать дочерний элемент', child: 'Дочерний', attachFile: 'Прикрепить файл', attach: 'Прикрепить', newDiagram: 'Новая диаграмма', diagram: 'Диаграмма', previousValueRemains: 'Предыдущее значение не изменено', titleCannotBeEmpty: 'Название не может быть пустым', enterTitle: 'Введите название…', clickToEditTitle: 'Нажмите, чтобы изменить название',
searchTestCases: 'Поиск тест-кейсов…', searchWorkItems: 'Поиск рабочих элементов…', loadingLinks: 'Загрузка связей…', removeLink: 'Удалить связь', linkType: 'Тип связи', chooseRelationshipType: 'Выберите тип отношения…', linkToTestCase: 'Связать элемент с тест-кейсом.', linkToPage: 'Связать элемент со страницей базы знаний в этом рабочем пространстве.', targetItem: 'Целевой элемент', targetPage: 'Целевая страница', testCase: 'Тест-кейс', page: 'Страница', linkedPages: 'Страницы', pagePickerPlaceholder: 'Поиск страниц…', selectLinkTypeToSearch: 'Выберите тип связи, чтобы начать поиск.', childWorkItems: 'Дочерние рабочие элементы', loadingChildItems: 'Загрузка дочерних элементов…',
diff --git a/frontend/src/lib/locales/zh-CN/workspace.js b/frontend/src/lib/locales/zh-CN/workspace.js
index 5b91b4aed..fab25c02b 100644
--- a/frontend/src/lib/locales/zh-CN/workspace.js
+++ b/frontend/src/lib/locales/zh-CN/workspace.js
@@ -257,6 +257,14 @@ export default {
copyWorkItem: '复制工作项',
unwatchWorkItem: '取消关注工作项',
watchWorkItem: '关注工作项',
+ watchers: '关注者',
+ watchersEmpty: '还没有人关注此工作项。',
+ watcherAdd: '添加关注者',
+ watcherRemove: '移除关注者',
+ watcherYou: '(你)',
+ watcherPickUser: '选择要添加的用户',
+ watchersAddFailed: '添加关注者失败',
+ watchersRemoveFailed: '移除关注者失败',
noSubIssueTypes: '没有可用的子事项类型',
cannotCreateChildItems: '无法为此事项层级创建子工作项。',
workItems: '工作项',
diff --git a/frontend/src/lib/mobile/MobileItemDetail.svelte b/frontend/src/lib/mobile/MobileItemDetail.svelte
index 8b2762d3e..919f56ffb 100644
--- a/frontend/src/lib/mobile/MobileItemDetail.svelte
+++ b/frontend/src/lib/mobile/MobileItemDetail.svelte
@@ -31,6 +31,7 @@
let transitions = $state([]);
let transitioning = $state(false);
let isWatching = $state(false);
+ let watcherCount = $state(0);
let watchBusy = $state(false);
// Workflow-less personal tasks use the permitted transition endpoint to toggle
@@ -114,6 +115,7 @@
item = summary?.item ?? null;
transitions = summary?.transitions?.available_transitions ?? [];
isWatching = summary?.watching || false;
+ watcherCount = summary?.watcher_count ?? 0;
personalTaskCount = summary?.personal_task_count ?? 0;
const childItems = Array.isArray(summary?.children) ? summary.children : [];
children = childItems.filter((child) => child?.id).map(normalizeChild);
@@ -206,13 +208,11 @@
if (watchBusy) return;
watchBusy = true;
try {
- if (isWatching) {
- await api.items.removeWatch(itemId);
- isWatching = false;
- } else {
- await api.items.addWatch(itemId);
- isWatching = true;
- }
+ const status = isWatching
+ ? await api.items.removeWatch(itemId)
+ : await api.items.addWatch(itemId);
+ isWatching = status?.watching ?? !isWatching;
+ watcherCount = status?.watcher_count ?? watcherCount;
} catch (err) {
console.error('Failed to toggle watch:', err);
} finally {
@@ -268,6 +268,7 @@
item = null;
transitions = [];
isWatching = false;
+ watcherCount = 0;
personalTaskCount = 0;
children = [];
ancestors = [];
@@ -322,7 +323,10 @@
} catch { /* keep prior */ }
try {
const res = await api.items.getWatchStatus(itemId);
- if (token === loadToken) isWatching = res?.watching || false;
+ if (token === loadToken) {
+ isWatching = res?.watching || false;
+ watcherCount = res?.watcher_count ?? watcherCount;
+ }
} catch { /* keep prior */ }
}
@@ -507,6 +511,9 @@
{#if canStartTimer}