UNPKG

ng-extract-i18n-merge

Version:

Extract and merge i18n xliff translation files for angular projects.

200 lines (199 loc) 13 kB
"use strict"; 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);