diff --git a/modules/angular2/src/i18n/i18n_html_parser.ts b/modules/angular2/src/i18n/i18n_html_parser.ts
index 0fb38f750b..e707d78c04 100644
--- a/modules/angular2/src/i18n/i18n_html_parser.ts
+++ b/modules/angular2/src/i18n/i18n_html_parser.ts
@@ -17,19 +17,19 @@ import {Message, id} from './message';
import {
messageFromAttribute,
I18nError,
- isI18nAttr,
+ I18N_ATTR_PREFIX,
+ I18N_ATTR,
partition,
Part,
stringifyNodes,
meaning
} from './shared';
-const I18N_ATTR = "i18n";
-const PLACEHOLDER_ELEMENT = "ph";
-const NAME_ATTR = "name";
-const I18N_ATTR_PREFIX = "i18n-";
-let PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\`);
-let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\\\<\\/ph\\>`);
+const _I18N_ATTR = "i18n";
+const _PLACEHOLDER_ELEMENT = "ph";
+const _NAME_ATTR = "name";
+const _I18N_ATTR_PREFIX = "i18n-";
+let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\\\<\\/ph\\>`);
/**
* Creates an i18n-ed version of the parsed template.
@@ -94,7 +94,7 @@ let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\ HtmlAst.
* 4. Walk the translated tree.
@@ -115,7 +115,7 @@ export class I18nHtmlParser implements HtmlParser {
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
- private _messages: {[key: string]: string}) {}
+ private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
this.errors = [];
@@ -149,17 +149,8 @@ export class I18nHtmlParser implements HtmlParser {
throw new I18nError(p.sourceSpan, `Cannot find message for id '${messageId}'`);
}
- // get the message and expand a placeholder so becomes
- // we need to do it cause we use HtmlParser to parse the message
- let message = _expandPlaceholder(this._messages[messageId]);
- let parsedMessage = this._htmlParser.parse(message, "source");
-
- if (parsedMessage.errors.length > 0) {
- this.errors = this.errors.concat(parsedMessage.errors);
- return [];
- } else {
- return this._mergeTrees(p, message, parsedMessage.rootNodes, p.children);
- }
+ let parsedMessage = this._messages[messageId];
+ return this._mergeTrees(p, parsedMessage, p.children);
}
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
@@ -189,14 +180,13 @@ export class I18nHtmlParser implements HtmlParser {
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
}
- private _mergeTrees(p: Part, translatedSource: string, translated: HtmlAst[],
- original: HtmlAst[]): HtmlAst[] {
+ private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {
let l = new _CreateNodeMapping();
htmlVisitAll(l, original);
// merge the translated tree with the original tree.
// we do it by preserving the source code position of the original tree
- let merged = this._mergeTreesHelper(translatedSource, translated, l.mapping);
+ let merged = this._mergeTreesHelper(translated, l.mapping);
// if the root element is present, we need to create a new root element with its attributes
// translated
@@ -217,11 +207,10 @@ export class I18nHtmlParser implements HtmlParser {
}
}
- private _mergeTreesHelper(translatedSource: string, translated: HtmlAst[],
- mapping: HtmlAst[]): HtmlAst[] {
+ private _mergeTreesHelper(translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
return translated.map(t => {
if (t instanceof HtmlElementAst) {
- return this._mergeElementOrInterpolation(t, translatedSource, translated, mapping);
+ return this._mergeElementOrInterpolation(t, translated, mapping);
} else if (t instanceof HtmlTextAst) {
return t;
@@ -232,52 +221,51 @@ export class I18nHtmlParser implements HtmlParser {
});
}
- private _mergeElementOrInterpolation(t: HtmlElementAst, translatedSource: string,
- translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst {
+ private _mergeElementOrInterpolation(t: HtmlElementAst, translated: HtmlAst[],
+ mapping: HtmlAst[]): HtmlAst {
let name = this._getName(t);
let type = name[0];
let index = NumberWrapper.parseInt(name.substring(1), 10);
let originalNode = mapping[index];
if (type == "t") {
- return this._mergeTextInterpolation(t, originalNode, translatedSource);
+ return this._mergeTextInterpolation(t, originalNode);
} else if (type == "e") {
- return this._mergeElement(t, originalNode, mapping, translatedSource);
+ return this._mergeElement(t, originalNode, mapping);
} else {
throw new BaseException("should not be reached");
}
}
private _getName(t: HtmlElementAst): string {
- if (t.name != PLACEHOLDER_ELEMENT) {
+ if (t.name != _PLACEHOLDER_ELEMENT) {
throw new I18nError(
t.sourceSpan,
- `Unexpected tag "${t.name}". Only "${PLACEHOLDER_ELEMENT}" tags are allowed.`);
+ `Unexpected tag "${t.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
}
- let names = t.attrs.filter(a => a.name == NAME_ATTR);
+ let names = t.attrs.filter(a => a.name == _NAME_ATTR);
if (names.length == 0) {
- throw new I18nError(t.sourceSpan, `Missing "${NAME_ATTR}" attribute.`);
+ throw new I18nError(t.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
}
return names[0].value;
}
- private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst,
- translatedSource: string): HtmlTextAst {
+ private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
let split =
this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let messageSubstring =
- translatedSource.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
+ this._messagesContent.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
let translated =
this._replacePlaceholdersWithExpressions(messageSubstring, exps, originalNode.sourceSpan);
return new HtmlTextAst(translated, originalNode.sourceSpan);
}
- private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst, mapping: HtmlAst[],
- translatedSource: string): HtmlElementAst {
- let children = this._mergeTreesHelper(translatedSource, t.children, mapping);
+ private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst,
+ mapping: HtmlAst[]): HtmlElementAst {
+ let children = this._mergeTreesHelper(t.children, mapping);
return new HtmlElementAst(originalNode.name, this._i18nAttributes(originalNode), children,
originalNode.sourceSpan, originalNode.startSourceSpan,
originalNode.endSourceSpan);
@@ -286,30 +274,46 @@ export class I18nHtmlParser implements HtmlParser {
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
let res = [];
el.attrs.forEach(attr => {
- if (isI18nAttr(attr.name)) {
- let messageId = id(messageFromAttribute(this._parser, el, attr));
- let expectedName = attr.name.substring(5);
- let m = el.attrs.filter(a => a.name == expectedName)[0];
+ if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
- if (StringMapWrapper.contains(this._messages, messageId)) {
- let split = this._parser.splitInterpolation(m.value, m.sourceSpan.toString());
- let exps = isPresent(split) ? split.expressions : [];
- let message = this._replacePlaceholdersWithExpressions(
- _expandPlaceholder(this._messages[messageId]), exps, m.sourceSpan);
- res.push(new HtmlAttrAst(m.name, message, m.sourceSpan));
-
- } else {
- throw new I18nError(m.sourceSpan, `Cannot find message for id '${messageId}'`);
- }
+ let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
+ if (i18ns.length == 0) {
+ res.push(attr);
+ return;
}
+ let i18n = i18ns[0];
+ let messageId = id(messageFromAttribute(this._parser, el, i18n));
+
+ if (StringMapWrapper.contains(this._messages, messageId)) {
+ let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
+ res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
+
+ } else {
+ throw new I18nError(attr.sourceSpan, `Cannot find message for id '${messageId}'`);
+ }
});
return res;
}
+ private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
+ let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
+ let exps = isPresent(split) ? split.expressions : [];
+
+ let first = msg[0];
+ let last = msg[msg.length - 1];
+
+ let start = first.sourceSpan.start.offset;
+ let end =
+ last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
+ let messageSubstring = this._messagesContent.substring(start, end);
+
+ return this._replacePlaceholdersWithExpressions(messageSubstring, exps, attr.sourceSpan);
+ };
+
private _replacePlaceholdersWithExpressions(message: string, exps: string[],
sourceSpan: ParseSourceSpan): string {
- return RegExpWrapper.replaceAll(PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
+ return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
let nameWithQuotes = match[2];
let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
let index = NumberWrapper.parseInt(name, 10);
@@ -343,11 +347,4 @@ class _CreateNodeMapping implements HtmlAstVisitor {
}
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
-}
-
-function _expandPlaceholder(input: string): string {
- return RegExpWrapper.replaceAll(PLACEHOLDER_REGEXP, input, (match) => {
- let nameWithQuotes = match[2];
- return ``;
- });
}
\ No newline at end of file
diff --git a/modules/angular2/src/i18n/message_extractor.ts b/modules/angular2/src/i18n/message_extractor.ts
index bae8d5f345..3895294374 100644
--- a/modules/angular2/src/i18n/message_extractor.ts
+++ b/modules/angular2/src/i18n/message_extractor.ts
@@ -16,10 +16,10 @@ import {Message, id} from './message';
import {
I18nError,
Part,
+ I18N_ATTR_PREFIX,
partition,
meaning,
description,
- isI18nAttr,
stringifyNodes,
messageFromAttribute
} from './shared';
@@ -161,7 +161,7 @@ export class MessageExtractor {
private _extractMessagesFromAttributes(p: HtmlElementAst): void {
p.attrs.forEach(attr => {
- if (isI18nAttr(attr.name)) {
+ if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
try {
this.messages.push(messageFromAttribute(this._parser, p, attr));
} catch (e) {
diff --git a/modules/angular2/src/i18n/shared.ts b/modules/angular2/src/i18n/shared.ts
index 9f249f2880..e221e1bc69 100644
--- a/modules/angular2/src/i18n/shared.ts
+++ b/modules/angular2/src/i18n/shared.ts
@@ -12,8 +12,8 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {Message} from './message';
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
-const I18N_ATTR = "i18n";
-const I18N_ATTR_PREFIX = "i18n-";
+export const I18N_ATTR = "i18n";
+export const I18N_ATTR_PREFIX = "i18n-";
/**
* An i18n error.
@@ -80,10 +80,6 @@ function _isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value == "/i18n";
}
-export function isI18nAttr(n: string): boolean {
- return n.startsWith(I18N_ATTR_PREFIX);
-}
-
function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
return i18n.length == 0 ? null : i18n[0];
diff --git a/modules/angular2/test/i18n/i18n_html_parser_spec.ts b/modules/angular2/test/i18n/i18n_html_parser_spec.ts
index f3fbf766fc..95d6aff8df 100644
--- a/modules/angular2/test/i18n/i18n_html_parser_spec.ts
+++ b/modules/angular2/test/i18n/i18n_html_parser_spec.ts
@@ -16,6 +16,7 @@ import {Message, id} from 'angular2/src/i18n/message';
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
import {Lexer} from 'angular2/src/core/change_detection/parser/lexer';
+import {StringMapWrapper} from 'angular2/src/facade/collection';
import {HtmlParser, HtmlParseTreeResult} from 'angular2/src/compiler/html_parser';
import {
HtmlAst,
@@ -26,6 +27,7 @@ import {
HtmlCommentAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
+import {serializeXmb, deserializeXmb} from 'angular2/src/i18n/xmb_serializer';
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
import {humanizeDom} from '../../test/compiler/html_ast_spec_utils';
@@ -34,7 +36,13 @@ export function main() {
function parse(template: string, messages: {[key: string]: string}): HtmlParseTreeResult {
var parser = new Parser(new Lexer());
let htmlParser = new HtmlParser();
- return new I18nHtmlParser(htmlParser, parser, messages).parse(template, "someurl");
+
+ let msgs = '';
+ StringMapWrapper.forEach(messages, (v, k) => msgs += `${v}`);
+ let res = deserializeXmb(`${msgs}`, 'someUrl');
+
+ return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
+ .parse(template, "someurl");
}
it("should delegate to the provided parser when no i18n", () => {
@@ -112,6 +120,18 @@ export function main() {
]);
});
+ it("should preserve non-i18n attributes", () => {
+ let translations: {[key: string]: string} = {};
+ translations[id(new Message('message', null, null))] = 'another message';
+
+ expect(humanizeDom(parse('message
', translations)))
+ .toEqual([
+ [HtmlElementAst, 'div', 0],
+ [HtmlAttrAst, 'value', "b"],
+ [HtmlTextAst, 'another message', 1]
+ ]);
+ });
+
it('should extract from partitions', () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
@@ -156,14 +176,6 @@ export function main() {
.toEqual([`Cannot find message for id '${mid}'`]);
});
- it("should error when message cannot be parsed", () => {
- let translations: {[key: string]: string} = {};
- translations[id(new Message("some message", null, null))] = "a";
-
- expect(humanizeErrors(parse("some message
", translations).errors))
- .toEqual([`Unexpected closing tag "b"`]);
- });
-
it("should error when a non-placeholder element appears in translation", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", null, null))] = "a";
@@ -180,11 +192,6 @@ export function main() {
.toEqual([`Missing "name" attribute.`]);
});
- it("should error when no matching attribute", () => {
- expect(humanizeErrors(parse("", {}).errors))
- .toEqual([`Missing attribute 'value'.`]);
- });
-
it("should error when the translation refers to an invalid expression", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message('hi ', null, null))] = 'hi ';