feat: add script to apply crowdin translations from memory
Node.js CI / Lint and Test (push) Successful in 28s

This commit is contained in:
2025-08-27 16:41:35 -07:00
parent cb6934ef22
commit 3541fdc411
10 changed files with 311 additions and 1 deletions
+47
View File
@@ -0,0 +1,47 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface File {
data: Array<{
data: {
id: number;
projectId: number;
branchId: number;
directoryId: number;
name: string;
title: string;
context: string;
type: string;
path: string;
status: string;
revisionId: number;
priority: string;
importOptions: {
importTranslations: boolean;
firstLineContainsHeader: boolean;
contentSegmentation: boolean;
customSegmentation: boolean;
scheme: {
identifier: number;
sourcePhrase: number;
en: number;
de: number;
};
};
exportOptions: {
exportPattern: string;
};
excludedTargetLanguages: Array<string>;
parserVersion: number;
createdAt: string;
updatedAt: string;
};
}>;
pagination: {
offset: number;
limit: number;
};
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface PreTranslation {
data: {
identifier: string;
status: string;
progress: number;
attributes: {
languageIds: Array<string>;
fileIds: Array<number>;
method: string;
autoApproveOption: string;
duplicateTranslations: boolean;
skipApprovedTranslations: boolean;
translateUntranslatedOnly: boolean;
translateWithPerfectMatchOnly: boolean;
};
createdAt: string;
updatedAt: string;
startedAt: string;
finishedAt: string;
};
}
+60
View File
@@ -0,0 +1,60 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Project {
data: {
id: number;
type: number;
userId: number;
sourceLanguageId: string;
targetLanguageIds: Array<string>;
languageAccessPolicy: string;
name: string;
cname: string;
identifier: string;
description: string;
visibility: string;
logo: string;
publicDownloads: boolean;
createdAt: string;
updatedAt: string;
lastActivity: string;
sourceLanguage: {
id: string;
name: string;
editorCode: string;
twoLettersCode: string;
threeLettersCode: string;
locale: string;
androidCode: string;
osxCode: string;
osxLocale: string;
pluralCategoryNames: Array<string>;
pluralRules: string;
pluralExamples: Array<string>;
textDirection: string;
dialectOf: string;
};
targetLanguages: Array<{
id: string;
name: string;
editorCode: string;
twoLettersCode: string;
threeLettersCode: string;
locale: string;
androidCode: string;
osxCode: string;
osxLocale: string;
pluralCategoryNames: Array<string>;
pluralRules: string;
pluralExamples: Array<string>;
textDirection: string;
dialectOf: string;
}>;
webUrl: string;
savingsReportSettingsTemplateId: number;
};
}
+71
View File
@@ -0,0 +1,71 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { sleep } from "../utils/sleep.js";
import { getFiles } from "./utils/getFiles.js";
import { getLanguages } from "./utils/getLanguages.js";
import type { PreTranslation } from "./interfaces/preTranslation.js";
const projectId = process.env.CROWDIN_PROJECT_ID;
const apiUrl = process.env.CROWDIN_API_URL;
const token = process.env.CROWDIN_TOKEN;
if (
projectId === undefined
|| projectId === ""
|| apiUrl === undefined
|| apiUrl === ""
|| token === undefined
|| token === ""
) {
throw new Error(`Project ID or API URL is missing! Did you run this script with 'op run'?`);
}
const languages = await getLanguages(projectId, apiUrl, token);
console.log(`Found ${languages.length.toString()} active languages.`);
const files = await getFiles(projectId, apiUrl, token);
console.log(`Found ${files.length.toString()} files.`);
const url = `${apiUrl}/projects/${projectId}/pre-translations`;
const request = await fetch(url, {
body: JSON.stringify({
autoApproveOption: "perfectMatchOnly",
fileIds: files,
languageIds: languages,
method: "tm",
skipApprovedTranslations: true,
translateWithPerfectMatchOnly: true,
}),
headers: {
"authorization": `Bearer ${token}`,
// eslint-disable-next-line @typescript-eslint/naming-convention -- header.
"content-type": "application/json",
},
method: "POST",
});
const response: PreTranslation = await request.json();
const { identifier } = response.data;
console.log(`Pre-translation ${identifier} started! Will report progress every 5 seconds.`);
let { progress } = response.data;
const progressUrl = `${apiUrl}/projects/${projectId}/pre-translations/${identifier}`;
while (progress < 100) {
await sleep(5000);
const progressRequest = await fetch(progressUrl, {
headers: {
authorization: `Bearer ${token}`,
},
});
const progressResult: PreTranslation = await progressRequest.json();
const { progress: updatedProgress } = progressResult.data;
progress = updatedProgress;
console.log(`Progress: ${progress.toString()}%`);
}
console.log("Pretranslation complete!");
+50
View File
@@ -0,0 +1,50 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { File } from "../interfaces/file.js";
/**
* Gets a list of all files from a project.
* @param projectId - The project ID (a numeric string).
* @param apiUrl - The base URL for the API.
* @param token - The API key.
* @returns An array of language IDs as strings.
*/
export const getFiles
= async(
projectId: string,
apiUrl: string,
token: string,
): Promise<Array<number>> => {
const url = `${apiUrl}/projects/${projectId}/files?limit=500`;
let offset = 0;
const ids: Array<number> = [];
console.log(`Requesting files ${offset.toString()} to ${(offset + 500).toString()}`);
let request = await fetch(url, {
headers: {
authorization: `Bearer ${token}`,
},
});
let response: File = await request.json();
ids.push(...response.data.map((datum) => {
return datum.data.id;
}));
while (response.data.length >= 500) {
offset = offset + 500;
console.log(`Requesting files ${offset.toString()} to ${(offset + 500).toString()}`);
request = await fetch(`${url}&offset=${offset.toString()}`, {
headers: {
authorization: `Bearer ${token}`,
},
});
response = await request.json();
ids.push(...response.data.map((datum) => {
return datum.data.id;
}));
}
return ids;
};
+31
View File
@@ -0,0 +1,31 @@
/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Project } from "../interfaces/project.js";
/**
* Fetches a project from Crowdin and returns the list of target language IDs.
* @param projectId - The project ID (a numeric string).
* @param apiUrl - The base URL for the API.
* @param token - The API key.
* @returns An array of language IDs as strings.
*/
export const getLanguages
= async(
projectId: string,
apiUrl: string,
token: string,
): Promise<Array<string>> => {
const url = `${apiUrl}/projects/${projectId}`;
const request = await fetch(url, {
headers: {
authorization: `Bearer ${token}`,
},
});
const response: Project = await request.json();
const ids = response.data.targetLanguageIds;
return ids;
};