feat(i18n): support plural and gender special forms
This commit is contained in:
parent
7c9717bba8
commit
88b0a239c4
|
@ -23,6 +23,8 @@ import {
|
|||
HtmlAttrAst,
|
||||
HtmlAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from './html_ast';
|
||||
import {HtmlParser} from './html_parser';
|
||||
|
@ -158,4 +160,7 @@ class TemplatePreparseVisitor implements HtmlAstVisitor {
|
|||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
||||
visitText(ast: HtmlTextAst, context: any): any { return null; }
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
}
|
||||
|
|
|
@ -12,6 +12,24 @@ export class HtmlTextAst implements HtmlAst {
|
|||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitText(this, context); }
|
||||
}
|
||||
|
||||
export class HtmlExpansionAst implements HtmlAst {
|
||||
constructor(public switchValue: string, public type: string, public cases: HtmlExpansionCaseAst[],
|
||||
public sourceSpan: ParseSourceSpan, public switchValueSourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any {
|
||||
return visitor.visitExpansion(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlExpansionCaseAst implements HtmlAst {
|
||||
constructor(public value: string, public expression: HtmlAst[],
|
||||
public sourceSpan: ParseSourceSpan, public valueSourceSpan: ParseSourceSpan,
|
||||
public expSourceSpan: ParseSourceSpan) {}
|
||||
|
||||
visit(visitor: HtmlAstVisitor, context: any): any {
|
||||
return visitor.visitExpansionCase(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class HtmlAttrAst implements HtmlAst {
|
||||
constructor(public name: string, public value: string, public sourceSpan: ParseSourceSpan) {}
|
||||
visit(visitor: HtmlAstVisitor, context: any): any { return visitor.visitAttr(this, context); }
|
||||
|
@ -34,6 +52,8 @@ export interface HtmlAstVisitor {
|
|||
visitAttr(ast: HtmlAttrAst, context: any): any;
|
||||
visitText(ast: HtmlTextAst, context: any): any;
|
||||
visitComment(ast: HtmlCommentAst, context: any): any;
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any;
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any;
|
||||
}
|
||||
|
||||
export function htmlVisitAll(visitor: HtmlAstVisitor, asts: HtmlAst[], context: any = null): any[] {
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
HtmlElementAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
HtmlAst
|
||||
} from './html_ast';
|
||||
import {HtmlParser, HtmlParseTreeResult} from './html_parser';
|
||||
|
@ -84,6 +86,13 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
|
|||
|
||||
visitText(ast: HtmlTextAst, context: any): HtmlTextAst { return ast; }
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||
let cases = ast.cases.map(c => c.visit(this, null));
|
||||
return new HtmlExpansionAst(ast.switchValue, ast.type, cases, ast.sourceSpan);
|
||||
}
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
|
||||
|
||||
private _rewriteLongSyntax(ast: HtmlAttrAst): HtmlAttrAst {
|
||||
let m = RegExpWrapper.firstMatch(LONG_SYNTAX_REGEXP, ast.name);
|
||||
let attrName = ast.name;
|
||||
|
@ -211,9 +220,10 @@ export class LegacyHtmlAstTransformer implements HtmlAstVisitor {
|
|||
|
||||
@Injectable()
|
||||
export class LegacyHtmlParser extends HtmlParser {
|
||||
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
|
||||
parse(sourceContent: string, sourceUrl: string,
|
||||
parseExpansionForms: boolean = false): HtmlParseTreeResult {
|
||||
let transformer = new LegacyHtmlAstTransformer();
|
||||
let htmlParseTreeResult = super.parse(sourceContent, sourceUrl);
|
||||
let htmlParseTreeResult = super.parse(sourceContent, sourceUrl, parseExpansionForms);
|
||||
|
||||
let rootNodes = htmlParseTreeResult.rootNodes.map(node => node.visit(transformer, null));
|
||||
|
||||
|
|
|
@ -64,6 +64,8 @@ import {
|
|||
HtmlAttrAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from './html_ast';
|
||||
|
||||
|
@ -268,7 +270,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
|||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return null; }
|
||||
|
||||
visitElement(element: HtmlElementAst, parent: ElementContext): any {
|
||||
visitElement(element: HtmlElementAst, component: ElementContext): any {
|
||||
var nodeName = element.name;
|
||||
var preparsedElement = preparseElement(element);
|
||||
if (preparsedElement.type === PreparsedElementType.SCRIPT ||
|
||||
|
@ -770,6 +772,9 @@ class NonBindableVisitor implements HtmlAstVisitor {
|
|||
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
|
||||
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
|
||||
}
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
|
||||
}
|
||||
|
||||
class BoundElementOrDirectiveProperty {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import {
|
||||
HtmlAst,
|
||||
HtmlAstVisitor,
|
||||
HtmlElementAst,
|
||||
HtmlAttrAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from 'angular2/src/compiler/html_ast';
|
||||
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
/**
|
||||
* Expands special forms into elements.
|
||||
*
|
||||
* For example,
|
||||
*
|
||||
* ```
|
||||
* { messages.length, plural,
|
||||
* =0 {zero}
|
||||
* =1 {one}
|
||||
* =other {more than one}
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* will be expanded into
|
||||
*
|
||||
* ```
|
||||
* <ul [ngPlural]="messages.length">
|
||||
* <template [ngPluralCase]="0"><li i18n="plural_0">zero</li></template>
|
||||
* <template [ngPluralCase]="1"><li i18n="plural_1">one</li></template>
|
||||
* <template [ngPluralCase]="other"><li i18n="plural_other">more than one</li></template>
|
||||
* </ul>
|
||||
* ```
|
||||
*/
|
||||
export class Expander implements HtmlAstVisitor {
|
||||
constructor() {}
|
||||
|
||||
visitElement(ast: HtmlElementAst, context: any): any {
|
||||
return new HtmlElementAst(ast.name, ast.attrs, htmlVisitAll(this, ast.children), ast.sourceSpan,
|
||||
ast.startSourceSpan, ast.endSourceSpan);
|
||||
}
|
||||
|
||||
visitAttr(ast: HtmlAttrAst, context: any): any { return ast; }
|
||||
|
||||
visitText(ast: HtmlTextAst, context: any): any { return ast; }
|
||||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||
return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
|
||||
}
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
|
||||
throw new BaseException("Should not be reached");
|
||||
}
|
||||
}
|
||||
|
||||
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 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 switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
|
||||
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
|
||||
ast.sourceSpan);
|
||||
}
|
|
@ -7,6 +7,8 @@ import {
|
|||
HtmlAttrAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from 'angular2/src/compiler/html_ast';
|
||||
import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
@ -14,6 +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 {
|
||||
messageFromAttribute,
|
||||
I18nError,
|
||||
|
@ -119,19 +122,25 @@ export class I18nHtmlParser implements HtmlParser {
|
|||
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
|
||||
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
|
||||
|
||||
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
|
||||
parse(sourceContent: string, sourceUrl: string,
|
||||
parseExpansionForms: boolean = false): HtmlParseTreeResult {
|
||||
this.errors = [];
|
||||
|
||||
let res = this._htmlParser.parse(sourceContent, sourceUrl);
|
||||
let res = this._htmlParser.parse(sourceContent, sourceUrl, parseExpansionForms);
|
||||
if (res.errors.length > 0) {
|
||||
return res;
|
||||
} else {
|
||||
let nodes = this._recurse(res.rootNodes);
|
||||
let nodes = this._recurse(this._expandNodes(res.rootNodes));
|
||||
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);
|
||||
|
@ -360,5 +369,9 @@ class _CreateNodeMapping implements HtmlAstVisitor {
|
|||
return null;
|
||||
}
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
|
||||
}
|
||||
|
|
|
@ -13,6 +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 {
|
||||
I18nError,
|
||||
Part,
|
||||
|
@ -121,15 +122,20 @@ export class MessageExtractor {
|
|||
this.messages = [];
|
||||
this.errors = [];
|
||||
|
||||
let res = this._htmlParser.parse(template, sourceUrl);
|
||||
let res = this._htmlParser.parse(template, sourceUrl, true);
|
||||
if (res.errors.length > 0) {
|
||||
return new ExtractionResult([], res.errors);
|
||||
} else {
|
||||
this._recurse(res.rootNodes);
|
||||
this._recurse(this._expandNodes(res.rootNodes));
|
||||
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));
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
HtmlAttrAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from 'angular2/src/compiler/html_ast';
|
||||
import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang';
|
||||
|
@ -179,6 +181,10 @@ class _StringifyVisitor implements HtmlAstVisitor {
|
|||
|
||||
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
||||
|
||||
private _join(strs: string[], str: string): string {
|
||||
return strs.filter(s => s.length > 0).join(str);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
HtmlAttrAst,
|
||||
HtmlTextAst,
|
||||
HtmlCommentAst,
|
||||
HtmlExpansionAst,
|
||||
HtmlExpansionCaseAst,
|
||||
htmlVisitAll
|
||||
} from 'angular2/src/compiler/html_ast';
|
||||
import {ParseError, ParseLocation} from 'angular2/src/compiler/parse_util';
|
||||
|
@ -70,6 +72,19 @@ class _Humanizer implements HtmlAstVisitor {
|
|||
return null;
|
||||
}
|
||||
|
||||
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||
var res = this._appendContext(ast, [HtmlExpansionAst, ast.switchValue, ast.type]);
|
||||
this.result.push(res);
|
||||
htmlVisitAll(this, ast.cases);
|
||||
return null;
|
||||
}
|
||||
|
||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
|
||||
var res = this._appendContext(ast, [HtmlExpansionCaseAst, ast.value]);
|
||||
this.result.push(res);
|
||||
return null;
|
||||
}
|
||||
|
||||
private _appendContext(ast: HtmlAst, input: any[]): any[] {
|
||||
if (!this.includeSourceSpan) return input;
|
||||
input.push(ast.sourceSpan.toString());
|
||||
|
|
|
@ -42,7 +42,7 @@ export function main() {
|
|||
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
|
||||
|
||||
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
|
||||
.parse(template, "someurl");
|
||||
.parse(template, "someurl", true);
|
||||
}
|
||||
|
||||
it("should delegate to the provided parser when no i18n", () => {
|
||||
|
@ -188,6 +188,82 @@ export function main() {
|
|||
expect(res[1].sourceSpan.start.offset).toEqual(10);
|
||||
});
|
||||
|
||||
it("should handle the plural special form", () => {
|
||||
let translations: {[key: string]: string} = {};
|
||||
translations[id(new Message('zero<ph name="e1">bold</ph>', "plural_0", null))] =
|
||||
'ZERO<ph name="e1">BOLD</ph>';
|
||||
|
||||
let res = parse(`{messages.length, plural,=0 {zero<b>bold</b>}}`, 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]
|
||||
]);
|
||||
});
|
||||
|
||||
it("should correctly set source code positions", () => {
|
||||
let translations: {[key: string]: string} = {};
|
||||
translations[id(new Message('<ph name="e0">bold</ph>', "plural_0", null))] =
|
||||
'<ph name="e0">BOLD</ph>';
|
||||
|
||||
let nodes = parse(`{messages.length, plural,=0 {<b>bold</b>}}`, translations).rootNodes;
|
||||
|
||||
let ul: HtmlElementAst = <HtmlElementAst>nodes[0];
|
||||
|
||||
expect(ul.sourceSpan.start.col).toEqual(0);
|
||||
expect(ul.sourceSpan.end.col).toEqual(42);
|
||||
|
||||
expect(ul.startSourceSpan.start.col).toEqual(0);
|
||||
expect(ul.startSourceSpan.end.col).toEqual(42);
|
||||
|
||||
expect(ul.endSourceSpan.start.col).toEqual(0);
|
||||
expect(ul.endSourceSpan.end.col).toEqual(42);
|
||||
|
||||
let switchExp = ul.attrs[0];
|
||||
expect(switchExp.sourceSpan.start.col).toEqual(1);
|
||||
expect(switchExp.sourceSpan.end.col).toEqual(16);
|
||||
|
||||
let template: HtmlElementAst = <HtmlElementAst>ul.children[0];
|
||||
expect(template.sourceSpan.start.col).toEqual(26);
|
||||
expect(template.sourceSpan.end.col).toEqual(41);
|
||||
|
||||
let switchCheck = template.attrs[0];
|
||||
expect(switchCheck.sourceSpan.start.col).toEqual(26);
|
||||
expect(switchCheck.sourceSpan.end.col).toEqual(28);
|
||||
|
||||
let li: HtmlElementAst = <HtmlElementAst>template.children[0];
|
||||
expect(li.sourceSpan.start.col).toEqual(26);
|
||||
expect(li.sourceSpan.end.col).toEqual(41);
|
||||
|
||||
let b: HtmlElementAst = <HtmlElementAst>li.children[0];
|
||||
expect(b.sourceSpan.start.col).toEqual(29);
|
||||
expect(b.sourceSpan.end.col).toEqual(32);
|
||||
});
|
||||
|
||||
it("should handle other special forms", () => {
|
||||
let translations: {[key: string]: string} = {};
|
||||
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],
|
||||
]);
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("should error when giving an invalid template", () => {
|
||||
expect(humanizeErrors(parse("<a>a</b>", {}).errors))
|
||||
|
|
|
@ -176,6 +176,24 @@ export function main() {
|
|||
]);
|
||||
});
|
||||
|
||||
it("should extract messages from special forms", () => {
|
||||
let res = extractor.extract(`
|
||||
<div>
|
||||
{messages.length, plural,
|
||||
=0 {You have <b>no</b> messages}
|
||||
=1 {You have one message}
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
"someurl");
|
||||
|
||||
expect(res.messages)
|
||||
.toEqual([
|
||||
new Message('You have <ph name="e1">no</ph> messages', "plural_0", null),
|
||||
new Message('You have one message', "plural_1", null)
|
||||
]);
|
||||
});
|
||||
|
||||
it("should remove duplicate messages", () => {
|
||||
let res = extractor.extract(`
|
||||
<!-- i18n: meaning|desc1 -->message<!-- /i18n -->
|
||||
|
|
Loading…
Reference in New Issue