parent
582550a90d
commit
08c038ebd9
|
@ -1,13 +0,0 @@
|
|||
<?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="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4">käännä teksti</translation>
|
||||
<translation id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">tervetuloa</translation>
|
||||
<translation id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</translation>
|
||||
</translationbundle>
|
|
@ -34,9 +34,9 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
|
|||
<!ELEMENT ex (#PCDATA)>
|
||||
]>
|
||||
<messagebundle>
|
||||
<msg id="63a85808f03b8181e36a952e0fa38202c2304862">other-3rdP-component</msg>
|
||||
<msg id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" desc="desc" meaning="meaning">translate me</msg>
|
||||
<msg id="65cc4ab3b4c438e07c89be2b677d08369fb62da2">Welcome</msg>
|
||||
<msg id="252798779920123642">other-3rdP-component</msg>
|
||||
<msg id="7281825156779575080" desc="desc" meaning="meaning">translate me</msg>
|
||||
<msg id="1325493959242906696">Welcome</msg>
|
||||
</messagebundle>
|
||||
`;
|
||||
|
||||
|
@ -79,5 +79,4 @@ describe('template i18n extraction output', () => {
|
|||
const xlf = fs.readFileSync(xlfOutput, {encoding: 'utf-8'});
|
||||
expect(xlf).toEqual(EXPECTED_XLIFF);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -8,10 +8,14 @@
|
|||
|
||||
import * as i18n from './i18n_ast';
|
||||
|
||||
export function digestMessage(message: i18n.Message): string {
|
||||
export function digest(message: i18n.Message): string {
|
||||
return sha1(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
|
||||
}
|
||||
|
||||
export function decimalDigest(message: i18n.Message): string {
|
||||
return fingerprint(serializeNodes(message.nodes).join('') + `[${message.meaning}]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the i18n ast to something xml-like in order to generate an UID.
|
||||
*
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as html from '../ml_parser/ast';
|
|||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {ParseTreeResult} from '../ml_parser/parser';
|
||||
|
||||
import {digestMessage} from './digest';
|
||||
import {digest} from './digest';
|
||||
import * as i18n from './i18n_ast';
|
||||
import {createI18nMessageFactory} from './i18n_parser';
|
||||
import {I18nError} from './parse_util';
|
||||
|
@ -214,8 +214,8 @@ class _Visitor implements html.Visitor {
|
|||
// Extract only top level nodes with the (implicit) "i18n" attribute if not in a block or an ICU
|
||||
// message
|
||||
const i18nAttr = _getI18nAttr(el);
|
||||
const isImplicit = this._implicitTags.some((tag: string): boolean => el.name === tag) &&
|
||||
!this._inIcu && !this._isInTranslatableSection;
|
||||
const isImplicit = this._implicitTags.some(tag => el.name === tag) && !this._inIcu &&
|
||||
!this._isInTranslatableSection;
|
||||
const isTopLevelImplicit = !wasInImplicitNode && isImplicit;
|
||||
this._inImplicitNode = this._inImplicitNode || isImplicit;
|
||||
|
||||
|
@ -348,14 +348,14 @@ class _Visitor implements html.Visitor {
|
|||
// no-op when called in extraction mode (returns [])
|
||||
private _translateMessage(el: html.Node, message: i18n.Message): html.Node[] {
|
||||
if (message && this._mode === _VisitorMode.Merge) {
|
||||
const id = digestMessage(message);
|
||||
const nodes = this._translations.get(id);
|
||||
const nodes = this._translations.get(message);
|
||||
|
||||
if (nodes) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
this._reportError(el, `Translation unavailable for message id="${id}"`);
|
||||
this._reportError(
|
||||
el, `Translation unavailable for message id="${this._translations.digest(message)}"`);
|
||||
}
|
||||
|
||||
return [];
|
||||
|
@ -384,19 +384,20 @@ class _Visitor implements html.Visitor {
|
|||
if (attr.value && attr.value != '' && i18nAttributeMeanings.hasOwnProperty(attr.name)) {
|
||||
const meaning = i18nAttributeMeanings[attr.name];
|
||||
const message: i18n.Message = this._createI18nMessage([attr], meaning, '');
|
||||
const id = digestMessage(message);
|
||||
const nodes = this._translations.get(id);
|
||||
const nodes = this._translations.get(message);
|
||||
if (nodes) {
|
||||
if (nodes[0] instanceof html.Text) {
|
||||
const value = (nodes[0] as html.Text).value;
|
||||
translatedAttributes.push(new html.Attribute(attr.name, value, attr.sourceSpan));
|
||||
} else {
|
||||
this._reportError(
|
||||
el, `Unexpected translation for attribute "${attr.name}" (id="${id}")`);
|
||||
el,
|
||||
`Unexpected translation for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
|
||||
}
|
||||
} else {
|
||||
this._reportError(
|
||||
el, `Translation unavailable for attribute "${attr.name}" (id="${id}")`);
|
||||
el,
|
||||
`Translation unavailable for attribute "${attr.name}" (id="${this._translations.digest(message)}")`);
|
||||
}
|
||||
} else {
|
||||
translatedAttributes.push(attr);
|
||||
|
|
|
@ -12,14 +12,13 @@ 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 placeholderToMessage maps placeholder names to messages (used for nested ICU messages)
|
||||
* @param meaning
|
||||
* @param description
|
||||
*/
|
||||
constructor(
|
||||
public nodes: Node[], public placeholders: {[name: string]: string},
|
||||
public placeholderToMsgIds: {[name: string]: string}, public meaning: string,
|
||||
public nodes: Node[], public placeholders: {[phName: string]: string},
|
||||
public placeholderToMessage: {[phName: string]: Message}, public meaning: string,
|
||||
public description: string) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +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 {digest} from './digest';
|
||||
|
||||
import * as i18n from './i18n_ast';
|
||||
import {PlaceholderRegistry} from './serializers/placeholder';
|
||||
|
@ -35,7 +35,7 @@ class _I18nVisitor implements html.Visitor {
|
|||
private _icuDepth: number;
|
||||
private _placeholderRegistry: PlaceholderRegistry;
|
||||
private _placeholderToContent: {[name: string]: string};
|
||||
private _placeholderToIds: {[name: string]: string};
|
||||
private _placeholderToMessage: {[name: string]: i18n.Message};
|
||||
|
||||
constructor(
|
||||
private _expressionParser: ExpressionParser,
|
||||
|
@ -46,12 +46,12 @@ class _I18nVisitor implements html.Visitor {
|
|||
this._icuDepth = 0;
|
||||
this._placeholderRegistry = new PlaceholderRegistry();
|
||||
this._placeholderToContent = {};
|
||||
this._placeholderToIds = {};
|
||||
this._placeholderToMessage = {};
|
||||
|
||||
const i18nodes: i18n.Node[] = html.visitAll(this, nodes, {});
|
||||
|
||||
return new i18n.Message(
|
||||
i18nodes, this._placeholderToContent, this._placeholderToIds, meaning, description);
|
||||
i18nodes, this._placeholderToContent, this._placeholderToMessage, meaning, description);
|
||||
}
|
||||
|
||||
visitElement(el: html.Element, context: any): i18n.Node {
|
||||
|
@ -110,7 +110,7 @@ class _I18nVisitor implements html.Visitor {
|
|||
// 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());
|
||||
const visitor = new _I18nVisitor(this._expressionParser, this._interpolationConfig);
|
||||
this._placeholderToIds[phName] = digestMessage(visitor.toI18nMessage([icu], '', ''));
|
||||
this._placeholderToMessage[phName] = visitor.toI18nMessage([icu], '', '');
|
||||
return new i18n.IcuPlaceholder(i18nIcu, phName, icu.sourceSpan);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import {HtmlParser} from '../ml_parser/html_parser';
|
|||
import {InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {ParseError} from '../parse_util';
|
||||
|
||||
import {digestMessage} from './digest';
|
||||
import {extractMessages} from './extractor_merger';
|
||||
import {Message} from './i18n_ast';
|
||||
import {Serializer} from './serializers/serializer';
|
||||
|
@ -19,7 +18,7 @@ import {Serializer} from './serializers/serializer';
|
|||
* A container for message extracted from the templates.
|
||||
*/
|
||||
export class MessageBundle {
|
||||
private _messageMap: {[id: string]: Message} = {};
|
||||
private _messages: Message[] = [];
|
||||
|
||||
constructor(
|
||||
private _htmlParser: HtmlParser, private _implicitTags: string[],
|
||||
|
@ -40,11 +39,10 @@ export class MessageBundle {
|
|||
return i18nParserResult.errors;
|
||||
}
|
||||
|
||||
i18nParserResult.messages.forEach(
|
||||
(message) => { this._messageMap[digestMessage(message)] = message; });
|
||||
this._messages.push(...i18nParserResult.messages);
|
||||
}
|
||||
|
||||
getMessageMap(): {[id: string]: Message} { return this._messageMap; }
|
||||
getMessages(): Message[] { return this._messages; }
|
||||
|
||||
write(serializer: Serializer): string { return serializer.write(this._messageMap); }
|
||||
write(serializer: Serializer): string { return serializer.write(this._messages); }
|
||||
}
|
||||
|
|
|
@ -40,7 +40,9 @@ const TAG_TO_PLACEHOLDER_NAMES: {[k: string]: string} = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Creates unique names for placeholder with different content
|
||||
* Creates unique names for placeholder with different content.
|
||||
*
|
||||
* Returns the same placeholder name when the content is identical.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
@ -105,18 +107,8 @@ export class PlaceholderRegistry {
|
|||
private _hashClosingTag(tag: string): string { return this._hashTag(`/${tag}`, {}, false); }
|
||||
|
||||
private _generateUniqueName(base: string): string {
|
||||
let name = base;
|
||||
let next = this._placeHolderNameCounts[name];
|
||||
|
||||
if (!next) {
|
||||
next = 1;
|
||||
} else {
|
||||
name += `_${next}`;
|
||||
next++;
|
||||
}
|
||||
|
||||
this._placeHolderNameCounts[base] = next;
|
||||
|
||||
return name;
|
||||
const next = this._placeHolderNameCounts[base];
|
||||
this._placeHolderNameCounts[base] = next ? next + 1 : 1;
|
||||
return next ? `${base}_${next}` : base;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,31 +11,29 @@ import * as i18n from '../i18n_ast';
|
|||
import {MessageBundle} from '../message_bundle';
|
||||
|
||||
export interface Serializer {
|
||||
write(messageMap: {[id: string]: i18n.Message}): string;
|
||||
write(messages: i18n.Message[]): string;
|
||||
|
||||
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]};
|
||||
|
||||
digest(message: i18n.Message): string;
|
||||
}
|
||||
|
||||
// Generate a map of placeholder to content indexed by message ids
|
||||
export function extractPlaceholders(messageBundle: MessageBundle) {
|
||||
const messageMap = messageBundle.getMessageMap();
|
||||
const placeholders: {[id: string]: {[name: string]: string}} = {};
|
||||
export function extractPlaceholders(messageMap: {[msgKey: string]: i18n.Message}) {
|
||||
const phByMsgId: {[msgId: string]: {[name: string]: string}} = {};
|
||||
|
||||
Object.keys(messageMap).forEach(msgId => {
|
||||
placeholders[msgId] = messageMap[msgId].placeholders;
|
||||
});
|
||||
Object.keys(messageMap).forEach(msgId => { phByMsgId[msgId] = messageMap[msgId].placeholders; });
|
||||
|
||||
return placeholders;
|
||||
return phByMsgId;
|
||||
}
|
||||
|
||||
// Generate a map of placeholder to message ids indexed by message ids
|
||||
export function extractPlaceholderToIds(messageBundle: MessageBundle) {
|
||||
const messageMap = messageBundle.getMessageMap();
|
||||
const placeholderToIds: {[id: string]: {[name: string]: string}} = {};
|
||||
export function extractPlaceholderToMessage(messageMap: {[msgKey: string]: i18n.Message}) {
|
||||
const phToMsgByMsgId: {[msgId: string]: {[name: string]: i18n.Message}} = {};
|
||||
|
||||
Object.keys(messageMap).forEach(msgId => {
|
||||
placeholderToIds[msgId] = messageMap[msgId].placeholderToMsgIds;
|
||||
phToMsgByMsgId[msgId] = messageMap[msgId].placeholderToMessage;
|
||||
});
|
||||
|
||||
return placeholderToIds;
|
||||
return phToMsgByMsgId;
|
||||
}
|
|
@ -12,11 +12,12 @@ import {HtmlParser} from '../../ml_parser/html_parser';
|
|||
import {InterpolationConfig} from '../../ml_parser/interpolation_config';
|
||||
import {XmlParser} from '../../ml_parser/xml_parser';
|
||||
import {ParseError} from '../../parse_util';
|
||||
import {digest} from '../digest';
|
||||
import * as i18n from '../i18n_ast';
|
||||
import {MessageBundle} from '../message_bundle';
|
||||
import {I18nError} from '../parse_util';
|
||||
|
||||
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
|
||||
import {Serializer, extractPlaceholderToMessage, extractPlaceholders} from './serializer';
|
||||
import * as xml from './xml_helper';
|
||||
|
||||
const _VERSION = '1.2';
|
||||
|
@ -33,15 +34,14 @@ const _UNIT_TAG = 'trans-unit';
|
|||
export class Xliff implements Serializer {
|
||||
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
write(messageMap: {[id: string]: i18n.Message}): string {
|
||||
write(messages: i18n.Message[]): string {
|
||||
const visitor = new _WriteVisitor();
|
||||
|
||||
const transUnits: xml.Node[] = [];
|
||||
|
||||
Object.keys(messageMap).forEach((id) => {
|
||||
const message = messageMap[id];
|
||||
messages.forEach(message => {
|
||||
|
||||
const transUnit = new xml.Tag(_UNIT_TAG, {id: id, datatype: 'html'});
|
||||
const transUnit = new xml.Tag(_UNIT_TAG, {id: this.digest(message), datatype: 'html'});
|
||||
transUnit.children.push(
|
||||
new xml.CR(8), new xml.Tag(_SOURCE_TAG, {}, visitor.serialize(message.nodes)),
|
||||
new xml.CR(8), new xml.Tag(_TARGET_TAG));
|
||||
|
@ -85,7 +85,7 @@ export class Xliff implements Serializer {
|
|||
}
|
||||
|
||||
// Replace the placeholders, messages are now string
|
||||
const {messages, errors} = new _LoadVisitor().parse(result.rootNodes, messageBundle);
|
||||
const {messages, errors} = new _LoadVisitor(this).parse(result.rootNodes, messageBundle);
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
||||
|
@ -108,6 +108,8 @@ export class Xliff implements Serializer {
|
|||
|
||||
return messageMap;
|
||||
}
|
||||
|
||||
digest(message: i18n.Message): string { return digest(message); }
|
||||
}
|
||||
|
||||
class _WriteVisitor implements i18n.Visitor {
|
||||
|
@ -174,8 +176,10 @@ class _LoadVisitor implements ml.Visitor {
|
|||
private _msgId: string;
|
||||
private _target: ml.Node[];
|
||||
private _errors: I18nError[];
|
||||
private _placeholders: {[name: string]: string};
|
||||
private _placeholderToIds: {[name: string]: string};
|
||||
private _placeholders: {[phName: string]: string};
|
||||
private _placeholderToMessage: {[phName: string]: i18n.Message};
|
||||
|
||||
constructor(private _serializer: Serializer) {}
|
||||
|
||||
parse(nodes: ml.Node[], messageBundle: MessageBundle):
|
||||
{messages: {[k: string]: string}, errors: I18nError[]} {
|
||||
|
@ -188,9 +192,10 @@ class _LoadVisitor implements ml.Visitor {
|
|||
// Find all messages
|
||||
ml.visitAll(this, nodes, null);
|
||||
|
||||
const messageMap = messageBundle.getMessageMap();
|
||||
const placeholders = extractPlaceholders(messageBundle);
|
||||
const placeholderToIds = extractPlaceholderToIds(messageBundle);
|
||||
const messageMap: {[msgId: string]: i18n.Message} = {};
|
||||
messageBundle.getMessages().forEach(m => messageMap[this._serializer.digest(m)] = m);
|
||||
const placeholdersByMsgId = extractPlaceholders(messageMap);
|
||||
const placeholderToMessageByMsgId = extractPlaceholderToMessage(messageMap);
|
||||
|
||||
this._messageNodes
|
||||
.filter(message => {
|
||||
|
@ -198,26 +203,26 @@ class _LoadVisitor implements ml.Visitor {
|
|||
return messageMap.hasOwnProperty(message[0]);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Because there could be no ICU placeholders inside an ICU message,
|
||||
// Because there could be no ICU placeholdersByMsgId 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) {
|
||||
if (Object.keys(messageMap[a[0]].placeholderToMessage).length == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
|
||||
if (Object.keys(messageMap[b[0]].placeholderToMessage).length == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.forEach(message => {
|
||||
const id = message[0];
|
||||
this._placeholders = placeholders[id] || {};
|
||||
this._placeholderToIds = placeholderToIds[id] || {};
|
||||
const msgId = message[0];
|
||||
this._placeholders = placeholdersByMsgId[msgId] || {};
|
||||
this._placeholderToMessage = placeholderToMessageByMsgId[msgId] || {};
|
||||
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
|
||||
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
|
||||
this._translatedMessages[msgId] = ml.visitAll(this, message[1]).join('');
|
||||
});
|
||||
|
||||
return {messages: this._translatedMessages, errors: this._errors};
|
||||
|
@ -252,17 +257,20 @@ class _LoadVisitor implements ml.Visitor {
|
|||
if (!idAttr) {
|
||||
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "id" attribute`);
|
||||
} else {
|
||||
const id = idAttr.value;
|
||||
if (this._placeholders.hasOwnProperty(id)) {
|
||||
return this._placeholders[id];
|
||||
const phName = idAttr.value;
|
||||
if (this._placeholders.hasOwnProperty(phName)) {
|
||||
return this._placeholders[phName];
|
||||
}
|
||||
if (this._placeholderToIds.hasOwnProperty(id) &&
|
||||
this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])) {
|
||||
return this._translatedMessages[this._placeholderToIds[id]];
|
||||
if (this._placeholderToMessage.hasOwnProperty(phName)) {
|
||||
const refMsgId = this._serializer.digest(this._placeholderToMessage[phName]);
|
||||
if (this._translatedMessages.hasOwnProperty(refMsgId)) {
|
||||
return this._translatedMessages[refMsgId];
|
||||
}
|
||||
}
|
||||
// TODO(vicb): better error message for when
|
||||
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[id])
|
||||
this._addError(element, `The placeholder "${id}" does not exists in the source message`);
|
||||
this._addError(
|
||||
element, `The placeholder "${phName}" does not exists in the source message`);
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import {ListWrapper} from '../../facade/collection';
|
||||
import * as html from '../../ml_parser/ast';
|
||||
import {decimalDigest} from '../digest';
|
||||
import * as i18n from '../i18n_ast';
|
||||
import {MessageBundle} from '../message_bundle';
|
||||
|
||||
|
@ -40,13 +41,12 @@ const _DOCTYPE = `<!ELEMENT messagebundle (msg)*>
|
|||
<!ELEMENT ex (#PCDATA)>`;
|
||||
|
||||
export class Xmb implements Serializer {
|
||||
write(messageMap: {[k: string]: i18n.Message}): string {
|
||||
write(messages: i18n.Message[]): string {
|
||||
const visitor = new _Visitor();
|
||||
const rootNode = new xml.Tag(_MESSAGES_TAG);
|
||||
|
||||
Object.keys(messageMap).forEach((id) => {
|
||||
const message = messageMap[id];
|
||||
const attrs: {[k: string]: string} = {id};
|
||||
messages.forEach(message => {
|
||||
const attrs: {[k: string]: string} = {id: this.digest(message)};
|
||||
|
||||
if (message.description) {
|
||||
attrs['desc'] = message.description;
|
||||
|
@ -75,6 +75,8 @@ export class Xmb implements Serializer {
|
|||
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: html.Node[]} {
|
||||
throw new Error('Unsupported');
|
||||
}
|
||||
|
||||
digest(message: i18n.Message): string { return digest(message); }
|
||||
}
|
||||
|
||||
class _Visitor implements i18n.Visitor {
|
||||
|
@ -124,3 +126,7 @@ class _Visitor implements i18n.Visitor {
|
|||
return ListWrapper.flatten(nodes.map(node => node.visit(this)));
|
||||
}
|
||||
}
|
||||
|
||||
export function digest(message: i18n.Message): string {
|
||||
return decimalDigest(message);
|
||||
}
|
|
@ -15,7 +15,8 @@ import * as i18n from '../i18n_ast';
|
|||
import {MessageBundle} from '../message_bundle';
|
||||
import {I18nError} from '../parse_util';
|
||||
|
||||
import {Serializer, extractPlaceholderToIds, extractPlaceholders} from './serializer';
|
||||
import {Serializer, extractPlaceholderToMessage, extractPlaceholders} from './serializer';
|
||||
import {digest} from './xmb';
|
||||
|
||||
const _TRANSLATIONS_TAG = 'translationbundle';
|
||||
const _TRANSLATION_TAG = 'translation';
|
||||
|
@ -24,7 +25,7 @@ const _PLACEHOLDER_TAG = 'ph';
|
|||
export class Xtb implements Serializer {
|
||||
constructor(private _htmlParser: HtmlParser, private _interpolationConfig: InterpolationConfig) {}
|
||||
|
||||
write(messageMap: {[id: string]: i18n.Message}): string { throw new Error('Unsupported'); }
|
||||
write(messages: i18n.Message[]): string { throw new Error('Unsupported'); }
|
||||
|
||||
load(content: string, url: string, messageBundle: MessageBundle): {[id: string]: ml.Node[]} {
|
||||
// Parse the xtb file into xml nodes
|
||||
|
@ -35,7 +36,7 @@ export class Xtb implements Serializer {
|
|||
}
|
||||
|
||||
// Replace the placeholders, messages are now string
|
||||
const {messages, errors} = new _Visitor().parse(result.rootNodes, messageBundle);
|
||||
const {messages, errors} = new _Visitor(this).parse(result.rootNodes, messageBundle);
|
||||
|
||||
if (errors.length) {
|
||||
throw new Error(`xtb parse errors:\n${errors.join('\n')}`);
|
||||
|
@ -46,10 +47,10 @@ export class Xtb implements Serializer {
|
|||
const messageMap: {[id: string]: ml.Node[]} = {};
|
||||
const parseErrors: ParseError[] = [];
|
||||
|
||||
Object.keys(messages).forEach((id) => {
|
||||
const res = this._htmlParser.parse(messages[id], url, true, this._interpolationConfig);
|
||||
Object.keys(messages).forEach((msgId) => {
|
||||
const res = this._htmlParser.parse(messages[msgId], url, true, this._interpolationConfig);
|
||||
parseErrors.push(...res.errors);
|
||||
messageMap[id] = res.rootNodes;
|
||||
messageMap[msgId] = res.rootNodes;
|
||||
});
|
||||
|
||||
if (parseErrors.length) {
|
||||
|
@ -58,6 +59,11 @@ export class Xtb implements Serializer {
|
|||
|
||||
return messageMap;
|
||||
}
|
||||
|
||||
digest(message: i18n.Message): string {
|
||||
// we must use the same digest as xmb
|
||||
return digest(message);
|
||||
}
|
||||
}
|
||||
|
||||
class _Visitor implements ml.Visitor {
|
||||
|
@ -66,11 +72,14 @@ class _Visitor implements ml.Visitor {
|
|||
private _bundleDepth: number;
|
||||
private _translationDepth: number;
|
||||
private _errors: I18nError[];
|
||||
private _placeholders: {[name: string]: string};
|
||||
private _placeholderToIds: {[name: string]: string};
|
||||
private _placeholders: {[phName: string]: string};
|
||||
private _placeholderToMessage: {[phName: string]: i18n.Message};
|
||||
|
||||
constructor(private _serializer: Serializer) {}
|
||||
|
||||
parse(nodes: ml.Node[], messageBundle: MessageBundle):
|
||||
{messages: {[k: string]: string}, errors: I18nError[]} {
|
||||
// Tuple [<message id>, [ml nodes]]
|
||||
this._messageNodes = [];
|
||||
this._translatedMessages = {};
|
||||
this._bundleDepth = 0;
|
||||
|
@ -80,9 +89,10 @@ class _Visitor implements ml.Visitor {
|
|||
// Find all messages
|
||||
ml.visitAll(this, nodes, null);
|
||||
|
||||
const messageMap = messageBundle.getMessageMap();
|
||||
const placeholders = extractPlaceholders(messageBundle);
|
||||
const placeholderToIds = extractPlaceholderToIds(messageBundle);
|
||||
const messageMap: {[msgId: string]: i18n.Message} = {};
|
||||
messageBundle.getMessages().forEach(m => messageMap[this._serializer.digest(m)] = m);
|
||||
const placeholdersByMsgId = extractPlaceholders(messageMap);
|
||||
const placeholderToMessageByMsgId = extractPlaceholderToMessage(messageMap);
|
||||
|
||||
this._messageNodes
|
||||
.filter(message => {
|
||||
|
@ -94,22 +104,23 @@ class _Visitor implements ml.Visitor {
|
|||
// 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) {
|
||||
if (Object.keys(messageMap[a[0]].placeholderToMessage).length == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Object.keys(messageMap[b[0]].placeholderToMsgIds).length == 0) {
|
||||
if (Object.keys(messageMap[b[0]].placeholderToMessage).length == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})
|
||||
.forEach(message => {
|
||||
const id = message[0];
|
||||
this._placeholders = placeholders[id] || {};
|
||||
this._placeholderToIds = placeholderToIds[id] || {};
|
||||
const msgId = message[0];
|
||||
this._placeholders = placeholdersByMsgId[msgId] || {};
|
||||
this._placeholderToMessage = placeholderToMessageByMsgId[msgId] || {};
|
||||
|
||||
// TODO(vicb): make sure there is no `_TRANSLATIONS_TAG` nor `_TRANSLATION_TAG`
|
||||
this._translatedMessages[id] = ml.visitAll(this, message[1]).join('');
|
||||
this._translatedMessages[msgId] = ml.visitAll(this, message[1]).join('');
|
||||
});
|
||||
|
||||
return {messages: this._translatedMessages, errors: this._errors};
|
||||
|
@ -149,18 +160,20 @@ class _Visitor implements ml.Visitor {
|
|||
if (!nameAttr) {
|
||||
this._addError(element, `<${_PLACEHOLDER_TAG}> misses the "name" attribute`);
|
||||
} else {
|
||||
const name = nameAttr.value;
|
||||
if (this._placeholders.hasOwnProperty(name)) {
|
||||
return this._placeholders[name];
|
||||
const phName = nameAttr.value;
|
||||
if (this._placeholders.hasOwnProperty(phName)) {
|
||||
return this._placeholders[phName];
|
||||
}
|
||||
if (this._placeholderToIds.hasOwnProperty(name) &&
|
||||
this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])) {
|
||||
return this._translatedMessages[this._placeholderToIds[name]];
|
||||
if (this._placeholderToMessage.hasOwnProperty(phName)) {
|
||||
const refMessageId = this._serializer.digest(this._placeholderToMessage[phName]);
|
||||
if (this._translatedMessages.hasOwnProperty(refMessageId)) {
|
||||
return this._translatedMessages[refMessageId];
|
||||
}
|
||||
}
|
||||
// TODO(vicb): better error message for when
|
||||
// !this._translatedMessages.hasOwnProperty(this._placeholderToIds[name])
|
||||
// !this._translatedMessages.hasOwnProperty(refMessageId)
|
||||
this._addError(
|
||||
element, `The placeholder "${name}" does not exists in the source message`);
|
||||
element, `The placeholder "${phName}" does not exists in the source message`);
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -8,21 +8,26 @@
|
|||
|
||||
import * as html from '../ml_parser/ast';
|
||||
|
||||
import {Message} from './i18n_ast';
|
||||
import {MessageBundle} from './message_bundle';
|
||||
import {Serializer} from './serializers/serializer';
|
||||
|
||||
|
||||
/**
|
||||
* A container for translated messages
|
||||
*/
|
||||
export class TranslationBundle {
|
||||
constructor(private _messageMap: {[id: string]: html.Node[]} = {}) {}
|
||||
constructor(
|
||||
private _messageMap: {[id: string]: html.Node[]} = {},
|
||||
public digest: (m: Message) => string) {}
|
||||
|
||||
static load(content: string, url: string, messageBundle: MessageBundle, serializer: Serializer):
|
||||
TranslationBundle {
|
||||
return new TranslationBundle(serializer.load(content, url, messageBundle));
|
||||
return new TranslationBundle(
|
||||
serializer.load(content, url, messageBundle), (m: Message) => serializer.digest(m));
|
||||
}
|
||||
|
||||
get(id: string): html.Node[] { return this._messageMap[id]; }
|
||||
get(message: Message): html.Node[] { return this._messageMap[this.digest(message)]; }
|
||||
|
||||
has(id: string): boolean { return id in this._messageMap; }
|
||||
has(message: Message): boolean { return this.digest(message) in this._messageMap; }
|
||||
}
|
|
@ -6,15 +6,14 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, HtmlParser} from '@angular/compiler';
|
||||
import {describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
||||
import {digestMessage, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
|
||||
import {digest, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
|
||||
import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
|
||||
import * as i18n from '../../src/i18n/i18n_ast';
|
||||
import {TranslationBundle} from '../../src/i18n/translation_bundle';
|
||||
import * as html from '../../src/ml_parser/ast';
|
||||
import {HtmlParser} from '../../src/ml_parser/html_parser';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG} from '../../src/ml_parser/interpolation_config';
|
||||
import {serializeNodes as serializeHtmlNodes} from '../ml_parser/ast_serializer_spec';
|
||||
|
||||
export function main() {
|
||||
|
@ -403,12 +402,12 @@ function fakeTranslate(
|
|||
const i18nMsgMap: {[id: string]: html.Node[]} = {};
|
||||
|
||||
messages.forEach(message => {
|
||||
const id = digestMessage(message);
|
||||
const id = digest(message);
|
||||
const text = serializeI18nNodes(message.nodes).join('');
|
||||
i18nMsgMap[id] = [new html.Text(`**${text}**`, null)];
|
||||
});
|
||||
|
||||
const translations = new TranslationBundle(i18nMsgMap);
|
||||
const translations = new TranslationBundle(i18nMsgMap, digest);
|
||||
|
||||
const translatedNodes =
|
||||
mergeTranslations(
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {digest} from '@angular/compiler/src/i18n/digest';
|
||||
import {extractMessages} from '@angular/compiler/src/i18n/extractor_merger';
|
||||
import {Message} from '@angular/compiler/src/i18n/i18n_ast';
|
||||
import {describe, expect, it} from '@angular/core/testing/testing_internal';
|
||||
|
@ -276,7 +277,7 @@ export function main() {
|
|||
// As such they have no static content but refs to message ids.
|
||||
expect(_humanizePlaceholders(html)).toEqual(['', '', '', '']);
|
||||
|
||||
expect(_humanizePlaceholdersToIds(html)).toEqual([
|
||||
expect(_humanizePlaceholdersToMessage(html)).toEqual([
|
||||
'ICU=f0f76923009914f1b05f41042a5c7231b9496504, ICU_1=73693d1f78d0fc882f0bcbce4cb31a0aa1995cfe',
|
||||
'',
|
||||
'',
|
||||
|
@ -308,13 +309,13 @@ function _humanizePlaceholders(
|
|||
// clang-format on
|
||||
}
|
||||
|
||||
function _humanizePlaceholdersToIds(
|
||||
function _humanizePlaceholdersToMessage(
|
||||
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(', '));
|
||||
msg => Object.keys(msg.placeholderToMessage).map(k => `${k}=${digest(msg.placeholderToMessage[k])}`).join(', '));
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
|
|
@ -153,51 +153,52 @@ class FrLocalization extends NgLocalization {
|
|||
|
||||
const XTB = `
|
||||
<translationbundle>
|
||||
<translation id="3cb04208df1c2f62553ed48e75939cf7107f9dad">attributs i18n sur les balises</translation>
|
||||
<translation id="52895b1221effb3f3585b689f049d2784d714952">imbriqué</translation>
|
||||
<translation id="88d5f22050a9df477ee5646153558b3a4862d47e">imbriqué</translation>
|
||||
<translation id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
|
||||
<translation id="1fe4616cce80a57c7707bac1c97054aa8e244a67">sur des balises non traductibles</translation>
|
||||
<translation id="67162b5af5f15fd0eb6480c88688dafdf952b93a">sur des balises traductibles</translation>
|
||||
<translation id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
|
||||
<translation id="018efa03821ca41e27611e4a584736810d56ed8a"><ph name="ICU"/></translation>
|
||||
<translation id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {homme} f {femme}}</translation>
|
||||
<translation id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></translation>
|
||||
<translation id="50dac33dc6fc0578884baac79d875785ed77c928">sexe = <ph name="INTERPOLATION"/></translation>
|
||||
<translation id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></translation>
|
||||
<translation id="2ec983b4893bcd5b24af33bebe3ecba63868453c">dans une section traductible</translation>
|
||||
<translation id="eee74a5be8a75881a4785905bd8302a71f7d9f75">
|
||||
<translation id="7613717798286137988">attributs i18n sur les balises</translation>
|
||||
<translation id="496143996034957490">imbriqué</translation>
|
||||
<translation id="4275167479475215567">imbriqué</translation>
|
||||
<translation id="7210334813789040330"><ph name="START_ITALIC_TEXT"/>avec des espaces réservés<ph name="CLOSE_ITALIC_TEXT"/></translation>
|
||||
<translation id="4769680004784140786">sur des balises non traductibles</translation>
|
||||
<translation id="4033143013932333681">sur des balises traductibles</translation>
|
||||
<translation id="6304278477201429103">{count, plural, =0 {zero} =1 {un} =2 {deux} other {<ph name="START_BOLD_TEXT"/>beaucoup<ph name="CLOSE_BOLD_TEXT"/>}}</translation>
|
||||
<translation id="7235359853951837339"><ph name="ICU"/></translation>
|
||||
<translation id="6141976475800220872">{sex, sex, m {homme} f {femme}}</translation>
|
||||
<translation id="5917557396782931034"><ph name="INTERPOLATION"/></translation>
|
||||
<translation id="4687596778889597732">sexe = <ph name="INTERPOLATION"/></translation>
|
||||
<translation id="2505882222003102347"><ph name="CUSTOM_NAME"/></translation>
|
||||
<translation id="5340176214595489533">dans une section traductible</translation>
|
||||
<translation id="8173674801943621225">
|
||||
<ph name="START_HEADING_LEVEL1"/>Balises dans les commentaires html<ph name="CLOSE_HEADING_LEVEL1"/>
|
||||
<ph name="START_TAG_DIV"/><ph name="CLOSE_TAG_DIV"/>
|
||||
<ph name="START_TAG_DIV_1"/><ph name="ICU"/><ph name="CLOSE_TAG_DIV"></ph>
|
||||
</translation>
|
||||
<translation id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
|
||||
<translation id="1309478472899123444">ca <ph name="START_BOLD_TEXT"/>devrait<ph name="CLOSE_BOLD_TEXT"/> marcher</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
// unused, for reference only
|
||||
// can be generated from xmb_spec as follow:
|
||||
// `iit('extract xmb', () => { console.log(toXmb(HTML)); });`
|
||||
// `fit('extract xmb', () => { console.log(toXmb(HTML)); });`
|
||||
const XMB = `
|
||||
<messagebundle>
|
||||
<msg id="3cb04208df1c2f62553ed48e75939cf7107f9dad">i18n attribute on tags</msg>
|
||||
<msg id="52895b1221effb3f3585b689f049d2784d714952">nested</msg>
|
||||
<msg id="88d5f22050a9df477ee5646153558b3a4862d47e" meaning="different meaning">nested</msg>
|
||||
<msg id="34fec9cc62e28e8aa6ffb306fa8569ef0a8087fe"><ph name="START_ITALIC_TEXT"><ex><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></ex></ph></msg>
|
||||
<msg id="1fe4616cce80a57c7707bac1c97054aa8e244a67">on not translatable node</msg>
|
||||
<msg id="67162b5af5f15fd0eb6480c88688dafdf952b93a">on translatable node</msg>
|
||||
<msg id="dc5536bb9e0e07291c185a0d306601a2ecd4813f">{count, plural, =0 {zero}=1 {one}=2 {two}other {<ph name="START_BOLD_TEXT"><ex><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph>}}</msg>
|
||||
<msg id="018efa03821ca41e27611e4a584736810d56ed8a">
|
||||
<msg id="7613717798286137988">i18n attribute on tags</msg>
|
||||
<msg id="496143996034957490">nested</msg>
|
||||
<msg id="4275167479475215567" meaning="different meaning">nested</msg>
|
||||
<msg id="7210334813789040330"><ph name="START_ITALIC_TEXT"><ex><i></ex></ph>with placeholders<ph name="CLOSE_ITALIC_TEXT"><ex></i></ex></ph></msg>
|
||||
<msg id="4769680004784140786">on not translatable node</msg>
|
||||
<msg id="4033143013932333681">on translatable node</msg>
|
||||
<msg id="6304278477201429103">{count, plural, =0 {zero} =1 {one} =2 {two} other {<ph name="START_BOLD_TEXT"><ex><b></ex></ph>many<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph>} }</msg>
|
||||
<msg id="7235359853951837339">
|
||||
<ph name="ICU"/>
|
||||
</msg>
|
||||
<msg id="fd3186ad2a9aa801fe072ddb16ca34cd98ae93da">{sex, sex, m {male}f {female}}</msg>
|
||||
<msg id="d9879678f727b244bc7c7e20f22b63d98cb14890"><ph name="INTERPOLATION"/></msg>
|
||||
<msg id="50dac33dc6fc0578884baac79d875785ed77c928">sex = <ph name="INTERPOLATION"/></msg>
|
||||
<msg id="a46f833b1fe6ca49e8b97c18f4b7ea0b930c9383"><ph name="CUSTOM_NAME"/></msg>
|
||||
<msg id="2ec983b4893bcd5b24af33bebe3ecba63868453c">in a translatable section</msg>
|
||||
<msg id="eee74a5be8a75881a4785905bd8302a71f7d9f75">
|
||||
<msg id="6141976475800220872">{sex, sex, m {male} f {female} }</msg>
|
||||
<msg id="5917557396782931034"><ph name="INTERPOLATION"/></msg>
|
||||
<msg id="4687596778889597732">sex = <ph name="INTERPOLATION"/></msg>
|
||||
<msg id="2505882222003102347"><ph name="CUSTOM_NAME"/></msg>
|
||||
<msg id="5340176214595489533">in a translatable section</msg>
|
||||
<msg id="8173674801943621225">
|
||||
<ph name="START_HEADING_LEVEL1"><ex><h1></ex></ph>Markers in html comments<ph name="CLOSE_HEADING_LEVEL1"><ex></h1></ex></ph>
|
||||
<ph name="START_TAG_DIV"><ex><div></ex></ph><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
<ph name="START_TAG_DIV_1"><ex><div></ex></ph><ph name="ICU"/><ph name="CLOSE_TAG_DIV"><ex></div></ex></ph>
|
||||
</msg>
|
||||
<msg id="93a30c67d4e6c9b37aecfe2ac0f2b5d366d7b520">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
||||
</messagebundle>`;
|
||||
<msg id="1309478472899123444">it <ph name="START_BOLD_TEXT"><ex><b></ex></ph>should<ph name="CLOSE_BOLD_TEXT"><ex></b></ex></ph> work</msg>
|
||||
</messagebundle>
|
||||
`;
|
||||
|
|
|
@ -26,17 +26,18 @@ export function main(): void {
|
|||
messages.updateFromTemplate(
|
||||
'<p i18n="m|d">Translate Me</p>', 'url', DEFAULT_INTERPOLATION_CONFIG);
|
||||
expect(humanizeMessages(messages)).toEqual([
|
||||
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me',
|
||||
'Translate Me (m|d)',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract the same message with different meaning in different entries', () => {
|
||||
it('should extract the all messages and duplicates', () => {
|
||||
messages.updateFromTemplate(
|
||||
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p>', 'url',
|
||||
'<p i18n="m|d">Translate Me</p><p i18n>Translate Me</p><p i18n>Translate Me</p>', 'url',
|
||||
DEFAULT_INTERPOLATION_CONFIG);
|
||||
expect(humanizeMessages(messages)).toEqual([
|
||||
'2e791a68a3324ecdd29e252198638dafacec46e9=Translate Me',
|
||||
'8ca133f957845af1b1868da1b339180d1f519644=Translate Me',
|
||||
'Translate Me (m|d)',
|
||||
'Translate Me (|)',
|
||||
'Translate Me (|)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -44,13 +45,14 @@ export function main(): void {
|
|||
}
|
||||
|
||||
class _TestSerializer implements Serializer {
|
||||
write(messageMap: {[id: string]: i18n.Message}): string {
|
||||
return Object.keys(messageMap)
|
||||
.map(id => `${id}=${serializeNodes(messageMap[id].nodes)}`)
|
||||
write(messages: i18n.Message[]): string {
|
||||
return messages.map(msg => `${serializeNodes(msg.nodes)} (${msg.meaning}|${msg.description})`)
|
||||
.join('//');
|
||||
}
|
||||
|
||||
load(content: string, url: string, placeholders: {}): {} { return null; }
|
||||
|
||||
digest(msg: i18n.Message): string { return 'unused'; }
|
||||
}
|
||||
|
||||
function humanizeMessages(catalog: MessageBundle): string[] {
|
||||
|
|
|
@ -43,10 +43,10 @@ export function main(): void {
|
|||
<!ELEMENT ex (#PCDATA)>
|
||||
]>
|
||||
<messagebundle>
|
||||
<msg id="ec1d033f2436133c14ab038286c4f5df4697484a">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"/></msg>
|
||||
<msg id="e2ccf3d131b15f54aa1fcf1314b1ca77c14bfcc2">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} }</msg>
|
||||
<msg id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23" desc="d" meaning="m">foo</msg>
|
||||
<msg id="0e16a673a5a7a135c9f7b957ec2c5c6f6ee6e2c4">{ count, plural, =0 {{ sex, select, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
<msg id="2348600990161399314">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"/></msg>
|
||||
<msg id="8332678508949127113">{ count, plural, =0 {<ph name="START_PARAGRAPH"><ex><p></ex></ph>test<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} }</msg>
|
||||
<msg id="130772889486467622" desc="d" meaning="m">foo</msg>
|
||||
<msg id="5848862331224404557">{ count, plural, =0 {{ sex, gender, other {<ph name="START_PARAGRAPH"><ex><p></ex></ph>deeply nested<ph name="CLOSE_PARAGRAPH"><ex></p></ex></ph>} } } }</msg>
|
||||
</messagebundle>
|
||||
`;
|
||||
|
||||
|
|
|
@ -50,10 +50,10 @@ export function main(): void {
|
|||
<!ATTLIST ph name CDATA #REQUIRED>
|
||||
]>
|
||||
<translationbundle>
|
||||
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation>
|
||||
<translation id="8841459487341224498">rab</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
|
||||
expect(loadAsText(HTML, XTB)).toEqual({'8841459487341224498': 'rab'});
|
||||
});
|
||||
|
||||
it('should load XTB files without placeholders', () => {
|
||||
|
@ -61,23 +61,22 @@ export function main(): void {
|
|||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226">rab</translation>
|
||||
<translation id="8841459487341224498">rab</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsText(HTML, XTB)).toEqual({'28a86c8a00ae573b2bac698d6609316dc7b4a226': 'rab'});
|
||||
expect(loadAsText(HTML, XTB)).toEqual({'8841459487341224498': 'rab'});
|
||||
});
|
||||
|
||||
|
||||
it('should load XTB files with placeholders', () => {
|
||||
const HTML = `<div i18n><p>bar</p></div>`;
|
||||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
|
||||
<translation id="8877975308926375834"><ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsText(HTML, XTB)).toEqual({
|
||||
'7de4d8ff1e42b7b31da6204074818236a9a5317f': '<p>rab</p>'
|
||||
});
|
||||
expect(loadAsText(HTML, XTB)).toEqual({'8877975308926375834': '<p>rab</p>'});
|
||||
});
|
||||
|
||||
it('should replace ICU placeholders with their translations', () => {
|
||||
|
@ -85,13 +84,13 @@ export function main(): void {
|
|||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<translationbundle>
|
||||
<translation id="eb404e202fed4846e25e7d9ac1fcb719fe4da257">*<ph name="ICU"/>*</translation>
|
||||
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
<translation id="1430521728694081603">*<ph name="ICU"/>*</translation>
|
||||
<translation id="4004755025589356097">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsText(HTML, XTB)).toEqual({
|
||||
'eb404e202fed4846e25e7d9ac1fcb719fe4da257': `*{ count, plural, =1 {<p>rab</p>}}*`,
|
||||
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`,
|
||||
'1430521728694081603': `*{ count, plural, =1 {<p>rab</p>}}*`,
|
||||
'4004755025589356097': `{ count, plural, =1 {<p>rab</p>}}`,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -104,21 +103,19 @@ export function main(): void {
|
|||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<translationbundle>
|
||||
<translation id="7103b4b13b616270a0044efade97d8b4f96f2ca6"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
|
||||
<translation id="fc92b9b781194a02ab773129c8c5a7fc0735efd7">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
<translation id="db3e0a6a5a96481f60aec61d98c3eecddef5ac23">oof</translation>
|
||||
<translation id="8fb569d3dd83e92eff2551b24f5290d3035ce61b">{ count, plural, =1 {{ sex, select, other {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
|
||||
<translation id="8281795707202401639"><ph name="INTERPOLATION"/><ph name="START_BOLD_TEXT"/>rab<ph name="CLOSE_BOLD_TEXT"/> oof</translation>
|
||||
<translation id="4004755025589356097">{ count, plural, =1 {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}}</translation>
|
||||
<translation id="130772889486467622">oof</translation>
|
||||
<translation id="4244993204427636474">{ count, plural, =1 {{ sex, gender, male {<ph name="START_PARAGRAPH"/>rab<ph name="CLOSE_PARAGRAPH"/>}} }}</translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(loadAsText(HTML, XTB)).toEqual({
|
||||
'7103b4b13b616270a0044efade97d8b4f96f2ca6': `{{ a + b }}<b>rab</b> oof`,
|
||||
'fc92b9b781194a02ab773129c8c5a7fc0735efd7': `{ count, plural, =1 {<p>rab</p>}}`,
|
||||
'db3e0a6a5a96481f60aec61d98c3eecddef5ac23': `oof`,
|
||||
'8fb569d3dd83e92eff2551b24f5290d3035ce61b':
|
||||
`{ count, plural, =1 {{ sex, select, other {<p>rab</p>}} }}`,
|
||||
'8281795707202401639': `{{ a + b }}<b>rab</b> oof`,
|
||||
'4004755025589356097': `{ count, plural, =1 {<p>rab</p>}}`,
|
||||
'130772889486467622': `oof`,
|
||||
'4244993204427636474': `{ count, plural, =1 {{ sex, gender, male {<p>rab</p>}} }}`,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('errors', () => {
|
||||
|
@ -145,7 +142,7 @@ export function main(): void {
|
|||
const HTML = '<div i18n>give me a message</div>';
|
||||
|
||||
const XTB = `<translationbundle>
|
||||
<translation id="8de97c6a35252d9409dcaca0b8171c952740b28c"><ph /></translation>
|
||||
<translation id="1186013544048295927"><ph /></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => { loadAsText(HTML, XTB); }).toThrowError(/<ph> misses the "name" attribute/);
|
||||
|
@ -156,7 +153,7 @@ export function main(): void {
|
|||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="28a86c8a00ae573b2bac698d6609316dc7b4a226"><ph name="UNKNOWN"/></translation>
|
||||
<translation id="8841459487341224498"><ph name="UNKNOWN"/></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => {
|
||||
|
@ -170,7 +167,7 @@ export function main(): void {
|
|||
|
||||
const XTB = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<translationbundle>
|
||||
<translation id="7de4d8ff1e42b7b31da6204074818236a9a5317f">rab<ph name="CLOSE_PARAGRAPH"/></translation>
|
||||
<translation id="8877975308926375834">rab<ph name="CLOSE_PARAGRAPH"/></translation>
|
||||
</translationbundle>`;
|
||||
|
||||
expect(() => {
|
||||
|
@ -188,6 +185,6 @@ export function main(): void {
|
|||
});
|
||||
|
||||
it('should throw when trying to save an xtb file',
|
||||
() => { expect(() => { serializer.write({}); }).toThrowError(/Unsupported/); });
|
||||
() => { expect(() => { serializer.write([]); }).toThrowError(/Unsupported/); });
|
||||
});
|
||||
}
|
|
@ -303,6 +303,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
|
|||
importIntoDoc(node: Node): any { return document.importNode(this.templateAwareRoot(node), true); }
|
||||
adoptNode(node: Node): any { return document.adoptNode(node); }
|
||||
getHref(el: Element): string { return (<any>el).href; }
|
||||
|
||||
getEventKey(event: any): string {
|
||||
let key = event.key;
|
||||
if (isBlank(key)) {
|
||||
|
|
Loading…
Reference in New Issue