@@ -20,6 +20,25 @@ const applicationRoot = resolve(sharedEmailsRoot, "..", "..");
2020const SYSTEMS = [ "account" , "main" ] as const ;
2121const LOCALES = Object . keys ( i18nConfig ) ;
2222
23+ // Sample values used to substitute hand-written Scriban placeholders ({{ Name }}) that cannot go
24+ // through the JSX helpers — typically those that live inside href attributes or Lingui <Trans>
25+ // strings (see emails.md "Exception — HTML attributes and Trans strings"). Applied only to the
26+ // preview render output, never to the production .html/.txt artifacts.
27+ const PREVIEW_PLACEHOLDER_VALUES : Record < string , string > = {
28+ SignupUrl : "https://app.platformplatform.net/signup" ,
29+ LoginUrl : "https://app.platformplatform.net/login" ,
30+ TenantName : "Acme Corp"
31+ } ;
32+
33+ function substitutePreviewPlaceholders ( input : string ) : string {
34+ let result = input ;
35+ for ( const [ name , value ] of Object . entries ( PREVIEW_PLACEHOLDER_VALUES ) ) {
36+ // Scriban accepts both `{{Name}}` and `{{ Name }}` — replace both forms.
37+ result = result . replaceAll ( `{{${ name } }}` , value ) . replaceAll ( `{{ ${ name } }}` , value ) ;
38+ }
39+ return result ;
40+ }
41+
2342type BuildTarget = {
2443 label : string ;
2544 // Folder containing lingui.config.ts and the translations/ directory.
@@ -111,28 +130,42 @@ async function renderTemplate(target: BuildTarget, templatePath: string, name: s
111130
112131 const wrapped = createElement ( I18nProvider , { i18n } , createElement ( Template , { locale } ) ) ;
113132
114- // eslint-disable-next-line no-await-in-loop
115- const html = await render ( wrapped , { pretty : false } ) ;
116- // eslint-disable-next-line no-await-in-loop
117- const text = await render ( wrapped , {
118- plainText : true ,
119- // The default html-to-text formatter uppercases <h1>/<h2> content; that mangles Scriban
120- // expressions like {{ firstName }} into {{ FIRSTNAME }}, breaking runtime substitution. Pin
121- // every heading level to its original casing so the .txt output preserves the template variables.
122- htmlToTextOptions : {
123- selectors : [
124- { selector : "h1" , options : { uppercase : false } } ,
125- { selector : "h2" , options : { uppercase : false } } ,
126- { selector : "h3" , options : { uppercase : false } } ,
127- { selector : "h4" , options : { uppercase : false } }
128- ]
129- }
130- } ) ;
131-
132- writeFileSync ( join ( target . distDir , `${ name } .${ locale } .html` ) , html , "utf8" ) ;
133- writeFileSync ( join ( target . distDir , `${ name } .${ locale } .txt` ) , text , "utf8" ) ;
134-
135- console . log ( `[emails] wrote ${ name } .${ locale } .{html,txt}` ) ;
133+ // Two render passes per template/locale:
134+ // "build" → helpers emit Scriban placeholders ({{ Var }}). The backend renders these at
135+ // runtime against the real model and sends the final email.
136+ // "preview" → helpers substitute their `sample` props with realistic dummy values. The in-app
137+ // preview page (BackOffice → Components → Emails) iframes these files so designers
138+ // can visually inspect the templates without sending live emails.
139+ // Both outputs share the same .po catalogs, so translations only need to be entered once.
140+ for ( const mode of [ "build" , "preview" ] as const ) {
141+ process . env . EMAIL_RENDER_MODE = mode ;
142+
143+ // eslint-disable-next-line no-await-in-loop
144+ const html = await render ( wrapped , { pretty : false } ) ;
145+ // eslint-disable-next-line no-await-in-loop
146+ const text = await render ( wrapped , {
147+ plainText : true ,
148+ // The default html-to-text formatter uppercases <h1>/<h2> content; that mangles Scriban
149+ // expressions like {{ firstName }} into {{ FIRSTNAME }}, breaking runtime substitution. Pin
150+ // every heading level to its original casing so the .txt output preserves the template variables.
151+ htmlToTextOptions : {
152+ selectors : [
153+ { selector : "h1" , options : { uppercase : false } } ,
154+ { selector : "h2" , options : { uppercase : false } } ,
155+ { selector : "h3" , options : { uppercase : false } } ,
156+ { selector : "h4" , options : { uppercase : false } }
157+ ]
158+ }
159+ } ) ;
160+
161+ const suffix = mode === "build" ? "" : ".preview" ;
162+ const finalHtml = mode === "preview" ? substitutePreviewPlaceholders ( html ) : html ;
163+ const finalText = mode === "preview" ? substitutePreviewPlaceholders ( text ) : text ;
164+ writeFileSync ( join ( target . distDir , `${ name } .${ locale } ${ suffix } .html` ) , finalHtml , "utf8" ) ;
165+ writeFileSync ( join ( target . distDir , `${ name } .${ locale } ${ suffix } .txt` ) , finalText , "utf8" ) ;
166+
167+ console . log ( `[emails] wrote ${ name } .${ locale } ${ suffix } .{html,txt}` ) ;
168+ }
136169 }
137170}
138171
0 commit comments