@@ -41,9 +41,11 @@ function parseArgs() {
4141 outputFile ?: string ,
4242 force : boolean ,
4343 help : boolean ,
44+ name : string ,
4445 } = {
4546 force : false ,
46- help : false
47+ help : false ,
48+ name : 'generatedTranslation'
4749 }
4850
4951 for ( let i = 0 ; i < args . length ; i ++ ) {
@@ -60,6 +62,11 @@ function parseArgs() {
6062 result . outputFile = args [ ++ i ]
6163 break
6264
65+ case '--name' :
66+ case '-n' :
67+ result . name = args [ ++ i ]
68+ break
69+
6370 case '--force' :
6471 case '-f' :
6572 result . force = true
@@ -84,6 +91,7 @@ Options:
8491 -i, --in <dir> Input directory containing .arb files
8592 -o, --out <file> Output file (e.g. ./i18n/translations.ts)
8693 -f, --force Overwrite output without prompt
94+ -n, --name <name> The name for exported translation within the code
8795 -h, --help Show this help message
8896` )
8997}
@@ -107,6 +115,19 @@ const force = parsed.force
107115
108116const outputDir = path . dirname ( OUTPUT_FILE )
109117
118+ const name = parsed . name
119+ . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' )
120+ . replace ( / ^ [ 0 - 9 ] / , '' )
121+
122+ if ( name . length < 1 || name [ 0 ] . toUpperCase ( ) === name [ 0 ] ) {
123+ console . error ( `The name ${ parsed . name } is invalid. Use [a-z][a-zA-Z0-9_]+` )
124+ process . exit ( 0 )
125+ } else if ( name . length !== parsed . name . length ) {
126+ console . warn ( `The name ${ parsed . name } cannot start with a number.` )
127+ }
128+
129+ console . log ( name )
130+
110131/* ------------------ prompts ------------------ */
111132
112133function askQuestion ( query : string ) : Promise < string > {
@@ -127,6 +148,14 @@ if (!fs.existsSync(outputDir)) {
127148 fs . mkdirSync ( outputDir , { recursive : true } )
128149}
129150
151+ const capitalize = ( s : string ) : string => {
152+ if ( s . length > 0 ) {
153+ return s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 )
154+ }
155+ return s
156+ }
157+
158+
130159const locales = new Set < string > ( )
131160
132161/* ------------------ ARB reader ------------------ */
@@ -209,12 +238,28 @@ function readARBDir(
209238/* ------------------ code generator: values ------------------ */
210239
211240function escapeForTemplateJS ( s : string ) : string {
212- return s . replace ( / ` / g, '\\`' )
241+ return s
242+ . replace ( / \\ / g, `\\\\` )
243+ . replace ( / ` / g, `\\\`` )
244+ . replace ( / \$ / g, `\\$` )
245+ }
246+
247+ type CompileContext = {
248+ numberParam ?: string ,
249+ inNode : boolean ,
250+ indentLevel : number ,
251+ isOnlyText : boolean ,
252+ }
253+
254+ const defaultCompileContext : CompileContext = {
255+ indentLevel : 0 ,
256+ inNode : false ,
257+ isOnlyText : false ,
213258}
214259
215260function compile (
216261 node : ICUASTNode ,
217- context : { numberParam ?: string , inNode : boolean , indentLevel : number } = { indentLevel : 0 , inNode : false }
262+ context : CompileContext = defaultCompileContext
218263) : string [ ] {
219264 const lines : string [ ] = [ ]
220265 let currentLine = ''
@@ -225,12 +270,15 @@ function compile(
225270 }
226271
227272 function flushCurrent ( ) {
228- if ( currentLine ) {
229- if ( context . inNode ) {
273+ if ( currentLine ) {
274+ if ( context . inNode ) {
230275 lines . push ( currentLine )
231276 } else {
232- const prefix = ! isTopLevel ? indent ( ) : '_out += '
233- const nextLine = `${ prefix } \`${ escapeForTemplateJS ( currentLine ) } \``
277+ const prefix =
278+ context . isOnlyText ? '' :
279+ ! isTopLevel ? indent ( )
280+ : '_out += '
281+ const nextLine = `${ prefix } \`${ currentLine } \``
234282 lines . push ( nextLine )
235283 }
236284 }
@@ -239,7 +287,7 @@ function compile(
239287
240288 switch ( node . type ) {
241289 case 'Text' :
242- currentLine += node . value
290+ currentLine += escapeForTemplateJS ( node . value )
243291 break
244292 case 'NumberField' :
245293 if ( context . numberParam ) {
@@ -264,6 +312,10 @@ function compile(
264312 break
265313 }
266314 case 'OptionReplace' : {
315+ if ( context . isOnlyText ) {
316+ currentLine += `{${ node . variableName } , ${ node . operatorName } , {options}}`
317+ break
318+ }
267319 flushCurrent ( )
268320 lines . push ( `${ isTopLevel ? '_out += ' : '' } TranslationGen.resolveSelect(${ node . variableName } , {` )
269321
@@ -277,7 +329,7 @@ function compile(
277329 indentLevel : context . indentLevel + 1 ,
278330 inNode : false ,
279331 } )
280- if ( expr . length === 0 ) continue
332+ if ( expr . length === 0 ) continue
281333 lines . push ( indent ( context . indentLevel + 1 ) + `'${ key } ': ${ expr [ 0 ] . trimStart ( ) } ` , ...expr . slice ( 1 ) )
282334 lines [ lines . length - 1 ] += ','
283335 }
@@ -326,7 +378,10 @@ function generateCode(
326378 ]
327379 str += `${ indent } ${ quotedKey } : ${ functionLines . join ( `\n${ indent } ` ) } ${ comma } \n`
328380 } else if ( entry . type === 'text' ) {
329- str += `${ indent } ${ quotedKey } : \`${ escapeForTemplateJS ( entry . value ) } \`${ comma } \n`
381+ const ast = ICUUtil . parse ( ICUUtil . lex ( entry . value ) )
382+ const compiled = compile ( ast , { ...defaultCompileContext , isOnlyText : true } )
383+ const text = compiled . length === 1 ? compiled [ 0 ] : `\`${ escapeForTemplateJS ( entry . value ) } \``
384+ str += `${ indent } ${ quotedKey } : ${ text } ${ comma } \n`
330385 } else {
331386 // nested object
332387 str + = `${ indent } ${ quotedKey } : {\n`
@@ -387,30 +442,34 @@ async function main(): Promise<void> {
387442 const translationData = readARBDir ( inputDir )
388443
389444 let output = `// AUTO-GENERATED. DO NOT EDIT.\n`
445+ output += '/* eslint-disable @stylistic/quote-props */\n'
446+ output += '/* eslint-disable no-useless-escape */\n'
447+ output += '/* eslint-disable @typescript-eslint/no-unused-vars */\n'
448+
390449 output += `import type { Translation } from '@helpwave/internationalization'\n`
391450 output += `import { TranslationGen } from '@helpwave/internationalization'\n\n`
392451
393- output += '/* eslint-disable @stylistic/quote-props */\n'
394-
395- output += `export const supportedLocales = [${ [
452+ const localesVarName = ` ${ name } Locales`
453+ const localesTypeName = ` ${ capitalize ( name ) } Locales`
454+ output += `export const ${ localesVarName } = [${ [
396455 ...locales . values ( )
397456 ]
398457 . map ( v => `'${ v } '` )
399458 . join ( ', ' ) } ] as const\n\n`
400459
401- output += `export type SupportedLocale = typeof supportedLocales [number]\n\n`
460+ output += `export type ${ localesTypeName } = typeof ${ localesVarName } [number]\n\n`
402461
462+ const typeName = `${ capitalize ( name ) } Entries`
403463 const generatedTyping = generateType ( translationData )
404- output += `export type GeneratedTranslationEntries = {\n${ generatedTyping } }\n\n`
464+ output += `export type ${ typeName } = {\n${ generatedTyping } }\n\n`
405465
406466 const value : Record < string , TranslationEntry > = { }
407467 for ( const locale of locales ) {
408468 value [ locale ] = { type : 'nested' , value : translationData [ locale ] }
409469 }
410470
411-
412471 const generatedTranslation = generateCode ( value )
413- output += `export const generatedTranslations : Translation<SupportedLocale , Partial<GeneratedTranslationEntries >> = {\n${ generatedTranslation } }\n\n`
472+ output += `export const ${ name } : Translation<${ localesTypeName } , Partial<${ typeName } >> = {\n${ generatedTranslation } }\n\n`
414473
415474 if ( fs . existsSync ( OUTPUT_FILE ) && ! force ) {
416475 const answer = await askQuestion (
0 commit comments