@lingui/cli
Version:
CLI for working wit message catalogs
301 lines (300 loc) • 12.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = syncProcess;
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const pofile_1 = __importDefault(require("pofile"));
const https_1 = __importDefault(require("https"));
const glob_1 = require("glob");
const date_fns_1 = require("date-fns");
const EXPLICIT_ID_FLAG = "js-lingui-explicit-id";
const EXPLICIT_ID_AND_CONTEXT_FLAG = "js-lingui-explicit-id-and-context";
const getCreateHeaders = (language) => ({
"POT-Creation-Date": (0, date_fns_1.format)(new Date(), "yyyy-MM-dd HH:mmxxxx"),
"MIME-Version": "1.0",
"Content-Type": "text/plain; charset=utf-8",
"Content-Transfer-Encoding": "8bit",
"X-Generator": "@lingui/cli",
Language: language,
});
const getTargetLocales = (config) => {
const sourceLocale = config.sourceLocale || "en";
const pseudoLocale = config.pseudoLocale || "pseudo";
return config.locales.filter((value) => value != sourceLocale && value != pseudoLocale);
};
// Main sync method, call "Init" or "Sync" depending on the project context
async function syncProcess(config, options) {
if (config.format != "po") {
console.error(`\n----------\nTranslation.io service is only compatible with the "po" format. Please update your Lingui configuration accordingly.\n----------`);
process.exit(1);
}
return await new Promise((resolve, reject) => {
const successCallback = (project) => {
resolve(`\n----------\nProject successfully synchronized. Please use this URL to translate: ${project.url}\n----------`);
};
const failCallback = (errors) => {
reject(`\n----------\nSynchronization with Translation.io failed: ${errors.join(", ")}\n----------`);
};
init(config, options, successCallback, (errors) => {
if (errors.length &&
errors[0] === "This project has already been initialized.") {
sync(config, options, successCallback, failCallback);
}
else {
failCallback(errors);
}
});
});
}
// Initialize project with source and existing translations (only first time!)
// Cf. https://translation.io/docs/create-library#initialization
function init(config, options, successCallback, failCallback) {
const sourceLocale = config.sourceLocale || "en";
const targetLocales = getTargetLocales(config);
const paths = poPathsPerLocale(config);
const segments = {};
targetLocales.forEach((targetLocale) => {
segments[targetLocale] = [];
});
// Create segments from source locale PO items
paths[sourceLocale].forEach((path) => {
const raw = fs_1.default.readFileSync(path).toString();
const po = pofile_1.default.parse(raw);
po.items
.filter((item) => !item["obsolete"])
.forEach((item) => {
targetLocales.forEach((targetLocale) => {
const newSegment = createSegmentFromPoItem(item);
segments[targetLocale].push(newSegment);
});
});
});
// Add translations to segments from target locale PO items
targetLocales.forEach((targetLocale) => {
paths[targetLocale].forEach((path) => {
const raw = fs_1.default.readFileSync(path).toString();
const po = pofile_1.default.parse(raw);
po.items
.filter((item) => !item["obsolete"])
.forEach((item, index) => {
segments[targetLocale][index].target = item.msgstr[0];
});
});
});
const request = {
client: "lingui",
version: require("@lingui/core/package.json").version,
source_language: sourceLocale,
target_languages: targetLocales,
segments: segments,
};
postTio("init", request, config.service.apiKey, (response) => {
if (response.errors) {
failCallback(response.errors);
}
else {
saveSegmentsToTargetPos(config, paths, response.segments);
successCallback(response.project);
}
}, (error) => {
console.error(`\n----------\nSynchronization with Translation.io failed: ${error}\n----------`);
});
}
// Send all source text from PO to Translation.io and create new PO based on received translations
// Cf. https://translation.io/docs/create-library#synchronization
function sync(config, options, successCallback, failCallback) {
const sourceLocale = config.sourceLocale || "en";
const targetLocales = getTargetLocales(config);
const paths = poPathsPerLocale(config);
const segments = [];
// Create segments with correct source
paths[sourceLocale].forEach((path) => {
const raw = fs_1.default.readFileSync(path).toString();
const po = pofile_1.default.parse(raw);
po.items
.filter((item) => !item["obsolete"])
.forEach((item) => {
const newSegment = createSegmentFromPoItem(item);
segments.push(newSegment);
});
});
const request = {
client: "lingui",
version: require("@lingui/core/package.json").version,
source_language: sourceLocale,
target_languages: targetLocales,
segments: segments,
// Sync and then remove unused segments (not present in the local application) from Translation.io
purge: Boolean(options.clean),
};
postTio("sync", request, config.service.apiKey, (response) => {
if (response.errors) {
failCallback(response.errors);
}
else {
saveSegmentsToTargetPos(config, paths, response.segments);
successCallback(response.project);
}
}, (error) => {
console.error(`\n----------\nSynchronization with Translation.io failed: ${error}\n----------`);
});
}
function createSegmentFromPoItem(item) {
const itemHasExplicitId = item.extractedComments.includes(EXPLICIT_ID_FLAG);
const itemHasContext = item.msgctxt != null;
const segment = {
type: "source", // No way to edit text for source language (inside code), so not using "key" here
source: "",
context: "",
references: [],
comment: "",
};
// For segment.source & segment.context, we must remain compatible with projects created/synced before Lingui V4
if (itemHasExplicitId) {
segment.source = item.msgstr[0];
segment.context = item.msgid;
}
else {
segment.source = item.msgid;
if (itemHasContext) {
segment.context = item.msgctxt;
}
}
if (item.references.length) {
segment.references = item.references;
}
// Since Lingui v4, when using explicit IDs, Lingui automatically adds 'js-lingui-explicit-id' to the extractedComments array
if (item.extractedComments.length) {
segment.comment = item.extractedComments.join(" | ");
if (itemHasExplicitId && itemHasContext) {
// segment.context is already used for the explicit ID, so we need to pass the context (for translators) in segment.comment
segment.comment = `${item.msgctxt} | ${segment.comment}`;
// Replace the flag to let us know how to recompose a target PO Item that is consistent with the source PO Item
segment.comment = segment.comment.replace(EXPLICIT_ID_FLAG, EXPLICIT_ID_AND_CONTEXT_FLAG);
}
}
return segment;
}
function createPoItemFromSegment(segment) {
var _a, _b;
const segmentHasExplicitId = (_a = segment.comment) === null || _a === void 0 ? void 0 : _a.includes(EXPLICIT_ID_FLAG);
const segmentHasExplicitIdAndContext = (_b = segment.comment) === null || _b === void 0 ? void 0 : _b.includes(EXPLICIT_ID_AND_CONTEXT_FLAG);
const item = new pofile_1.default.Item();
if (segmentHasExplicitId || segmentHasExplicitIdAndContext) {
item.msgid = segment.context;
}
else {
item.msgid = segment.source;
item.msgctxt = segment.context;
}
item.msgstr = [segment.target];
item.references =
segment.references && segment.references.length ? segment.references : [];
if (segment.comment) {
segment.comment = segment.comment.replace(EXPLICIT_ID_AND_CONTEXT_FLAG, EXPLICIT_ID_FLAG);
item.extractedComments = segment.comment ? segment.comment.split(" | ") : [];
// We recompose a target PO Item that is consistent with the source PO Item
if (segmentHasExplicitIdAndContext) {
item.msgctxt = item.extractedComments.shift();
}
}
return item;
}
function saveSegmentsToTargetPos(config, paths, segmentsPerLocale) {
Object.keys(segmentsPerLocale).forEach((targetLocale) => {
// Remove existing target POs and JS for this target locale
paths[targetLocale].forEach((path) => {
const jsPath = path.replace(/\.po?$/, "") + ".js";
const dirPath = (0, path_1.dirname)(path);
// Remove PO, JS and empty dir
if (fs_1.default.existsSync(path)) {
fs_1.default.unlinkSync(path);
}
if (fs_1.default.existsSync(jsPath)) {
fs_1.default.unlinkSync(jsPath);
}
if (fs_1.default.existsSync(dirPath) && fs_1.default.readdirSync(dirPath).length === 0) {
fs_1.default.rmdirSync(dirPath);
}
});
// Find target path (ignoring {name})
const localePath = "".concat(config.catalogs[0].path
.replace(/{locale}/g, targetLocale)
.replace(/{name}/g, ""), ".po");
const segments = segmentsPerLocale[targetLocale];
const po = new pofile_1.default();
po.headers = getCreateHeaders(targetLocale);
const items = [];
segments.forEach((segment) => {
const item = createPoItemFromSegment(segment);
items.push(item);
});
// Sort items by messageId
po.items = items.sort((a, b) => {
if (a.msgid < b.msgid) {
return -1;
}
if (a.msgid > b.msgid) {
return 1;
}
return 0;
});
// Check that localePath directory exists and save PO file
fs_1.default.promises.mkdir((0, path_1.dirname)(localePath), { recursive: true }).then(() => {
po.save(localePath, (err) => {
if (err) {
console.error("Error while saving target PO files:");
console.error(err);
process.exit(1);
}
});
});
});
}
function poPathsPerLocale(config) {
const paths = {};
config.locales.forEach((locale) => {
paths[locale] = [];
config.catalogs.forEach((catalog) => {
const path = "".concat(catalog.path.replace(/{locale}/g, locale).replace(/{name}/g, "*"), ".po");
// If {name} is present (replaced by *), list all the existing POs
if (path.includes("*")) {
paths[locale] = paths[locale].concat((0, glob_1.globSync)(path));
}
else {
paths[locale].push(path);
}
});
});
return paths;
}
function postTio(action, request, apiKey, successCallback, failCallback) {
const jsonRequest = JSON.stringify(request);
const options = {
hostname: "translation.io",
path: `/api/v1/segments/${action}.json?api_key=${apiKey}`,
method: "POST",
headers: {
"Content-Type": "application/json",
},
};
const req = https_1.default.request(options, (res) => {
res.setEncoding("utf8");
let body = "";
res.on("data", (chunk) => {
body = body.concat(chunk);
});
res.on("end", () => {
const response = JSON.parse(body);
successCallback(response);
});
});
req.on("error", (e) => {
failCallback(e);
});
req.write(jsonRequest);
req.end();
}