ng-extract-i18n-merge
Version:
Extract and merge i18n xliff translation files for angular projects.
142 lines (141 loc) • 7.07 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.Merger = void 0;
const translationFileModels_1 = require("./model/translationFileModels");
const strings_1 = require("@angular-devkit/core/src/utils/strings");
const stringUtils_1 = require("./stringUtils");
const FUZZY_THRESHOLD = 0.2;
function onlyXmlNodes(source) {
return source.replace(/<[^>]+\/>|<([\w-]+)[^>]*>.*<\/\1>/g, '').trim() === '';
}
class Merger {
constructor(options, normalizedTranslationSourceFile, initialTranslationState) {
this.options = options;
this.normalizedTranslationSourceFile = normalizedTranslationSourceFile;
this.initialTranslationState = initialTranslationState;
this.idMapping = {};
}
mergeWithMapping(destFileContent, isSourceLang) {
var _a, _b, _c;
const inUnitsById = new Map(this.normalizedTranslationSourceFile.units.map(unit => [unit.id, unit]));
const destUnitsById = new Map(destFileContent.units.map(unit => [unit.id, unit]));
const allInUnitsWithoutDestinationUnit = this.normalizedTranslationSourceFile.units.filter(u => !destUnitsById.has(u.id));
const allInUnitsWithDestinationUnit = this.normalizedTranslationSourceFile.units.filter(u => destUnitsById.has(u.id));
// collect (potentially) obsolete units (defer actual removal to allow for fuzzy matching..):
const removeNodes = destFileContent.units.filter(destUnit => !inUnitsById.has(destUnit.id));
const result = new translationFileModels_1.TranslationFile([...destFileContent.units], destFileContent.sourceLang, destFileContent.targetLang, destFileContent.xmlHeader);
for (const unit of allInUnitsWithDestinationUnit) {
result.replaceUnit(unit, this.handle(unit, destUnitsById.get(unit.id), isSourceLang, removeNodes));
}
if ((_a = this.options.fuzzyMatch) !== null && _a !== void 0 ? _a : true) {
const bestMatchesIdToUnits = new Map(allInUnitsWithoutDestinationUnit.map((inUnit) => [inUnit.id, findCloseMatches(inUnit, removeNodes)]));
while (bestMatchesIdToUnits.size) {
const inUnitId = (_b = getMinScoreId(bestMatchesIdToUnits)) !== null && _b !== void 0 ? _b : Array.from(bestMatchesIdToUnits.keys())[0];
const bestMatch = (_c = bestMatchesIdToUnits.get(inUnitId)[0]) === null || _c === void 0 ? void 0 : _c.elem;
const updated = this.handle(inUnitsById.get(inUnitId), bestMatch, isSourceLang, removeNodes);
if (bestMatch) {
result.replaceUnit(bestMatch, updated);
}
else {
result.addUnit(updated);
}
bestMatchesIdToUnits.delete(inUnitId);
if (bestMatch) {
bestMatchesIdToUnits.forEach(x => {
const i = x.findIndex(y => y.elem === bestMatch);
if (i >= 0) {
x.splice(i, 1);
}
});
}
}
}
else {
for (const unit of allInUnitsWithoutDestinationUnit) {
result.addUnit(this.handle(unit, undefined, isSourceLang, removeNodes));
}
}
console.debug(`removing ${removeNodes.length} ids: ${removeNodes.map(n => n.id).join(', ')}`);
return result.mapUnitsList(units => units.filter(unit => !removeNodes.includes(unit)));
}
normalize(source) {
var _a, _b;
if ((_a = this.options.collapseWhitespace) !== null && _a !== void 0 ? _a : true) {
const adjusted = ((_b = this.options.prettyNestedTags) !== null && _b !== void 0 ? _b : true) && onlyXmlNodes(source) ? ` ${source.replace(/></g, '> <')} ` : source;
return (0, stringUtils_1.doCollapseWhitespace)(adjusted);
}
else {
return source;
}
}
/** Syncs `unit` to `destUnit` or adds `unit` as new, if `destUnit` is not given. */
handle(unit, destUnit, isSourceLang, removeNodes) {
var _a;
if (destUnit) {
let updatedDestUnit = destUnit;
if (this.normalize(destUnit.source) !== this.normalize(unit.source)) {
console.debug(`update element with id "${unit.id}" with new source: ${unit.source} (was: ${destUnit.source})`);
const syncSourceLang = isSourceLang && destUnit.source === destUnit.target; // sync source language only if target is unchanged
const syncTarget = syncSourceLang || isUntranslated(destUnit, this.initialTranslationState);
updatedDestUnit = {
...destUnit,
state: isSourceLang ? 'final' : this.initialTranslationState,
source: unit.source,
target: syncTarget && !(destUnit.target === undefined && this.options.newTranslationTargetsBlank === 'omit') ? unit.source : destUnit.target
};
}
if (destUnit.id !== unit.id) {
console.debug(`matched unit with previous id "${destUnit.id}" to new id: "${unit.id}"`);
this.idMapping[destUnit.id] = unit.id;
removeNodes.splice(removeNodes.indexOf(destUnit), 1);
updatedDestUnit = {
...updatedDestUnit,
id: unit.id
};
}
updatedDestUnit = {
...updatedDestUnit,
locations: unit.locations,
meaning: unit.meaning,
description: unit.description
};
return updatedDestUnit;
}
else {
console.debug(`adding element with id "${unit.id}"`);
return {
...unit,
target: this.options.newTranslationTargetsBlank === 'omit' ? undefined : (((_a = this.options.newTranslationTargetsBlank) !== null && _a !== void 0 ? _a : false) && !isSourceLang ? '' : unit.source),
state: isSourceLang ? 'final' : this.initialTranslationState
};
}
}
}
exports.Merger = Merger;
function isUntranslated(destUnit, initialState) {
return destUnit.state === initialState && (destUnit.target === undefined || destUnit.source === destUnit.target);
}
function findCloseMatches(originUnit, destUnits) {
const originText = originUnit.source;
return destUnits
.map(n => ({
elem: n,
score: (0, strings_1.levenshtein)(originText, n.source) / originText.length
}))
.filter(x => x.score < FUZZY_THRESHOLD)
.sort((a, b) => a.score - b.score);
}
function getMinScoreId(bestMatchesIdToUnits) {
let minScoreId;
let minScore = Number.MAX_VALUE;
bestMatchesIdToUnits.forEach((x, id) => {
if (x.length) {
const score = x[0].score;
if (score < minScore) {
minScore = score;
minScoreId = id;
}
}
});
return minScoreId;
}
;