const pLimit = require("p-limit");
/**
* Represents a utility class for addons.
* @constructor
* @param {Object} objectToApply - The object to apply the utility methods and properties to.
* @param {boolean} [override=false] - Indicates whether to override existing properties in the object.
*/
const AddonUtils = function(objectToApply={}, override = false) {
if(new.target === undefined) {
// if called directly
return new AddonUtils(objectToApply, override)
}
this.alwaysExportFromStage = false;
this.workerNum = 4;
this.defaultInjectCopyOption = "copyIfNotExist"
if (objectToApply) {
for (let i in this) {
if (!override) {
if (typeof objectToApply[i] !== "undefined") continue;
} else {
if (typeof objectToApply[i] !== "undefined") console.log("%cOverriding", "color:yellow", i)
}
if (typeof this[i] == "function") {
objectToApply[i] = this[i].bind(objectToApply);
} else {
objectToApply[i] = this[i];
}
}
}
}
/**
* Object representing translation data.
* @typedef {Object} TransData
* @property {string[][]} data - An array of arrays of strings representing translation data.
* @property {string[][]} context - An array of arrays of strings representing context data.
* @property {string[][]} tags - An array of arrays of strings representing tags data.
* @property {Object[]} parameters - An array of objects representing parameters data.
* @property {Object} indexIds - An object representing index IDs.
*/
/**
* Retrieves translation data for a given file.
* @async
* @function getTransData
* @memberof AddonUtils
* @param {string} file - The name of the file.
* @param {string} basePath - The base path for the file.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<TransData>} A promise resolving to the file data object containing data, context, tags, parameters, and indexIds.
*/
AddonUtils.prototype.getTransData = async function(file, basePath, options={}) {
// replace with your logic
// should return fileData object
const transData = {
"data": [[]],
"context": [[]],
"tags": [],
"parameters": [],
"indexIds": {}
}
return transData;
}
/**
* Writes a file with given translation.
* @async
* @function writeFile
* @memberof AddonUtils
* @param {string} file - The name of the file.
* @param {string} basePath - The base path for the file.
* @param {string} targetDir - The target directory.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<string>} A promise resolving to the path of the written file.
*/
AddonUtils.prototype.writeFile = async function(file, basePath, targetDir, options={}) {
// replace with your logic
return nwPath.join(basePath, file)
}
/**
* Creates translation data for multiple files.
* @async
* @function createTransData
* @memberof AddonUtils
* @param {string[]} files - An array of file names.
* @param {string} basePath - The base path for the files.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<TransData>} A promise resolving to the translation data object.
*/
AddonUtils.prototype.createTransData = async function(files, basePath, options={}) {
const transFiles = {}
const queue = []
const limit = pLimit(this.workerNum || 4);
ui.log(`Processing with ${this.workerNum || 4} concurrent worker(s).`);
for (let file of files) {
queue.push(limit(async ()=>{
let thisFileData = await this.getTransData(file, basePath, options) || {};
// skip if no value is presents
if (!thisFileData) return;
let thisFileObj = trans.createFileData(file, {
originalFormat: await this.setFileFormatName(file, basePath, options) || "Translator++ Original Format"
},
{
leadSlash:true
}
);
transFiles[thisFileObj.path] = {...thisFileObj, ...thisFileData};
}));
}
await Promise.all(queue);
const transData = trans.initTransData()
console.log("%c initializing trans data", "color:aqua");
transData.project.gameEngine = await this.setEngineName(transData, basePath, options) || this.gameEngine,
transData.project.files = common.sortObjectByKey(transFiles);
transData.project.parser = this.parserName || this.package?.name,
transData.project.parserVersion = this.parserVersion || this.package?.version,
transData.project.options.init = options.init;
transData.project.gameTitle = await this.setProjectTitle(transData, basePath, options);
return transData
}
AddonUtils.prototype.setFileFormatName = async function(file, basePath, options={}) {
const ext = nwPath.extname(file);
if (ext) {
return ext.substr(1).toUpperCase()+" file"
}
return;
}
AddonUtils.prototype.setEngineName = async function(transData, basePath, options={}) {
return;
}
AddonUtils.prototype.setProjectTitle = async function(transData, basePath, options={}) {
return options?.init?.projectTitle || "untitled project";
}
AddonUtils.prototype.setProjectLocation = async function(transData, basePath, options={}) {
// add with your logic
return;
}
AddonUtils.prototype.finalizeTransData = async function(transData, basePath, options={}) {
// add with your logic
return transData;
}
/**
* Creates a project based on provided files and options.
* @async
* @function createProject
* @memberof AddonUtils
* @param {string[]} files - An array of file names.
* @param {string} basePath - The base path for the files.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<Object>} A promise resolving to the created project data.
*/
AddonUtils.prototype.createProject = async function(files, basePath, options={}) {
console.log("Creating project with args", arguments);
var transData = await this.createTransData.call(this, files, basePath, options);
ui.log("Create staging data")
var stagingInfo;
if (options.noCache) {
stagingInfo = await common.initStaging(transData.project.projectId, options?.init?.projectTitle, {noCache:true});
} else {
stagingInfo = await common.initStaging(transData.project.projectId, options?.init?.projectTitle, {...options.init, ...{basePath:basePath, files:files}});
}
console.log("stagging Info :", stagingInfo);
transData.project.cache = stagingInfo;
transData.project.loc ||= (await this.setProjectLocation(transData, basePath, options)) || basePath;
console.log("transData:", transData);
transData = (await this.finalizeTransData(transData, basePath, options)) || transData;
trans.openFromTransObj(transData, {isNew: true});
return transData;
}
AddonUtils.prototype.selectTranslationData = function(translationDatas, path) {
const blankResult = {
info:{},
translationPair:{}
}
if (!translationDatas?.translationData?.[path]) return blankResult;
return translationDatas.translationData[path];
}
AddonUtils.prototype.beforeExportToFolder = async function(destinationDir, transData, options) {
// replace with your logic
}
AddonUtils.prototype.afterExportToFolder = async function(exportResult, destinationDir, transData, options) {
// replace with your logic
}
/**
* Exports translation data to a folder.
* @async
* @function exportToFolder
* @memberof AddonUtils
* @param {string} destinationDir - The directory to export to.
* @param {Object} [transData] - The translation data object. If not provided, fetched from the current context.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<string[]>} A promise resolving to an array of exported file paths.
*/
AddonUtils.prototype.exportToFolder = async function(destinationDir, transData, options) {
console.log("Export to folder with arguments", arguments);
transData = transData || trans.getSaveData();
options = options || {};
options.translationDatas = options.translationDatas || trans.getTranslationData(transData, options);
options.resourcePath ||= trans.getStagingDataPath(transData);
// if none is selected then select all except references
if (!options.files?.length) options.files = trans.getAllFiles(transData, true);
await this.beforeExportToFolder(destinationDir, transData, options);
const queue = []
const limit = pLimit(this.workerNum || 4);
const result = []
for (let file of options.files) {
if (!await common.isFileAsync(nwPath.join(options.resourcePath, file))) {
ui.logError("File not found: "+nwPath.join(options.resourcePath, file));
continue;
}
queue.push(limit(async ()=>{
let thisOptions = {
translationDatas: options.translationDatas,
parserOptions: {
writeMode: true,
},
projectInfo: {
init: transData?.project?.options?.init
},
exportOptions: options.exportOptions
}
let thisResult = await this.writeFile(file, options.resourcePath, destinationDir, thisOptions);
result.push(thisResult)
}));
}
await Promise.all(queue);
await this.afterExportToFolder(result, destinationDir, transData, options);
return result;
}
/**
* Exports translation data as a zip file.
* @async
* @function exportAsZip
* @memberof AddonUtils
* @param {string} targetPath - The target path for the zip file.
* @param {Object} [transData] - The translation data object. If not provided, fetched from the current context.
* @param {Object} [options={}] - Additional options.
* @returns {Promise<string>} A promise resolving to the path of the created zip file.
*/
AddonUtils.prototype.exportAsZip = async function(targetPath, transData, options) {
transData = transData || trans.getSaveData();
options = options || {};
options.translationDatas = options.translationDatas || trans.getTranslationData(transData, options);
options.resourcePath ||= trans.getStagingDataPath(transData);
var tmpPath = nwPath.join(nw.process.env.TMP, transData.project.projectId);
await ui.log("Generating a temporary folder", tmpPath);
const fse = require("fs-extra");
try {
await fse.remove(tmpPath);
await common.mkDir(tmpPath, {recursive:true});
} catch(e) {
console.warn("Unable to create directory ", tmpPath);
console.error(e);
return;
}
await ui.log("Exporting to a temporary folder", tmpPath);
await this.exportToFolder(tmpPath, transData, options);
await ui.log("Data exported to the temp dir");
await ui.log("Zipping temporary data");
var _7z = require('7zip-min');
return new Promise((resolve, reject) => {
_7z.cmd(['a', '-tzip', targetPath, tmpPath+"\\*"], err => {
resolve(targetPath);
});
})
}
/**
* Handles exporting translation data based on the specified mode.
* @async
* @function handleExport
* @memberof AddonUtils
* @param {string} targetPath - The target path for the exported data.
* @param {Object} options - Export options.
* @param {string} options.mode - The export mode ('zip' or 'directory').
* @returns {Promise<void>} A promise resolving when export is completed.
*/
AddonUtils.prototype.handleExport = async function(targetPath, options) {
ui.showLoading();
if (options.mode == "zip") {
ui.loadingProgress("Processing", "Exporting as a zip", {consoleOnly:false, mode:'consoleOutput'});
ui.log("Target path: ", targetPath);
ui.log("Exporting with options: ", options);
await this.exportAsZip(targetPath, undefined, options);
} else if (options.mode == "dir") {
ui.loadingProgress("Processing", "Exporting to directory", {consoleOnly:false, mode:'consoleOutput'});
ui.log("Target directory: ", targetPath);
ui.log("Exporting with options: ", options);
await this.exportToFolder(targetPath, undefined, options);
}
ui.loadingProgress("Completed", "Exported completed!", {consoleOnly:false, mode:'consoleOutput'});
ui.loadingEnd();
}
AddonUtils.prototype.injectCopyMaterials = async function(sourceDir, targetDir, injectOption) {
const bCopy = require("better-copy");
injectOption.copyOptions ||= this.defaultInjectCopyOption;
if (injectOption.copyOptions == "copyNothing") return;
await ui.log(`Copy ${sourceDir} to ${targetDir}`);
await ui.log("Copy option:"+injectOption.copyOptions);
var overWrite = true;
if (injectOption.copyOptions == "copyIfNotExist") overWrite = false;
await ui.log("Overwrite?: "+overWrite.toString());
await bCopy(sourceDir, targetDir, {
onBeforeCopy: async (from, to) => {
await ui.log(`Start copying ${from}`);
},
onAfterCopy: async (from, to) => {
await ui.log(`Successfully copied into ${to}`);
},
overwrite: overWrite
});
}
AddonUtils.prototype.injectDone = async function(targetDir, sourceMaterial, options) {
// do something here
ui.loadingProgress("Completed", "Exported completed!", {consoleOnly:false, mode:'consoleOutput'});
ui.loadingEnd();
return common.halt();
}
// Static
AddonUtils.applyCommonHandlers = function(supportedEngines, thisAddon) {
engines.addHandler(supportedEngines, 'exportHandler', async function(targetPath, options) {
if (options.mode !== "dir" && options.mode !== "zip") return false;
await thisAddon.handleExport(targetPath, options);
return common.halt();
})
engines.addHandler(supportedEngines, 'onOpenInjectDialog', async function($dialogInject, options) {
$dialogInject.find(".copyOptionsBlock").removeClass("hidden")
})
engines.addHandler(supportedEngines, 'injectHandler', async function(targetDir, sourceMaterial, options) {
ui.showLoading();
ui.loadingProgress("Processing", "Injecting to directory", {consoleOnly:false, mode:'consoleOutput'});
ui.log("Target directory: ", targetDir);
ui.log("Source Material: ", sourceMaterial);
if (sourceMaterial) ui.log("Warning: The app will try to apply translations to the files with the exact match relative location.");
await thisAddon.injectCopyMaterials(sourceMaterial, targetDir, options);
ui.log("Injecting with options: ", options);
console.log("self.alwaysExportFromStage", thisAddon.alwaysExportFromStage);
if (!thisAddon.alwaysExportFromStage) {
options.resourcePath = sourceMaterial;
}
await thisAddon.exportToFolder(targetDir, undefined, options);
return await thisAddon.injectDone(targetDir, sourceMaterial, options)
});
}
module.exports = AddonUtils;