@@ -37,6 +37,10 @@ import type { MJMLComponentType } from '../email_builder/types'
3737import MjmlCodeEditor , { STARTER_TEMPLATE } from '../email_builder/MjmlCodeEditor'
3838import type { MjmlCodeEditorRef } from '../email_builder/MjmlCodeEditor'
3939import type { MjmlCompileError } from '../../services/api/template'
40+ import { SUPPORTED_LANGUAGES } from '../../lib/languages'
41+ import TemplateTranslationsTab from './TemplateTranslationsTab'
42+ import type { TranslationEditorState } from './TemplateTranslationsTab'
43+ import type { TemplateTranslation } from '../../services/api/template'
4044
4145/**
4246 * Validates liquid template tags in a string to ensure they are properly closed
@@ -189,6 +193,12 @@ export function CreateTemplateDrawer({
189193 if ( fromTemplate ?. email ?. mjml_source ) return fromTemplate . email . mjml_source
190194 return STARTER_TEMPLATE
191195 } )
196+ const [ translationsState , setTranslationsState ] = useState < Record < string , TranslationEditorState > > ( { } )
197+
198+ const translationLanguages = ( workspace . settings . languages || [ ] ) . filter (
199+ ( l ) => l !== workspace . settings . default_language
200+ )
201+ const showTranslationsTab = translationLanguages . length > 0
192202
193203 // Refs for tour targets
194204 const treePanelRef = useRef < HTMLDivElement > ( null )
@@ -294,6 +304,25 @@ export function CreateTemplateDrawer({
294304 }
295305 } , [ workspace . settings . custom_endpoint_url , workspace . id ] )
296306
307+ const loadTranslations = ( translations ?: Record < string , TemplateTranslation > ) => {
308+ if ( ! translations ) return
309+ const loaded : Record < string , TranslationEditorState > = { }
310+ for ( const [ lang , trans ] of Object . entries ( translations ) ) {
311+ loaded [ lang ] = {
312+ enabled : true ,
313+ subject : trans . email ?. subject || '' ,
314+ subjectPreview : trans . email ?. subject_preview || '' ,
315+ visualEditorTree : trans . email ?. visual_editor_tree
316+ ? ( typeof trans . email . visual_editor_tree === 'object'
317+ ? ( JSON . parse ( JSON . stringify ( trans . email . visual_editor_tree ) ) as EmailBlock )
318+ : ( JSON . parse ( trans . email . visual_editor_tree as unknown as string ) as EmailBlock ) )
319+ : undefined ,
320+ mjmlSource : trans . email ?. mjml_source || undefined
321+ }
322+ }
323+ setTranslationsState ( loaded )
324+ }
325+
297326 const showDrawer = ( ) => {
298327 if ( template ) {
299328 // Set editor mode from existing template
@@ -315,6 +344,7 @@ export function CreateTemplateDrawer({
315344 } ,
316345 test_data : template . test_data || defaultTestData
317346 } )
347+ loadTranslations ( template . translations )
318348 } else if ( fromTemplate ) {
319349 // Clone template functionality - lock to source template's editor mode
320350 setEditorMode ( fromTemplate . email ?. editor_mode === 'code' ? 'code' : 'visual' )
@@ -337,6 +367,8 @@ export function CreateTemplateDrawer({
337367 test_data : fromTemplate . test_data || defaultTestData
338368 } )
339369
370+ loadTranslations ( fromTemplate . translations )
371+
340372 // Update the visual editor tree
341373 if ( fromTemplate . email ?. visual_editor_tree ) {
342374 if ( typeof fromTemplate . email . visual_editor_tree === 'object' ) {
@@ -366,6 +398,7 @@ export function CreateTemplateDrawer({
366398 setTab ( 'settings' )
367399 setEditorMode ( 'visual' )
368400 setMjmlSource ( STARTER_TEMPLATE )
401+ setTranslationsState ( { } )
369402 if ( onClose ) {
370403 onClose ( )
371404 }
@@ -429,7 +462,11 @@ export function CreateTemplateDrawer({
429462 }
430463
431464 const goNext = ( ) => {
432- setTab ( 'template' )
465+ if ( tab === 'settings' ) {
466+ setTab ( 'template' )
467+ } else if ( tab === 'template' && showTranslationsTab ) {
468+ setTab ( 'translations' )
469+ }
433470 }
434471
435472 return (
@@ -473,8 +510,28 @@ export function CreateTemplateDrawer({
473510 { t `Previous` }
474511 </ Button >
475512 ) }
476-
477- { tab === 'template' && (
513+ { tab === 'template' && ! showTranslationsTab && (
514+ < Button
515+ loading = { loading || createTemplateMutation . isPending }
516+ onClick = { ( ) => {
517+ form . submit ( )
518+ } }
519+ type = "primary"
520+ >
521+ { t `Save` }
522+ </ Button >
523+ ) }
524+ { tab === 'template' && showTranslationsTab && (
525+ < Button type = "primary" onClick = { goNext } >
526+ { t `Next` }
527+ </ Button >
528+ ) }
529+ { tab === 'translations' && (
530+ < Button type = "primary" ghost onClick = { ( ) => setTab ( 'template' ) } >
531+ { t `Previous` }
532+ </ Button >
533+ ) }
534+ { tab === 'translations' && (
478535 < Button
479536 loading = { loading || createTemplateMutation . isPending }
480537 onClick = { ( ) => {
@@ -503,6 +560,41 @@ export function CreateTemplateDrawer({
503560 values . email . editor_mode = 'visual'
504561 values . email . visual_editor_tree = visualEditorTree
505562 }
563+
564+ // Validate and build translations from state
565+ if ( showTranslationsTab ) {
566+ // Check enabled translations have required fields
567+ for ( const [ lang , state ] of Object . entries ( translationsState ) ) {
568+ if ( ! state . enabled ) continue
569+ if ( ! state . subject || ! state . subjectPreview ) {
570+ const langName = SUPPORTED_LANGUAGES [ lang ] || lang
571+ message . error ( t `${ langName } translation is missing required fields (subject and preview)` )
572+ setTab ( 'translations' )
573+ setLoading ( false )
574+ return
575+ }
576+ }
577+
578+ const translations : Record < string , TemplateTranslation > = { }
579+ for ( const [ lang , state ] of Object . entries ( translationsState ) ) {
580+ if ( ! state . enabled ) continue
581+ const emailTranslation : Record < string , unknown > = {
582+ subject : state . subject ,
583+ subject_preview : state . subjectPreview || ''
584+ }
585+ if ( editorMode === 'code' ) {
586+ emailTranslation . editor_mode = 'code'
587+ emailTranslation . mjml_source = state . mjmlSource || ''
588+ } else {
589+ emailTranslation . editor_mode = 'visual'
590+ emailTranslation . visual_editor_tree = state . visualEditorTree || visualEditorTree
591+ }
592+ translations [ lang ] = { email : emailTranslation as TemplateTranslation [ 'email' ] }
593+ }
594+ // Always send translations (even empty) so disabling all clears them on the server
595+ values . translations = translations
596+ }
597+
506598 createTemplateMutation . mutate ( values )
507599 } }
508600 onFinishFailed = { ( info ) => {
@@ -552,8 +644,18 @@ export function CreateTemplateDrawer({
552644 } ,
553645 {
554646 key : 'template' ,
555- label : t `2. Template`
556- }
647+ label : showTranslationsTab
648+ ? t `2. Template (${ workspace . settings . default_language } )`
649+ : t `2. Template`
650+ } ,
651+ ...( showTranslationsTab
652+ ? [
653+ {
654+ key : 'translations' ,
655+ label : t `3. Translations`
656+ }
657+ ]
658+ : [ ] )
557659 ] }
558660 />
559661 </ div >
@@ -653,21 +755,16 @@ export function CreateTemplateDrawer({
653755 </ Col >
654756 < Col span = { 6 } >
655757 < Form . Item label = { t `Editor mode` } >
656- { template || fromTemplate ? (
657- < Tag color = { editorMode === 'code' ? 'geekblue' : 'default' } >
658- { editorMode === 'code' ? t `Code (MJML)` : t `Visual` }
659- </ Tag >
660- ) : (
661- < Radio . Group
662- value = { editorMode }
663- onChange = { ( e ) => setEditorMode ( e . target . value as 'visual' | 'code' ) }
664- optionType = "button"
665- options = { [
666- { label : t `Visual` , value : 'visual' } ,
667- { label : t `Code (MJML)` , value : 'code' }
668- ] }
669- />
670- ) }
758+ < Radio . Group
759+ value = { editorMode }
760+ onChange = { ( e ) => setEditorMode ( e . target . value as 'visual' | 'code' ) }
761+ optionType = "button"
762+ disabled = { ! ! ( template || fromTemplate ) }
763+ options = { [
764+ { label : t `Visual` , value : 'visual' } ,
765+ { label : t `Code (MJML)` , value : 'code' }
766+ ] }
767+ />
671768 </ Form . Item >
672769 </ Col >
673770 </ Row >
@@ -677,7 +774,7 @@ export function CreateTemplateDrawer({
677774 < Col span = { 12 } >
678775 < Form . Item
679776 name = { [ 'email' , 'subject' ] }
680- label = { t `Email subject` }
777+ label = { showTranslationsTab ? t `Email subject ( ${ workspace . settings . default_language } )` : t `Email subject` }
681778 rules = { [
682779 { required : true , type : 'string' } ,
683780 {
@@ -695,7 +792,7 @@ export function CreateTemplateDrawer({
695792 </ Form . Item >
696793 < Form . Item
697794 name = { [ 'email' , 'subject_preview' ] }
698- label = { t `Subject preview` }
795+ label = { showTranslationsTab ? t `Subject preview ( ${ workspace . settings . default_language } )` : t `Subject preview` }
699796 rules = { [
700797 { required : true , type : 'string' } ,
701798 {
@@ -712,27 +809,32 @@ export function CreateTemplateDrawer({
712809 < Input placeholder = { t `Templating markup allowed` } />
713810 </ Form . Item >
714811
715- < Form . Item
716- name = { [ 'email' , 'reply_to' ] }
717- label = { t `Reply to` }
718- rules = { [ { required : false , type : 'email' } ] }
719- >
720- < Input />
721- </ Form . Item >
722-
723- < Form . Item
724- name = { [ 'email' , 'sender_id' ] }
725- label = { categoryValue === 'marketing' ? t `Custom sender (marketing email provider)` : t `Custom sender (transactional email provider)` }
726- rules = { [ { required : false , type : 'string' } ] }
727- >
728- < Select
729- options = { emailProvider ?. email_provider ?. senders . map ( ( sender ) => ( {
730- value : sender . id ,
731- label : `${ sender . name } <${ sender . email } >`
732- } ) ) }
733- allowClear = { true }
734- />
735- </ Form . Item >
812+ < Row gutter = { 24 } >
813+ < Col span = { 12 } >
814+ < Form . Item
815+ name = { [ 'email' , 'reply_to' ] }
816+ label = { t `Reply to` }
817+ rules = { [ { required : false , type : 'email' } ] }
818+ >
819+ < Input />
820+ </ Form . Item >
821+ </ Col >
822+ < Col span = { 12 } >
823+ < Form . Item
824+ name = { [ 'email' , 'sender_id' ] }
825+ label = { categoryValue === 'marketing' ? t `Custom sender (marketing email provider)` : t `Custom sender (transactional email provider)` }
826+ rules = { [ { required : false , type : 'string' } ] }
827+ >
828+ < Select
829+ options = { emailProvider ?. email_provider ?. senders . map ( ( sender ) => ( {
830+ value : sender . id ,
831+ label : `${ sender . name } <${ sender . email } >`
832+ } ) ) }
833+ allowClear = { true }
834+ />
835+ </ Form . Item >
836+ </ Col >
837+ </ Row >
736838 </ Col >
737839 < Col span = { 12 } >
738840 < div className = "flex justify-center" >
@@ -900,6 +1002,25 @@ export function CreateTemplateDrawer({
9001002 } }
9011003 </ Form . Item >
9021004 </ div >
1005+
1006+ { showTranslationsTab && (
1007+ < div style = { { display : tab === 'translations' ? 'block' : 'none' } } >
1008+ < TemplateTranslationsTab
1009+ workspace = { workspace }
1010+ editorMode = { editorMode }
1011+ translationsState = { translationsState }
1012+ onTranslationsStateChange = { setTranslationsState }
1013+ defaultSubject = { emailSubject }
1014+ defaultSubjectPreview = { emailPreview }
1015+ defaultVisualEditorTree = { visualEditorTree }
1016+ defaultMjmlSource = { mjmlSource }
1017+ testData = { form . getFieldValue ( 'test_data' ) }
1018+ onTestDataChange = { ( newTestData ) => form . setFieldsValue ( { test_data : newTestData } ) }
1019+ savedBlocks = { workspace . settings . template_blocks || [ ] }
1020+ onSaveBlock = { handleSaveBlock }
1021+ />
1022+ </ div >
1023+ ) }
9031024 </ div >
9041025 </ Form >
9051026 < Tour
0 commit comments