UNPKG

translatinator

Version:

Automated translation management for web applications. Supports multiple translation engines (Google, DeepL, Yandex, LibreTranslate)

604 lines (597 loc) 21.3 kB
#!/usr/bin/env node var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/translator.ts import translate from "translate"; var TranslationService; var init_translator = __esm({ "src/translator.ts"() { "use strict"; TranslationService = class { constructor(config, cache, logger) { this.config = config; this.cache = cache; this.logger = logger; this.setupTranslateEngine(); } setupTranslateEngine() { translate.engine = this.config.engine || "google"; if (this.config.apiKey) { translate.key = this.config.apiKey; } if (this.config.endpointUrl) { try { translate.url = this.config.endpointUrl; } catch (error) { this.logger.warn("Custom endpoint URL setting is not supported by this version of the translate package"); } } this.logger.debug(`Translation engine set to: ${translate.engine}`); } async translateText(text, targetLang, sourceLang = "en") { const cached = this.cache.getCachedTranslation(text, targetLang); if (cached && !this.config.force) { this.logger.debug(`Using cached translation for "${text}" -> ${targetLang}`); return cached.translated; } try { this.logger.debug(`Translating "${text}" from ${sourceLang} to ${targetLang}`); const translatedText = await translate(text, { from: sourceLang, to: targetLang }); this.cache.setCachedTranslation(text, targetLang, { original: text, translated: translatedText, timestamp: Date.now(), version: "1.0.0" }); return translatedText; } catch (error) { this.logger.error(`Failed to translate "${text}" to ${targetLang}:`, error); throw error; } } async translateObject(obj, targetLang, sourceLang = "en") { if (typeof obj === "string") { return await this.translateText(obj, targetLang, sourceLang); } if (Array.isArray(obj)) { const results = []; for (const item of obj) { results.push(await this.translateObject(item, targetLang, sourceLang)); } return results; } if (typeof obj === "object" && obj !== null) { const result = {}; for (const [key, value] of Object.entries(obj)) { if (this.shouldExcludeKey(key)) { result[key] = value; continue; } result[key] = await this.translateObject(value, targetLang, sourceLang); } return result; } return obj; } shouldExcludeKey(key) { if (!this.config.excludeKeys) return false; return this.config.excludeKeys.includes(key); } async getUsage() { try { this.logger.warn("Usage information is not available with the current translation engine"); return { character: { count: 0, limit: "unlimited" }, engine: translate.engine }; } catch (error) { this.logger.error("Failed to get API usage:", error); throw error; } } }; } }); // src/cache.ts import fs from "fs-extra"; import * as path from "path"; var CacheManager; var init_cache = __esm({ "src/cache.ts"() { "use strict"; CacheManager = class { constructor(cacheDir, logger) { this.cacheDir = cacheDir; this.cachePath = path.join(cacheDir, "translations.json"); this.cache = {}; this.logger = logger; } async initialize() { try { await fs.ensureDir(this.cacheDir); if (await fs.pathExists(this.cachePath)) { const cacheData = await fs.readJson(this.cachePath); this.cache = cacheData || {}; this.logger.debug(`Loaded translation cache with ${Object.keys(this.cache).length} entries`); } else { this.logger.debug("No existing cache found, starting fresh"); } } catch (error) { this.logger.warn("Failed to load translation cache:", error); this.cache = {}; } } getCachedTranslation(sourceText, targetLang) { if (this.cache[sourceText] && this.cache[sourceText][targetLang]) { return this.cache[sourceText][targetLang]; } return null; } setCachedTranslation(sourceText, targetLang, entry) { if (!this.cache[sourceText]) { this.cache[sourceText] = {}; } this.cache[sourceText][targetLang] = entry; } async saveCache() { try { await fs.ensureDir(this.cacheDir); await fs.writeJson(this.cachePath, this.cache, { spaces: 2 }); this.logger.debug("Translation cache saved successfully"); } catch (error) { this.logger.error("Failed to save translation cache:", error); } } async clearCache() { try { this.cache = {}; if (await fs.pathExists(this.cachePath)) { await fs.remove(this.cachePath); } this.logger.info("Translation cache cleared"); } catch (error) { this.logger.error("Failed to clear translation cache:", error); } } getCacheStats() { const languages = /* @__PURE__ */ new Set(); let totalEntries = 0; for (const sourceText in this.cache) { for (const lang in this.cache[sourceText]) { languages.add(lang); totalEntries++; } } return { totalEntries, languages: Array.from(languages) }; } }; } }); // src/logger.ts var Logger; var init_logger = __esm({ "src/logger.ts"() { "use strict"; Logger = class { constructor(verbose = false) { this.verbose = verbose; } info(message, ...args) { console.log(`[INFO] ${message}`, ...args); } error(message, ...args) { console.error(`[ERROR] ${message}`, ...args); } warn(message, ...args) { console.warn(`[WARN] ${message}`, ...args); } debug(message, ...args) { if (this.verbose) { console.log(`[DEBUG] ${message}`, ...args); } } success(message, ...args) { console.log(`[SUCCESS] ${message}`, ...args); } }; } }); // src/translatinator.ts var translatinator_exports = {}; __export(translatinator_exports, { Translatinator: () => Translatinator }); import fs2 from "fs-extra"; import * as path2 from "path"; import * as chokidar from "chokidar"; var Translatinator; var init_translatinator = __esm({ "src/translatinator.ts"() { "use strict"; init_translator(); init_cache(); init_logger(); Translatinator = class { constructor(config) { this.config = { engine: "google", watch: false, force: false, filePattern: "{lang}.json", preserveFormatting: true, cacheDir: ".translatinator-cache", verbose: false, ...config }; this.logger = new Logger(this.config.verbose); this.cache = new CacheManager(this.config.cacheDir, this.logger); this.translator = new TranslationService(this.config, this.cache, this.logger); } async initialize() { await this.cache.initialize(); this.logger.info("Translatinator initialized"); } async translateAll() { try { this.logger.info("Starting translation process..."); await fs2.ensureDir(this.config.localesDir); const sourceFilePath = path2.join(this.config.localesDir, this.config.sourceFile); if (!await fs2.pathExists(sourceFilePath)) { throw new Error(`Source file not found: ${sourceFilePath}`); } const sourceData = await fs2.readJson(sourceFilePath); this.logger.info(`Loaded source data from ${this.config.sourceFile}`); for (const targetLang of this.config.targetLanguages) { await this.translateToLanguage(sourceData, targetLang); } await this.cache.saveCache(); this.logger.success("Translation process completed successfully!"); } catch (error) { this.logger.error("Translation process failed:", error); throw error; } } async translateToLanguage(sourceData, targetLang) { this.logger.info(`Translating to ${targetLang}...`); try { const targetFileName = this.config.filePattern.replace("{lang}", targetLang); const targetFilePath = path2.join(this.config.localesDir, targetFileName); let existingData = {}; const fileExists = await fs2.pathExists(targetFilePath); if (!this.config.force && fileExists) { existingData = await fs2.readJson(targetFilePath); this.logger.debug(`Loaded existing translations for ${targetLang}`); } let finalData; let translationsPerformed = false; if (this.config.force) { finalData = await this.translator.translateObject(sourceData, targetLang); translationsPerformed = true; } else { finalData = { ...existingData }; const keysToTranslate = this.getMissingKeys(sourceData, existingData); if (Object.keys(keysToTranslate).length > 0) { const newTranslations = await this.translator.translateObject(keysToTranslate, targetLang); finalData = this.deepMerge(finalData, newTranslations); translationsPerformed = true; } } if (translationsPerformed || !fileExists) { await fs2.writeJson(targetFilePath, finalData, { spaces: 2 }); if (translationsPerformed) { this.logger.success(`\u2713 Created/updated ${targetFileName}`); } else { this.logger.success(`\u2713 Created ${targetFileName}`); } } else { this.logger.success(`\u2713 No translation required for ${targetFileName}`); } } catch (error) { this.logger.error(`Failed to translate to ${targetLang}:`, error); throw error; } } deepMerge(target, source) { if (typeof source !== "object" || source === null) { return source; } if (Array.isArray(source)) { return source; } const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) { result[key] = this.deepMerge(result[key] || {}, source[key]); } else { result[key] = source[key]; } } } return result; } getMissingKeys(source, existing) { if (typeof source !== "object" || source === null) { return source; } if (Array.isArray(source)) { return source; } const result = {}; for (const key in source) { if (source.hasOwnProperty(key)) { if (!(key in existing)) { result[key] = source[key]; } else if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) { const nestedMissing = this.getMissingKeys(source[key], existing[key] || {}); if (Object.keys(nestedMissing).length > 0) { result[key] = nestedMissing; } } } } return result; } async startWatching() { if (!this.config.watch) { this.logger.warn("Watch mode is not enabled in configuration"); return; } const sourceFilePath = path2.join(this.config.localesDir, this.config.sourceFile); this.logger.info(`Starting file watcher for ${sourceFilePath}...`); this.watcher = chokidar.watch(sourceFilePath, { persistent: true, ignoreInitial: true }); this.watcher.on("change", async () => { this.logger.info("Source file changed, triggering retranslation..."); try { await this.translateAll(); } catch (error) { this.logger.error("Auto-translation failed:", error); } }); this.logger.info("File watcher started successfully"); } async stopWatching() { if (this.watcher) { await this.watcher.close(); this.logger.info("File watcher stopped"); } } async clearCache() { await this.cache.clearCache(); } async getUsageInfo() { try { const usage = await this.translator.getUsage(); const cacheStats = this.cache.getCacheStats(); return { deeplUsage: usage, cacheStats }; } catch (error) { this.logger.error("Failed to get usage info:", error); throw error; } } }; } }); // src/config.ts var config_exports = {}; __export(config_exports, { ConfigLoader: () => ConfigLoader }); import fs3 from "fs-extra"; import * as path3 from "path"; var ConfigLoader; var init_config = __esm({ "src/config.ts"() { "use strict"; ConfigLoader = class { static async loadConfig(configPath) { const defaultConfig = { engine: "google", sourceFile: "en.json", localesDir: "./locales", watch: false, force: false, filePattern: "{lang}.json", preserveFormatting: true, cacheDir: ".translatinator-cache", verbose: false, targetLanguages: [], excludeKeys: [] }; const envConfig = {}; if (process.env.TRANSLATION_API_KEY) { envConfig.apiKey = process.env.TRANSLATION_API_KEY; } else if (process.env.DEEPL_API_KEY) { envConfig.apiKey = process.env.DEEPL_API_KEY; envConfig.engine = "deepl"; } if (process.env.TRANSLATION_ENGINE) { envConfig.engine = process.env.TRANSLATION_ENGINE; } if (process.env.TRANSLATION_ENDPOINT_URL) { envConfig.endpointUrl = process.env.TRANSLATION_ENDPOINT_URL; } if (process.env.TRANSLATINATOR_SOURCE_FILE) { envConfig.sourceFile = process.env.TRANSLATINATOR_SOURCE_FILE; } if (process.env.TRANSLATINATOR_TARGET_LANGUAGES) { envConfig.targetLanguages = process.env.TRANSLATINATOR_TARGET_LANGUAGES.split(","); } if (configPath) { if (await fs3.pathExists(configPath)) { const fileExt = path3.extname(configPath); let userConfig = {}; if (fileExt === ".js") { const configModule = __require(path3.resolve(configPath)); userConfig = configModule.default || configModule; } else { userConfig = await fs3.readJson(configPath); } return { ...defaultConfig, ...envConfig, ...userConfig }; } return { ...defaultConfig, ...envConfig }; } const possibleConfigPaths = [ "translatinator.config.js", "translatinator.config.json", ".translatinatorrc", ".translatinatorrc.json" ]; for (const configFile of possibleConfigPaths) { if (await fs3.pathExists(configFile)) { const fileExt = path3.extname(configFile); let userConfig = {}; if (fileExt === ".js") { const configModule = __require(path3.resolve(configFile)); userConfig = configModule.default || configModule; } else { userConfig = await fs3.readJson(configFile); } return { ...defaultConfig, ...envConfig, ...userConfig }; } } return { ...defaultConfig, ...envConfig }; } static async createSampleConfig(outputPath = "translatinator.config.json") { const sampleConfig = { engine: "google", apiKey: "your-api-key-here", sourceFile: "en.json", targetLanguages: ["de", "fr", "es", "it", "nl", "pl"], localesDir: "./locales", watch: false, force: false, filePattern: "{lang}.json", preserveFormatting: true, excludeKeys: ["version", "build", "debug"], cacheDir: ".translatinator-cache", verbose: false }; await fs3.writeJson(outputPath, sampleConfig, { spaces: 2 }); } }; } }); // src/index.ts init_translatinator(); init_config(); var TranslatinatorDevServer = class { constructor(options = {}) { this.isRunning = false; this.config = options; } async start() { if (this.isRunning) { console.log("[Translatinator Dev] Already running..."); return; } try { const { Translatinator: Translatinator2 } = await Promise.resolve().then(() => (init_translatinator(), translatinator_exports)); const { ConfigLoader: ConfigLoader2 } = await Promise.resolve().then(() => (init_config(), config_exports)); const config = await ConfigLoader2.loadConfig(this.config.configPath); const hasApiKey = config.apiKey; if (!hasApiKey || hasApiKey === "your-api-key-here") { console.warn("[Translatinator Dev] No API key found, translation watcher not started"); return; } if (!config.targetLanguages || config.targetLanguages.length === 0) { console.warn("[Translatinator Dev] No target languages specified, translation watcher not started"); return; } this.translatinator = new Translatinator2(config); await this.translatinator.initialize(); console.log("[Translatinator Dev] Running initial translation..."); await this.translatinator.translateAll(); await this.setupFileWatcher(config); this.isRunning = true; console.log("[Translatinator Dev] Translation watcher started successfully! \u{1F680}"); console.log("[Translatinator Dev] Watching for changes in:", config.sourceFile); } catch (error) { console.error("[Translatinator Dev] Failed to start translation watcher:", error); } } async stop() { if (this.watcher) { await this.watcher.close(); this.watcher = void 0; } this.isRunning = false; console.log("[Translatinator Dev] Translation watcher stopped"); } async setupFileWatcher(config) { const chokidar2 = await import("chokidar"); const path4 = await import("path"); const sourceFilePath = path4.join(config.localesDir, config.sourceFile); this.watcher = chokidar2.watch(sourceFilePath, { persistent: true, ignoreInitial: true }); this.watcher.on("change", async () => { console.log("[Translatinator Dev] \u{1F4DD} Source file changed, updating translations..."); try { if (this.translatinator) { await this.translatinator.translateAll(); console.log("[Translatinator Dev] \u2705 Translations updated successfully"); } } catch (error) { console.error("[Translatinator Dev] \u274C Auto-translation failed:", error); } }); const cleanup = async () => { await this.stop(); }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("beforeExit", cleanup); } }; // src/dev.ts import { fileURLToPath } from "url"; async function startDevServer() { const configPath = process.argv[2]; const devServer = new TranslatinatorDevServer({ configPath }); console.log("\u{1F30D} Starting Translatinator development server..."); await devServer.start(); process.on("SIGINT", async () => { console.log("\n\u{1F6D1} Shutting down Translatinator development server..."); await devServer.stop(); process.exit(0); }); } var __filename = fileURLToPath(import.meta.url); var isMainModule = process.argv[1] === __filename; if (isMainModule) { startDevServer().catch((error) => { console.error("Failed to start development server:", error); process.exit(1); }); } //# sourceMappingURL=dev.js.map