@@ -12,11 +12,17 @@ import {
1212import { isPresent , isBlank } from 'angular2/src/facade/lang' ;
1313import { StringMapWrapper } from 'angular2/src/facade/collection' ;
1414import { Parser } from 'angular2/src/core/change_detection/parser/parser' ;
15- import { Interpolation } from 'angular2/src/core/change_detection/parser/ast' ;
1615import { Message , id } from './message' ;
17-
18- const I18N_ATTR = "i18n" ;
19- const I18N_ATTR_PREFIX = "i18n-" ;
16+ import {
17+ I18nError ,
18+ Part ,
19+ partition ,
20+ meaning ,
21+ description ,
22+ isI18nAttr ,
23+ stringifyNodes ,
24+ messageFromAttribute
25+ } from './shared' ;
2026
2127/**
2228 * All messages extracted from a template.
@@ -25,13 +31,6 @@ export class ExtractionResult {
2531 constructor ( public messages : Message [ ] , public errors : ParseError [ ] ) { }
2632}
2733
28- /**
29- * An extraction error.
30- */
31- export class I18nExtractionError extends ParseError {
32- constructor ( span : ParseSourceSpan , msg : string ) { super ( span , msg ) ; }
33- }
34-
3534/**
3635 * Removes duplicate messages.
3736 *
@@ -56,20 +55,61 @@ export function removeDuplicates(messages: Message[]): Message[] {
5655/**
5756 * Extracts all messages from a template.
5857 *
59- * It works like this. First, the extractor uses the provided html parser to get
60- * the html AST of the template. Then it partitions the root nodes into parts.
61- * Everything between two i18n comments becomes a single part. Every other nodes becomes
62- * a part too.
58+ * Algorithm:
59+ *
60+ * To understand the algorithm, you need to know how partitioning works.
61+ * Partitioning is required as we can use two i18n comments to group node siblings together.
62+ * That is why we cannot just use nodes.
63+ *
64+ * Partitioning transforms an array of HtmlAst into an array of Part.
65+ * A part can optionally contain a root element or a root text node. And it can also contain
66+ * children.
67+ * A part can contain i18n property, in which case it needs to be extracted.
68+ *
69+ * Example:
6370 *
64- * We process every part as follows. Say we have a part A.
71+ * The following array of nodes will be split into four parts:
6572 *
66- * If the part has the i18n attribute, it gets converted into a message.
67- * And we do not recurse into that part, except to extract messages from the attributes.
73+ * ```
74+ * <a>A</a>
75+ * <b i18n>B</b>
76+ * <!-- i18n -->
77+ * <c>C</c>
78+ * D
79+ * <!-- /i18n -->
80+ * E
81+ * ```
82+ *
83+ * Part 1 containing the a tag. It should not be translated.
84+ * Part 2 containing the b tag. It should be translated.
85+ * Part 3 containing the c tag and the D text node. It should be translated.
86+ * Part 4 containing the E text node. It should not be translated..
87+ *
88+ * It is also important to understand how we stringify nodes to create a message.
89+ *
90+ * We walk the tree and replace every element node with a placeholder. We also replace
91+ * all expressions in interpolation with placeholders. We also insert a placeholder element
92+ * to wrap a text node containing interpolation.
6893 *
69- * If the part doesn't have the i18n attribute, we recurse into that part and
70- * partition its children.
94+ * Example:
95+ *
96+ * The following tree:
97+ *
98+ * ```
99+ * <a>A{{I}}</a><b>B</b>
100+ * ```
71101 *
72- * While walking the AST we also remove i18n attributes from messages.
102+ * will be stringified into:
103+ * ```
104+ * <ph name="e0"><ph name="t1">A<ph name="0"/></ph></ph><ph name="e2">B</ph>
105+ * ```
106+ *
107+ * This is what the algorithm does:
108+ *
109+ * 1. Use the provided html parser to get the html AST of the template.
110+ * 2. Partition the root nodes, and process each part separately.
111+ * 3. If a part does not have the i18n attribute, recurse to process children and attributes.
112+ * 4. If a part has the i18n attribute, stringify the nodes to create a Message.
73113 */
74114export class MessageExtractor {
75115 messages : Message [ ] ;
@@ -85,16 +125,14 @@ export class MessageExtractor {
85125 if ( res . errors . length > 0 ) {
86126 return new ExtractionResult ( [ ] , res . errors ) ;
87127 } else {
88- let ps = this . _partition ( res . rootNodes ) ;
89- ps . forEach ( p => this . _extractMessagesFromPart ( p ) ) ;
128+ this . _recurse ( res . rootNodes ) ;
90129 return new ExtractionResult ( this . messages , this . errors ) ;
91130 }
92131 }
93132
94- private _extractMessagesFromPart ( p : _Part ) : void {
133+ private _extractMessagesFromPart ( p : Part ) : void {
95134 if ( p . hasI18n ) {
96- this . messages . push ( new Message ( _stringifyNodes ( p . children , this . _parser ) , _meaning ( p . i18n ) ,
97- _description ( p . i18n ) ) ) ;
135+ this . messages . push ( p . createMessage ( this . _parser ) ) ;
98136 this . _recurseToExtractMessagesFromAttributes ( p . children ) ;
99137 } else {
100138 this . _recurse ( p . children ) ;
@@ -106,8 +144,10 @@ export class MessageExtractor {
106144 }
107145
108146 private _recurse ( nodes : HtmlAst [ ] ) : void {
109- let ps = this . _partition ( nodes ) ;
110- ps . forEach ( p => this . _extractMessagesFromPart ( p ) ) ;
147+ if ( isPresent ( nodes ) ) {
148+ let ps = partition ( nodes , this . errors ) ;
149+ ps . forEach ( p => this . _extractMessagesFromPart ( p ) ) ;
150+ }
111151 }
112152
113153 private _recurseToExtractMessagesFromAttributes ( nodes : HtmlAst [ ] ) : void {
@@ -121,130 +161,17 @@ export class MessageExtractor {
121161
122162 private _extractMessagesFromAttributes ( p : HtmlElementAst ) : void {
123163 p . attrs . forEach ( attr => {
124- if ( attr . name . startsWith ( I18N_ATTR_PREFIX ) ) {
125- let expectedName = attr . name . substring ( 5 ) ;
126- let matching = p . attrs . filter ( a => a . name == expectedName ) ;
127-
128- if ( matching . length > 0 ) {
129- let value = _removeInterpolation ( matching [ 0 ] . value , p . sourceSpan , this . _parser ) ;
130- this . messages . push ( new Message ( value , _meaning ( attr . value ) , _description ( attr . value ) ) ) ;
131- } else {
132- this . errors . push (
133- new I18nExtractionError ( p . sourceSpan , `Missing attribute '${ expectedName } '.` ) ) ;
134- }
135- }
136- } ) ;
137- }
138-
139- // Man, this is so ugly!
140- private _partition ( nodes : HtmlAst [ ] ) : _Part [ ] {
141- let res = [ ] ;
142-
143- for ( let i = 0 ; i < nodes . length ; ++ i ) {
144- let n = nodes [ i ] ;
145- let temp = [ ] ;
146- if ( _isOpeningComment ( n ) ) {
147- let i18n = ( < HtmlCommentAst > n ) . value . substring ( 5 ) . trim ( ) ;
148- i ++ ;
149- while ( ! _isClosingComment ( nodes [ i ] ) ) {
150- temp . push ( nodes [ i ++ ] ) ;
151- if ( i === nodes . length ) {
152- this . errors . push (
153- new I18nExtractionError ( n . sourceSpan , "Missing closing 'i18n' comment." ) ) ;
154- break ;
164+ if ( isI18nAttr ( attr . name ) ) {
165+ try {
166+ this . messages . push ( messageFromAttribute ( this . _parser , p , attr ) ) ;
167+ } catch ( e ) {
168+ if ( e instanceof I18nError ) {
169+ this . errors . push ( e ) ;
170+ } else {
171+ throw e ;
155172 }
156173 }
157- res . push ( new _Part ( null , temp , i18n , true ) ) ;
158-
159- } else if ( n instanceof HtmlElementAst ) {
160- let i18n = _findI18nAttr ( n ) ;
161- res . push ( new _Part ( n , n . children , isPresent ( i18n ) ? i18n . value : null , isPresent ( i18n ) ) ) ;
162- }
163- }
164-
165- return res ;
166- }
167- }
168-
169- class _Part {
170- constructor ( public rootElement : HtmlElementAst , public children : HtmlAst [ ] , public i18n : string ,
171- public hasI18n : boolean ) { }
172- }
173-
174- function _isOpeningComment ( n : HtmlAst ) : boolean {
175- return n instanceof HtmlCommentAst && isPresent ( n . value ) && n . value . startsWith ( "i18n:" ) ;
176- }
177-
178- function _isClosingComment ( n : HtmlAst ) : boolean {
179- return n instanceof HtmlCommentAst && isPresent ( n . value ) && n . value == "/i18n" ;
180- }
181-
182- function _stringifyNodes ( nodes : HtmlAst [ ] , parser : Parser ) {
183- let visitor = new _StringifyVisitor ( parser ) ;
184- return htmlVisitAll ( visitor , nodes ) . join ( "" ) ;
185- }
186-
187- class _StringifyVisitor implements HtmlAstVisitor {
188- constructor ( private _parser : Parser ) { }
189-
190- visitElement ( ast : HtmlElementAst , context : any ) : any {
191- let attrs = this . _join ( htmlVisitAll ( this , ast . attrs ) , " " ) ;
192- let children = this . _join ( htmlVisitAll ( this , ast . children ) , "" ) ;
193- return `<${ ast . name } ${ attrs } >${ children } </${ ast . name } >` ;
194- }
195-
196- visitAttr ( ast : HtmlAttrAst , context : any ) : any {
197- if ( ast . name . startsWith ( I18N_ATTR_PREFIX ) ) {
198- return "" ;
199- } else {
200- return `${ ast . name } ="${ ast . value } "` ;
201- }
202- }
203-
204- visitText ( ast : HtmlTextAst , context : any ) : any {
205- return _removeInterpolation ( ast . value , ast . sourceSpan , this . _parser ) ;
206- }
207-
208- visitComment ( ast : HtmlCommentAst , context : any ) : any { return "" ; }
209-
210- private _join ( strs : string [ ] , str : string ) : string {
211- return strs . filter ( s => s . length > 0 ) . join ( str ) ;
212- }
213- }
214-
215- function _removeInterpolation ( value : string , source : ParseSourceSpan , parser : Parser ) : string {
216- try {
217- let parsed = parser . parseInterpolation ( value , source . toString ( ) ) ;
218- if ( isPresent ( parsed ) ) {
219- let ast : Interpolation = < any > parsed . ast ;
220- let res = "" ;
221- for ( let i = 0 ; i < ast . strings . length ; ++ i ) {
222- res += ast . strings [ i ] ;
223- if ( i != ast . strings . length - 1 ) {
224- res += `{{I${ i } }}` ;
225- }
226174 }
227- return res ;
228- } else {
229- return value ;
230- }
231- } catch ( e ) {
232- return value ;
175+ } ) ;
233176 }
234- }
235-
236- function _findI18nAttr ( p : HtmlElementAst ) : HtmlAttrAst {
237- let i18n = p . attrs . filter ( a => a . name == I18N_ATTR ) ;
238- return i18n . length == 0 ? null : i18n [ 0 ] ;
239- }
240-
241- function _meaning ( i18n : string ) : string {
242- if ( isBlank ( i18n ) || i18n == "" ) return null ;
243- return i18n . split ( "|" ) [ 0 ] ;
244- }
245-
246- function _description ( i18n : string ) : string {
247- if ( isBlank ( i18n ) || i18n == "" ) return null ;
248- let parts = i18n . split ( "|" ) ;
249- return parts . length > 1 ? parts [ 1 ] : null ;
250177}
0 commit comments