feat(i18n): add support for nested expansion forms

Closes #7977
This commit is contained in:
vsavkin 2016-04-13 16:01:25 -07:00 committed by Victor Savkin
parent 22ae2d0976
commit c6244d1470
9 changed files with 246 additions and 94 deletions

View File

@ -125,8 +125,7 @@ class _HtmlTokenizer {
private currentTokenStart: ParseLocation; private currentTokenStart: ParseLocation;
private currentTokenType: HtmlTokenType; private currentTokenType: HtmlTokenType;
private inExpansionCase: boolean = false; private expansionCaseStack = [];
private inExpansionForm: boolean = false;
tokens: HtmlToken[] = []; tokens: HtmlToken[] = [];
errors: HtmlTokenError[] = []; errors: HtmlTokenError[] = [];
@ -169,10 +168,12 @@ class _HtmlTokenizer {
} else if (this.peek === $EQ && this.tokenizeExpansionForms) { } else if (this.peek === $EQ && this.tokenizeExpansionForms) {
this._consumeExpansionCaseStart(); this._consumeExpansionCaseStart();
} else if (this.peek === $RBRACE && this.inExpansionCase && this.tokenizeExpansionForms) { } else if (this.peek === $RBRACE && this.isInExpansionCase() &&
this.tokenizeExpansionForms) {
this._consumeExpansionCaseEnd(); this._consumeExpansionCaseEnd();
} else if (this.peek === $RBRACE && !this.inExpansionCase && this.tokenizeExpansionForms) { } else if (this.peek === $RBRACE && this.isInExpansionForm() &&
this.tokenizeExpansionForms) {
this._consumeExpansionFormEnd(); this._consumeExpansionFormEnd();
} else { } else {
@ -551,7 +552,7 @@ class _HtmlTokenizer {
this._requireCharCode($COMMA); this._requireCharCode($COMMA);
this._attemptCharCodeUntilFn(isNotWhitespace); this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionForm = true; this.expansionCaseStack.push(HtmlTokenType.EXPANSION_FORM_START);
} }
private _consumeExpansionCaseStart() { private _consumeExpansionCaseStart() {
@ -567,7 +568,7 @@ class _HtmlTokenizer {
this._endToken([], this._getLocation()); this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace); this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionCase = true; this.expansionCaseStack.push(HtmlTokenType.EXPANSION_CASE_EXP_START);
} }
private _consumeExpansionCaseEnd() { private _consumeExpansionCaseEnd() {
@ -576,7 +577,7 @@ class _HtmlTokenizer {
this._endToken([], this._getLocation()); this._endToken([], this._getLocation());
this._attemptCharCodeUntilFn(isNotWhitespace); this._attemptCharCodeUntilFn(isNotWhitespace);
this.inExpansionCase = false; this.expansionCaseStack.pop();
} }
private _consumeExpansionFormEnd() { private _consumeExpansionFormEnd() {
@ -584,7 +585,7 @@ class _HtmlTokenizer {
this._requireCharCode($RBRACE); this._requireCharCode($RBRACE);
this._endToken([]); this._endToken([]);
this.inExpansionForm = false; this.expansionCaseStack.pop();
} }
private _consumeText() { private _consumeText() {
@ -622,7 +623,9 @@ class _HtmlTokenizer {
if (this.peek === $LT || this.peek === $EOF) return true; if (this.peek === $LT || this.peek === $EOF) return true;
if (this.tokenizeExpansionForms) { if (this.tokenizeExpansionForms) {
if (isSpecialFormStart(this.peek, this.nextPeek)) return true; 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; return false;
} }
@ -648,6 +651,18 @@ class _HtmlTokenizer {
this.tokens = ListWrapper.slice(this.tokens, 0, nbTokens); 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 { function isNotWhitespace(code: number): boolean {

View File

@ -124,40 +124,9 @@ class TreeBuilder {
// read = // read =
while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) { while (this.peek.type === HtmlTokenType.EXPANSION_CASE_VALUE) {
let value = this._advance(); let expCase = this._parseExpansionCase();
if (isBlank(expCase)) return; // error
// read { cases.push(expCase);
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(<HtmlTreeError[]>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));
} }
// read the final } // read the final }
@ -173,6 +142,80 @@ class TreeBuilder {
mainSourceSpan, switchValue.sourceSpan)); 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(<HtmlTreeError[]>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) { private _consumeText(token: HtmlToken) {
let text = token.parts[0]; let text = token.parts[0];
if (text.length > 0 && text[0] == '\n') { if (text.length > 0 && text[0] == '\n') {
@ -321,3 +364,7 @@ function getElementFullName(prefix: string, localName: string,
return mergeNsAndName(prefix, localName); return mergeNsAndName(prefix, localName);
} }
function lastOnStack(stack: any[], element: any): boolean {
return stack.length > 0 && stack[stack.length - 1] === element;
}

View File

@ -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 { visitText(ast: HtmlTextAst, parent: ElementContext): any {
var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR); var ngContentIndex = parent.findNgContentIndex(TEXT_CSS_SELECTOR);
var expr = this._parseInterpolation(ast.value, ast.sourceSpan); var expr = this._parseInterpolation(ast.value, ast.sourceSpan);
@ -270,7 +274,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return null; } 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 nodeName = element.name;
var preparsedElement = preparseElement(element); var preparsedElement = preparseElement(element);
if (preparsedElement.type === PreparsedElementType.SCRIPT || if (preparsedElement.type === PreparsedElementType.SCRIPT ||
@ -773,7 +777,6 @@ class NonBindableVisitor implements HtmlAstVisitor {
return new TextAst(ast.value, ngContentIndex, ast.sourceSpan); return new TextAst(ast.value, ngContentIndex, ast.sourceSpan);
} }
visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; } visitExpansion(ast: HtmlExpansionAst, context: any): any { return ast; }
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; } visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return ast; }
} }

View File

@ -12,6 +12,7 @@ import {
import {BaseException} from 'angular2/src/facade/exceptions'; import {BaseException} from 'angular2/src/facade/exceptions';
/** /**
* Expands special forms into elements. * Expands special forms into elements.
* *
@ -35,7 +36,18 @@ import {BaseException} from 'angular2/src/facade/exceptions';
* </ul> * </ul>
* ``` * ```
*/ */
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() {} constructor() {}
visitElement(ast: HtmlElementAst, context: any): any { visitElement(ast: HtmlElementAst, context: any): any {
@ -50,6 +62,7 @@ export class Expander implements HtmlAstVisitor {
visitComment(ast: HtmlCommentAst, context: any): any { return ast; } visitComment(ast: HtmlCommentAst, context: any): any { return ast; }
visitExpansion(ast: HtmlExpansionAst, context: any): any { visitExpansion(ast: HtmlExpansionAst, context: any): any {
this.expanded = true;
return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast); return ast.type == "plural" ? _expandPluralForm(ast) : _expandDefaultForm(ast);
} }
@ -59,36 +72,44 @@ export class Expander implements HtmlAstVisitor {
} }
function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst { function _expandPluralForm(ast: HtmlExpansionAst): HtmlElementAst {
let children = ast.cases.map( let children = ast.cases.map(c => {
c => new HtmlElementAst( let expansionResult = expandNodes(c.expression);
`template`, 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 HtmlAttrAst("ngPluralCase", c.value, c.valueSourceSpan),
], ],
[ [
new HtmlElementAst( new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)], c.sourceSpan, c.sourceSpan, c.sourceSpan)
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
], ],
c.sourceSpan, c.sourceSpan, c.sourceSpan)); c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan); let switchAttr = new HtmlAttrAst("[ngPlural]", ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan, return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
ast.sourceSpan); ast.sourceSpan);
} }
function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst { function _expandDefaultForm(ast: HtmlExpansionAst): HtmlElementAst {
let children = ast.cases.map( let children = ast.cases.map(c => {
c => new HtmlElementAst( let expansionResult = expandNodes(c.expression);
`template`, 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 HtmlAttrAst("ngSwitchWhen", c.value, c.valueSourceSpan),
], ],
[ [
new HtmlElementAst( new HtmlElementAst(`li`, i18nAttrs, expansionResult.nodes,
`li`, [new HtmlAttrAst("i18n", `${ast.type}_${c.value}`, c.valueSourceSpan)], c.sourceSpan, c.sourceSpan, c.sourceSpan)
c.expression, c.sourceSpan, c.sourceSpan, c.sourceSpan)
], ],
c.sourceSpan, c.sourceSpan, c.sourceSpan)); c.sourceSpan, c.sourceSpan, c.sourceSpan);
});
let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan); let switchAttr = new HtmlAttrAst("[ngSwitch]", ast.switchValue, ast.switchValueSourceSpan);
return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan, return new HtmlElementAst("ul", [switchAttr], children, ast.sourceSpan, ast.sourceSpan,
ast.sourceSpan); ast.sourceSpan);

View File

@ -16,7 +16,7 @@ import {RegExpWrapper, NumberWrapper, isPresent} from 'angular2/src/facade/lang'
import {BaseException} from 'angular2/src/facade/exceptions'; import {BaseException} from 'angular2/src/facade/exceptions';
import {Parser} from 'angular2/src/compiler/expression_parser/parser'; import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message'; import {Message, id} from './message';
import {Expander} from './expander'; import {expandNodes} from './expander';
import { import {
messageFromAttribute, messageFromAttribute,
I18nError, I18nError,
@ -126,21 +126,16 @@ export class I18nHtmlParser implements HtmlParser {
parseExpansionForms: boolean = false): HtmlParseTreeResult { parseExpansionForms: boolean = false): HtmlParseTreeResult {
this.errors = []; this.errors = [];
let res = this._htmlParser.parse(sourceContent, sourceUrl, parseExpansionForms); let res = this._htmlParser.parse(sourceContent, sourceUrl, true);
if (res.errors.length > 0) { if (res.errors.length > 0) {
return res; return res;
} else { } 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) : return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
new HtmlParseTreeResult(nodes, []); new HtmlParseTreeResult(nodes, []);
} }
} }
private _expandNodes(nodes: HtmlAst[]): HtmlAst[] {
let e = new Expander();
return htmlVisitAll(e, nodes);
}
private _processI18nPart(p: Part): HtmlAst[] { private _processI18nPart(p: Part): HtmlAst[] {
try { try {
return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p); return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
@ -155,9 +150,11 @@ export class I18nHtmlParser implements HtmlParser {
} }
private _mergeI18Part(p: Part): HtmlAst[] { 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)) { 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]; let parsedMessage = this._messages[messageId];
@ -294,14 +291,17 @@ export class I18nHtmlParser implements HtmlParser {
} }
let i18n = i18ns[0]; 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)) { if (StringMapWrapper.contains(this._messages, messageId)) {
let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]); let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan)); res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
} else { } 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; return res;

