Skip to content

Commit 08fdba7

Browse files
vsavkinRobert Messerle
authored andcommitted
feat(i18n): support plural and gender special forms
1 parent 0a4241b commit 08fdba7

File tree

11 files changed

+277
-8
lines changed

11 files changed

+277
-8
lines changed

modules/angular2/src/compiler/html_ast.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ export class HtmlTextAst implements HtmlAst {
1212
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
1313
}
1414

15+
export class HtmlExpansionAst implements HtmlAst {
16+
constructor(public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
17+
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
18+
visit(visitor: HtmlAstVisitor, context: any): any {
19+
return visitor.visitExpansion(this, context);
20+
}
21+
}
22+
23+
export class HtmlExpansionCaseAst implements HtmlAst {
24+
constructor(public value: string, public expression: HtmlAst[],
25+
public sourceSpan: ParseSourceSpan, public valueSourceSpan: ParseSourceSpan,
26+
public expSourceSpan: ParseSourceSpan) {}
27+
28+
visit(visitor: HtmlAstVisitor, context: any): any {
29+
return visitor.visitExpansionCase(this, context);
30+
}
31+
}
32+
1533
export class HtmlAttrAst implements HtmlAst {
1634
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
1735
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
@@ -34,6 +52,8 @@ export interface HtmlAstVisitor {
3452
visitAttr(ast: HtmlAttrAst, context: any): any;
3553
visitText(ast: HtmlTextAst, context: any): any;
3654
visitComment(ast: HtmlCommentAst, context: any): any;
55+
visitExpansion(ast: HtmlExpansionAst, context: any): any;
56+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
3757
}
3858

3959
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {

modules/angular2/src/compiler/legacy_template.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
HtmlElementAst,
1515
HtmlTextAst,
1616
HtmlCommentAst,
17+
HtmlExpansionAst,
18+
HtmlExpansionCaseAst,
1719
HtmlAst
1820
} from './html_ast';
1921
import {HtmlParser, HtmlParseTreeResult} from './html_parser';
@@ -84,6 +86,13 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
8486

8587
visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; }
8688

89+
visitExpansion(ast: HtmlExpansionAst, context: any): any {
90+
let cases = ast.cases.map(c => c.visit(this, null));
91+
return new HtmlExpansionAst(ast.switchValue, ast.type, cases, ast.sourceSpan);
92+
}
93+
94+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
95+
8796
private _rewriteLongSyntax(ast: HtmlAttrAst): HtmlAttrAst {
8897
let m = RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name);
8998
let attrName = ast.name;
@@ -211,9 +220,10 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
211220

212221
@Injectable()
213222
export class LegacyHtmlParser extends HtmlParser {
214-
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
223+
parse(sourceContent: string, sourceUrl: string,
224+
parseExpansionForms: boolean = false): HtmlParseTreeResult {
215225
let transformer = new LegacyHtmlAstTransformer();
216-
let htmlParseTreeResult = super.parse(sourceContent, sourceUrl);
226+
let htmlParseTreeResult = super.parse(sourceContent, sourceUrl, parseExpansionForms);
217227

218228
let rootNodes = htmlParseTreeResult.rootNodes.map(node => node.visit(transformer, null));
219229

modules/angular2/src/compiler/template_normalizer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
HtmlAttrAst,
2222
HtmlAst,
2323
HtmlCommentAst,
24+
HtmlExpansionAst,
25+
HtmlExpansionCaseAst,
2426
htmlVisitAll
2527
} from './html_ast';
2628
import {HtmlParser} from './html_parser';
@@ -130,4 +132,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
130132
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
131133
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
132134
visitText(ast: HtmlTextAst, context: any): any { return null; }
135+
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
136+
137+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
133138
}

modules/angular2/src/compiler/template_parser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import {
4242
HtmlAttrAst,
4343
HtmlTextAst,
4444
HtmlCommentAst,
45+
HtmlExpansionAst,
46+
HtmlExpansionCaseAst,
4547
htmlVisitAll
4648
} from './html_ast';
4749

@@ -711,6 +713,9 @@ class NonBindableVisitor implements HtmlAstVisitor {
711713
var ngContentIndex = component.findNgContentIndex(TEXT_CSS_SELECTOR);
712714
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
713715
}
716+
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
717+
718+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
714719
}
715720

