X Tutup
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion modules/angular2/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
*/
export * from './src/i18n/message';
export * from './src/i18n/xmb_serializer';
export * from './src/i18n/message_extractor';
export * from './src/i18n/message_extractor';
export * from './src/i18n/i18n_html_parser';
123 changes: 60 additions & 63 deletions modules/angular2/src/i18n/i18n_html_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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 = [];
Expand Down Expand Up @@ -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[] {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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 (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}'`);
}
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;

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);
Expand Down Expand Up @@ -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>`;
});
}
4 changes: 2 additions & 2 deletions modules/angular2/src/i18n/message_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {Message, id} from './message';
import {
I18nError,
Part,
I18N_ATTR_PREFIX,
partition,
meaning,
description,
isI18nAttr,
stringifyNodes,
messageFromAttribute
} from './shared';
Expand Down Expand Up @@ -161,7 +161,7 @@ export class MessageExtractor {

private _extractMessagesFromAttributes(p: HtmlElementAst): void {
p.attrs.forEach(attr => {
if (isI18nAttr(attr.name)) {
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
try {
this.messages.push(messageFromAttribute(this._parser, p, attr));
} catch (e) {
Expand Down
8 changes: 2 additions & 6 deletions modules/angular2/src/i18n/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {Message} from './message';
import {Parser} from 'angular2/src/core/change_detection/parser/parser';

const I18N_ATTR = "i18n";
const I18N_ATTR_PREFIX = "i18n-";
export const I18N_ATTR = "i18n";
export const I18N_ATTR_PREFIX = "i18n-";

/**
* An i18n error.
Expand Down Expand Up @@ -80,10 +80,6 @@ function _isClosingComment(n: HtmlAst): boolean {
return n instanceof HtmlCommentAst && isPresent(n.value) && n.value == "/i18n";
}

export function isI18nAttr(n: string): boolean {
return n.startsWith(I18N_ATTR_PREFIX);
}

function _findI18nAttr(p: HtmlElementAst): HtmlAttrAst {
let i18n = p.attrs.filter(a => a.name == I18N_ATTR);
return i18n.length == 0 ? null : i18n[0];
Expand Down
89 changes: 86 additions & 3 deletions modules/angular2/src/i18n/xmb_serializer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,95 @@
import {isPresent} from 'angular2/src/facade/lang';
import {isPresent, isBlank, RegExpWrapper} from 'angular2/src/facade/lang';
import {HtmlAst, HtmlElementAst} from 'angular2/src/compiler/html_ast';
import {Message, id} from './message';
import {HtmlParser} from 'angular2/src/compiler/html_parser';
import {ParseSourceSpan, ParseError} from 'angular2/src/compiler/parse_util';

export function serialize(messages: Message[]): string {
let _PLACEHOLDER_REGEXP = RegExpWrapper.create(`\\<ph(\\s)+name=("(\\w)+")\\/\\>`);
const _ID_ATTR = "id";
const _MSG_ELEMENT = "msg";
const _BUNDLE_ELEMENT = "message-bundle";

export function serializeXmb(messages: Message[]): string {
let ms = messages.map((m) => _serializeMessage(m)).join("");
return `<message-bundle>${ms}</message-bundle>`;
}

export class XmbDeserializationResult {
constructor(public content: string, public messages: {[key: string]: HtmlAst[]},
public errors: ParseError[]) {}
}

export class XmbDeserializationError extends ParseError {
constructor(span: ParseSourceSpan, msg: string) { super(span, msg); }
}

export function deserializeXmb(content: string, url: string): XmbDeserializationResult {
let parser = new HtmlParser();
let normalizedContent = _expandPlaceholder(content.trim());
let parsed = parser.parse(normalizedContent, url);

if (parsed.errors.length > 0) {
return new XmbDeserializationResult(null, {}, parsed.errors);
}

if (_checkRootElement(parsed.rootNodes)) {
return new XmbDeserializationResult(
null, {}, [new XmbDeserializationError(null, `Missing element "${_BUNDLE_ELEMENT}"`)]);
}

let bundleEl = <HtmlElementAst>parsed.rootNodes[0]; // test this
let errors = [];
let messages: {[key: string]: HtmlAst[]} = {};

_createMessages(bundleEl.children, messages, errors);

return (errors.length == 0) ?
new XmbDeserializationResult(normalizedContent, messages, []) :
new XmbDeserializationResult(null, <{[key: string]: HtmlAst[]}>{}, errors);
}

function _checkRootElement(nodes: HtmlAst[]): boolean {
return nodes.length < 1 || !(nodes[0] instanceof HtmlElementAst) ||
(<HtmlElementAst>nodes[0]).name != _BUNDLE_ELEMENT;
}

function _createMessages(nodes: HtmlAst[], messages: {[key: string]: HtmlAst[]},
errors: ParseError[]): void {
nodes.forEach((item) => {
if (item instanceof HtmlElementAst) {
let msg = <HtmlElementAst>item;

if (msg.name != _MSG_ELEMENT) {
errors.push(
new XmbDeserializationError(item.sourceSpan, `Unexpected element "${msg.name}"`));
return;
}

let id = _id(msg);
if (isBlank(id)) {
errors.push(
new XmbDeserializationError(item.sourceSpan, `"${_ID_ATTR}" attribute is missing`));
return;
}

messages[id] = msg.children;
}
});
}

function _id(el: HtmlElementAst): string {
let ids = el.attrs.filter(a => a.name == _ID_ATTR);
return ids.length > 0 ? ids[0].value : null;
}

function _serializeMessage(m: Message): string {
let desc = isPresent(m.description) ? ` desc='${m.description}'` : "";
return `<msg id='${id(m)}'${desc}>${m.content}</msg>`;
}
}

function _expandPlaceholder(input: string): string {
return RegExpWrapper.replaceAll(_PLACEHOLDER_REGEXP, input, (match) => {
let nameWithQuotes = match[2];
return `<ph name=${nameWithQuotes}></ph>`;
});
}
Loading
X Tutup