@@ -25,16 +25,24 @@ import {NO_CHANGE} from './tokens';
25
25
import { addAllToArray , getNativeByIndex , getNativeByTNode , getTNode , isLContainer , renderStringify } from './util' ;
26
26
27
27
const MARKER = `�` ;
28
- const ICU_BLOCK_REGEX = / ^ \s * ( � \d + : ? \d * � ) \s * , \s * ( s e l e c t | p l u r a l ) \s * , / ;
28
+ const ICU_BLOCK_REGEXP = / ^ \s * ( � \d + : ? \d * � ) \s * , \s * ( s e l e c t | p l u r a l ) \s * , / ;
29
29
const SUBTEMPLATE_REGEXP = / � \/ ? \* ( \d + : \d + ) � / gi;
30
30
const PH_REGEXP = / � ( \/ ? [ # * ] \d + ) : ? \d * � / gi;
31
31
const BINDING_REGEXP = / � ( \d + ) : ? \d * � / gi;
32
32
const ICU_REGEXP = / ( { \s * � \d + : ? \d * � \s * , \s * \S { 6 } \s * , [ \s \S ] * } ) / gi;
33
33
34
- // i18nPostproocess regexps
35
- const PP_PLACEHOLDERS = / \[ ( � .+ ?� ? ) \] / g;
36
- const PP_ICU_VARS = / ( { \s * ) ( V A R _ ( P L U R A L | S E L E C T ) ( _ \d + ) ? ) ( \s * , ) / g;
37
- const PP_ICUS = / � I 1 8 N _ E X P _ ( I C U ( _ \d + ) ? ) � / g;
34
+ // i18nPostprocess consts
35
+ const ROOT_TEMPLATE_ID = 0 ;
36
+ const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = / \[ ( � .+ ?� ? ) \] / ;
37
+ const PP_PLACEHOLDERS_REGEXP = / \[ ( � .+ ?� ? ) \] | ( � \/ ? \* \d + : \d + � ) / g;
38
+ const PP_ICU_VARS_REGEXP = / ( { \s * ) ( V A R _ ( P L U R A L | S E L E C T ) ( _ \d + ) ? ) ( \s * , ) / g;
39
+ const PP_ICUS_REGEXP = / � I 1 8 N _ E X P _ ( I C U ( _ \d + ) ? ) � / g;
40
+ const PP_CLOSE_TEMPLATE_REGEXP = / \/ \* / ;
41
+ const PP_TEMPLATE_ID_REGEXP = / \d + \: ( \d + ) / ;
42
+
43
+ // Parsed placeholder structure used in postprocessing (within `i18nPostprocess` function)
44
+ // Contains the following fields: [templateId, isCloseTemplateTag, placeholder]
45
+ type PostprocessPlaceholder = [ number , boolean , string ] ;
38
46
39
47
interface IcuExpression {
40
48
type : IcuType ;
@@ -104,7 +112,7 @@ function extractParts(pattern: string): (string | IcuExpression)[] {
104
112
if ( braceStack . length == 0 ) {
105
113
// End of the block.
106
114
const block = pattern . substring ( prevPos , pos ) ;
107
- if ( ICU_BLOCK_REGEX . test ( block ) ) {
115
+ if ( ICU_BLOCK_REGEXP . test ( block ) ) {
108
116
results . push ( parseICUBlock ( block ) ) ;
109
117
} else if ( block ) { // Don't push empty strings
110
118
results . push ( block ) ;
@@ -142,7 +150,7 @@ function parseICUBlock(pattern: string): IcuExpression {
142
150
const values : ( string | IcuExpression ) [ ] [ ] = [ ] ;
143
151
let icuType = IcuType . plural ;
144
152
let mainBinding = 0 ;
145
- pattern = pattern . replace ( ICU_BLOCK_REGEX , function ( str : string , binding : string , type : string ) {
153
+ pattern = pattern . replace ( ICU_BLOCK_REGEXP , function ( str : string , binding : string , type : string ) {
146
154
if ( type === 'select' ) {
147
155
icuType = IcuType . select ;
148
156
} else {
@@ -505,43 +513,81 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
505
513
*/
506
514
export function i18nPostprocess (
507
515
message : string , replacements : { [ key : string ] : ( string | string [ ] ) } = { } ) : string {
508
- //
509
- // Step 1: resolve all multi-value cases (like [�*1:1��#2:1�|�#4:1�|�5�])
510
- //
511
- const matches : { [ key : string ] : string [ ] } = { } ;
512
- let result = message . replace ( PP_PLACEHOLDERS , ( _match , content : string ) : string => {
513
- if ( ! matches [ content ] ) {
514
- matches [ content ] = content . split ( '|' ) ;
515
- }
516
- if ( ! matches [ content ] . length ) {
517
- throw new Error ( `i18n postprocess: unmatched placeholder - ${ content } ` ) ;
516
+ /**
517
+ * Step 1: resolve all multi-value placeholders like [�#5�|�*1:1��#2:1�|�#4:1�]
518
+ *
519
+ * Note: due to the way we process nested templates (BFS), multi-value placeholders are typically
520
+ * grouped by templates, for example: [�#5�|�#6�|�#1:1�|�#3:2�] where �#5� and �#6� belong to root
521
+ * template, �#1:1� belong to nested template with index 1 and �#1:2� - nested template with index
522
+ * 3. However in real templates the order might be different: i.e. �#1:1� and/or �#3:2� may go in
523
+ * front of �#6�. The post processing step restores the right order by keeping track of the
524
+ * template id stack and looks for placeholders that belong to the currently active template.
525
+ */
526
+ let result : string = message ;
527
+ if ( PP_MULTI_VALUE_PLACEHOLDERS_REGEXP . test ( message ) ) {
528
+ const matches : { [ key : string ] : PostprocessPlaceholder [ ] } = { } ;
529
+ const templateIdsStack : number [ ] = [ ROOT_TEMPLATE_ID ] ;
530
+ result = result . replace ( PP_PLACEHOLDERS_REGEXP , ( m : any , phs : string , tmpl : string ) : string => {
531
+ const content = phs || tmpl ;
532
+ if ( ! matches [ content ] ) {
533
+ const placeholders : PostprocessPlaceholder [ ] = [ ] ;
534
+ content . split ( '|' ) . forEach ( ( placeholder : string ) => {
535
+ const match = placeholder . match ( PP_TEMPLATE_ID_REGEXP ) ;
536
+ const templateId = match ? parseInt ( match [ 1 ] , 10 ) : ROOT_TEMPLATE_ID ;
537
+ const isCloseTemplateTag = PP_CLOSE_TEMPLATE_REGEXP . test ( placeholder ) ;
538
+ placeholders . push ( [ templateId , isCloseTemplateTag , placeholder ] ) ;
539
+ } ) ;
540
+ matches [ content ] = placeholders ;
541
+ }
542
+ if ( ! matches [ content ] . length ) {
543
+ throw new Error ( `i18n postprocess: unmatched placeholder - ${ content } ` ) ;
544
+ }
545
+ const currentTemplateId = templateIdsStack [ templateIdsStack . length - 1 ] ;
546
+ const placeholders = matches [ content ] ;
547
+ let idx = 0 ;
548
+ // find placeholder index that matches current template id
549
+ for ( let i = 0 ; i < placeholders . length ; i ++ ) {
550
+ if ( placeholders [ i ] [ 0 ] === currentTemplateId ) {
551
+ idx = i ;
552
+ break ;
553
+ }
554
+ }
555
+ // update template id stack based on the current tag extracted
556
+ const [ templateId , isCloseTemplateTag , placeholder ] = placeholders [ idx ] ;
557
+ if ( isCloseTemplateTag ) {
558
+ templateIdsStack . pop ( ) ;
559
+ } else if ( currentTemplateId !== templateId ) {
560
+ templateIdsStack . push ( templateId ) ;
561
+ }
562
+ // remove processed tag from the list
563
+ placeholders . splice ( idx , 1 ) ;
564
+ return placeholder ;
565
+ } ) ;
566
+
567
+ // verify that we injected all values
568
+ const hasUnmatchedValues = Object . keys ( matches ) . some ( key => ! ! matches [ key ] . length ) ;
569
+ if ( hasUnmatchedValues ) {
570
+ throw new Error ( `i18n postprocess: unmatched values - ${ JSON . stringify ( matches ) } ` ) ;
518
571
}
519
- return matches [ content ] . shift ( ) ! ;
520
- } ) ;
521
-
522
- // verify that we injected all values
523
- const hasUnmatchedValues = Object . keys ( matches ) . some ( key => ! ! matches [ key ] . length ) ;
524
- if ( hasUnmatchedValues ) {
525
- throw new Error ( `i18n postprocess: unmatched values - ${ JSON . stringify ( matches ) } ` ) ;
526
572
}
527
573
528
574
// return current result if no replacements specified
529
575
if ( ! Object . keys ( replacements ) . length ) {
530
576
return result ;
531
577
}
532
578
533
- //
534
- // Step 2: replace all ICU vars (like "VAR_PLURAL")
535
- / /
536
- result = result . replace ( PP_ICU_VARS , ( match , start , key , _type , _idx , end ) : string => {
579
+ /**
580
+ * Step 2: replace all ICU vars (like "VAR_PLURAL")
581
+ * /
582
+ result = result . replace ( PP_ICU_VARS_REGEXP , ( match , start , key , _type , _idx , end ) : string => {
537
583
return replacements . hasOwnProperty ( key ) ? `${ start } ${ replacements [ key ] } ${ end } ` : match ;
538
584
} ) ;
539
585
540
- //
541
- // Step 3: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
542
- // in case multiple ICUs have the same placeholder name
543
- / /
544
- result = result . replace ( PP_ICUS , ( match , key ) : string => {
586
+ /**
587
+ * Step 3: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) in case
588
+ * multiple ICUs have the same placeholder name
589
+ * /
590
+ result = result . replace ( PP_ICUS_REGEXP , ( match , key ) : string => {
545
591
if ( replacements . hasOwnProperty ( key ) ) {
546
592
const list = replacements [ key ] as string [ ] ;
547
593
if ( ! list . length ) {
0 commit comments