Skip to content

Commit 73a84a7

Browse files
committed
refactor(i18n): remove utility functions into a separate file
1 parent 17c8ec8 commit 73a84a7

File tree

4 files changed

+300
-188
lines changed

4 files changed

+300
-188
lines changed

modules/angular2/src/i18n/message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {isPresent, escape} from 'angular2/src/facade/lang';
88
* `description` is additional information provided to the translator.
99
*/
1010
export class Message {
11-
constructor(public content: string, public meaning: string, public description: string) {}
11+
constructor(public content: string, public meaning: string, public description: string = null) {}
1212
}
1313

1414
/**

modules/angular2/src/i18n/message_extractor.ts

Lines changed: 77 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ import {
1212
import {isPresent, isBlank} from 'angular2/src/facade/lang';
1313
import {StringMapWrapper} from 'angular2/src/facade/collection';
1414
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
15-
import {Interpolation} from 'angular2/src/core/change_detection/parser/ast';
1615
import {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
*/
74114
export 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

Comments
 (0)