feat(i18n): update I18nHtmlParser to accept parsed messages

This commit is contained in:
vsavkin
2016-03-27 18:31:02 -07:00
committed by Victor Savkin
parent d7e1175df0
commit 756121acc1
4 changed files with 84 additions and 84 deletions

View File

@ -17,19 +17,19 @@ import {Message, id} from './message';
import {
messageFromAttribute,
I18nError,
isI18nAttr,
I18N_ATTR_PREFIX,
I18N_ATTR,
partition,
Part,
stringifyNodes,
meaning
} from './shared';
const I18N_ATTR = "i18n";
const PLACEHOLDER_ELEMENT = "ph";
const NAME_ATTR = "name";
const I18N_ATTR_PREFIX = "i18n-";
let PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\/\\>`);
let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\>\\<\\/ph\\>`);
const _I18N_ATTR = "i18n";
const _PLACEHOLDER_ELEMENT = "ph";
const _NAME_ATTR = "name";
const _I18N_ATTR_PREFIX = "i18n-";
let _PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+")\\>\\<\\/ph\\>`);
/**
* Creates an i18n-ed version of the parsed template.
@ -94,7 +94,7 @@ let PLACEHOLDER_EXPANDED_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\d)+
* This is how the merging works:
*
* 1. Use the stringify function to get the message id. Look up the message in the map.
* 2. Parse the translated message. At this point we have two trees: the original tree
* 2. Get the translated message. At this point we have two trees: the original tree
* and the translated tree, where all the elements are replaced with placeholders.
* 3. Use the original tree to create a mapping Index:number -> HtmlAst.
* 4. Walk the translated tree.
@ -115,7 +115,7 @@ export class I18nHtmlParser implements HtmlParser {
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
private _messages: {[key: string]: string}) {}
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
parse(sourceContent: string, sourceUrl: string): HtmlParseTreeResult {
this.errors = [];
@ -149,17 +149,8 @@ export class I18nHtmlParser implements HtmlParser {
throw new I18nError(p.sourceSpan, `Cannot find message for id '${messageId}'`);
}
// get the message and expand a placeholder so <ph/> becomes <ph></ph>
// we need to do it cause we use HtmlParser to parse the message
let message = _expandPlaceholder(this._messages[messageId]);
let parsedMessage = this._htmlParser.parse(message, "source");
if (parsedMessage.errors.length > 0) {
this.errors = this.errors.concat(parsedMessage.errors);
return [];
} else {
return this._mergeTrees(p, message, parsedMessage.rootNodes, p.children);
}
let parsedMessage = this._messages[messageId];
return this._mergeTrees(p, parsedMessage, p.children);
}
private _recurseIntoI18nPart(p: Part): HtmlAst[] {
@ -189,14 +180,13 @@ export class I18nHtmlParser implements HtmlParser {
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
}
private _mergeTrees(p: Part, translatedSource: string, translated: HtmlAst[],
original: HtmlAst[]): HtmlAst[] {
private _mergeTrees(p: Part, translated: HtmlAst[], original: HtmlAst[]): HtmlAst[] {
let l = new _CreateNodeMapping();
htmlVisitAll(l, original);
// merge the translated tree with the original tree.
// we do it by preserving the source code position of the original tree
let merged = this._mergeTreesHelper(translatedSource, translated, l.mapping);
let merged = this._mergeTreesHelper(translated, l.mapping);
// if the root element is present, we need to create a new root element with its attributes
// translated
@ -217,11 +207,10 @@ export class I18nHtmlParser implements HtmlParser {
}
}
private _mergeTreesHelper(translatedSource: string, translated: HtmlAst[],
mapping: HtmlAst[]): HtmlAst[] {
private _mergeTreesHelper(translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst[] {
return translated.map(t => {
if (t instanceof HtmlElementAst) {
return this._mergeElementOrInterpolation(t, translatedSource, translated, mapping);
return this._mergeElementOrInterpolation(t, translated, mapping);
} else if (t instanceof HtmlTextAst) {
return t;
@ -232,52 +221,51 @@ export class I18nHtmlParser implements HtmlParser {
});
}
private _mergeElementOrInterpolation(t: HtmlElementAst, translatedSource: string,
translated: HtmlAst[], mapping: HtmlAst[]): HtmlAst {
private _mergeElementOrInterpolation(t: HtmlElementAst, translated: HtmlAst[],
mapping: HtmlAst[]): HtmlAst {
let name = this._getName(t);
let type = name[0];
let index = NumberWrapper.parseInt(name.substring(1), 10);
let originalNode = mapping[index];
if (type == "t") {
return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode, translatedSource);
return this._mergeTextInterpolation(t, <HtmlTextAst>originalNode);
} else if (type == "e") {
return this._mergeElement(t, <HtmlElementAst>originalNode, mapping, translatedSource);
return this._mergeElement(t, <HtmlElementAst>originalNode, mapping);
} else {
throw new BaseException("should not be reached");
}
}
private _getName(t: HtmlElementAst): string {
if (t.name != PLACEHOLDER_ELEMENT) {
if (t.name != _PLACEHOLDER_ELEMENT) {
throw new I18nError(
t.sourceSpan,
`Unexpected tag "${t.name}". Only "${PLACEHOLDER_ELEMENT}" tags are allowed.`);
`Unexpected tag "${t.name}". Only "${_PLACEHOLDER_ELEMENT}" tags are allowed.`);
}
let names = t.attrs.filter(a => a.name == NAME_ATTR);
let names = t.attrs.filter(a => a.name == _NAME_ATTR);
if (names.length == 0) {
throw new I18nError(t.sourceSpan, `Missing "${NAME_ATTR}" attribute.`);
throw new I18nError(t.sourceSpan, `Missing "${_NAME_ATTR}" attribute.`);
}
return names[0].value;
}
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst,
translatedSource: string): HtmlTextAst {
private _mergeTextInterpolation(t: HtmlElementAst, originalNode: HtmlTextAst): HtmlTextAst {
let split =
this._parser.splitInterpolation(originalNode.value, originalNode.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let messageSubstring =
translatedSource.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
this._messagesContent.substring(t.startSourceSpan.end.offset, t.endSourceSpan.start.offset);
let translated =
this._replacePlaceholdersWithExpressions(messageSubstring, exps, originalNode.sourceSpan);
return new HtmlTextAst(translated, originalNode.sourceSpan);
}
private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst, mapping: HtmlAst[],
translatedSource: string): HtmlElementAst {
let children = this._mergeTreesHelper(translatedSource, t.children, mapping);
private _mergeElement(t: HtmlElementAst, originalNode: HtmlElementAst,
mapping: HtmlAst[]): HtmlElementAst {
let children = this._mergeTreesHelper(t.children, mapping);
return new HtmlElementAst(originalNode.name, this._i18nAttributes(originalNode), children,
originalNode.sourceSpan, originalNode.startSourceSpan,
originalNode.endSourceSpan);
@ -286,30 +274,46 @@ export class I18nHtmlParser implements HtmlParser {
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
let res = [];
el.attrs.forEach(attr => {
if (isI18nAttr(attr.name)) {
let messageId = id(messageFromAttribute(this._parser, el, attr));
let expectedName = attr.name.substring(5);
let m = el.attrs.filter(a => a.name == expectedName)[0];
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
if (StringMapWrapper.contains(this._messages, messageId)) {
let split = this._parser.splitInterpolation(m.value, m.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let message = this._replacePlaceholdersWithExpressions(
_expandPlaceholder(this._messages[messageId]), exps, m.sourceSpan);
res.push(new HtmlAttrAst(m.name, message, m.sourceSpan));
} else {
throw new I18nError(m.sourceSpan, `Cannot find message for id '${messageId}'`);
}
let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
if (i18ns.length == 0) {
res.push(attr);
return;
}
let i18n = i18ns[0];
let messageId = id(messageFromAttribute(this._parser, el, i18n));
if (StringMapWrapper.contains(this._messages, messageId)) {
let updatedMessage = this._replaceInterpolationInAttr(attr, this._messages[messageId]);
res.push(new HtmlAttrAst(attr.name, updatedMessage, attr.sourceSpan));
} else {
throw new I18nError(attr.sourceSpan, `Cannot find message for id '${messageId}'`);
}
});
return res;
}
private _replaceInterpolationInAttr(attr: HtmlAttrAst, msg: HtmlAst[]): string {
let split = this._parser.splitInterpolation(attr.value, attr.sourceSpan.toString());
let exps = isPresent(split) ? split.expressions : [];
let first = msg[0];
let last = msg[msg.length - 1];
let start = first.sourceSpan.start.offset;
let end =
last instanceof HtmlElementAst ? last.endSourceSpan.end.offset : last.sourceSpan.end.offset;
let messageSubstring = this._messagesContent.substring(start, end);
return this._replacePlaceholdersWithExpressions(messageSubstring, exps, attr.sourceSpan);
};
private _replacePlaceholdersWithExpressions(message: string, exps: string[],
sourceSpan: ParseSourceSpan): string {
return RegExpWrapper.replaceAll(PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
return RegExpWrapper.replaceAll(_PLACEHOLDER_EXPANDED_REGEXP, message, (match) => {
let nameWithQuotes = match[2];
let name = nameWithQuotes.substring(1, nameWithQuotes.length - 1);
let index = NumberWrapper.parseInt(name, 10);
@ -343,11 +347,4 @@ class _CreateNodeMapping implements HtmlAstVisitor {
}
visitComment(ast: HtmlCommentAst, context: any): any { return ""; }
}
function _expandPlaceholder(input: string): string {
return RegExpWrapper.replaceAll(PLACEHOLDER_REGEXP, input, (match) => {
let nameWithQuotes = match[2];
return `<ph name=${nameWithQuotes}></ph>`;
});
}