feat(ICU): extract ICU messages
This commit is contained in:
parent
3c3e9ddb10
commit
28e8b2faab
@ -117,7 +117,10 @@ export class I18nHtmlParser implements HtmlParser {
|
|||||||
|
|
||||||
// Look for the translated message and merge it back to the tree
|
// Look for the translated message and merge it back to the tree
|
||||||
private _mergeI18Part(part: Part): HtmlAst[] {
|
private _mergeI18Part(part: Part): HtmlAst[] {
|
||||||
let message = part.createMessage(this._expressionParser, this._interpolationConfig);
|
let messages = part.createMessages(this._expressionParser, this._interpolationConfig);
|
||||||
|
// TODO - dirty smoke fix
|
||||||
|
let message = messages[0];
|
||||||
|
|
||||||
let messageId = id(message);
|
let messageId = id(message);
|
||||||
|
|
||||||
if (!StringMapWrapper.contains(this._messages, messageId)) {
|
if (!StringMapWrapper.contains(this._messages, messageId)) {
|
||||||
|
@ -123,7 +123,7 @@ export class MessageExtractor {
|
|||||||
|
|
||||||
private _extractMessagesFromPart(part: Part, interpolationConfig: InterpolationConfig): void {
|
private _extractMessagesFromPart(part: Part, interpolationConfig: InterpolationConfig): void {
|
||||||
if (part.hasI18n) {
|
if (part.hasI18n) {
|
||||||
this._messages.push(part.createMessage(this._expressionParser, interpolationConfig));
|
this._messages.push(...part.createMessages(this._expressionParser, interpolationConfig));
|
||||||
this._recurseToExtractMessagesFromAttributes(part.children, interpolationConfig);
|
this._recurseToExtractMessagesFromAttributes(part.children, interpolationConfig);
|
||||||
} else {
|
} else {
|
||||||
this._recurse(part.children, interpolationConfig);
|
this._recurse(part.children, interpolationConfig);
|
||||||
|
@ -74,10 +74,12 @@ export class Part {
|
|||||||
this.children[0].sourceSpan.start, this.children[this.children.length - 1].sourceSpan.end);
|
this.children[0].sourceSpan.start, this.children[this.children.length - 1].sourceSpan.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
createMessage(parser: ExpressionParser, interpolationConfig: InterpolationConfig): Message {
|
createMessages(parser: ExpressionParser, interpolationConfig: InterpolationConfig): Message[] {
|
||||||
return new Message(
|
let {message, icuMessages} = stringifyNodes(this.children, parser, interpolationConfig);
|
||||||
stringifyNodes(this.children, parser, interpolationConfig), meaning(this.i18n),
|
return [
|
||||||
description(this.i18n));
|
new Message(message, meaning(this.i18n), description(this.i18n)),
|
||||||
|
...icuMessages.map(icu => new Message(icu, null))
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,28 +199,33 @@ export function dedupePhName(usedNames: Map<string, number>, name: string): stri
|
|||||||
*/
|
*/
|
||||||
export function stringifyNodes(
|
export function stringifyNodes(
|
||||||
nodes: HtmlAst[], expressionParser: ExpressionParser,
|
nodes: HtmlAst[], expressionParser: ExpressionParser,
|
||||||
interpolationConfig: InterpolationConfig): string {
|
interpolationConfig: InterpolationConfig): {message: string, icuMessages: string[]} {
|
||||||
const visitor = new _StringifyVisitor(expressionParser, interpolationConfig);
|
const visitor = new _StringifyVisitor(expressionParser, interpolationConfig);
|
||||||
return htmlVisitAll(visitor, nodes).join('');
|
const icuMessages: string[] = [];
|
||||||
|
const message = htmlVisitAll(visitor, nodes, icuMessages).join('');
|
||||||
|
return {message, icuMessages};
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StringifyVisitor implements HtmlAstVisitor {
|
class _StringifyVisitor implements HtmlAstVisitor {
|
||||||
private _index: number = 0;
|
private _index: number = 0;
|
||||||
|
private _nestedExpansion = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _parser: ExpressionParser, private _interpolationConfig: InterpolationConfig) {}
|
private _expressionParser: ExpressionParser,
|
||||||
|
private _interpolationConfig: InterpolationConfig) {}
|
||||||
|
|
||||||
visitElement(ast: HtmlElementAst, context: any): any {
|
visitElement(ast: HtmlElementAst, context: any): any {
|
||||||
let name = this._index++;
|
const index = this._index++;
|
||||||
let children = this._join(htmlVisitAll(this, ast.children), '');
|
const children = this._join(htmlVisitAll(this, ast.children), '');
|
||||||
return `<ph name="e${name}">${children}</ph>`;
|
return `<ph name="e${index}">${children}</ph>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
visitAttr(ast: HtmlAttrAst, context: any): any { return null; }
|
||||||
|
|
||||||
visitText(ast: HtmlTextAst, context: any): any {
|
visitText(ast: HtmlTextAst, context: any): any {
|
||||||
let index = this._index++;
|
const index = this._index++;
|
||||||
let noInterpolation =
|
const noInterpolation = removeInterpolation(
|
||||||
removeInterpolation(ast.value, ast.sourceSpan, this._parser, this._interpolationConfig);
|
ast.value, ast.sourceSpan, this._expressionParser, this._interpolationConfig);
|
||||||
if (noInterpolation != ast.value) {
|
if (noInterpolation != ast.value) {
|
||||||
return `<ph name="t${index}">${noInterpolation}</ph>`;
|
return `<ph name="t${index}">${noInterpolation}</ph>`;
|
||||||
}
|
}
|
||||||
@ -227,9 +234,19 @@ class _StringifyVisitor implements HtmlAstVisitor {
|
|||||||
|
|
||||||
visitComment(ast: HtmlCommentAst, context: any): any { return ''; }
|
visitComment(ast: HtmlCommentAst, context: any): any { return ''; }
|
||||||
|
|
||||||
visitExpansion(ast: HtmlExpansionAst, context: any): any { return null; }
|
visitExpansion(ast: HtmlExpansionAst, context: any): any {
|
||||||
|
const index = this._index++;
|
||||||
|
this._nestedExpansion++;
|
||||||
|
const content = `{${ast.switchValue}, ${ast.type}${htmlVisitAll(this, ast.cases).join('')}}`;
|
||||||
|
this._nestedExpansion--;
|
||||||
|
|
||||||
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any { return null; }
|
return this._nestedExpansion == 0 ? `<ph name="x${index}">${content}</ph>` : content;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitExpansionCase(ast: HtmlExpansionCaseAst, context: any): any {
|
||||||
|
const expStr = htmlVisitAll(this, ast.expression).join('');
|
||||||
|
return ` ${ast.value} {${expStr}}`;
|
||||||
|
}
|
||||||
|
|
||||||
private _join(strs: string[], str: string): string {
|
private _join(strs: string[], str: string): string {
|
||||||
return strs.filter(s => s.length > 0).join(str);
|
return strs.filter(s => s.length > 0).join(str);
|
||||||
|
@ -46,25 +46,7 @@ export function main() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should replace attributes', () => {
|
describe('interpolation', () => {
|
||||||
let translations: {[key: string]: string} = {};
|
|
||||||
translations[id(new Message('some message', 'meaning', null))] = 'another message';
|
|
||||||
|
|
||||||
expect(
|
|
||||||
humanizeDom(parse(
|
|
||||||
'<div value=\'some message\' i18n-value=\'meaning|comment\'></div>', translations)))
|
|
||||||
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'another message']]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should replace elements with the i18n attr', () => {
|
|
||||||
let translations: {[key: string]: string} = {};
|
|
||||||
translations[id(new Message('message', 'meaning', null))] = 'another message';
|
|
||||||
|
|
||||||
expect(humanizeDom(parse('<div i18n=\'meaning|desc\'>message</div>', translations))).toEqual([
|
|
||||||
[HtmlElementAst, 'div', 0], [HtmlTextAst, 'another message', 1]
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle interpolation', () => {
|
it('should handle interpolation', () => {
|
||||||
let translations: {[key: string]: string} = {};
|
let translations: {[key: string]: string} = {};
|
||||||
translations[id(new Message(
|
translations[id(new Message(
|
||||||
@ -120,20 +102,6 @@ export function main() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle nested html', () => {
|
|
||||||
let translations: {[key: string]: string} = {};
|
|
||||||
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
|
|
||||||
'<ph name="e2">B</ph><ph name="e0">A</ph>';
|
|
||||||
|
|
||||||
expect(humanizeDom(parse('<div i18n><a>a</a><b>b</b></div>', translations))).toEqual([
|
|
||||||
[HtmlElementAst, 'div', 0],
|
|
||||||
[HtmlElementAst, 'b', 1],
|
|
||||||
[HtmlTextAst, 'B', 2],
|
|
||||||
[HtmlElementAst, 'a', 1],
|
|
||||||
[HtmlTextAst, 'A', 2],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support interpolation', () => {
|
it('should support interpolation', () => {
|
||||||
let translations: {[key: string]: string} = {};
|
let translations: {[key: string]: string} = {};
|
||||||
translations[id(new Message(
|
translations[id(new Message(
|
||||||
@ -148,6 +116,22 @@ export function main() {
|
|||||||
[HtmlTextAst, 'A', 2],
|
[HtmlTextAst, 'A', 2],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('html', () => {
|
||||||
|
it('should handle nested html', () => {
|
||||||
|
let translations: {[key: string]: string} = {};
|
||||||
|
translations[id(new Message('<ph name="e0">a</ph><ph name="e2">b</ph>', null, null))] =
|
||||||
|
'<ph name="e2">B</ph><ph name="e0">A</ph>';
|
||||||
|
|
||||||
|
expect(humanizeDom(parse('<div i18n><a>a</a><b>b</b></div>', translations))).toEqual([
|
||||||
|
[HtmlElementAst, 'div', 0],
|
||||||
|
[HtmlElementAst, 'b', 1],
|
||||||
|
[HtmlTextAst, 'B', 2],
|
||||||
|
[HtmlElementAst, 'a', 1],
|
||||||
|
[HtmlTextAst, 'A', 2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should i18n attributes of placeholder elements', () => {
|
it('should i18n attributes of placeholder elements', () => {
|
||||||
let translations: {[key: string]: string} = {};
|
let translations: {[key: string]: string} = {};
|
||||||
@ -173,6 +157,25 @@ export function main() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should replace attributes', () => {
|
||||||
|
let translations: {[key: string]: string} = {};
|
||||||
|
translations[id(new Message('some message', 'meaning', null))] = 'another message';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
humanizeDom(parse(
|
||||||
|
'<div value=\'some message\' i18n-value=\'meaning|comment\'></div>', translations)))
|
||||||
|
.toEqual([[HtmlElementAst, 'div', 0], [HtmlAttrAst, 'value', 'another message']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace elements with the i18n attr', () => {
|
||||||
|
let translations: {[key: string]: string} = {};
|
||||||
|
translations[id(new Message('message', 'meaning', null))] = 'another message';
|
||||||
|
|
||||||
|
expect(humanizeDom(parse('<div i18n=\'meaning|desc\'>message</div>', translations)))
|
||||||
|
.toEqual([[HtmlElementAst, 'div', 0], [HtmlTextAst, 'another message', 1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should extract from partitions', () => {
|
it('should extract from partitions', () => {
|
||||||
let translations: {[key: string]: string} = {};
|
let translations: {[key: string]: string} = {};
|
||||||
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
|
translations[id(new Message('message1', 'meaning1', null))] = 'another message1';
|
||||||
|
@ -21,40 +21,9 @@ export function main() {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const expParser = new ExpressionParser(new ExpressionLexer());
|
const expParser = new ExpressionParser(new ExpressionLexer());
|
||||||
const htmlParser = new HtmlParser();
|
const htmlParser = new HtmlParser();
|
||||||
// TODO: pass expression parser
|
|
||||||
extractor = new MessageExtractor(htmlParser, expParser, ['i18n-tag'], {'i18n-el': ['trans']});
|
extractor = new MessageExtractor(htmlParser, expParser, ['i18n-tag'], {'i18n-el': ['trans']});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should extract from elements with the i18n attr', () => {
|
|
||||||
let res = extractor.extract('<div i18n=\'meaning|desc\'>message</div>', 'someurl');
|
|
||||||
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract from elements with the i18n attr without a desc', () => {
|
|
||||||
let res = extractor.extract('<div i18n=\'meaning\'>message</div>', 'someurl');
|
|
||||||
expect(res.messages).toEqual([new Message('message', 'meaning', null)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract from elements with the i18n attr without a meaning', () => {
|
|
||||||
let res = extractor.extract('<div i18n>message</div>', 'someurl');
|
|
||||||
expect(res.messages).toEqual([new Message('message', null, null)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract from attributes', () => {
|
|
||||||
let res = extractor.extract(
|
|
||||||
`
|
|
||||||
<div
|
|
||||||
title1='message1' i18n-title1='meaning1|desc1'
|
|
||||||
title2='message2' i18n-title2='meaning2|desc2'>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
'someurl');
|
|
||||||
|
|
||||||
expect(res.messages).toEqual([
|
|
||||||
new Message('message1', 'meaning1', 'desc1'), new Message('message2', 'meaning2', 'desc2')
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should extract from partitions', () => {
|
it('should extract from partitions', () => {
|
||||||
let res = extractor.extract(
|
let res = extractor.extract(
|
||||||
`
|
`
|
||||||
@ -79,6 +48,39 @@ export function main() {
|
|||||||
expect(res.messages).toEqual([new Message('message1', 'meaning1', 'desc1')]);
|
expect(res.messages).toEqual([new Message('message1', 'meaning1', 'desc1')]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ICU messages', () => {
|
||||||
|
it('should replace icu messages with placeholders', () => {
|
||||||
|
let res = extractor.extract('<div i18n>{count, plural, =0 {text} }</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message(
|
||||||
|
'<ph name="x0">{count, plural =0 {text}}</ph>', null, null)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace HTML with placeholders in ICU cases', () => {
|
||||||
|
let res =
|
||||||
|
extractor.extract('<div i18n>{count, plural, =0 {<p>html</p>} }</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message(
|
||||||
|
'<ph name="x0">{count, plural =0 {<ph name="e1">html</ph>}}</ph>', null, null)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace interpolation with placeholders in ICU cases', () => {
|
||||||
|
let res =
|
||||||
|
extractor.extract('<div i18n>{count, plural, =0 {{{interpolation}}}}</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message(
|
||||||
|
'<ph name="x0">{count, plural =0 {<ph name="t1"><ph name="INTERPOLATION_0"/></ph>}}</ph>',
|
||||||
|
null, null)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not replace nested interpolation with placeholders in ICU cases', () => {
|
||||||
|
let res = extractor.extract(
|
||||||
|
'<div i18n>{count, plural, =0 {{sex, gender, =m {{{he}}} =f {<b>she</b>}}}}</div>',
|
||||||
|
'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message(
|
||||||
|
'<ph name="x0">{count, plural =0 {{sex, gender =m {<ph name="t2"><ph name="INTERPOLATION_0"/></ph>} =f {<ph name="e3">she</ph>}}}}</ph>',
|
||||||
|
null, null)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('interpolation', () => {
|
||||||
it('should replace interpolation with placeholders (text nodes)', () => {
|
it('should replace interpolation with placeholders (text nodes)', () => {
|
||||||
let res = extractor.extract('<div i18n>Hi {{one}} and {{two}}</div>', 'someurl');
|
let res = extractor.extract('<div i18n>Hi {{one}} and {{two}}</div>', 'someurl');
|
||||||
expect(res.messages).toEqual([new Message(
|
expect(res.messages).toEqual([new Message(
|
||||||
@ -111,7 +113,9 @@ export function main() {
|
|||||||
expect(res.messages).toEqual([new Message(
|
expect(res.messages).toEqual([new Message(
|
||||||
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
|
'Hi <ph name="FIRST"/> and <ph name="SECOND"/>', null, null)]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('placehoders', () => {
|
||||||
it('should match named placeholders with extra spacing', () => {
|
it('should match named placeholders with extra spacing', () => {
|
||||||
let res = extractor.extract(
|
let res = extractor.extract(
|
||||||
`
|
`
|
||||||
@ -129,7 +133,40 @@ export function main() {
|
|||||||
i18n-title></div>`,
|
i18n-title></div>`,
|
||||||
'someurl');
|
'someurl');
|
||||||
expect(res.messages).toEqual([new Message(
|
expect(res.messages).toEqual([new Message(
|
||||||
'Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>', null, null)]);
|
'Hi <ph name="FIRST"/> and <ph name="FIRST_1"/> and <ph name="FIRST_2"/>', null,
|
||||||
|
null)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('html', () => {
|
||||||
|
it('should extract from elements with the i18n attr', () => {
|
||||||
|
let res = extractor.extract('<div i18n=\'meaning|desc\'>message</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message('message', 'meaning', 'desc')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract from elements with the i18n attr without a desc', () => {
|
||||||
|
let res = extractor.extract('<div i18n=\'meaning\'>message</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message('message', 'meaning', null)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract from elements with the i18n attr without a meaning', () => {
|
||||||
|
let res = extractor.extract('<div i18n>message</div>', 'someurl');
|
||||||
|
expect(res.messages).toEqual([new Message('message', null, null)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract from attributes', () => {
|
||||||
|
let res = extractor.extract(
|
||||||
|
`
|
||||||
|
<div
|
||||||
|
title1='message1' i18n-title1='meaning1|desc1'
|
||||||
|
title2='message2' i18n-title2='meaning2|desc2'>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
'someurl');
|
||||||
|
|
||||||
|
expect(res.messages).toEqual([
|
||||||
|
new Message('message1', 'meaning1', 'desc1'), new Message('message2', 'meaning2', 'desc2')
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle html content', () => {
|
it('should handle html content', () => {
|
||||||
@ -164,6 +201,7 @@ export function main() {
|
|||||||
new Message('value', 'meaning', 'desc')
|
new Message('value', 'meaning', 'desc')
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove duplicate messages', () => {
|
it('should remove duplicate messages', () => {
|
||||||
let res = extractor.extract(
|
let res = extractor.extract(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user