@lingui/cli
Version:
Lingui CLI to extract messages, compile catalogs, and manage translation workflows
154 lines (153 loc) • 6.88 kB
JavaScript
import fs from "fs";
import { dirname } from "path";
import { tioInit, tioSync, } from "./translationIO/translationio-api.js";
import { order } from "../api/catalog.js";
import { createLinguiItemFromSegment, createSegmentFromLinguiItem, } from "./translationIO/segment-converters.js";
import { readFileSync } from "node:fs";
import path from "node:path";
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
export default async function syncProcess(config, options, extractionResult) {
const reportSuccess = (project) => {
return `\n----------\nProject successfully synchronized. Please use this URL to translate: ${project.url}\n----------`;
};
const reportError = (errors) => {
throw `\n----------\nSynchronization with Translation.io failed: ${errors.join(", ")}\n----------`;
};
const { success, project, errors } = await init(config, extractionResult);
if (success) {
return reportSuccess(project);
}
if (errors[0] === "This project has already been initialized.") {
const { success, project, errors } = await sync(config, options, extractionResult);
if (success) {
return reportSuccess(project);
}
return reportError(errors);
}
return reportError(errors);
}
function getLinguiVersion() {
const packageJson = JSON.parse(readFileSync(path.resolve(import.meta.dirname, "../../package.json"), "utf8"));
return packageJson.version;
}
// Initialize project with source and existing translations (only first time!)
// Cf. https://translation.io/docs/create-library#initialization
export async function init(config, extractionResult) {
const sourceLocale = config.sourceLocale || "en";
const targetLocales = getTargetLocales(config);
const segments = {};
targetLocales.forEach((targetLocale) => {
segments[targetLocale] = [];
});
// Create segments from source locale PO items
for (const { messagesByLocale } of extractionResult) {
const messages = messagesByLocale[sourceLocale];
Object.entries(messages).forEach(([key, entry]) => {
if (entry.obsolete)
return;
targetLocales.forEach((targetLocale) => {
segments[targetLocale].push(createSegmentFromLinguiItem(key, entry));
});
});
}
// Add translations to segments from target locale PO items
for (const { messagesByLocale } of extractionResult) {
for (const targetLocale of targetLocales) {
const messages = messagesByLocale[targetLocale];
Object.entries(messages)
.filter(([, entry]) => !entry.obsolete)
.forEach(([, entry], index) => {
segments[targetLocale][index].target = entry.translation;
});
}
}
const { data, error } = await tioInit({
client: "lingui",
version: getLinguiVersion(),
source_language: sourceLocale,
target_languages: targetLocales,
segments: segments,
}, config.service.apiKey);
if (error) {
return { success: false, errors: [error.message] };
}
if ("errors" in data) {
return { success: false, errors: data.errors };
}
await writeSegmentsToCatalogs(config, sourceLocale, extractionResult, data.segments);
return { success: true, project: data.project };
}
// 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
export async function sync(config, options, extractionResult) {
const sourceLocale = config.sourceLocale || "en";
const targetLocales = getTargetLocales(config);
const segments = [];
// Create segments with correct source
for (const { messagesByLocale } of extractionResult) {
const messages = messagesByLocale[sourceLocale];
Object.entries(messages).forEach(([key, entry]) => {
if (entry.obsolete)
return;
segments.push(createSegmentFromLinguiItem(key, entry));
});
}
const { data, error } = await tioSync({
client: "lingui",
version: getLinguiVersion(),
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),
}, config.service.apiKey);
if (error) {
return { success: false, errors: [error.message] };
}
if ("errors" in data) {
return { success: false, errors: data.errors };
}
await writeSegmentsToCatalogs(config, sourceLocale, extractionResult, data.segments);
return { success: true, project: data.project };
}
export async function writeSegmentsToCatalogs(config, sourceLocale, extractionResult, segmentsPerLocale) {
// Create segments from source locale PO items
for (const { catalog, messagesByLocale } of extractionResult) {
const sourceMessages = messagesByLocale[sourceLocale];
for (const targetLocale of Object.keys(segmentsPerLocale)) {
// Remove existing target POs and JS for this target locale
{
const path = catalog.getFilename(targetLocale);
const jsPath = path.replace(new RegExp(`${catalog.format.getCatalogExtension()}$`), "") + ".js";
const dirPath = dirname(path);
// todo: check tests and all these logic, maybe it could be simplified to just drop the folder
// Remove PO, JS and empty dir
if (fs.existsSync(path)) {
await fs.promises.unlink(path);
}
if (fs.existsSync(jsPath)) {
await fs.promises.unlink(jsPath);
}
if (fs.existsSync(dirPath) && fs.readdirSync(dirPath).length === 0) {
await fs.promises.rmdir(dirPath);
}
}
const translations = Object.fromEntries(segmentsPerLocale[targetLocale].map((segment) => createLinguiItemFromSegment(segment)));
const messages = Object.fromEntries(Object.entries(sourceMessages).map(([key, entry]) => {
return [
key,
{
...entry,
translation: translations[key]?.translation,
},
];
}));
await catalog.write(targetLocale, order(config.orderBy, messages));
}
}
}