Skip to content

Commit 756121a

Browse files
committed
feat(i18n): update I18nHtmlParser to accept parsed messages
1 parent d7e1175 commit 756121a

File tree

4 files changed

+85
-85
lines changed

4 files changed

+85
-85
lines changed

modules/angular2/src/i18n/i18n_html_parser.ts

Lines changed: 60 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ import {Message, id} from './message';
1717
import {
1818
messageFromAttribute,
1919
I18nError,
20-
isI18nAttr,
20+
I18N_ATTR_PREFIX,
21+
I18N_ATTR,
2122
partition,
2223
Part,
2324
stringifyNodes,
2425
meaning
2526
} from './shared';
2627

27-
const I18N_ATTR = "i18n";
28-
const PLACEHOLDER_ELEMENT = "ph";
29-
const NAME_ATTR = "name";
30-
const I18N_ATTR_PREFIX = "i18n-";
31-
let PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\/\\>`);
32-
let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\>\\<\\/ph\\>`);
28+
const _I18N_ATTR = "i18n";
29+
const _PLACEHOLDER_ELEMENT = "ph";
30+
const _NAME_ATTR = "name";
31+
const _I18N_ATTR_PREFIX = "i18n-";
32+
let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\>\\<\\/ph\\>`);
3333

3434
/**
3535
* Creates an i18n-ed version of the parsed template.
@@ -94,7 +94,7 @@ let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+
9494
* This is how the merging works:
9595
*
9696
* 1. Use the stringify function to get the message id. Look up the message in the map.
97-
* 2. Parse the translated message. At this point we have two trees: the original tree
97+
* 2. Get the translated message. At this point we have two trees: the original tree
9898
* and the translated tree, where all the elements are replaced with placeholders.
9999
* 3. Use the original tree to create a mapping Index:number -> HtmlAst.
100100
* 4. Walk the translated tree.
@@ -115,7 +115,7 @@ export class I18nHtmlParser implements HtmlParser {
115115
errors: ParseError[];
116116

117117
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
118-
private _messages: {[key: string]: string}) {}
118+
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
119119

120120
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
121121
this.errors = [];
@@ -149,17 +149,8 @@ export class I18nHtmlParser implements HtmlParser {
149149
throw new I18nError(p.sourceSpan, `Cannot find message for id '${messageId}'`);
150150
}
151151

152-
// get the message and expand a placeholder so <ph/> becomes <ph></ph>
153-
// we need to do it cause we use HtmlParser to parse the message
154-
let message = _expandPlaceholder(this._messages[messageId]);
155-
let parsedMessage = this._htmlParser.parse(message, "source");
156-
157-
if (parsedMessage.errors.length > 0) {
158-
this.errors = this.errors.concat(parsedMessage.errors);
159-
return [];
160-
} else {
161-
return this._mergeTrees(p, message, parsedMessage.rootNodes, p.children);
162-
}
152+
let parsedMessage = this._messages[messageId];
153+
return this._mergeTrees(p, parsedMessage, p.children);
163154
}
164155

165156
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
@@ -189,14 +180,13 @@ export class I18nHtmlParser implements HtmlParser {
189180
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
190181
}
191182

192-
private _mergeTrees(p: Part, translatedSource: string, translated: HtmlAst[],
193-
original: HtmlAst[]): HtmlAst[] {
183+
private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {
194184
let l = new _CreateNodeMapping();
195185
htmlVisitAll(l, original);
196186

197187
// merge the translated tree with the original tree.
198188
// we do it by preserving the source code position of the original tree
199-
let merged = this._mergeTreesHelper(translatedSource, translated, l.mapping);
189+
let merged = this._mergeTreesHelper(translated, l.mapping);
200190

201191
// if the root element is present, we need to create a new root element with its attributes
202192
// translated
@@ -217,11 +207,10 @@ export class I18nHtmlParser implements HtmlParser {
217207
}
218208
}
219209

220-
private _mergeTreesHelper(translatedSource: string, translated: HtmlAst[],
221-
mapping: HtmlAst[]): HtmlAst[] {
210+
private _mergeTreesHelper(translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
222211
return translated.map(t => {
223212
if (t instanceof HtmlElementAst) {
224-
return this._mergeElementOrInterpolation(t, translatedSource, translated, mapping);
213+
return this._mergeElementOrInterpolation(t, translated, mapping);
225214

226215
} else if (t instanceof HtmlTextAst) {
227216
return t;
@@ -232,52 +221,51 @@ export class I18nHtmlParser implements HtmlParser {
232221
});
233222
}
234223

235-
private _mergeElementOrInterpolation(t: HtmlElementAst, translatedSource: string,
236-
translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst {
224+
private _mergeElementOrInterpolation(t: HtmlElementAst, translated: HtmlAst[],
225+
mapping: HtmlAst[]): HtmlAst {
237226
let name = this._getName(t);
238227
let type = name[0];
239228
let index = NumberWrapper.parseInt(name.substring(1), 10);
240229
let originalNode = mapping[index];
241230

242231
if (type == "t") {
243-
return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode, translatedSource);
232+
return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode);
244233
} else if (type == "e") {
245-
return this._mergeElement(t, <HtmlElementAst>originalNode, mapping, translatedSource);
234+
return this._mergeElement(t, <HtmlElementAst>originalNode, mapping);
246235
} else {
247236
throw new BaseException("should not be reached");
248237
}
249238
}
250239

251240
private _getName(t: HtmlElementAst): string {
252-
if (t.name != PLACEHOLDER_ELEMENT) {
241+
if (t.name != _PLACEHOLDER_ELEMENT) {
253242
throw new I18nError(
254243
t.sourceSpan,
255-
`Unexpected tag "${t.name}". Only "${PLACEHOLDER_ELEMENT}" tags are allowed.`);
244+
`Unexpected tag "${t.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
256245
}
257-
let names = t.attrs.filter(a => a.name == NAME_ATTR);
246+
let names = t.attrs.filter(a => a.name == _NAME_ATTR);
258247
if (names.length == 0) {
259-
throw new I18nError(t.sourceSpan, `Missing "${NAME_ATTR}" attribute.`);
248+
throw new I18nError(t.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
260249
}
261250
return names[0].value;
262251
}
263252

