@@ -11,9 +11,10 @@ import {
11
11
PluginObj ,
12
12
parseSync ,
13
13
transformAsync ,
14
- traverse ,
14
+ transformFromAstSync ,
15
15
types ,
16
16
} from '@babel/core' ;
17
+ import templateBuilder from '@babel/template' ;
17
18
import { createHash } from 'crypto' ;
18
19
import * as fs from 'fs' ;
19
20
import * as path from 'path' ;
@@ -490,6 +491,59 @@ function createReplacePlugin(replacements: [string, string][]): PluginObj {
490
491
} ;
491
492
}
492
493
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
+
493
547
export interface InlineOptions {
494
548
filename : string ;
495
549
code : string ;
@@ -500,13 +554,6 @@ export interface InlineOptions {
500
554
setLocale ?: boolean ;
501
555
}
502
556
503
- interface LocalizePosition {
504
- start : number ;
505
- end : number ;
506
- messageParts : TemplateStringsArray ;
507
- expressions : types . Expression [ ] ;
508
- }
509
-
510
557
const localizeName = '$localize' ;
511
558
512
559
export async function inlineLocales ( options : InlineOptions ) {
@@ -522,88 +569,91 @@ export async function inlineLocales(options: InlineOptions) {
522
569
return inlineCopyOnly ( options ) ;
523
570
}
524
571
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
+ }
535
590
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 } "` ) ;
539
593
}
540
594
541
- // tslint:disable-next-line: no-any
542
- let content = new MagicString ( options . code , { filename : options . filename } as any ) ;
595
+ const diagnostics = [ ] ;
543
596
const inputMap = options . map && ( JSON . parse ( options . map ) as RawSourceMap ) ;
544
- let contentClone ;
545
597
for ( const locale of i18n . inlineLocales ) {
546
598
const isSourceLocale = locale === i18n . sourceLocale ;
547
599
// tslint:disable-next-line: no-any
548
600
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 ;
564
602
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
-
569
603
// 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 ;
571
605
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 ) ;
575
607
}
576
608
}
577
609
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
+
579
635
const outputPath = path . join (
580
636
options . outputPath ,
581
637
i18n . flatOutput ? '' : locale ,
582
638
options . filename ,
583
639
) ;
584
- fs . writeFileSync ( outputPath , output ) ;
640
+ fs . writeFileSync ( outputPath , transformResult . code ) ;
585
641
586
- if ( inputMap ) {
587
- const contentMap = content . generateMap ( ) ;
642
+ if ( inputMap && transformResult . map ) {
588
643
const outputMap = mergeSourceMaps (
589
644
options . code ,
590
645
inputMap ,
591
- output ,
592
- contentMap ,
646
+ transformResult . code ,
647
+ transformResult . map ,
593
648
options . filename ,
594
649
options . code . length > FAST_SOURCEMAP_THRESHOLD ,
595
650
) ;
596
651
597
652
fs . writeFileSync ( outputPath + '.map' , JSON . stringify ( outputMap ) ) ;
598
653
}
599
-
600
- if ( contentClone ) {
601
- content = contentClone ;
602
- contentClone = undefined ;
603
- }
604
654
}
605
655
606
- return { file : options . filename , diagnostics : diagnostics . messages , count : positions . length } ;
656
+ return { file : options . filename , diagnostics } ;
607
657
}
608
658
609
659
function inlineCopyOnly ( options : InlineOptions ) {
@@ -626,85 +676,6 @@ function inlineCopyOnly(options: InlineOptions) {
626
676
return { file : options . filename , diagnostics : [ ] , count : 0 } ;
627
677
}
628
678
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
-
708
679
async function loadLocaleData ( path : string , optimize : boolean ) : Promise < string > {
709
680
// The path is validated during option processing before the build starts
710
681
const content = fs . readFileSync ( path , 'utf8' ) ;
0 commit comments