From c6244d14706c0da44612125cbf6588d4c96a7061 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Wed, 13 Apr 2016 16:01:25 -0700 Subject: [PATCH] feat(i18n): add support for nested expansion forms Closes #7977 --- modules/angular2/src/compiler/html_lexer.ts | 33 +++-- modules/angular2/src/compiler/html_parser.ts | 115 ++++++++++++------ .../angular2/src/compiler/template_parser.ts | 7 +- modules/angular2/src/i18n/expander.ts | 71 +++++++---- modules/angular2/src/i18n/i18n_html_parser.ts | 24 ++-- .../angular2/src/i18n/message_extractor.ts | 9 +- .../angular2/test/compiler/html_lexer_spec.ts | 25 ++++ .../test/compiler/html_parser_spec.ts | 21 ++++ .../test/i18n/i18n_html_parser_spec.ts | 35 +++++- 9 files changed, 246 insertions(+), 94 deletions(-) diff --git a/modules/angular2/src/compiler/html_lexer.ts b/modules/angular2/src/compiler/html_lexer.ts index 76f48683d9..bedab75ca8 100644 --- a/modules/angular2/src/compiler/html_lexer.ts +++ b/modules/angular2/src/compiler/html_lexer.ts @@ -125,8 +125,7 @@ class _HtmlTokenizer { private currentTokenStart: ParseLocation; private currentTokenType: HtmlTokenType; - private inExpansionCase: boolean = false; - private inExpansionForm: boolean = false; + private expansionCaseStack = []; tokens: HtmlToken[] = []; errors: HtmlTokenError[] = []; @@ -169,10 +168,12 @@ class _HtmlTokenizer { } else if (this.peek === $EQ && this.tokenizeExpansionForms) { this._consumeExpansionCaseStart(); - } else if (this.peek === $RBRACE && this.inExpansionCase && this.tokenizeExpansionForms) { + } else if (this.peek === $RBRACE && this.isInExpansionCase() && + this.tokenizeExpansionForms) { this._consumeExpansionCaseEnd(); - } else if (this.peek === $RBRACE && !this.inExpansionCase && this.tokenizeExpansionForms) { + } else if (this.peek === $RBRACE && this.isInExpansionForm() && + this.tokenizeExpansionForms) { this._consumeExpansionFormEnd(); } else { @@ -551,7 +552,7 @@ class _HtmlTokenizer { this._requireCharCode($COMMA); this._attemptCharCodeUntilFn(isNotWhitespace); - this.inExpansionForm = true; + this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START); } private _consumeExpansionCaseStart() { @@ -567,7 +568,7 @@ class _HtmlTokenizer { this._endToken([], this._getLocation()); this._attemptCharCodeUntilFn(isNotWhitespace); - this.inExpansionCase = true; + this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START); } private _consumeExpansionCaseEnd() { @@ -576,7 +577,7 @@ class _HtmlTokenizer { this._endToken([], this._getLocation()); this._attemptCharCodeUntilFn(isNotWhitespace); - this.inExpansionCase = false; + this.expansionCaseStack.pop(); } private _consumeExpansionFormEnd() { @@ -584,7 +585,7 @@ class _HtmlTokenizer { this._requireCharCode($RBRACE); this._endToken([]); - this.inExpansionForm = false; + this.expansionCaseStack.pop(); } private _consumeText() { @@ -622,7 +623,9 @@ class _HtmlTokenizer { if (this.peek === $LT || this.peek === $EOF) return true; if (this.tokenizeExpansionForms) { if (isSpecialFormStart(this.peek, this.nextPeek)) return true; - if (this.peek === $RBRACE && !interpolation && this.inExpansionForm) return true; + if (this.peek === $RBRACE && !interpolation && + (this.isInExpansionCase() || this.isInExpansionForm())) + return true; } return false; } @@ -648,6 +651,18 @@ class _HtmlTokenizer { this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens); } } + + private isInExpansionCase(): boolean { + return this.expansionCaseStack.length > 0 && + this.expansionCaseStack[this.expansionCaseStack.length - 1] === + HtmlTokenType.EXPANSION_CASE_EXP_START; + } + + private isInExpansionForm(): boolean { + return this.expansionCaseStack.length > 0 && + this.expansionCaseStack[this.expansionCaseStack.length - 1] === + HtmlTokenType.EXPANSION_FORM_START; + } } function isNotWhitespace(code: number): boolean { diff --git a/modules/angular2/src/compiler/html_parser.ts b/modules/angular2/src/compiler/html_parser.ts index 0bd124cbe4..14437d0d9a 100644 --- a/modules/angular2/src/compiler/html_parser.ts +++ b/modules/angular2/src/compiler/html_parser.ts @@ -124,40 +124,9 @@ class TreeBuilder { // read = while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) { - let value = this._advance(); - - // read { - let exp = []; - if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) { - this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan, - `Invalid expansion form. Missing '{'.,`)); - return; - } - - // read until } - let start = this._advance(); - while (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_END) { - exp.push(this._advance()); - if (this.peek.type === HtmlTokenType.EOF) { - this.errors.push( - HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`)); - return; - } - } - let end = this._advance(); - exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan)); - - // parse everything in between { and } - let parsedExp = new TreeBuilder(exp).build(); - if (parsedExp.errors.length > 0) { - this.errors = this.errors.concat(parsedExp.errors); - return; - } - - let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end); - let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end); - cases.push(new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan, - value.sourceSpan, expSourceSpan)); + let expCase = this._parseExpansionCase(); + if (isBlank(expCase)) return; // error + cases.push(expCase); } // read the final } @@ -173,6 +142,80 @@ class TreeBuilder { mainSourceSpan, switchValue.sourceSpan)); } + private _parseExpansionCase(): HtmlExpansionCaseAst { + let value = this._advance(); + + // read { + if (this.peek.type !== HtmlTokenType.EXPANSION_CASE_EXP_START) { + this.errors.push(HtmlTreeError.create(null, this.peek.sourceSpan, + `Invalid expansion form. Missing '{'.,`)); + return null; + } + + // read until } + let start = this._advance(); + + let exp = this._collectExpansionExpTokens(start); + if (isBlank(exp)) return null; + + let end = this._advance(); + exp.push(new HtmlToken(HtmlTokenType.EOF, [], end.sourceSpan)); + + // parse everything in between { and } + let parsedExp = new TreeBuilder(exp).build(); + if (parsedExp.errors.length > 0) { + this.errors = this.errors.concat(parsedExp.errors); + return null; + } + + let sourceSpan = new ParseSourceSpan(value.sourceSpan.start, end.sourceSpan.end); + let expSourceSpan = new ParseSourceSpan(start.sourceSpan.start, end.sourceSpan.end); + return new HtmlExpansionCaseAst(value.parts[0], parsedExp.rootNodes, sourceSpan, + value.sourceSpan, expSourceSpan); + } + + private _collectExpansionExpTokens(start: HtmlToken): HtmlToken[] { + let exp = []; + let expansionFormStack = [HtmlTokenType.EXPANSION_CASE_EXP_START]; + + while (true) { + if (this.peek.type === HtmlTokenType.EXPANSION_FORM_START || + this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_START) { + expansionFormStack.push(this.peek.type); + } + + if (this.peek.type === HtmlTokenType.EXPANSION_CASE_EXP_END) { + if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_CASE_EXP_START)) { + expansionFormStack.pop(); + if (expansionFormStack.length == 0) return exp; + + } else { + this.errors.push( + HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`)); + return null; + } + } + + if (this.peek.type === HtmlTokenType.EXPANSION_FORM_END) { + if (lastOnStack(expansionFormStack, HtmlTokenType.EXPANSION_FORM_START)) { + expansionFormStack.pop(); + } else { + this.errors.push( + HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`)); + return null; + } + } + + if (this.peek.type === HtmlTokenType.EOF) { + this.errors.push( + HtmlTreeError.create(null, start.sourceSpan, `Invalid expansion form. Missing '}'.`)); + return null; + } + + exp.push(this._advance()); + } + } + private _consumeText(token: HtmlToken) { let text = token.parts[0]; if (text.length > 0 && text[0] == '\n') { @@ -321,3 +364,7 @@ function getElementFullName(prefix: string, localName: string, return mergeNsAndName(prefix, localName); } + +function lastOnStack(stack: any[], element: any): boolean { + return stack.length > 0 && stack[stack.length - 1] === element; +} \ No newline at end of file diff --git a/modules/angular2/src/compiler/template_parser.ts b/modules/angular2/src/compiler/template_parser.ts index 1c332773f3..0595beedd0 100644 --- a/modules/angular2/src/compiler/template_parser.ts +++ b/modules/angular2/src/compiler/template_parser.ts @@ -254,6 +254,10 @@ class TemplateParseVisitor implements HtmlAstVisitor { } } + visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; } + + visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; } + visitText(ast: HtmlTextAst, parent: ElementContext): any { var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR); var expr = this._parseInterpolation(ast.value, ast.sourceSpan); @@ -270,7 +274,7 @@ class TemplateParseVisitor implements HtmlAstVisitor { visitComment(ast: HtmlCommentAst, context: any): any { return null; } - visitElement(element: HtmlElementAst, component: ElementContext): any { + visitElement(element: HtmlElementAst, parent: ElementContext): any { var nodeName = element.name; var preparsedElement = preparseElement(element); if (preparsedElement.type === PreparsedElementType.SCRIPT || @@ -773,7 +777,6 @@ class NonBindableVisitor implements HtmlAstVisitor { return new TextAst(ast.value, ngContentIndex, ast.sourceSpan); } visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; } - visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; } } diff --git a/modules/angular2/src/i18n/expander.ts b/modules/angular2/src/i18n/expander.ts index 6a27dd6373..6fc8127a9f 100644 --- a/modules/angular2/src/i18n/expander.ts +++ b/modules/angular2/src/i18n/expander.ts @@ -12,6 +12,7 @@ import { import {BaseException} from 'angular2/src/facade/exceptions'; + /** * Expands special forms into elements. * @@ -35,7 +36,18 @@ import {BaseException} from 'angular2/src/facade/exceptions'; * * ``` */ -export class Expander implements HtmlAstVisitor { +export function expandNodes(nodes: HtmlAst[]): ExpansionResult { + let e = new _Expander(); + let n = htmlVisitAll(e, nodes); + return new ExpansionResult(n, e.expanded); +} + +export class ExpansionResult { + constructor(public nodes: HtmlAst[], public expanded: boolean) {} +} + +class _Expander implements HtmlAstVisitor { + expanded: boolean = false; constructor() {} visitElement(ast: HtmlElementAst, context: any): any { @@ -50,6 +62,7 @@ export class Expander implements HtmlAstVisitor { visitComment(ast: HtmlCommentAst, context: any): any { return ast; } visitExpansion(ast: HtmlExpansionAst, context: any): any { + this.expanded = true; return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast); } @@ -59,36 +72,44 @@ export class Expander implements HtmlAstVisitor { } function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst { - let children = ast.cases.map( - c => new HtmlElementAst( - `template`, - [ - new HtmlAttrAst("[ngPluralCase]", c.value, c.valueSourceSpan), - ], - [ - new HtmlElementAst( - `li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)], - c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan) - ], - c.sourceSpan, c.sourceSpan, c.sourceSpan)); + let children = ast.cases.map(c => { + let expansionResult = expandNodes(c.expression); + let i18nAttrs = expansionResult.expanded ? + [] : + [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)]; + + return new HtmlElementAst(`template`, + [ + new HtmlAttrAst("ngPluralCase", c.value, c.valueSourceSpan), + ], + [ + new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes, + c.sourceSpan, c.sourceSpan, c.sourceSpan) + ], + c.sourceSpan, c.sourceSpan, c.sourceSpan); + }); let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan); return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan); } function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst { - let children = ast.cases.map( - c => new HtmlElementAst( - `template`, - [ - new HtmlAttrAst("[ngSwitchWhen]", c.value, c.valueSourceSpan), - ], - [ - new HtmlElementAst( - `li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)], - c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan) - ], - c.sourceSpan, c.sourceSpan, c.sourceSpan)); + let children = ast.cases.map(c => { + let expansionResult = expandNodes(c.expression); + let i18nAttrs = expansionResult.expanded ? + [] : + [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)]; + + return new HtmlElementAst(`template`, + [ + new HtmlAttrAst("ngSwitchWhen", c.value, c.valueSourceSpan), + ], + [ + new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes, + c.sourceSpan, c.sourceSpan, c.sourceSpan) + ], + c.sourceSpan, c.sourceSpan, c.sourceSpan); + }); let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan); return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan, ast.sourceSpan); diff --git a/modules/angular2/src/i18n/i18n_html_parser.ts b/modules/angular2/src/i18n/i18n_html_parser.ts index 23b09735a0..bcf22061a4 100644 --- a/modules/angular2/src/i18n/i18n_html_parser.ts +++ b/modules/angular2/src/i18n/i18n_html_parser.ts @@ -16,7 +16,7 @@ import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang' import {BaseException} from 'angular2/src/facade/exceptions'; import {Parser} from 'angular2/src/compiler/expression_parser/parser'; import {Message, id} from './message'; -import {Expander} from './expander'; +import {expandNodes} from './expander'; import { messageFromAttribute, I18nError, @@ -126,21 +126,16 @@ export class I18nHtmlParser implements HtmlParser { parseExpansionForms: boolean = false): HtmlParseTreeResult { this.errors = []; - let res = this._htmlParser.parse(sourceContent, sourceUrl, parseExpansionForms); + let res = this._htmlParser.parse(sourceContent, sourceUrl, true); if (res.errors.length > 0) { return res; } else { - let nodes = this._recurse(this._expandNodes(res.rootNodes)); + let nodes = this._recurse(expandNodes(res.rootNodes).nodes); return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) : new HtmlParseTreeResult(nodes, []); } } - private _expandNodes(nodes: HtmlAst[]): HtmlAst[] { - let e = new Expander(); - return htmlVisitAll(e, nodes); - } - private _processI18nPart(p: Part): HtmlAst[] { try { return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p); @@ -155,9 +150,11 @@ export class I18nHtmlParser implements HtmlParser { } private _mergeI18Part(p: Part): HtmlAst[] { - let messageId = id(p.createMessage(this._parser)); + let message = p.createMessage(this._parser); + let messageId = id(message); if (!StringMapWrapper.contains(this._messages, messageId)) { - throw new I18nError(p.sourceSpan, `Cannot find message for id '${messageId}'`); + throw new I18nError( + p.sourceSpan, `Cannot find message for id '${messageId}', content '${message.content}'.`); } let parsedMessage = this._messages[messageId]; @@ -294,14 +291,17 @@ export class I18nHtmlParser implements HtmlParser { } let i18n = i18ns[0]; - let messageId = id(messageFromAttribute(this._parser, el, i18n)); + let message = messageFromAttribute(this._parser, el, i18n); + let messageId = id(message); 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}'`); + throw new I18nError( + attr.sourceSpan, + `Cannot find message for id '${messageId}', content '${message.content}'.`); } }); return res; diff --git a/modules/angular2/src/i18n/message_extractor.ts b/modules/angular2/src/i18n/message_extractor.ts index 7da4ee0f79..5630d44af7 100644 --- a/modules/angular2/src/i18n/message_extractor.ts +++ b/modules/angular2/src/i18n/message_extractor.ts @@ -13,7 +13,7 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {Parser} from 'angular2/src/compiler/expression_parser/parser'; import {Message, id} from './message'; -import {Expander} from './expander'; +import {expandNodes} from './expander'; import { I18nError, Part, @@ -126,16 +126,11 @@ export class MessageExtractor { if (res.errors.length > 0) { return new ExtractionResult([], res.errors); } else { - this._recurse(this._expandNodes(res.rootNodes)); + this._recurse(expandNodes(res.rootNodes).nodes); return new ExtractionResult(this.messages, this.errors); } } - private _expandNodes(nodes: HtmlAst[]): HtmlAst[] { - let e = new Expander(); - return htmlVisitAll(e, nodes); - } - private _extractMessagesFromPart(p: Part): void { if (p.hasI18n) { this.messages.push(p.createMessage(this._parser)); diff --git a/modules/angular2/test/compiler/html_lexer_spec.ts b/modules/angular2/test/compiler/html_lexer_spec.ts index aef9b09658..63f57d231d 100644 --- a/modules/angular2/test/compiler/html_lexer_spec.ts +++ b/modules/angular2/test/compiler/html_lexer_spec.ts @@ -646,6 +646,31 @@ export function main() { [HtmlTokenType.EOF] ]); }); + + it("should parse nested expansion forms", () => { + expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true)) + .toEqual([ + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + + [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'xx'], + [HtmlTokenType.RAW_TEXT, 'yy'], + [HtmlTokenType.EXPANSION_CASE_VALUE, 'x'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'one'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.TEXT, ' '], + + [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); + }); }); describe('errors', () => { diff --git a/modules/angular2/test/compiler/html_parser_spec.ts b/modules/angular2/test/compiler/html_parser_spec.ts index 403deb3d4f..945ab983ae 100644 --- a/modules/angular2/test/compiler/html_parser_spec.ts +++ b/modules/angular2/test/compiler/html_parser_spec.ts @@ -271,6 +271,27 @@ export function main() { .toEqual([[HtmlTextAst, 'One {{message}}', 0]]); }); + it("should parse out nested expansion forms", () => { + let parsed = parser.parse(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, + 'TestComp', true); + + + expect(humanizeDom(parsed)) + .toEqual([ + [HtmlExpansionAst, 'messages.length', 'plural'], + [HtmlExpansionCaseAst, '0'], + ]); + + let firstCase = (parsed.rootNodes[0]).cases[0]; + + expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))) + .toEqual([ + [HtmlExpansionAst, 'p.gender', 'gender'], + [HtmlExpansionCaseAst, 'm'], + [HtmlTextAst, ' ', 0], + ]); + }); + it("should error when expansion form is not closed", () => { let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true); expect(humanizeErrors(p.errors)) diff --git a/modules/angular2/test/i18n/i18n_html_parser_spec.ts b/modules/angular2/test/i18n/i18n_html_parser_spec.ts index 65ebe42450..0b4f2e93c7 100644 --- a/modules/angular2/test/i18n/i18n_html_parser_spec.ts +++ b/modules/angular2/test/i18n/i18n_html_parser_spec.ts @@ -188,7 +188,7 @@ export function main() { expect(res[1].sourceSpan.start.offset).toEqual(10); }); - it("should handle the plural special form", () => { + it("should handle the plural expansion form", () => { let translations: {[key: string]: string} = {}; translations[id(new Message('zerobold', "plural_0", null))] = 'ZEROBOLD'; @@ -200,7 +200,7 @@ export function main() { [HtmlElementAst, 'ul', 0], [HtmlAttrAst, '[ngPlural]', 'messages.length'], [HtmlElementAst, 'template', 1], - [HtmlAttrAst, '[ngPluralCase]', '0'], + [HtmlAttrAst, 'ngPluralCase', '0'], [HtmlElementAst, 'li', 2], [HtmlTextAst, 'ZERO', 3], [HtmlElementAst, 'b', 3], @@ -208,6 +208,31 @@ export function main() { ]); }); + it("should handle nested expansion forms", () => { + let translations: {[key: string]: string} = {}; + translations[id(new Message('m', "gender_m", null))] = 'M'; + + let res = parse(`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, translations); + + expect(humanizeDom(res)) + .toEqual([ + [HtmlElementAst, 'ul', 0], + [HtmlAttrAst, '[ngPlural]', 'messages.length'], + [HtmlElementAst, 'template', 1], + [HtmlAttrAst, 'ngPluralCase', '0'], + [HtmlElementAst, 'li', 2], + + [HtmlElementAst, 'ul', 3], + [HtmlAttrAst, '[ngSwitch]', 'p.gender'], + [HtmlElementAst, 'template', 4], + [HtmlAttrAst, 'ngSwitchWhen', 'm'], + [HtmlElementAst, 'li', 5], + [HtmlTextAst, 'M', 6], + + [HtmlTextAst, ' ', 3] + ]); + }); + it("should correctly set source code positions", () => { let translations: {[key: string]: string} = {}; translations[id(new Message('bold', "plural_0", null))] = @@ -258,7 +283,7 @@ export function main() { [HtmlElementAst, 'ul', 0], [HtmlAttrAst, '[ngSwitch]', 'person.gender'], [HtmlElementAst, 'template', 1], - [HtmlAttrAst, '[ngSwitchWhen]', 'male'], + [HtmlAttrAst, 'ngSwitchWhen', 'male'], [HtmlElementAst, 'li', 2], [HtmlTextAst, 'M', 3], ]); @@ -273,13 +298,13 @@ export function main() { it("should error when no matching message (attr)", () => { let mid = id(new Message("some message", null, null)); expect(humanizeErrors(parse("
", {}).errors)) - .toEqual([`Cannot find message for id '${mid}'`]); + .toEqual([`Cannot find message for id '${mid}', content 'some message'.`]); }); it("should error when no matching message (text)", () => { let mid = id(new Message("some message", null, null)); expect(humanizeErrors(parse("
some message
", {}).errors)) - .toEqual([`Cannot find message for id '${mid}'`]); + .toEqual([`Cannot find message for id '${mid}', content 'some message'.`]); }); it("should error when a non-placeholder element appears in translation", () => {