attranslate
Version:
Text Translator for Websites and Apps
204 lines (203 loc) • 8.13 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypeChatTranslate = void 0;
const tslib_1 = require("tslib");
const axios_1 = (0, tslib_1.__importDefault)(require("axios"));
const clipboardy_1 = (0, tslib_1.__importDefault)(require("clipboardy"));
const inquirer_1 = (0, tslib_1.__importDefault)(require("inquirer"));
const lodash_1 = require("lodash");
const typechat_1 = require("typechat");
const util_1 = require("../util/util");
const MINUTE_MS = 60 * 1000;
function generateSchema(batch, name, comment) {
const properties = batch
.map((tString) => {
return ` '${tString.key}': string;`;
})
.join("\n");
const schema = `export interface ${name} {\n${properties}\n}\n`;
return comment ? `// ${comment} \n\n${schema}` : schema;
}
function generatePrompt(batch, args) {
const entries = batch.reduce((entries, tString) => {
entries[tString.key] = tString.value;
return entries;
}, {});
const basePrompt = `Translate the following JSON object from ${args.srcLng} into ${args.targetLng}:\n`;
return basePrompt + "\n" + JSON.stringify(entries, null, 2);
}
function parseResponse(batch, data) {
return batch.map((tString) => {
var _a;
const result = {
key: tString.key,
translated: (_a = data[tString.key]) !== null && _a !== void 0 ? _a : "",
};
return result;
});
}
async function translateBatch(model, batch, args, env) {
var _a;
console.log("Translate a batch of " + batch.length + " strings with TypeChat...");
const schemaName = (_a = env.TYPECHAT_SCHEMA_NAME) !== null && _a !== void 0 ? _a : "AppLocalizations";
const schemaComment = env.TYPECHAT_SCHEMA_COMMENT;
const translator = (0, typechat_1.createJsonTranslator)(model, generateSchema(batch, schemaName, schemaComment), schemaName);
const response = await translator.translate(generatePrompt(batch, args));
if (!response.success) {
(0, util_1.logFatal)(response.message);
}
return parseResponse(batch, response.data);
}
function createLanguageModel(env) {
var _a, _b, _c;
const apiKey = (_a = env.OPENAI_API_KEY) !== null && _a !== void 0 ? _a : missingEnvironmentVariable("OPENAI_API_KEY");
const model = (_b = env.OPENAI_MODEL) !== null && _b !== void 0 ? _b : "gpt-4o-mini-2024-07-18";
const url = (_c = env.OPENAI_ENDPOINT) !== null && _c !== void 0 ? _c : "https://api.openai.com/v1/chat/completions";
return createAxiosLanguageModel(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
}, { model });
}
function createManualModel() {
const model = {
complete,
};
return model;
async function complete(prompt) {
await clipboardy_1.default.write(prompt);
console.log(`Prompt copied to clipboard`);
await inquirer_1.default.prompt([
{
name: "Enter",
message: "Press enter after you copied the response.",
type: "input",
},
]);
const result = await clipboardy_1.default.read();
return (0, typechat_1.success)(result);
}
}
function sanitize_keys(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (args) {
const sanitizedKeys = args.strings.map(({ key }) => {
return {
new: key.replace(/[^a-zA-Z0-9_]+/g, "_"),
old: key,
};
});
// Replace keys with sanitized versions
for (let i = 0; i < sanitizedKeys.length; i++) {
args.strings[i].key = sanitizedKeys[i].new;
}
// Call the original method
const results = await originalMethod.apply(this, [args]);
// Restore original keys in the results
for (let i = 0; i < sanitizedKeys.length; i++) {
results[i].key = sanitizedKeys[i].old;
}
return results;
};
return descriptor;
}
class TypeChatTranslate {
constructor(manual) {
this.manual = manual !== null && manual !== void 0 ? manual : false;
}
async translateStrings(args) {
var _a, _b;
const rpm = parseInt((_a = process.env.TYPECHAT_RPM) !== null && _a !== void 0 ? _a : "");
const batchSize = parseInt((_b = process.env.OPEN_AI_BATCH_SIZE) !== null && _b !== void 0 ? _b : "");
const batches = (0, lodash_1.chunk)(args.strings, isNaN(batchSize) ? 10 : batchSize);
const results = [];
const model = this.manual
? createManualModel()
: createLanguageModel(process.env);
for (var i = 0; i < batches.length; i++) {
const batch = batches[i];
const start = new Date();
const result = await translateBatch(model, batch, args, process.env);
results.push(result);
// Sleep to not exceeded the specified requests per minute (RPM)
if (!this.manual && !isNaN(rpm) && rpm > 0 && i < batches.length - 1) {
const requestDuration = new Date().getTime() - start.getTime();
const sleepDuration = (MINUTE_MS / rpm) - requestDuration;
console.log(`Going to sleep for ${sleepDuration} ms`);
await sleep(sleepDuration);
}
}
return (0, lodash_1.flatten)(results);
}
}
(0, tslib_1.__decorate)([
sanitize_keys
], TypeChatTranslate.prototype, "translateStrings", null);
exports.TypeChatTranslate = TypeChatTranslate;
// The following code is from TypeChat
// https://github.com/microsoft/TypeChat/blob/e8395ef2e4688ec7b94a7612046aeaec0af93046/src/model.ts#L65
// MIT License - https://github.com/microsoft/TypeChat/blob/main/LICENSE
// Copyright (c) Microsoft Corporation.
/**
* Common implementation of language model encapsulation of an OpenAI REST API endpoint.
*/
function createAxiosLanguageModel(url, config, defaultParams) {
const client = axios_1.default.create(config);
const model = {
complete,
};
return model;
async function complete(prompt) {
var _a, _b, _c, _d;
let retryCount = 0;
const retryMaxAttempts = (_a = model.retryMaxAttempts) !== null && _a !== void 0 ? _a : 3;
const retryPauseMs = (_b = model.retryPauseMs) !== null && _b !== void 0 ? _b : 1000;
while (true) {
const params = {
max_tokens: 2048,
temperature: 0,
...defaultParams,
messages: [{ role: "user", content: prompt }],
n: 1,
};
const result = await client.post(url, params, {
validateStatus: (status) => true,
});
if (result.status === 200) {
return (0, typechat_1.success)((_d = (_c = result.data.choices[0].message) === null || _c === void 0 ? void 0 : _c.content) !== null && _d !== void 0 ? _d : "");
}
if (result.status === 401) {
return (0, typechat_1.error)(`REST API error ${result.status}: ${result.statusText}`);
}
if (!isTransientHttpError(result.status) ||
retryCount >= retryMaxAttempts) {
return (0, typechat_1.error)(`REST API error ${result.status}: ${result.statusText}`);
}
await sleep(retryPauseMs);
retryCount++;
}
}
}
/**
* Returns true of the given HTTP status code represents a transient error.
*/
function isTransientHttpError(code) {
switch (code) {
case 429: // TooManyRequests
case 500: // InternalServerError
case 502: // BadGateway
case 503: // ServiceUnavailable
case 504: // GatewayTimeout
return true;
}
return false;
}
/**
* Sleeps for the given number of milliseconds.
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms > 0 ? ms : 0));
}
function missingEnvironmentVariable(name) {
(0, util_1.logFatal)(`Missing environment variable: ${name}`);
}