From 3f6e107eb83e6a4a5690fb2977ffde3fade54874 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Thu, 9 Jun 2016 14:53:03 -0700 Subject: [PATCH] feat(I18N): generate error on unknown cases fixes #9094 --- modules/@angular/compiler/src/html_lexer.ts | 39 +++-- .../@angular/compiler/src/i18n/expander.ts | 27 ++- .../compiler/src/i18n/i18n_html_parser.ts | 5 +- .../compiler/src/i18n/message_extractor.ts | 13 +- .../@angular/compiler/test/html_lexer_spec.ts | 119 +++++-------- .../compiler/test/html_parser_spec.ts | 49 +++--- .../test/i18n/i18n_html_parser_spec.ts | 161 +++++++++--------- .../test/i18n/message_extractor_spec.ts | 26 ++- 8 files changed, 206 insertions(+), 233 deletions(-) diff --git a/modules/@angular/compiler/src/html_lexer.ts b/modules/@angular/compiler/src/html_lexer.ts index 4263b6d6f141c0..2a235794b1505f 100644 --- a/modules/@angular/compiler/src/html_lexer.ts +++ b/modules/@angular/compiler/src/html_lexer.ts @@ -155,18 +155,21 @@ class _HtmlTokenizer { } else { this._consumeTagOpen(start); } - } else if (_isExpansionFormStart(this._peek, this._nextPeek) && this.tokenizeExpansionForms) { + } else if ( + _isExpansionFormStart(this._peek, this._nextPeek) && this.tokenizeExpansionForms) { this._consumeExpansionFormStart(); - } else if ((this._peek === $EQ || (isAsciiLetter(this._peek) && this._isInExpansionForm()) && this.tokenizeExpansionForms)) { + } else if ((this._peek === $EQ || + (isAsciiLetter(this._peek) && this._isInExpansionForm()) && + this.tokenizeExpansionForms)) { this._consumeExpansionCaseStart(); - } else if (this._peek === $RBRACE && this._isInExpansionCase() && - this.tokenizeExpansionForms) { + } else if ( + this._peek === $RBRACE && this._isInExpansionCase() && this.tokenizeExpansionForms) { this._consumeExpansionCaseEnd(); - } else if (this._peek === $RBRACE && this._isInExpansionForm() && - this.tokenizeExpansionForms) { + } else if ( + this._peek === $RBRACE && this._isInExpansionForm() && this.tokenizeExpansionForms) { this._consumeExpansionFormEnd(); } else { @@ -211,8 +214,8 @@ class _HtmlTokenizer { if (isBlank(end)) { end = this._getLocation(); } - var token = new HtmlToken(this._currentTokenType, parts, - new ParseSourceSpan(this._currentTokenStart, end)); + var token = new HtmlToken( + this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end)); this.tokens.push(token); this._currentTokenStart = null; this._currentTokenType = null; @@ -237,9 +240,11 @@ class _HtmlTokenizer { this._column++; } this._index++; - this._peek = this._index >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index); - this._nextPeek = - this._index + 1 >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index + 1); + this._peek = + this._index >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index); + this._nextPeek = this._index + 1 >= this._length ? + $EOF : + StringWrapper.charCodeAt(this._input, this._index + 1); } private _attemptCharCode(charCode: number): boolean { @@ -261,8 +266,8 @@ class _HtmlTokenizer { private _requireCharCode(charCode: number) { var location = this._getLocation(); if (!this._attemptCharCode(charCode)) { - throw this._createError(unexpectedCharacterErrorMsg(this._peek), - this._getSpan(location, location)); + throw this._createError( + unexpectedCharacterErrorMsg(this._peek), this._getSpan(location, location)); } } @@ -653,14 +658,14 @@ class _HtmlTokenizer { private _isInExpansionCase(): boolean { return this._expansionCaseStack.length > 0 && - this._expansionCaseStack[this._expansionCaseStack.length - 1] === - HtmlTokenType.EXPANSION_CASE_EXP_START; + 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; + this._expansionCaseStack[this._expansionCaseStack.length - 1] === + HtmlTokenType.EXPANSION_FORM_START; } } diff --git a/modules/@angular/compiler/src/i18n/expander.ts b/modules/@angular/compiler/src/i18n/expander.ts index ed28256ef4b6e4..612da747f22d14 100644 --- a/modules/@angular/compiler/src/i18n/expander.ts +++ b/modules/@angular/compiler/src/i18n/expander.ts @@ -1,7 +1,10 @@ import {BaseException} from '../facade/exceptions'; import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast'; +import {ParseError} from '../parse_util'; +import {I18nError} from './shared'; - +// http://cldr.unicode.org/index/cldr-spec/plural-rules +const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other']; /** * Expands special forms into elements. @@ -20,25 +23,25 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, Ht * * ``` * * ``` */ export function expandNodes(nodes: HtmlAst[]): ExpansionResult { let e = new _Expander(); let n = htmlVisitAll(e, nodes); - return new ExpansionResult(n, e.expanded); + return new ExpansionResult(n, e.expanded, e.errors); } export class ExpansionResult { - constructor(public nodes: HtmlAst[], public expanded: boolean) {} + constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {} } class _Expander implements HtmlAstVisitor { expanded: boolean = false; - constructor() {} + errors: ParseError[] = []; visitElement(ast: HtmlElementAst, context: any): any { return new HtmlElementAst( @@ -54,7 +57,7 @@ class _Expander implements HtmlAstVisitor { visitExpansion(ast: HtmlExpansionAst, context: any): any { this.expanded = true; - return ast.type == 'plural' ? _expandPluralForm(ast) : _expandDefaultForm(ast); + return ast.type == 'plural' ? _expandPluralForm(ast, this.errors) : _expandDefaultForm(ast); } visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { @@ -62,9 +65,15 @@ class _Expander implements HtmlAstVisitor { } } -function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst { +function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst { let children = ast.cases.map(c => { + if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) { + errors.push(new I18nError( + c.valueSourceSpan, + `Plural cases should be "=" or one of ${PLURAL_CASES.join(", ")}`)); + } let expansionResult = expandNodes(c.expression); + expansionResult.errors.forEach(e => errors.push(e)); let i18nAttrs = expansionResult.expanded ? [] : [new HtmlAttrAst('i18n', `${ast.type}_${c.value}`, c.valueSourceSpan)]; diff --git a/modules/@angular/compiler/src/i18n/i18n_html_parser.ts b/modules/@angular/compiler/src/i18n/i18n_html_parser.ts index 1fe5df99e2ee5d..7091c42079ba28 100644 --- a/modules/@angular/compiler/src/i18n/i18n_html_parser.ts +++ b/modules/@angular/compiler/src/i18n/i18n_html_parser.ts @@ -111,7 +111,10 @@ export class I18nHtmlParser implements HtmlParser { if (res.errors.length > 0) { return res; } else { - let nodes = this._recurse(expandNodes(res.rootNodes).nodes); + let expanded = expandNodes(res.rootNodes); + let nodes = this._recurse(expanded.nodes); + this.errors = this.errors.concat(expanded.errors); + return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) : new HtmlParseTreeResult(nodes, []); } diff --git a/modules/@angular/compiler/src/i18n/message_extractor.ts b/modules/@angular/compiler/src/i18n/message_extractor.ts index 4ef2aeff44764c..ad2559cbe11b32 100644 --- a/modules/@angular/compiler/src/i18n/message_extractor.ts +++ b/modules/@angular/compiler/src/i18n/message_extractor.ts @@ -19,14 +19,6 @@ export class ExtractionResult { /** * Removes duplicate messages. - * - * E.g. - * - * ``` - * var m = [new Message("message", "meaning", "desc1"), new Message("message", "meaning", - * "desc2")]; - * expect(removeDuplicates(m)).toEqual([new Message("message", "meaning", "desc1")]); - * ``` */ export function removeDuplicates(messages: Message[]): Message[] { let uniq: {[key: string]: Message} = {}; @@ -113,8 +105,9 @@ export class MessageExtractor { if (res.errors.length > 0) { return new ExtractionResult([], res.errors); } else { - this._recurse(expandNodes(res.rootNodes).nodes); - return new ExtractionResult(this.messages, this.errors); + let expanded = expandNodes(res.rootNodes); + this._recurse(expanded.nodes); + return new ExtractionResult(this.messages, this.errors.concat(expanded.errors)); } } diff --git a/modules/@angular/compiler/test/html_lexer_spec.ts b/modules/@angular/compiler/test/html_lexer_spec.ts index b7f5fa0b843c28..b8acecb93f53ab 100644 --- a/modules/@angular/compiler/test/html_lexer_spec.ts +++ b/modules/@angular/compiler/test/html_lexer_spec.ts @@ -485,98 +485,65 @@ export function main() { }); - describe("expansion forms", () => { - it("should parse an expansion form", () => { + describe('expansion forms', () => { + it('should parse an expansion form', () => { expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', 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.TEXT, 'four'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_CASE_VALUE, '=5'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], - [HtmlTokenType.TEXT, 'five'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], - [HtmlTokenType.TEXT, 'bar'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'], + [HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_CASE_VALUE, '=5'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'five'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'], + [HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'bar'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], [HtmlTokenType.EOF] ]); }); - it("should parse an expansion form with text elements surrounding it", () => { - expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)) - .toEqual([ - [HtmlTokenType.TEXT, "before"], - [HtmlTokenType.EXPANSION_FORM_START], - [HtmlTokenType.RAW_TEXT, 'one.two'], - [HtmlTokenType.RAW_TEXT, 'three'], - [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], - [HtmlTokenType.EXPANSION_CASE_EXP_START], - [HtmlTokenType.TEXT, 'four'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.TEXT, "after"], - [HtmlTokenType.EOF] - ]); + it('should parse an expansion form with text elements surrounding it', () => { + expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([ + [HtmlTokenType.TEXT, 'before'], [HtmlTokenType.EXPANSION_FORM_START], + [HtmlTokenType.RAW_TEXT, 'one.two'], [HtmlTokenType.RAW_TEXT, 'three'], + [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], [HtmlTokenType.EXPANSION_CASE_EXP_START], + [HtmlTokenType.TEXT, 'four'], [HtmlTokenType.EXPANSION_CASE_EXP_END], + [HtmlTokenType.EXPANSION_FORM_END], [HtmlTokenType.TEXT, 'after'], [HtmlTokenType.EOF] + ]); }); - it("should parse an expansion forms with elements in it", () => { - expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four a}}', 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.TEXT, 'four '], - [HtmlTokenType.TAG_OPEN_START, null, 'b'], - [HtmlTokenType.TAG_OPEN_END], - [HtmlTokenType.TEXT, 'a'], - [HtmlTokenType.TAG_CLOSE, null, 'b'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.EOF] - ]); + it('should parse an expansion forms with elements in it', () => { + expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four a}}', 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.TEXT, 'four '], + [HtmlTokenType.TAG_OPEN_START, null, 'b'], [HtmlTokenType.TAG_OPEN_END], + [HtmlTokenType.TEXT, 'a'], [HtmlTokenType.TAG_CLOSE, null, 'b'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); }); - it("should parse an expansion forms with interpolation in it", () => { - expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', 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.TEXT, 'four {{a}}'], - [HtmlTokenType.EXPANSION_CASE_EXP_END], - [HtmlTokenType.EXPANSION_FORM_END], - [HtmlTokenType.EOF] - ]); + it('should parse an expansion forms with interpolation in it', () => { + expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', 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.TEXT, 'four {{a}}'], + [HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END], + [HtmlTokenType.EOF] + ]); }); - it("should parse nested expansion forms", () => { + 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_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.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], diff --git a/modules/@angular/compiler/test/html_parser_spec.ts b/modules/@angular/compiler/test/html_parser_spec.ts index 630c0725c95256..e1dd19c955d91a 100644 --- a/modules/@angular/compiler/test/html_parser_spec.ts +++ b/modules/@angular/compiler/test/html_parser_spec.ts @@ -1,15 +1,9 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '@angular/compiler/src/html_ast'; +import {HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '@angular/compiler/src/html_ast'; import {HtmlTokenType} from '@angular/compiler/src/html_lexer'; -import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from '@angular/compiler/src/html_parser'; -import { - HtmlElementAst, - HtmlAttrAst, - HtmlTextAst, - HtmlCommentAst, - HtmlExpansionAst, - HtmlExpansionCaseAst -} from '@angular/compiler/src/html_ast'; +import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '@angular/compiler/src/html_parser'; import {ParseError} from '@angular/compiler/src/parse_util'; + import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils'; export function main() { @@ -238,15 +232,14 @@ export function main() { `
before{messages.length, plural, =0 {You have no messages} =1 {One {{message}}}}after
`, 'TestComp', true); - expect(humanizeDom(parsed)) - .toEqual([ - [HtmlElementAst, 'div', 0], - [HtmlTextAst, 'before', 1], - [HtmlExpansionAst, 'messages.length', 'plural'], - [HtmlExpansionCaseAst, '=0'], - [HtmlExpansionCaseAst, '=1'], - [HtmlTextAst, 'after', 1], - ]); + expect(humanizeDom(parsed)).toEqual([ + [HtmlElementAst, 'div', 0], + [HtmlTextAst, 'before', 1], + [HtmlExpansionAst, 'messages.length', 'plural'], + [HtmlExpansionCaseAst, '=0'], + [HtmlExpansionCaseAst, '=1'], + [HtmlTextAst, 'after', 1], + ]); let cases = (parsed.rootNodes[0]).children[1].cases; expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, []))).toEqual([ @@ -263,20 +256,18 @@ export function main() { 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'], - ]); + 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], - ]); + expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))).toEqual([ + [HtmlExpansionAst, 'p.gender', 'gender'], + [HtmlExpansionCaseAst, '=m'], + [HtmlTextAst, ' ', 0], + ]); }); it('should error when expansion form is not closed', () => { diff --git a/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts b/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts index e1c2bf0fb12d61..1a265eeb8d2231 100644 --- a/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts +++ b/modules/@angular/compiler/test/i18n/i18n_html_parser_spec.ts @@ -21,8 +21,7 @@ export function main() { let msgs = ''; StringMapWrapper.forEach( - messages, (v: any /** TODO #9100 */, k: any /** TODO #9100 */) => msgs += - `${v}`); + messages, (v: string, k: string) => msgs += `${v}`); let res = deserializeXmb(`${msgs}`, 'someUrl'); return new I18nHtmlParser( @@ -175,52 +174,45 @@ export function main() { it('should handle the plural expansion form', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('zerobold', "plural_=0", null))] = + translations[id(new Message('zerobold', 'plural_=0', null))] = 'ZEROBOLD'; let res = parse(`{messages.length, plural,=0 {zerobold}}`, translations); - expect(humanizeDom(res)) - .toEqual([ - [HtmlElementAst, 'ul', 0], - [HtmlAttrAst, '[ngPlural]', 'messages.length'], - [HtmlElementAst, 'template', 1], - [HtmlAttrAst, 'ngPluralCase', '=0'], - [HtmlElementAst, 'li', 2], - [HtmlTextAst, 'ZERO', 3], - [HtmlElementAst, 'b', 3], - [HtmlTextAst, 'BOLD', 4], - ]); + expect(humanizeDom(res)).toEqual([ + [HtmlElementAst, 'ul', 0], + [HtmlAttrAst, '[ngPlural]', 'messages.length'], + [HtmlElementAst, 'template', 1], + [HtmlAttrAst, 'ngPluralCase', '=0'], + [HtmlElementAst, 'li', 2], + [HtmlTextAst, 'ZERO', 3], + [HtmlElementAst, 'b', 3], + [HtmlTextAst, 'BOLD', 4], + ]); }); it('should handle nested expansion forms', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('m', "gender_=m", null))] = 'M'; + 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] - ]); + 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))] = + translations[id(new Message('bold', 'plural_=0', null))] = 'BOLD'; let nodes = parse(`{messages.length, plural,=0 {bold}}`, translations).rootNodes; @@ -259,19 +251,18 @@ export function main() { it('should handle other special forms', () => { let translations: {[key: string]: string} = {}; - translations[id(new Message('m', "gender_=male", null))] = 'M'; + translations[id(new Message('m', 'gender_=male', null))] = 'M'; let res = parse(`{person.gender, gender,=male {m}}`, translations); - expect(humanizeDom(res)) - .toEqual([ - [HtmlElementAst, 'ul', 0], - [HtmlAttrAst, '[ngSwitch]', 'person.gender'], - [HtmlElementAst, 'template', 1], - [HtmlAttrAst, 'ngSwitchWhen', '=male'], - [HtmlElementAst, 'li', 2], - [HtmlTextAst, 'M', 3], - ]); + expect(humanizeDom(res)).toEqual([ + [HtmlElementAst, 'ul', 0], + [HtmlAttrAst, '[ngSwitch]', 'person.gender'], + [HtmlElementAst, 'template', 1], + [HtmlAttrAst, 'ngSwitchWhen', '=male'], + [HtmlElementAst, 'li', 2], + [HtmlTextAst, 'M', 3], + ]); }); describe('errors', () => { @@ -321,45 +312,51 @@ export function main() { .toEqual(['Invalid interpolation name \'99\'']); }); - describe('implicit translation', () => { - it('should support attributes', () => { - let translations: {[key: string]: string} = {}; - translations[id(new Message('some message', null, null))] = 'another message'; - - expect(humanizeDom(parse('', translations, [], { - 'i18n-el': ['value'] - }))).toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]); - }); - - it('should support attributes with meaning and description', () => { - let translations: {[key: string]: string} = {}; - translations[id(new Message('some message', 'meaning', 'description'))] = - 'another message'; - - expect( - humanizeDom(parse( - '', - translations, [], {'i18n-el': ['value']}))) - .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]); - }); - - it('should support elements', () => { - let translations: {[key: string]: string} = {}; - translations[id(new Message('message', null, null))] = 'another message'; - - expect(humanizeDom(parse('message', translations, ['i18n-el']))) - .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]); - }); - - it('should support elements with meaning and description', () => { - let translations: {[key: string]: string} = {}; - translations[id(new Message('message', 'meaning', 'description'))] = 'another message'; - - expect(humanizeDom(parse( - 'message', translations, - ['i18n-el']))) - .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]); - }); + it('should error on unknown plural cases', () => { + let mid = id(new Message('-', 'plural_unknown', null)); + expect(humanizeErrors(parse('{n, plural, unknown {-}}', {mid: ''}).errors)).toEqual([ + `Cannot find message for id '${mid}', content '-'.`, + `Plural cases should be "=" or one of zero, one, two, few, many, other`, + ]); + }); + }); + + describe('implicit translation', () => { + it('should support attributes', () => { + let translations: {[key: string]: string} = {}; + translations[id(new Message('some message', null, null))] = 'another message'; + + expect(humanizeDom(parse('', translations, [], { + 'i18n-el': ['value'] + }))).toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]); + }); + + it('should support attributes with meaning and description', () => { + let translations: {[key: string]: string} = {}; + translations[id(new Message('some message', 'meaning', 'description'))] = 'another message'; + + expect(humanizeDom(parse( + '', + translations, [], {'i18n-el': ['value']}))) + .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]); + }); + + it('should support elements', () => { + let translations: {[key: string]: string} = {}; + translations[id(new Message('message', null, null))] = 'another message'; + + expect(humanizeDom(parse('message', translations, ['i18n-el']))) + .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]); + }); + + it('should support elements with meaning and description', () => { + let translations: {[key: string]: string} = {}; + translations[id(new Message('message', 'meaning', 'description'))] = 'another message'; + + expect(humanizeDom(parse( + 'message', translations, + ['i18n-el']))) + .toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]); }); }); }); diff --git a/modules/@angular/compiler/test/i18n/message_extractor_spec.ts b/modules/@angular/compiler/test/i18n/message_extractor_spec.ts index 4b0cbb45d8b7f7..438a5b02495dfc 100644 --- a/modules/@angular/compiler/test/i18n/message_extractor_spec.ts +++ b/modules/@angular/compiler/test/i18n/message_extractor_spec.ts @@ -151,8 +151,9 @@ export function main() { ]); }); - it("should extract messages from expansion forms", () => { - let res = extractor.extract(` + it('should extract messages from expansion forms', () => { + let res = extractor.extract( + `
{messages.length, plural, =0 {You have no messages} @@ -160,14 +161,13 @@ export function main() { other {You have messages} }
`, - "someurl"); + 'someurl'); - expect(res.messages) - .toEqual([ - new Message('You have no messages', "plural_=0", null), - new Message('You have one message', "plural_=1", null), - new Message('You have messages', "plural_other", null), - ]); + expect(res.messages).toEqual([ + new Message('You have no messages', 'plural_=0', null), + new Message('You have one message', 'plural_=1', null), + new Message('You have messages', 'plural_other', null), + ]); }); it('should remove duplicate messages', () => { @@ -235,6 +235,14 @@ export function main() { expect(res.errors.length).toEqual(1); expect(res.errors[0].msg).toEqual('Unexpected character "s"'); }); + + it('should return parse errors on unknown plural cases', () => { + let res = extractor.extract('{n, plural, unknown {-}}', 'someUrl'); + expect(res.errors.length).toEqual(1); + expect(res.errors[0].msg) + .toEqual( + 'Plural cases should be "=" or one of zero, one, two, few, many, other'); + }); }); }); }