View File

@ -13,7 +13,7 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection'; import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/compiler/expression_parser/parser'; import {Parser} from 'angular2/src/compiler/expression_parser/parser';
import {Message, id} from './message'; import {Message, id} from './message';
import {Expander} from './expander'; import {expandNodes} from './expander';
import { import {
I18nError, I18nError,
Part, Part,
@ -126,16 +126,11 @@ export class MessageExtractor {
if (res.errors.length > 0) { if (res.errors.length > 0) {
return new ExtractionResult([], res.errors); return new ExtractionResult([], res.errors);
} else { } else {
this._recurse(this._expandNodes(res.rootNodes)); this._recurse(expandNodes(res.rootNodes).nodes);
return new ExtractionResult(this.messages, this.errors); 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 { private _extractMessagesFromPart(p: Part): void {
if (p.hasI18n) { if (p.hasI18n) {
this.messages.push(p.createMessage(this._parser)); this.messages.push(p.createMessage(this._parser));

View File

@ -646,6 +646,31 @@ export function main() {
[HtmlTokenType.EOF] [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', () => { describe('errors', () => {

View File

@ -271,6 +271,27 @@ export function main() {
.toEqual([[HtmlTextAst, 'One {{message}}', 0]]); .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 = (<any>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", () => { it("should error when expansion form is not closed", () => {
let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true); let p = parser.parse(`{messages.length, plural, =0 {one}`, 'TestComp', true);
expect(humanizeErrors(p.errors)) expect(humanizeErrors(p.errors))

View File

@ -188,7 +188,7 @@ export function main() {
expect(res[1].sourceSpan.start.offset).toEqual(10); 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} = {}; let translations: {[key: string]: string} = {};
translations[id(new Message('zero<ph name="e1">bold</ph>', "plural_0", null))] = translations[id(new Message('zero<ph name="e1">bold</ph>', "plural_0", null))] =
'ZERO<ph name="e1">BOLD</ph>'; 'ZERO<ph name="e1">BOLD</ph>';
@ -200,7 +200,7 @@ export function main() {
[HtmlElementAst, 'ul', 0], [HtmlElementAst, 'ul', 0],
[HtmlAttrAst, '[ngPlural]', 'messages.length'], [HtmlAttrAst, '[ngPlural]', 'messages.length'],
[HtmlElementAst, 'template', 1], [HtmlElementAst, 'template', 1],
[HtmlAttrAst, '[ngPluralCase]', '0'], [HtmlAttrAst, 'ngPluralCase', '0'],
[HtmlElementAst, 'li', 2], [HtmlElementAst, 'li', 2],
[HtmlTextAst, 'ZERO', 3], [HtmlTextAst, 'ZERO', 3],
[HtmlElementAst, 'b', 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", () => { it("should correctly set source code positions", () => {
let translations: {[key: string]: string} = {}; let translations: {[key: string]: string} = {};
translations[id(new Message('<ph name="e0">bold</ph>', "plural_0", null))] = translations[id(new Message('<ph name="e0">bold</ph>', "plural_0", null))] =
@ -258,7 +283,7 @@ export function main() {
[HtmlElementAst, 'ul', 0], [HtmlElementAst, 'ul', 0],
[HtmlAttrAst, '[ngSwitch]', 'person.gender'], [HtmlAttrAst, '[ngSwitch]', 'person.gender'],
[HtmlElementAst, 'template', 1], [HtmlElementAst, 'template', 1],
[HtmlAttrAst, '[ngSwitchWhen]', 'male'], [HtmlAttrAst, 'ngSwitchWhen', 'male'],
[HtmlElementAst, 'li', 2], [HtmlElementAst, 'li', 2],
[HtmlTextAst, 'M', 3], [HtmlTextAst, 'M', 3],
]); ]);
@ -273,13 +298,13 @@ export function main() {
it("should error when no matching message (attr)", () => { it("should error when no matching message (attr)", () => {
let mid = id(new Message("some message", null, null)); let mid = id(new Message("some message", null, null));
expect(humanizeErrors(parse("<div value='some message' i18n-value></div>", {}).errors)) expect(humanizeErrors(parse("<div value='some message' i18n-value></div>", {}).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)", () => { it("should error when no matching message (text)", () => {
let mid = id(new Message("some message", null, null)); let mid = id(new Message("some message", null, null));
expect(humanizeErrors(parse("<div i18n>some message</div>", {}).errors)) expect(humanizeErrors(parse("<div i18n>some message</div>", {}).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", () => { it("should error when a non-placeholder element appears in translation", () => {