UNPKG

ng-extract-i18n-merge

Version:

Extract and merge i18n xliff translation files for angular projects.

264 lines (263 loc) 13.3 kB
"use strict"; 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 xmlDeclaration = (_a = xlf2.match(XML_DECLARATION_MATCHER)) === null || _a === void 0 ? void 0 : _a[0]; 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, xmlDeclaration); } exports.fromXlf2 = fromXlf2; function fromXlf1(xlf1, options = { sortNestedTagAttributes: false }) { var _a; const xmlDeclaration = (_a = xlf1.match(XML_DECLARATION_MATCHER)) === null || _a === void 0 ? void 0 : _a[0]; 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'], xmlDeclaration); } 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; 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); } exports.toXlf2 = toXlf2; function toXlf1(translationFile, options) { var _a; 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); } 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) { removeWhitespace(doc); addPrettyWhitespace(doc, 0, options); return doc.toString({ preserveWhitespace: true, compressed: true }); } 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 })); }