HH-560: localize upstream activity messages (#137)
* fix(HH-560): localize upstream activity messages * fix(HH-560): preserve unknown activity content --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
+266
-15
@@ -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: '<div><slot /></div>',
|
||||
@@ -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 = `来源页面:<a href="${longURL}">${longURL}</a>`;
|
||||
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<img src=x onerror="window.activityXss = true">',
|
||||
},
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "切换布局",
|
||||
|
||||
Reference in New Issue
Block a user