angular-docs-cn/modules/angular2/src/i18n/message_extractor.ts

261 lines
7.8 KiB
TypeScript
Raw Normal View History

import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from 'angular2/src/compiler/html_ast';
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {Parser} from 'angular2/src/core/change_detection/parser/parser';
import {Interpolation} from 'angular2/src/core/change_detection/parser/ast';
const I18N_ATTR = "i18n";
const I18N_ATTR_PREFIX = "i18n-";
/**
* A message extracted from a template.
*
* The identity of a message is comprised of `content` and `meaning`.
*
* `description` is additional information provided to the translator.
*/
export class Message {
constructor(public content: string, public meaning: string, public description: string) {}
}
/**
* All messages extracted from a template.
*/
export class ExtractionResult {
constructor(public messages: Message[], public errors: ParseError[]) {}
}
/**
* An extraction error.
*/
export class I18nExtractionError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}
/**
* Removes duplicate messages.
*
* E.g.
*
* ```
* var m = [new Message("message", "meaning", "desc1"), new Message("message", "meaning",
* "desc2")];
* expect(removeDuplicates(m)).toEqual([new Message("message", "meaning", "desc1")]);
* ```
*/
export function removeDuplicates(messages: Message[]): Message[] {
let uniq: {[key: string]: Message} = {};
messages.forEach(m => {
let key = `$ng__${m.meaning}__|${m.content}`;
if (!StringMapWrapper.contains(uniq, key)) {
uniq[key] = m;
}
});
return StringMapWrapper.values(uniq);
}
/**
* Extracts all messages from a template.
*
* It works like this. First, the extractor uses the provided html parser to get
* the html AST of the template. Then it partitions the root nodes into parts.
* Everything between two i18n comments becomes a single part. Every other nodes becomes
* a part too.
*
* We process every part as follows. Say we have a part A.
*
* If the part has the i18n attribute, it gets converted into a message.
* And we do not recurse into that part, except to extract messages from the attributes.
*
* If the part doesn't have the i18n attribute, we recurse into that part and
* partition its children.
*
* While walking the AST we also remove i18n attributes from messages.
*/
export class MessageExtractor {
messages: Message[];
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser) {}
extract(template: string, sourceUrl: string): ExtractionResult {
this.messages = [];
this.errors = [];
let res = this._htmlParser.parse(template, sourceUrl);
if (res.errors.length > 0) {
return new ExtractionResult([], res.errors);
} else {
let ps = this._partition(res.rootNodes);
ps.forEach(p => this._extractMessagesFromPart(p));
return new ExtractionResult(this.messages, this.errors);
}
}
private _extractMessagesFromPart(p: _Part): void {
if (p.hasI18n) {
this.messages.push(new Message(_stringifyNodes(p.children, this._parser), _meaning(p.i18n),
_description(p.i18n)));
this._recurseToExtractMessagesFromAttributes(p.children);
} else {
this._recurse(p.children);
}
if (isPresent(p.rootElement)) {
this._extractMessagesFromAttributes(p.rootElement);
}
}
private _recurse(nodes: HtmlAst[]): void {
let ps = this._partition(nodes);
ps.forEach(p => this._extractMessagesFromPart(p));
}
private _recurseToExtractMessagesFromAttributes(nodes: HtmlAst[]): void {
nodes.forEach(n => {
if (n instanceof HtmlElementAst) {
this._extractMessagesFromAttributes(n);
this._recurseToExtractMessagesFromAttributes(n.children);
}
});
}
private _extractMessagesFromAttributes(p: HtmlElementAst): void {
p.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
let expectedName = attr.name.substring(5);
let matching = p.attrs.filter(a => a.name == expectedName);
if (matching.length > 0) {
let value = _removeInterpolation(matching[0].value, p.sourceSpan, this._parser);
this.messages.push(new Message(value, _meaning(attr.value), _description(attr.value)));
} else {
this.errors.push(
new I18nExtractionError(p.sourceSpan, `Missing attribute '${expectedName}'.`));
}
}
});
}
// Man, this is so ugly!
private _partition(nodes: HtmlAst[]): _Part[] {
let res = [];
for (let i = 0; i < nodes.length; ++i) {
let n = nodes[i];
let temp = [];
if (_isOpeningComment(n)) {
let i18n = (<HtmlCommentAst>n).value.substring(5).trim();
i++;
while (!_isClosingComment(nodes[i])) {
temp.push(nodes[i++]);
if (i === nodes.length) {
this.errors.push(
new I18nExtractionError(n.sourceSpan, "Missing closing 'i18n' comment."));
break;
}
}
res.push(new _Part(null, temp, i18n, true));
} else if (n instanceof HtmlElementAst) {
let i18n = _findI18nAttr(n);
res.push(new _Part(n, n.children, isPresent(i18n) ? i18n.value : null, isPresent(i18n)));
}
}
return res;
}
}
class _Part {
constructor(public rootElement: HtmlElementAst, public children: HtmlAst[], public i18n: string,
public hasI18n: boolean) {}
}
function _isOpeningComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value.startsWith("i18n:");
}
function _isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value == "/i18n";
}
function _stringifyNodes(nodes: HtmlAst[], parser: Parser) {
let visitor = new _StringifyVisitor(parser);
return htmlVisitAll(visitor, nodes).join("");
}
class _StringifyVisitor implements HtmlAstVisitor {
constructor(private _parser: Parser) {}
visitElement(ast: HtmlElementAst, context: any): any {
let attrs = this._join(htmlVisitAll(this, ast.attrs), " ");
let children = this._join(htmlVisitAll(this, ast.children), "");
return `<${ast.name} ${attrs}>${children}</${ast.name}>`;
}
visitAttr(ast: HtmlAttrAst, context: any): any {
if (ast.name.startsWith(I18N_ATTR_PREFIX)) {
return "";
} else {
return `${ast.name}="${ast.value}"`;
}
}
visitText(ast: HtmlTextAst, context: any): any {
return _removeInterpolation(ast.value, ast.sourceSpan, this._parser);
}
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
private _join(strs: string[], str: string): string {
return strs.filter(s => s.length > 0).join(str);
}
}
function _removeInterpolation(value: string, source: ParseSourceSpan, parser: Parser): string {
try {
let parsed = parser.parseInterpolation(value, source.toString());
if (isPresent(parsed)) {
let ast: Interpolation = <any>parsed.ast;
let res = "";
for (let i = 0; i < ast.strings.length; ++i) {
res += ast.strings[i];
if (i != ast.strings.length - 1) {
res += `{{I${i}}}`;
}
}
return res;
} else {
return value;
}
} catch (e) {
return value;
}
}
function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
return i18n.length == 0 ? null : i18n[0];
}
function _meaning(i18n: string): string {
if (isBlank(i18n) || i18n == "") return null;
return i18n.split("|")[0];
}
function _description(i18n: string): string {
if (isBlank(i18n) || i18n == "") return null;
let parts = i18n.split("|");
return parts.length > 1 ? parts[1] : null;
}