Skip to content

Commit 5267115

Browse files
committed
feat(I18N): generate error on unknown cases
fixes #9094
1 parent 43148d8 commit 5267115

File tree

8 files changed

+201
-232
lines changed

8 files changed

+201
-232
lines changed

modules/@angular/compiler/src/html_lexer.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ class _HtmlTokenizer {
164164
this.tokenizeExpansionForms) {
165165
this._consumeExpansionCaseStart();
166166

167-
} else if (this._peek === $RBRACE && this._isInExpansionCase() &&
168-
this.tokenizeExpansionForms) {
167+
} else if (
168+
this._peek === $RBRACE && this._isInExpansionCase() && this.tokenizeExpansionForms) {
169169
this._consumeExpansionCaseEnd();
170170

171-
} else if (this._peek === $RBRACE && this._isInExpansionForm() &&
172-
this.tokenizeExpansionForms) {
171+
} else if (
172+
this._peek === $RBRACE && this._isInExpansionForm() && this.tokenizeExpansionForms) {
173173
this._consumeExpansionFormEnd();
174174

175175
} else {
@@ -214,8 +214,8 @@ class _HtmlTokenizer {
214214
if (isBlank(end)) {
215215
end = this._getLocation();
216216
}
217-
var token = new HtmlToken(this._currentTokenType, parts,
218-
new ParseSourceSpan(this._currentTokenStart, end));
217+
var token = new HtmlToken(
218+
this._currentTokenType, parts, new ParseSourceSpan(this._currentTokenStart, end));
219219
this.tokens.push(token);
220220
this._currentTokenStart = null;
221221
this._currentTokenType = null;
@@ -240,9 +240,11 @@ class _HtmlTokenizer {
240240
this._column++;
241241
}
242242
this._index++;
243-
this._peek = this._index >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index);
244-
this._nextPeek =
245-
this._index + 1 >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index + 1);
243+
this._peek =
244+
this._index >= this._length ? $EOF : StringWrapper.charCodeAt(this._input, this._index);
245+
this._nextPeek = this._index + 1 >= this._length ?
246+
$EOF :
247+
StringWrapper.charCodeAt(this._input, this._index + 1);
246248
}
247249