264-
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst,
265-
translatedSource: string): HtmlTextAst {
253+
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
266254
let split =
267255
this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
268256
let exps = isPresent(split) ? split.expressions : [];
269257

270258
let messageSubstring =
271-
translatedSource.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
259+
this._messagesContent.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
272260
let translated =
273261
this._replacePlaceholdersWithExpressions(messageSubstring, exps, originalNode.sourceSpan);
274262

275263
return new HtmlTextAst(translated, originalNode.sourceSpan);
276264
}
277265

278-
private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst, mapping: HtmlAst[],
279-
translatedSource: string): HtmlElementAst {
280-
let children = this._mergeTreesHelper(translatedSource, t.children, mapping);
266+
private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst,
267+
mapping: HtmlAst[]): HtmlElementAst {
268+
let children = this._mergeTreesHelper(t.children, mapping);
281269
return new HtmlElementAst(originalNode.name, this._i18nAttributes(originalNode), children,
282270
originalNode.sourceSpan, originalNode.startSourceSpan,
283271
originalNode.endSourceSpan);
@@ -286,30 +274,46 @@ export class I18nHtmlParser implements HtmlParser {
286274
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
287275
let res = [];
288276
el.attrs.forEach(attr => {
289-
if (isI18nAttr(attr.name)) {
290-
let messageId = id(messageFromAttribute(this._parser, el, attr));
291-
let expectedName = attr.name.substring(5);
292-
let m = el.attrs.filter(a => a.name == expectedName)[0];
293-
294-
if (StringMapWrapper.contains(this._messages, messageId)) {
295-
let split = this._parser.splitInterpolation(m.value, m.sourceSpan.toString());
296-
let exps = isPresent(split) ? split.expressions : [];
297-
let message = this._replacePlaceholdersWithExpressions(
298-
_expandPlaceholder(this._messages[messageId]), exps, m.sourceSpan);
299-
res.push(new HtmlAttrAst(m.name, message, m.sourceSpan));
300-
301-
} else {
302-
throw new I18nError(m.sourceSpan, `Cannot find message for id '${messageId}'`);
303-
}
277+
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
278+
279+
let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
280+
if (i18ns.length == 0) {
281+
res.push(attr);
282+
return;
304283
}
305284

285+
let i18n = i18ns[0];
286+
let messageId = id(messageFromAttribute(this._parser, el, i18n));
287+
288+
if (StringMapWrapper.contains(this._messages, messageId)) {
289+
let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
290+
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
291+
292+
} else {
293+
throw new I18nError(attr.sourceSpan, `Cannot find message for id '${messageId}'`);
294+
}
306295
});
307296
return res;
308297
}
309298

299+
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
300+
let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
301+
let exps = isPresent(split) ? split.expressions : [];
302+
303+
let first = msg[0];
304+
let last = msg[msg.length - 1];
305+
306+
let start = first.sourceSpan.start.offset;
307+
let end =
308+
last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
309+
let messageSubstring = this._messagesContent.substring(start, end);
310+
311+
return this._replacePlaceholdersWithExpressions(messageSubstring, exps, attr.sourceSpan);
312+
};
313+
310314
private _replacePlaceholdersWithExpressions(message: string, exps: string[],
311315
sourceSpan: ParseSourceSpan): string {
312-
return RegExpWrapper.replaceAll(PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
316+
return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
313317
let nameWithQuotes = match[2];
314318
let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
315319
let index = NumberWrapper.parseInt(name, 10);
@@ -343,11 +347,4 @@ class _CreateNodeMapping implements HtmlAstVisitor {
343347
}
344348

345349
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
346-
}
347-
348-
function _expandPlaceholder(input: string): string {
349-
return RegExpWrapper.replaceAll(PLACEHOLDER_REGEXP, input, (match) => {
350-
let nameWithQuotes = match[2];
351-
return `<ph name=${nameWithQuotes}></ph>`;
352-
});
353350
}

modules/angular2/src/i18n/message_extractor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import {Message, id} from './message';
1616
import {
1717
I18nError,
1818
Part,
19+
I18N_ATTR_PREFIX,
1920
partition,
2021
meaning,
2122
description,
22-
isI18nAttr,
2323
stringifyNodes,
2424
messageFromAttribute
2525
} from './shared';
@@ -161,7 +161,7 @@ export class MessageExtractor {
161161

162162
private _extractMessagesFromAttributes(p: HtmlElementAst): void {
163163
p.attrs.forEach(attr => {
164-
if (isI18nAttr(attr.name)) {
164+
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
165165
try {
166166
this.messages.push(messageFromAttribute(this._parser, p, attr));
167167
} catch (e) {

modules/angular2/src/i18n/shared.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
1212
import {Message} from './message';
1313
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
1414

15-
const I18N_ATTR = "i18n";
16-
const I18N_ATTR_PREFIX = "i18n-";
15+
export const I18N_ATTR = "i18n";
16+
export const I18N_ATTR_PREFIX = "i18n-";
1717

1818
/**
1919
* An i18n error.
@@ -80,10 +80,6 @@ function _isClosingComment(n: HtmlAst): boolean {
8080
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value == "/i18n";
8181
}
8282

83-
export function isI18nAttr(n: string): boolean {
84-
return n.startsWith(I18N_ATTR_PREFIX);
85-
}
86-
8783
function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
8884
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
8985
return i18n.length == 0 ? null : i18n[0];

modules/angular2/test/i18n/i18n_html_parser_spec.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Message, id} from 'angular2/src/i18n/message';
1616
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
1717
import {Lexer} from 'angular2/src/core/change_detection/parser/lexer';
1818

19+
import {StringMapWrapper} from 'angular2/src/facade/collection';
1920
import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser';
2021
import {
2122
HtmlAst,
@@ -26,6 +27,7 @@ import {
2627
HtmlCommentAst,
2728
htmlVisitAll
2829
} from 'angular2/src/compiler/html_ast';
30+
import {serializeXmb, deserializeXmb} from 'angular2/src/i18n/xmb_serializer';
2931
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
3032
import {humanizeDom} from '../../test/compiler/html_ast_spec_utils';
3133

@@ -34,7 +36,13 @@ export function main() {
3436
function parse(template: string, messages: {[key: string]: string}): HtmlParseTreeResult {
3537
var parser = new Parser(new Lexer());
3638
let htmlParser = new HtmlParser();
37-
return new I18nHtmlParser(htmlParser, parser, messages).parse(template, "someurl");
39+
40+
let msgs = '';
41+
StringMapWrapper.forEach(messages, (v, k) => msgs += `<msg id="${k}">${v}</msg>`);
42+
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
43+
44+
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
45+
.parse(template, "someurl");
3846
}
3947

4048
it("should delegate to the provided parser when no i18n", () => {
@@ -112,6 +120,18 @@ export function main() {
112120
]);
113121
});
114122

123+
it("should preserve non-i18n attributes", () => {
124+
let translations: {[key: string]: string} = {};
125+
translations[id(new Message('message', null, null))] = 'another message';
126+
127+
expect(humanizeDom(parse('<div i18n value="b">message</div>', translations)))
128+
.toEqual([
129+
[HtmlElementAst, 'div', 0],
130+
[HtmlAttrAst, 'value', "b"],
131+
[HtmlTextAst, 'another message', 1]
132+
]);
133+
});
134+
115135
it('should extract from partitions', () => {
116136
let translations: {[key: string]: string} = {};
117137
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
@@ -156,14 +176,6 @@ export function main() {
156176
.toEqual([`Cannot find message for id '${mid}'`]);
157177
});
158178

159-
it("should error when message cannot be parsed", () => {
160-
let translations: {[key: string]: string} = {};
161-
translations[id(new Message("some message", null, null))] = "<a>a</b>";
162-
163-
expect(humanizeErrors(parse("<div i18n>some message</div>", translations).errors))
164-
.toEqual([`Unexpected closing tag "b"`]);
165-
});
166-
167179
it("should error when a non-placeholder element appears in translation", () => {
168180
let translations: {[key: string]: string} = {};
169181
translations[id(new Message("some message", null, null))] = "<a>a</a>";
@@ -180,11 +192,6 @@ export function main() {
180192
.toEqual([`Missing "name" attribute.`]);
181193
});
182194

183-
it("should error when no matching attribute", () => {
184-
expect(humanizeErrors(parse("<div i18n-value></div>", {}).errors))
185-
.toEqual([`Missing attribute 'value'.`]);
186-
});
187-
188195
it("should error when the translation refers to an invalid expression", () => {
189196
let translations: {[key: string]: string} = {};
190197
translations[id(new Message('hi <ph name="0"/>', null, null))] = 'hi <ph name="99"/>';

0 commit comments

Comments
 (0)