From 72509f9e38ba71d6c64b0acf748d5cb0c4fd9e56 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:59:06 +0530 Subject: [PATCH] chore: Improve translation service with HTML and plain text support (#11305) # Pull Request Template ## Description This PR changes to translation to properly handle different content types during translation. ### Changes 1. **Email translation with HTML support** - Properly detects and preserves HTML content from emails - Sets `mime_type` to 'text/html' when HTML content is present 2. **Email translation with plain text support** - Falls back to email text content when HTML is not available - Sets `mime_type` to 'text/plain' when HTML is not available and content type includes 'text/plain' 3. **Plain message with plain text support (Non email channels)** - Sets `mime_type` to 'text/plain' for non-email channels - Fixes an issue where Markdown formatting was being lost due to incorrect `mime_type` **Note**: Translation for very long emails is not currently supported. Fixes https://linear.app/chatwoot/issue/CW-4244/translate-button-doesnt-work-in-email-channels ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? **Loom video** https://www.loom.com/share/8f8428ed2cfe415ea5cb6c547c070f00?sid=eab9fa11-05f8-4838-9181-334bee1023c4 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../message/TranslationToggle.vue | 24 ++++++ .../message/bubbles/Email/Index.vue | 76 ++++++++++++++++--- .../message/bubbles/Text/Index.vue | 32 +++----- .../composables/spec/useTranslations.spec.js | 39 ++++++++++ .../dashboard/composables/useTranslations.js | 22 ++++++ .../google_translate/processor_service.rb | 47 +++++++++++- 6 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 app/javascript/dashboard/components-next/message/TranslationToggle.vue create mode 100644 app/javascript/dashboard/composables/spec/useTranslations.spec.js create mode 100644 app/javascript/dashboard/composables/useTranslations.js diff --git a/app/javascript/dashboard/components-next/message/TranslationToggle.vue b/app/javascript/dashboard/components-next/message/TranslationToggle.vue new file mode 100644 index 000000000..19b80dd9f --- /dev/null +++ b/app/javascript/dashboard/components-next/message/TranslationToggle.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue index 7efb1ed53..2e03e97af 100644 --- a/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue +++ b/app/javascript/dashboard/components-next/message/bubbles/Email/Index.vue @@ -9,9 +9,11 @@ import BaseBubble from 'next/message/bubbles/Base.vue'; import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue'; import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; import EmailMeta from './EmailMeta.vue'; +import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue'; import { useMessageContext } from '../../provider.js'; import { MESSAGE_TYPES } from 'next/message/constants.js'; +import { useTranslations } from 'dashboard/composables/useTranslations'; const { content, contentAttributes, attachments, messageType } = useMessageContext(); @@ -19,35 +21,77 @@ const { content, contentAttributes, attachments, messageType } = const isExpandable = ref(false); const isExpanded = ref(false); const showQuotedMessage = ref(false); +const renderOriginal = ref(false); const contentContainer = useTemplateRef('contentContainer'); onMounted(() => { isExpandable.value = contentContainer.value?.scrollHeight > 400; }); -const isOutgoing = computed(() => { - return messageType.value === MESSAGE_TYPES.OUTGOING; -}); +const isOutgoing = computed(() => messageType.value === MESSAGE_TYPES.OUTGOING); const isIncoming = computed(() => !isOutgoing.value); -const textToShow = computed(() => { +const { hasTranslations, translationContent } = + useTranslations(contentAttributes); + +const originalEmailText = computed(() => { const text = contentAttributes?.value?.email?.textContent?.full ?? content.value; return text?.replace(/\n/g, '
'); }); -// Use TextContent as the default to fullHTML +const originalEmailHtml = computed( + () => + contentAttributes?.value?.email?.htmlContent?.full ?? + originalEmailText.value +); + +const messageContent = computed(() => { + // If translations exist and we're showing translations (not original) + if (hasTranslations.value && !renderOriginal.value) { + return translationContent.value; + } + // Otherwise show original content + return content.value; +}); + +const textToShow = computed(() => { + // If translations exist and we're showing translations (not original) + if (hasTranslations.value && !renderOriginal.value) { + return translationContent.value; + } + // Otherwise show original text + return originalEmailText.value; +}); + const fullHTML = computed(() => { - return contentAttributes?.value?.email?.htmlContent?.full ?? textToShow.value; + // If translations exist and we're showing translations (not original) + if (hasTranslations.value && !renderOriginal.value) { + return translationContent.value; + } + // Otherwise show original HTML + return originalEmailHtml.value; }); -const unquotedHTML = computed(() => { - return EmailQuoteExtractor.extractQuotes(fullHTML.value); +const unquotedHTML = computed(() => + EmailQuoteExtractor.extractQuotes(fullHTML.value) +); + +const hasQuotedMessage = computed(() => + EmailQuoteExtractor.hasQuotes(fullHTML.value) +); + +// Ensure unique keys for when toggling between original and translated views. +// This forces Vue to re-render the component and update content correctly. +const translationKeySuffix = computed(() => { + if (renderOriginal.value) return 'original'; + if (hasTranslations.value) return 'translated'; + return 'original'; }); -const hasQuotedMessage = computed(() => { - return EmailQuoteExtractor.hasQuotes(fullHTML.value); -}); +const handleSeeOriginal = () => { + renderOriginal.value = !renderOriginal.value; +};