UNPKG

i18n-ai-translate

Version:

Use LLMs to translate your i18n JSON to any language.

311 lines (273 loc) 11.2 kB
import { createPatch, diffJson } from "diff"; import { getLanguageCodeFromFilename, printError, printInfo } from "./utils"; import { translate, translateDiff } from "./translate"; import colors from "colors/safe"; import fs from "fs"; import path from "path"; import type TranslateFileDiffOptions from "./interfaces/translate_file_diff_options"; import type TranslateFileOptions from "./interfaces/translate_file_options"; /** * Wraps translate to take an input file and output its translation to another file * @param options - The file translation's options */ export async function translateFile( options: TranslateFileOptions, ): Promise<void> { let inputJSON = {}; try { const inputFile = fs.readFileSync(options.inputFilePath, "utf-8"); inputJSON = JSON.parse(inputFile); } catch (e) { printError(`Invalid input JSON: ${e}`); return; } const inputLanguage = getLanguageCodeFromFilename(options.inputFilePath); let outputLanguage = ""; if (options.forceLanguageName) { outputLanguage = options.forceLanguageName; } else { outputLanguage = getLanguageCodeFromFilename(options.outputFilePath); } try { const outputJSON = await 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 outputText = JSON.stringify(outputJSON, null, 4); if (!options.dryRun) { fs.writeFileSync(options.outputFilePath, `${outputText}\n`); } else { const translationDiff = diffJson(inputJSON, outputJSON); fs.writeFileSync( `${options.dryRun.basePath}/${path.basename(options.outputFilePath)}.new.json`, outputText, ); const patch = createPatch( options.outputFilePath, JSON.stringify(inputJSON, null, 4), outputText, ); fs.writeFileSync( `${options.dryRun.basePath}/${path.basename(options.outputFilePath)}.patch`, patch, ); printInfo( `Wrote new JSON to ${options.dryRun.basePath}/${path.basename(options.outputFilePath)}.new.json`, ); printInfo( `Wrote patch to ${options.dryRun.basePath}/${path.basename(options.outputFilePath)}.patch`, ); if (options.verbose) { for (const part of translationDiff) { const colorFns = { green: colors.green, grey: colors.grey, red: colors.red, } as const; type ColorKey = keyof typeof colorFns; let color: ColorKey; 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) { printError(`Failed to translate file to ${outputLanguage}: ${err}`); throw err; } } /** * Wraps translateDiff to take two versions of a source file and update * the target translation's file by only modifying keys that changed in the source * @param options - The file diff translation's options */ export async function translateFileDiff( options: TranslateFileDiffOptions, ): Promise<void> { // Get all the *json files from the same path as beforeInputPath const outputFilesOrPaths = fs .readdirSync(path.dirname(options.inputBeforeFileOrPath)) .filter((file: string) => file.endsWith(".json")) .filter( (file) => file !== path.basename(options.inputBeforeFileOrPath) && file !== path.basename(options.inputAfterFileOrPath), ) .map((file) => path.resolve(path.dirname(options.inputBeforeFileOrPath), file), ); const jsonFolder = path.resolve(process.cwd(), "jsons"); let inputBeforePath: string; let inputAfterPath: string; if (path.isAbsolute(options.inputBeforeFileOrPath)) { inputBeforePath = path.resolve(options.inputBeforeFileOrPath); } else { inputBeforePath = path.resolve( jsonFolder, options.inputBeforeFileOrPath, ); if (!fs.existsSync(inputBeforePath)) { inputBeforePath = path.resolve( process.cwd(), options.inputBeforeFileOrPath, ); } } if (path.isAbsolute(options.inputAfterFileOrPath)) { inputAfterPath = path.resolve(options.inputAfterFileOrPath); } else { inputAfterPath = path.resolve(jsonFolder, options.inputAfterFileOrPath); } const outputPaths: Array<string> = []; for (const outputFileOrPath of outputFilesOrPaths) { let outputPath: string; if (path.isAbsolute(outputFileOrPath)) { outputPath = path.resolve(outputFileOrPath); } else { outputPath = path.resolve(jsonFolder, outputFileOrPath); if (!fs.existsSync(jsonFolder)) { outputPath = path.resolve(process.cwd(), outputFileOrPath); } } outputPaths.push(outputPath); } let inputBeforeJSON = {}; let inputAfterJSON = {}; try { let inputFile = fs.readFileSync(inputBeforePath, "utf-8"); inputBeforeJSON = JSON.parse(inputFile); inputFile = fs.readFileSync(inputAfterPath, "utf-8"); inputAfterJSON = JSON.parse(inputFile); } catch (e) { printError(`Invalid input JSON: ${e}`); return; } const toUpdateJSONs: { [language: string]: Object } = {}; const languageCodeToOutputPath: { [language: string]: string } = {}; for (const outputPath of outputPaths) { const languageCode = getLanguageCodeFromFilename( path.basename(outputPath), ); if (!languageCode) { throw new Error( "Invalid output file name. Use a valid ISO 639-1 language code as the file name.", ); } try { const outputFile = fs.readFileSync(outputPath, "utf-8"); toUpdateJSONs[languageCode] = JSON.parse(outputFile); languageCodeToOutputPath[languageCode] = outputPath; } catch (e) { printError(`Invalid output JSON: ${e}`); } } try { const outputJSON = await translateDiff({ apiKey: options.apiKey, batchMaxTokens: options.batchMaxTokens, batchSize: options.batchSize, chatParams: options.chatParams, engine: options.engine, ensureChangedTranslation: options.ensureChangedTranslation, host: options.host, inputJSONAfter: inputAfterJSON, inputJSONBefore: inputBeforeJSON, inputLanguage: options.inputLanguageCode, 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 language in outputJSON) { if (Object.prototype.hasOwnProperty.call(outputJSON, language)) { const outputText = JSON.stringify( outputJSON[language], null, 4, ); const inputJSON = toUpdateJSONs[language]; const outputPath = languageCodeToOutputPath[language]; if (!options.dryRun) { fs.writeFileSync(outputPath, `${outputText}\n`); } else { const translationDiff = diffJson( inputJSON, outputJSON[language], ); fs.writeFileSync( `${options.dryRun.basePath}/${path.basename(outputPath)}.new.json`, outputText, ); const patch = createPatch( outputPath, JSON.stringify(inputJSON, null, 4), outputText, ); fs.writeFileSync( `${options.dryRun.basePath}/${path.basename(outputPath)}.patch`, patch, ); printInfo( `Wrote new JSON to ${options.dryRun.basePath}/${path.basename(outputPath)}.new.json`, ); printInfo( `Wrote patch to ${options.dryRun.basePath}/${path.basename(outputPath)}.patch`, ); if (options.verbose) { for (const part of translationDiff) { const colorFns = { green: colors.green, grey: colors.grey, red: colors.red, } as const; type ColorKey = keyof typeof colorFns; let color: ColorKey; 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) { printError(`Failed to translate file diff: ${err}`); throw err; } }