UNPKG

@angular/localize

Version:

Angular - library for localizing messages

362 lines (350 loc) 16.4 kB
#!/usr/bin/env node import {createRequire as __cjsCompatRequire} from 'module'; const require = __cjsCompatRequire(import.meta.url); import { ArbTranslationParser, SimpleJsonTranslationParser, Xliff1TranslationParser, Xliff2TranslationParser, XtbTranslationParser, makeEs2015TranslatePlugin, makeEs5TranslatePlugin, makeLocalePlugin } from "../../chunk-IVRM6V2B.js"; import { Diagnostics } from "../../chunk-HR5KPXEW.js"; // packages/localize/tools/src/translate/cli.ts import { NodeJSFileSystem, setFileSystem } from "@angular/compiler-cli/private/localize"; import { globSync } from "tinyglobby"; import yargs from "yargs"; // packages/localize/tools/src/translate/output_path.js function getOutputPathFn(fs2, outputFolder) { const [pre, post] = outputFolder.split("{{LOCALE}}"); return post === void 0 ? (_locale, relativePath) => fs2.join(pre, relativePath) : (locale, relativePath) => fs2.join(pre + locale + post, relativePath); } // packages/localize/tools/src/translate/index.js import { getFileSystem, relativeFrom } from "@angular/compiler-cli/private/localize"; // packages/localize/tools/src/translate/asset_files/asset_translation_handler.js import { absoluteFrom } from "@angular/compiler-cli/private/localize"; var AssetTranslationHandler = class { fs; constructor(fs2) { this.fs = fs2; } canTranslate(_relativeFilePath, _contents) { return true; } translate(diagnostics2, _sourceRoot, relativeFilePath, contents, outputPathFn2, translations, sourceLocale2) { for (const translation of translations) { this.writeAssetFile(diagnostics2, outputPathFn2, translation.locale, relativeFilePath, contents); } if (sourceLocale2 !== void 0) { this.writeAssetFile(diagnostics2, outputPathFn2, sourceLocale2, relativeFilePath, contents); } } writeAssetFile(diagnostics2, outputPathFn2, locale, relativeFilePath, contents) { try { const outputPath = absoluteFrom(outputPathFn2(locale, relativeFilePath)); this.fs.ensureDir(this.fs.dirname(outputPath)); this.fs.writeFile(outputPath, contents); } catch (e) { diagnostics2.error(e.message); } } }; // packages/localize/tools/src/translate/source_files/source_file_translation_handler.js import { absoluteFrom as absoluteFrom2 } from "@angular/compiler-cli/private/localize"; import babel from "@babel/core"; var SourceFileTranslationHandler = class { fs; translationOptions; sourceLocaleOptions; constructor(fs2, translationOptions = {}) { this.fs = fs2; this.translationOptions = translationOptions; this.sourceLocaleOptions = { ...this.translationOptions, missingTranslation: "ignore" }; } canTranslate(relativeFilePath, _contents) { return this.fs.extname(relativeFilePath) === ".js"; } translate(diagnostics2, sourceRoot, relativeFilePath, contents, outputPathFn2, translations, sourceLocale2) { const sourceCode = Buffer.from(contents).toString("utf8"); if (!sourceCode.includes("$localize")) { for (const translation of translations) { this.writeSourceFile(diagnostics2, outputPathFn2, translation.locale, relativeFilePath, contents); } if (sourceLocale2 !== void 0) { this.writeSourceFile(diagnostics2, outputPathFn2, sourceLocale2, relativeFilePath, contents); } } else { const ast = babel.parseSync(sourceCode, { sourceRoot, filename: relativeFilePath }); if (!ast) { diagnostics2.error(`Unable to parse source file: ${this.fs.join(sourceRoot, relativeFilePath)}`); return; } for (const translationBundle of translations) { this.translateFile(diagnostics2, ast, translationBundle, sourceRoot, relativeFilePath, outputPathFn2, this.translationOptions); } if (sourceLocale2 !== void 0) { this.translateFile(diagnostics2, ast, { locale: sourceLocale2, translations: {} }, sourceRoot, relativeFilePath, outputPathFn2, this.sourceLocaleOptions); } } } translateFile(diagnostics2, ast, translationBundle, sourceRoot, filename, outputPathFn2, options2) { const translated = babel.transformFromAstSync(ast, void 0, { compact: true, generatorOpts: { minified: true }, plugins: [ makeLocalePlugin(translationBundle.locale), makeEs2015TranslatePlugin(diagnostics2, translationBundle.translations, options2, this.fs), makeEs5TranslatePlugin(diagnostics2, translationBundle.translations, options2, this.fs) ], cwd: sourceRoot, filename }); if (translated && translated.code) { this.writeSourceFile(diagnostics2, outputPathFn2, translationBundle.locale, filename, translated.code); const outputPath = absoluteFrom2(outputPathFn2(translationBundle.locale, filename)); this.fs.ensureDir(this.fs.dirname(outputPath)); this.fs.writeFile(outputPath, translated.code); } else { diagnostics2.error(`Unable to translate source file: ${this.fs.join(sourceRoot, filename)}`); return; } } writeSourceFile(diagnostics2, outputPathFn2, locale, relativeFilePath, contents) { try { const outputPath = absoluteFrom2(outputPathFn2(locale, relativeFilePath)); this.fs.ensureDir(this.fs.dirname(outputPath)); this.fs.writeFile(outputPath, contents); } catch (e) { diagnostics2.error(e.message); } } }; // packages/localize/tools/src/translate/translation_files/translation_loader.js var TranslationLoader = class { fs; translationParsers; duplicateTranslation; diagnostics; constructor(fs2, translationParsers, duplicateTranslation2, diagnostics2) { this.fs = fs2; this.translationParsers = translationParsers; this.duplicateTranslation = duplicateTranslation2; this.diagnostics = diagnostics2; } /** * Load and parse the translation files into a collection of `TranslationBundles`. * * @param translationFilePaths An array, per locale, of absolute paths to translation files. * * For each locale to be translated, there is an element in `translationFilePaths`. Each element * is an array of absolute paths to translation files for that locale. * If the array contains more than one translation file, then the translations are merged. * If allowed by the `duplicateTranslation` property, when more than one translation has the same * message id, the message from the earlier translation file in the array is used. * For example, if the files are `[app.xlf, lib-1.xlf, lib-2.xlif]` then a message that appears in * `app.xlf` will override the same message in `lib-1.xlf` or `lib-2.xlf`. * * @param translationFileLocales An array of locales for each of the translation files. * * If there is a locale provided in `translationFileLocales` then this is used rather than a * locale extracted from the file itself. * If there is neither a provided locale nor a locale parsed from the file, then an error is * thrown. * If there are both a provided locale and a locale parsed from the file, and they are not the * same, then a warning is reported. */ loadBundles(translationFilePaths2, translationFileLocales2) { return translationFilePaths2.map((filePaths, index) => { const providedLocale = translationFileLocales2[index]; return this.mergeBundles(filePaths, providedLocale); }); } /** * Load all the translations from the file at the given `filePath`. */ loadBundle(filePath, providedLocale) { const fileContents = this.fs.readFile(filePath); const unusedParsers = /* @__PURE__ */ new Map(); for (const translationParser of this.translationParsers) { const result = translationParser.analyze(filePath, fileContents); if (!result.canParse) { unusedParsers.set(translationParser, result); continue; } const { locale: parsedLocale, translations, diagnostics: diagnostics2 } = translationParser.parse(filePath, fileContents, result.hint); if (diagnostics2.hasErrors) { throw new Error(diagnostics2.formatDiagnostics(`The translation file "${filePath}" could not be parsed.`)); } const locale = providedLocale || parsedLocale; if (locale === void 0) { throw new Error(`The translation file "${filePath}" does not contain a target locale and no explicit locale was provided for this file.`); } if (parsedLocale !== void 0 && providedLocale !== void 0 && parsedLocale !== providedLocale) { diagnostics2.warn(`The provided locale "${providedLocale}" does not match the target locale "${parsedLocale}" found in the translation file "${filePath}".`); } if (this.diagnostics) { this.diagnostics.merge(diagnostics2); } return { locale, translations, diagnostics: diagnostics2 }; } const diagnosticsMessages = []; for (const [parser, result] of unusedParsers.entries()) { diagnosticsMessages.push(result.diagnostics.formatDiagnostics(` ${parser.constructor.name} cannot parse translation file.`)); } throw new Error(`There is no "TranslationParser" that can parse this translation file: ${filePath}.` + diagnosticsMessages.join("\n")); } /** * There is more than one `filePath` for this locale, so load each as a bundle and then merge * them all together. */ mergeBundles(filePaths, providedLocale) { const bundles = filePaths.map((filePath) => this.loadBundle(filePath, providedLocale)); const bundle = bundles[0]; for (let i = 1; i < bundles.length; i++) { const nextBundle = bundles[i]; if (nextBundle.locale !== bundle.locale) { if (this.diagnostics) { const previousFiles = filePaths.slice(0, i).map((f) => `"${f}"`).join(", "); this.diagnostics.warn(`When merging multiple translation files, the target locale "${nextBundle.locale}" found in "${filePaths[i]}" does not match the target locale "${bundle.locale}" found in earlier files [${previousFiles}].`); } } Object.keys(nextBundle.translations).forEach((messageId) => { if (bundle.translations[messageId] !== void 0) { this.diagnostics?.add(this.duplicateTranslation, `Duplicate translations for message "${messageId}" when merging "${filePaths[i]}".`); } else { bundle.translations[messageId] = nextBundle.translations[messageId]; } }); } return bundle; } }; // packages/localize/tools/src/translate/translator.js var Translator = class { fs; resourceHandlers; diagnostics; constructor(fs2, resourceHandlers, diagnostics2) { this.fs = fs2; this.resourceHandlers = resourceHandlers; this.diagnostics = diagnostics2; } translateFiles(inputPaths, rootPath, outputPathFn2, translations, sourceLocale2) { inputPaths.forEach((inputPath) => { const absInputPath = this.fs.resolve(rootPath, inputPath); const contents = this.fs.readFileBuffer(absInputPath); const relativePath = this.fs.relative(rootPath, absInputPath); for (const resourceHandler of this.resourceHandlers) { if (resourceHandler.canTranslate(relativePath, contents)) { return resourceHandler.translate(this.diagnostics, rootPath, relativePath, contents, outputPathFn2, translations, sourceLocale2); } } this.diagnostics.error(`Unable to handle resource file: ${inputPath}`); }); } }; // packages/localize/tools/src/translate/index.js function translateFiles({ sourceRootPath: sourceRootPath2, sourceFilePaths: sourceFilePaths2, translationFilePaths: translationFilePaths2, translationFileLocales: translationFileLocales2, outputPathFn: outputPathFn2, diagnostics: diagnostics2, missingTranslation: missingTranslation2, duplicateTranslation: duplicateTranslation2, sourceLocale: sourceLocale2 }) { const fs2 = getFileSystem(); const translationLoader = new TranslationLoader(fs2, [ new Xliff2TranslationParser(), new Xliff1TranslationParser(), new XtbTranslationParser(), new SimpleJsonTranslationParser(), new ArbTranslationParser() ], duplicateTranslation2, diagnostics2); const resourceProcessor = new Translator(fs2, [new SourceFileTranslationHandler(fs2, { missingTranslation: missingTranslation2 }), new AssetTranslationHandler(fs2)], diagnostics2); const translationFilePathsArrays = translationFilePaths2.map((filePaths) => Array.isArray(filePaths) ? filePaths.map((p) => fs2.resolve(p)) : [fs2.resolve(filePaths)]); const translations = translationLoader.loadBundles(translationFilePathsArrays, translationFileLocales2); sourceRootPath2 = fs2.resolve(sourceRootPath2); resourceProcessor.translateFiles(sourceFilePaths2.map(relativeFrom), fs2.resolve(sourceRootPath2), outputPathFn2, translations, sourceLocale2); } // packages/localize/tools/src/translate/cli.ts process.title = "Angular Localization Message Translator (localize-translate)"; var args = process.argv.slice(2); var options = yargs(args).option("r", { alias: "root", required: true, describe: "The root path of the files to translate, either absolute or relative to the current working directory. E.g. `dist/en`.", type: "string" }).option("s", { alias: "source", required: true, describe: "A glob pattern indicating what files to translate, relative to the `root` path. E.g. `bundles/**/*`.", type: "string" }).option("l", { alias: "source-locale", describe: "The source locale of the application. If this is provided then a copy of the application will be created with no translation but just the `$localize` calls stripped out.", type: "string" }).option("t", { alias: "translations", required: true, array: true, describe: 'A list of paths to the translation files to load, either absolute or relative to the current working directory.\nE.g. `-t src/locale/messages.en.xlf src/locale/messages.fr.xlf src/locale/messages.de.xlf`.\nIf you want to merge multiple translation files for each locale, then provide the list of files in an array.\nNote that the arrays must be in double quotes if you include any whitespace within the array.\nE.g. `-t "[src/locale/messages.en.xlf, src/locale/messages-2.en.xlf]" [src/locale/messages.fr.xlf,src/locale/messages-2.fr.xlf]`', type: "string" }).option("target-locales", { array: true, describe: 'A list of target locales for the translation files, which will override any target locale parsed from the translation file.\nE.g. "-t en fr de".', type: "string" }).option("o", { alias: "outputPath", required: true, describe: "A output path pattern to where the translated files will be written.\nThe path must be either absolute or relative to the current working directory.\nThe marker `{{LOCALE}}` will be replaced with the target locale. E.g. `dist/{{LOCALE}}`.", type: "string" }).option("m", { alias: "missingTranslation", describe: "How to handle missing translations.", choices: ["error", "warning", "ignore"], default: "warning", type: "string" }).option("d", { alias: "duplicateTranslation", describe: "How to handle duplicate translations.", choices: ["error", "warning", "ignore"], default: "warning", type: "string" }).strict().help().parseSync(); var fs = new NodeJSFileSystem(); setFileSystem(fs); var sourceRootPath = options.r; var sourceFilePaths = globSync(options.s, { cwd: sourceRootPath, onlyFiles: true }); var translationFilePaths = convertArraysFromArgs(options.t); var outputPathFn = getOutputPathFn(fs, fs.resolve(options.o)); var diagnostics = new Diagnostics(); var missingTranslation = options.m; var duplicateTranslation = options.d; var sourceLocale = options.l; var translationFileLocales = options["target-locales"] || []; translateFiles({ sourceRootPath, sourceFilePaths, translationFilePaths, translationFileLocales, outputPathFn, diagnostics, missingTranslation, duplicateTranslation, sourceLocale }); diagnostics.messages.forEach((m) => console.warn(`${m.type}: ${m.message}`)); process.exit(diagnostics.hasErrors ? 1 : 0); function convertArraysFromArgs(args2) { return args2.map( (arg) => arg.startsWith("[") && arg.endsWith("]") ? arg.slice(1, -1).split(",").map((arg2) => arg2.trim()) : arg ); } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */