feat(i18n): support implicit tags/attributes
This commit is contained in:
parent
75e6dfb9ab
commit
3e5716ec16
|
@ -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';
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(`
|
||||
|
|
Loading…
Reference in New Issue