feat(I18nExtractor): Add file paths to error messages (#9177)

* feat(I18nExtractor): Add file paths to error messages

relates to #9071

* feat(i18n): allow i18n start comments without meaning

* refactor(i18n): cleanup

* test(HtmlParser): Add depth to expansion forms
This commit is contained in:
Victor Berchet
2016-06-14 17:50:23 -07:00
committed by GitHub
parent 7afee97d1b
commit fe01e2efb7
8 changed files with 112 additions and 104 deletions

View File

@ -23,9 +23,9 @@ const PLURAL_CASES: string[] = ['zero', 'one', 'two', 'few', 'many', 'other'];
*
* ```
* <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>
* <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>
* ```
*/
@ -39,6 +39,11 @@ export class ExpansionResult {
constructor(public nodes: HtmlAst[], public expanded: boolean, public errors: ParseError[]) {}
}
/**
* Expand expansion forms (plural, select) to directives
*
* @internal
*/
class _Expander implements HtmlAstVisitor {
expanded: boolean = false;
errors: ParseError[] = [];
@ -73,7 +78,7 @@ function _expandPluralForm(ast: HtmlExpansionAst, errors: ParseError[]): HtmlEle
`Plural cases should be "=<number>" or one of ${PLURAL_CASES.join(", ")}`));
}
let expansionResult = expandNodes(c.expression);
expansionResult.errors.forEach(e => errors.push(e));
errors.push(...expansionResult.errors);
let i18nAttrs = expansionResult.expanded ?
[] :
[new HtmlAttrAst('i18n', `${ast.type}_${c.value}`, c.valueSourceSpan)];

View File

