generated from nhcarrigan/template
feat: add script to apply crowdin translations from memory
Node.js CI / Lint and Test (push) Successful in 28s
Node.js CI / Lint and Test (push) Successful in 28s
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Crowdin
|
||||
CROWDIN_PROJECT_ID="op://Environment Variables - Development/Scripts/Crowdin Project ID"
|
||||
CROWDIN_API_URL="op://Environment Variables - Development/Scripts/Crowdin API Url"
|
||||
CROWDIN_TOKEN="op://Environment Variables - Development/Scripts/Crowdin Token"
|
||||
@@ -5,6 +5,8 @@ export default [
|
||||
{
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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!");
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+3
-1
@@ -8,4 +8,6 @@ console.log(`Hello there~!
|
||||
|
||||
It looks like you may be trying to run this tool like a typical project. But it is not!
|
||||
|
||||
Instead of running "pnpm start", you should identify the script you want to run and use "tsx src/path/to/script.js".`);
|
||||
Instead of running "pnpm start", you should identify the script you want to run and use "tsx src/path/to/script.js".
|
||||
|
||||
Or, if your script requires environment variables, run "op run --env-file=.env --no-masking -- tsx src/path/to/script.js"`);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/**
|
||||
* Uses async promises to pause exection for the specified time.
|
||||
* @param ms - The number of milliseconds to pause for.
|
||||
* @returns The promise.
|
||||
*/
|
||||
export const sleep = async(ms: number): Promise<Promise<void>> => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user