UNPKG

i18n-ai-translate

Version:

Use LLMs to translate your i18n JSON to any language.

376 lines 19.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.translateDirectory = translateDirectory; exports.translateDirectoryDiff = translateDirectoryDiff; const constants_1 = require("./constants"); const diff_1 = require("diff"); const flat_1 = require("flat"); const utils_1 = require("./utils"); const translate_1 = require("./translate"); const safe_1 = __importDefault(require("colors/safe")); const fs_1 = __importDefault(require("fs")); const path_1 = __importStar(require("path")); /** * Wraps translate to take all keys of all files in a directory and re-create the exact * directory structure and translations for the target language * @param options - The directory translation's options */ async function translateDirectory(options) { const jsonFolder = path_1.default.resolve(process.cwd(), "jsons"); let fullBasePath; if (path_1.default.isAbsolute(options.baseDirectory)) { fullBasePath = path_1.default.resolve(options.baseDirectory); } else { fullBasePath = path_1.default.resolve(jsonFolder, options.baseDirectory); if (!fs_1.default.existsSync(fullBasePath)) { fullBasePath = path_1.default.resolve(process.cwd(), options.baseDirectory); } } const sourceLanguagePath = path_1.default.resolve(fullBasePath, options.inputLanguage); if (!fs_1.default.existsSync(sourceLanguagePath)) { throw new Error(`Source language path does not exist. sourceLanguagePath = ${sourceLanguagePath}`); } const sourceFilePaths = (0, utils_1.getAllFilesInPath)(sourceLanguagePath); const inputJSON = {}; for (const sourceFilePath of sourceFilePaths) { const fileContents = fs_1.default.readFileSync(sourceFilePath, "utf-8"); const fileJSON = JSON.parse(fileContents); const flatJSON = (0, flat_1.flatten)(fileJSON, { delimiter: constants_1.FLATTEN_DELIMITER, }); for (const key in flatJSON) { if (Object.prototype.hasOwnProperty.call(flatJSON, key)) { inputJSON[(0, utils_1.getTranslationDirectoryKey)(sourceFilePath, key, options.inputLanguage, options.outputLanguage)] = flatJSON[key]; } } } const inputLanguage = options.inputLanguage; let outputLanguage = ""; if (options.forceLanguageName) { outputLanguage = options.forceLanguageName; } else { outputLanguage = options.outputLanguage; } try { const outputJSON = (await (0, translate_1.translate)({ apiKey: options.apiKey, batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams: options.chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host: options.host, inputJSON, inputLanguage, model: options.model, outputLanguage, overridePrompt: options.overridePrompt, promptMode: options.promptMode, rateLimitMs: options.rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, verbose: options.verbose, })); const filesToJSON = {}; for (const pathWithKey in outputJSON) { if (Object.prototype.hasOwnProperty.call(outputJSON, pathWithKey)) { const filePath = pathWithKey.split(":").slice(0, -1).join(":"); if (!filesToJSON[filePath]) { filesToJSON[filePath] = {}; } const key = pathWithKey.split(":").pop(); filesToJSON[filePath][key] = outputJSON[pathWithKey]; } } for (const perFileJSON in filesToJSON) { if (Object.prototype.hasOwnProperty.call(filesToJSON, perFileJSON)) { const unflattenedOutput = (0, flat_1.unflatten)(filesToJSON[perFileJSON], { delimiter: constants_1.FLATTEN_DELIMITER, }); const outputText = JSON.stringify(unflattenedOutput, null, 4); if (!options.dryRun) { fs_1.default.mkdirSync((0, path_1.dirname)(perFileJSON), { recursive: true }); fs_1.default.writeFileSync(perFileJSON, `${outputText}\n`); } else { // TODO: find a cleaner way to get the input file from here // Might lead to a bug if the path has the language code multiple times const relativeOutputPath = path_1.default.relative(options.baseDirectory, perFileJSON); const input = fs_1.default.readFileSync(perFileJSON.replace(`/${outputLanguage}/`, `/${inputLanguage}/`), "utf-8"); const translationDiff = (0, diff_1.diffJson)(input, outputText); fs_1.default.mkdirSync((0, path_1.dirname)(`${options.dryRun.basePath}/${relativeOutputPath}`), { recursive: true }); fs_1.default.writeFileSync(`${options.dryRun.basePath}/${relativeOutputPath}`, outputText); const patch = (0, diff_1.createPatch)(perFileJSON, // Use the absolute path for the patch header input, outputText); fs_1.default.writeFileSync(`${options.dryRun.basePath}/${relativeOutputPath}.patch`, patch); (0, utils_1.printInfo)(`Wrote new JSON to ${options.dryRun.basePath}/${relativeOutputPath}`); (0, utils_1.printInfo)(`Wrote patch to ${options.dryRun.basePath}/${relativeOutputPath}.patch`); if (options.verbose) { for (const part of translationDiff) { const colorFns = { green: safe_1.default.green, grey: safe_1.default.grey, red: safe_1.default.red, }; let color; if (part.added) { color = "green"; } else if (part.removed) { color = "red"; } else { color = "grey"; } process.stderr.write(colorFns[color](part.value)); } } console.log(); } } } } catch (err) { (0, utils_1.printError)(`Failed to translate directory to ${outputLanguage}: ${err}`); throw err; } } /** * Wraps translateDiff to take the changed keys of all files in a directory * and write the translation of those keys in the target translation * @param options - The directory translation diff's options */ async function translateDirectoryDiff(options) { const jsonFolder = path_1.default.resolve(process.cwd(), "jsons"); let fullBasePath; if (path_1.default.isAbsolute(options.baseDirectory)) { fullBasePath = path_1.default.resolve(options.baseDirectory); } else { fullBasePath = path_1.default.resolve(jsonFolder, options.baseDirectory); if (!fs_1.default.existsSync(fullBasePath)) { fullBasePath = path_1.default.resolve(process.cwd(), options.baseDirectory); } } const sourceLanguagePathBefore = path_1.default.resolve(fullBasePath, options.inputFolderNameBefore); const sourceLanguagePathAfter = path_1.default.resolve(fullBasePath, options.inputFolderNameAfter); if (!fs_1.default.existsSync(sourceLanguagePathBefore)) { throw new Error(`Source language path before does not exist. sourceLanguagePathBefore = ${sourceLanguagePathBefore}`); } if (!fs_1.default.existsSync(sourceLanguagePathAfter)) { throw new Error(`Source language path after does not exist. sourceLanguagePathAfter = ${sourceLanguagePathAfter}`); } // TODO: abstract to fn const sourceFilePathsBefore = (0, utils_1.getAllFilesInPath)(sourceLanguagePathBefore); const inputJSONBefore = {}; for (const sourceFilePath of sourceFilePathsBefore) { const fileContents = fs_1.default.readFileSync(sourceFilePath, "utf-8"); const fileJSON = JSON.parse(fileContents); const flatJSON = (0, flat_1.flatten)(fileJSON, { delimiter: constants_1.FLATTEN_DELIMITER, }); for (const key in flatJSON) { if (Object.prototype.hasOwnProperty.call(flatJSON, key)) { inputJSONBefore[(0, utils_1.getTranslationDirectoryKey)(sourceFilePath, key, options.inputLanguageCode)] = flatJSON[key]; } } } const sourceFilePathsAfter = (0, utils_1.getAllFilesInPath)(sourceLanguagePathAfter); const inputJSONAfter = {}; for (const sourceFilePath of sourceFilePathsAfter) { const fileContents = fs_1.default.readFileSync(sourceFilePath, "utf-8"); const fileJSON = JSON.parse(fileContents); const flatJSON = (0, flat_1.flatten)(fileJSON, { delimiter: constants_1.FLATTEN_DELIMITER, }); for (const key in flatJSON) { if (Object.prototype.hasOwnProperty.call(flatJSON, key)) { inputJSONAfter[(0, utils_1.getTranslationDirectoryKey)(sourceFilePath.replace(options.inputFolderNameAfter, options.inputFolderNameBefore), key, options.inputLanguageCode)] = flatJSON[key]; } } } const outputLanguagePaths = fs_1.default .readdirSync(options.baseDirectory) .filter((folder) => folder !== path_1.default.basename(options.inputFolderNameBefore) && folder !== path_1.default.basename(options.inputFolderNameAfter)) .map((folder) => path_1.default.resolve(options.baseDirectory, folder)); const toUpdateJSONs = {}; for (const outputLanguagePath of outputLanguagePaths) { const files = (0, utils_1.getAllFilesInPath)(outputLanguagePath); for (const file of files) { const fileContents = fs_1.default.readFileSync(file, "utf-8"); const fileJSON = JSON.parse(fileContents); const flatJSON = (0, flat_1.flatten)(fileJSON, { delimiter: constants_1.FLATTEN_DELIMITER, }); const relative = path_1.default.relative(options.baseDirectory, outputLanguagePath); const segments = relative.split(path_1.default.sep).filter(Boolean); const language = segments[0]; if (!toUpdateJSONs[language]) { toUpdateJSONs[language] = {}; } for (const key in flatJSON) { if (Object.prototype.hasOwnProperty.call(flatJSON, key)) { toUpdateJSONs[language][(0, utils_1.getTranslationDirectoryKey)(`${fullBasePath}/${file.replace(outputLanguagePath, options.inputFolderNameBefore)}`, key, options.inputLanguageCode)] = flatJSON[key]; } } } } const inputLanguage = options.inputLanguageCode; try { const perLanguageOutputJSON = await (0, translate_1.translateDiff)({ apiKey: options.apiKey, batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams: options.chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host: options.host, inputJSONAfter, inputJSONBefore, inputLanguage, model: options.model, overridePrompt: options.overridePrompt, promptMode: options.promptMode, rateLimitMs: options.rateLimitMs, skipStylingVerification: options.skipStylingVerification, skipTranslationVerification: options.skipTranslationVerification, templatedStringPrefix: options.templatedStringPrefix, templatedStringSuffix: options.templatedStringSuffix, toUpdateJSONs, verbose: options.verbose, }); for (const outputLanguage in perLanguageOutputJSON) { if (Object.prototype.hasOwnProperty.call(perLanguageOutputJSON, outputLanguage)) { const filesToJSON = {}; const outputJSON = perLanguageOutputJSON[outputLanguage]; for (const pathWithKey in outputJSON) { if (Object.prototype.hasOwnProperty.call(outputJSON, pathWithKey)) { const beforeBaseName = path_1.default.basename(path_1.default.resolve(options.baseDirectory, options.inputFolderNameBefore)); const filePath = pathWithKey .split(":") .slice(0, -1) .join(":") .replace(`/${beforeBaseName}/`, `/${outputLanguage}/`); if (!filesToJSON[filePath]) { filesToJSON[filePath] = {}; } const key = pathWithKey.split(":").pop(); filesToJSON[filePath][key] = outputJSON[pathWithKey]; } } for (const perFileJSON in filesToJSON) { if (Object.prototype.hasOwnProperty.call(filesToJSON, perFileJSON)) { const unflattenedOutput = (0, flat_1.unflatten)(filesToJSON[perFileJSON], { delimiter: constants_1.FLATTEN_DELIMITER, }); const outputText = JSON.stringify(unflattenedOutput, null, 4); if (!options.dryRun) { fs_1.default.mkdirSync((0, path_1.dirname)(perFileJSON), { recursive: true, }); fs_1.default.writeFileSync(perFileJSON, `${outputText}\n`); } else { // TODO: find a cleaner way to get the input file from here // Might lead to a bug if the path has the language code multiple times const relativeOutputPath = path_1.default.relative(options.baseDirectory, perFileJSON.replace(`/${inputLanguage}/`, `/${outputLanguage}/`)); const input = fs_1.default.readFileSync(perFileJSON, "utf-8"); const translationDiff = (0, diff_1.diffJson)(input, outputText); fs_1.default.mkdirSync((0, path_1.dirname)(`${options.dryRun.basePath}/${relativeOutputPath}`), { recursive: true }); fs_1.default.writeFileSync(`${options.dryRun.basePath}/${relativeOutputPath}`, outputText); const patch = (0, diff_1.createPatch)(perFileJSON.replace(`/${inputLanguage}/`, `/${outputLanguage}/`), // Use the absolute path for the patch header input, outputText); fs_1.default.writeFileSync(`${options.dryRun.basePath}/${relativeOutputPath}.patch`, patch); (0, utils_1.printInfo)(`Wrote new JSON to ${options.dryRun.basePath}/${relativeOutputPath}`); (0, utils_1.printInfo)(`Wrote patch to ${options.dryRun.basePath}/${relativeOutputPath}.patch`); if (options.verbose) { for (const part of translationDiff) { const colorFns = { green: safe_1.default.green, grey: safe_1.default.grey, red: safe_1.default.red, }; let color; if (part.added) { color = "green"; } else if (part.removed) { color = "red"; } else { color = "grey"; } process.stderr.write(colorFns[color](part.value)); } } } console.log(); } } } } } catch (err) { (0, utils_1.printError)(`Failed to translate directory diff: ${err}`); throw err; } // Remove any files in before not in after const fileNamesBefore = sourceFilePathsBefore.map((x) => x.slice(sourceLanguagePathBefore.length)); const fileNamesAfter = sourceFilePathsAfter.map((x) => x.slice(sourceLanguagePathAfter.length)); const removedFiles = fileNamesBefore.filter((x) => !fileNamesAfter.includes(x)); for (const languagePath of outputLanguagePaths) { for (const removedFile of removedFiles) { const removedFilePath = languagePath + removedFile; fs_1.default.rmSync(removedFilePath); // Recursively cleanup parent folders if they're also empty let folder = path_1.default.dirname(removedFilePath); while (fs_1.default.readdirSync(folder).length === 0) { const parentFolder = path_1.default.resolve(folder, ".."); fs_1.default.rmdirSync(folder); folder = parentFolder; } } } } //# sourceMappingURL=translate_directory.js.map