@@ -11,9 +11,10 @@ import {
1111 PluginObj ,
1212 parseSync ,
1313 transformAsync ,
14- traverse ,
14+ transformFromAstSync ,
1515 types ,
1616} from '@babel/core' ;
17+ import templateBuilder from '@babel/template' ;
1718import { createHash } from 'crypto' ;
1819import * as fs from 'fs' ;
1920import * as path from 'path' ;
@@ -490,6 +491,59 @@ function createReplacePlugin(replacements: [string, string][]): PluginObj {
490491 } ;
491492}
492493
494+ async function createI18nPlugins (
495+ locale : string ,
496+ translation : unknown | undefined ,
497+ missingTranslation : 'error' | 'warning' | 'ignore' ,
498+ localeDataContent : string | undefined ,
499+ ) {
500+ const plugins = [ ] ;
501+ // tslint:disable-next-line: no-implicit-dependencies
502+ const localizeDiag = await import ( '@angular/localize/src/tools/src/diagnostics' ) ;
503+
504+ const diagnostics = new localizeDiag . Diagnostics ( ) ;
505+
506+ const es2015 = await import (
507+ // tslint:disable-next-line: trailing-comma no-implicit-dependencies
508+ '@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin'
509+ ) ;
510+ plugins . push (
511+ // tslint:disable-next-line: no-any
512+ es2015 . makeEs2015TranslatePlugin ( diagnostics , ( translation || { } ) as any , {
513+ missingTranslation : translation === undefined ? 'ignore' : missingTranslation ,
514+ } ) ,
515+ ) ;
516+
517+ const es5 = await import (
518+ // tslint:disable-next-line: trailing-comma no-implicit-dependencies
519+ '@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin'
520+ ) ;
521+ plugins . push (
522+ // tslint:disable-next-line: no-any
523+ es5 . makeEs5TranslatePlugin ( diagnostics , ( translation || { } ) as any , {
524+ missingTranslation : translation === undefined ? 'ignore' : missingTranslation ,
525+ } ) ,
526+ ) ;
527+
528+ const inlineLocale = await import (
529+ // tslint:disable-next-line: trailing-comma no-implicit-dependencies
530+ '@angular/localize/src/tools/src/translate/source_files/locale_plugin'
531+ ) ;
532+ plugins . push ( inlineLocale . makeLocalePlugin ( locale ) ) ;
533+
534+ if ( localeDataContent ) {
535+ plugins . push ( {
536+ visitor : {
537+ Program ( path : NodePath < types . Program > ) {
538+ path . unshiftContainer ( 'body' , templateBuilder . ast ( localeDataContent ) ) ;
539+ } ,
540+ } ,
541+ } ) ;
542+ }
543+
544+ return { diagnostics, plugins } ;
545+ }
546+
493547export interface InlineOptions {
494548 filename : string ;
495549 code : string ;
@@ -500,13 +554,6 @@ export interface InlineOptions {
500554 setLocale ?: boolean ;
501555}
502556
503- interface LocalizePosition {
504- start : number ;
505- end : number ;
506- messageParts : TemplateStringsArray ;
507- expressions : types . Expression [ ] ;
508- }
509-
510557const localizeName = '$localize' ;
511558
512559export async function inlineLocales ( options : InlineOptions ) {
@@ -522,88 +569,91 @@ export async function inlineLocales(options: InlineOptions) {
522569 return inlineCopyOnly ( options ) ;
523570 }
524571
525- const { default : MagicString } = await import ( 'magic-string' ) ;
526- const { default : generate } = await import ( '@babel/generator' ) ;
527- const utils = await import (
528- // tslint:disable-next-line: trailing-comma no-implicit-dependencies
529- '@angular/localize/src/tools/src/translate/source_files/source_file_utils'
530- ) ;
531- // tslint:disable-next-line: no-implicit-dependencies
532- const localizeDiag = await import ( '@angular/localize/src/tools/src/diagnostics' ) ;
533-
534- const diagnostics = new localizeDiag . Diagnostics ( ) ;
572+ let ast : ParseResult | undefined | null ;
573+ try {
574+ ast = parseSync ( options . code , {
575+ babelrc : false ,
576+ configFile : false ,
577+ sourceType : 'script' ,
578+ filename : options . filename ,
579+ } ) ;
580+ } catch ( error ) {
581+ if ( error . message ) {
582+ // Make the error more readable.
583+ // Same errors will contain the full content of the file as the error message
584+ // Which makes it hard to find the actual error message.
585+ const index = error . message . indexOf ( ')\n' ) ;
586+ const msg = index !== - 1 ? error . message . substr ( 0 , index + 1 ) : error . message ;
587+ throw new Error ( `${ msg } \nAn error occurred inlining file "${ options . filename } "` ) ;
588+ }
589+ }
535590
536- const positions = findLocalizePositions ( options , utils ) ;
537- if ( positions . length === 0 && ! options . setLocale ) {
538- return inlineCopyOnly ( options ) ;
591+ if ( ! ast ) {
592+ throw new Error ( `Unknown error occurred inlining file "${ options . filename } "` ) ;
539593 }
540594
541- // tslint:disable-next-line: no-any
542- let content = new MagicString ( options . code , { filename : options . filename } as any ) ;
595+ const diagnostics = [ ] ;
543596 const inputMap = options . map && ( JSON . parse ( options . map ) as RawSourceMap ) ;
544- let contentClone ;
545597 for ( const locale of i18n . inlineLocales ) {
546598 const isSourceLocale = locale === i18n . sourceLocale ;
547599 // tslint:disable-next-line: no-any
548600 const translations : any = isSourceLocale ? { } : i18n . locales [ locale ] . translation || { } ;
549- for ( const position of positions ) {
550- const translated = utils . translate (
551- diagnostics ,
552- translations ,
553- position . messageParts ,
554- position . expressions ,
555- isSourceLocale ? 'ignore' : options . missingTranslation || 'warning' ,
556- ) ;
557-
558- const expression = utils . buildLocalizeReplacement ( translated [ 0 ] , translated [ 1 ] ) ;
559- const { code } = generate ( expression ) ;
560-
561- content . overwrite ( position . start , position . end , code ) ;
562- }
563-
601+ let localeDataContent ;
564602 if ( options . setLocale ) {
565- const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${ locale } "});` ;
566- contentClone = content . clone ( ) ;
567- content . prepend ( setLocaleText ) ;
568-
569603 // If locale data is provided, load it and prepend to file
570- const localeDataPath = i18n . locales [ locale ] && i18n . locales [ locale ] . dataPath ;
604+ const localeDataPath = i18n . locales [ locale ] ? .dataPath ;
571605 if ( localeDataPath ) {
572- const localDataContent = await loadLocaleData ( localeDataPath , true ) ;
573- // The semicolon ensures that there is no syntax error between statements
574- content . prepend ( localDataContent + ';' ) ;
606+ localeDataContent = await loadLocaleData ( localeDataPath , true ) ;
575607 }
576608 }
577609
578- const output = content . toString ( ) ;
610+ const { diagnostics : localeDiagnostics , plugins } = await createI18nPlugins (
611+ locale ,
612+ translations ,
613+ isSourceLocale ? 'ignore' : options . missingTranslation || 'warning' ,
614+ localeDataContent ,
615+ ) ;
616+ const transformResult = await transformFromAstSync ( ast , options . code , {
617+ filename : options . filename ,
618+ // using false ensures that babel will NOT search and process sourcemap comments (large memory usage)
619+ // The types do not include the false option even though it is valid
620+ // tslint:disable-next-line: no-any
621+ inputSourceMap : false as any ,
622+ babelrc : false ,
623+ configFile : false ,
624+ plugins,
625+ compact : ! shouldBeautify ,
626+ sourceMaps : ! ! inputMap ,
627+ } ) ;
628+
629+ diagnostics . push ( ...localeDiagnostics . messages ) ;
630+
631+ if ( ! transformResult || ! transformResult . code ) {
632+ throw new Error ( `Unknown error occurred processing bundle for "${ options . filename } ".` ) ;
633+ }
634+
579635 const outputPath = path . join (
580636 options . outputPath ,
581637 i18n . flatOutput ? '' : locale ,
582638 options . filename ,
583639 ) ;
584- fs . writeFileSync ( outputPath , output ) ;
640+ fs . writeFileSync ( outputPath , transformResult . code ) ;
585641
586- if ( inputMap ) {
587- const contentMap = content . generateMap ( ) ;
642+ if ( inputMap && transformResult . map ) {
588643 const outputMap = mergeSourceMaps (
589644 options . code ,
590645 inputMap ,
591- output ,
592- contentMap ,
646+ transformResult . code ,
647+ transformResult . map ,
593648 options . filename ,
594649 options . code . length > FAST_SOURCEMAP_THRESHOLD ,
595650 ) ;
596651
597652 fs . writeFileSync ( outputPath + '.map' , JSON . stringify ( outputMap ) ) ;
598653 }
599-
600- if ( contentClone ) {
601- content = contentClone ;
602- contentClone = undefined ;
603- }
604654 }
605655
606- return { file : options . filename , diagnostics : diagnostics . messages , count : positions . length } ;
656+ return { file : options . filename , diagnostics } ;
607657}
608658
609659function inlineCopyOnly ( options : InlineOptions ) {
@@ -626,85 +676,6 @@ function inlineCopyOnly(options: InlineOptions) {
626676 return { file : options . filename , diagnostics : [ ] , count : 0 } ;
627677}
628678
629- function findLocalizePositions (
630- options : InlineOptions ,
631- // tslint:disable-next-line: no-implicit-dependencies
632- utils : typeof import ( '@angular/localize/src/tools/src/translate/source_files/source_file_utils' ) ,
633- ) : LocalizePosition [ ] {
634- let ast : ParseResult | undefined | null ;
635-
636- try {
637- ast = parseSync ( options . code , {
638- babelrc : false ,
639- configFile : false ,
640- sourceType : 'script' ,
641- filename : options . filename ,
642- } ) ;
643- } catch ( error ) {
644- if ( error . message ) {
645- // Make the error more readable.
646- // Same errors will contain the full content of the file as the error message
647- // Which makes it hard to find the actual error message.
648- const index = error . message . indexOf ( ')\n' ) ;
649- const msg = index !== - 1 ? error . message . substr ( 0 , index + 1 ) : error . message ;
650- throw new Error ( `${ msg } \nAn error occurred inlining file "${ options . filename } "` ) ;
651- }
652- }
653-
654- if ( ! ast ) {
655- throw new Error ( `Unknown error occurred inlining file "${ options . filename } "` ) ;
656- }
657-
658- const positions : LocalizePosition [ ] = [ ] ;
659- if ( options . es5 ) {
660- traverse ( ast , {
661- CallExpression ( path : NodePath < types . CallExpression > ) {
662- const callee = path . get ( 'callee' ) ;
663- if (
664- callee . isIdentifier ( ) &&
665- callee . node . name === localizeName &&
666- utils . isGlobalIdentifier ( callee )
667- ) {
668- const messageParts = utils . unwrapMessagePartsFromLocalizeCall ( path ) ;
669- const expressions = utils . unwrapSubstitutionsFromLocalizeCall ( path . node ) ;
670- positions . push ( {
671- // tslint:disable-next-line: no-non-null-assertion
672- start : path . node . start ! ,
673- // tslint:disable-next-line: no-non-null-assertion
674- end : path . node . end ! ,
675- messageParts,
676- expressions,
677- } ) ;
678- }
679- } ,
680- } ) ;
681- } else {
682- const traverseFast = ( ( types as unknown ) as {
683- traverseFast : ( node : types . Node , enter : ( node : types . Node ) => void ) => void ;
684- } ) . traverseFast ;
685-
686- traverseFast ( ast , node => {
687- if (
688- node . type === 'TaggedTemplateExpression' &&
689- types . isIdentifier ( node . tag ) &&
690- node . tag . name === localizeName
691- ) {
692- const messageParts = utils . unwrapMessagePartsFromTemplateLiteral ( node . quasi . quasis ) ;
693- positions . push ( {
694- // tslint:disable-next-line: no-non-null-assertion
695- start : node . start ! ,
696- // tslint:disable-next-line: no-non-null-assertion
697- end : node . end ! ,
698- messageParts,
699- expressions : node . quasi . expressions ,
700- } ) ;
701- }
702- } ) ;
703- }
704-
705- return positions ;
706- }
707-
708679async function loadLocaleData ( path : string , optimize : boolean ) : Promise < string > {
709680 // The path is validated during option processing before the build starts
710681 const content = fs . readFileSync ( path , 'utf8' ) ;
0 commit comments