UNPKG

@iobroker/adapter-dev

Version:

All developer dependencies an ioBroker adapter developer needs

366 lines 15.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.allLanguages = void 0; exports.handleConvertCommand = handleConvertCommand; exports.parseOptions = parseOptions; exports.handleTranslateCommand = handleTranslateCommand; exports.handleToJsonCommand = handleToJsonCommand; exports.handleToWordsCommand = handleToWordsCommand; exports.handleAllCommand = handleAllCommand; const ansi_colors_1 = require("ansi-colors"); const fs_extra_1 = require("fs-extra"); const node_path_1 = __importDefault(require("node:path")); const tiny_glob_1 = __importDefault(require("tiny-glob")); const translate_1 = require("./translate"); const util_1 = require("./util"); let ioPackage; let admin; let words; let i18nBases; let translateLanguages; const EOL = "\n"; // Use only LINUX line endings /********************************** Helpers ***********************************/ const _languages = { en: {}, de: {}, ru: {}, pt: {}, nl: {}, fr: {}, it: {}, es: {}, pl: {}, uk: {}, "zh-cn": {}, }; exports.allLanguages = Object.keys(_languages); function createEmptyLangObject(createDefault) { return translateLanguages.reduce((obj, curr) => ({ ...obj, [curr]: createDefault() }), {}); } /** * Creates a regexp pattern for an english base file name. * It matches file names and allows to find/replace the language code */ function createFilePattern(baseFile) { if (!baseFile.match(/\Wen\W/)) { throw new Error("Base file must be an English JSON file"); } return new RegExp(`^(${(0, util_1.escapeRegExp)(baseFile).replace(/(?<=\W)en(?=\W)/, ")([a-z-]+)(")})$`, "i"); } async function findAllLanguageFiles(baseFile) { const filePattern = createFilePattern(baseFile); const allJsonFiles = await (0, tiny_glob_1.default)(node_path_1.default.join(admin, "**", "*.json").replace(/\\/g, "/"), { absolute: true }); return allJsonFiles.filter((file) => { const match = file.match(filePattern); if (!match) { return false; } const lang = match[2]; return translateLanguages.includes(lang); }); } /** Convert the "LANG/translation.json" files to "LANG.json" files */ async function convertTranslationJson2LanguageJson(basePath) { const dirs = (0, fs_extra_1.readdirSync)(basePath, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); for (const dir of dirs) { const langPath = node_path_1.default.join(basePath, dir, "translations.json"); const text = await (0, fs_extra_1.readJson)(langPath); // Write the new file await (0, fs_extra_1.writeJson)(node_path_1.default.join(basePath, `${dir}.json`), text, { spaces: 4, EOL, }); (0, fs_extra_1.unlinkSync)(langPath); (0, fs_extra_1.rmdirSync)(node_path_1.default.join(basePath, dir)); } // Try to sort the files const files = (0, fs_extra_1.readdirSync)(basePath).filter((file) => file.endsWith(".json")); // Read a file, sort the keys and write it back for (const file of files) { const filePath = node_path_1.default.join(basePath, file); const text = await (0, fs_extra_1.readJson)(filePath); // Sort the keys const sortedText = {}; Object.keys(text) .sort() .forEach((key) => { sortedText[key] = text[key]; }); // Write the new file await (0, fs_extra_1.writeJson)(filePath, sortedText, { spaces: 4, EOL, }); } } /******************************** Middlewares *********************************/ async function handleConvertCommand() { await convertTranslationJson2LanguageJson(node_path_1.default.join(admin, "i18n")); } async function parseOptions(options) { var _a; // io-package.json ioPackage = node_path_1.default.resolve(options["io-package"]); if (!(0, fs_extra_1.existsSync)(ioPackage) || !(await (0, fs_extra_1.stat)(ioPackage)).isFile()) { return (0, util_1.die)(`Couldn't find file ${ioPackage}`); } // admin directory admin = node_path_1.default.resolve(options.admin); if (!(0, fs_extra_1.existsSync)(admin) || !(await (0, fs_extra_1.stat)(admin)).isDirectory()) { return (0, util_1.die)(`Couldn't find directory ${admin}`); } // words.js if (options.words) { words = node_path_1.default.resolve(options.words); } else if ((0, fs_extra_1.existsSync)(node_path_1.default.join(admin, "js", "words.js"))) { words = node_path_1.default.join(admin, "js", "words.js"); } else { words = node_path_1.default.join(admin, "words.js"); } // i18n base file if (options.base) { i18nBases = options.base.map((p) => node_path_1.default.resolve(p)); } else { const defaultPath = node_path_1.default.join(admin, "i18n", "en.json"); i18nBases = [ defaultPath, node_path_1.default.join(admin, "i18n", "en", "translations.json"), node_path_1.default.join(admin, "src", "i18n", "en.json"), node_path_1.default.join(admin, "..", "src", "src", "i18n", "en.json"), node_path_1.default.join(admin, "..", "src-admin", "src", "i18n", "en.json"), ].filter(fs_extra_1.existsSync); if (i18nBases.length === 0) { // if no path exists, we are most likely using words.js and // expect the i18n file to be in the default path i18nBases = [defaultPath]; } } if ((_a = options.languages) === null || _a === void 0 ? void 0 : _a.length) { // Check if an unknown language was specified const unknownLanguages = options.languages.filter((l) => !exports.allLanguages.includes(l)); if (unknownLanguages.length > 0) { return (0, util_1.die)(`Unknown language(s): ${unknownLanguages.join(", ")}`); } translateLanguages = options.languages; } else { translateLanguages = exports.allLanguages; } } /***************************** Command Handlers *******************************/ async function handleTranslateCommand() { await translateIoPackage(); for (const i18nBase of i18nBases) { await translateI18n(i18nBase); } } function handleToJsonCommand() { if (!(0, fs_extra_1.existsSync)(words)) { return (0, util_1.die)(`Couldn't find words file ${words}`); } return adminWords2languages(words, i18nBases[0]); } function handleToWordsCommand() { return adminLanguages2words(i18nBases[0]); } async function handleAllCommand() { await handleTranslateCommand(); // execute it only if words.js exists, but now we do not need it if ((0, fs_extra_1.existsSync)(words)) { await handleToWordsCommand(); await handleToJsonCommand(); } } /****************************** Implementation ********************************/ async function translateIoPackage() { const ioPackageFile = await (0, fs_extra_1.readFile)(ioPackage, "utf-8"); const indentation = (0, util_1.getIndentation)(ioPackageFile); const content = JSON.parse(ioPackageFile); if (content.common.news) { console.log("Translate News"); for (const [k, nw] of Object.entries(content.common.news)) { console.log(`News: ${k}`); await translateNotExisting(nw); } } if (content.common.titleLang) { console.log("Translate Title"); await translateNotExisting(content.common.titleLang, content.common.title); } if (content.common.desc) { console.log("Translate Description"); await translateNotExisting(content.common.desc); } // https://github.com/ioBroker/adapter-dev/issues/138 if (content.common.messages) { console.log("Translate Messages"); for (const message of content.common.messages) { console.log(` Message: ${message.title.en}`); await translateNotExisting(message.title); await translateNotExisting(message.text); if (message.linkText) { await translateNotExisting(message.linkText); } } } await (0, fs_extra_1.writeJson)(ioPackage, content, { spaces: indentation, EOL }); console.log(`Successfully updated ${node_path_1.default.relative(".", ioPackage)}`); } async function translateNotExisting(obj, baseText) { const text = obj.en || baseText; if (text) { for (const lang of translateLanguages) { if (!obj[lang]) { const time = new Date().getTime(); obj[lang] = await (0, translate_1.translateText)(text, lang); console.log((0, ansi_colors_1.gray)(`en -> ${lang} ${new Date().getTime() - time} ms`)); } } } } async function translateI18n(baseFile) { const filePattern = createFilePattern(baseFile); const baseContent = await (0, fs_extra_1.readJson)(baseFile); const missingLanguages = new Set(translateLanguages); const files = await findAllLanguageFiles(baseFile); for (const file of files) { const match = file.match(filePattern); if (!match) continue; const lang = match[2]; missingLanguages.delete(lang); if (lang === "en") continue; const translation = await (0, fs_extra_1.readJson)(file); await translateI18nJson(translation, lang, baseContent); await (0, fs_extra_1.writeJson)(file, translation, { spaces: 4, EOL }); console.log(`Successfully updated ${node_path_1.default.relative(".", file)}`); } for (const lang of missingLanguages) { const translation = {}; await translateI18nJson(translation, lang, baseContent); const filename = baseFile.replace(filePattern, `$1${lang}$3`); await (0, fs_extra_1.ensureDir)(node_path_1.default.dirname(filename)); await (0, fs_extra_1.writeJson)(filename, translation, { spaces: 4, EOL }); console.log(`Successfully created ${node_path_1.default.relative(".", filename)}`); } } async function translateI18nJson(content, lang, baseContent) { if (lang === "en") { return; } const time = new Date().getTime(); for (const [t, base] of Object.entries(baseContent)) { if (!content[t]) { content[t] = await (0, translate_1.translateText)(base, lang); } } console.log((0, ansi_colors_1.gray)(`Translate Admin en -> ${lang} ${new Date().getTime() - time} ms`)); } async function adminWords2languages(words, i18nBase) { const filePattern = createFilePattern(i18nBase); const data = parseWordJs(await (0, fs_extra_1.readFile)(words, "utf-8")); const langs = createEmptyLangObject(() => ({})); for (const [word, translations] of Object.entries(data)) { for (const [lang, translation] of Object.entries(translations)) { const language = lang; langs[language][word] = translation; // pre-fill all other languages for (const j of translateLanguages) { if (langs.hasOwnProperty(j)) { langs[j][word] = langs[j][word] || ""; } } } } for (const [lang, translations] of Object.entries(langs)) { const language = lang; const keys = Object.keys(translations); keys.sort(); const obj = {}; for (const key of keys) { obj[key] = langs[language][key]; } const filename = i18nBase.replace(filePattern, `$1${lang}$3`); await (0, fs_extra_1.ensureDir)(node_path_1.default.dirname(filename)); await (0, fs_extra_1.writeJson)(filename, obj, { spaces: 4, EOL }); console.log(`Successfully updated ${node_path_1.default.relative(".", filename)}`); } } function parseWordJs(words) { words = words.substring(words.indexOf("{"), words.length); words = words.substring(0, words.lastIndexOf(";")); const resultFunc = new Function("return " + words + ";"); return resultFunc(); } async function adminLanguages2words(i18nBase) { const filePattern = createFilePattern(i18nBase); const newWords = {}; const files = await findAllLanguageFiles(i18nBase); for (const file of files) { const match = file.match(filePattern); if (!match) continue; const lang = match[2]; const translations = await (0, fs_extra_1.readJson)(file); for (const key of Object.keys(translations)) { newWords[key] = newWords[key] || createEmptyLangObject(() => ""); newWords[key][lang] = translations[key]; } } try { // merge existing and new words together (and check for missing translations) const existingWords = parseWordJs(await (0, fs_extra_1.readFile)(words, "utf-8")); for (const [key, translations] of Object.entries(existingWords)) { if (!newWords[key]) { console.warn((0, ansi_colors_1.yellow)(`Take from current words.js: ${key}`)); newWords[key] = translations; } translateLanguages .filter((lang) => !newWords[key][lang]) .forEach((lang) => console.warn((0, ansi_colors_1.yellow)(`Missing "${lang}": ${key}`))); } } catch { // ignore error, we just use the strings from the translation files //console.log(error); } await (0, fs_extra_1.writeFile)(words, createWordsJs(newWords)); console.log(`Successfully updated ${node_path_1.default.relative(".", words)}`); } function createWordsJs(data) { const lines = []; lines.push("/*global systemDictionary:true */"); lines.push("/*"); lines.push("+===================== DO NOT MODIFY ======================+"); lines.push("| This file was generated by translate-adapter, please use |"); lines.push("| `translate-adapter adminLanguages2words` to update it. |"); lines.push("+===================== DO NOT MODIFY ======================+"); lines.push("*/"); lines.push("'use strict';\n"); lines.push("systemDictionary = {"); for (const [word, translations] of Object.entries(data)) { let line = ""; for (const [lang, item] of Object.entries(translations)) { const text = (0, util_1.padRight)(item.replace(/"/g, '\\"') + '",', 50); line += `"${lang}": "${text} `; } if (line) { line = line.trim(); line = line.substring(0, line.length - 1); } const preamble = (0, util_1.padRight)(`"${word.replace(/"/g, '\\"')}": {`, 50); lines.push(` ${preamble}${line}},`); } lines.push("};"); return lines.join(EOL).trimEnd(); } //# sourceMappingURL=translate-adapter-handlers.js.map