diff --git a/app/javascript/dashboard/components-next/message/MessageList.vue b/app/javascript/dashboard/components-next/message/MessageList.vue index b73b44d5a..fd725f808 100644 --- a/app/javascript/dashboard/components-next/message/MessageList.vue +++ b/app/javascript/dashboard/components-next/message/MessageList.vue @@ -42,7 +42,10 @@ const props = defineProps({ const emit = defineEmits(['retry']); const allMessages = computed(() => { - return useCamelCase(props.messages, { deep: true }); + return useCamelCase(props.messages, { + deep: true, + stopPaths: ['content_attributes.translations'], + }); }); const currentChat = useMapGetter('getSelectedChat'); diff --git a/app/javascript/dashboard/composables/spec/useTranslations.spec.js b/app/javascript/dashboard/composables/spec/useTranslations.spec.js index c7ed3cb88..7f8e29bc5 100644 --- a/app/javascript/dashboard/composables/spec/useTranslations.spec.js +++ b/app/javascript/dashboard/composables/spec/useTranslations.spec.js @@ -1,39 +1,31 @@ -import { ref } from 'vue'; -import { useTranslations } from '../useTranslations'; +import { selectTranslation } from '../useTranslations'; -describe('useTranslations', () => { - it('returns false and null when contentAttributes is null', () => { - const contentAttributes = ref(null); - const { hasTranslations, translationContent } = - useTranslations(contentAttributes); - expect(hasTranslations.value).toBe(false); - expect(translationContent.value).toBeNull(); +describe('selectTranslation', () => { + it('returns null when translations is null', () => { + expect(selectTranslation(null, 'en', 'en')).toBeNull(); }); - it('returns false and null when translations are missing', () => { - const contentAttributes = ref({}); - const { hasTranslations, translationContent } = - useTranslations(contentAttributes); - expect(hasTranslations.value).toBe(false); - expect(translationContent.value).toBeNull(); + it('returns null when translations is empty', () => { + expect(selectTranslation({}, 'en', 'en')).toBeNull(); }); - it('returns false and null when translations is an empty object', () => { - const contentAttributes = ref({ translations: {} }); - const { hasTranslations, translationContent } = - useTranslations(contentAttributes); - expect(hasTranslations.value).toBe(false); - expect(translationContent.value).toBeNull(); + it('returns first translation when no locale matches', () => { + const translations = { en: 'Hello', es: 'Hola' }; + expect(selectTranslation(translations, 'fr', 'de')).toBe('Hello'); }); - it('returns true and correct translation content when translations exist', () => { - const contentAttributes = ref({ - translations: { en: 'Hello' }, - }); - const { hasTranslations, translationContent } = - useTranslations(contentAttributes); - expect(hasTranslations.value).toBe(true); - // Should return the first translation (en: 'Hello') - expect(translationContent.value).toBe('Hello'); + it('returns translation matching agent locale', () => { + const translations = { en: 'Hello', es: 'Hola', zh_CN: '你好' }; + expect(selectTranslation(translations, 'es', 'en')).toBe('Hola'); + }); + + it('falls back to account locale when agent locale not found', () => { + const translations = { en: 'Hello', zh_CN: '你好' }; + expect(selectTranslation(translations, 'fr', 'zh_CN')).toBe('你好'); + }); + + it('returns first translation when both locales are undefined', () => { + const translations = { en: 'Hello', es: 'Hola' }; + expect(selectTranslation(translations, undefined, undefined)).toBe('Hello'); }); }); diff --git a/app/javascript/dashboard/composables/useTranslations.js b/app/javascript/dashboard/composables/useTranslations.js index 0b18ea3da..5be0c011f 100644 --- a/app/javascript/dashboard/composables/useTranslations.js +++ b/app/javascript/dashboard/composables/useTranslations.js @@ -1,4 +1,25 @@ import { computed } from 'vue'; +import { useUISettings } from './useUISettings'; +import { useAccount } from './useAccount'; + +/** + * Select translation based on locale priority. + * @param {Object} translations - Translations object with locale keys + * @param {string} agentLocale - Agent's preferred locale + * @param {string} accountLocale - Account's default locale + * @returns {string|null} Selected translation or null + */ +export function selectTranslation(translations, agentLocale, accountLocale) { + if (!translations || Object.keys(translations).length === 0) return null; + + if (agentLocale && translations[agentLocale]) { + return translations[agentLocale]; + } + if (accountLocale && translations[accountLocale]) { + return translations[accountLocale]; + } + return translations[Object.keys(translations)[0]]; +} /** * Composable to extract translation state/content from contentAttributes. @@ -6,6 +27,9 @@ import { computed } from 'vue'; * @returns {Object} { hasTranslations, translationContent } */ export function useTranslations(contentAttributes) { + const { uiSettings } = useUISettings(); + const { currentAccount } = useAccount(); + const hasTranslations = computed(() => { if (!contentAttributes.value) return false; const { translations = {} } = contentAttributes.value; @@ -14,8 +38,11 @@ export function useTranslations(contentAttributes) { const translationContent = computed(() => { if (!hasTranslations.value) return null; - const translations = contentAttributes.value.translations; - return translations[Object.keys(translations)[0]]; + return selectTranslation( + contentAttributes.value.translations, + uiSettings.value?.locale, + currentAccount.value?.locale + ); }); return { hasTranslations, translationContent }; diff --git a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue index 632425feb..b55949428 100644 --- a/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue +++ b/app/javascript/dashboard/modules/conversations/components/MessageContextMenu.vue @@ -62,6 +62,7 @@ export default { ...mapGetters({ getAccount: 'accounts/getAccount', currentAccountId: 'getCurrentAccountId', + getUISettings: 'getUISettings', }), plainTextContent() { return this.getPlainText(this.messageContent); @@ -117,11 +118,13 @@ export default { this.$emit('close', e); }, handleTranslate() { - const { locale } = this.getAccount(this.currentAccountId); + const { locale: accountLocale } = this.getAccount(this.currentAccountId); + const agentLocale = this.getUISettings?.locale; + const targetLanguage = agentLocale || accountLocale || 'en'; this.$store.dispatch('translateMessage', { conversationId: this.conversationId, messageId: this.messageId, - targetLanguage: locale || 'en', + targetLanguage, }); useTrack(CONVERSATION_EVENTS.TRANSLATE_A_MESSAGE); this.handleClose(); diff --git a/lib/integrations/google_translate/processor_service.rb b/lib/integrations/google_translate/processor_service.rb index 33fe56770..d17a802a7 100644 --- a/lib/integrations/google_translate/processor_service.rb +++ b/lib/integrations/google_translate/processor_service.rb @@ -11,7 +11,7 @@ class Integrations::GoogleTranslate::ProcessorService response = client.translate_text( contents: [content], - target_language_code: target_language, + target_language_code: bcp47_language_code, parent: "projects/#{hook.settings['project_id']}", mime_type: mime_type ) @@ -23,6 +23,10 @@ class Integrations::GoogleTranslate::ProcessorService private + def bcp47_language_code + target_language.tr('_', '-') + end + def email_channel? message&.inbox&.email? end