i18n-ai-translate
Version:
Use LLMs to translate your i18n JSON to any language.
376 lines • 19.5 kB
JavaScript
;
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