@ -113,16 +113,16 @@ export class I18nHtmlParser implements HtmlParser {
} else {
let expanded = expandNodes(res.rootNodes);
let nodes = this._recurse(expanded.nodes);
this.errors = this.errors.concat(expanded.errors);
this.errors.push(...expanded.errors);
return this.errors.length > 0 ? new HtmlParseTreeResult([], this.errors) :
new HtmlParseTreeResult(nodes, []);
}
}
private _processI18nPart(p: Part): HtmlAst[] {
private _processI18nPart(part: Part): HtmlAst[] {
try {
return p.hasI18n ? this._mergeI18Part(p) : this._recurseIntoI18nPart(p);
return part.hasI18n ? this._mergeI18Part(part) : this._recurseIntoI18nPart(part);
} catch (e) {
if (e instanceof I18nError) {
this.errors.push(e);
@ -133,16 +133,17 @@ export class I18nHtmlParser implements HtmlParser {
}
}
private _mergeI18Part(p: Part): HtmlAst[] {
let message = p.createMessage(this._parser);
private _mergeI18Part(part: Part): HtmlAst[] {
let message = part.createMessage(this._parser);
let messageId = id(message);
if (!StringMapWrapper.contains(this._messages, messageId)) {
throw new I18nError(
p.sourceSpan, `Cannot find message for id '${messageId}', content '${message.content}'.`);
part.sourceSpan,
`Cannot find message for id '${messageId}', content '${message.content}'.`);
}
let parsedMessage = this._messages[messageId];
return this._mergeTrees(p, parsedMessage, p.children);
return this._mergeTrees(part, parsedMessage, part.children);
}
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
@ -166,8 +167,8 @@ export class I18nHtmlParser implements HtmlParser {
}
private _recurse(nodes: HtmlAst[]): HtmlAst[] {
let ps = partition(nodes, this.errors, this._implicitTags);
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
let parts = partition(nodes, this.errors, this._implicitTags);
return ListWrapper.flatten(parts.map(p => this._processI18nPart(p)));
}
private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {

View File

@ -90,16 +90,16 @@ export function removeDuplicates(messages: Message[]): Message[] {
* 4. If a part has the i18n attribute, stringify the nodes to create a Message.
*/
export class MessageExtractor {
messages: Message[];
errors: ParseError[];
private _messages: Message[];
private _errors: ParseError[];
constructor(
private _htmlParser: HtmlParser, private _parser: Parser, private _implicitTags: string[],
private _implicitAttrs: {[k: string]: string[]}) {}
extract(template: string, sourceUrl: string): ExtractionResult {
this.messages = [];
this.errors = [];
this._messages = [];
this._errors = [];
let res = this._htmlParser.parse(template, sourceUrl, true);
if (res.errors.length > 0) {
@ -107,27 +107,27 @@ export class MessageExtractor {
} else {
let expanded = expandNodes(res.rootNodes);
this._recurse(expanded.nodes);
return new ExtractionResult(this.messages, this.errors.concat(expanded.errors));
return new ExtractionResult(this._messages, this._errors.concat(expanded.errors));
}
}
private _extractMessagesFromPart(p: Part): void {
if (p.hasI18n) {
this.messages.push(p.createMessage(this._parser));
this._recurseToExtractMessagesFromAttributes(p.children);
private _extractMessagesFromPart(part: Part): void {
if (part.hasI18n) {
this._messages.push(part.createMessage(this._parser));
this._recurseToExtractMessagesFromAttributes(part.children);
} else {
this._recurse(p.children);
this._recurse(part.children);
}
if (isPresent(p.rootElement)) {
this._extractMessagesFromAttributes(p.rootElement);
if (isPresent(part.rootElement)) {
this._extractMessagesFromAttributes(part.rootElement);
}
}
private _recurse(nodes: HtmlAst[]): void {
if (isPresent(nodes)) {
let ps = partition(nodes, this.errors, this._implicitTags);
ps.forEach(p => this._extractMessagesFromPart(p));
let parts = partition(nodes, this._errors, this._implicitTags);
parts.forEach(part => this._extractMessagesFromPart(part));
}
}
@ -145,17 +145,15 @@ export class MessageExtractor {
isPresent(this._implicitAttrs[p.name]) ? this._implicitAttrs[p.name] : [];
let explicitAttrs: string[] = [];
p.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
try {
explicitAttrs.push(attr.name.substring(I18N_ATTR_PREFIX.length));
this.messages.push(messageFromI18nAttribute(this._parser, p, attr));
} catch (e) {
if (e instanceof I18nError) {
this.errors.push(e);
} else {
throw e;
}
p.attrs.filter(attr => attr.name.startsWith(I18N_ATTR_PREFIX)).forEach(attr => {
try {
explicitAttrs.push(attr.name.substring(I18N_ATTR_PREFIX.length));
this._messages.push(messageFromI18nAttribute(this._parser, p, attr));
} catch (e) {
if (e instanceof I18nError) {
this._errors.push(e);
} else {
throw e;
}
}
});
@ -163,6 +161,6 @@ export class MessageExtractor {
p.attrs.filter(attr => !attr.name.startsWith(I18N_ATTR_PREFIX))
.filter(attr => explicitAttrs.indexOf(attr.name) == -1)
.filter(attr => transAttrs.indexOf(attr.name) > -1)
.forEach(attr => this.messages.push(messageFromAttribute(this._parser, attr)));
.forEach(attr => this._messages.push(messageFromAttribute(this._parser, attr)));
}
}

View File

@ -2,7 +2,6 @@ import {Parser} from '../expression_parser/parser';
import {StringWrapper, isBlank, isPresent} from '../facade/lang';
import {HtmlAst, HtmlAstVisitor, HtmlAttrAst, HtmlCommentAst, HtmlElementAst, HtmlExpansionAst, HtmlExpansionCaseAst, HtmlTextAst, htmlVisitAll} from '../html_ast';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {Message} from './message';
export const I18N_ATTR = 'i18n';
@ -16,16 +15,14 @@ export class I18nError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
// Man, this is so ugly!
export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags: string[]): Part[] {
let res: Part[] = [];
let parts: Part[] = [];
for (let i = 0; i < nodes.length; ++i) {
let n = nodes[i];
let temp: HtmlAst[] = [];
if (_isOpeningComment(n)) {
let i18n = (<HtmlCommentAst>n).value.substring(5).trim();
let i18n = (<HtmlCommentAst>n).value.replace(/^i18n:?/, '').trim();
i++;
while (!_isClosingComment(nodes[i])) {
temp.push(nodes[i++]);
@ -34,18 +31,18 @@ export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags:
break;
}
}
res.push(new Part(null, null, temp, i18n, true));
parts.push(new Part(null, null, temp, i18n, true));
} else if (n instanceof HtmlElementAst) {
let i18n = _findI18nAttr(n);
let hasI18n: boolean = isPresent(i18n) || implicitTags.indexOf(n.name) > -1;
res.push(new Part(n, null, n.children, isPresent(i18n) ? i18n.value : null, hasI18n));
parts.push(new Part(n, null, n.children, isPresent(i18n) ? i18n.value : null, hasI18n));
} else if (n instanceof HtmlTextAst) {
res.push(new Part(null, n, null, null, false));
parts.push(new Part(null, n, null, null, false));
}
}
return res;
return parts;
}
export class Part {
@ -54,12 +51,14 @@ export class Part {
public children: HtmlAst[], public i18n: string, public hasI18n: boolean) {}
get sourceSpan(): ParseSourceSpan {
if (isPresent(this.rootElement))
if (isPresent(this.rootElement)) {
return this.rootElement.sourceSpan;
else if (isPresent(this.rootTextNode))
}
if (isPresent(this.rootTextNode)) {
return this.rootTextNode.sourceSpan;
else
return this.children[0].sourceSpan;
}
return this.children[0].sourceSpan;
}
createMessage(parser: Parser): Message {
@ -69,7 +68,7 @@ export class Part {
}
function _isOpeningComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith('i18n:');
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith('i18n');
}
function _isClosingComment(n: HtmlAst): boolean {
@ -77,8 +76,13 @@ function _isClosingComment(n: HtmlAst): boolean {
}
function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
return i18n.length == 0 ? null : i18n[0];
let attrs = p.attrs;
for (let i = 0; i < attrs.length; i++) {
if (attrs[i].name === I18N_ATTR) {
return attrs[i];
}
}
return null;
}
export function meaning(i18n: string): string {
@ -88,7 +92,7 @@ export function meaning(i18n: string): string {
export function description(i18n: string): string {
if (isBlank(i18n) || i18n == '') return null;
let parts = i18n.split('|');
let parts = i18n.split('|', 2);
return parts.length > 1 ? parts[1] : null;
}
@ -100,11 +104,10 @@ export function description(i18n: string): string {
export function messageFromI18nAttribute(
parser: Parser, p: HtmlElementAst, i18nAttr: HtmlAttrAst): Message {
let expectedName = i18nAttr.name.substring(5);
let matching = p.attrs.filter(a => a.name == expectedName);
let attr = p.attrs.find(a => a.name == expectedName);
if (matching.length > 0) {
return messageFromAttribute(
parser, matching[0], meaning(i18nAttr.value), description(i18nAttr.value));
if (attr) {
return messageFromAttribute(parser, attr, meaning(i18nAttr.value), description(i18nAttr.value));
}
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
@ -179,9 +182,8 @@ class _StringifyVisitor implements HtmlAstVisitor {
let noInterpolation = removeInterpolation(ast.value, ast.sourceSpan, this._parser);
if (noInterpolation != ast.value) {
return `<ph name="t${index}">${noInterpolation}</ph>`;
} else {
return ast.value;
}
return ast.value;
}
visitComment(ast: HtmlCommentAst, context: any): any { return ''; }