@lingui/cli
Version:
CLI for working wit message catalogs
261 lines (260 loc) • 9.93 kB
JavaScript
"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;
}, {});
}