716721
class BoundElementOrDirectiveProperty {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {
2+
HtmlAst,
3+
HtmlAstVisitor,
4+
HtmlElementAst,
5+
HtmlAttrAst,
6+
HtmlTextAst,
7+
HtmlCommentAst,
8+
HtmlExpansionAst,
9+
HtmlExpansionCaseAst,
10+
htmlVisitAll
11+
} from 'angular2/src/compiler/html_ast';
12+
13+
import {BaseException} from 'angular2/src/facade/exceptions';
14+
15+
/**
16+
* Expands special forms into elements.
17+
*
18+
* For example,
19+
*
20+
* ```
21+
* { messages.length, plural,
22+
* =0 {zero}
23+
* =1 {one}
24+
* =other {more than one}
25+
* }
26+
* ```
27+
*
28+
* will be expanded into
29+
*
30+
* ```
31+
* <ul [ngPlural]="messages.length">
32+
* <template [ngPluralCase]="0"><li i18n="plural_0">zero</li></template>
33+
* <template [ngPluralCase]="1"><li i18n="plural_1">one</li></template>
34+
* <template [ngPluralCase]="other"><li i18n="plural_other">more than one</li></template>
35+
* </ul>
36+
* ```
37+
*/
38+
export class Expander implements HtmlAstVisitor {
39+
constructor() {}
40+
41+
visitElement(ast: HtmlElementAst, context: any): any {
42+
return new HtmlElementAst(ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan,
43+
ast.startSourceSpan, ast.endSourceSpan);
44+
}
45+
46+
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
47+
48+
visitText(ast: HtmlTextAst, context: any): any { return ast; }
49+
50+
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
51+
52+
visitExpansion(ast: HtmlExpansionAst, context: any): any {
53+
return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
54+
}
55+
56+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
57+
throw new BaseException("Should not be reached");
58+
}
59+
}
60+
61+
function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
62+
let children = ast.cases.map(
63+
c => new HtmlElementAst(
64+
`template`,
65+
[
66+
new HtmlAttrAst("[ngPluralCase]", c.value, c.valueSourceSpan),
67+
],
68+
[
69+
new HtmlElementAst(
70+
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
71+
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
72+
],
73+
c.sourceSpan, c.sourceSpan, c.sourceSpan));
74+
let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan);
75+
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
76+
ast.sourceSpan);
77+
}
78+
79+
function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst {
80+
let children = ast.cases.map(
81+
c => new HtmlElementAst(
82+
`template`,
83+
[
84+
new HtmlAttrAst("[ngSwitchWhen]", c.value, c.valueSourceSpan),
85+
],
86+
[
87+
new HtmlElementAst(
88+
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)],
89+
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
90+
],
91+
c.sourceSpan, c.sourceSpan, c.sourceSpan));
92+
let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
93+
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
94+
ast.sourceSpan);
95+
}

