Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from Jun 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 27 additions & 23 deletions modules/@angular/compiler-cli/src/extract_i18n.ts
Expand Up @@ -19,33 +19,34 @@ import {CompileMetadataResolver, HtmlParser, DirectiveNormalizer, Lexer, Parser,
import {ReflectorHost} from './reflector_host';
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';


function extract(
ngOptions: tsc.AngularCompilerOptions, program: ts.Program, host: ts.CompilerHost) {
return Extractor.create(ngOptions, program, host).extract();
}

const GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;
const _dirPaths = new Map<compiler.CompileDirectiveMetadata, string>();

const _GENERATED_FILES = /\.ngfactory\.ts$|\.css\.ts$|\.css\.shim\.ts$/;

class Extractor {
constructor(
private options: tsc.AngularCompilerOptions, private program: ts.Program,
private _options: tsc.AngularCompilerOptions, private _program: ts.Program,
public host: ts.CompilerHost, private staticReflector: StaticReflector,
private resolver: CompileMetadataResolver, private compiler: compiler.OfflineCompiler,
private reflectorHost: ReflectorHost, private _extractor: MessageExtractor) {}
private _resolver: CompileMetadataResolver, private _compiler: compiler.OfflineCompiler,
private _reflectorHost: ReflectorHost, private _extractor: MessageExtractor) {}

private extractCmpMessages(metadatas: compiler.CompileDirectiveMetadata[]):
private _extractCmpMessages(metadatas: compiler.CompileDirectiveMetadata[]):
Promise<ExtractionResult> {
if (!metadatas || !metadatas.length) {
return null;
}

const normalize = (metadata: compiler.CompileDirectiveMetadata) => {
const directiveType = metadata.type.runtime;
const directives = this.resolver.getViewDirectivesMetadata(directiveType);
return Promise.all(directives.map(d => this.compiler.normalizeDirectiveMetadata(d)))
const directives = this._resolver.getViewDirectivesMetadata(directiveType);
return Promise.all(directives.map(d => this._compiler.normalizeDirectiveMetadata(d)))
.then(normalizedDirectives => {
const pipes = this.resolver.getViewPipesMetadata(directiveType);
const pipes = this._resolver.getViewPipesMetadata(directiveType);
return new compiler.NormalizedComponentWithViewDirectives(
metadata, normalizedDirectives, pipes);
});
Expand All @@ -56,8 +57,8 @@ class Extractor {
let messages: Message[] = [];
let errors: ParseError[] = [];
cmps.forEach(cmp => {
// TODO(vicb): url
let result = this._extractor.extract(cmp.component.template.template, 'url');
let url = _dirPaths.get(cmp.component);
let result = this._extractor.extract(cmp.component.template.template, url);
errors = errors.concat(result.errors);
messages = messages.concat(result.messages);
});
Expand All @@ -67,7 +68,7 @@ class Extractor {
});
}

private readComponents(absSourcePath: string) {
private _readComponents(absSourcePath: string): Promise<compiler.CompileDirectiveMetadata>[] {
const result: Promise<compiler.CompileDirectiveMetadata>[] = [];
const metadata = this.staticReflector.getModuleMetadata(absSourcePath);
if (!metadata) {
Expand All @@ -80,26 +81,29 @@ class Extractor {
return result;
}
for (const symbol of symbols) {
const staticType = this.reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
const staticType = this._reflectorHost.findDeclaration(absSourcePath, symbol, absSourcePath);
let directive: compiler.CompileDirectiveMetadata;
directive = this.resolver.maybeGetDirectiveMetadata(<any>staticType);
directive = this._resolver.maybeGetDirectiveMetadata(<any>staticType);

if (!directive || !directive.isComponent) {
continue;
if (directive && directive.isComponent) {
let promise = this._compiler.normalizeDirectiveMetadata(directive);
promise.then(md => _dirPaths.set(md, absSourcePath));
result.push(promise);
}
result.push(this.compiler.normalizeDirectiveMetadata(directive));
}
return result;
}

extract(): Promise<any> {
const promises = this.program.getSourceFiles()
_dirPaths.clear();

const promises = this._program.getSourceFiles()
.map(sf => sf.fileName)
.filter(f => !GENERATED_FILES.test(f))
.filter(f => !_GENERATED_FILES.test(f))
.map(
(absSourcePath: string): Promise<any> =>
Promise.all(this.readComponents(absSourcePath))
.then(metadatas => this.extractCmpMessages(metadatas))
Promise.all(this._readComponents(absSourcePath))
.then(metadatas => this._extractCmpMessages(metadatas))
.catch(e => console.error(e.stack)));

let messages: Message[] = [];
Expand All @@ -112,12 +116,12 @@ class Extractor {
});

if (errors.length) {
throw errors;
throw new Error(errors.map(e => e.toString()).join('\n'));
}

messages = removeDuplicates(messages);

let genPath = path.join(this.options.genDir, 'messages.xmb');
let genPath = path.join(this._options.genDir, 'messages.xmb');
let msgBundle = serializeXmb(messages);

this.host.writeFile(genPath, msgBundle, false);
Expand Down
13 changes: 9 additions & 4 deletions modules/@angular/compiler/src/i18n/expander.ts
Expand Up @@ -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>
* ```
*/
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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)];
Expand Down
19 changes: 10 additions & 9 deletions modules/@angular/compiler/src/i18n/i18n_html_parser.ts
Expand Up @@ -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);
Expand All @@ -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[] {
Expand All @@ -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[] {
Expand Down
50 changes: 24 additions & 26 deletions modules/@angular/compiler/src/i18n/message_extractor.ts
Expand Up @@ -90,44 +90,44 @@ 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) {
return new ExtractionResult([], res.errors);
} 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));
}
}

Expand All @@ -145,24 +145,22 @@ 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;
}
}
});

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)));
}
}