UNPKG

attranslate

Version:

Text Translator for Websites and Apps

204 lines (203 loc) 8.13 kB
"use strict"; 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}`); }