From 1b5ea7c2274d463922d1e4f2bd2b6bdc96957882 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 23 Aug 2026 21:21:44 +0800 Subject: [PATCH] HH-551: localize activity messages (#132) * fix(HH-551): localize activity messages * fix(HH-551): handle null activity content --------- Co-authored-by: Rogee --- .../message/bubbles/Activity.vue | 48 ++++++- .../message/bubbles/specs/Activity.spec.js | 131 +++++++++++++----- .../i18n/locale/en/conversation.json | 9 ++ .../i18n/locale/zh_CN/conversation.json | 9 ++ 4 files changed, 156 insertions(+), 41 deletions(-) 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 3528276b..9126cad7 100644 --- a/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue +++ b/frontend/app/javascript/dashboard/components-next/message/bubbles/Activity.vue @@ -8,7 +8,49 @@ import { useMessageContext } from '../provider.js'; const { content, createdAt } = useMessageContext(); -const { locale } = useI18n(); +const { locale, t } = useI18n(); + +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 }); + } + if ( + normalizedContent === + 'Conversation was reopened due to agent bot webhook failure' + ) { + return t('CONVERSATION.ACTIVITY.BOT_WEBHOOK_FAILURE'); + } + 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); + } + if (linear[2] === 'linked') { + return t('CONVERSATION.ACTIVITY.LINEAR_LINKED', params); + } + return t('CONVERSATION.ACTIVITY.LINEAR_UNLINKED', params); + } + return normalizedContent; +}); const readableTime = computed(() => { if (locale.value === 'zh_CN') { @@ -28,8 +70,8 @@ const readableTime = computed(() => { data-bubble-name="activity" > 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 78e4a907..f762ffa7 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,53 +1,108 @@ -import { mount } from '@vue/test-utils'; -import { ref } from 'vue'; -import { describe, expect, it, vi } from 'vitest'; +import { config, mount } from '@vue/test-utils'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import Activity from '../Activity.vue'; -vi.mock('vue-i18n', () => ({ - useI18n: () => ({ locale: ref('zh_CN') }), +const mocks = vi.hoisted(() => ({ + content: '', })); vi.mock('shared/helpers/timeHelper', () => ({ - messageTimestamp: () => '8月6日 下午 3:12', + messageTimestamp: () => '8月6日 下午 3:12', })); const longURL = - 'https://ada.baidu.com/site/wjzt6ah8/agent?imid=e159aa97cd2c403f2fba76899e47615f&trinfo=XzFWIaclcWbzcMY&word=%E4%B8%89%E9%98%B3%E7%97%85%E6%AF%92%E6%90%BA%E5%B8%A6'; + 'https://ada.baidu.com/site/wjzt6ah8/agent?imid=e159aa97cd2c403f2fba76899e47615f&trinfo=XzFWIaclcWbzcMY&word=%E4%B8%89%E9%98%B3%E7%97%85%E6%AF%92%E6%90%BA%E5%B8%A6'; -vi.mock('../../provider.js', () => ({ - useMessageContext: () => ({ - content: ref(`来源页面:${longURL}`), - createdAt: ref(1786018000), - }), -})); +const i18n = config.global.plugins.find(plugin => plugin.global?.locale); +const initialLocale = i18n.global.locale.value; + +beforeAll(() => { + i18n.global.locale.value = 'zh_CN'; +}); + +afterAll(() => { + i18n.global.locale.value = initialLocale; +}); + +vi.mock('../../provider.js', async () => { + const { ref } = await vi.importActual('vue'); + return { + useMessageContext: () => ({ + content: ref(mocks.content), + createdAt: ref(1786018000), + }), + }; +}); + +const mountActivity = () => + mount(Activity, { + global: { + directives: { + dompurifyHtml: (element, binding) => { + element.innerHTML = binding.value; + }, + }, + stubs: { + BaseBubble: { + template: '
', + }, + }, + }, + }); describe('Activity bubble', () => { - it('allows long links in system messages to wrap inside the bubble', () => { - const wrapper = mount(Activity, { - global: { - directives: { - dompurifyHtml: (element, binding) => { - element.innerHTML = binding.value; - }, - tooltip: () => {}, - }, - stubs: { - BaseBubble: { - template: '
', - }, - }, - }, - }); + it('allows long links in system messages to wrap inside the bubble', () => { + mocks.content = `来源页面:${longURL}`; + const wrapper = mountActivity(); - expect( - wrapper.get('[data-bubble-name="activity"]').classes() - ).toContain('max-w-full'); + expect(wrapper.get('[data-bubble-name="activity"]').classes()).toContain( + 'max-w-full' + ); - const content = wrapper.get('span'); - expect(content.classes()).toContain('min-w-0'); - expect(content.classes()).toContain('max-w-full'); - expect(content.classes()).toContain('[overflow-wrap:anywhere]'); - expect(content.get('a').text()).toBe(longURL); - }); + const content = wrapper.get('span'); + expect(content.classes()).toContain('min-w-0'); + expect(content.classes()).toContain('max-w-full'); + expect(content.classes()).toContain('[overflow-wrap:anywhere]'); + expect(content.get('a').text()).toBe(longURL); + }); + + it.each([ + ['Conversation was resolved by Anonymous Visitor', '会话已由匿名访客解决'], + ['Conversation was resolved by Alice', '会话已由Alice解决'], + [ + 'Conversation was reopened due to agent bot webhook failure', + '由于客服机器人 Webhook 失败,会话已重新打开', + ], + [ + 'CSAT survey not sent due to outgoing message restrictions', + '由于发送消息受限,未发送 CSAT 调查', + ], + [ + 'Linear issue ENG-1 was created by Alice', + 'Linear 问题 ENG-1 已由 Alice 创建', + ], + [ + 'Linear issue ENG-1 was linked by Alice', + 'Linear 问题 ENG-1 已由 Alice 关联', + ], + [ + 'Linear issue ENG-1 was unlinked by Alice', + 'Linear 问题 ENG-1 已由 Alice 取消关联', + ], + ])('localizes known backend activity content: %s', (content, expected) => { + mocks.content = content; + const wrapper = mountActivity(); + + expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe( + expected + ); + }); + + it('renders an empty activity when content is null', () => { + mocks.content = null; + const wrapper = mountActivity(); + + expect(wrapper.get('[data-bubble-name="activity"] span').text()).toBe(''); + }); }); diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json index 00a82936..fdfbcad3 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -2,6 +2,15 @@ "CONVERSATION": { "SELECT_A_CONVERSATION": "Please select a conversation from left pane", "CSAT_REPLY_MESSAGE": "Please rate the conversation", + "ACTIVITY": { + "ANONYMOUS_VISITOR": "Anonymous visitor", + "RESOLVED_BY": "Conversation was resolved by {name}", + "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}" + }, "404": "Sorry, we cannot find the conversation. Please try again", "SWITCH_VIEW_LAYOUT": "Switch the layout", "DASHBOARD_APP_TAB_MESSAGES": "Messages", 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 67cd8d14..181bdcdd 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/conversation.json @@ -2,6 +2,15 @@ "CONVERSATION": { "SELECT_A_CONVERSATION": "请从左侧窗格选择一个对话", "CSAT_REPLY_MESSAGE": "请为会话评分", + "ACTIVITY": { + "ANONYMOUS_VISITOR": "匿名访客", + "RESOLVED_BY": "会话已由{name}解决", + "BOT_WEBHOOK_FAILURE": "由于客服机器人 Webhook 失败,会话已重新打开", + "CSAT_RESTRICTED": "由于发送消息受限,未发送 CSAT 调查", + "LINEAR_CREATED": "Linear 问题 {issueId} 已由 {name} 创建", + "LINEAR_LINKED": "Linear 问题 {issueId} 已由 {name} 关联", + "LINEAR_UNLINKED": "Linear 问题 {issueId} 已由 {name} 取消关联" + }, "404": "抱歉,我们找不到对话。请重试", "SWITCH_VIEW_LAYOUT": "切换布局", "DASHBOARD_APP_TAB_MESSAGES": "消息",