ng-extract-i18n-merge
Version:
Extract and merge i18n xliff translation files for angular projects.
271 lines (270 loc) • 13.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.toXlf1 = exports.toXlf2 = exports.fromXlf1 = exports.fromXlf2 = void 0;
const xmldoc_1 = require("xmldoc");
const translationFileModels_1 = require("./translationFileModels");
const XML_DECLARATION_MATCHER = /^<\?xml [^>]*>\s*/i;
const REGULAR_ATTRIBUTES_XLF1 = {
'trans-unit': ['id', 'datatype'],
'source': [],
'target': ['state'],
'note': ['priority', 'from'],
'context': ['context-type'],
'context-group': ['purpose']
};
const REGULAR_ATTRIBUTES_XLF2 = {
'unit': ['id'],
'notes': [],
'note': ['category'],
'segment': ['state'],
'source': [],
'target': []
};
function fromXlf2(xlf2, options = { sortNestedTagAttributes: false }) {
var _a;
const doc = new xmldoc_1.XmlDocument(xlf2);
const file = doc.childNamed('file');
const units = file.children
.filter((n) => n.type === 'element')
.map(unit => {
var _a, _b, _c, _d;
const segment = unit.childNamed('segment');
const notes = unit.childNamed('notes');
const result = {
id: unit.attr.id,
source: toString(options, ...segment.childNamed('source').children),
target: toStringOrUndefined(options, (_a = segment.childNamed('target')) === null || _a === void 0 ? void 0 : _a.children),
state: segment.attr.state,
meaning: toStringOrUndefined(options, (_b = notes === null || notes === void 0 ? void 0 : notes.childWithAttribute('category', 'meaning')) === null || _b === void 0 ? void 0 : _b.children),
description: toStringOrUndefined(options, (_c = notes === null || notes === void 0 ? void 0 : notes.childWithAttribute('category', 'description')) === null || _c === void 0 ? void 0 : _c.children),
locations: (_d = notes === null || notes === void 0 ? void 0 : notes.children.filter((n) => n.type === 'element' && n.attr.category === 'location').map(note => {
const [file, lines] = note.val.split(':', 2);
const [lineStart, lineEnd] = lines.split(',', 2);
return {
file,
lineStart: parseInt(lineStart, 10),
lineEnd: lineEnd !== undefined ? parseInt(lineEnd, 10) : undefined
};
})) !== null && _d !== void 0 ? _d : []
};
const additionalAttributes = getAdditionalAttributes(unit, REGULAR_ATTRIBUTES_XLF2);
if (additionalAttributes.length) {
result.additionalAttributes = additionalAttributes;
}
return result;
});
return new translationFileModels_1.TranslationFile(units, doc.attr.srcLang, doc.attr.trgLang, (_a = xlf2.match(XML_DECLARATION_MATCHER)) === null || _a === void 0 ? void 0 : _a[0], getTrailingWhitespace(xlf2));
}
exports.fromXlf2 = fromXlf2;
function getTrailingWhitespace(xml) {
return xml.substring(xml.lastIndexOf('>') + 1) || undefined;
}
function fromXlf1(xlf1, options = { sortNestedTagAttributes: false }) {
var _a;
const doc = new xmldoc_1.XmlDocument(xlf1);
const file = doc.childNamed('file');
const units = file.childNamed('body').children
.filter((n) => n.type === 'element')
.map(unit => {
var _a, _b, _c;
const notes = unit.childrenNamed('note');
const target = unit.childNamed('target');
const result = {
id: unit.attr.id,
source: toString(options, ...unit.childNamed('source').children),
target: toStringOrUndefined(options, target === null || target === void 0 ? void 0 : target.children),
state: target === null || target === void 0 ? void 0 : target.attr.state,
meaning: toStringOrUndefined(options, (_a = notes === null || notes === void 0 ? void 0 : notes.find(note => note.attr.from === 'meaning')) === null || _a === void 0 ? void 0 : _a.children),
description: toStringOrUndefined(options, (_b = notes === null || notes === void 0 ? void 0 : notes.find(note => note.attr.from === 'description')) === null || _b === void 0 ? void 0 : _b.children),
locations: (_c = unit.childrenNamed('context-group')
.map(contextGroup => ({
file: contextGroup.childWithAttribute('context-type', 'sourcefile').val,
lineStart: parseInt(contextGroup.childWithAttribute('context-type', 'linenumber').val, 10)
}))) !== null && _c !== void 0 ? _c : []
};
const additionalAttributes = getAdditionalAttributes(unit, REGULAR_ATTRIBUTES_XLF1);
if (additionalAttributes.length) {
result.additionalAttributes = additionalAttributes;
}
return result;
});
return new translationFileModels_1.TranslationFile(units, file.attr['source-language'], file.attr['target-language'], (_a = xlf1.match(XML_DECLARATION_MATCHER)) === null || _a === void 0 ? void 0 : _a[0], getTrailingWhitespace(xlf1));
}
exports.fromXlf1 = fromXlf1;
function toString(options, ...nodes) {
return nodes.map(n => {
if (options.sortNestedTagAttributes && n instanceof xmldoc_1.XmlElement) {
const attr = Object.entries(n.attr).sort((a, b) => a[0].localeCompare(b[0]));
n.attr = Object.fromEntries(attr);
}
return n.toString({ preserveWhitespace: true, compressed: true });
}).join('');
}
function toStringOrUndefined(options, nodes) {
return nodes ? toString(options, ...nodes) : undefined;
}
function toXlf2(translationFile, options) {
var _a, _b;
const doc = new xmldoc_1.XmlDocument(`<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="${translationFile.sourceLang}">
<file id="ngi18n" original="ng.template">
</file>
</xliff>`);
if (translationFile.targetLang) {
doc.attr.trgLang = translationFile.targetLang;
}
const file = doc.childNamed('file');
file.children = translationFile.units.map(unit => {
var _a;
const u = new xmldoc_1.XmlDocument(`<unit id=""><segment><source>${unit.source}</source></segment></unit>`);
u.attr.id = unit.id;
const segment = u.childNamed('segment');
if (unit.target !== undefined) {
segment.children.push(new xmldoc_1.XmlDocument(`<target>${unit.target}</target>`));
}
if (unit.state) {
segment.attr.state = unit.state;
}
if (unit.meaning !== undefined || unit.description !== undefined || unit.locations.length) {
const notes = new xmldoc_1.XmlDocument('<notes></notes>');
u.children.splice(0, 0, notes);
notes.children.push(...unit.locations.map(location => new xmldoc_1.XmlDocument(`<note category="location">${location.file}:${location.lineStart}${location.lineEnd ? ',' + location.lineEnd : ''}</note>`)));
if (unit.description !== undefined) {
notes.children.push(new xmldoc_1.XmlDocument(`<note category="description">${unit.description}</note>`));
}
if (unit.meaning !== undefined) {
notes.children.push(new xmldoc_1.XmlDocument(`<note category="meaning">${unit.meaning}</note>`));
}
}
updateFirstAndLastChild(u);
(_a = unit.additionalAttributes) === null || _a === void 0 ? void 0 : _a.forEach(attr => {
(attr.path === '.' ? u : u.descendantWithPath(attr.path)).attr[attr.name] = attr.value;
});
return u;
});
updateFirstAndLastChild(doc);
return ((_a = translationFile.xmlHeader) !== null && _a !== void 0 ? _a : '') + pretty(doc, options) + ((_b = translationFile.trailingWhitespace) !== null && _b !== void 0 ? _b : '');
}
exports.toXlf2 = toXlf2;
function toXlf1(translationFile, options) {
var _a, _b;
const doc = new xmldoc_1.XmlDocument(`<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="${translationFile.sourceLang}" datatype="plaintext" original="ng2.template">
<body></body>
</file>
</xliff>`);
const file = doc.childNamed('file');
if (translationFile.targetLang !== undefined) {
// assure "correct" order:
file.attr = {
'source-language': translationFile.sourceLang,
'target-language': translationFile.targetLang,
datatype: 'plaintext',
original: 'ng2.template'
};
}
const body = file.childNamed('body');
body.children = translationFile.units.map(unit => {
var _a;
const transUnit = new xmldoc_1.XmlDocument(`<trans-unit id="" datatype="html">
<source>${unit.source}</source>
</trans-unit>`);
transUnit.attr.id = unit.id;
if (unit.target !== undefined) {
const target = new xmldoc_1.XmlDocument(`<target>${unit.target}</target>`);
if (unit.state !== undefined) {
target.attr.state = unit.state;
}
transUnit.children.push(target);
}
if (unit.description !== undefined) {
transUnit.children.push(new xmldoc_1.XmlDocument(`<note priority="1" from="description">${unit.description}</note>`));
}
if (unit.meaning !== undefined) {
transUnit.children.push(new xmldoc_1.XmlDocument(`<note priority="1" from="meaning">${unit.meaning}</note>`));
}
if (unit.locations.length) {
transUnit.children.push(...unit.locations.map(location => new xmldoc_1.XmlDocument(`<context-group purpose="location">
<context context-type="sourcefile">${location.file}</context>
<context context-type="linenumber">${location.lineStart}</context>
</context-group>`)));
}
updateFirstAndLastChild(body);
(_a = unit.additionalAttributes) === null || _a === void 0 ? void 0 : _a.forEach(attr => {
(attr.path === '.' ? transUnit : transUnit.descendantWithPath(attr.path)).attr[attr.name] = attr.value;
});
return transUnit;
});
return ((_a = translationFile.xmlHeader) !== null && _a !== void 0 ? _a : '') + pretty(doc, options) + ((_b = translationFile.trailingWhitespace) !== null && _b !== void 0 ? _b : '');
}
exports.toXlf1 = toXlf1;
function updateFirstAndLastChild(u) {
u.firstChild = u.children[0];
u.lastChild = u.children[u.children.length - 1];
}
function isWhiteSpace(node) {
return node.type === 'text' && !!node.text.match(/^\s*$/);
}
function isSourceOrTarget(node) {
return node.name === 'source' || node.name === 'target';
}
/// removes all whitespace text nodes that are not mixed with other nodes. For source/target nodes whitespace is unchanged.
function removeWhitespace(node) {
if (node.type === 'element' && isSourceOrTarget(node)) {
return;
}
if (node.type === 'element' && node.children.every(n => n.type !== 'text' || isWhiteSpace(n))) {
node.children = node.children.filter(c => !isWhiteSpace(c));
updateFirstAndLastChild(node);
}
node.children.filter((n) => n.type === 'element').forEach(e => removeWhitespace(e));
}
/// format with 2 spaces indentation, except for source/target nodes: there nested nodes are assured to keep (non-)whitespaces (potentially collapsed/expanded)
function pretty(doc, options) {
var _a;
removeWhitespace(doc);
addPrettyWhitespace(doc, 0, options);
const s = doc.toString({ preserveWhitespace: true, compressed: true });
return ((_a = options.selfClosingEmptyTargets) !== null && _a !== void 0 ? _a : true) ? s : expandSelfClosingTags(s);
}
// this only addresses 'target' nodes, to avoid breaking nested html tags (<hr/> -> <hr></hr>):
function expandSelfClosingTags(xml) {
return xml.replace(/<(target)([^>]*)\/>/g, '<$1$2></$1>');
}
function indentChildren(doc, indent) {
for (let i = doc.children.length - 1; i >= 0; i--) {
doc.children.splice(i, 0, new xmldoc_1.XmlTextNode('\n' + ' '.repeat(indent + 1)));
}
doc.children.push(new xmldoc_1.XmlTextNode('\n' + ' '.repeat(indent)));
updateFirstAndLastChild(doc);
}
function addPrettyWhitespace(doc, indent, options, sourceOrTarget = false) {
if (isSourceOrTarget(doc) || sourceOrTarget) {
if (options.prettyNestedTags && doc.children.length && doc.children.every(c => isWhiteSpace(c) || c.type === 'element')) {
doc.children = doc.children.filter(c => !isWhiteSpace(c));
updateFirstAndLastChild(doc);
indentChildren(doc, indent);
doc.children.forEach(c => c.type === 'element' ? addPrettyWhitespace(c, indent + 1, options, true) : null);
}
return;
}
if (doc.children.length && doc.children.some(e => e.type === 'element')) {
indentChildren(doc, indent);
doc.children.forEach(c => c.type === 'element' ? addPrettyWhitespace(c, indent + 1, options) : null);
}
}
function allChildrenWithPath(unit, currentPath = '.') {
return unit.children.flatMap(child => {
if (child.type === 'element') {
const path = currentPath === '.' ? child.name : (currentPath + '.' + child.name);
return [{ element: child, path }, ...allChildrenWithPath(child, path)];
}
return [];
});
}
function getAdditionalAttributes(unit, knownAttributes) {
return [{ element: unit, path: '.' }, ...allChildrenWithPath(unit)]
.flatMap(({ element, path }) => Object.entries(element.attr)
.map(([attrName, attrValue]) => ({ element, attrName, attrValue, path })))
.filter(({ element, attrName }) => { var _a; return knownAttributes[element.name] ? !((_a = knownAttributes[element.name]) === null || _a === void 0 ? void 0 : _a.includes(attrName)) : false; })
.map(({ attrName, attrValue, path }) => ({ name: attrName, value: attrValue, path }));
}
;