248250
private _attemptCharCode(charCode: number): boolean {
@@ -264,8 +266,8 @@ class _HtmlTokenizer {
264266
private _requireCharCode(charCode: number) {
265267
var location = this._getLocation();
266268
if (!this._attemptCharCode(charCode)) {
267-
throw this._createError(unexpectedCharacterErrorMsg(this._peek),
268-
this._getSpan(location, location));
269+
throw this._createError(
270+
unexpectedCharacterErrorMsg(this._peek), this._getSpan(location, location));
269271
}
270272
}
271273

@@ -656,14 +658,14 @@ class _HtmlTokenizer {
656658

657659
private _isInExpansionCase(): boolean {
658660
return this._expansionCaseStack.length > 0 &&
659-
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
660-
HtmlTokenType.EXPANSION_CASE_EXP_START;
661+
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
662+
HtmlTokenType.EXPANSION_CASE_EXP_START;
661663
}
662664

663665
private _isInExpansionForm(): boolean {
664666
return this._expansionCaseStack.length > 0 &&
665-
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
666-
HtmlTokenType.EXPANSION_FORM_START;
667+
this._expansionCaseStack[this._expansionCaseStack.length - 1] ===
668+
HtmlTokenType.EXPANSION_FORM_START;
667669
}
668670
}
669671

modules/@angular/compiler/src/i18n/expander.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import {BaseException} from '../facade/exceptions';
22
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast';
3+
import {ParseError} from '../parse_util';
4+
import {I18nError} from './shared';
35

4-
6+
// http://cldr.unicode.org/index/cldr-spec/plural-rules
7+
const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
58

69
/**
710
* Expands special forms into elements.
@@ -20,25 +23,25 @@ import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, Ht
2023
*
2124
* ```
2225
* <ul [ngPlural]="messages.length">
23-
* <template [ngPluralCase]="0"><li i18n="plural_0">zero</li></template>
24-
* <template [ngPluralCase]="1"><li i18n="plural_1">one</li></template>
25-
* <template [ngPluralCase]="other"><li i18n="plural_other">more than one</li></template>
26+
* <template [ngPluralCase]="'=0'"><li i18n="plural_=0">zero</li></template>
27+
* <template [ngPluralCase]="'=1'"><li i18n="plural_=1">one</li></template>
28+
* <template [ngPluralCase]="'other'"><li i18n="plural_other">more than one</li></template>
2629
* </ul>
2730
* ```
2831
*/
2932
export function expandNodes(nodes: HtmlAst[]): ExpansionResult {
3033
let e = new _Expander();
3134
let n = htmlVisitAll(e, nodes);
32-
return new ExpansionResult(n, e.expanded);
35+
return new ExpansionResult(n, e.expanded, e.errors);
3336
}
3437

3538
export class ExpansionResult {
36-
constructor(public nodes: HtmlAst[], public expanded: boolean) {}
39+
constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {}
3740
}
3841

3942
class _Expander implements HtmlAstVisitor {
4043
expanded: boolean = false;
41-
constructor() {}
44+
errors: ParseError[] = [];
4245

4346
visitElement(ast: HtmlElementAst, context: any): any {
4447
return new HtmlElementAst(
@@ -54,17 +57,23 @@ class _Expander implements HtmlAstVisitor {
5457

5558
visitExpansion(ast: HtmlExpansionAst, context: any): any {
5659
this.expanded = true;
57-
return ast.type == 'plural' ? _expandPluralForm(ast) : _expandDefaultForm(ast);
60+
return ast.type == 'plural' ? _expandPluralForm(ast, this.errors) : _expandDefaultForm(ast);
5861
}
5962

6063
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
6164
throw new BaseException('Should not be reached');
6265
}
6366
}
6467

65-
function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
68+
function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlElementAst {
6669
let children = ast.cases.map(c => {
70+
if (PLURAL_CASES.indexOf(c.value) == -1 && !c.value.match(/^=\d+$/)) {
71+
errors.push(new I18nError(
72+
c.valueSourceSpan,
73+
`Plural cases should be "=<number>" or one of ${PLURAL_CASES.join(", ")}`));
74+
}
6775
let expansionResult = expandNodes(c.expression);
76+
expansionResult.errors.forEach(e => errors.push(e));
6877
let i18nAttrs = expansionResult.expanded ?
6978
[] :
7079
[new HtmlAttrAst('i18n', `${ast.type}_${c.value}`, c.valueSourceSpan)];

modules/@angular/compiler/src/i18n/i18n_html_parser.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export class I18nHtmlParser implements HtmlParser {
111111
if (res.errors.length > 0) {
112112
return res;
113113
} else {
114-
let nodes = this._recurse(expandNodes(res.rootNodes).nodes);
114+
let expanded = expandNodes(res.rootNodes);
115+
let nodes = this._recurse(expanded.nodes);
116+
this.errors = this.errors.concat(expanded.errors);
117+
115118
return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
116119
new HtmlParseTreeResult(nodes, []);
117120
}

modules/@angular/compiler/src/i18n/message_extractor.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@ export class ExtractionResult {
1919

2020
/**
2121
* Removes duplicate messages.
22-
*
23-
* E.g.
24-
*
25-
* ```
26-
* var m = [new Message("message", "meaning", "desc1"), new Message("message", "meaning",
27-
* "desc2")];
28-
* expect(removeDuplicates(m)).toEqual([new Message("message", "meaning", "desc1")]);
29-
* ```
3022
*/
3123
export function removeDuplicates(messages: Message[]): Message[] {
3224
let uniq: {[key: string]: Message} = {};
@@ -113,8 +105,9 @@ export class MessageExtractor {
113105
if (res.errors.length > 0) {
114106
return new ExtractionResult([], res.errors);
115107
} else {
116-
this._recurse(expandNodes(res.rootNodes).nodes);
117-
return new ExtractionResult(this.messages, this.errors);
108+
let expanded = expandNodes(res.rootNodes);
109+
this._recurse(expanded.nodes);
110+
return new ExtractionResult(this.messages, this.errors.concat(expanded.errors));
118111
}
119112
}
120113

modules/@angular/compiler/test/html_lexer_spec.ts

Lines changed: 43 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -485,98 +485,65 @@ export function main() {
485485

486486
});
487487

488-
describe("expansion forms", () => {
489-
it("should parse an expansion form", () => {
488+
describe('expansion forms', () => {
489+
it('should parse an expansion form', () => {
490490
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four} =5 {five} foo {bar} }', true))
491491
.toEqual([
492-
[HtmlTokenType.EXPANSION_FORM_START],
493-
[HtmlTokenType.RAW_TEXT, 'one.two'],
494-
[HtmlTokenType.RAW_TEXT, 'three'],
495-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
496-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
497-
[HtmlTokenType.TEXT, 'four'],
498-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
499-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=5'],
500-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
501-
[HtmlTokenType.TEXT, 'five'],
502-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
503-
[HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'],
504-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
505-
[HtmlTokenType.TEXT, 'bar'],
506-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
507-
[HtmlTokenType.EXPANSION_FORM_END],
492+
[HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'],
493+
[HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
494+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four'],
495+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_CASE_VALUE, '=5'],
496+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'five'],
497+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_CASE_VALUE, 'foo'],
498+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'bar'],
499+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END],
508500
[HtmlTokenType.EOF]
509501
]);
510502
});
511503

512-
it("should parse an expansion form with text elements surrounding it", () => {
513-
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true))
514-
.toEqual([
515-
[HtmlTokenType.TEXT, "before"],
516-
[HtmlTokenType.EXPANSION_FORM_START],
517-
[HtmlTokenType.RAW_TEXT, 'one.two'],
518-
[HtmlTokenType.RAW_TEXT, 'three'],
519-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
520-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
521-
[HtmlTokenType.TEXT, 'four'],
522-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
523-
[HtmlTokenType.EXPANSION_FORM_END],
524-
[HtmlTokenType.TEXT, "after"],
525-
[HtmlTokenType.EOF]
526-
]);
504+
it('should parse an expansion form with text elements surrounding it', () => {
505+
expect(tokenizeAndHumanizeParts('before{one.two, three, =4 {four}}after', true)).toEqual([
506+
[HtmlTokenType.TEXT, 'before'], [HtmlTokenType.EXPANSION_FORM_START],
507+
[HtmlTokenType.RAW_TEXT, 'one.two'], [HtmlTokenType.RAW_TEXT, 'three'],
508+
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'], [HtmlTokenType.EXPANSION_CASE_EXP_START],
509+
[HtmlTokenType.TEXT, 'four'], [HtmlTokenType.EXPANSION_CASE_EXP_END],
510+
[HtmlTokenType.EXPANSION_FORM_END], [HtmlTokenType.TEXT, 'after'], [HtmlTokenType.EOF]
511+
]);
527512
});
528513

529-
it("should parse an expansion forms with elements in it", () => {
530-
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true))
531-
.toEqual([
532-
[HtmlTokenType.EXPANSION_FORM_START],
533-
[HtmlTokenType.RAW_TEXT, 'one.two'],
534-
[HtmlTokenType.RAW_TEXT, 'three'],
535-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
536-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
537-
[HtmlTokenType.TEXT, 'four '],
538-
[HtmlTokenType.TAG_OPEN_START, null, 'b'],
539-
[HtmlTokenType.TAG_OPEN_END],
540-
[HtmlTokenType.TEXT, 'a'],
541-
[HtmlTokenType.TAG_CLOSE, null, 'b'],
542-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
543-
[HtmlTokenType.EXPANSION_FORM_END],
544-
[HtmlTokenType.EOF]
545-
]);
514+
it('should parse an expansion forms with elements in it', () => {
515+
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four <b>a</b>}}', true)).toEqual([
516+
[HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'],
517+
[HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
518+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four '],
519+
[HtmlTokenType.TAG_OPEN_START, null, 'b'], [HtmlTokenType.TAG_OPEN_END],
520+
[HtmlTokenType.TEXT, 'a'], [HtmlTokenType.TAG_CLOSE, null, 'b'],
521+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END],
522+
[HtmlTokenType.EOF]
523+
]);
546524
});
547525

548-
it("should parse an expansion forms with interpolation in it", () => {
549-
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true))
550-
.toEqual([
551-
[HtmlTokenType.EXPANSION_FORM_START],
552-
[HtmlTokenType.RAW_TEXT, 'one.two'],
553-
[HtmlTokenType.RAW_TEXT, 'three'],
554-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
555-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
556-
[HtmlTokenType.TEXT, 'four {{a}}'],
557-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
558-
[HtmlTokenType.EXPANSION_FORM_END],
559-
[HtmlTokenType.EOF]
560-
]);
526+
it('should parse an expansion forms with interpolation in it', () => {
527+
expect(tokenizeAndHumanizeParts('{one.two, three, =4 {four {{a}}}}', true)).toEqual([
528+
[HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'],
529+
[HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
530+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'four {{a}}'],
531+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END],
532+
[HtmlTokenType.EOF]
533+
]);
561534
});
562535

563-
it("should parse nested expansion forms", () => {
536+
it('should parse nested expansion forms', () => {
564537
expect(tokenizeAndHumanizeParts(`{one.two, three, =4 { {xx, yy, =x {one}} }}`, true))
565538
.toEqual([
566-
[HtmlTokenType.EXPANSION_FORM_START],
567-
[HtmlTokenType.RAW_TEXT, 'one.two'],
568-
[HtmlTokenType.RAW_TEXT, 'three'],
569-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
539+
[HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'one.two'],
540+
[HtmlTokenType.RAW_TEXT, 'three'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=4'],
570541
[HtmlTokenType.EXPANSION_CASE_EXP_START],
571542

572-
[HtmlTokenType.EXPANSION_FORM_START],
573-
[HtmlTokenType.RAW_TEXT, 'xx'],
574-
[HtmlTokenType.RAW_TEXT, 'yy'],
575-
[HtmlTokenType.EXPANSION_CASE_VALUE, '=x'],
576-
[HtmlTokenType.EXPANSION_CASE_EXP_START],
577-
[HtmlTokenType.TEXT, 'one'],
578-
[HtmlTokenType.EXPANSION_CASE_EXP_END],
579-
[HtmlTokenType.EXPANSION_FORM_END],
543+
[HtmlTokenType.EXPANSION_FORM_START], [HtmlTokenType.RAW_TEXT, 'xx'],
544+
[HtmlTokenType.RAW_TEXT, 'yy'], [HtmlTokenType.EXPANSION_CASE_VALUE, '=x'],
545+
[HtmlTokenType.EXPANSION_CASE_EXP_START], [HtmlTokenType.TEXT, 'one'],
546+
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END],
580547
[HtmlTokenType.TEXT, ' '],
581548

582549
[HtmlTokenType.EXPANSION_CASE_EXP_END], [HtmlTokenType.EXPANSION_FORM_END],

modules/@angular/compiler/test/html_parser_spec.ts

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
1-
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '@angular/compiler/src/html_ast';
1+
import {HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst} from '@angular/compiler/src/html_ast';
22
import {HtmlTokenType} from '@angular/compiler/src/html_lexer';
3-
import {HtmlParser, HtmlParseTreeResult, HtmlTreeError} from '@angular/compiler/src/html_parser';
4-
import {
5-
HtmlElementAst,
6-
HtmlAttrAst,
7-
HtmlTextAst,
8-
HtmlCommentAst,
9-
HtmlExpansionAst,
10-
HtmlExpansionCaseAst
11-
} from '@angular/compiler/src/html_ast';
3+
import {HtmlParseTreeResult, HtmlParser, HtmlTreeError} from '@angular/compiler/src/html_parser';
124
import {ParseError} from '@angular/compiler/src/parse_util';
5+
136
import {humanizeDom, humanizeDomSourceSpans, humanizeLineColumn} from './html_ast_spec_utils';
147

158
export function main() {
@@ -238,15 +231,14 @@ export function main() {
238231
`<div>before{messages.length, plural, =0 {You have <b>no</b> messages} =1 {One {{message}}}}after</div>`,
239232
'TestComp', true);
240233

241-
expect(humanizeDom(parsed))
242-
.toEqual([
243-
[HtmlElementAst, 'div', 0],
244-
[HtmlTextAst, 'before', 1],
245-
[HtmlExpansionAst, 'messages.length', 'plural'],
246-
[HtmlExpansionCaseAst, '=0'],
247-
[HtmlExpansionCaseAst, '=1'],
248-
[HtmlTextAst, 'after', 1],
249-
]);
234+
expect(humanizeDom(parsed)).toEqual([
235+
[HtmlElementAst, 'div', 0],
236+
[HtmlTextAst, 'before', 1],
237+
[HtmlExpansionAst, 'messages.length', 'plural'],
238+
[HtmlExpansionCaseAst, '=0'],
239+
[HtmlExpansionCaseAst, '=1'],
240+
[HtmlTextAst, 'after', 1],
241+
]);
250242
let cases = (<any>parsed.rootNodes[0]).children[1].cases;
251243

252244
expect(humanizeDom(new HtmlParseTreeResult(cases[0].expression, []))).toEqual([
@@ -263,20 +255,18 @@ export function main() {
263255
it('should parse out nested expansion forms', () => {
264256
let parsed = parser.parse(
265257
`{messages.length, plural, =0 { {p.gender, gender, =m {m}} }}`, 'TestComp', true);
266-
expect(humanizeDom(parsed))
267-
.toEqual([
268-
[HtmlExpansionAst, 'messages.length', 'plural'],
269-
[HtmlExpansionCaseAst, '=0'],
270-
]);
258+
expect(humanizeDom(parsed)).toEqual([
259+
[HtmlExpansionAst, 'messages.length', 'plural'],
260+
[HtmlExpansionCaseAst, '=0'],
261+
]);
271262

272263
let firstCase = (<any>parsed.rootNodes[0]).cases[0];
273264

274-
expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, [])))
275-
.toEqual([
276-
[HtmlExpansionAst, 'p.gender', 'gender'],
277-
[HtmlExpansionCaseAst, '=m'],
278-
[HtmlTextAst, ' ', 0],
279-
]);
265+
expect(humanizeDom(new HtmlParseTreeResult(firstCase.expression, []))).toEqual([
266+
[HtmlExpansionAst, 'p.gender', 'gender'],
267+
[HtmlExpansionCaseAst, '=m'],
268+
[HtmlTextAst, ' ', 0],
269+
]);
280270
});
281271

282272
it('should error when expansion form is not closed', () => {

0 commit comments

Comments
 (0)