UNPKG

@lingui/cli

Version:

CLI for working wit message catalogs

261 lines (260 loc) 9.93 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Catalog = void 0; exports.cleanObsolete = cleanObsolete; exports.order = order; exports.writeCompiled = writeCompiled; exports.orderByMessage = orderByMessage; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const glob_1 = require("glob"); const normalize_path_1 = __importDefault(require("normalize-path")); const getTranslationsForCatalog_1 = require("./catalog/getTranslationsForCatalog"); const mergeCatalog_1 = require("./catalog/mergeCatalog"); const extractFromFiles_1 = require("./catalog/extractFromFiles"); const utils_1 = require("./utils"); const LOCALE = "{locale}"; const LOCALE_SUFFIX_RE = /\{locale\}.*$/; class Catalog { constructor({ name, path, include, templatePath, format, exclude = [] }, config) { this.config = config; this.name = name; this.path = (0, utils_1.normalizeRelativePath)(path); this.include = include.map(utils_1.normalizeRelativePath); this.exclude = [this.localeDir, ...exclude.map(utils_1.normalizeRelativePath)]; this.format = format; this.templateFile = templatePath || getTemplatePath(this.format.getTemplateExtension(), this.path); } getFilename(locale) { return ((0, utils_1.replacePlaceholders)(this.path, { locale }) + this.format.getCatalogExtension()); } async make(options) { const [nextCatalog, prevCatalogs] = await Promise.all([ this.collect({ files: options.files, workerPool: options.workerPool }), this.readAll(), ]); if (!nextCatalog) return false; const catalogs = this.merge(prevCatalogs, nextCatalog, { overwrite: options.overwrite, files: options.files, }); // Map over all locales and post-process each catalog const sortedCatalogs = Object.fromEntries(Object.entries(catalogs).map(([locale, catalog]) => { if (options.clean) { catalog = cleanObsolete(catalog); } catalog = order(options.orderBy, catalog); return [locale, catalog]; })); const locales = options.locale ? options.locale : this.locales; await Promise.all(locales.map((locale) => this.write(locale, sortedCatalogs[locale]))); return sortedCatalogs; } async makeTemplate(options) { const catalog = await this.collect({ files: options.files, workerPool: options.workerPool, }); if (!catalog) return false; const sorted = order(options.orderBy, catalog); await this.writeTemplate(sorted); return sorted; } /** * Collect messages from source paths. Return a raw message catalog as JSON. */ async collect(options = {}) { let paths = this.sourcePaths; if (options.files) { options.files = options.files.map((p) => (0, utils_1.makePathRegexSafe)((0, normalize_path_1.default)(p, false))); const regex = new RegExp(options.files.join("|"), "i"); paths = paths.filter((path) => regex.test((0, normalize_path_1.default)(path))); } if (options.workerPool) { return await (0, extractFromFiles_1.extractFromFilesWithWorkerPool)(options.workerPool, paths, this.config); } return await (0, extractFromFiles_1.extractFromFiles)(paths, this.config); } /* * * prevCatalogs - map of message catalogs in all available languages with translations * nextCatalog - language-agnostic catalog with collected messages * * Note: if a catalog in prevCatalogs is null it means the language is available, but * no previous catalog was generated (usually first run). * * Orthogonal use-cases * -------------------- * * Message IDs: * - auto-generated IDs: message is used as a key, `defaults` is not set * - custom IDs: message is used as `defaults`, custom ID as a key * * Source locale (defined by `sourceLocale` in config): * - catalog for `sourceLocale`: initially, `translation` is prefilled with `defaults` * (for custom IDs) or `key` (for auto-generated IDs) * - all other languages: translation is kept empty */ merge(prevCatalogs, nextCatalog, options) { return Object.fromEntries(Object.entries(prevCatalogs).map(([locale, prevCatalog]) => [ locale, (0, mergeCatalog_1.mergeCatalog)(prevCatalog, nextCatalog, this.config.sourceLocale === locale, options), ])); } async getTranslations(locale, options) { return await (0, getTranslationsForCatalog_1.getTranslationsForCatalog)(this, locale, options); } async write(locale, messages) { const filename = this.getFilename(locale); const created = !fs_1.default.existsSync(filename); await this.format.write(filename, messages, locale); return [created, filename]; } async writeTemplate(messages) { const filename = this.templateFile; await this.format.write(filename, messages, undefined); } async read(locale) { return await this.format.read(this.getFilename(locale), locale); } async readAll(locales = this.locales) { const res = {}; await Promise.all(locales.map(async (locale) => (res[locale] = await this.read(locale)))); // statement above will save locales in object in undetermined order // resort here to have keys order the same as in locales definition return this.locales.reduce((acc, locale) => { acc[locale] = res[locale]; return acc; }, {}); } async readTemplate() { const filename = this.templateFile; return await this.format.read(filename, undefined); } get sourcePaths() { const includeGlobs = this.include .map((includePath) => { const isDir = (0, utils_1.isDirectory)(includePath); /** * glob library results from absolute patterns such as /foo/* are mounted onto the root setting using path.join. * On windows, this will by default result in /foo/* matching C:\foo\bar.txt. */ return isDir ? (0, normalize_path_1.default)(path_1.default.resolve(process.cwd(), includePath === "/" ? "" : includePath, "**/*.*")) : includePath; }) .map(utils_1.makePathRegexSafe); return (0, glob_1.globSync)(includeGlobs, { ignore: this.exclude, mark: true }); } get localeDir() { const localePatternIndex = this.path.indexOf(LOCALE); if (localePatternIndex === -1) { throw Error(`Invalid catalog path: ${LOCALE} variable is missing`); } return this.path.substring(0, localePatternIndex); } get locales() { return this.config.locales; } } exports.Catalog = Catalog; function getTemplatePath(ext, path) { return path.replace(LOCALE_SUFFIX_RE, "messages" + ext); } function cleanObsolete(messages) { return Object.fromEntries(Object.entries(messages).filter(([, message]) => !message.obsolete)); } function order(by, catalog) { return { messageId: orderByMessageId, message: orderByMessage, origin: orderByOrigin, }[by](catalog); } /** * Object keys are in the same order as they were created * https://stackoverflow.com/a/31102605/1535540 */ function orderByMessageId(messages) { return Object.keys(messages) .sort() .reduce((acc, key) => { ; acc[key] = messages[key]; return acc; }, {}); } function orderByOrigin(messages) { function getFirstOrigin(messageKey) { const sortedOrigins = messages[messageKey].origin.sort((a, b) => { if (a[0] < b[0]) return -1; if (a[0] > b[0]) return 1; return 0; }); return sortedOrigins[0]; } return Object.keys(messages) .sort((a, b) => { const [aFile, aLineNumber] = getFirstOrigin(a); const [bFile, bLineNumber] = getFirstOrigin(b); if (aFile < bFile) return -1; if (aFile > bFile) return 1; if (aLineNumber < bLineNumber) return -1; if (aLineNumber > bLineNumber) return 1; return 0; }) .reduce((acc, key) => { ; acc[key] = messages[key]; return acc; }, {}); } async function writeCompiled(path, locale, compiledCatalog, namespace) { let ext; switch (namespace) { case "es": ext = "mjs"; break; case "ts": case "json": ext = namespace; break; default: ext = "js"; } const filename = `${(0, utils_1.replacePlaceholders)(path, { locale })}.${ext}`; await (0, utils_1.writeFile)(filename, compiledCatalog); return filename; } function orderByMessage(messages) { // hardcoded en-US locale to have consistent sorting // @see https://github.com/lingui/js-lingui/pull/1808 const collator = new Intl.Collator("en-US"); return Object.keys(messages) .sort((a, b) => { const aMsg = messages[a].message || ""; const bMsg = messages[b].message || ""; const aCtxt = messages[a].context || ""; const bCtxt = messages[b].context || ""; return collator.compare(aMsg, bMsg) || collator.compare(aCtxt, bCtxt); }) .reduce((acc, key) => { ; acc[key] = messages[key]; return acc; }, {}); }