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 ';