refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
66
packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
Normal file
66
packages/compiler/test/i18n/serializers/i18n_ast_spec.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
|
||||
|
||||
import {serializeNodes} from '../../../src/i18n/digest';
|
||||
import {_extractMessages} from '../i18n_parser_spec';
|
||||
|
||||
export function main(): void {
|
||||
describe('i18n AST', () => {
|
||||
describe('CloneVisitor', () => {
|
||||
it('should clone an AST', () => {
|
||||
const messages = _extractMessages(
|
||||
'<div i18n="m|d">b{count, plural, =0 {{sex, select, male {m}}}}a</div>');
|
||||
const nodes = messages[0].nodes;
|
||||
const text = serializeNodes(nodes).join('');
|
||||
expect(text).toEqual(
|
||||
'b<ph icu name="ICU">{count, plural, =0 {[{sex, select, male {[m]}}]}}</ph>a');
|
||||
const visitor = new i18n.CloneVisitor();
|
||||
const cloneNodes = nodes.map(n => n.visit(visitor));
|
||||
expect(serializeNodes(nodes)).toEqual(serializeNodes(cloneNodes));
|
||||
nodes.forEach((n: i18n.Node, i: number) => {
|
||||
expect(n).toEqual(cloneNodes[i]);
|
||||
expect(n).not.toBe(cloneNodes[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RecurseVisitor', () => {
|
||||
it('should visit all nodes', () => {
|
||||
const visitor = new RecurseVisitor();
|
||||
const container = new i18n.Container(
|
||||
[
|
||||
new i18n.Text('', null),
|
||||
new i18n.Placeholder('', '', null),
|
||||
new i18n.IcuPlaceholder(null, '', null),
|
||||
],
|
||||
null);
|
||||
const tag = new i18n.TagPlaceholder('', {}, '', '', [container], false, null);
|
||||
const icu = new i18n.Icu('', '', {tag}, null);
|
||||
|
||||
icu.visit(visitor);
|
||||
expect(visitor.textCount).toEqual(1);
|
||||
expect(visitor.phCount).toEqual(1);
|
||||
expect(visitor.icuPhCount).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class RecurseVisitor extends i18n.RecurseVisitor {
|
||||
textCount = 0;
|
||||
phCount = 0;
|
||||
icuPhCount = 0;
|
||||
|
||||
visitText(text: i18n.Text, context?: any): any { this.textCount++; }
|
||||
|
||||
visitPlaceholder(ph: i18n.Placeholder, context?: any): any { this.phCount++; }
|
||||
|
||||
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any { this.icuPhCount++; }
|
||||
}
|
90
packages/compiler/test/i18n/serializers/placeholder_spec.ts
Normal file
90
packages/compiler/test/i18n/serializers/placeholder_spec.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
|
||||
|
||||
export function main(): void {
|
||||
describe('PlaceholderRegistry', () => {
|
||||
let reg: PlaceholderRegistry;
|
||||
|
||||
beforeEach(() => { reg = new PlaceholderRegistry(); });
|
||||
|
||||
describe('tag placeholder', () => {
|
||||
it('should generate names for well known tags', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
|
||||
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
|
||||
});
|
||||
|
||||
it('should generate names for custom tags', () => {
|
||||
expect(reg.getStartTagPlaceholderName('my-cmp', {}, false)).toEqual('START_TAG_MY-CMP');
|
||||
expect(reg.getCloseTagPlaceholderName('my-cmp')).toEqual('CLOSE_TAG_MY-CMP');
|
||||
});
|
||||
|
||||
it('should generate the same name for the same tag', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
|
||||
});
|
||||
|
||||
it('should be case sensitive for tag name', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, false)).toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('P', {}, false)).toEqual('START_PARAGRAPH_1');
|
||||
expect(reg.getCloseTagPlaceholderName('p')).toEqual('CLOSE_PARAGRAPH');
|
||||
expect(reg.getCloseTagPlaceholderName('P')).toEqual('CLOSE_PARAGRAPH_1');
|
||||
});
|
||||
|
||||
it('should generate the same name for the same tag with the same attributes', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {bar: 'b', foo: 'a'}, false))
|
||||
.toEqual('START_PARAGRAPH');
|
||||
});
|
||||
|
||||
it('should generate different names for the same tag with different attributes', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {foo: 'a'}, false)).toEqual('START_PARAGRAPH_1');
|
||||
});
|
||||
|
||||
it('should be case sensitive for attributes', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {foo: 'a', bar: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {fOo: 'a', bar: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH_1');
|
||||
expect(reg.getStartTagPlaceholderName('p', {fOo: 'a', bAr: 'b'}, false))
|
||||
.toEqual('START_PARAGRAPH_2');
|
||||
});
|
||||
|
||||
it('should support void tags', () => {
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, true)).toEqual('PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {}, true)).toEqual('PARAGRAPH');
|
||||
expect(reg.getStartTagPlaceholderName('p', {other: 'true'}, true)).toEqual('PARAGRAPH_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('arbitrary placeholders', () => {
|
||||
it('should generate the same name given the same name and content', () => {
|
||||
expect(reg.getPlaceholderName('name', 'content')).toEqual('NAME');
|
||||
expect(reg.getPlaceholderName('name', 'content')).toEqual('NAME');
|
||||
});
|
||||
|
||||
it('should generate a different name given different content', () => {
|
||||
expect(reg.getPlaceholderName('name', 'content1')).toEqual('NAME');
|
||||
expect(reg.getPlaceholderName('name', 'content2')).toEqual('NAME_1');
|
||||
expect(reg.getPlaceholderName('name', 'content3')).toEqual('NAME_2');
|
||||
});
|
||||
|
||||
it('should generate a different name given different names', () => {
|
||||
expect(reg.getPlaceholderName('name1', 'content')).toEqual('NAME1');
|
||||
expect(reg.getPlaceholderName('name2', 'content')).toEqual('NAME2');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
252
packages/compiler/test/i18n/serializers/xliff_spec.ts
Normal file
252
packages/compiler/test/i18n/serializers/xliff_spec.ts
Normal file
@ -0,0 +1,252 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {escapeRegExp} from '@angular/compiler/src/util';
|
||||
|
||||
import {serializeNodes} from '../../../src/i18n/digest';
|
||||
import {MessageBundle} from '../../../src/i18n/message_bundle';
|
||||
import {Xliff} from '../../../src/i18n/serializers/xliff';
|
||||
import {HtmlParser} from '../../../src/ml_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
|
||||
|
||||
const HTML = `
|
||||
<p i18n-title title="translatable attribute">not translatable</p>
|
||||
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
|
||||
<p i18n="m|d">foo</p>
|
||||
<p i18n="m|d@@i">foo</p>
|
||||
<p i18n="@@bar">foo</p>
|
||||
<p i18n="ph names"><br><img><div></div></p>
|
||||
`;
|
||||
|
||||
const WRITE_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="983775b9a51ce14b036be72d4cfd65d68d64e231" datatype="html">
|
||||
<source>translatable attribute</source>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
<trans-unit id="ec1d033f2436133c14ab038286c4f5df4697484a" datatype="html">
|
||||
<source>translatable element <x id="START_BOLD_TEXT" ctype="x-b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="x-b"/> <x id="INTERPOLATION"/></source>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
<trans-unit id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" datatype="html">
|
||||
<source>foo</source>
|
||||
<target/>
|
||||
<note priority="1" from="description">d</note>
|
||||
<note priority="1" from="meaning">m</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="i" datatype="html">
|
||||
<source>foo</source>
|
||||
<target/>
|
||||
<note priority="1" from="description">d</note>
|
||||
<note priority="1" from="meaning">m</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bar" datatype="html">
|
||||
<source>foo</source>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
|
||||
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
|
||||
<target/>
|
||||
<note priority="1" from="description">ph names</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
const LOAD_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" target-language="fr" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="983775b9a51ce14b036be72d4cfd65d68d64e231" datatype="html">
|
||||
<source>translatable attribute</source>
|
||||
<target>etubirtta elbatalsnart</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ec1d033f2436133c14ab038286c4f5df4697484a" datatype="html">
|
||||
<source>translatable element <x id="START_BOLD_TEXT" ctype="b"/>with placeholders<x id="CLOSE_BOLD_TEXT" ctype="b"/> <x id="INTERPOLATION"/></source>
|
||||
<target><x id="INTERPOLATION"/> footnemele elbatalsnart <x id="START_BOLD_TEXT" ctype="x-b"/>sredlohecalp htiw<x id="CLOSE_BOLD_TEXT" ctype="x-b"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" datatype="html">
|
||||
<source>foo</source>
|
||||
<target>oof</target>
|
||||
<note priority="1" from="description">d</note>
|
||||
<note priority="1" from="meaning">m</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="i" datatype="html">
|
||||
<source>foo</source>
|
||||
<target>toto</target>
|
||||
<note priority="1" from="description">d</note>
|
||||
<note priority="1" from="meaning">m</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="bar" datatype="html">
|
||||
<source>foo</source>
|
||||
<target>tata</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="d7fa2d59aaedcaa5309f13028c59af8c85b8c49d" datatype="html">
|
||||
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
|
||||
<target><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/><x id="TAG_IMG" ctype="image"/><x id="LINE_BREAK" ctype="lb"/></target>
|
||||
<note priority="1" from="description">ph names</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="empty target" datatype="html">
|
||||
<source><x id="LINE_BREAK" ctype="lb"/><x id="TAG_IMG" ctype="image"/><x id="START_TAG_DIV" ctype="x-div"/><x id="CLOSE_TAG_DIV" ctype="x-div"/></source>
|
||||
<target/>
|
||||
<note priority="1" from="description">ph names</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
`;
|
||||
|
||||
export function main(): void {
|
||||
const serializer = new Xliff();
|
||||
|
||||
function toXliff(html: string, locale: string | null = null): string {
|
||||
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
|
||||
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
||||
return catalog.write(serializer);
|
||||
}
|
||||
|
||||
function loadAsMap(xliff: string): {[id: string]: string} {
|
||||
const {i18nNodesByMsgId} = serializer.load(xliff, 'url');
|
||||
|
||||
const msgMap: {[id: string]: string} = {};
|
||||
Object.keys(i18nNodesByMsgId)
|
||||
.forEach(id => msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join(''));
|
||||
|
||||
return msgMap;
|
||||
}
|
||||
|
||||
describe('XLIFF serializer', () => {
|
||||
describe('write', () => {
|
||||
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
|
||||
it('should write a valid xliff file with a source language',
|
||||
() => { expect(toXliff(HTML, 'fr')).toContain('file source-language="fr"'); });
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('should load XLIFF files', () => {
|
||||
expect(loadAsMap(LOAD_XLIFF)).toEqual({
|
||||
'983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart',
|
||||
'ec1d033f2436133c14ab038286c4f5df4697484a':
|
||||
'<ph name="INTERPOLATION"/> footnemele elbatalsnart <ph name="START_BOLD_TEXT"/>sredlohecalp htiw<ph name="CLOSE_BOLD_TEXT"/>',
|
||||
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
|
||||
'i': 'toto',
|
||||
'bar': 'tata',
|
||||
'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
|
||||
'<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/><ph name="TAG_IMG"/><ph name="LINE_BREAK"/>',
|
||||
'empty target': '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the target locale',
|
||||
() => { expect(serializer.load(LOAD_XLIFF, 'url').locale).toEqual('fr'); });
|
||||
|
||||
|
||||
describe('structure errors', () => {
|
||||
it('should throw when a trans-unit has no translation', () => {
|
||||
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="missingtarget">
|
||||
<source/>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XLIFF);
|
||||
}).toThrowError(/Message missingtarget misses a translation/);
|
||||
});
|
||||
|
||||
|
||||
it('should throw when a trans-unit has no id attribute', () => {
|
||||
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit datatype="html">
|
||||
<source/>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XLIFF);
|
||||
}).toThrowError(/<trans-unit> misses the "id" attribute/);
|
||||
});
|
||||
|
||||
it('should throw on duplicate trans-unit id', () => {
|
||||
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="deadbeef">
|
||||
<source/>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
<trans-unit id="deadbeef">
|
||||
<source/>
|
||||
<target/>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XLIFF);
|
||||
}).toThrowError(/Duplicated translations for msg deadbeef/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('message errors', () => {
|
||||
it('should throw on unknown message tags', () => {
|
||||
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="deadbeef" datatype="html">
|
||||
<source/>
|
||||
<target><b>msg should contain only ph tags</b></target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>`;
|
||||
|
||||
expect(() => { loadAsMap(XLIFF); })
|
||||
.toThrowError(
|
||||
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
|
||||
});
|
||||
|
||||
it('should throw when a placeholder misses an id attribute', () => {
|
||||
const XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file source-language="en" datatype="plaintext" original="ng2.template">
|
||||
<body>
|
||||
<trans-unit id="deadbeef" datatype="html">
|
||||
<source/>
|
||||
<target><x/></target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XLIFF);
|
||||
}).toThrowError(new RegExp(escapeRegExp(`<x> misses the "id" attribute`)));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
81
packages/compiler/test/i18n/serializers/xmb_spec.ts
Normal file
81
packages/compiler/test/i18n/serializers/xmb_spec.ts
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MessageBundle} from '@angular/compiler/src/i18n/message_bundle';
|
||||
import {Xmb} from '@angular/compiler/src/i18n/serializers/xmb';
|
||||
import {HtmlParser} from '@angular/compiler/src/ml_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '@angular/compiler/src/ml_parser/interpolation_config';
|
||||
|
||||
export function main(): void {
|
||||
describe('XMB serializer', () => {
|
||||
const HTML = `
|
||||
<p>not translatable</p>
|
||||
<p i18n>translatable element <b>with placeholders</b> {{ interpolation}}</p>
|
||||
<!-- i18n -->{ count, plural, =0 {<p>test</p>}}<!-- /i18n -->
|
||||
<p i18n="m|d">foo</p>
|
||||
<p i18n="m|d@@i">foo</p>
|
||||
<p i18n="@@bar">foo</p>
|
||||
<p i18n="@@baz">{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>
|
||||
<p i18n>{ count, plural, =0 { { sex, select, other {<p>deeply nested</p>}} }}</p>`;
|
||||
|
||||
const XMB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE messagebundle [
|
||||
<!ELEMENT messagebundle (msg)*>
|
||||
<!ATTLIST messagebundle class CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT msg (#PCDATA|ph|source)*>
|
||||
<!ATTLIST msg id CDATA #IMPLIED>
|
||||
<!ATTLIST msg seq CDATA #IMPLIED>
|
||||
<!ATTLIST msg name CDATA #IMPLIED>
|
||||
<!ATTLIST msg desc CDATA #IMPLIED>
|
||||
<!ATTLIST msg meaning CDATA #IMPLIED>
|
||||
<!ATTLIST msg obsolete (obsolete) #IMPLIED>
|
||||
<!ATTLIST msg xml:space (default|preserve) "default">
|
||||
<!ATTLIST msg is_hidden CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT source (#PCDATA)>
|
||||
|
||||
<!ELEMENT ph (#PCDATA|ex)*>
|
||||
<!ATTLIST ph name CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT ex (#PCDATA)>
|
||||
]>
|
||||
<messagebundle>
|
||||
<msg id="7056919470098446707">translatable element <ph name="START_BOLD_TEXT"><ex><b></ex></ph>with placeholders<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> <ph name="INTERPOLATION"><ex>INTERPOLATION</ex></ph></msg>
|
||||
<msg id="2981514368455622387">{VAR_PLURAL, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} }</msg>
|
||||
<msg id="7999024498831672133" desc="d" meaning="m">foo</msg>
|
||||
<msg id="i" desc="d" meaning="m">foo</msg>
|
||||
<msg id="bar">foo</msg>
|
||||
<msg id="baz">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
<msg id="2015957479576096115">{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
</messagebundle>
|
||||
`;
|
||||
|
||||
it('should write a valid xmb file', () => {
|
||||
expect(toXmb(HTML)).toEqual(XMB);
|
||||
// the locale is not specified in the xmb file
|
||||
expect(toXmb(HTML, 'fr')).toEqual(XMB);
|
||||
});
|
||||
|
||||
it('should throw when trying to load an xmb file', () => {
|
||||
expect(() => {
|
||||
const serializer = new Xmb();
|
||||
serializer.load(XMB, 'url');
|
||||
}).toThrowError(/Unsupported/);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toXmb(html: string, locale: string | null = null): string {
|
||||
const catalog = new MessageBundle(new HtmlParser, [], {}, locale);
|
||||
const serializer = new Xmb();
|
||||
|
||||
catalog.updateFromTemplate(html, '', DEFAULT_INTERPOLATION_CONFIG);
|
||||
|
||||
return catalog.write(serializer);
|
||||
}
|
47
packages/compiler/test/i18n/serializers/xml_helper_spec.ts
Normal file
47
packages/compiler/test/i18n/serializers/xml_helper_spec.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as xml from '../../../src/i18n/serializers/xml_helper';
|
||||
|
||||
export function main(): void {
|
||||
describe('XML helper', () => {
|
||||
it('should serialize XML declaration', () => {
|
||||
expect(xml.serialize([new xml.Declaration({version: '1.0'})]))
|
||||
.toEqual('<?xml version="1.0" ?>');
|
||||
});
|
||||
|
||||
it('should serialize text node',
|
||||
() => { expect(xml.serialize([new xml.Text('foo bar')])).toEqual('foo bar'); });
|
||||
|
||||
it('should escape text nodes',
|
||||
() => { expect(xml.serialize([new xml.Text('<>')])).toEqual('<>'); });
|
||||
|
||||
it('should serialize xml nodes without children', () => {
|
||||
expect(xml.serialize([new xml.Tag('el', {foo: 'bar'}, [])])).toEqual('<el foo="bar"/>');
|
||||
});
|
||||
|
||||
it('should serialize xml nodes with children', () => {
|
||||
expect(xml.serialize([
|
||||
new xml.Tag('parent', {}, [new xml.Tag('child', {}, [new xml.Text('content')])])
|
||||
])).toEqual('<parent><child>content</child></parent>');
|
||||
});
|
||||
|
||||
it('should serialize node lists', () => {
|
||||
expect(xml.serialize([
|
||||
new xml.Tag('el', {order: '0'}, []),
|
||||
new xml.Tag('el', {order: '1'}, []),
|
||||
])).toEqual('<el order="0"/><el order="1"/>');
|
||||
});
|
||||
|
||||
it('should escape attribute values', () => {
|
||||
expect(xml.serialize([new xml.Tag('el', {foo: '<">'}, [])]))
|
||||
.toEqual('<el foo="<">"/>');
|
||||
});
|
||||
|
||||
});
|
||||
}
|
192
packages/compiler/test/i18n/serializers/xtb_spec.ts
Normal file
192
packages/compiler/test/i18n/serializers/xtb_spec.ts
Normal file
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {escapeRegExp} from '@angular/compiler/src/util';
|
||||
import {serializeNodes} from '../../../src/i18n/digest';
|
||||
import * as i18n from '../../../src/i18n/i18n_ast';
|
||||
import {Xtb} from '../../../src/i18n/serializers/xtb';
|
||||
|
||||
|
||||
export function main(): void {
|
||||
describe('XTB serializer', () => {
|
||||
const serializer = new Xtb();
|
||||
|
||||
function loadAsMap(xtb: string): {[id: string]: string} {
|
||||
const {i18nNodesByMsgId} = serializer.load(xtb, 'url');
|
||||
const msgMap: {[id: string]: string} = {};
|
||||
Object.keys(i18nNodesByMsgId).forEach(id => {
|
||||
msgMap[id] = serializeNodes(i18nNodesByMsgId[id]).join('');
|
||||
});
|
||||
return msgMap;
|
||||
}
|
||||
|
||||
describe('load', () => {
|
||||
it('should load XTB files with a doctype', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE translationbundle [<!ELEMENT translationbundle (translation)*>
|
||||
<!ATTLIST translationbundle lang CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT translation (#PCDATA|ph)*>
|
||||
<!ATTLIST translation id CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT ph EMPTY>
|
||||
<!ATTLIST ph name CDATA #REQUIRED>
|
||||
]>
|
||||
<translationbundle>
|
||||
<translation id="8841459487341224498">rab</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
|
||||
});
|
||||
|
||||
it('should load XTB files without placeholders', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="8841459487341224498">rab</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
|
||||
});
|
||||
|
||||
it('should return the target locale', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle lang='fr'>
|
||||
<translation id="8841459487341224498">rab</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(serializer.load(XTB, 'url').locale).toEqual('fr');
|
||||
});
|
||||
|
||||
it('should load XTB files with placeholders', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="8877975308926375834"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsMap(XTB)).toEqual({
|
||||
'8877975308926375834': '<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>'
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace ICU placeholders with their translations', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<translationbundle>
|
||||
<translation id="7717087045075616176">*<ph name="ICU"/>*</translation>
|
||||
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsMap(XTB)).toEqual({
|
||||
'7717087045075616176': `*<ph name="ICU"/>*`,
|
||||
'5115002811911870583':
|
||||
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should load complex XTB files', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<translationbundle>
|
||||
<translation id="8281795707202401639"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
|
||||
<translation id="5115002811911870583">{VAR_PLURAL, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
<translation id="130772889486467622">oof</translation>
|
||||
<translation id="4739316421648347533">{VAR_PLURAL, plural, =1 {{VAR_GENDER, gender, male {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsMap(XTB)).toEqual({
|
||||
'8281795707202401639':
|
||||
`<ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof`,
|
||||
'5115002811911870583':
|
||||
`{VAR_PLURAL, plural, =1 {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}`,
|
||||
'130772889486467622': `oof`,
|
||||
'4739316421648347533':
|
||||
`{VAR_PLURAL, plural, =1 {[{VAR_GENDER, gender, male {[<ph name="START_PARAGRAPH"/>, rab, <ph name="CLOSE_PARAGRAPH"/>]}}, ]}}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
it('should be able to parse non-angular xtb files without error', () => {
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<translationbundle>
|
||||
<translation id="angular">is great</translation>
|
||||
<translation id="non angular">is <invalid>less</invalid> {count, plural, =0 {{GREAT}}}</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
// Invalid messages should not cause the parser to throw
|
||||
let i18nNodesByMsgId: {[id: string]: i18n.Node[]};
|
||||
expect(() => {
|
||||
i18nNodesByMsgId = serializer.load(XTB, 'url').i18nNodesByMsgId;
|
||||
}).not.toThrow();
|
||||
|
||||
expect(Object.keys(i18nNodesByMsgId).length).toEqual(2);
|
||||
expect(serializeNodes(i18nNodesByMsgId['angular']).join('')).toEqual('is great');
|
||||
// Messages that contain unsupported feature should throw on access
|
||||
expect(() => {
|
||||
const read = i18nNodesByMsgId['non angular'];
|
||||
}).toThrowError(/xtb parse errors/);
|
||||
});
|
||||
|
||||
it('should throw on nested <translationbundle>', () => {
|
||||
const XTB =
|
||||
'<translationbundle><translationbundle></translationbundle></translationbundle>';
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XTB);
|
||||
}).toThrowError(/<translationbundle> elements can not be nested/);
|
||||
});
|
||||
|
||||
it('should throw when a <translation> has no id attribute', () => {
|
||||
const XTB = `<translationbundle>
|
||||
<translation></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => { loadAsMap(XTB); }).toThrowError(/<translation> misses the "id" attribute/);
|
||||
});
|
||||
|
||||
it('should throw when a placeholder has no name attribute', () => {
|
||||
const XTB = `<translationbundle>
|
||||
<translation id="1186013544048295927"><ph /></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => { loadAsMap(XTB); }).toThrowError(/<ph> misses the "name" attribute/);
|
||||
});
|
||||
|
||||
it('should throw on unknown xtb tags', () => {
|
||||
const XTB = `<what></what>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XTB);
|
||||
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]<what></what>")`)));
|
||||
});
|
||||
|
||||
it('should throw on unknown message tags', () => {
|
||||
const XTB = `<translationbundle>
|
||||
<translation id="1186013544048295927"><b>msg should contain only ph tags</b></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => { loadAsMap(XTB); })
|
||||
.toThrowError(
|
||||
new RegExp(escapeRegExp(`[ERROR ->]<b>msg should contain only ph tags</b>`)));
|
||||
});
|
||||
|
||||
it('should throw on duplicate message id', () => {
|
||||
const XTB = `<translationbundle>
|
||||
<translation id="1186013544048295927">msg1</translation>
|
||||
<translation id="1186013544048295927">msg2</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => {
|
||||
loadAsMap(XTB);
|
||||
}).toThrowError(/Duplicated translations for msg 1186013544048295927/);
|
||||
});
|
||||
|
||||
it('should throw when trying to save an xtb file',
|
||||
() => { expect(() => { serializer.write([], null); }).toThrowError(/Unsupported/); });
|
||||
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user