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: {
|
rules: {
|
||||||
"no-console": "off",
|
"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!
|
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