From 43512aa5ebd3844ec989bdbf0457fdc113221e59 Mon Sep 17 00:00:00 2001
From: Victor Berchet
Date: Tue, 9 Aug 2016 21:05:04 -0700
Subject: [PATCH] fix(i18n): ICU placeholders are replaced by their
translations (#10586)
They were replaced by the original message.
---
modules/@angular/compiler/src/i18n/digest.ts | 2 +-
.../@angular/compiler/src/i18n/i18n_ast.ts | 11 +-
.../@angular/compiler/src/i18n/i18n_parser.ts | 17 +-
.../compiler/src/i18n/message_bundle.ts | 2 +
.../src/i18n/serializers/serializer.ts | 28 +++-
.../compiler/src/i18n/serializers/xmb.ts | 4 +-
.../compiler/src/i18n/serializers/xtb.ts | 78 ++++++++--
.../compiler/src/i18n/translation_bundle.ts | 10 +-
.../compiler/test/i18n/i18n_parser_spec.ts | 20 ++-
.../test/i18n/serializers/xmb_spec.ts | 4 +-
.../test/i18n/serializers/xtb_spec.ts | 146 ++++++++++--------
11 files changed, 220 insertions(+), 102 deletions(-)
diff --git a/modules/@angular/compiler/src/i18n/digest.ts b/modules/@angular/compiler/src/i18n/digest.ts
index dd37340a9d..7159df5bb1 100644
--- a/modules/@angular/compiler/src/i18n/digest.ts
+++ b/modules/@angular/compiler/src/i18n/digest.ts
@@ -97,7 +97,7 @@ export function sha1(str: string): string {
hex += (b >>> 4 & 0x0f).toString(16) + (b & 0x0f).toString(16);
}
- return hex;
+ return hex.toLowerCase();
}
function utf8Encode(str: string): string {
diff --git a/modules/@angular/compiler/src/i18n/i18n_ast.ts b/modules/@angular/compiler/src/i18n/i18n_ast.ts
index 1a2806d96e..7ed07b6b87 100644
--- a/modules/@angular/compiler/src/i18n/i18n_ast.ts
+++ b/modules/@angular/compiler/src/i18n/i18n_ast.ts
@@ -9,8 +9,17 @@
import {ParseSourceSpan} from '../parse_util';
export class Message {
+ /**
+ * @param nodes message AST
+ * @param placeholders maps placeholder names to static content
+ * @param placeholderToMsgIds maps placeholder names to translatable message IDs (used for ICU
+ * messages)
+ * @param meaning
+ * @param description
+ */
constructor(
- public nodes: Node[], public placeholders: {[name: string]: string}, public meaning: string,
+ public nodes: Node[], public placeholders: {[name: string]: string},
+ public placeholderToMsgIds: {[name: string]: string}, public meaning: string,
public description: string) {}
}
diff --git a/modules/@angular/compiler/src/i18n/i18n_parser.ts b/modules/@angular/compiler/src/i18n/i18n_parser.ts
index 7d58dac71b..55b0412bb6 100644
--- a/modules/@angular/compiler/src/i18n/i18n_parser.ts
+++ b/modules/@angular/compiler/src/i18n/i18n_parser.ts
@@ -12,6 +12,7 @@ import * as html from '../ml_parser/ast';
import {getHtmlTagDefinition} from '../ml_parser/html_tags';
import {InterpolationConfig} from '../ml_parser/interpolation_config';
import {ParseSourceSpan} from '../parse_util';
+import {digestMessage} from './digest';
import * as i18n from './i18n_ast';
import {PlaceholderRegistry} from './serializers/placeholder';
@@ -19,7 +20,7 @@ import {PlaceholderRegistry} from './serializers/placeholder';
const _expParser = new ExpressionParser(new ExpressionLexer());
/**
- * Returns a function converting html Messages to i18n Messages given an interpolationConfig
+ * Returns a function converting html nodes to an i18n Message given an interpolationConfig
*/
export function createI18nMessageFactory(interpolationConfig: InterpolationConfig): (
nodes: html.Node[], meaning: string, description: string) => i18n.Message {
@@ -34,6 +35,7 @@ class _I18nVisitor implements html.Visitor {
private _icuDepth: number;
private _placeholderRegistry: PlaceholderRegistry;
private _placeholderToContent: {[name: string]: string};
+ private _placeholderToIds: {[name: string]: string};
constructor(
private _expressionParser: ExpressionParser,
@@ -44,10 +46,12 @@ class _I18nVisitor implements html.Visitor {
this._icuDepth = 0;
this._placeholderRegistry = new PlaceholderRegistry();
this._placeholderToContent = {};
+ this._placeholderToIds = {};
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
- return new i18n.Message(i18nodes, this._placeholderToContent, meaning, description);
+ return new i18n.Message(
+ i18nodes, this._placeholderToContent, this._placeholderToIds, meaning, description);
}
visitElement(el: html.Element, context: any): i18n.Node {
@@ -99,9 +103,14 @@ class _I18nVisitor implements html.Visitor {
return i18nIcu;
}
- // else returns a placeholder
+ // Else returns a placeholder
+ // ICU placeholders should not be replaced with their original content but with the their
+ // translations. We need to create a new visitor (they are not re-entrant) to compute the
+ // message id.
+ // TODO(vicb): add a html.Node -> i18n.Message cache to avoid having to re-create the msg
const phName = this._placeholderRegistry.getPlaceholderName('ICU', icu.sourceSpan.toString());
- this._placeholderToContent[phName] = icu.sourceSpan.toString();
+ const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
+ this._placeholderToIds[phName] = digestMessage(visitor.toI18nMessage([icu], '', ''));
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
}
diff --git a/modules/@angular/compiler/src/i18n/message_bundle.ts b/modules/@angular/compiler/src/i18n/message_bundle.ts
index e3b7dbb0d1..a3918e8100 100644
--- a/modules/@angular/compiler/src/i18n/message_bundle.ts
+++ b/modules/@angular/compiler/src/i18n/message_bundle.ts
@@ -45,5 +45,7 @@ export class MessageBundle {
(message) => { this._messageMap[digestMessage(message)] = message; });
}
+ getMessageMap(): {[id: string]: Message} { return this._messageMap; }
+
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
}
diff --git a/modules/@angular/compiler/src/i18n/serializers/serializer.ts b/modules/@angular/compiler/src/i18n/serializers/serializer.ts
index ad8565c204..3c5f94d042 100644
--- a/modules/@angular/compiler/src/i18n/serializers/serializer.ts
+++ b/modules/@angular/compiler/src/i18n/serializers/serializer.ts
@@ -8,10 +8,34 @@
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast';
+import {MessageBundle} from '../message_bundle';
export interface Serializer {
write(messageMap: {[id: string]: i18n.Message}): string;
- load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
- {[id: string]: html.Node[]};
+ load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]};
+}
+
+// Generate a map of placeholder to content indexed by message ids
+export function extractPlaceholders(messageBundle: MessageBundle) {
+ const messageMap = messageBundle.getMessageMap();
+ let placeholders: {[id: string]: {[name: string]: string}} = {};
+
+ Object.keys(messageMap).forEach(msgId => {
+ placeholders[msgId] = messageMap[msgId].placeholders;
+ });
+
+ return placeholders;
+}
+
+// Generate a map of placeholder to message ids indexed by message ids
+export function extractPlaceholderToIds(messageBundle: MessageBundle) {
+ const messageMap = messageBundle.getMessageMap();
+ let placeholderToIds: {[id: string]: {[name: string]: string}} = {};
+
+ Object.keys(messageMap).forEach(msgId => {
+ placeholderToIds[msgId] = messageMap[msgId].placeholderToMsgIds;
+ });
+
+ return placeholderToIds;
}
\ No newline at end of file
diff --git a/modules/@angular/compiler/src/i18n/serializers/xmb.ts b/modules/@angular/compiler/src/i18n/serializers/xmb.ts
index fc87064e05..a0b26e0c87 100644
--- a/modules/@angular/compiler/src/i18n/serializers/xmb.ts
+++ b/modules/@angular/compiler/src/i18n/serializers/xmb.ts
@@ -9,6 +9,7 @@
import {ListWrapper} from '../../facade/collection';
import * as html from '../../ml_parser/ast';
import * as i18n from '../i18n_ast';
+import {MessageBundle} from '../message_bundle';
import {Serializer} from './serializer';
import * as xml from './xml_helper';
@@ -70,8 +71,7 @@ export class Xmb implements Serializer {
]);
}
- load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
- {[id: string]: html.Node[]} {
+ load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]} {
throw new Error('Unsupported');
}
}
diff --git a/modules/@angular/compiler/src/i18n/serializers/xtb.ts b/modules/@angular/compiler/src/i18n/serializers/xtb.ts
index 485b00d2b6..3c12f95c04 100644
--- a/modules/@angular/compiler/src/i18n/serializers/xtb.ts
+++ b/modules/@angular/compiler/src/i18n/serializers/xtb.ts
@@ -12,9 +12,10 @@ import {InterpolationConfig} from '../../ml_parser/interpolation_config';
import {XmlParser} from '../../ml_parser/xml_parser';
import {ParseError} from '../../parse_util';
import * as i18n from '../i18n_ast';
+import {MessageBundle} from '../message_bundle';
import {I18nError} from '../parse_util';
-import {Serializer} from './serializer';
+import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
const _TRANSLATIONS_TAG = 'translationbundle';
const _TRANSLATION_TAG = 'translation';
@@ -25,8 +26,7 @@ export class Xtb implements Serializer {
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
- load(content: string, url: string, placeholders: {[id: string]: {[name: string]: string}}):
- {[id: string]: ml.Node[]} {
+ load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} {
// Parse the xtb file into xml nodes
const result = new XmlParser().parse(content, url);
@@ -35,7 +35,7 @@ export class Xtb implements Serializer {
}
// Replace the placeholders, messages are now string
- const {messages, errors} = new _Serializer().parse(result.rootNodes, placeholders);
+ const {messages, errors} = new _Serializer().parse(result.rootNodes, messageBundle);
if (errors.length) {
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
@@ -44,7 +44,7 @@ export class Xtb implements Serializer {
// Convert the string messages to html ast
// TODO(vicb): map error message back to the original message in xtb
let messageMap: {[id: string]: ml.Node[]} = {};
- let parseErrors: ParseError[] = [];
+ const parseErrors: ParseError[] = [];
Object.keys(messages).forEach((id) => {
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
@@ -61,24 +61,58 @@ export class Xtb implements Serializer {
}
class _Serializer implements ml.Visitor {
- private _messages: {[id: string]: string};
+ private _messageNodes: [string, ml.Node[]][];
+ private _translatedMessages: {[id: string]: string};
private _bundleDepth: number;
private _translationDepth: number;
private _errors: I18nError[];
- private _placeholders: {[id: string]: {[name: string]: string}};
- private _currentPlaceholders: {[name: string]: string};
+ private _placeholders: {[name: string]: string};
+ private _placeholderToIds: {[name: string]: string};
- parse(nodes: ml.Node[], _placeholders: {[id: string]: {[name: string]: string}}):
+ parse(nodes: ml.Node[], messageBundle: MessageBundle):
{messages: {[k: string]: string}, errors: I18nError[]} {
- this._messages = {};
+ this._messageNodes = [];
+ this._translatedMessages = {};
this._bundleDepth = 0;
this._translationDepth = 0;
this._errors = [];
- this._placeholders = _placeholders;
+ // Find all messages
ml.visitAll(this, nodes, null);
- return {messages: this._messages, errors: this._errors};
+ const messageMap = messageBundle.getMessageMap();
+ const placeholders = extractPlaceholders(messageBundle);
+ const placeholderToIds = extractPlaceholderToIds(messageBundle);
+
+ this._messageNodes
+ .filter(message => {
+ // Remove any messages that is not present in the source message bundle.
+ return messageMap.hasOwnProperty(message[0]);
+ })
+ .sort((a, b) => {
+ // Because there could be no ICU placeholders inside an ICU message,
+ // we do not need to take into account the `placeholderToMsgIds` of the referenced
+ // messages, those would always be empty
+ // TODO(vicb): overkill - create 2 buckets and [...woDeps, ...wDeps].process()
+ if (Object.keys(messageMap[a[0]].placeholderToMsgIds).length == 0) {
+ return -1;
+ }
+
+ if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
+ return 1;
+ }
+
+ return 0;
+ })
+ .forEach(message => {
+ const id = message[0];
+ this._placeholders = placeholders[id] || {};
+ this._placeholderToIds = placeholderToIds[id] || {};
+ // TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
+ this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
+ });
+
+ return {messages: this._translatedMessages, errors: this._errors};
}
visitElement(element: ml.Element, context: any): any {
@@ -101,8 +135,11 @@ class _Serializer implements ml.Visitor {
if (!idAttr) {
this._addError(element, `<${_TRANSLATION_TAG}> misses the "id" attribute`);
} else {
- this._currentPlaceholders = this._placeholders[idAttr.value] || {};
- this._messages[idAttr.value] = ml.visitAll(this, element.children).join('');
+ // ICU placeholders are reference to other messages.
+ // The referenced message might not have been decoded yet.
+ // We need to have all messages available to make sure deps are decoded first.
+ // TODO(vicb): report an error on duplicate id
+ this._messageNodes.push([idAttr.value, element.children]);
}
this._translationDepth--;
break;
@@ -112,11 +149,18 @@ class _Serializer implements ml.Visitor {
if (!nameAttr) {
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
} else {
- if (this._currentPlaceholders.hasOwnProperty(nameAttr.value)) {
- return this._currentPlaceholders[nameAttr.value];
+ const name = nameAttr.value;
+ if (this._placeholders.hasOwnProperty(name)) {
+ return this._placeholders[name];
}
+ if (this._placeholderToIds.hasOwnProperty(name) &&
+ this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])) {
+ return this._translatedMessages[this._placeholderToIds[name]];
+ }
+ // TODO(vicb): better error message for when
+ // !this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])
this._addError(
- element, `The placeholder "${nameAttr.value}" does not exists in the source message`);
+ element, `The placeholder "${name}" does not exists in the source message`);
}
break;
diff --git a/modules/@angular/compiler/src/i18n/translation_bundle.ts b/modules/@angular/compiler/src/i18n/translation_bundle.ts
index c8072abb10..28cbe0587f 100644
--- a/modules/@angular/compiler/src/i18n/translation_bundle.ts
+++ b/modules/@angular/compiler/src/i18n/translation_bundle.ts
@@ -7,8 +7,9 @@
*/
import * as html from '../ml_parser/ast';
-import {Serializer} from './serializers/serializer';
+import {MessageBundle} from './message_bundle';
+import {Serializer} from './serializers/serializer';
/**
* A container for translated messages
@@ -16,10 +17,9 @@ import {Serializer} from './serializers/serializer';
export class TranslationBundle {
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {}
- static load(
- content: string, url: string, placeholders: {[id: string]: {[name: string]: string}},
- serializer: Serializer): TranslationBundle {
- return new TranslationBundle(serializer.load(content, url, placeholders));
+ static load(content: string, url: string, messageBundle: MessageBundle, serializer: Serializer):
+ TranslationBundle {
+ return new TranslationBundle(serializer.load(content, url, messageBundle));
}
get(id: string): html.Node[] { return this._messageMap[id]; }
diff --git a/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts b/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts
index fbb3e941a9..7a0063ed35 100644
--- a/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts
+++ b/modules/@angular/compiler/test/i18n/i18n_parser_spec.ts
@@ -272,12 +272,17 @@ export function main() {
[['{count, plural, =1 {[1]}}'], '', ''],
]);
- expect(_humanizePlaceholders(html)).toEqual([
- 'ICU={count, plural, =0 {0}}, ICU_1={count, plural, =1 {1}}',
+ // ICU message placeholders are reference to translations.
+ // As such they have no static content but refs to message ids.
+ expect(_humanizePlaceholders(html)).toEqual(['', '', '', '']);
+
+ expect(_humanizePlaceholdersToIds(html)).toEqual([
+ 'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe',
'',
'',
'',
]);
+
});
});
});
@@ -303,6 +308,17 @@ function _humanizePlaceholders(
// clang-format on
}
+function _humanizePlaceholdersToIds(
+ html: string, implicitTags: string[] = [],
+ implicitAttrs: {[k: string]: string[]} = {}): string[] {
+ // clang-format off
+ // https://github.com/angular/clang-format/issues/35
+ return _extractMessages(html, implicitTags, implicitAttrs).map(
+ msg => Object.keys(msg.placeholderToMsgIds).map(k => `${k}=${msg.placeholderToMsgIds[k]}`).join(', '));
+ // clang-format on
+}
+
+
function _extractMessages(
html: string, implicitTags: string[] = [],
implicitAttrs: {[k: string]: string[]} = {}): Message[] {
diff --git a/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
index e6cebae272..0c49bbf951 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xmb_spec.ts
@@ -56,8 +56,8 @@ export function main(): void {
it('should throw when trying to load an xmb file', () => {
expect(() => {
const serializer = new Xmb();
- serializer.load(XMB, 'url', {});
- }).toThrow();
+ serializer.load(XMB, 'url', null);
+ }).toThrowError(/Unsupported/);
});
});
}
diff --git a/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts b/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
index 8828e7e4fe..f8f8e97a84 100644
--- a/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
+++ b/modules/@angular/compiler/test/i18n/serializers/xtb_spec.ts
@@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Xtb} from '@angular/compiler/src/i18n/serializers/xtb';
import {escapeRegExp} from '@angular/core/src/facade/lang';
import {beforeEach, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
+import {MessageBundle} from '../../../src/i18n/message_bundle';
+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';
@@ -17,21 +18,28 @@ import {serializeNodes} from '../../ml_parser/ast_serializer_spec';
export function main(): void {
describe('XTB serializer', () => {
let serializer: Xtb;
+ let htmlParser: HtmlParser;
- function loadAsText(content: string, placeholders: {[id: string]: {[name: string]: string}}):
- {[id: string]: string} {
- const asAst = serializer.load(content, 'url', placeholders);
+ function loadAsText(template: string, xtb: string): {[id: string]: string} {
+ let messageBundle = new MessageBundle(htmlParser, [], {});
+ messageBundle.updateFromTemplate(template, 'url', DEFAULT_INTERPOLATION_CONFIG);
+
+ const asAst = serializer.load(xtb, 'url', messageBundle);
let asText: {[id: string]: string} = {};
Object.keys(asAst).forEach(id => { asText[id] = serializeNodes(asAst[id]).join(''); });
return asText;
}
- beforeEach(() => { serializer = new Xtb(new HtmlParser(), DEFAULT_INTERPOLATION_CONFIG); });
-
+ 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 = `
@@ -43,62 +51,75 @@ export function main(): void {
]>
- bar
+ rab
`;
- expect(loadAsText(XTB, {})).toEqual({foo: 'bar'});
+ expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
});
it('should load XTB files without placeholders', () => {
+ const HTML = `bar
`;
+
const XTB = `
- bar
+ rab
`;
- expect(loadAsText(XTB, {})).toEqual({foo: 'bar'});
+ expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
});
it('should load XTB files with placeholders', () => {
+ const HTML = ``;
+
const XTB = `
- bar
+ rab
`;
- expect(loadAsText(XTB, {foo: {PLACEHOLDER: '!'}})).toEqual({foo: 'bar!!'});
+ expect(loadAsText(HTML, XTB)).toEqual({
+ '7de4d8ff1e42b7b31da6204074818236a9a5317f': 'rab
'
+ });
+ });
+
+ it('should replace ICU placeholders with their translations', () => {
+ const HTML = `-{ count, plural, =0 {
bar
}}-
`;
+
+ const XTB = ` xml version="1.0" encoding="UTF-8" ?>
+
+ **
+ { count, plural, =1 {rab}}
+`;
+
+ expect(loadAsText(HTML, XTB)).toEqual({
+ 'eb404e202fed4846e25e7d9ac1fcb719fe4da257': `*{ count, plural, =1 {rab
}}*`,
+ 'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {rab
}}`,
+ });
});
it('should load complex XTB files', () => {
+ const HTML = `
+foo bar {{ a + b }}
+{ count, plural, =0 {
bar
}}
+foo
+{ count, plural, =0 {{ sex, gender, other {
bar
}} }}
`;
+
const XTB = ` xml version="1.0" encoding="UTF-8" ?>
- translatable element <b>with placeholders</b>
- { count, plural, =0 {<p>test</p>}}
- foo
- { count, plural, =0 {{ sex, gender, other {<p>deeply nested</p>}} }}
+ rab oof
+ { count, plural, =1 {rab}}
+ oof
+ { count, plural, =1 {{ sex, gender, male {rab}} }}
`;
- const PLACEHOLDERS = {
- a: {
- START_BOLD_TEXT: '',
- CLOSE_BOLD_TEXT: '',
- INTERPOLATION: '{{ a + b }}',
- },
- b: {
- START_PARAGRAPH: '',
- CLOSE_PARAGRAPH: '
',
- },
- d: {
- START_PARAGRAPH: '',
- CLOSE_PARAGRAPH: '
',
- },
- };
-
- expect(loadAsText(XTB, PLACEHOLDERS)).toEqual({
- a: 'translatable element with placeholders {{ a + b }}',
- b: '{ count, plural, =0 {test
}}',
- c: 'foo',
- d: '{ count, plural, =0 {{ sex, gender, other {deeply nested
}} }}',
+ expect(loadAsText(HTML, XTB)).toEqual({
+ '7103b4b13b616270a0044efade97d8b4f96f2ca6': `{{ a + b }}rab oof`,
+ 'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {rab
}}`,
+ 'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': `oof`,
+ 'e3bf2d706c3da16ce05658e07f62f0519f7c561c':
+ `{ count, plural, =1 {{ sex, gender, male {rab
}} }}`,
});
});
+
});
describe('errors', () => {
@@ -107,61 +128,54 @@ export function main(): void {
'';
expect(() => {
- serializer.load(XTB, 'url', {});
+ loadAsText('', XTB);
}).toThrowError(/ elements can not be nested/);
});
- it('should throw on nested ', () => {
- const XTB = `
-
-
-
-
-`;
-
- expect(() => {
- serializer.load(XTB, 'url', {});
- }).toThrowError(/ elements can not be nested/);
- });
-
it('should throw when a has no id attribute', () => {
const XTB = `
`;
expect(() => {
- serializer.load(XTB, 'url', {});
+ loadAsText('', 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(() => {
- serializer.load(XTB, 'url', {});
- }).toThrowError(/ misses the "name" attribute/);
+ expect(() => { loadAsText(HTML, XTB); }).toThrowError(/ misses the "name" attribute/);
});
it('should throw when a placeholder is not present in the source message', () => {
- const XTB = `
-
+ const HTML = `bar
`;
+
+ const XTB = `
+
+
`;
expect(() => {
- serializer.load(XTB, 'url', {});
+ loadAsText(HTML, XTB);
}).toThrowError(/The placeholder "UNKNOWN" does not exists in the source message/);
});
});
it('should throw when the translation results in invalid html', () => {
- const XTB = `
- foobar
+ const HTML = ``;
+
+ const XTB = `
+
+ rab
`;
expect(() => {
- serializer.load(XTB, 'url', {fail: {CLOSE_P: '
'}});
+ loadAsText(HTML, XTB);
}).toThrowError(/xtb parse errors:\nUnexpected closing tag "p"/);
});
@@ -170,11 +184,11 @@ export function main(): void {
const XTB = ``;
expect(() => {
- serializer.load(XTB, 'url', {});
+ loadAsText('', XTB);
}).toThrowError(new RegExp(escapeRegExp(`Unexpected tag ("[ERROR ->]")`)));
});
- it('should throw when trying to save an xmb file',
- () => { expect(() => { serializer.write({}); }).toThrow(); });
+ it('should throw when trying to save an xtb file',
+ () => { expect(() => { serializer.write({}); }).toThrowError(/Unsupported/); });
});
-}
+}
\ No newline at end of file