mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-09 13:21:47 +00:00
127 lines
3.6 KiB
JavaScript
127 lines
3.6 KiB
JavaScript
const API = "https://api.crowdin.com/api/v2";
|
|
const ENGINE_ID = 649248; // Google Translate MT engine
|
|
const PROJECT_NAME = "termix-ssh";
|
|
const SOURCE_FILE = "en.json";
|
|
|
|
function token() {
|
|
const t = process.env.CROWDIN_API_KEY;
|
|
if (!t) throw new Error("CROWDIN_API_KEY is not set");
|
|
return t;
|
|
}
|
|
|
|
async function request(method, pathname, body) {
|
|
const res = await fetch(`${API}${pathname}`, {
|
|
method,
|
|
headers: {
|
|
Authorization: `Bearer ${token()}`,
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
throw new Error(`${method} ${pathname} -> ${res.status}: ${text}`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
async function paged(pathname) {
|
|
const items = [];
|
|
let offset = 0;
|
|
const limit = 500;
|
|
for (;;) {
|
|
const sep = pathname.includes("?") ? "&" : "?";
|
|
const page = await request(
|
|
"GET",
|
|
`${pathname}${sep}limit=${limit}&offset=${offset}`,
|
|
);
|
|
const batch = (page.data || []).map((row) => row.data);
|
|
items.push(...batch);
|
|
if (batch.length < limit) break;
|
|
offset += limit;
|
|
}
|
|
return items;
|
|
}
|
|
|
|
async function resolveProjectId() {
|
|
const projects = await paged("/projects");
|
|
const match = projects.find(
|
|
(p) => p.identifier === PROJECT_NAME || p.name === PROJECT_NAME,
|
|
);
|
|
if (!match) throw new Error(`project "${PROJECT_NAME}" not found`);
|
|
return { id: match.id, targetLanguageIds: match.targetLanguageIds || [] };
|
|
}
|
|
|
|
function isSourceFile(file) {
|
|
return file.path === `/${SOURCE_FILE}`;
|
|
}
|
|
|
|
async function resolveFileIds(projectId) {
|
|
const files = await paged(`/projects/${projectId}/files`);
|
|
const ids = files.filter(isSourceFile).map((f) => f.id);
|
|
if (ids.length === 0)
|
|
throw new Error(`no "${SOURCE_FILE}" source files found in project`);
|
|
return ids;
|
|
}
|
|
|
|
async function pollPreTranslation(projectId, preTranslationId) {
|
|
for (;;) {
|
|
const { data } = await request(
|
|
"GET",
|
|
`/projects/${projectId}/pre-translations/${preTranslationId}`,
|
|
);
|
|
if (data.status === "finished") return data;
|
|
if (data.status === "failed" || data.status === "canceled") {
|
|
throw new Error(
|
|
`pre-translation ${data.status} (progress ${data.progress}%)`,
|
|
);
|
|
}
|
|
await new Promise((r) => setTimeout(r, 5000));
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const { id: projectId, targetLanguageIds } = await resolveProjectId();
|
|
if (targetLanguageIds.length === 0) {
|
|
throw new Error("project has no target languages configured");
|
|
}
|
|
|
|
const fileIds = await resolveFileIds(projectId);
|
|
|
|
console.log(
|
|
`Pre-translating project ${projectId}, ${fileIds.length} files, ${targetLanguageIds.length} languages`,
|
|
);
|
|
|
|
// Translation memory first so strings that moved between files keep their
|
|
// existing translations, then machine translation for whatever is left.
|
|
const passes = [
|
|
{ method: "tm", translateUntranslatedOnly: true },
|
|
{ method: "mt", engineId: ENGINE_ID, scope: "untranslated" },
|
|
];
|
|
for (const pass of passes) {
|
|
const { data } = await request(
|
|
"POST",
|
|
`/projects/${projectId}/pre-translations`,
|
|
{ languageIds: targetLanguageIds, fileIds, ...pass },
|
|
);
|
|
const result = await pollPreTranslation(projectId, data.identifier);
|
|
console.log(
|
|
`${pass.method} pre-translation finished (${result.progress}%)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
resolveProjectId,
|
|
resolveFileIds,
|
|
isSourceFile,
|
|
pollPreTranslation,
|
|
};
|
|
|
|
if (require.main === module) {
|
|
main().catch((err) => {
|
|
console.error(`crowdin-pretranslate: ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
}
|