translate.js 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158
  1. /**
  2. * @description MeshCentral MeshAgent
  3. * @author Ylian Saint-Hilaire
  4. * @copyright Intel Corporation 2019-2021
  5. * @license Apache-2.0
  6. * @version v0.0.1
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. const os = require('os');
  11. //const zlib = require('zlib');
  12. var performCheck = false;
  13. var translationTable = null;
  14. var sourceStrings = null;
  15. var jsdom = null; //require('jsdom');
  16. var esprima = null; //require('esprima'); // https://www.npmjs.com/package/esprima
  17. var minifyLib = 2; // 0 = None, 1 = minify-js, 2 = HTMLMinifier
  18. var minify = null;
  19. var meshCentralSourceFiles = [
  20. "../views/agentinvite.handlebars",
  21. "../views/invite.handlebars",
  22. "../views/default.handlebars",
  23. "../views/default3.handlebars",
  24. "../views/default-mobile.handlebars",
  25. "../views/download.handlebars",
  26. "../views/download2.handlebars",
  27. "../views/error404.handlebars",
  28. "../views/error404-mobile.handlebars",
  29. "../views/login.handlebars",
  30. "../views/login2.handlebars",
  31. "../views/login-mobile.handlebars",
  32. "../views/terms.handlebars",
  33. "../views/terms-mobile.handlebars",
  34. "../views/xterm.handlebars",
  35. "../views/message.handlebars",
  36. "../views/message2.handlebars",
  37. "../views/messenger.handlebars",
  38. "../views/player.handlebars",
  39. "../views/sharing.handlebars",
  40. "../views/sharing-mobile.handlebars",
  41. "../views/mstsc.handlebars",
  42. "../views/ssh.handlebars",
  43. "../emails/account-check.html",
  44. "../emails/account-invite.html",
  45. "../emails/account-login.html",
  46. "../emails/account-reset.html",
  47. "../emails/mesh-invite.html",
  48. "../emails/device-notify.html",
  49. "../emails/device-help.html",
  50. "../emails/account-check.txt",
  51. "../emails/account-invite.txt",
  52. "../emails/account-login.txt",
  53. "../emails/account-reset.txt",
  54. "../emails/mesh-invite.txt",
  55. "../emails/device-notify.txt",
  56. "../emails/device-help.txt",
  57. "../emails/sms-messages.txt",
  58. "../agents/agent-translations.json",
  59. "../agents/modules_meshcore/coretranslations.json"
  60. ];
  61. var minifyMeshCentralSourceFiles = [
  62. "../views/agentinvite.handlebars",
  63. "../views/invite.handlebars",
  64. "../views/default.handlebars",
  65. "../views/default3.handlebars",
  66. "../views/default-mobile.handlebars",
  67. "../views/download.handlebars",
  68. "../views/download2.handlebars",
  69. "../views/error404.handlebars",
  70. "../views/error4042.handlebars",
  71. "../views/error404-mobile.handlebars",
  72. "../views/login.handlebars",
  73. "../views/login2.handlebars",
  74. "../views/login-mobile.handlebars",
  75. "../views/terms.handlebars",
  76. "../views/terms-mobile.handlebars",
  77. "../views/xterm.handlebars",
  78. "../views/message.handlebars",
  79. "../views/message2.handlebars",
  80. "../views/messenger.handlebars",
  81. "../views/player.handlebars",
  82. "../views/sharing.handlebars",
  83. "../views/sharing-mobile.handlebars",
  84. "../views/mstsc.handlebars",
  85. "../views/ssh.handlebars",
  86. "../public/scripts/agent-desktop-0.0.2.js",
  87. "../public/scripts/agent-rdp-0.0.1.js",
  88. "../public/scripts/agent-redir-rtc-0.1.0.js",
  89. "../public/scripts/agent-redir-ws-0.1.1.js",
  90. "../public/scripts/amt-0.2.0.js",
  91. "../public/scripts/amt-desktop-0.0.2.js",
  92. "../public/scripts/amt-ider-ws-0.0.1.js",
  93. "../public/scripts/amt-redir-ws-0.1.0.js",
  94. "../public/scripts/amt-script-0.2.0.js",
  95. "../public/scripts/amt-setupbin-0.1.0.js",
  96. "../public/scripts/amt-terminal-0.0.2.js",
  97. "../public/scripts/amt-wsman-0.2.0.js",
  98. "../public/scripts/amt-wsman-ws-0.2.0.js",
  99. "../public/scripts/common-0.0.1.js",
  100. "../public/scripts/meshcentral.js",
  101. "../public/scripts/u2f-api.js",
  102. "../public/scripts/xterm-addon-fit.js",
  103. "../public/scripts/xterm.js",
  104. "../public/scripts/zlib-adler32.js",
  105. "../public/scripts/zlib-crc32.js",
  106. "../public/scripts/zlib-inflate.js",
  107. "../public/scripts/zlib.js"
  108. ];
  109. // True is this module is run directly using NodeJS
  110. var directRun = (require.main === module);
  111. // Check NodeJS version
  112. const NodeJSVer = Number(process.version.match(/^v(\d+\.\d+)/)[1]);
  113. if (directRun && (NodeJSVer < 8)) { log("Translate.js requires Node v8 or above, current version is " + process.version + "."); return; }
  114. // node translate.json CHECK ../meshcentral/views/default.handlebars
  115. // node translate.json EXTRACT bob.json ../meshcentral/views/default.handlebars
  116. // node translate.js TRANSLATE fr test2.json ../meshcentral/views/default.handlebars
  117. var worker = null;
  118. function log() {
  119. if (worker == null) {
  120. console.log(...arguments);
  121. } else {
  122. worker.parentPort.postMessage({ msg: arguments[0] })
  123. }
  124. }
  125. if (directRun && (NodeJSVer >= 12)) {
  126. const xworker = require('worker_threads');
  127. try {
  128. if (xworker.isMainThread == false) {
  129. // We are being called to do some work
  130. worker = xworker;
  131. const op = worker.workerData.op;
  132. const args = worker.workerData.args;
  133. // Get things setup
  134. jsdom = require('jsdom');
  135. esprima = require('esprima'); // https://www.npmjs.com/package/esprima
  136. if (minifyLib == 1) { log("minify-js is no longer used, please switch to \"html-minifier-terser\""); process.exit(); return; }
  137. if (minifyLib == 2) { minify = require('html-minifier-terser').minify; } // https://www.npmjs.com/package/html-minifier
  138. switch (op) {
  139. case 'translate': {
  140. translateSingleThreaded(args[0], args[1], args[2], args[3]);
  141. break;
  142. }
  143. }
  144. return;
  145. }
  146. } catch (ex) { log(ex); }
  147. }
  148. if (directRun) { setup(); }
  149. function setup() {
  150. var libs = ['[email protected]', '[email protected]'];
  151. if (minifyLib == 1) { log("minify-js is no longer used, please switch to \"html-minifier-terser\""); process.exit(); return; }
  152. if (minifyLib == 2) { libs.push('[email protected]'); }
  153. InstallModules(libs, start);
  154. }
  155. function start() { startEx(process.argv); }
  156. async function startEx(argv) {
  157. // Load dependencies
  158. jsdom = require('jsdom');
  159. esprima = require('esprima'); // https://www.npmjs.com/package/esprima
  160. if (minifyLib == 1) { log("minify-js is no longer used, please switch to \"html-minifier-terser\""); process.exit(); return; }
  161. if (minifyLib == 2) { minify = require('html-minifier-terser').minify; } // https://www.npmjs.com/package/html-minifier-terser
  162. var command = null;
  163. if (argv.length > 2) { command = argv[2].toLowerCase(); }
  164. if (['minify', 'check', 'extract', 'extractall', 'translate', 'translateall', 'minifyall', 'minifydir', 'merge', 'totext', 'fromtext', 'remove'].indexOf(command) == -1) { command = null; }
  165. if (directRun) { log('MeshCentral web site translator'); }
  166. if (command == null) {
  167. log('Usage "node translate.js [command] [options]');
  168. log('Possible commands:');
  169. log('');
  170. log(' CHECK [files]');
  171. log(' Check will pull string out of a web page and display a report.');
  172. log('');
  173. log(' EXTRACT [languagefile] [files]');
  174. log(' Extract strings from web pages and generate a language (.json) file.');
  175. log('');
  176. log(' EXTRACTALL (languagefile)');
  177. log(' Extract all MeshCentral strings from web pages and generate the languages.json file.');
  178. log('');
  179. log(' TRANSLATE [language] [languagefile] [files]');
  180. log(' Use a language (.json) file to translate web pages to a give language.');
  181. log('');
  182. log(' TRANSLATEALL (languagefile) (language code)');
  183. log(' Translate all MeshCentral strings using the languages.json file.');
  184. log('');
  185. log(' MINIFY [sourcefile]');
  186. log(' Minify a single file.');
  187. log('');
  188. log(' MINIFYDIR [sourcedir] [destinationdir]');
  189. log(' Minify all files in a directory.');
  190. log('');
  191. log(' MINIFYALL');
  192. log(' Minify the main MeshCentral english web pages.');
  193. log('');
  194. log(' MERGE [sourcefile] [targetfile] [language code]');
  195. log(' Merge a language from a translation file into another translation file.');
  196. log('');
  197. log(' TOTEXT [translationfile] [textfile] [language code]');
  198. log(' Save a text for with all strings of a given language.');
  199. log('');
  200. log(' FROMTEXT [translationfile] [textfile] [language code]');
  201. log(' Import raw text string as translations for a language code.');
  202. process.exit();
  203. return;
  204. }
  205. // Extract strings from web pages and display a report
  206. if (command == 'check') {
  207. var sources = [];
  208. for (var i = 3; i < argv.length; i++) { if (fs.existsSync(argv[i]) == false) { log('Missing file: ' + argv[i]); process.exit(); return; } sources.push(argv[i]); }
  209. if (sources.length == 0) { log('No source files specified.'); process.exit(); return; }
  210. performCheck = true;
  211. sourceStrings = {};
  212. for (var i = 0; i < sources.length; i++) { extractFromHtml(sources[i]); }
  213. var count = 0;
  214. for (var i in sourceStrings) { count++; }
  215. log('Extracted ' + count + ' strings.');
  216. process.exit();
  217. return;
  218. }
  219. // Extract strings from web pages
  220. if (command == 'extract') {
  221. if (argv.length < 4) { log('No language file specified.'); process.exit(); return; }
  222. var sources = [];
  223. for (var i = 4; i < argv.length; i++) { if (fs.existsSync(argv[i]) == false) { log('Missing file: ' + argv[i]); process.exit(); return; } sources.push(argv[i]); }
  224. if (sources.length == 0) { log('No source files specified.'); process.exit(); return; }
  225. extract(argv[3], sources);
  226. }
  227. // Save a text file with all the strings for a given language
  228. if (command == 'totext') {
  229. if ((argv.length == 6)) {
  230. if (fs.existsSync(argv[3]) == false) { log('Unable to find: ' + argv[3]); return; }
  231. totext(argv[3], argv[4], argv[5]);
  232. } else {
  233. log('Usage: TOTEXT [translationfile] [textfile] [language code]');
  234. }
  235. return;
  236. }
  237. // Read a text file and use it as translation for a given language
  238. if (command == 'fromtext') {
  239. if ((argv.length == 6)) {
  240. if (fs.existsSync(argv[3]) == false) { log('Unable to find: ' + argv[3]); return; }
  241. if (fs.existsSync(argv[4]) == false) { log('Unable to find: ' + argv[4]); return; }
  242. fromtext(argv[3], argv[4], argv[5]);
  243. } else {
  244. log('Usage: FROMTEXT [translationfile] [textfile] [language code]');
  245. }
  246. return;
  247. }
  248. // Merge one language from a language file into another language file.
  249. if (command == 'merge') {
  250. if ((argv.length == 6)) {
  251. if (fs.existsSync(argv[3]) == false) { log('Unable to find: ' + argv[3]); return; }
  252. if (fs.existsSync(argv[4]) == false) { log('Unable to find: ' + argv[4]); return; }
  253. merge(argv[3], argv[4], argv[5]);
  254. } else {
  255. log('Usage: MERGE [sourcefile] [tartgetfile] [language code]');
  256. }
  257. return;
  258. }
  259. // Extract or translate all MeshCentral strings
  260. if (command == 'extractall') {
  261. if (argv.length > 4) { lang = argv[4].toLowerCase(); }
  262. var translationFile = 'translate.json';
  263. if (argv.length > 3) {
  264. if (fs.existsSync(argv[3]) == false) { log('Unable to find: ' + argv[3]); return; } else { translationFile = argv[3]; }
  265. }
  266. extract(translationFile, meshCentralSourceFiles, translationFile);
  267. }
  268. // Remove a language from a translation file
  269. if (command == 'remove') {
  270. if (argv.length <= 3) { log('Usage: remove [language] (file)'); return; }
  271. lang = argv[3].toLowerCase();
  272. var translationFile = 'translate.json';
  273. if (argv.length > 4) {
  274. if (fs.existsSync(argv[4]) == false) { log('Unable to find: ' + argv[4]); return; } else { translationFile = argv[4]; }
  275. }
  276. sourceStrings = {};
  277. if (fs.existsSync(translationFile) == false) { log('Unable to find: ' + translationFile); return; }
  278. var langFileData = null;
  279. try { langFileData = JSON.parse(fs.readFileSync(translationFile)); } catch (ex) { }
  280. if ((langFileData == null) || (langFileData.strings == null)) { log("Invalid language file."); process.exit(); return; }
  281. for (var i in langFileData.strings) { delete langFileData.strings[i][lang]; }
  282. fs.writeFileSync(translationFile, translationsToJson({ strings: langFileData.strings }), { flag: 'w+' });
  283. log("Done.");
  284. return;
  285. }
  286. if (command == 'translateall') {
  287. if (fs.existsSync('../views/translations') == false) { fs.mkdirSync('../views/translations'); }
  288. //if (fs.existsSync('../public/translations') == false) { fs.mkdirSync('../public/translations'); }
  289. var lang = null;
  290. if (argv.length > 4) { lang = argv[4].toLowerCase(); }
  291. if (argv.length > 3) {
  292. if (fs.existsSync(argv[3]) == false) {
  293. log('Unable to find: ' + argv[3]);
  294. } else {
  295. translate(lang, argv[3], meshCentralSourceFiles, 'translations');
  296. }
  297. } else {
  298. if (fs.existsSync('translate.json') == false) {
  299. log('Unable to find translate.json.');
  300. } else {
  301. translate(lang, 'translate.json', meshCentralSourceFiles, 'translations');
  302. }
  303. }
  304. return;
  305. }
  306. // Translate web pages to a given language given a language file
  307. if (command == 'translate') {
  308. if (argv.length < 4) { log("No language specified."); process.exit(); return; }
  309. if (argv.length < 5) { log("No language file specified."); process.exit(); return; }
  310. var lang = argv[3].toLowerCase();
  311. var langFile = argv[4];
  312. if (fs.existsSync(langFile) == false) { log("Missing language file: " + langFile); process.exit(); return; }
  313. var sources = [], subdir = null;
  314. for (var i = 5; i < argv.length; i++) {
  315. if (argv[i].startsWith('--subdir:')) {
  316. subdir = argv[i].substring(9);
  317. } else {
  318. if (fs.existsSync(argv[i]) == false) { log("Missing file: " + argv[i]); process.exit(); return; } sources.push(argv[i]);
  319. }
  320. }
  321. if (sources.length == 0) { log("No source files specified."); process.exit(); return; }
  322. translate(lang, langFile, sources, subdir);
  323. }
  324. if (command == 'minifydir') {
  325. if (argv.length < 4) { log("Command source and/or destination folders missing."); process.exit(); return; }
  326. const sourceFiles = fs.readdirSync(argv[3]);
  327. for (var i in sourceFiles) {
  328. if (sourceFiles[i].endsWith('.js') || sourceFiles[i].endsWith('.json')) {
  329. console.log("Processing " + sourceFiles[i] + "...");
  330. const sourceFile = path.join(argv[3], sourceFiles[i]);
  331. if (sourceFiles[i].endsWith('.js')) {
  332. // Minify the file .js file
  333. const destinationFile = path.join(argv[4], sourceFiles[i].substring(0, sourceFiles[i].length - 3) + '.min.js');
  334. if (minifyLib == 2) {
  335. var inFile = fs.readFileSync(sourceFile).toString();
  336. // Perform minification pre-processing
  337. if (sourceFile.endsWith('.handlebars') >= 0) { inFile = inFile.split('{{{pluginHandler}}}').join('"{{{pluginHandler}}}"'); }
  338. if (sourceFile.endsWith('.js')) { inFile = '<script>' + inFile + '</script>'; }
  339. var minifiedOut = await minify(inFile, {
  340. collapseBooleanAttributes: true,
  341. collapseInlineTagWhitespace: false, // This is not good.
  342. collapseWhitespace: true,
  343. minifyCSS: true,
  344. minifyJS: true,
  345. removeComments: true,
  346. removeOptionalTags: true,
  347. removeEmptyAttributes: true,
  348. removeAttributeQuotes: true,
  349. removeRedundantAttributes: true,
  350. removeScriptTypeAttributes: true,
  351. removeTagWhitespace: true,
  352. preserveLineBreaks: false,
  353. useShortDoctype: true
  354. });
  355. // Perform minification post-processing
  356. if (sourceFile.endsWith('.js')) { minifiedOut = minifiedOut.substring(8, minifiedOut.length - 9); }
  357. if (sourceFile.endsWith('.handlebars') >= 0) { minifiedOut = minifiedOut.split('"{{{pluginHandler}}}"').join('{{{pluginHandler}}}'); }
  358. fs.writeFileSync(destinationFile, minifiedOut, { flag: 'w+' });
  359. }
  360. } else if (sourceFiles[i].endsWith('.json')) {
  361. // Minify the file .json file
  362. const destinationFile = path.join(argv[4], sourceFiles[i]);
  363. var inFile = JSON.parse(fs.readFileSync(sourceFile).toString());
  364. fs.writeFileSync(destinationFile, JSON.stringify(inFile), { flag: 'w+' });
  365. }
  366. }
  367. }
  368. }
  369. if (command == 'minifyall') {
  370. for (var i in minifyMeshCentralSourceFiles) {
  371. var outname = minifyMeshCentralSourceFiles[i];
  372. var outnamemin = null;
  373. if (outname.endsWith('.handlebars')) {
  374. outnamemin = (outname.substring(0, outname.length - 11) + '-min.handlebars');
  375. } else if (outname.endsWith('.html')) {
  376. outnamemin = (outname.substring(0, outname.length - 5) + '-min.html');
  377. } else if (outname.endsWith('.htm')) {
  378. outnamemin = (outname.substring(0, outname.length - 4) + '-min.htm');
  379. } else if (outname.endsWith('.js')) {
  380. outnamemin = (outname.substring(0, outname.length - 3) + '-min.js');
  381. } else {
  382. outnamemin = (outname, outname + '.min');
  383. }
  384. log('Generating ' + outnamemin + '...');
  385. /*
  386. // Minify the file
  387. if (minifyLib == 1) {
  388. minify.file({
  389. file: outname,
  390. dist: outnamemin
  391. }, function (e, compress) {
  392. if (e) { log('ERROR ', e); return done(); }
  393. compress.run((e) => { e ? log('Minification fail', e) : log('Minification success'); minifyDone(); });
  394. }
  395. );
  396. }
  397. */
  398. // Minify the file
  399. if (minifyLib == 2) {
  400. var inFile = fs.readFileSync(outname).toString();
  401. // Perform minification pre-processing
  402. if (outname.endsWith('.handlebars') >= 0) { inFile = inFile.split('{{{pluginHandler}}}').join('"{{{pluginHandler}}}"'); }
  403. if (outname.endsWith('.js')) { inFile = '<script>' + inFile + '</script>'; }
  404. var minifiedOut = null;
  405. try {
  406. minifiedOut = await minify(inFile, {
  407. collapseBooleanAttributes: true,
  408. collapseInlineTagWhitespace: false, // This is not good.
  409. collapseWhitespace: true,
  410. minifyCSS: true,
  411. minifyJS: true,
  412. removeComments: true,
  413. removeOptionalTags: true,
  414. removeEmptyAttributes: true,
  415. removeAttributeQuotes: true,
  416. removeRedundantAttributes: true,
  417. removeScriptTypeAttributes: true,
  418. removeTagWhitespace: true,
  419. preserveLineBreaks: false,
  420. log: function(a) { if (typeof a !== 'string') { console.log(a); } } // Log errors from UglifyJS to console output
  421. });
  422. } catch (ex) {
  423. console.log(ex);
  424. }
  425. // Perform minification post-processing
  426. if (outname.endsWith('.js')) { minifiedOut = minifiedOut.substring(8, minifiedOut.length - 9); }
  427. if (outname.endsWith('.handlebars') >= 0) { minifiedOut = minifiedOut.split('"{{{pluginHandler}}}"').join('{{{pluginHandler}}}'); }
  428. fs.writeFileSync(outnamemin, minifiedOut, { flag: 'w+' });
  429. /*
  430. if (outname.endsWith('.js')) {
  431. var compressHandler = function compressHandlerFunc(err, buffer, outnamemin2) {
  432. if (err == null) {
  433. console.log('GZIP', compressHandlerFunc.outname);
  434. fs.writeFileSync(compressHandlerFunc.outname, buffer, { flag: 'w+' });
  435. }
  436. };
  437. compressHandler.outname = outnamemin;
  438. zlib.gzip(Buffer.from(minifiedOut), compressHandler);
  439. } else {
  440. fs.writeFileSync(outnamemin, minifiedOut, { flag: 'w+' });
  441. }
  442. */
  443. }
  444. }
  445. }
  446. if (command == 'minify') {
  447. var outname = argv[3];
  448. var outnamemin = null;
  449. if (outname.endsWith('.handlebars')) {
  450. outnamemin = (outname.substring(0, outname.length - 11) + '-min.handlebars');
  451. } else if (outname.endsWith('.html')) {
  452. outnamemin = (outname.substring(0, outname.length - 5) + '-min.html');
  453. } else if (outname.endsWith('.htm')) {
  454. outnamemin = (outname.substring(0, outname.length - 4) + '-min.htm');
  455. } else if (outname.endsWith('.js')) {
  456. outnamemin = (outname.substring(0, outname.length - 3) + '-min.js');
  457. } else {
  458. outnamemin = (outname, outname + '.min');
  459. }
  460. log('Generating ' + path.basename(outnamemin) + '...');
  461. // Minify the file
  462. if (minifyLib == 2) {
  463. var inFile = fs.readFileSync(outname).toString()
  464. // Perform minification pre-processing
  465. if (outname.endsWith('.handlebars') >= 0) { inFile = inFile.split('{{{pluginHandler}}}').join('"{{{pluginHandler}}}"'); }
  466. if (outname.endsWith('.js')) { inFile = '<script>' + inFile + '</script>'; }
  467. var minifiedOut = await minify(inFile, {
  468. collapseBooleanAttributes: true,
  469. collapseInlineTagWhitespace: false, // This is not good.
  470. collapseWhitespace: true,
  471. minifyCSS: true,
  472. minifyJS: true,
  473. removeComments: true,
  474. removeOptionalTags: true,
  475. removeEmptyAttributes: true,
  476. removeAttributeQuotes: true,
  477. removeRedundantAttributes: true,
  478. removeScriptTypeAttributes: true,
  479. removeTagWhitespace: true,
  480. preserveLineBreaks: false,
  481. useShortDoctype: true
  482. });
  483. // Perform minification post-processing
  484. if (outname.endsWith('.js')) { minifiedOut = minifiedOut.substring(8, minifiedOut.length - 9); }
  485. if (outname.endsWith('.handlebars') >= 0) { minifiedOut = minifiedOut.split('"{{{pluginHandler}}}"').join('{{{pluginHandler}}}'); }
  486. fs.writeFileSync(outnamemin, minifiedOut, { flag: 'w+' });
  487. }
  488. }
  489. }
  490. function totext(source, target, lang) {
  491. // Load the source language file
  492. var sourceLangFileData = null;
  493. try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
  494. if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
  495. log('Writing ' + lang + '...');
  496. // Generate raw text
  497. var output = [];
  498. var outputCharCount = 0; // Google has a 5000 character limit
  499. var splitOutput = [];
  500. var splitOutputPtr = 1;
  501. var count = 0;
  502. for (var i in sourceLangFileData.strings) {
  503. if ((sourceLangFileData.strings[i][lang] != null) && (sourceLangFileData.strings[i][lang].indexOf('\r') == -1) && (sourceLangFileData.strings[i][lang].indexOf('\n') == -1)) {
  504. output.push(sourceLangFileData.strings[i][lang]);
  505. outputCharCount += (sourceLangFileData.strings[i][lang].length + 2);
  506. if (outputCharCount > 4500) { outputCharCount = 0; splitOutputPtr++; }
  507. if (splitOutput[splitOutputPtr] == null) { splitOutput[splitOutputPtr] = []; }
  508. splitOutput[splitOutputPtr].push(sourceLangFileData.strings[i][lang]);
  509. } else {
  510. output.push('');
  511. outputCharCount += 2;
  512. if (outputCharCount > 4500) { outputCharCount = 0; splitOutputPtr++; }
  513. if (splitOutput[splitOutputPtr] == null) { splitOutput[splitOutputPtr] = []; }
  514. splitOutput[splitOutputPtr].push('');
  515. }
  516. count++;
  517. }
  518. if (splitOutputPtr == 1) {
  519. // Save the target back
  520. fs.writeFileSync(target + '-' + lang + '.txt', output.join(os.EOL), { flag: 'w+' });
  521. log('Done.');
  522. } else {
  523. // Save the text in 1000 string bunches
  524. for (var i in splitOutput) {
  525. log('Writing ' + target + '-' + lang + '-' + i + '.txt...');
  526. fs.writeFileSync(target + '-' + lang + '-' + i + '.txt', splitOutput[i].join(os.EOL), { flag: 'w+' });
  527. }
  528. log('Done.');
  529. }
  530. }
  531. function fromtext(source, target, lang) {
  532. // Load the source language file
  533. var sourceLangFileData = null;
  534. try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
  535. if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
  536. log('Updating ' + lang + '...');
  537. // Read raw text
  538. var rawText = fs.readFileSync(target).toString('utf8');
  539. var rawTextArray = rawText.split(/\r?\n/);
  540. var rawTextPtr = 0;
  541. log('Translation file: ' + sourceLangFileData.strings.length + ' string(s)');
  542. log('Text file: ' + rawTextArray.length + ' string(s)');
  543. if (sourceLangFileData.strings.length != rawTextArray.length) { log('String count mismatch, unable to import.'); process.exit(1); return; }
  544. var output = [];
  545. var splitOutput = [];
  546. for (var i in sourceLangFileData.strings) {
  547. if ((sourceLangFileData.strings[i]['en'] != null) && (sourceLangFileData.strings[i]['en'].indexOf('\r') == -1) && (sourceLangFileData.strings[i]['en'].indexOf('\n') == -1)) {
  548. if (sourceLangFileData.strings[i][lang] == null) { sourceLangFileData.strings[i][lang] = rawTextArray[i]; }
  549. }
  550. }
  551. fs.writeFileSync(source + '-new', translationsToJson(sourceLangFileData), { flag: 'w+' });
  552. log('Done.');
  553. }
  554. function merge(source, target, lang) {
  555. // Load the source language file
  556. var sourceLangFileData = null;
  557. try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
  558. if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
  559. // Load the target language file
  560. var targetLangFileData = null;
  561. try { targetLangFileData = JSON.parse(fs.readFileSync(target)); } catch (ex) { console.log(ex); }
  562. if ((targetLangFileData == null) || (targetLangFileData.strings == null)) { log("Invalid target language file."); process.exit(); return; }
  563. log('Merging ' + lang + '...');
  564. // Index the target file
  565. var index = {};
  566. for (var i in targetLangFileData.strings) { if (targetLangFileData.strings[i].en != null) { index[targetLangFileData.strings[i].en] = targetLangFileData.strings[i]; } }
  567. // Merge the translation
  568. for (var i in sourceLangFileData.strings) {
  569. if ((sourceLangFileData.strings[i].en != null) && (sourceLangFileData.strings[i][lang] != null) && (index[sourceLangFileData.strings[i].en] != null)) {
  570. //if (sourceLangFileData.strings[i][lang] == null) {
  571. index[sourceLangFileData.strings[i].en][lang] = sourceLangFileData.strings[i][lang];
  572. //}
  573. }
  574. }
  575. // Deindex the new target file
  576. var targetData = { strings: [] };
  577. for (var i in index) { targetData.strings.push(index[i]); }
  578. // Save the target back
  579. fs.writeFileSync(target, translationsToJson(targetData), { flag: 'w+' });
  580. log('Done.');
  581. }
  582. function translate(lang, langFile, sources, createSubDir) {
  583. if (directRun && (NodeJSVer >= 12) && (lang == null)) {
  584. // Multi threaded translation
  585. log("Multi-threaded translation.");
  586. // Load the language file
  587. var langFileData = null;
  588. try { langFileData = JSON.parse(fs.readFileSync(langFile)); } catch (ex) { console.log(ex); }
  589. if ((langFileData == null) || (langFileData.strings == null)) { log("Invalid language file."); process.exit(); return; }
  590. langs = {};
  591. for (var i in langFileData.strings) { var entry = langFileData.strings[i]; for (var j in entry) { if ((j != 'en') && (j != 'xloc') && (j != '*')) { langs[j.toLowerCase()] = true; } } }
  592. var Worker = require('worker_threads').Worker;
  593. var MAX_WORKERS = os.cpus().length; // limit to the number of CPU cores for now
  594. var activeWorkers = 0;
  595. var taskQueue = [];
  596. function processNextTask() {
  597. if (activeWorkers < MAX_WORKERS && taskQueue.length > 0) {
  598. var nextTask = taskQueue.shift();
  599. activeWorkers++;
  600. var worker = new Worker('./translate.js', { stdout: true, workerData: { op: 'translate', args: [nextTask.lang, nextTask.langFile, nextTask.sources, nextTask.createSubDir] } });
  601. worker.stdout.on('data', function (msg) { console.log('wstdio:', msg.toString()); });
  602. worker.on('message', function (message) { console.log(message.msg); });
  603. worker.on('error', function (error) { console.log('error', error); activeWorkers--; processNextTask(); });
  604. worker.on('exit', function (code) { /*console.log('exit', code);*/ activeWorkers--; processNextTask(); });
  605. }
  606. }
  607. for (var lang in langs) {
  608. if (langs.hasOwnProperty(lang)) {
  609. taskQueue.push({ lang: lang, langFile: langFile, sources: sources, createSubDir: createSubDir});
  610. }
  611. }
  612. for (var i = 0; i < Math.min(MAX_WORKERS, taskQueue.length); i++) { processNextTask(); }
  613. } else {
  614. // Single threaded translation
  615. translateSingleThreaded(lang, langFile, sources, createSubDir);
  616. }
  617. // Translate any JSON files
  618. for (var i = 0; i < sources.length; i++) { if (sources[i].endsWith('.json')) { translateAllInJson(lang, langFile, sources[i]); } }
  619. }
  620. function translateSingleThreaded(lang, langFile, sources, createSubDir) {
  621. // Load the language file
  622. var langFileData = null;
  623. try { langFileData = JSON.parse(fs.readFileSync(langFile)); } catch (ex) { }
  624. if ((langFileData == null) || (langFileData.strings == null)) { log("Invalid language file."); process.exit(); return; }
  625. if ((lang != null) && (lang != '*')) {
  626. // Translate a single language
  627. translateEx(lang, langFileData, sources, createSubDir);
  628. } else {
  629. // See that languages are in the translation file
  630. langs = {};
  631. for (var i in langFileData.strings) { var entry = langFileData.strings[i]; for (var j in entry) { if ((j != 'en') && (j != 'xloc') && (j != '*')) { langs[j.toLowerCase()] = true; } } }
  632. for (var i in langs) { translateEx(i, langFileData, sources, createSubDir); }
  633. }
  634. //process.exit();
  635. return;
  636. }
  637. function translateEx(lang, langFileData, sources, createSubDir) {
  638. // Build translation table, simple source->target for the given language.
  639. translationTable = {};
  640. for (var i in langFileData.strings) {
  641. var entry = langFileData.strings[i];
  642. if ((entry['en'] != null) && (entry[lang] != null)) { translationTable[entry['en']] = entry[lang]; }
  643. }
  644. // Translate the files
  645. for (var i = 0; i < sources.length; i++) {
  646. if (sources[i].endsWith('.html') || sources[i].endsWith('.htm') || sources[i].endsWith('.handlebars')) { translateFromHtml(lang, sources[i], createSubDir); }
  647. else if (sources[i].endsWith('.txt')) { translateFromTxt(lang, sources[i], createSubDir); }
  648. }
  649. }
  650. function extract(langFile, sources) {
  651. sourceStrings = {};
  652. if (fs.existsSync(langFile) == true) {
  653. var langFileData = null;
  654. try { langFileData = JSON.parse(fs.readFileSync(langFile)); } catch (ex) { }
  655. if ((langFileData == null) || (langFileData.strings == null)) { log("Invalid language file."); process.exit(); return; }
  656. for (var i in langFileData.strings) {
  657. sourceStrings[langFileData.strings[i]['en']] = langFileData.strings[i];
  658. delete sourceStrings[langFileData.strings[i]['en']].xloc;
  659. }
  660. }
  661. for (var i = 0; i < sources.length; i++) {
  662. if (sources[i].endsWith('.html') || sources[i].endsWith('.htm') || sources[i].endsWith('.handlebars')) { extractFromHtml(sources[i]); }
  663. else if (sources[i].endsWith('.txt')) { extractFromTxt(sources[i]); }
  664. else if (sources[i].endsWith('.json')) { extractFromJson(sources[i]); }
  665. }
  666. var count = 0, output = [];
  667. for (var i in sourceStrings) {
  668. count++;
  669. sourceStrings[i]['en'] = i;
  670. //if ((sourceStrings[i].xloc != null) && (sourceStrings[i].xloc.length > 0)) { output.push(sourceStrings[i]); } // Only save results that have a source location.
  671. output.push(sourceStrings[i]); // Save all results
  672. }
  673. fs.writeFileSync(langFile, translationsToJson({ strings: output }), { flag: 'w+' });
  674. log(format("{0} strings in output file.", count));
  675. //process.exit();
  676. return;
  677. }
  678. function extractFromTxt(file) {
  679. log("Processing TXT: " + path.basename(file));
  680. var lines = fs.readFileSync(file).toString().split(/\r?\n/);
  681. var name = path.basename(file);
  682. for (var i in lines) {
  683. var line = lines[i];
  684. if ((line.length > 1) && (line[0] != '~')) {
  685. if (sourceStrings[line] == null) { sourceStrings[line] = { en: line, xloc: [name] }; } else { if (sourceStrings[line].xloc == null) { sourceStrings[line].xloc = []; } sourceStrings[line].xloc.push(name); }
  686. }
  687. }
  688. }
  689. function extractFromJson(file) {
  690. log("Processing JSON: " + path.basename(file));
  691. var json = JSON.parse(fs.readFileSync(file).toString());
  692. var name = path.basename(file);
  693. if (json.en == null) return;
  694. for (var i in json.en) {
  695. if (typeof json.en[i] == 'string') {
  696. const str = json.en[i]
  697. if (sourceStrings[str] == null) {
  698. sourceStrings[str] = { en: str, xloc: [name] };
  699. } else {
  700. if (sourceStrings[str].xloc == null) { sourceStrings[str].xloc = []; } sourceStrings[str].xloc.push(name);
  701. }
  702. } else if (Array.isArray(json.en[i])) {
  703. for (var k in json.en[i]) {
  704. if (typeof json.en[i][k] == 'string') {
  705. const str = json.en[i][k];
  706. if (sourceStrings[str] == null) { sourceStrings[str] = { en: str, xloc: [name] }; } else { if (sourceStrings[str].xloc == null) { sourceStrings[str].xloc = []; } sourceStrings[str].xloc.push(name); }
  707. }
  708. }
  709. }
  710. }
  711. }
  712. function extractFromHtml(file) {
  713. var data = fs.readFileSync(file);
  714. var { JSDOM } = jsdom;
  715. const dom = new JSDOM(data, { includeNodeLocations: true });
  716. log("Processing HTML: " + path.basename(file));
  717. getStringsHtml(path.basename(file), dom.window.document.querySelector('body'));
  718. }
  719. function getStringsHtml(name, node) {
  720. for (var i = 0; i < node.childNodes.length; i++) {
  721. var subnode = node.childNodes[i];
  722. // Check if the "value" attribute exists and needs to be translated
  723. var subnodeignore = false;
  724. var subnodevalueignore = false;
  725. if ((subnode.attributes != null) && (subnode.attributes.length > 0)) {
  726. var subnodevalue = null, subnodeplaceholder = null, subnodetitle = null;
  727. for (var j in subnode.attributes) {
  728. if ((subnode.attributes[j].name == 'notrans') && (subnode.attributes[j].value == '1')) { subnodeignore = true; }
  729. if ((subnode.attributes[j].name == 'notransval') && (subnode.attributes[j].value == '1')) { subnodevalueignore = true; }
  730. if ((subnode.attributes[j].name == 'type') && (subnode.attributes[j].value == 'hidden')) { subnodeignore = true; }
  731. if (subnode.attributes[j].name == 'value') { subnodevalue = subnode.attributes[j].value; }
  732. if (subnode.attributes[j].name == 'placeholder') { subnodeplaceholder = subnode.attributes[j].value; }
  733. if (subnode.attributes[j].name == 'title') { subnodetitle = subnode.attributes[j].value; }
  734. }
  735. if ((subnodevalue != null) && isNumber(subnodevalue) == true) { subnodevalue = null; }
  736. if ((subnodeplaceholder != null) && isNumber(subnodeplaceholder) == true) { subnodeplaceholder = null; }
  737. if ((subnodetitle != null) && isNumber(subnodetitle) == true) { subnodetitle = null; }
  738. if ((subnodeignore == false) && (subnodevalueignore == false) && (subnodevalue != null)) {
  739. // Add a new string to the list (value)
  740. if (sourceStrings[subnodevalue] == null) { sourceStrings[subnodevalue] = { en: subnodevalue, xloc: [name] }; } else { if (sourceStrings[subnodevalue].xloc == null) { sourceStrings[subnodevalue].xloc = []; } sourceStrings[subnodevalue].xloc.push(name); }
  741. }
  742. if (subnodeplaceholder != null) {
  743. // Add a new string to the list (placeholder)
  744. if (sourceStrings[subnodeplaceholder] == null) { sourceStrings[subnodeplaceholder] = { en: subnodeplaceholder, xloc: [name] }; } else { if (sourceStrings[subnodeplaceholder].xloc == null) { sourceStrings[subnodeplaceholder].xloc = []; } sourceStrings[subnodeplaceholder].xloc.push(name); }
  745. }
  746. if (subnodetitle != null) {
  747. // Add a new string to the list (title)
  748. if (sourceStrings[subnodetitle] == null) { sourceStrings[subnodetitle] = { en: subnodetitle, xloc: [name] }; } else { if (sourceStrings[subnodetitle].xloc == null) { sourceStrings[subnodetitle].xloc = []; } sourceStrings[subnodetitle].xloc.push(name); }
  749. }
  750. }
  751. if (subnodeignore == false) {
  752. // Check the content of the element
  753. var subname = subnode.id;
  754. if (subname == null || subname == '') { subname = i; }
  755. if (subnode.hasChildNodes()) {
  756. getStringsHtml(name + '->' + subname, subnode);
  757. } else {
  758. if (subnode.nodeValue == null) continue;
  759. var nodeValue = subnode.nodeValue.trim().split('\\r').join('').split('\\n').join('').trim();
  760. if ((nodeValue.length > 0) && (subnode.nodeType == 3)) {
  761. if ((node.tagName != 'SCRIPT') && (node.tagName != 'STYLE') && (nodeValue.length < 8000) && (nodeValue.startsWith('{{{') == false) && (nodeValue != ' ')) {
  762. if (performCheck) { log(' "' + nodeValue + '"'); }
  763. // Add a new string to the list
  764. if (sourceStrings[nodeValue] == null) { sourceStrings[nodeValue] = { en: nodeValue, xloc: [name] }; } else { if (sourceStrings[nodeValue].xloc == null) { sourceStrings[nodeValue].xloc = []; } sourceStrings[nodeValue].xloc.push(name); }
  765. } else if (node.tagName == 'SCRIPT') {
  766. // Parse JavaScript
  767. getStringFromJavaScript(name, subnode.nodeValue);
  768. }
  769. }
  770. }
  771. }
  772. }
  773. }
  774. function getStringFromJavaScript(name, script) {
  775. if (performCheck) { log(format('Processing JavaScript of {0} bytes: {1}', script.length, name)); }
  776. var tokenScript = esprima.tokenize(script), count = 0;
  777. for (var i in tokenScript) {
  778. var token = tokenScript[i];
  779. if ((token.type == 'String') && (token.value.length > 2) && (token.value[0] == '"')) {
  780. var str = token.value.substring(1, token.value.length - 1);
  781. //if (performCheck) { log(' ' + name + '->' + (++count), token.value); }
  782. if (performCheck) { log(' ' + token.value); }
  783. if (sourceStrings[str] == null) { sourceStrings[str] = { en: str, xloc: [name + '->' + (++count)] }; } else { if (sourceStrings[str].xloc == null) { sourceStrings[str].xloc = []; } sourceStrings[str].xloc.push(name + '->' + (++count)); }
  784. }
  785. }
  786. }
  787. function translateFromTxt(lang, file, createSubDir) {
  788. log("Translating TXT (" + lang + "): " + path.basename(file));
  789. var lines = fs.readFileSync(file).toString().split(/\r?\n/), outlines = [];
  790. for (var i in lines) {
  791. var line = lines[i];
  792. if ((line.length > 1) && (line[0] != '~')) {
  793. if (translationTable[line] != null) { outlines.push(translationTable[line]); } else { outlines.push(line); }
  794. } else {
  795. outlines.push(line);
  796. }
  797. }
  798. var outname = file, out = outlines.join(os.EOL);
  799. if (createSubDir != null) {
  800. var outfolder = path.join(path.dirname(file), createSubDir);
  801. if (fs.existsSync(outfolder) == false) { fs.mkdirSync(outfolder); }
  802. outname = path.join(path.dirname(file), createSubDir, path.basename(file));
  803. }
  804. outname = (outname.substring(0, outname.length - 4) + '_' + lang + '.txt');
  805. fs.writeFileSync(outname, out, { flag: 'w+' });
  806. }
  807. function translateAllInJson(xlang, langFile, file) {
  808. log("Translating JSON (" + ((xlang == null)?'All':xlang) + "): " + path.basename(file));
  809. // Load the language file
  810. var langFileData = null;
  811. try { langFileData = JSON.parse(fs.readFileSync(langFile)); } catch (ex) { console.log(ex); }
  812. if ((langFileData == null) || (langFileData.strings == null)) { log("Invalid language file."); process.exit(); return; }
  813. var languages = [];
  814. // Build translation table, simple source->target for the given language.
  815. var xtranslationTable = {};
  816. for (var i in langFileData.strings) {
  817. var entry = langFileData.strings[i];
  818. for (var lang in entry) {
  819. if ((lang == 'en') || (lang == 'xloc')) continue;
  820. if ((xlang != null) && (lang != xlang)) continue;
  821. if (languages.indexOf(lang) == -1) { languages.push(lang); xtranslationTable[lang] = {}; }
  822. if ((entry['en'] != null) && (entry[lang] != null)) { xtranslationTable[lang][entry['en']] = entry[lang]; }
  823. }
  824. }
  825. // Load and translate
  826. var json = JSON.parse(fs.readFileSync(file).toString());
  827. if (json.en != null) {
  828. for (var j in languages) {
  829. var lang = languages[j];
  830. for (var i in json.en) {
  831. if ((typeof json.en[i] == 'string') && (xtranslationTable[lang][json.en[i]] != null)) {
  832. // Translate a string
  833. if (json[lang] == null) { json[lang] = {}; }
  834. json[lang][i] = xtranslationTable[lang][json.en[i]];
  835. } else if (Array.isArray(json.en[i])) {
  836. // Translate an array of strings
  837. var r = [], translateCount = 0;
  838. for (var k in json.en[i]) {
  839. var str = json.en[i][k];
  840. if (xtranslationTable[lang][str] != null) { r.push(xtranslationTable[lang][str]); translateCount++; } else { r.push(str); }
  841. }
  842. if (translateCount > 0) { json[lang][i] = r; }
  843. }
  844. }
  845. }
  846. }
  847. // Save the results
  848. fs.writeFileSync(file, JSON.stringify(json, null, 2), { flag: 'w+' });
  849. }
  850. async function translateFromHtml(lang, file, createSubDir) {
  851. var data = fs.readFileSync(file);
  852. if (file.endsWith('.js')) { data = '<html><head></head><body><script>' + data + '</script></body></html>'; }
  853. var { JSDOM } = jsdom;
  854. const dom = new JSDOM(data, { includeNodeLocations: true });
  855. log("Translating HTML (" + lang + "): " + path.basename(file));
  856. translateStrings(path.basename(file), dom.window.document.querySelector('body'));
  857. var out = dom.serialize();
  858. // Change the <html lang="en"> tag.
  859. out = out.split('<html lang="en"').join('<html lang="' + lang + '"');
  860. var outname = file;
  861. var outnamemin = null;
  862. if (createSubDir != null) {
  863. var outfolder = path.join(path.dirname(file), createSubDir);
  864. if (fs.existsSync(outfolder) == false) { fs.mkdirSync(outfolder); }
  865. outname = path.join(path.dirname(file), createSubDir, path.basename(file));
  866. }
  867. if (outname.endsWith('.handlebars')) {
  868. outnamemin = (outname.substring(0, outname.length - 11) + '-min_' + lang + '.handlebars');
  869. outname = (outname.substring(0, outname.length - 11) + '_' + lang + '.handlebars');
  870. } else if (outname.endsWith('.html')) {
  871. outnamemin = (outname.substring(0, outname.length - 5) + '-min_' + lang + '.html');
  872. outname = (outname.substring(0, outname.length - 5) + '_' + lang + '.html');
  873. } else if (outname.endsWith('.htm')) {
  874. outnamemin = (outname.substring(0, outname.length - 4) + '-min_' + lang + '.htm');
  875. outname = (outname.substring(0, outname.length - 4) + '_' + lang + '.htm');
  876. } else if (outname.endsWith('.js')) {
  877. if (out.startsWith('<html><head></head><body><script>')) { out = out.substring(33); }
  878. if (out.endsWith('</script></body></html>')) { out = out.substring(0, out.length - 23); }
  879. outnamemin = (outname.substring(0, outname.length - 3) + '-min_' + lang + '.js');
  880. outname = (outname.substring(0, outname.length - 3) + '_' + lang + '.js');
  881. } else {
  882. outnamemin = (outname + '_' + lang + '.min');
  883. outname = (outname + '_' + lang);
  884. }
  885. fs.writeFileSync(outname, out, { flag: 'w+' });
  886. // Minify the file
  887. if (minifyLib == 1) {
  888. minify.file({
  889. file: outname,
  890. dist: outnamemin
  891. }, function(e, compress) {
  892. if (e) { log('ERROR ', e); return done(); }
  893. compress.run((e) => { e ? log('Minification fail', e) : log('Minification success'); minifyDone(); });
  894. }
  895. );
  896. }
  897. // Minify the file
  898. if (minifyLib == 2) {
  899. if (outnamemin.endsWith('.handlebars') >= 0) { out = out.split('{{{pluginHandler}}}').join('"{{{pluginHandler}}}"'); }
  900. var minifiedOut = await minify(out, {
  901. collapseBooleanAttributes: true,
  902. collapseInlineTagWhitespace: false, // This is not good.
  903. collapseWhitespace: true,
  904. minifyCSS: true,
  905. minifyJS: true,
  906. removeComments: true,
  907. removeOptionalTags: true,
  908. removeEmptyAttributes: true,
  909. removeAttributeQuotes: true,
  910. removeRedundantAttributes: true,
  911. removeScriptTypeAttributes: true,
  912. removeTagWhitespace: true,
  913. preserveLineBreaks: false,
  914. useShortDoctype: true
  915. });
  916. if (outnamemin.endsWith('.handlebars') >= 0) { minifiedOut = minifiedOut.split('"{{{pluginHandler}}}"').join('{{{pluginHandler}}}'); }
  917. fs.writeFileSync(outnamemin, minifiedOut, { flag: 'w+' });
  918. }
  919. }
  920. function minifyDone() { log('Completed minification.'); }
  921. function translateStrings(name, node) {
  922. for (var i = 0; i < node.childNodes.length; i++) {
  923. var subnode = node.childNodes[i];
  924. // Check if the "value" attribute exists and needs to be translated
  925. var subnodeignore = false;
  926. if ((subnode.attributes != null) && (subnode.attributes.length > 0)) {
  927. var subnodevalue = null, subnodeindex = null, subnodeplaceholder = null, subnodeplaceholderindex = null, subnodetitle = null, subnodetitleindex = null;
  928. for (var j in subnode.attributes) {
  929. if ((subnode.attributes[j].name == 'notrans') && (subnode.attributes[j].value == '1')) { subnodeignore = true; }
  930. if ((subnode.attributes[j].name == 'type') && (subnode.attributes[j].value == 'hidden')) { subnodeignore = true; }
  931. if (subnode.attributes[j].name == 'value') { subnodevalue = subnode.attributes[j].value; subnodeindex = j; }
  932. if (subnode.attributes[j].name == 'placeholder') { subnodeplaceholder = subnode.attributes[j].value; subnodeplaceholderindex = j; }
  933. if (subnode.attributes[j].name == 'title') { subnodetitle = subnode.attributes[j].value; subnodetitleindex = j; }
  934. }
  935. if ((subnodevalue != null) && isNumber(subnodevalue) == true) { subnodevalue = null; }
  936. if ((subnodeplaceholder != null) && isNumber(subnodeplaceholder) == true) { subnodeplaceholder = null; }
  937. if ((subnodetitle != null) && isNumber(subnodetitle) == true) { subnodetitle = null; }
  938. if ((subnodeignore == false) && (subnodevalue != null)) {
  939. // Perform attribute translation for value
  940. if (translationTable[subnodevalue] != null) { subnode.attributes[subnodeindex].value = translationTable[subnodevalue]; }
  941. }
  942. if (subnodeplaceholder != null) {
  943. // Perform attribute translation for placeholder
  944. if (translationTable[subnodeplaceholder] != null) { subnode.attributes[subnodeplaceholderindex].value = translationTable[subnodeplaceholder]; }
  945. }
  946. if (subnodetitle != null) {
  947. // Perform attribute translation for title
  948. if (translationTable[subnodetitle] != null) { subnode.attributes[subnodetitleindex].value = translationTable[subnodetitle]; }
  949. }
  950. }
  951. if (subnodeignore == false) {
  952. var subname = subnode.id;
  953. if (subname == null || subname == '') { subname = i; }
  954. if (subnode.hasChildNodes()) {
  955. translateStrings(name + '->' + subname, subnode);
  956. } else {
  957. if (subnode.nodeValue == null) continue;
  958. var nodeValue = subnode.nodeValue.trim().split('\\r').join('').split('\\n').join('').trim();
  959. // Look for the front trim
  960. var frontTrim = '', backTrim = '';;
  961. var x1 = subnode.nodeValue.indexOf(nodeValue);
  962. if (x1 > 0) { frontTrim = subnode.nodeValue.substring(0, x1); }
  963. if (x1 != -1) { backTrim = subnode.nodeValue.substring(x1 + nodeValue.length); }
  964. if ((nodeValue.length > 0) && (subnode.nodeType == 3)) {
  965. if ((node.tagName != 'SCRIPT') && (node.tagName != 'STYLE') && (nodeValue.length < 8000) && (nodeValue.startsWith('{{{') == false) && (nodeValue != ' ')) {
  966. // Check if we have a translation for this string
  967. if (translationTable[nodeValue]) { subnode.nodeValue = (frontTrim + translationTable[nodeValue] + backTrim); }
  968. } else if (node.tagName == 'SCRIPT') {
  969. // Translate JavaScript
  970. subnode.nodeValue = translateStringsFromJavaScript(name, subnode.nodeValue);
  971. }
  972. }
  973. }
  974. }
  975. }
  976. }
  977. function translateStringsFromJavaScript(name, script) {
  978. if (performCheck) { log(format('Translating JavaScript of {0} bytes: {1}', script.length, name)); }
  979. var tokenScript = esprima.tokenize(script, { range: true }), count = 0;
  980. var output = [], ptr = 0;
  981. for (var i in tokenScript) {
  982. var token = tokenScript[i];
  983. if ((token.type == 'String') && (token.value.length > 2) && (token.value[0] == '"')) {
  984. var str = token.value.substring(1, token.value.length - 1);
  985. if (translationTable[str]) {
  986. output.push(script.substring(ptr, token.range[0]));
  987. output.push('"' + translationTable[str] + '"');
  988. ptr = token.range[1];
  989. }
  990. }
  991. }
  992. output.push(script.substring(ptr));
  993. return output.join('');
  994. }
  995. function isNumber(x) { return (('' + parseInt(x)) === x) || (('' + parseFloat(x)) === x); }
  996. function format(format) { var args = Array.prototype.slice.call(arguments, 1); return format.replace(/{(\d+)}/g, function (match, number) { return typeof args[number] != 'undefined' ? args[number] : match; }); };
  997. // Check if a list of modules are present and install any missing ones
  998. var InstallModuleChildProcess = null;
  999. function InstallModules(modules, func) {
  1000. var missingModules = [];
  1001. if (modules.length > 0) {
  1002. for (var i in modules) {
  1003. var moduleName = modules[i].split('@')[0];
  1004. try {
  1005. var xxmodule = require(moduleName);
  1006. } catch (e) {
  1007. missingModules.push(modules[i]);
  1008. }
  1009. }
  1010. if (missingModules.length > 0) {
  1011. console.log('Missing modules: ' + missingModules.join(', ') + '.');
  1012. InstallModuleEx(modules, func);
  1013. } else { func(); }
  1014. }
  1015. }
  1016. // Check if a module is present and install it if missing
  1017. function InstallModuleEx(modulenames, func) {
  1018. log('Installing modules...');
  1019. var names = modulenames.join(' ');
  1020. var child_process = require('child_process');
  1021. var parentpath = __dirname;
  1022. // Get the working directory
  1023. if ((__dirname.endsWith('/node_modules/meshcentral')) || (__dirname.endsWith('\\node_modules\\meshcentral')) || (__dirname.endsWith('/node_modules/meshcentral/')) || (__dirname.endsWith('\\node_modules\\meshcentral\\'))) { parentpath = require('path').join(__dirname, '../..'); }
  1024. // Looks like we need to keep a global reference to the child process object for this to work correctly.
  1025. InstallModuleChildProcess = child_process.exec(`npm install --no-audit --no-optional --omit=optional ${names}`, { maxBuffer: 512000, timeout: 300000, cwd: parentpath }, function (error, stdout, stderr) {
  1026. InstallModuleChildProcess = null;
  1027. if ((error != null) && (error != '')) {
  1028. log('ERROR: Unable to install required modules. May not have access to npm, or npm may not have suffisent rights to load the new modules. Try "npm install ' + names + '" to manually install the modules.\r\n');
  1029. process.exit();
  1030. return;
  1031. }
  1032. func();
  1033. return;
  1034. });
  1035. }
  1036. // Convert the translations to a standardized JSON we can use in GitHub
  1037. // Strings are sorder by english source and object keys are sorted
  1038. function translationsToJson(t) {
  1039. var arr2 = [], arr = t.strings;
  1040. for (var i in arr) {
  1041. var names = [], el = arr[i], el2 = {};
  1042. for (var j in el) { names.push(j); }
  1043. names.sort(function (a, b) { if (a == b) { return 0; } if (a == 'xloc') { return 1; } if (b == 'xloc') { return -1; } return a - b });
  1044. for (var j in names) { el2[names[j]] = el[names[j]]; }
  1045. if (el2.xloc != null) { el2.xloc.sort(); }
  1046. arr2.push(el2);
  1047. }
  1048. arr2.sort(function (a, b) { if (a.en > b.en) return 1; if (a.en < b.en) return -1; return 0; });
  1049. return JSON.stringify({ strings: arr2 }, null, ' ');
  1050. }
  1051. // Export table
  1052. module.exports.startEx = startEx;