@dev.smartpricing/message-composer-layer 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/app/api/ai.ts +28 -0
- package/app/api/brands.ts +54 -0
- package/app/api/client.ts +56 -0
- package/app/api/compilation.ts +34 -0
- package/app/api/dynamicValues.ts +11 -0
- package/app/api/index.ts +11 -0
- package/app/api/languages.ts +6 -0
- package/app/api/media.ts +26 -0
- package/app/api/messageGroups.ts +49 -0
- package/app/api/messages.ts +22 -0
- package/app/api/meta.ts +18 -0
- package/app/api/tracking.ts +9 -0
- package/app/components/Brand/BrandDrawer.vue +281 -0
- package/app/components/Brand/BrandPreview.vue +137 -0
- package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
- package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
- package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
- package/app/components/Common/BrandColorSwatches.vue +37 -0
- package/app/components/Common/ColorInput.vue +144 -0
- package/app/components/Common/EmojiPickerButton.vue +35 -0
- package/app/components/Common/FontFamilySelect.vue +39 -0
- package/app/components/Common/HtmlPreviewIframe.vue +68 -0
- package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
- package/app/components/Common/LanguageList.vue +356 -0
- package/app/components/Common/LoadingOverlay.vue +12 -0
- package/app/components/Common/MediaUploadOrSelect.vue +299 -0
- package/app/components/Common/MessagePreviewSlideover.vue +88 -0
- package/app/components/Common/MonacoEditor.vue +110 -0
- package/app/components/Common/PhoneInput.vue +804 -0
- package/app/components/Common/SectionHeader.vue +26 -0
- package/app/components/Common/TranslationActionBar.vue +139 -0
- package/app/components/Common/ViewLessMore.vue +40 -0
- package/app/components/CreateTemplateButton.vue +59 -0
- package/app/components/DynamicDataButton.vue +75 -0
- package/app/components/DynamicDataSlideover.vue +149 -0
- package/app/components/Editor/AiGenerateSlideover.vue +292 -0
- package/app/components/Editor/EditorLinkPopover.vue +154 -0
- package/app/components/Editor/RichTextEditor.vue +495 -0
- package/app/components/Email/Editor/BlockLibrary.vue +89 -0
- package/app/components/Email/Editor/BlockSettings.vue +60 -0
- package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
- package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
- package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
- package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
- package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
- package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
- package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
- package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
- package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
- package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
- package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
- package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
- package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
- package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
- package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
- package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
- package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
- package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
- package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
- package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
- package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
- package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
- package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
- package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
- package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
- package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
- package/app/components/Email/Editor/Canvas.vue +143 -0
- package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
- package/app/components/Email/Editor/Index.vue +77 -0
- package/app/components/Email/Editor/LeftSidebar.vue +95 -0
- package/app/components/Email/InlinePreview.vue +124 -0
- package/app/components/Email/Preview.vue +126 -0
- package/app/components/Email/PreviewPanel.vue +57 -0
- package/app/components/Email/SendTestEmailModal.vue +121 -0
- package/app/components/Email/TranslationEditor.vue +198 -0
- package/app/components/Email/Translator/Button.vue +101 -0
- package/app/components/Email/Translator/Discount.vue +60 -0
- package/app/components/Email/Translator/Footer.vue +66 -0
- package/app/components/Email/Translator/Grid.vue +127 -0
- package/app/components/Email/Translator/Header.vue +90 -0
- package/app/components/Email/Translator/Heading.vue +66 -0
- package/app/components/Email/Translator/Image.vue +91 -0
- package/app/components/Email/Translator/Index.vue +138 -0
- package/app/components/Email/Translator/Paragraph.vue +66 -0
- package/app/components/Email/Translator/RawHtml.vue +64 -0
- package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
- package/app/components/TemplateList/BaseTable.vue +354 -0
- package/app/components/TemplateList/StatusCell.vue +67 -0
- package/app/components/Whatsapp/CategoryChanger.vue +63 -0
- package/app/components/Whatsapp/Editor/Body.vue +53 -0
- package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
- package/app/components/Whatsapp/Editor/Footer.vue +70 -0
- package/app/components/Whatsapp/Editor/Header.vue +156 -0
- package/app/components/Whatsapp/Editor/Index.vue +89 -0
- package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
- package/app/components/Whatsapp/Preview.vue +197 -0
- package/app/components/Whatsapp/PreviewPanel.vue +24 -0
- package/app/components/Whatsapp/TranslationEditor.vue +150 -0
- package/app/components/Whatsapp/Translator/Body.vue +61 -0
- package/app/components/Whatsapp/Translator/Button.vue +209 -0
- package/app/components/Whatsapp/Translator/Footer.vue +56 -0
- package/app/components/Whatsapp/Translator/Header.vue +61 -0
- package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
- package/app/components/Whatsapp/Translator/Index.vue +159 -0
- package/app/composables/composerContext.ts +35 -0
- package/app/composables/useAutoTranslate.ts +242 -0
- package/app/composables/useAutoTranslateEmail.ts +294 -0
- package/app/composables/useChangeTracking.ts +212 -0
- package/app/composables/useChangeTrackingEmail.ts +227 -0
- package/app/composables/useChannels.ts +36 -0
- package/app/composables/useEmailBlockContext.ts +43 -0
- package/app/composables/useExitConfirmation.ts +39 -0
- package/app/composables/useGoBack.ts +15 -0
- package/app/composables/useIncompleteTranslations.ts +55 -0
- package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
- package/app/composables/useMediaLimits.ts +42 -0
- package/app/composables/useTemplateFormValidation.ts +109 -0
- package/app/composables/useTemplateOperations.ts +147 -0
- package/app/composables/useTracking.ts +39 -0
- package/app/composables/useTranslationFilters.ts +176 -0
- package/app/composables/useTranslationFiltersEmail.ts +244 -0
- package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
- package/app/middleware/channel-guard.ts +13 -0
- package/app/pages/brands.vue +195 -0
- package/app/pages/images.vue +141 -0
- package/app/pages/index.vue +198 -0
- package/app/pages/preview/[id].vue +47 -0
- package/app/pages/template/email/[id].vue +596 -0
- package/app/pages/template/email/create.vue +562 -0
- package/app/pages/template/whatsapp/[id].vue +561 -0
- package/app/pages/template/whatsapp/create.vue +468 -0
- package/app/plugins/admin-shortcut.client.ts +7 -0
- package/app/plugins/tracking.client.ts +12 -0
- package/app/queries/ai.ts +20 -0
- package/app/queries/brands.ts +75 -0
- package/app/queries/compilation.ts +37 -0
- package/app/queries/dynamicValues.ts +13 -0
- package/app/queries/index.ts +9 -0
- package/app/queries/languages.ts +10 -0
- package/app/queries/media.ts +27 -0
- package/app/queries/messageGroups.ts +112 -0
- package/app/queries/messages.ts +42 -0
- package/app/queries/meta.ts +13 -0
- package/app/stores/adminStore.ts +50 -0
- package/app/stores/appContext.ts +99 -0
- package/app/stores/emailEditor.ts +220 -0
- package/app/stores/main.ts +3 -0
- package/app/stores/metaSubmission.ts +116 -0
- package/app/types/app-config.d.ts +14 -0
- package/app/types/hooks.d.ts +9 -0
- package/app/types/tracking.ts +148 -0
- package/app/utils/locale.ts +17 -0
- package/app/utils/notifyParent.ts +7 -0
- package/app/utils/resolveDefaultLanguage.ts +14 -0
- package/app/utils/socialDefaults.ts +42 -0
- package/app/utils/testIds.const.ts +728 -0
- package/app.config.ts +5 -0
- package/i18n/check-translations.ts +56 -0
- package/i18n/locales/de.ts +834 -0
- package/i18n/locales/en.ts +826 -0
- package/i18n/locales/es.ts +833 -0
- package/i18n/locales/fr.ts +837 -0
- package/i18n/locales/it.ts +831 -0
- package/nuxt.config.ts +65 -0
- package/package.json +64 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type { WhatsappMessage, PostEntity } from '@dev.smartpricing/message-composer-utils/types'
|
|
3
|
+
import {
|
|
4
|
+
isTiptapEmpty,
|
|
5
|
+
extractText,
|
|
6
|
+
createTipTapContent,
|
|
7
|
+
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
8
|
+
import { useTranslateMutation } from '~/queries'
|
|
9
|
+
|
|
10
|
+
export function useAutoTranslate() {
|
|
11
|
+
const { t } = useI18n()
|
|
12
|
+
const { mutateAsync: translate, isLoading: isTranslating } = useTranslateMutation()
|
|
13
|
+
const error = ref<string | null>(null)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Translate text using the backend API
|
|
17
|
+
*/
|
|
18
|
+
const translateText = async (
|
|
19
|
+
text: string,
|
|
20
|
+
sourceLanguage: string,
|
|
21
|
+
targetLanguage: string,
|
|
22
|
+
): Promise<string | null> => {
|
|
23
|
+
try {
|
|
24
|
+
const result = await translate({
|
|
25
|
+
text,
|
|
26
|
+
sourceLanguage,
|
|
27
|
+
targetLanguages: [targetLanguage],
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (result?.translations?.[targetLanguage]) {
|
|
31
|
+
return result.translations[targetLanguage]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('Translation error:', err)
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Translate all empty fields in a message
|
|
43
|
+
*/
|
|
44
|
+
const translateAllEmpty = async (
|
|
45
|
+
baseMessage: PostEntity<WhatsappMessage>,
|
|
46
|
+
targetMessage: PostEntity<WhatsappMessage>,
|
|
47
|
+
baseLanguage: string,
|
|
48
|
+
targetLanguage: string,
|
|
49
|
+
): Promise<boolean> => {
|
|
50
|
+
error.value = null
|
|
51
|
+
|
|
52
|
+
if (!targetMessage.body) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
let hasTranslations = false
|
|
58
|
+
|
|
59
|
+
// Translate header if empty
|
|
60
|
+
if (
|
|
61
|
+
baseMessage.body?.header &&
|
|
62
|
+
targetMessage.body?.header &&
|
|
63
|
+
baseMessage.body.header.type === 'text' &&
|
|
64
|
+
targetMessage.body.header.type === 'text' &&
|
|
65
|
+
!isTiptapEmpty(baseMessage.body.header.content, baseMessage.render_type) &&
|
|
66
|
+
isTiptapEmpty(targetMessage.body?.header.content, targetMessage.render_type)
|
|
67
|
+
) {
|
|
68
|
+
const headerText = extractText(baseMessage.body.header.content, baseMessage.render_type)
|
|
69
|
+
if (headerText) {
|
|
70
|
+
const translated = await translateText(headerText, baseLanguage, targetLanguage)
|
|
71
|
+
if (translated) {
|
|
72
|
+
targetMessage.body.header.content = createTipTapContent(translated)
|
|
73
|
+
hasTranslations = true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Translate body if empty
|
|
79
|
+
if (
|
|
80
|
+
baseMessage.body?.body &&
|
|
81
|
+
targetMessage.body?.body &&
|
|
82
|
+
!isTiptapEmpty(baseMessage.body.body.content, baseMessage.render_type) &&
|
|
83
|
+
isTiptapEmpty(targetMessage.body?.body.content, targetMessage.render_type)
|
|
84
|
+
) {
|
|
85
|
+
const bodyText = extractText(baseMessage.body.body.content, baseMessage.render_type)
|
|
86
|
+
if (bodyText) {
|
|
87
|
+
const translated = await translateText(bodyText, baseLanguage, targetLanguage)
|
|
88
|
+
if (translated) {
|
|
89
|
+
targetMessage.body.body.content = createTipTapContent(translated)
|
|
90
|
+
hasTranslations = true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Translate footer if empty
|
|
96
|
+
if (
|
|
97
|
+
baseMessage.body?.footer &&
|
|
98
|
+
targetMessage.body?.footer &&
|
|
99
|
+
!isTiptapEmpty(baseMessage.body.footer.content, baseMessage.render_type) &&
|
|
100
|
+
isTiptapEmpty(targetMessage.body?.footer.content, targetMessage.render_type)
|
|
101
|
+
) {
|
|
102
|
+
const footerText = extractText(baseMessage.body.footer.content, baseMessage.render_type)
|
|
103
|
+
if (footerText) {
|
|
104
|
+
const translated = await translateText(footerText, baseLanguage, targetLanguage)
|
|
105
|
+
if (translated) {
|
|
106
|
+
targetMessage.body.footer.content = translated
|
|
107
|
+
hasTranslations = true
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Translate buttons if empty
|
|
113
|
+
if (baseMessage.body?.buttons && baseMessage.body.buttons.buttons.length > 0) {
|
|
114
|
+
for (let i = 0; i < baseMessage.body.buttons.buttons.length; i++) {
|
|
115
|
+
const baseButton = baseMessage.body.buttons.buttons[i]
|
|
116
|
+
const targetButton = targetMessage.body?.buttons?.buttons?.[i]
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
baseButton?.text &&
|
|
120
|
+
(!targetButton || isTiptapEmpty(targetButton.text, targetMessage.render_type))
|
|
121
|
+
) {
|
|
122
|
+
const buttonText = extractText(baseButton.text, baseMessage.render_type)
|
|
123
|
+
if (buttonText) {
|
|
124
|
+
const translated = await translateText(buttonText, baseLanguage, targetLanguage)
|
|
125
|
+
if (translated && targetMessage.body.buttons?.buttons?.[i]) {
|
|
126
|
+
targetMessage.body.buttons.buttons[i]!.text = translated
|
|
127
|
+
hasTranslations = true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return hasTranslations
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('Auto-translate error:', err)
|
|
137
|
+
error.value = err instanceof Error ? err.message : t('common.translation_failed')
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Translate all fields in a message (overwrite existing translations)
|
|
144
|
+
*/
|
|
145
|
+
const translateMessage = async (
|
|
146
|
+
baseMessage: PostEntity<WhatsappMessage>,
|
|
147
|
+
targetMessage: PostEntity<WhatsappMessage>,
|
|
148
|
+
baseLanguage: string,
|
|
149
|
+
targetLanguage: string,
|
|
150
|
+
): Promise<boolean> => {
|
|
151
|
+
error.value = null
|
|
152
|
+
|
|
153
|
+
if (!targetMessage.body) {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
let hasTranslations = false
|
|
159
|
+
|
|
160
|
+
// Translate header (overwrite)
|
|
161
|
+
if (
|
|
162
|
+
baseMessage.body?.header &&
|
|
163
|
+
targetMessage.body?.header &&
|
|
164
|
+
baseMessage.body.header.type === 'text' &&
|
|
165
|
+
targetMessage.body.header.type === 'text' &&
|
|
166
|
+
!isTiptapEmpty(baseMessage.body.header.content, baseMessage.render_type)
|
|
167
|
+
) {
|
|
168
|
+
const headerText = extractText(baseMessage.body.header.content, baseMessage.render_type)
|
|
169
|
+
if (headerText) {
|
|
170
|
+
const translated = await translateText(headerText, baseLanguage, targetLanguage)
|
|
171
|
+
if (translated) {
|
|
172
|
+
targetMessage.body.header.content = createTipTapContent(translated)
|
|
173
|
+
hasTranslations = true
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Translate body (overwrite)
|
|
179
|
+
if (
|
|
180
|
+
baseMessage.body?.body &&
|
|
181
|
+
targetMessage.body?.body &&
|
|
182
|
+
!isTiptapEmpty(baseMessage.body.body.content, baseMessage.render_type)
|
|
183
|
+
) {
|
|
184
|
+
const bodyText = extractText(baseMessage.body.body.content, baseMessage.render_type)
|
|
185
|
+
if (bodyText) {
|
|
186
|
+
const translated = await translateText(bodyText, baseLanguage, targetLanguage)
|
|
187
|
+
if (translated) {
|
|
188
|
+
targetMessage.body.body.content = createTipTapContent(translated)
|
|
189
|
+
hasTranslations = true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Translate footer (overwrite)
|
|
195
|
+
if (
|
|
196
|
+
baseMessage.body?.footer &&
|
|
197
|
+
targetMessage.body?.footer &&
|
|
198
|
+
!isTiptapEmpty(baseMessage.body.footer.content, baseMessage.render_type)
|
|
199
|
+
) {
|
|
200
|
+
const footerText = extractText(baseMessage.body.footer.content, baseMessage.render_type)
|
|
201
|
+
if (footerText) {
|
|
202
|
+
const translated = await translateText(footerText, baseLanguage, targetLanguage)
|
|
203
|
+
if (translated) {
|
|
204
|
+
targetMessage.body.footer.content = translated
|
|
205
|
+
hasTranslations = true
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Translate buttons (overwrite)
|
|
211
|
+
if (baseMessage.body?.buttons && baseMessage.body.buttons.buttons.length > 0) {
|
|
212
|
+
for (let i = 0; i < baseMessage.body.buttons.buttons.length; i++) {
|
|
213
|
+
const baseButton = baseMessage.body.buttons.buttons[i]
|
|
214
|
+
|
|
215
|
+
if (baseButton?.text && !isTiptapEmpty(baseButton.text, baseMessage.render_type)) {
|
|
216
|
+
const buttonText = extractText(baseButton.text, baseMessage.render_type)
|
|
217
|
+
if (buttonText) {
|
|
218
|
+
const translated = await translateText(buttonText, baseLanguage, targetLanguage)
|
|
219
|
+
if (translated && targetMessage.body.buttons?.buttons?.[i]) {
|
|
220
|
+
targetMessage.body.buttons.buttons[i]!.text = translated
|
|
221
|
+
hasTranslations = true
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return hasTranslations
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error('Auto-translate error:', err)
|
|
231
|
+
error.value = err instanceof Error ? err.message : t('common.translation_failed')
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
translateMessage,
|
|
238
|
+
translateAllEmpty,
|
|
239
|
+
isTranslating,
|
|
240
|
+
error,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
EmailMessage,
|
|
4
|
+
PostEntity,
|
|
5
|
+
EmailBlock,
|
|
6
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
7
|
+
import {
|
|
8
|
+
isTiptapEmpty,
|
|
9
|
+
extractText,
|
|
10
|
+
createTipTapContent,
|
|
11
|
+
deepClone,
|
|
12
|
+
} from '@dev.smartpricing/message-composer-utils/utils'
|
|
13
|
+
import { useTranslateMutation } from '~/queries'
|
|
14
|
+
|
|
15
|
+
export function useAutoTranslateEmail() {
|
|
16
|
+
const { mutateAsync: translate, isLoading: isTranslating } = useTranslateMutation()
|
|
17
|
+
const error = ref<string | null>(null)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Translate text using backend API
|
|
21
|
+
*/
|
|
22
|
+
async function translateText(
|
|
23
|
+
text: string,
|
|
24
|
+
sourceLanguage: string,
|
|
25
|
+
targetLanguage: string,
|
|
26
|
+
): Promise<string | null> {
|
|
27
|
+
try {
|
|
28
|
+
const result = await translate({
|
|
29
|
+
text,
|
|
30
|
+
sourceLanguage,
|
|
31
|
+
targetLanguages: [targetLanguage],
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (result?.translations?.[targetLanguage]) {
|
|
35
|
+
return result.translations[targetLanguage]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error('Translation error:', err)
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Translate a single block
|
|
47
|
+
*/
|
|
48
|
+
async function translateBlock(
|
|
49
|
+
baseBlock: EmailBlock,
|
|
50
|
+
targetBlock: EmailBlock,
|
|
51
|
+
baseLanguage: string,
|
|
52
|
+
targetLanguage: string,
|
|
53
|
+
onlyEmpty: boolean = false,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
if (baseBlock.type !== targetBlock.type) return false
|
|
56
|
+
|
|
57
|
+
switch (baseBlock.type) {
|
|
58
|
+
case 'heading':
|
|
59
|
+
case 'paragraph':
|
|
60
|
+
case 'footer':
|
|
61
|
+
case 'unsubscribe_link': {
|
|
62
|
+
// Check if base has content
|
|
63
|
+
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If onlyEmpty, check if target already has content
|
|
68
|
+
if (onlyEmpty && !isTiptapEmpty(targetBlock.settings.content, 'email_v1')) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract text from base
|
|
73
|
+
const text = extractText(baseBlock.settings.content, 'email_v1')
|
|
74
|
+
if (!text) return false
|
|
75
|
+
|
|
76
|
+
// Translate
|
|
77
|
+
const translated = await translateText(text, baseLanguage, targetLanguage)
|
|
78
|
+
if (!translated) return false
|
|
79
|
+
|
|
80
|
+
// Update target block
|
|
81
|
+
targetBlock.settings.content = createTipTapContent(translated, { preset: 'email' })
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'discount': {
|
|
86
|
+
// Check if base has content
|
|
87
|
+
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
targetBlock.settings.content = deepClone(baseBlock.settings.content)
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case 'button': {
|
|
95
|
+
// Check if base has content
|
|
96
|
+
if (isTiptapEmpty(baseBlock.settings.content, 'email_v1')) {
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If onlyEmpty, check if target already has content
|
|
101
|
+
if (onlyEmpty && !isTiptapEmpty(targetBlock.settings.content, 'email_v1')) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract text from base
|
|
106
|
+
const text = extractText(baseBlock.settings.content, 'email_v1')
|
|
107
|
+
if (!text) return false
|
|
108
|
+
|
|
109
|
+
// Translate button text only (not URL)
|
|
110
|
+
const translated = await translateText(text, baseLanguage, targetLanguage)
|
|
111
|
+
if (!translated) return false
|
|
112
|
+
|
|
113
|
+
// Update target block content only
|
|
114
|
+
targetBlock.settings.content = createTipTapContent(translated, { preset: 'email' })
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'raw_html': {
|
|
119
|
+
const translated = await translateText(
|
|
120
|
+
baseBlock.settings.html,
|
|
121
|
+
baseLanguage,
|
|
122
|
+
targetLanguage,
|
|
123
|
+
)
|
|
124
|
+
if (!translated) return false
|
|
125
|
+
targetBlock.settings.html = translated
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Recursively translate blocks including nested grid blocks
|
|
136
|
+
*/
|
|
137
|
+
async function translateBlocksRecursive(
|
|
138
|
+
baseBlocks: EmailBlock[],
|
|
139
|
+
targetBlocks: EmailBlock[],
|
|
140
|
+
baseLanguage: string,
|
|
141
|
+
targetLanguage: string,
|
|
142
|
+
onlyEmpty: boolean = false,
|
|
143
|
+
): Promise<number> {
|
|
144
|
+
let translatedCount = 0
|
|
145
|
+
|
|
146
|
+
// Build ID-based map of target blocks
|
|
147
|
+
const targetBlocksMap = new Map(targetBlocks.map((b) => [b.id, b]))
|
|
148
|
+
|
|
149
|
+
for (const baseBlock of baseBlocks) {
|
|
150
|
+
const targetBlock = targetBlocksMap.get(baseBlock.id)
|
|
151
|
+
|
|
152
|
+
if (!baseBlock || !targetBlock) continue
|
|
153
|
+
|
|
154
|
+
// Handle grid blocks recursively
|
|
155
|
+
if (baseBlock.type === 'grid' && targetBlock.type === 'grid') {
|
|
156
|
+
// Build ID-based map of target columns
|
|
157
|
+
const targetColumnsMap = new Map(
|
|
158
|
+
targetBlock.settings.columnData.map((col) => [col.id, col]),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
for (const baseColumn of baseBlock.settings.columnData) {
|
|
162
|
+
const targetColumn = targetColumnsMap.get(baseColumn.id)
|
|
163
|
+
|
|
164
|
+
if (baseColumn && targetColumn) {
|
|
165
|
+
const nestedCount = await translateBlocksRecursive(
|
|
166
|
+
baseColumn.blocks,
|
|
167
|
+
targetColumn.blocks,
|
|
168
|
+
baseLanguage,
|
|
169
|
+
targetLanguage,
|
|
170
|
+
onlyEmpty,
|
|
171
|
+
)
|
|
172
|
+
translatedCount += nestedCount
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// Translate regular block
|
|
177
|
+
const success = await translateBlock(
|
|
178
|
+
baseBlock,
|
|
179
|
+
targetBlock,
|
|
180
|
+
baseLanguage,
|
|
181
|
+
targetLanguage,
|
|
182
|
+
onlyEmpty,
|
|
183
|
+
)
|
|
184
|
+
if (success) translatedCount++
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return translatedCount
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Translate all empty fields in message
|
|
193
|
+
*/
|
|
194
|
+
async function translateAllEmpty(
|
|
195
|
+
baseMessage: PostEntity<EmailMessage>,
|
|
196
|
+
targetMessage: PostEntity<EmailMessage>,
|
|
197
|
+
baseLanguage: string,
|
|
198
|
+
targetLanguage: string,
|
|
199
|
+
): Promise<boolean> {
|
|
200
|
+
error.value = null
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
let hasTranslations = false
|
|
204
|
+
|
|
205
|
+
// Translate subject if empty
|
|
206
|
+
if (
|
|
207
|
+
baseMessage.body.subject &&
|
|
208
|
+
baseMessage.body.subject.trim() !== '' &&
|
|
209
|
+
(!targetMessage.body.subject || targetMessage.body.subject.trim() === '')
|
|
210
|
+
) {
|
|
211
|
+
const translated = await translateText(
|
|
212
|
+
baseMessage.body.subject,
|
|
213
|
+
baseLanguage,
|
|
214
|
+
targetLanguage,
|
|
215
|
+
)
|
|
216
|
+
if (translated) {
|
|
217
|
+
targetMessage.body.subject = translated
|
|
218
|
+
hasTranslations = true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Translate blocks (only empty ones)
|
|
223
|
+
const count = await translateBlocksRecursive(
|
|
224
|
+
baseMessage.body.blocks,
|
|
225
|
+
targetMessage.body.blocks,
|
|
226
|
+
baseLanguage,
|
|
227
|
+
targetLanguage,
|
|
228
|
+
true, // onlyEmpty
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if (count > 0) hasTranslations = true
|
|
232
|
+
|
|
233
|
+
return hasTranslations
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error('Auto-translate error:', err)
|
|
236
|
+
error.value = err instanceof Error ? err.message : 'Translation failed'
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Translate all fields in message (overwrite existing)
|
|
243
|
+
*/
|
|
244
|
+
async function translateMessage(
|
|
245
|
+
baseMessage: PostEntity<EmailMessage>,
|
|
246
|
+
targetMessage: PostEntity<EmailMessage>,
|
|
247
|
+
baseLanguage: string,
|
|
248
|
+
targetLanguage: string,
|
|
249
|
+
): Promise<boolean> {
|
|
250
|
+
error.value = null
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
let hasTranslations = false
|
|
254
|
+
|
|
255
|
+
// Translate subject (overwrite)
|
|
256
|
+
if (baseMessage.body.subject && baseMessage.body.subject.trim() !== '') {
|
|
257
|
+
const translated = await translateText(
|
|
258
|
+
baseMessage.body.subject,
|
|
259
|
+
baseLanguage,
|
|
260
|
+
targetLanguage,
|
|
261
|
+
)
|
|
262
|
+
if (translated) {
|
|
263
|
+
targetMessage.body.subject = translated
|
|
264
|
+
hasTranslations = true
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Translate blocks (overwrite all)
|
|
269
|
+
const count = await translateBlocksRecursive(
|
|
270
|
+
baseMessage.body.blocks,
|
|
271
|
+
targetMessage.body.blocks,
|
|
272
|
+
baseLanguage,
|
|
273
|
+
targetLanguage,
|
|
274
|
+
false, // overwrite
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if (count > 0) hasTranslations = true
|
|
278
|
+
|
|
279
|
+
return hasTranslations
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.error('Auto-translate error:', err)
|
|
282
|
+
error.value = err instanceof Error ? err.message : 'Translation failed'
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
translateMessage,
|
|
289
|
+
translateAllEmpty,
|
|
290
|
+
translateText,
|
|
291
|
+
isTranslating,
|
|
292
|
+
error,
|
|
293
|
+
}
|
|
294
|
+
}
|