modules/angular2/src/i18n/i18n_html_parser.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import {
77
HtmlAttrAst,
88
HtmlTextAst,
99
HtmlCommentAst,
10+
HtmlExpansionAst,
11+
HtmlExpansionCaseAst,
1012
htmlVisitAll
1113
} from 'angular2/src/compiler/html_ast';
1214
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
1315
import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang';
1416
import {BaseException} from 'angular2/src/facade/exceptions';
1517
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
1618
import {Message, id} from './message';
19+
import {Expander} from './expander';
1720
import {
1821
messageFromAttribute,
1922
I18nError,
@@ -117,19 +120,25 @@ export class I18nHtmlParser implements HtmlParser {
117120
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
118121
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
119122

120-
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
123+
parse(sourceContent: string, sourceUrl: string,
124+
parseExpansionForms: boolean = false): HtmlParseTreeResult {
121125
this.errors = [];
122126

123-
let res = this._htmlParser.parse(sourceContent, sourceUrl);
127+
let res = this._htmlParser.parse(sourceContent, sourceUrl, parseExpansionForms);
124128
if (res.errors.length > 0) {
125129
return res;
126130
} else {
127-
let nodes = this._recurse(res.rootNodes);
131+
let nodes = this._recurse(this._expandNodes(res.rootNodes));
128132
return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
129133
new HtmlParseTreeResult(nodes, []);
130134
}
131135
}
132136

137+
private _expandNodes(nodes: HtmlAst[]): HtmlAst[] {
138+
let e = new Expander();
139+
return htmlVisitAll(e, nodes);
140+
}
141+
133142
private _processI18nPart(p: Part): HtmlAst[] {
134143
try {
135144
return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
@@ -346,5 +355,9 @@ class _CreateNodeMapping implements HtmlAstVisitor {
346355
return null;
347356
}
348357

358+
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
359+
360+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
361+
349362
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
350363
}

modules/angular2/src/i18n/message_extractor.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
1313
import {StringMapWrapper} from 'angular2/src/facade/collection';
1414
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
1515
import {Message, id} from './message';
16+
import {Expander} from './expander';
1617
import {
1718
I18nError,
1819
Part,
@@ -121,15 +122,20 @@ export class MessageExtractor {
121122
this.messages = [];
122123
this.errors = [];
123124

124-
let res = this._htmlParser.parse(template, sourceUrl);
125+
let res = this._htmlParser.parse(template, sourceUrl, true);
125126
if (res.errors.length > 0) {
126127
return new ExtractionResult([], res.errors);
127128
} else {
128-
this._recurse(res.rootNodes);
129+
this._recurse(this._expandNodes(res.rootNodes));
129130
return new ExtractionResult(this.messages, this.errors);
130131
}
131132
}
132133

134+
private _expandNodes(nodes: HtmlAst[]): HtmlAst[] {
135+
let e = new Expander();
136+
return htmlVisitAll(e, nodes);
137+
}
138+
133139
private _extractMessagesFromPart(p: Part): void {
134140
if (p.hasI18n) {
135141
this.messages.push(p.createMessage(this._parser));

modules/angular2/src/i18n/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
HtmlAttrAst,
77
HtmlTextAst,
88
HtmlCommentAst,
9+
HtmlExpansionAst,
10+
HtmlExpansionCaseAst,
911
htmlVisitAll
1012
} from 'angular2/src/compiler/html_ast';
1113
import {isPresent, isBlank} from 'angular2/src/facade/lang';
@@ -159,6 +161,10 @@ class _StringifyVisitor implements HtmlAstVisitor {
159161

160162
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
161163

164+
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
165+
166+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
167+
162168
private _join(strs: string[], str: string): string {
163169
return strs.filter(s => s.length > 0).join(str);
164170
}

modules/angular2/test/compiler/html_ast_spec_utils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
HtmlAttrAst,
77
HtmlTextAst,
88
HtmlCommentAst,
9+
HtmlExpansionAst,
10+
HtmlExpansionCaseAst,
911
htmlVisitAll
1012
} from 'angular2/src/compiler/html_ast';
1113
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
@@ -70,6 +72,19 @@ class _Humanizer implements HtmlAstVisitor {
7072
return null;
7173
}
7274

75+
visitExpansion(ast: HtmlExpansionAst, context: any): any {
76+
var res = this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type]);
77+
this.result.push(res);
78+
htmlVisitAll(this, ast.cases);
79+
return null;
80+
}
81+
82+
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
83+
var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value]);
84+
this.result.push(res);
85+
return null;
86+
}
87+
7388
private _appendContext(ast: HtmlAst, input: any[]): any[] {
7489
if (!this.includeSourceSpan) return input;
7590
input.push(ast.sourceSpan.toString());

0 commit comments

Comments
 (0)