ng-extract-i18n-merge
Version:
Extract and merge i18n xliff translation files for angular projects.
200 lines (199 loc) • 13 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const architect_1 = require("@angular-devkit/architect");
const core_1 = require("@angular-devkit/core");
const fs_1 = require("fs");
const fileUtils_1 = require("./fileUtils");
const lexUtils_1 = require("./lexUtils");
const translationFileSerialization_1 = require("./model/translationFileSerialization");
const translationFileModels_1 = require("./model/translationFileModels");
const merger_1 = require("./merger");
const stringUtils_1 = require("./stringUtils");
const buildTargetAttribute_1 = require("./buildTargetAttribute");
const STATE_INITIAL_XLF_2_0 = 'initial';
const STATE_INITIAL_XLF_1_2 = 'new';
const builder = (0, architect_1.createBuilder)(extractI18nMergeBuilder);
exports.default = builder;
/**
* Sorts translation units of `updatedTranslationSourceFile` by the order of their appearance in `originalTranslationSourceFile` (returned as children of `translationUnitsParent`).
* If an id is not found in `originalTranslationSourceFile`, it is returned in `newUnits`.
*/
function resetSortOrderStable(originalTranslationSourceFile, updatedTranslationSourceFile, idMapping) {
var _a;
const originalIdsOrder = (_a = originalTranslationSourceFile === null || originalTranslationSourceFile === void 0 ? void 0 : originalTranslationSourceFile.map(unit => { var _a; return (_a = idMapping[unit.id]) !== null && _a !== void 0 ? _a : unit.id; })) !== null && _a !== void 0 ? _a : [];
const originalIds = new Set(originalIdsOrder);
const result = updatedTranslationSourceFile
.filter(n => originalIds.has(n.id))
.sort((a, b) => {
const indexA = originalIdsOrder.indexOf(a.id);
const indexB = originalIdsOrder.indexOf(b.id);
return indexA - indexB;
});
return {
updatedTranslationSourceDoc: result,
newUnits: updatedTranslationSourceFile.filter(n => !originalIds.has(n.id))
};
}
function resetSortOrderStableAppendNew(originalTranslationSourceFile, updatedTranslationSourceFile, idMapping) {
const resetStable = resetSortOrderStable(originalTranslationSourceFile, updatedTranslationSourceFile, idMapping);
const newUnitsSorted = resetStable.newUnits.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
resetStable.updatedTranslationSourceDoc.push(...newUnitsSorted);
return resetStable.updatedTranslationSourceDoc;
}
function resetSortOrderStableAlphabetNew(originalTranslationSourceFile, updatedTranslationSourceFile, idMapping) {
const resetStable = resetSortOrderStable(originalTranslationSourceFile, updatedTranslationSourceFile, idMapping);
resetStable.newUnits
.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()))
.forEach(newUnit => {
const [index, before] = (0, lexUtils_1.findLexClosestIndex)(newUnit.id.toLowerCase(), resetStable.updatedTranslationSourceDoc, unit => unit.id.toLowerCase());
resetStable.updatedTranslationSourceDoc.splice(index + (before ? 0 : 1), 0, newUnit);
});
return resetStable.updatedTranslationSourceDoc;
}
async function extractI18nMergeBuilder(options, context) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
context.logger.info(`Running ng-extract-i18n-merge for project ${(_a = context.target) === null || _a === void 0 ? void 0 : _a.project}`);
if (!options.verbose) {
console.debug = () => null; // prevent debug output from xml_normalize and xliff-simple-merge
}
context.logger.debug(`options: ${JSON.stringify(options)}`);
const outputPath = options.outputPath || '.';
const format = options.format || 'xlf';
const isXliffV2 = format.includes('2');
const initialTranslationState = isXliffV2 ? STATE_INITIAL_XLF_2_0 : STATE_INITIAL_XLF_1_2;
function fromXlf(input) {
var _a;
const inputOptions = { sortNestedTagAttributes: (_a = options.sortNestedTagAttributes) !== null && _a !== void 0 ? _a : false };
return (input !== undefined && input !== null) ? (isXliffV2 ?
(0, translationFileSerialization_1.fromXlf2)(input, inputOptions) : (0, translationFileSerialization_1.fromXlf1)(input, inputOptions)) : undefined;
}
function toXlf(output) {
var _a;
const outputOptions = { prettyNestedTags: (_a = options.prettyNestedTags) !== null && _a !== void 0 ? _a : true };
return isXliffV2 ? (0, translationFileSerialization_1.toXlf2)(output, outputOptions) : (0, translationFileSerialization_1.toXlf1)(output, outputOptions);
}
function filterUnits(unit) {
var _a, _b;
if ((_a = options.includeIdsWithPrefix) === null || _a === void 0 ? void 0 : _a.length) {
return options.includeIdsWithPrefix.some(includePrefix => unit.id.startsWith(includePrefix));
}
if (options.removeIdsWithPrefix) {
return !((_b = options.removeIdsWithPrefix) === null || _b === void 0 ? void 0 : _b.some(removePrefix => unit.id.startsWith(removePrefix)));
}
return true;
}
context.logger.info('running "extract-i18n" ...');
const sourcePath = (0, core_1.join)((0, core_1.normalize)(outputPath), (_b = options.sourceFile) !== null && _b !== void 0 ? _b : 'messages.xlf');
const translationSourceFileOriginal = fromXlf(await (0, fileUtils_1.readFileIfExists)(sourcePath));
const extractI18nRun = await context.scheduleBuilder((_c = options.builderI18n) !== null && _c !== void 0 ? _c : '@angular-devkit/build-angular:extract-i18n', {
[buildTargetAttribute_1.buildTargetAttribute]: (_d = options.browserTarget) !== null && _d !== void 0 ? _d : options.buildTarget,
outputPath: (0, core_1.dirname)(sourcePath),
outFile: (0, core_1.basename)(sourcePath),
format,
progress: false
}, { target: context.target, logger: context.logger.createChild('extract-i18n') });
const extractI18nResult = await extractI18nRun.result;
if (!extractI18nResult.success) {
return { success: false, error: `"extract-i18n" failed: ${extractI18nResult.error}` };
}
context.logger.info(`extracted translations successfully`);
context.logger.info(`normalize ${sourcePath} ...`);
const translationSourceFile = fromXlf(await fs_1.promises.readFile(sourcePath, 'utf8'));
const sort = (_e = options.sort) !== null && _e !== void 0 ? _e : 'stableAppendNew';
const identityMapper = (x) => x;
const mapper = pipe(((_f = options.collapseWhitespace) !== null && _f !== void 0 ? _f : true) ? stringUtils_1.doCollapseWhitespace : identityMapper, ((_g = options.trim) !== null && _g !== void 0 ? _g : false) ? (text) => text === null || text === void 0 ? void 0 : text.trim() : identityMapper);
const removeContextSource = options.includeContext !== true && options.includeContext !== 'sourceFileOnly';
const normalizedTranslationSourceFile = translationSourceFile.mapUnitsList(units => {
const updatedUnits = units
.filter(filterUnits)
.map(unit => {
var _a, _b;
return ({
...unit,
source: mapper(unit.source),
target: unit.target !== undefined ? mapper(unit.target) : undefined,
locations: removeContextSource ? [] : unit.locations,
description: ((_a = options.includeMeaningAndDescription) !== null && _a !== void 0 ? _a : true) ? mapper(unit.description) : undefined,
meaning: ((_b = options.includeMeaningAndDescription) !== null && _b !== void 0 ? _b : true) ? mapper(unit.meaning) : undefined
});
});
if (sort === 'idAsc') {
return updatedUnits.sort((a, b) => a.id.localeCompare(b.id));
}
return updatedUnits;
});
const merger = new merger_1.Merger(options, normalizedTranslationSourceFile, initialTranslationState);
const targetFilesSourceLangFirst = [
...options.targetFiles.filter(f => f === options.sourceLanguageTargetFile),
...options.targetFiles.filter(f => f !== options.sourceLanguageTargetFile)
];
const idsOfUnitsWithSourceChangedToSourceLangTarget = new Set();
for (const targetFile of targetFilesSourceLangFirst) {
const targetPath = (0, core_1.join)((0, core_1.normalize)(outputPath), targetFile);
context.logger.info(`merge and normalize ${targetPath} ...`);
const translationTargetFileContent = await (0, fileUtils_1.readFileIfExists)(targetPath);
const translationTargetFileRaw = translationTargetFileContent ? fromXlf(translationTargetFileContent) : new translationFileModels_1.TranslationFile([], translationSourceFile.sourceLang, (_j = (_h = targetPath === null || targetPath === void 0 ? void 0 : targetPath.match(/\.([a-zA-Z-]+)\.xlf$/)) === null || _h === void 0 ? void 0 : _h[1]) !== null && _j !== void 0 ? _j : 'en');
const translationTargetFile = translationTargetFileRaw.mapUnitsList(units => units
.filter(filterUnits)
.map(unit => {
var _a, _b;
return ({
...unit,
source: mapper(unit.source),
target: unit.target !== undefined ? mapper(unit.target) : undefined,
meaning: ((_a = options.includeMeaningAndDescription) !== null && _a !== void 0 ? _a : true) ? mapper(unit.meaning) : undefined,
description: ((_b = options.includeMeaningAndDescription) !== null && _b !== void 0 ? _b : true) ? mapper(unit.description) : undefined,
});
}));
const isSourceLang = targetFile === options.sourceLanguageTargetFile;
const mergedTarget = merger.mergeWithMapping(translationTargetFile, isSourceLang);
const normalizedTarget = mergedTarget.mapUnitsList(units => {
const updatedUnits = units
.map(unit => {
var _a, _b;
return ({
...unit,
locations: options.includeContext === true ? unit.locations : [],
// reset to original state, if source was changed to target from sourceLangTarget:
state: idsOfUnitsWithSourceChangedToSourceLangTarget.has(unit.id) ? ((_b = (_a = translationTargetFile.units.find(u => u.id === unit.id)) === null || _a === void 0 ? void 0 : _a.state) !== null && _b !== void 0 ? _b : unit.state) : unit.state
});
});
if (sort === 'idAsc') {
updatedUnits.sort((a, b) => a.id.localeCompare(b.id));
}
else if (sort === 'stableAlphabetNew') {
return resetSortOrderStableAlphabetNew((translationTargetFile === null || translationTargetFile === void 0 ? void 0 : translationTargetFile.units) || null, updatedUnits, merger.idMapping);
}
return updatedUnits;
});
if (isSourceLang) {
normalizedTarget.units
.filter(unit => unit.target !== undefined && unit.target !== unit.source)
.forEach(unit => context.logger.warn(`Found manual changed target with id=${unit.id} in sourceLanguageTargetFile. Consider changing the source code occurrences from "${unit.source}" to "${unit.target}".`));
normalizedTarget.units
.filter(unit => {
const oldUnit = translationTargetFile.units.find(u => u.id === unit.id);
return unit.target !== undefined && unit.target === unit.source && (oldUnit === null || oldUnit === void 0 ? void 0 : oldUnit.source) !== (oldUnit === null || oldUnit === void 0 ? void 0 : oldUnit.target) && (oldUnit === null || oldUnit === void 0 ? void 0 : oldUnit.target) === unit.source;
})
.map(unit => unit.id)
.forEach(id => idsOfUnitsWithSourceChangedToSourceLangTarget.add(id));
}
await fs_1.promises.writeFile(targetPath, toXlf(normalizedTarget));
}
const sortedTranslationSource = normalizedTranslationSourceFile.mapUnitsList(units => {
var _a, _b;
if (sort === 'stableAppendNew') {
return resetSortOrderStableAppendNew((_a = translationSourceFileOriginal === null || translationSourceFileOriginal === void 0 ? void 0 : translationSourceFileOriginal.units) !== null && _a !== void 0 ? _a : null, units, merger.idMapping);
}
else if (sort === 'stableAlphabetNew') {
return resetSortOrderStableAlphabetNew((_b = translationSourceFileOriginal === null || translationSourceFileOriginal === void 0 ? void 0 : translationSourceFileOriginal.units) !== null && _b !== void 0 ? _b : null, units, merger.idMapping);
}
else {
return units;
}
});
await fs_1.promises.writeFile(sourcePath, toXlf(sortedTranslationSource));
context.logger.info('finished i18n merging and normalizing');
return { success: true };
}
const pipe = (...fns) => fns.reduce((prevFn, nextFn) => value => nextFn(prevFn(value)), x => x);
;