before**[ph tag name="START_PARAGRAPH">foo[/ph name="CLOSE_PARAGRAPH">[ph' +
+ ' tag name="START_TAG_SPAN">[ph tag name="START_ITALIC_TEXT">bar[/ph' +
+ ' name="CLOSE_ITALIC_TEXT">[/ph name="CLOSE_TAG_SPAN">**after
');
});
});
@@ -399,12 +402,12 @@ function fakeTranslate(
extractMessages(htmlNodes, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs)
.messages;
- const i18nMsgMap: {[id: string]: html.Node[]} = {};
+ const i18nMsgMap: {[id: string]: i18n.Node[]} = {};
messages.forEach(message => {
const id = digest(message);
- const text = serializeI18nNodes(message.nodes).join('');
- i18nMsgMap[id] = [new html.Text(`**${text}**`, null)];
+ const text = serializeI18nNodes(message.nodes).join('').replace(/
@@ -119,6 +125,9 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
{sex, select, m {male} f {female}}
+
+ {sexB, select, m {male} f {female}}
+
{{ "count = " + count }}
sex = {{ sex }}
@@ -135,8 +144,9 @@ function expectHtml(el: DebugElement, cssSelector: string): any {
`
})
class I18nComponent {
- count: number = 0;
- sex: string = 'm';
+ count: number;
+ sex: string;
+ sexB: string;
}
class FrLocalization extends NgLocalization {
@@ -159,14 +169,14 @@ const XTB = `
avec des espaces réservés
sur des balises non traductibles
sur des balises traductibles
-
{count, plural, =0 {zero} =1 {un} =2 {deux} other {beaucoup}}
-
-
{sex, select, m {homme} f {femme}}
+
{VAR_PLURAL, plural, =0 {zero} =1 {un} =2 {deux} other {beaucoup}}
+
+
{VAR_SELECT, select, m {homme} f {femme}}
sexe =
dans une section traductible
-
+
Balises dans les commentaires html
@@ -185,16 +195,16 @@ const XMB = `
<i>with placeholders</i>
on not translatable node
on translatable node
- {count, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>} }
-
+ {VAR_PLURAL, plural, =0 {zero} =1 {one} =2 {two} other {<b>many</b>} }
+
- {sex, select, m {male} f {female} }
+ {VAR_SELECT, select, m {male} f {female} }
sex =
in a translatable section
-
+
<h1>Markers in html comments</h1>
<div></div>
<div></div>
diff --git a/modules/@angular/compiler/test/i18n/message_bundle_spec.ts b/modules/@angular/compiler/test/i18n/message_bundle_spec.ts
index ed1692094f..b82a30322b 100644
--- a/modules/@angular/compiler/test/i18n/message_bundle_spec.ts
+++ b/modules/@angular/compiler/test/i18n/message_bundle_spec.ts
@@ -6,12 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
-import * as i18n from '@angular/compiler/src/i18n/i18n_ast';
-import {Serializer} from '@angular/compiler/src/i18n/serializers/serializer';
-import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
-
import {serializeNodes} from '../../src/i18n/digest';
+import * as i18n from '../../src/i18n/i18n_ast';
import {MessageBundle} from '../../src/i18n/message_bundle';
+import {Serializer} from '../../src/i18n/serializers/serializer';
import {HtmlParser} from '../../src/ml_parser/html_parser';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
@@ -50,7 +48,7 @@ class _TestSerializer implements Serializer {
.join('//');
}
- load(content: string, url: string, placeholders: {}): {} { return null; }
+ load(content: string, url: string): {} { return null; }
digest(msg: i18n.Message): string { return 'unused'; }
}
diff --git a/modules/@angular/compiler/test/i18n/serializers/placeholder_spec.ts b/modules/@angular/compiler/test/i18n/serializers/placeholder_spec.ts
index ce6e3b1811..eb068695c0 100644
--- a/modules/@angular/compiler/test/i18n/serializers/placeholder_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/placeholder_spec.ts
@@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
-
import {PlaceholderRegistry} from '../../../src/i18n/serializers/placeholder';
export function main(): void {
diff --git a/modules/@angular/compiler/test/i18n/serializers/xliff_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xliff_spec.ts
index 43639813c0..2dcadf090d 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xliff_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xliff_spec.ts
@@ -6,12 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Xliff} from '@angular/compiler/src/i18n/serializers/xliff';
-import {beforeEach, describe, expect, it} from '@angular/core/testing/testing_internal';
+import {escapeRegExp} from '@angular/core/src/facade/lang';
+
+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';
-import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
const HTML = `
not translatable
@@ -77,8 +78,7 @@ const LOAD_XLIFF = `
`;
export function main(): void {
- let serializer: Xliff;
- let htmlParser: HtmlParser;
+ const serializer = new Xliff();
function toXliff(html: string): string {
const catalog = new MessageBundle(new HtmlParser, [], {});
@@ -86,37 +86,89 @@ export function main(): void {
return catalog.write(serializer);
}
- function loadAsText(template: string, xliff: string): {[id: string]: string} {
- const messageBundle = new MessageBundle(htmlParser, [], {});
- messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG);
+ 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(''));
- const asAst = serializer.load(xliff, 'url', messageBundle);
- const asText: {[id: string]: string} = {};
- Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
-
- return asText;
+ return msgMap;
}
describe('XLIFF serializer', () => {
-
- beforeEach(() => {
- htmlParser = new HtmlParser();
- serializer = new Xliff(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
- });
-
-
describe('write', () => {
it('should write a valid xliff file', () => { expect(toXliff(HTML)).toEqual(WRITE_XLIFF); });
});
describe('load', () => {
it('should load XLIFF files', () => {
- expect(loadAsText(HTML, LOAD_XLIFF)).toEqual({
+ expect(loadAsMap(LOAD_XLIFF)).toEqual({
'983775b9a51ce14b036be72d4cfd65d68d64e231': 'etubirtta elbatalsnart',
'ec1d033f2436133c14ab038286c4f5df4697484a':
- '{{ interpolation}} footnemele elbatalsnart sredlohecalp htiw',
+ ' footnemele elbatalsnart sredlohecalp htiw',
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': 'oof',
- 'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d': '![]()
',
+ 'd7fa2d59aaedcaa5309f13028c59af8c85b8c49d':
+ '',
+ });
+ });
+
+ describe('errors', () => {
+ it('should throw when a placeholder has no id attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+`;
+
+ expect(() => {
+ loadAsMap(XLIFF);
+ }).toThrowError(/ misses the "id" attribute/);
+ });
+
+ it('should throw on unknown message tags', () => {
+ const XLIFF = `
+
+
+
+
+
+ msg should contain only ph tags
+
+
+
+`;
+
+ expect(() => { loadAsMap(XLIFF); })
+ .toThrowError(
+ new RegExp(escapeRegExp(`[ERROR ->]msg should contain only ph tags`)));
+ });
+
+ it('should throw when a placeholder has no name attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ expect(() => {
+ loadAsMap(XLIFF);
+ }).toThrowError(/Duplicated translations for msg deadbeef/);
});
});
});
diff --git a/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
index 315a451c05..0b00577c0a 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
@@ -44,9 +44,9 @@ export function main(): void {
]>
translatable element <b>with placeholders</b>
- { count, plural, =0 {<p>test</p>} }
+ {VAR_PLURAL, plural, =0 {<p>test</p>} }
foo
- { count, plural, =0 {{ sex, gender, other {<p>deeply nested</p>} } } }
+ {VAR_PLURAL, plural, =0 {{VAR_GENDER, gender, other {<p>deeply nested</p>} } } }
`;
@@ -55,7 +55,7 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new Xmb();
- serializer.load(XMB, 'url', null);
+ serializer.load(XMB, 'url');
}).toThrowError(/Unsupported/);
});
});
diff --git a/modules/@angular/compiler/test/i18n/serializers/xml_helper_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xml_helper_spec.ts
index 8bcd7e3d30..e1f9215b4d 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xml_helper_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xml_helper_spec.ts
@@ -6,8 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {describe, expect, it} from '@angular/core/testing/testing_internal';
-
import * as xml from '../../../src/i18n/serializers/xml_helper';
export function main(): void {
diff --git a/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
index 2b8d0828d4..57c9c52693 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
@@ -8,37 +8,24 @@
import {escapeRegExp} from '@angular/core/src/facade/lang';
-import {MessageBundle} from '../../../src/i18n/message_bundle';
+import {serializeNodes} from '../../../src/i18n/digest';
import {Xtb} from '../../../src/i18n/serializers/xtb';
-import {HtmlParser} from '../../../src/ml_parser/html_parser';
-import {DEFAULT_INTERPOLATION_CONFIG} from '../../../src/ml_parser/interpolation_config';
-import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
export function main(): void {
describe('XTB serializer', () => {
- let serializer: Xtb;
- let htmlParser: HtmlParser;
+ const serializer = new Xtb();
- function loadAsText(template: string, xtb: string): {[id: string]: string} {
- const messageBundle = new MessageBundle(htmlParser, [], {});
- messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG);
-
- const asAst = serializer.load(xtb, 'url', messageBundle);
- const asText: {[id: string]: string} = {};
- Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
-
- return asText;
+ 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;
}
- beforeEach(() => {
- htmlParser = new HtmlParser();
- serializer = new Xtb(htmlParser, DEFAULT_INTERPOLATION_CONFIG);
- });
-
describe('load', () => {
it('should load XTB files with a doctype', () => {
- const HTML = `bar
`;
-
const XTB = `
@@ -53,67 +40,61 @@ export function main(): void {
rab
`;
- expect(loadAsText(HTML, XTB)).toEqual({'8841459487341224498': 'rab'});
+ expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
});
it('should load XTB files without placeholders', () => {
- const HTML = `bar
`;
-
const XTB = `
rab
`;
- expect(loadAsText(HTML, XTB)).toEqual({'8841459487341224498': 'rab'});
+ expect(loadAsMap(XTB)).toEqual({'8841459487341224498': 'rab'});
});
it('should load XTB files with placeholders', () => {
- const HTML = ``;
-
const XTB = `
rab
`;
- expect(loadAsText(HTML, XTB)).toEqual({'8877975308926375834': 'rab
'});
+ expect(loadAsMap(XTB)).toEqual({
+ '8877975308926375834': 'rab'
+ });
});
it('should replace ICU placeholders with their translations', () => {
- const HTML = `-{ count, plural, =0 {
bar
}}-
`;
-
const XTB = `
- **
- { count, plural, =1 {rab}}
+ **
+ {VAR_PLURAL, plural, =1 {rab}}
`;
- expect(loadAsText(HTML, XTB)).toEqual({
- '1430521728694081603': `*{ count, plural, =1 {rab
}}*`,
- '4004755025589356097': `{ count, plural, =1 {rab
}}`,
+ expect(loadAsMap(XTB)).toEqual({
+ '7717087045075616176': `**`,
+ '5115002811911870583':
+ `{VAR_PLURAL, plural, =1 {[, rab, ]}}`,
});
});
it('should load complex XTB files', () => {
- const HTML = `
-foo bar {{ a + b }}
-{ count, plural, =0 {
bar
}}
-foo
-{ count, plural, =0 {{ sex, select, other {
bar
}} }}
`;
-
const XTB = `
rab oof
- { count, plural, =1 {rab}}
+ {VAR_PLURAL, plural, =1 {rab}}
oof
- { count, plural, =1 {{ sex, gender, male {rab}} }}
+ {VAR_PLURAL, plural, =1 {{VAR_GENDER, gender, male {rab}} }}
`;
- expect(loadAsText(HTML, XTB)).toEqual({
- '8281795707202401639': `{{ a + b }}rab oof`,
- '4004755025589356097': `{ count, plural, =1 {rab
}}`,
+ expect(loadAsMap(XTB)).toEqual({
+ '8281795707202401639':
+ `rab oof`,
+ '5115002811911870583':
+ `{VAR_PLURAL, plural, =1 {[, rab, ]}}`,
'130772889486467622': `oof`,
- '4244993204427636474': `{ count, plural, =1 {{ sex, gender, male {rab
}} }}`,
+ '4739316421648347533':
+ `{VAR_PLURAL, plural, =1 {[{VAR_GENDER, gender, male {[, rab, ]}}, ]}}`,
});
});
});
@@ -124,7 +105,7 @@ export function main(): void {
'';
expect(() => {
- loadAsText('', XTB);
+ loadAsMap(XTB);
}).toThrowError(/ elements can not be nested/);
});
@@ -133,58 +114,49 @@ export function main(): void {
`;
- expect(() => {
- loadAsText('', XTB);
- }).toThrowError(/ misses the "id" attribute/);
+ expect(() => { loadAsMap(XTB); }).toThrowError(/ misses the "id" attribute/);
});
it('should throw when a placeholder has no name attribute', () => {
- const HTML = 'give me a message
';
-
const XTB = `
`;
- expect(() => { loadAsText(HTML, XTB); }).toThrowError(/ misses the "name" attribute/);
+ expect(() => { loadAsMap(XTB); }).toThrowError(/ misses the "name" attribute/);
});
- it('should throw when a placeholder is not present in the source message', () => {
- const HTML = `bar
`;
+ it('should throw on unknown xtb tags', () => {
+ const XTB = ``;
- const XTB = `
-
-
+ expect(() => {
+ loadAsMap(XTB);
+ }).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]")`)));
+ });
+
+ it('should throw on unknown message tags', () => {
+ const XTB = `
+ msg should contain only ph tags
+`;
+
+ expect(() => { loadAsMap(XTB); })
+ .toThrowError(
+ new RegExp(escapeRegExp(`[ERROR ->]msg should contain only ph tags`)));
+ });
+
+ it('should throw on duplicate message id', () => {
+ const XTB = `
+ msg1
+ msg2
`;
expect(() => {
- loadAsText(HTML, XTB);
- }).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
+ loadAsMap(XTB);
+ }).toThrowError(/Duplicated translations for msg 1186013544048295927/);
});
- });
- it('should throw when the translation results in invalid html', () => {
- const HTML = ``;
-
- const XTB = `
-
- rab
-`;
-
- expect(() => {
- loadAsText(HTML, XTB);
- }).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
+ it('should throw when trying to save an xtb file',
+ () => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
});
-
- it('should throw on unknown tags', () => {
- const XTB = ``;
-
- expect(() => {
- loadAsText('', XTB);
- }).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]")`)));
- });
-
- it('should throw when trying to save an xtb file',
- () => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
});
}
\ No newline at end of file
diff --git a/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts b/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts
new file mode 100644
index 0000000000..d6f51e331d
--- /dev/null
+++ b/modules/@angular/compiler/test/i18n/translation_bundle_spec.ts
@@ -0,0 +1,110 @@
+/**
+ * @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 '../../src/i18n/i18n_ast';
+import {TranslationBundle} from '../../src/i18n/translation_bundle';
+import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '../../src/parse_util';
+import {serializeNodes} from '../ml_parser/ast_serializer_spec';
+
+export function main(): void {
+ describe('TranslationBundle', () => {
+ const file = new ParseSourceFile('content', 'url');
+ const location = new ParseLocation(file, 0, 0, 0);
+ const span = new ParseSourceSpan(location, null);
+ const srcNode = new i18n.Text('src', span);
+
+ it('should translate a plain message', () => {
+ const msgMap = {foo: [new i18n.Text('bar', null)]};
+ const tb = new TranslationBundle(msgMap, (_) => 'foo');
+ const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
+ expect(serializeNodes(tb.get(msg))).toEqual(['bar']);
+ });
+
+ it('should translate a message with placeholder', () => {
+ const msgMap = {
+ foo: [
+ new i18n.Text('bar', null),
+ new i18n.Placeholder('', 'ph1', null),
+ ]
+ };
+ const phMap = {
+ ph1: '*phContent*',
+ };
+ const tb = new TranslationBundle(msgMap, (_) => 'foo');
+ const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
+ expect(serializeNodes(tb.get(msg))).toEqual(['bar*phContent*']);
+ });
+
+ it('should translate a message with placeholder referencing messages', () => {
+ const msgMap = {
+ foo: [
+ new i18n.Text('--', null),
+ new i18n.Placeholder('', 'ph1', null),
+ new i18n.Text('++', null),
+ ],
+ ref: [
+ new i18n.Text('*refMsg*', null),
+ ],
+ };
+ const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
+ const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
+ let count = 0;
+ const digest = (_: any) => count++ ? 'ref' : 'foo';
+ const tb = new TranslationBundle(msgMap, digest);
+
+ expect(serializeNodes(tb.get(msg))).toEqual(['--*refMsg*++']);
+ });
+
+ describe('errors', () => {
+ it('should report unknown placeholders', () => {
+ const msgMap = {
+ foo: [
+ new i18n.Text('bar', null),
+ new i18n.Placeholder('', 'ph1', span),
+ ]
+ };
+ const tb = new TranslationBundle(msgMap, (_) => 'foo');
+ const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
+ expect(() => tb.get(msg)).toThrowError(/Unknown placeholder/);
+ });
+
+ it('should report missing translation', () => {
+ const tb = new TranslationBundle({}, (_) => 'foo');
+ const msg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
+ expect(() => tb.get(msg)).toThrowError(/Missing translation for message foo/);
+ });
+
+ it('should report missing referenced message', () => {
+ const msgMap = {
+ foo: [new i18n.Placeholder('', 'ph1', span)],
+ };
+ const refMsg = new i18n.Message([srcNode], {}, {}, 'm', 'd');
+ const msg = new i18n.Message([srcNode], {}, {ph1: refMsg}, 'm', 'd');
+ let count = 0;
+ const digest = (_: any) => count++ ? 'ref' : 'foo';
+ const tb = new TranslationBundle(msgMap, digest);
+ expect(() => tb.get(msg)).toThrowError(/Missing translation for message ref/);
+ });
+
+ it('should report invalid translated html', () => {
+ const msgMap = {
+ foo: [
+ new i18n.Text('text', null),
+ new i18n.Placeholder('', 'ph1', null),
+ ]
+ };
+ const phMap = {
+ ph1: '',
+ };
+ const tb = new TranslationBundle(msgMap, (_) => 'foo');
+ const msg = new i18n.Message([srcNode], phMap, {}, 'm', 'd');
+ expect(() => tb.get(msg)).toThrowError(/Unexpected closing tag "b"/);
+ });
+ });
+ });
+}
diff --git a/modules/@angular/compiler/test/ml_parser/ast_serializer_spec.ts b/modules/@angular/compiler/test/ml_parser/ast_serializer_spec.ts
index 5e33a302d5..9452c3c9fb 100644
--- a/modules/@angular/compiler/test/ml_parser/ast_serializer_spec.ts
+++ b/modules/@angular/compiler/test/ml_parser/ast_serializer_spec.ts
@@ -97,4 +97,4 @@ const serializerVisitor = new _SerializerVisitor();
export function serializeNodes(nodes: html.Node[]): string[] {
return nodes.map(node => node.visit(serializerVisitor, null));
-}
+}
\ No newline at end of file