diff --git a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue index 9126cad7..f95dd14d 100644 --- a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue +++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue @@ -6,49 +6,234 @@ import { messageTimestamp } from 'shared/helpers/timeHelper'; import BaseBubble from './Base.vue'; import { useMessageContext } from '../provider.js'; -const { content, createdAt } = useMessageContext(); +const { content, contentAttributes, createdAt } = useMessageContext(); const { locale, t } = useI18n(); +// Mirrors Chatwoot v4.14 conversations.activity. Unknown/custom activity types +// and the unreachable upstream sla.updated branch intentionally use content. +const ACTIVITY_KEYS = { + 'status.resolved': 'STATUS_RESOLVED', + 'status.contact_resolved': 'RESOLVED_BY', + 'status.open': 'STATUS_OPEN', + 'status.pending': 'STATUS_PENDING', + 'status.snoozed': 'STATUS_SNOOZED', + 'status.auto_resolved_days': 'STATUS_AUTO_RESOLVED_DAYS', + 'status.auto_resolved_hours': 'STATUS_AUTO_RESOLVED_HOURS', + 'status.auto_resolved_minutes': 'STATUS_AUTO_RESOLVED_MINUTES', + 'status.system_auto_open': 'STATUS_SYSTEM_AUTO_OPEN', + 'priority.added': 'PRIORITY_ADDED', + 'priority.updated': 'PRIORITY_UPDATED', + 'priority.removed': 'PRIORITY_REMOVED', + 'assignee.self_assigned': 'ASSIGNEE_SELF_ASSIGNED', + 'assignee.assigned': 'ASSIGNEE_ASSIGNED', + 'assignee.removed': 'ASSIGNEE_REMOVED', + 'team.assigned': 'TEAM_ASSIGNED', + 'team.assigned_with_assignee': 'TEAM_ASSIGNED_WITH_ASSIGNEE', + 'team.removed': 'TEAM_REMOVED', + 'labels.added': 'LABELS_ADDED', + 'labels.removed': 'LABELS_REMOVED', + 'sla.added': 'SLA_ADDED', + 'sla.removed': 'SLA_REMOVED', + muted: 'MUTED', + unmuted: 'UNMUTED', + 'captain.resolved': 'CAPTAIN_RESOLVED', + 'captain.resolved_with_reason': 'CAPTAIN_RESOLVED_WITH_REASON', + 'captain.resolved_by_tool': 'CAPTAIN_RESOLVED_BY_TOOL', + 'captain.open': 'CAPTAIN_OPEN', + 'captain.open_with_reason': 'CAPTAIN_OPEN_WITH_REASON', + 'captain.auto_opened_after_agent_reply': + 'CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY', + 'agent_bot.error_moved_to_open': 'BOT_WEBHOOK_FAILURE', + 'csat.not_sent_due_to_messaging_window': 'CSAT_RESTRICTED', + 'auto_resolve.not_sent_due_to_messaging_window': 'AUTO_RESOLVE_RESTRICTED', + auto_resolution_message: 'AUTO_RESOLUTION_MESSAGE', + 'linear.issue_created': 'LINEAR_CREATED', + 'linear.issue_linked': 'LINEAR_LINKED', + 'linear.issue_unlinked': 'LINEAR_UNLINKED', + 'whatsapp_call.permission_requested': 'WHATSAPP_CALL_PERMISSION_REQUESTED', + 'whatsapp_call.permission_granted': 'WHATSAPP_CALL_PERMISSION_GRANTED', +}; + +const FIXED_CONTENT_KEYS = { + 'Conversation was reopened due to agent bot webhook failure': + 'BOT_WEBHOOK_FAILURE', + 'Conversation was marked open by system due to an error with the agent bot.': + 'BOT_WEBHOOK_FAILURE', + 'CSAT survey not sent due to outgoing message restrictions': + 'CSAT_RESTRICTED', + 'Auto-resolve message not sent due to outgoing message restrictions': + 'AUTO_RESOLVE_RESTRICTED', + 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.': + 'AUTO_RESOLUTION_MESSAGE', + 'System reopened the conversation due to a new incoming message.': + 'STATUS_SYSTEM_AUTO_OPEN', + 'Conversation was marked open automatically after an agent reply': + 'CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY', +}; + +const CONTENT_PATTERNS = [ + [ + /^Conversation was marked resolved by (.+) due to inactivity$/, + 'CAPTAIN_RESOLVED', + ['userName'], + ], + [ + /^Conversation was marked resolved by (.+) \((.+)\)$/, + 'CAPTAIN_RESOLVED_WITH_REASON', + ['userName', 'reason'], + ], + [ + /^Conversation was marked resolved by (.+): (.+)$/, + 'CAPTAIN_RESOLVED_BY_TOOL', + ['userName', 'reason'], + ], + [ + /^Conversation was marked open by (.+) \((.+)\)$/, + 'CAPTAIN_OPEN_WITH_REASON', + ['userName', 'reason'], + ], + [/^Conversation was marked open by (.+)$/, 'CAPTAIN_OPEN', ['userName']], + [/^Conversation was resolved by (.+)$/, 'RESOLVED_BY', ['contactName']], + [ + /^Conversation was marked resolved by system due to (\d+) days of inactivity$/, + 'STATUS_AUTO_RESOLVED_DAYS', + ['count'], + ], + [ + /^Conversation was marked resolved by system due to (\d+) hours of inactivity$/, + 'STATUS_AUTO_RESOLVED_HOURS', + ['count'], + ], + [ + /^Conversation was marked resolved by system due to (\d+) minutes of inactivity$/, + 'STATUS_AUTO_RESOLVED_MINUTES', + ['count'], + ], + [ + /^Conversation was marked resolved by (.+)$/, + 'STATUS_RESOLVED', + ['userName'], + ], + [/^Conversation was reopened by (.+)$/, 'STATUS_OPEN', ['userName']], + [ + /^Conversation was marked as pending by (.+)$/, + 'STATUS_PENDING', + ['userName'], + ], + [/^Conversation was snoozed by (.+)$/, 'STATUS_SNOOZED', ['userName']], + [ + /^(.+) set the priority to (.+)$/, + 'PRIORITY_ADDED', + ['userName', 'newPriority'], + ], + [ + /^(.+) changed the priority from (.+) to (.+)$/, + 'PRIORITY_UPDATED', + ['userName', 'oldPriority', 'newPriority'], + ], + [/^(.+) removed the priority$/, 'PRIORITY_REMOVED', ['userName']], + [ + /^(.+) self-assigned this conversation$/, + 'ASSIGNEE_SELF_ASSIGNED', + ['userName'], + ], + [ + /^Assigned to (.+) via (.+) by (.+)$/, + 'TEAM_ASSIGNED_WITH_ASSIGNEE', + ['assigneeName', 'teamName', 'userName'], + ], + [ + /^Assigned to (.+) by (.+)$/, + 'ASSIGNEE_ASSIGNED', + ['assigneeName', 'userName'], + ], + [/^Conversation unassigned by (.+)$/, 'ASSIGNEE_REMOVED', ['userName']], + [/^Unassigned from (.+) by (.+)$/, 'TEAM_REMOVED', ['teamName', 'userName']], + [/^(.+) added SLA policy (.+)$/, 'SLA_ADDED', ['userName', 'slaName']], + [/^(.+) removed SLA policy (.*)$/, 'SLA_REMOVED', ['userName', 'slaName']], + [/^(.+) added (.+)$/, 'LABELS_ADDED', ['userName', 'labels']], + [/^(.+) removed (.+)$/, 'LABELS_REMOVED', ['userName', 'labels']], + [/^(.+) has muted the conversation$/, 'MUTED', ['userName']], + [/^(.+) has unmuted the conversation$/, 'UNMUTED', ['userName']], + [ + /^Linear issue (\S+) was created by (.+)$/, + 'LINEAR_CREATED', + ['issueId', 'userName'], + ], + [ + /^Linear issue (\S+) was linked by (.+)$/, + 'LINEAR_LINKED', + ['issueId', 'userName'], + ], + [ + /^Linear issue (\S+) was unlinked by (.+)$/, + 'LINEAR_UNLINKED', + ['issueId', 'userName'], + ], + [ + /^Sent a call permission request to (.+)\.$/, + 'WHATSAPP_CALL_PERMISSION_REQUESTED', + ['contactName'], + ], + [ + /^(.+) accepted the call permission request\.$/, + 'WHATSAPP_CALL_PERMISSION_GRANTED', + ['contactName'], + ], +]; + const localizedContent = computed(() => { const normalizedContent = String(content.value ?? ''); - const resolvedBy = normalizedContent.match( - /^Conversation was resolved by (.+)$/ - ); - if (resolvedBy) { - const name = ['Anonymous Visitor', 'visitor'].includes(resolvedBy[1]) - ? t('CONVERSATION.ACTIVITY.ANONYMOUS_VISITOR') - : resolvedBy[1]; - return t('CONVERSATION.ACTIVITY.RESOLVED_BY', { name }); - } + const structuredActivity = contentAttributes.value?.activity; if ( - normalizedContent === - 'Conversation was reopened due to agent bot webhook failure' + structuredActivity && + !Object.hasOwn(ACTIVITY_KEYS, structuredActivity.type) ) { - return t('CONVERSATION.ACTIVITY.BOT_WEBHOOK_FAILURE'); + return normalizedContent; } - if ( - normalizedContent === - 'CSAT survey not sent due to outgoing message restrictions' - ) { - return t('CONVERSATION.ACTIVITY.CSAT_RESTRICTED'); - } - const linear = normalizedContent.match( - /^Linear issue (\S+) was (created|linked|unlinked) by (.+)$/ - ); - if (linear) { - const params = { - issueId: linear[1], - name: linear[3], - }; - if (linear[2] === 'created') { - return t('CONVERSATION.ACTIVITY.LINEAR_CREATED', params); + + let key = structuredActivity + ? ACTIVITY_KEYS[structuredActivity.type] + : FIXED_CONTENT_KEYS[normalizedContent]; + let params = structuredActivity; + + if (!structuredActivity && !key) { + const matchedPattern = CONTENT_PATTERNS.find(([pattern]) => + pattern.test(normalizedContent) + ); + if (matchedPattern) { + const [pattern, patternKey, paramNames] = matchedPattern; + const match = normalizedContent.match(pattern); + key = patternKey; + params = Object.fromEntries( + paramNames.map((paramName, index) => [paramName, match[index + 1]]) + ); } - if (linear[2] === 'linked') { - return t('CONVERSATION.ACTIVITY.LINEAR_LINKED', params); - } - return t('CONVERSATION.ACTIVITY.LINEAR_UNLINKED', params); } + + if (key) { + params = { ...params }; + if (Array.isArray(params.labels)) params.labels = params.labels.join(', '); + if (['Anonymous Visitor', 'visitor'].includes(params.contactName)) { + params.contactName = t('CONVERSATION.ACTIVITY.ANONYMOUS_VISITOR'); + } + if (params.userName === 'Automation System') { + params.userName = t('CONVERSATION.ACTIVITY.AUTOMATION_SYSTEM'); + } + ['oldPriority', 'newPriority'].forEach(priorityKey => { + const priority = params[priorityKey]; + if (['urgent', 'high', 'medium', 'low'].includes(priority)) { + // eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys + params[priorityKey] = t( + `CONVERSATION.PRIORITY.OPTIONS.${priority.toUpperCase()}` + ); + } + }); + // eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys + return t(`CONVERSATION.ACTIVITY.${key}`, params); + } + return normalizedContent; }); diff --git a/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js b/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js index f762ffa7..50a5e968 100644 --- a/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js +++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/specs/Activity.spec.js @@ -1,10 +1,20 @@ import { config, mount } from '@vue/test-utils'; -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { + afterAll, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; import Activity from '../Activity.vue'; const mocks = vi.hoisted(() => ({ content: '', + contentAttributes: {}, })); vi.mock('shared/helpers/timeHelper', () => ({ @@ -30,6 +40,7 @@ vi.mock('../../provider.js', async () => { return { useMessageContext: () => ({ content: ref(mocks.content), + contentAttributes: ref(mocks.contentAttributes), createdAt: ref(1786018000), }), }; @@ -38,11 +49,7 @@ vi.mock('../../provider.js', async () => { const mountActivity = () => mount(Activity, { global: { - directives: { - dompurifyHtml: (element, binding) => { - element.innerHTML = binding.value; - }, - }, + plugins: [VueDOMPurifyHTML], stubs: { BaseBubble: { template: '
', @@ -52,6 +59,12 @@ const mountActivity = () => }); describe('Activity bubble', () => { + beforeEach(() => { + i18n.global.locale.value = 'zh_CN'; + mocks.content = ''; + mocks.contentAttributes = {}; + }); + it('allows long links in system messages to wrap inside the bubble', () => { mocks.content = `来源页面:${longURL}`; const wrapper = mountActivity(); @@ -68,8 +81,36 @@ describe('Activity bubble', () => { }); it.each([ - ['Conversation was resolved by Anonymous Visitor', '会话已由匿名访客解决'], - ['Conversation was resolved by Alice', '会话已由Alice解决'], + [ + 'Conversation was resolved by Anonymous Visitor', + '会话已由 匿名访客 解决', + ], + [ + 'Conversation was marked resolved by Alice', + '会话已由 Alice 标记为已解决', + ], + ['Conversation was reopened by Alice', '会话已由 Alice 重新打开'], + [ + 'Conversation was marked as pending by Alice', + '会话已由 Alice 标记为待处理', + ], + ['Conversation was snoozed by Alice', '会话已由 Alice 推迟'], + [ + 'Conversation was marked resolved by system due to 2 hours of inactivity', + '会话因 2 小时无活动已由系统自动解决', + ], + [ + 'Alice changed the priority from low to high', + 'Alice 将优先级从 低 更改为 高', + ], + ['Assigned to Bob by Alice', 'Alice 将会话分配给 Bob'], + [ + 'Assigned to Bob via Support by Alice', + 'Alice 通过团队 Support 将会话分配给 Bob', + ], + ['Alice added vip, billing', 'Alice 添加了标签:vip, billing'], + ['Alice removed SLA policy Gold', 'Alice 移除了 SLA 策略 Gold'], + ['Alice has muted the conversation', 'Alice 已将会话静音'], [ 'Conversation was reopened due to agent bot webhook failure', '由于客服机器人 Webhook 失败,会话已重新打开', @@ -78,19 +119,20 @@ describe('Activity bubble', () => { 'CSAT survey not sent due to outgoing message restrictions', '由于发送消息受限,未发送 CSAT 调查', ], + [ + 'Auto-resolve message not sent due to outgoing message restrictions', + '由于发送消息受限,未发送自动解决消息', + ], [ 'Linear issue ENG-1 was created by Alice', 'Linear 问题 ENG-1 已由 Alice 创建', ], [ - 'Linear issue ENG-1 was linked by Alice', - 'Linear 问题 ENG-1 已由 Alice 关联', + 'Conversation was marked resolved by Captain due to inactivity', + 'Captain 因无活动将会话标记为已解决', ], - [ - 'Linear issue ENG-1 was unlinked by Alice', - 'Linear 问题 ENG-1 已由 Alice 取消关联', - ], - ])('localizes known backend activity content: %s', (content, expected) => { + ['Sent a call permission request to Bob.', '已向 Bob 发送通话权限请求。'], + ])('localizes known legacy activity content: %s', (content, expected) => { mocks.content = content; const wrapper = mountActivity(); @@ -99,6 +141,215 @@ describe('Activity bubble', () => { ); }); + it.each([ + [ + { type: 'status.resolved', userName: 'Alice' }, + '会话已由 Alice 标记为已解决', + ], + [ + { type: 'status.contact_resolved', contactName: 'visitor' }, + '会话已由 匿名访客 解决', + ], + [{ type: 'status.open', userName: 'Alice' }, '会话已由 Alice 重新打开'], + [ + { type: 'status.pending', userName: 'Alice' }, + '会话已由 Alice 标记为待处理', + ], + [{ type: 'status.snoozed', userName: 'Alice' }, '会话已由 Alice 推迟'], + [ + { type: 'status.auto_resolved_days', count: 2 }, + '会话因 2 天无活动已由系统自动解决', + ], + [ + { type: 'status.auto_resolved_hours', count: 3 }, + '会话因 3 小时无活动已由系统自动解决', + ], + [ + { type: 'status.auto_resolved_minutes', count: 15 }, + '会话因 15 分钟无活动已由系统自动解决', + ], + [ + { type: 'status.system_auto_open' }, + '收到新的传入消息后,系统重新打开了会话。', + ], + [ + { type: 'priority.added', userName: 'Alice', newPriority: 'urgent' }, + 'Alice 将优先级设为 紧急的', + ], + [ + { + type: 'priority.updated', + userName: 'Alice', + oldPriority: 'low', + newPriority: 'high', + }, + 'Alice 将优先级从 低 更改为 高', + ], + [{ type: 'priority.removed', userName: 'Alice' }, 'Alice 移除了优先级'], + [ + { type: 'assignee.self_assigned', userName: 'Alice' }, + 'Alice 将此会话分配给自己', + ], + [ + { type: 'assignee.assigned', userName: 'Alice', assigneeName: 'Bob' }, + 'Alice 将会话分配给 Bob', + ], + [{ type: 'assignee.removed', userName: 'Alice' }, 'Alice 取消了会话分配'], + [ + { type: 'team.assigned', userName: 'Alice', teamName: 'Support' }, + 'Alice 将会话分配给团队 Support', + ], + [ + { + type: 'team.assigned_with_assignee', + userName: 'Alice', + teamName: 'Support', + assigneeName: 'Bob', + }, + 'Alice 通过团队 Support 将会话分配给 Bob', + ], + [ + { type: 'team.removed', userName: 'Alice', teamName: 'Support' }, + 'Alice 将会话从团队 Support 中移除', + ], + [ + { type: 'labels.added', userName: 'Alice', labels: ['vip', 'billing'] }, + 'Alice 添加了标签:vip, billing', + ], + [ + { type: 'labels.removed', userName: 'Alice', labels: 'vip' }, + 'Alice 移除了标签:vip', + ], + [ + { type: 'sla.added', userName: 'Alice', slaName: 'Gold' }, + 'Alice 添加了 SLA 策略 Gold', + ], + [ + { type: 'sla.removed', userName: 'Alice', slaName: 'Gold' }, + 'Alice 移除了 SLA 策略 Gold', + ], + [{ type: 'muted', userName: 'Alice' }, 'Alice 已将会话静音'], + [{ type: 'unmuted', userName: 'Alice' }, 'Alice 已取消会话静音'], + [ + { type: 'captain.resolved', userName: 'Captain' }, + 'Captain 因无活动将会话标记为已解决', + ], + [ + { + type: 'captain.resolved_with_reason', + userName: 'Captain', + reason: 'Done', + }, + 'Captain 将会话标记为已解决(Done)', + ], + [ + { type: 'captain.resolved_by_tool', userName: 'Captain', reason: 'Done' }, + 'Captain 将会话标记为已解决:Done', + ], + [{ type: 'captain.open', userName: 'Captain' }, 'Captain 将会话标记为打开'], + [ + { + type: 'captain.open_with_reason', + userName: 'Captain', + reason: 'Reply', + }, + 'Captain 将会话标记为打开(Reply)', + ], + [ + { type: 'captain.auto_opened_after_agent_reply' }, + '客服回复后,会话已自动标记为打开', + ], + [ + { type: 'agent_bot.error_moved_to_open' }, + '由于客服机器人 Webhook 失败,会话已重新打开', + ], + [ + { type: 'csat.not_sent_due_to_messaging_window' }, + '由于发送消息受限,未发送 CSAT 调查', + ], + [ + { type: 'auto_resolve.not_sent_due_to_messaging_window' }, + '由于发送消息受限,未发送自动解决消息', + ], + [ + { type: 'auto_resolution_message' }, + '会话已有一段时间没有活动,现将自动解决。如需进一步帮助,请发起新会话。', + ], + [ + { type: 'linear.issue_created', issueId: 'ENG-1', userName: 'Alice' }, + 'Linear 问题 ENG-1 已由 Alice 创建', + ], + [ + { type: 'linear.issue_linked', issueId: 'ENG-1', userName: 'Alice' }, + 'Linear 问题 ENG-1 已由 Alice 关联', + ], + [ + { type: 'linear.issue_unlinked', issueId: 'ENG-1', userName: 'Alice' }, + 'Linear 问题 ENG-1 已由 Alice 取消关联', + ], + [ + { type: 'whatsapp_call.permission_requested', contactName: 'Bob' }, + '已向 Bob 发送通话权限请求。', + ], + [ + { type: 'whatsapp_call.permission_granted', contactName: 'Bob' }, + 'Bob 已接受通话权限请求。', + ], + ])('localizes a structured activity type', (activity, expected) => { + mocks.content = 'Untranslated fallback'; + mocks.contentAttributes = { activity }; + const wrapper = mountActivity(); + + expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe( + expected + ); + }); + + it.each([ + ['custom.unknown', 'Conversation was resolved by Alice'], + ['sla.updated', 'Alice added SLA policy Gold'], + ['toString', 'Conversation was reopened by Alice'], + ['constructor', 'Conversation was marked as pending by Alice'], + ])( + 'falls back to stored content for unsupported structured type %s', + (type, content) => { + mocks.content = content; + mocks.contentAttributes = { activity: { type } }; + + expect(mountActivity().get('span').text()).toBe(content); + } + ); + + it('uses the English activity catalog when the dashboard locale is English', () => { + i18n.global.locale.value = 'en'; + mocks.contentAttributes = { + activity: { + type: 'priority.updated', + userName: 'Automation System', + oldPriority: 'low', + newPriority: 'high', + }, + }; + + expect(mountActivity().get('span').text()).toBe( + 'Automation System changed the priority from Low to High' + ); + }); + + it('sanitizes malicious dynamic activity names with the real directive', () => { + mocks.contentAttributes = { + activity: { + type: 'status.resolved', + userName: 'Alice', + }, + }; + const content = mountActivity().get('span'); + + expect(content.text()).toBe('会话已由 Alice 标记为已解决'); + expect(content.get('img').attributes('onerror')).toBeUndefined(); + expect(window.activityXss).toBeUndefined(); + }); + it('renders an empty activity when content is null', () => { mocks.content = null; const wrapper = mountActivity(); diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json index fdfbcad3..71b0b70b 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -4,12 +4,46 @@ "CSAT_REPLY_MESSAGE": "Please rate the conversation", "ACTIVITY": { "ANONYMOUS_VISITOR": "Anonymous visitor", - "RESOLVED_BY": "Conversation was resolved by {name}", + "AUTOMATION_SYSTEM": "Automation System", + "RESOLVED_BY": "Conversation was resolved by {contactName}", + "STATUS_RESOLVED": "Conversation was marked resolved by {userName}", + "STATUS_OPEN": "Conversation was reopened by {userName}", + "STATUS_PENDING": "Conversation was marked as pending by {userName}", + "STATUS_SNOOZED": "Conversation was snoozed by {userName}", + "STATUS_AUTO_RESOLVED_DAYS": "Conversation was marked resolved by system due to {count} days of inactivity", + "STATUS_AUTO_RESOLVED_HOURS": "Conversation was marked resolved by system due to {count} hours of inactivity", + "STATUS_AUTO_RESOLVED_MINUTES": "Conversation was marked resolved by system due to {count} minutes of inactivity", + "STATUS_SYSTEM_AUTO_OPEN": "System reopened the conversation due to a new incoming message.", + "PRIORITY_ADDED": "{userName} set the priority to {newPriority}", + "PRIORITY_UPDATED": "{userName} changed the priority from {oldPriority} to {newPriority}", + "PRIORITY_REMOVED": "{userName} removed the priority", + "ASSIGNEE_SELF_ASSIGNED": "{userName} self-assigned this conversation", + "ASSIGNEE_ASSIGNED": "Assigned to {assigneeName} by {userName}", + "ASSIGNEE_REMOVED": "Conversation unassigned by {userName}", + "TEAM_ASSIGNED": "Assigned to {teamName} by {userName}", + "TEAM_ASSIGNED_WITH_ASSIGNEE": "Assigned to {assigneeName} via {teamName} by {userName}", + "TEAM_REMOVED": "Unassigned from {teamName} by {userName}", + "LABELS_ADDED": "{userName} added {labels}", + "LABELS_REMOVED": "{userName} removed {labels}", + "SLA_ADDED": "{userName} added SLA policy {slaName}", + "SLA_REMOVED": "{userName} removed SLA policy {slaName}", + "MUTED": "{userName} has muted the conversation", + "UNMUTED": "{userName} has unmuted the conversation", + "CAPTAIN_RESOLVED": "Conversation was marked resolved by {userName} due to inactivity", + "CAPTAIN_RESOLVED_WITH_REASON": "Conversation was marked resolved by {userName} ({reason})", + "CAPTAIN_RESOLVED_BY_TOOL": "Conversation was marked resolved by {userName}: {reason}", + "CAPTAIN_OPEN": "Conversation was marked open by {userName}", + "CAPTAIN_OPEN_WITH_REASON": "Conversation was marked open by {userName} ({reason})", + "CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY": "Conversation was marked open automatically after an agent reply", "BOT_WEBHOOK_FAILURE": "Conversation was reopened because the agent bot webhook failed", "CSAT_RESTRICTED": "CSAT survey was not sent due to outgoing message restrictions", - "LINEAR_CREATED": "Linear issue {issueId} was created by {name}", - "LINEAR_LINKED": "Linear issue {issueId} was linked by {name}", - "LINEAR_UNLINKED": "Linear issue {issueId} was unlinked by {name}" + "AUTO_RESOLVE_RESTRICTED": "Auto-resolve message was not sent due to outgoing message restrictions", + "AUTO_RESOLUTION_MESSAGE": "Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.", + "LINEAR_CREATED": "Linear issue {issueId} was created by {userName}", + "LINEAR_LINKED": "Linear issue {issueId} was linked by {userName}", + "LINEAR_UNLINKED": "Linear issue {issueId} was unlinked by {userName}", + "WHATSAPP_CALL_PERMISSION_REQUESTED": "Sent a call permission request to {contactName}.", + "WHATSAPP_CALL_PERMISSION_GRANTED": "{contactName} accepted the call permission request." }, "404": "Sorry, we cannot find the conversation. Please try again", "SWITCH_VIEW_LAYOUT": "Switch the layout", diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json index 181bdcdd..d4e80997 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json @@ -4,12 +4,46 @@ "CSAT_REPLY_MESSAGE": "请为会话评分", "ACTIVITY": { "ANONYMOUS_VISITOR": "匿名访客", - "RESOLVED_BY": "会话已由{name}解决", + "AUTOMATION_SYSTEM": "自动化系统", + "RESOLVED_BY": "会话已由 {contactName} 解决", + "STATUS_RESOLVED": "会话已由 {userName} 标记为已解决", + "STATUS_OPEN": "会话已由 {userName} 重新打开", + "STATUS_PENDING": "会话已由 {userName} 标记为待处理", + "STATUS_SNOOZED": "会话已由 {userName} 推迟", + "STATUS_AUTO_RESOLVED_DAYS": "会话因 {count} 天无活动已由系统自动解决", + "STATUS_AUTO_RESOLVED_HOURS": "会话因 {count} 小时无活动已由系统自动解决", + "STATUS_AUTO_RESOLVED_MINUTES": "会话因 {count} 分钟无活动已由系统自动解决", + "STATUS_SYSTEM_AUTO_OPEN": "收到新的传入消息后,系统重新打开了会话。", + "PRIORITY_ADDED": "{userName} 将优先级设为 {newPriority}", + "PRIORITY_UPDATED": "{userName} 将优先级从 {oldPriority} 更改为 {newPriority}", + "PRIORITY_REMOVED": "{userName} 移除了优先级", + "ASSIGNEE_SELF_ASSIGNED": "{userName} 将此会话分配给自己", + "ASSIGNEE_ASSIGNED": "{userName} 将会话分配给 {assigneeName}", + "ASSIGNEE_REMOVED": "{userName} 取消了会话分配", + "TEAM_ASSIGNED": "{userName} 将会话分配给团队 {teamName}", + "TEAM_ASSIGNED_WITH_ASSIGNEE": "{userName} 通过团队 {teamName} 将会话分配给 {assigneeName}", + "TEAM_REMOVED": "{userName} 将会话从团队 {teamName} 中移除", + "LABELS_ADDED": "{userName} 添加了标签:{labels}", + "LABELS_REMOVED": "{userName} 移除了标签:{labels}", + "SLA_ADDED": "{userName} 添加了 SLA 策略 {slaName}", + "SLA_REMOVED": "{userName} 移除了 SLA 策略 {slaName}", + "MUTED": "{userName} 已将会话静音", + "UNMUTED": "{userName} 已取消会话静音", + "CAPTAIN_RESOLVED": "{userName} 因无活动将会话标记为已解决", + "CAPTAIN_RESOLVED_WITH_REASON": "{userName} 将会话标记为已解决({reason})", + "CAPTAIN_RESOLVED_BY_TOOL": "{userName} 将会话标记为已解决:{reason}", + "CAPTAIN_OPEN": "{userName} 将会话标记为打开", + "CAPTAIN_OPEN_WITH_REASON": "{userName} 将会话标记为打开({reason})", + "CAPTAIN_AUTO_OPENED_AFTER_AGENT_REPLY": "客服回复后,会话已自动标记为打开", "BOT_WEBHOOK_FAILURE": "由于客服机器人 Webhook 失败,会话已重新打开", "CSAT_RESTRICTED": "由于发送消息受限,未发送 CSAT 调查", - "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {name} 创建", - "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {name} 关联", - "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {name} 取消关联" + "AUTO_RESOLVE_RESTRICTED": "由于发送消息受限,未发送自动解决消息", + "AUTO_RESOLUTION_MESSAGE": "会话已有一段时间没有活动,现将自动解决。如需进一步帮助,请发起新会话。", + "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {userName} 创建", + "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {userName} 关联", + "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {userName} 取消关联", + "WHATSAPP_CALL_PERMISSION_REQUESTED": "已向 {contactName} 发送通话权限请求。", + "WHATSAPP_CALL_PERMISSION_GRANTED": "{contactName} 已接受通话权限请求。" }, "404": "抱歉,我们找不到对话。请重试", "SWITCH_VIEW_LAYOUT": "切换布局",