UNPKG

attranslate

Version:

Semi-automated Text Translator for Websites and Apps

170 lines (169 loc) 6.62 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"); 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`; const customPrompt = args.prompt ? `\nAdditional instructions: ${args.prompt}\n\n` : "\n"; return basePrompt + customPrompt + 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-3.5-turbo-instruct"; 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); } } class TypeChatTranslate { constructor(manual) { this.manual = manual !== null && manual !== void 0 ? manual : false; } async translateStrings(args) { var _a; const batchSize = parseInt((_a = process.env.OPEN_AI_BATCH_SIZE) !== null && _a !== void 0 ? _a : ""); const batches = (0, lodash_1.chunk)(args.strings, isNaN(batchSize) ? 10 : batchSize); const results = []; const model = this.manual ? createManualModel() : createLanguageModel(process.env); for (const batch of batches) { const result = await translateBatch(model, batch, args, process.env); results.push(result); } return (0, lodash_1.flatten)(results); } } 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)); } function missingEnvironmentVariable(name) { (0, util_1.logFatal)(`Missing environment variable: ${name}`); }