@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.
Files changed (169) hide show
  1. package/README.md +75 -0
  2. package/app/api/ai.ts +28 -0
  3. package/app/api/brands.ts +54 -0
  4. package/app/api/client.ts +56 -0
  5. package/app/api/compilation.ts +34 -0
  6. package/app/api/dynamicValues.ts +11 -0
  7. package/app/api/index.ts +11 -0
  8. package/app/api/languages.ts +6 -0
  9. package/app/api/media.ts +26 -0
  10. package/app/api/messageGroups.ts +49 -0
  11. package/app/api/messages.ts +22 -0
  12. package/app/api/meta.ts +18 -0
  13. package/app/api/tracking.ts +9 -0
  14. package/app/components/Brand/BrandDrawer.vue +281 -0
  15. package/app/components/Brand/BrandPreview.vue +137 -0
  16. package/app/components/Brand/Steps/BrandStepGeneral.vue +37 -0
  17. package/app/components/Brand/Steps/BrandStepSocials.vue +48 -0
  18. package/app/components/Brand/Steps/BrandStepVisual.vue +214 -0
  19. package/app/components/Common/BrandColorSwatches.vue +37 -0
  20. package/app/components/Common/ColorInput.vue +144 -0
  21. package/app/components/Common/EmojiPickerButton.vue +35 -0
  22. package/app/components/Common/FontFamilySelect.vue +39 -0
  23. package/app/components/Common/HtmlPreviewIframe.vue +68 -0
  24. package/app/components/Common/IncompleteTranslationsDialog.vue +97 -0
  25. package/app/components/Common/LanguageList.vue +356 -0
  26. package/app/components/Common/LoadingOverlay.vue +12 -0
  27. package/app/components/Common/MediaUploadOrSelect.vue +299 -0
  28. package/app/components/Common/MessagePreviewSlideover.vue +88 -0
  29. package/app/components/Common/MonacoEditor.vue +110 -0
  30. package/app/components/Common/PhoneInput.vue +804 -0
  31. package/app/components/Common/SectionHeader.vue +26 -0
  32. package/app/components/Common/TranslationActionBar.vue +139 -0
  33. package/app/components/Common/ViewLessMore.vue +40 -0
  34. package/app/components/CreateTemplateButton.vue +59 -0
  35. package/app/components/DynamicDataButton.vue +75 -0
  36. package/app/components/DynamicDataSlideover.vue +149 -0
  37. package/app/components/Editor/AiGenerateSlideover.vue +292 -0
  38. package/app/components/Editor/EditorLinkPopover.vue +154 -0
  39. package/app/components/Editor/RichTextEditor.vue +495 -0
  40. package/app/components/Email/Editor/BlockLibrary.vue +89 -0
  41. package/app/components/Email/Editor/BlockSettings.vue +60 -0
  42. package/app/components/Email/Editor/Blocks/ButtonBlock.vue +117 -0
  43. package/app/components/Email/Editor/Blocks/ButtonSettings.vue +228 -0
  44. package/app/components/Email/Editor/Blocks/DiscountCodeBlock.vue +74 -0
  45. package/app/components/Email/Editor/Blocks/DiscountCodeSettings.vue +162 -0
  46. package/app/components/Email/Editor/Blocks/DividerBlock.vue +31 -0
  47. package/app/components/Email/Editor/Blocks/DividerSettings.vue +108 -0
  48. package/app/components/Email/Editor/Blocks/FooterBlock.vue +61 -0
  49. package/app/components/Email/Editor/Blocks/FooterSettings.vue +123 -0
  50. package/app/components/Email/Editor/Blocks/GridBlock.vue +128 -0
  51. package/app/components/Email/Editor/Blocks/GridSettings.vue +109 -0
  52. package/app/components/Email/Editor/Blocks/HTMLBlock.vue +31 -0
  53. package/app/components/Email/Editor/Blocks/HTMLSettings.vue +85 -0
  54. package/app/components/Email/Editor/Blocks/HeaderBlock.vue +53 -0
  55. package/app/components/Email/Editor/Blocks/HeaderSettings.vue +113 -0
  56. package/app/components/Email/Editor/Blocks/HeadingBlock.vue +61 -0
  57. package/app/components/Email/Editor/Blocks/HeadingSettings.vue +156 -0
  58. package/app/components/Email/Editor/Blocks/ImageBlock.vue +49 -0
  59. package/app/components/Email/Editor/Blocks/ImageSettings.vue +53 -0
  60. package/app/components/Email/Editor/Blocks/IndexBlock.vue +155 -0
  61. package/app/components/Email/Editor/Blocks/ParagraphBlock.vue +61 -0
  62. package/app/components/Email/Editor/Blocks/ParagraphSettings.vue +157 -0
  63. package/app/components/Email/Editor/Blocks/SocialsBlock.vue +63 -0
  64. package/app/components/Email/Editor/Blocks/SocialsSettings.vue +117 -0
  65. package/app/components/Email/Editor/Blocks/SpacerBlock.vue +37 -0
  66. package/app/components/Email/Editor/Blocks/SpacerSettings.vue +95 -0
  67. package/app/components/Email/Editor/Blocks/TopProductsBlock.vue +47 -0
  68. package/app/components/Email/Editor/Blocks/TopProductsSettings.vue +87 -0
  69. package/app/components/Email/Editor/Blocks/UnsubscribeLinkBlock.vue +61 -0
  70. package/app/components/Email/Editor/Blocks/UnsubscribeLinkSettings.vue +123 -0
  71. package/app/components/Email/Editor/Canvas.vue +143 -0
  72. package/app/components/Email/Editor/GeneralStyleSettings.vue +125 -0
  73. package/app/components/Email/Editor/Index.vue +77 -0
  74. package/app/components/Email/Editor/LeftSidebar.vue +95 -0
  75. package/app/components/Email/InlinePreview.vue +124 -0
  76. package/app/components/Email/Preview.vue +126 -0
  77. package/app/components/Email/PreviewPanel.vue +57 -0
  78. package/app/components/Email/SendTestEmailModal.vue +121 -0
  79. package/app/components/Email/TranslationEditor.vue +198 -0
  80. package/app/components/Email/Translator/Button.vue +101 -0
  81. package/app/components/Email/Translator/Discount.vue +60 -0
  82. package/app/components/Email/Translator/Footer.vue +66 -0
  83. package/app/components/Email/Translator/Grid.vue +127 -0
  84. package/app/components/Email/Translator/Header.vue +90 -0
  85. package/app/components/Email/Translator/Heading.vue +66 -0
  86. package/app/components/Email/Translator/Image.vue +91 -0
  87. package/app/components/Email/Translator/Index.vue +138 -0
  88. package/app/components/Email/Translator/Paragraph.vue +66 -0
  89. package/app/components/Email/Translator/RawHtml.vue +64 -0
  90. package/app/components/Email/Translator/UnsubscribeLink.vue +59 -0
  91. package/app/components/TemplateList/BaseTable.vue +354 -0
  92. package/app/components/TemplateList/StatusCell.vue +67 -0
  93. package/app/components/Whatsapp/CategoryChanger.vue +63 -0
  94. package/app/components/Whatsapp/Editor/Body.vue +53 -0
  95. package/app/components/Whatsapp/Editor/Buttons.vue +341 -0
  96. package/app/components/Whatsapp/Editor/Footer.vue +70 -0
  97. package/app/components/Whatsapp/Editor/Header.vue +156 -0
  98. package/app/components/Whatsapp/Editor/Index.vue +89 -0
  99. package/app/components/Whatsapp/Editor/SyncWrapper.vue +166 -0
  100. package/app/components/Whatsapp/Preview.vue +197 -0
  101. package/app/components/Whatsapp/PreviewPanel.vue +24 -0
  102. package/app/components/Whatsapp/TranslationEditor.vue +150 -0
  103. package/app/components/Whatsapp/Translator/Body.vue +61 -0
  104. package/app/components/Whatsapp/Translator/Button.vue +209 -0
  105. package/app/components/Whatsapp/Translator/Footer.vue +56 -0
  106. package/app/components/Whatsapp/Translator/Header.vue +61 -0
  107. package/app/components/Whatsapp/Translator/HeaderMedia.vue +117 -0
  108. package/app/components/Whatsapp/Translator/Index.vue +159 -0
  109. package/app/composables/composerContext.ts +35 -0
  110. package/app/composables/useAutoTranslate.ts +242 -0
  111. package/app/composables/useAutoTranslateEmail.ts +294 -0
  112. package/app/composables/useChangeTracking.ts +212 -0
  113. package/app/composables/useChangeTrackingEmail.ts +227 -0
  114. package/app/composables/useChannels.ts +36 -0
  115. package/app/composables/useEmailBlockContext.ts +43 -0
  116. package/app/composables/useExitConfirmation.ts +39 -0
  117. package/app/composables/useGoBack.ts +15 -0
  118. package/app/composables/useIncompleteTranslations.ts +55 -0
  119. package/app/composables/useIncompleteTranslationsEmail.ts +56 -0
  120. package/app/composables/useMediaLimits.ts +42 -0
  121. package/app/composables/useTemplateFormValidation.ts +109 -0
  122. package/app/composables/useTemplateOperations.ts +147 -0
  123. package/app/composables/useTracking.ts +39 -0
  124. package/app/composables/useTranslationFilters.ts +176 -0
  125. package/app/composables/useTranslationFiltersEmail.ts +244 -0
  126. package/app/composables/useWhatsappTemplateBehaviors.ts +58 -0
  127. package/app/middleware/channel-guard.ts +13 -0
  128. package/app/pages/brands.vue +195 -0
  129. package/app/pages/images.vue +141 -0
  130. package/app/pages/index.vue +198 -0
  131. package/app/pages/preview/[id].vue +47 -0
  132. package/app/pages/template/email/[id].vue +596 -0
  133. package/app/pages/template/email/create.vue +562 -0
  134. package/app/pages/template/whatsapp/[id].vue +561 -0
  135. package/app/pages/template/whatsapp/create.vue +468 -0
  136. package/app/plugins/admin-shortcut.client.ts +7 -0
  137. package/app/plugins/tracking.client.ts +12 -0
  138. package/app/queries/ai.ts +20 -0
  139. package/app/queries/brands.ts +75 -0
  140. package/app/queries/compilation.ts +37 -0
  141. package/app/queries/dynamicValues.ts +13 -0
  142. package/app/queries/index.ts +9 -0
  143. package/app/queries/languages.ts +10 -0
  144. package/app/queries/media.ts +27 -0
  145. package/app/queries/messageGroups.ts +112 -0
  146. package/app/queries/messages.ts +42 -0
  147. package/app/queries/meta.ts +13 -0
  148. package/app/stores/adminStore.ts +50 -0
  149. package/app/stores/appContext.ts +99 -0
  150. package/app/stores/emailEditor.ts +220 -0
  151. package/app/stores/main.ts +3 -0
  152. package/app/stores/metaSubmission.ts +116 -0
  153. package/app/types/app-config.d.ts +14 -0
  154. package/app/types/hooks.d.ts +9 -0
  155. package/app/types/tracking.ts +148 -0
  156. package/app/utils/locale.ts +17 -0
  157. package/app/utils/notifyParent.ts +7 -0
  158. package/app/utils/resolveDefaultLanguage.ts +14 -0
  159. package/app/utils/socialDefaults.ts +42 -0
  160. package/app/utils/testIds.const.ts +728 -0
  161. package/app.config.ts +5 -0
  162. package/i18n/check-translations.ts +56 -0
  163. package/i18n/locales/de.ts +834 -0
  164. package/i18n/locales/en.ts +826 -0
  165. package/i18n/locales/es.ts +833 -0
  166. package/i18n/locales/fr.ts +837 -0
  167. package/i18n/locales/it.ts +831 -0
  168. package/nuxt.config.ts +65 -0
  169. 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
+ }