feat(i18n): support implicit tags/attributes

This commit is contained in:
Victor Berchet 2016-05-20 15:44:58 -07:00
parent 75e6dfb9ab
commit 3e5716ec16
6 changed files with 157 additions and 92 deletions

View File

@ -1,5 +1,5 @@
import {StringWrapper, NumberWrapper, isPresent, isBlank, serializeEnum} from '../src/facade/lang';
import {ListWrapper} from '../src/facade/collection';
import {StringWrapper, NumberWrapper, isPresent, isBlank} from './facade/lang';
import {ListWrapper} from './facade/collection';
import {ParseLocation, ParseError, ParseSourceFile, ParseSourceSpan} from './parse_util';
import {getHtmlTagDefinition, HtmlTagContentType, NAMED_ENTITIES} from './html_tags';

View File

@ -1,5 +1,5 @@
import {HtmlParser, HtmlParseTreeResult} from '../html_parser';
import {ParseSourceSpan, ParseError} from '../parse_util';
import {HtmlParser, HtmlParseTreeResult} from "../html_parser";
import {ParseSourceSpan, ParseError} from "../parse_util";
import {
HtmlAst,
HtmlAstVisitor,
@ -10,31 +10,28 @@ import {
HtmlExpansionAst,
HtmlExpansionCaseAst,
htmlVisitAll
} from '../html_ast';
import {ListWrapper, StringMapWrapper} from '../../src/facade/collection';
import {RegExpWrapper, NumberWrapper, isPresent} from '../../src/facade/lang';
import {BaseException} from '../../src/facade/exceptions';
import {Parser} from '../expression_parser/parser';
import {id} from './message';
import {expandNodes} from './expander';
} from "../html_ast";
import {ListWrapper, StringMapWrapper} from "../facade/collection";
import {RegExpWrapper, NumberWrapper, isPresent} from "../facade/lang";
import {BaseException} from "../facade/exceptions";
import {Parser} from "../expression_parser/parser";
import {id} from "./message";
import {expandNodes} from "./expander";
import {
messageFromAttribute,
messageFromI18nAttribute,
I18nError,
I18N_ATTR_PREFIX,
I18N_ATTR,
partition,
Part,
stringifyNodes,
meaning,
getPhNameFromBinding,
dedupePhName
} from './shared';
dedupePhName,
messageFromAttribute
} from "./shared";
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=("(\\w)+")\\>\\<\\/ph\\>`);
let _PLACEHOLDER_EXPANDED_REGEXP = /<ph(\s)+name=("(\w)+")><\/ph>/gi;
/**
* Creates an i18n-ed version of the parsed template.
@ -120,13 +117,15 @@ export class I18nHtmlParser implements HtmlParser {
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]}) {}
private _messagesContent: string, private _messages: {[key: string]: HtmlAst[]},
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
parse(sourceContent: string, sourceUrl: string,
parseExpansionForms: boolean = false): HtmlParseTreeResult {
this.errors = [];
let res = this._htmlParser.parse(sourceContent, sourceUrl, true);
if (res.errors.length > 0) {
return res;
} else {
@ -184,7 +183,7 @@ export class I18nHtmlParser implements HtmlParser {
}
private _recurse(nodes: HtmlAst[]): HtmlAst[] {
let ps = partition(nodes, this.errors);
let ps = partition(nodes, this.errors, this._implicitTags);
return ListWrapper.flatten(ps.map(p => this._processI18nPart(p)));
}
@ -281,17 +280,26 @@ export class I18nHtmlParser implements HtmlParser {
private _i18nAttributes(el: HtmlElementAst): HtmlAttrAst[] {
let res = [];
let implicitAttrs: string[] =
isPresent(this._implicitAttrs[el.name]) ? this._implicitAttrs[el.name] : [];
el.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX) || attr.name == I18N_ATTR) return;
let i18ns = el.attrs.filter(a => a.name == `i18n-${attr.name}`);
let message;
let i18ns = el.attrs.filter(a => a.name == `${I18N_ATTR_PREFIX}${attr.name}`);
if (i18ns.length == 0) {
res.push(attr);
return;
if (implicitAttrs.indexOf(attr.name) == -1) {
res.push(attr);
return;
}
message = messageFromAttribute(this._parser, attr);
} else {
message = messageFromI18nAttribute(this._parser, el, i18ns[0]);
}
let i18n = i18ns[0];
let message = messageFromAttribute(this._parser, el, i18n);
let messageId = id(message);
if (StringMapWrapper.contains(this._messages, messageId)) {

View File

@ -1,29 +1,19 @@
import {HtmlParser} from '../html_parser';
import {ParseSourceSpan, ParseError} from '../parse_util';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from '../html_ast';
import {isPresent} from '../../src/facade/lang';
import {StringMapWrapper} from '../../src/facade/collection';
import {Parser} from '../expression_parser/parser';
import {Message, id} from './message';
import {expandNodes} from './expander';
import {HtmlParser} from "../html_parser";
import {ParseError} from "../parse_util";
import {HtmlAst, HtmlElementAst} from "../html_ast";
import {isPresent} from "../facade/lang";
import {StringMapWrapper} from "../facade/collection";
import {Parser} from "../expression_parser/parser";
import {Message, id} from "./message";
import {expandNodes} from "./expander";
import {
I18nError,
Part,
I18N_ATTR_PREFIX,
partition,
meaning,
description,
stringifyNodes,
messageFromI18nAttribute,
messageFromAttribute
} from './shared';
} from "./shared";
/**
* All messages extracted from a template.
@ -116,7 +106,8 @@ export class MessageExtractor {
messages: Message[];
errors: ParseError[];
constructor(private _htmlParser: HtmlParser, private _parser: Parser) {}
constructor(private _htmlParser: HtmlParser, private _parser: Parser,
private _implicitTags: string[], private _implicitAttrs: {[k: string]: string[]}) {}
extract(template: string, sourceUrl: string): ExtractionResult {
this.messages = [];
@ -146,7 +137,7 @@ export class MessageExtractor {
private _recurse(nodes: HtmlAst[]): void {
if (isPresent(nodes)) {
let ps = partition(nodes, this.errors);
let ps = partition(nodes, this.errors, this._implicitTags);
ps.forEach(p => this._extractMessagesFromPart(p));
}
}
@ -161,10 +152,15 @@ export class MessageExtractor {
}
private _extractMessagesFromAttributes(p: HtmlElementAst): void {
let transAttrs: string[] =
isPresent(this._implicitAttrs[p.name]) ? this._implicitAttrs[p.name] : [];
let explicitAttrs: string[] = [];
p.attrs.forEach(attr => {
if (attr.name.startsWith(I18N_ATTR_PREFIX)) {
try {
this.messages.push(messageFromAttribute(this._parser, p, attr));
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);
@ -174,5 +170,10 @@ export class MessageExtractor {
}
}
});
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)));
}
}

View File

@ -27,7 +27,7 @@ export class I18nError extends ParseError {
// Man, this is so ugly!
export function partition(nodes: HtmlAst[], errors: ParseError[]): Part[] {
export function partition(nodes: HtmlAst[], errors: ParseError[], implicitTags: string[]): Part[] {
let res = [];
for (let i = 0; i < nodes.length; ++i) {
@ -47,7 +47,8 @@ export function partition(nodes: HtmlAst[], errors: ParseError[]): Part[] {
} else if (n instanceof HtmlElementAst) {
let i18n = _findI18nAttr(n);
res.push(new Part(n, null, n.children, isPresent(i18n) ? i18n.value : null, isPresent(i18n)));
let hasI18n: boolean = isPresent(i18n) || implicitTags.indexOf(n.name) > -1;
res.push(new Part(n, null, n.children, isPresent(i18n) ? i18n.value : null, hasI18n));
} else if (n instanceof HtmlTextAst) {
res.push(new Part(null, n, null, null, false));
}
@ -99,17 +100,28 @@ export function description(i18n: string): string {
return parts.length > 1 ? parts[1] : null;
}
export function messageFromAttribute(parser: Parser, p: HtmlElementAst,
attr: HtmlAttrAst): Message {
let expectedName = attr.name.substring(5);
/**
* Extract a translation string given an `i18n-` prefixed attribute.
*
* @internal
*/
export function messageFromI18nAttribute(parser: Parser, p: HtmlElementAst,
i18nAttr: HtmlAttrAst): Message {
let expectedName = i18nAttr.name.substring(5);
let matching = p.attrs.filter(a => a.name == expectedName);
if (matching.length > 0) {
let value = removeInterpolation(matching[0].value, matching[0].sourceSpan, parser);
return new Message(value, meaning(attr.value), description(attr.value));
} else {
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
return messageFromAttribute(parser, matching[0], meaning(i18nAttr.value),
description(i18nAttr.value));
}
throw new I18nError(p.sourceSpan, `Missing attribute '${expectedName}'.`);
}
export function messageFromAttribute(parser: Parser, attr: HtmlAttrAst, meaning: string = null,
description: string = null): Message {
let value = removeInterpolation(attr.value, attr.sourceSpan, parser);
return new Message(value, meaning, description);
}
export function removeInterpolation(value: string, source: ParseSourceSpan,

View File

@ -1,38 +1,19 @@
import {
beforeEach,
ddescribe,
describe,
expect,
iit,
inject,
it,
xdescribe,
xit
} from '@angular/core/testing/testing_internal';
import {I18nHtmlParser} from '@angular/compiler/src/i18n/i18n_html_parser';
import {Message, id} from '@angular/compiler/src/i18n/message';
import {Parser} from '@angular/compiler/src/expression_parser/parser';
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
import {StringMapWrapper} from '../../src/facade/collection';
import {HtmlParser, HtmlParseTreeResult} from '@angular/compiler/src/html_parser';
import {
HtmlAst,
HtmlAstVisitor,
HtmlElementAst,
HtmlAttrAst,
HtmlTextAst,
HtmlCommentAst,
htmlVisitAll
} from '@angular/compiler/src/html_ast';
import {serializeXmb, deserializeXmb} from '@angular/compiler/src/i18n/xmb_serializer';
import {ParseError, ParseLocation} from '@angular/compiler/src/parse_util';
import {humanizeDom} from '@angular/compiler/test/html_ast_spec_utils';
import {describe, expect, it, iit, ddescribe} from "@angular/core/testing/testing_internal";
import {I18nHtmlParser} from "@angular/compiler/src/i18n/i18n_html_parser";
import {Message, id} from "@angular/compiler/src/i18n/message";
import {Parser} from "@angular/compiler/src/expression_parser/parser";
import {Lexer} from "@angular/compiler/src/expression_parser/lexer";
import {StringMapWrapper} from "../../src/facade/collection";
import {HtmlParser, HtmlParseTreeResult} from "@angular/compiler/src/html_parser";
import {HtmlElementAst, HtmlAttrAst, HtmlTextAst} from "@angular/compiler/src/html_ast";
import {deserializeXmb} from "@angular/compiler/src/i18n/xmb_serializer";
import {ParseError} from "@angular/compiler/src/parse_util";
import {humanizeDom} from "@angular/compiler/test/html_ast_spec_utils";
export function main() {
describe('I18nHtmlParser', () => {
function parse(template: string, messages: {[key: string]: string}): HtmlParseTreeResult {
function parse(template: string, messages: {[key: string]: string}, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): HtmlParseTreeResult {
var parser = new Parser(new Lexer());
let htmlParser = new HtmlParser();
@ -40,7 +21,8 @@ export function main() {
StringMapWrapper.forEach(messages, (v, k) => msgs += `<msg id="${k}">${v}</msg>`);
let res = deserializeXmb(`<message-bundle>${msgs}</message-bundle>`, 'someUrl');
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages)
return new I18nHtmlParser(htmlParser, parser, res.content, res.messages, implicitTags,
implicitAttrs)
.parse(template, "someurl", true);
}
@ -331,6 +313,44 @@ export function main() {
.toEqual(["Invalid interpolation name '99'"]);
});
describe('implicit translation', () => {
it("should support attributes", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", null, null))] = "another message";
expect(humanizeDom(parse("<i18n-el value='some message'></i18n-el>", translations, [],
{'i18n-el': ['value']})))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it("should support attributes with meaning and description", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("some message", "meaning", "description"))] =
"another message";
expect(humanizeDom(parse(
"<i18n-el value='some message' i18n-value='meaning|description'></i18n-el>",
translations, [], {'i18n-el': ['value']})))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlAttrAst, 'value', 'another message']]);
});
it("should support elements", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("message", null, null))] = "another message";
expect(humanizeDom(parse("<i18n-el>message</i18n-el>", translations, ['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
it("should support elements with meaning and description", () => {
let translations: {[key: string]: string} = {};
translations[id(new Message("message", "meaning", "description"))] = "another message";
expect(humanizeDom(parse("<i18n-el i18n='meaning|description'>message</i18n-el>",
translations, ['i18n-el'])))
.toEqual([[HtmlElementAst, 'i18n-el', 0], [HtmlTextAst, 'another message', 1]]);
});
});
});
});
}

View File

@ -23,7 +23,7 @@ export function main() {
beforeEach(() => {
let htmlParser = new HtmlParser();
var parser = new Parser(new Lexer());
extractor = new MessageExtractor(htmlParser, parser);
extractor = new MessageExtractor(htmlParser, parser, ['i18n-tag'], {'i18n-el': ['trans']});
});
it('should extract from elements with the i18n attr', () => {
@ -205,6 +205,30 @@ export function main() {
]);
});
describe('implicit translation', () => {
it('should extract from elements', () => {
let res = extractor.extract("<i18n-tag>message</i18n-tag>", "someurl");
expect(res.messages).toEqual([new Message("message", null, null)]);
});
it('should extract meaning and description from elements when present', () => {
let res =
extractor.extract("<i18n-tag i18n='meaning|description'>message</i18n-tag>", "someurl");
expect(res.messages).toEqual([new Message("message", "meaning", "description")]);
});
it('should extract from attributes', () => {
let res = extractor.extract(`<i18n-el trans='message'></i18n-el>`, "someurl");
expect(res.messages).toEqual([new Message("message", null, null)]);
});
it('should extract meaning and description from attributes when present', () => {
let res = extractor.extract(`<i18n-el trans='message' i18n-trans="meaning|desc"></i18n-el>`,
"someurl");
expect(res.messages).toEqual([new Message("message", "meaning", "desc")]);
});
});
describe("errors", () => {
it('should error on i18n attributes without matching "real" attributes', () => {
let res = extractor.extract(`