UNPKG

translatinator

Version:

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

779 lines (773 loc) 28.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; 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 }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/translator.ts var import_translate, TranslationService; var init_translator = __esm({ "src/translator.ts"() { "use strict"; import_translate = __toESM(require("translate"), 1); TranslationService = class { constructor(config, cache, logger) { this.config = config; this.cache = cache; this.logger = logger; this.setupTranslateEngine(); } setupTranslateEngine() { import_translate.default.engine = this.config.engine || "google"; if (this.config.apiKey) { import_translate.default.key = this.config.apiKey; } if (this.config.endpointUrl) { try { import_translate.default.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: ${import_translate.default.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 (0, import_translate.default)(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: import_translate.default.engine }; } catch (error) { this.logger.error("Failed to get API usage:", error); throw error; } } }; } }); // src/cache.ts var import_fs_extra, path, CacheManager; var init_cache = __esm({ "src/cache.ts"() { "use strict"; import_fs_extra = __toESM(require("fs-extra"), 1); path = __toESM(require("path"), 1); CacheManager = class { constructor(cacheDir, logger) { this.cacheDir = cacheDir; this.cachePath = path.join(cacheDir, "translations.json"); this.cache = {}; this.logger = logger; } async initialize() { try { await import_fs_extra.default.ensureDir(this.cacheDir); if (await import_fs_extra.default.pathExists(this.cachePath)) { const cacheData = await import_fs_extra.default.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 import_fs_extra.default.ensureDir(this.cacheDir); await import_fs_extra.default.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 import_fs_extra.default.pathExists(this.cachePath)) { await import_fs_extra.default.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 }); var import_fs_extra2, path2, chokidar, Translatinator; var init_translatinator = __esm({ "src/translatinator.ts"() { "use strict"; import_fs_extra2 = __toESM(require("fs-extra"), 1); path2 = __toESM(require("path"), 1); chokidar = __toESM(require("chokidar"), 1); 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 import_fs_extra2.default.ensureDir(this.config.localesDir); const sourceFilePath = path2.join(this.config.localesDir, this.config.sourceFile); if (!await import_fs_extra2.default.pathExists(sourceFilePath)) { throw new Error(`Source file not found: ${sourceFilePath}`); } const sourceData = await import_fs_extra2.default.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 import_fs_extra2.default.pathExists(targetFilePath); if (!this.config.force && fileExists) { existingData = await import_fs_extra2.default.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 import_fs_extra2.default.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 }); var import_fs_extra3, path3, ConfigLoader; var init_config = __esm({ "src/config.ts"() { "use strict"; import_fs_extra3 = __toESM(require("fs-extra"), 1); path3 = __toESM(require("path"), 1); 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 import_fs_extra3.default.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 import_fs_extra3.default.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 import_fs_extra3.default.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 import_fs_extra3.default.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 import_fs_extra3.default.writeJson(outputPath, sampleConfig, { spaces: 2 }); } }; } }); // src/index.ts var index_exports = {}; __export(index_exports, { ConfigLoader: () => ConfigLoader, Translatinator: () => Translatinator, TranslatinatorDevServer: () => TranslatinatorDevServer, TranslatinatorNextPlugin: () => TranslatinatorNextPlugin, TranslatinatorWebpackPlugin: () => TranslatinatorWebpackPlugin, translate: () => translate2, withTranslatinator: () => withTranslatinator, withTranslatinatorDev: () => withTranslatinatorDev }); module.exports = __toCommonJS(index_exports); init_translatinator(); init_config(); async function translate2(configPath) { const config = await ConfigLoader.loadConfig(configPath); const hasApiKey = config.apiKey; if (!hasApiKey || hasApiKey === "your-api-key-here") { throw new Error("API key is required. Set it in config file or TRANSLATION_API_KEY environment variable."); } if (!config.targetLanguages || config.targetLanguages.length === 0) { throw new Error("Target languages must be specified in configuration."); } const translatinator = new Translatinator(config); await translatinator.initialize(); await translatinator.translateAll(); if (config.watch) { await translatinator.startWatching(); process.on("SIGINT", async () => { await translatinator.stopWatching(); process.exit(0); }); } } var TranslatinatorWebpackPlugin = class { constructor(options = {}) { this.config = options; } apply(compiler) { compiler.hooks.beforeCompile.tapAsync("TranslatinatorWebpackPlugin", async (params, callback) => { try { await translate2(this.config.configPath); callback(); } catch (error) { callback(error); } }); } }; var TranslatinatorNextPlugin = class { constructor(options = {}) { this.isInitialized = false; this.config = options; } apply(nextConfig = {}) { return { ...nextConfig, webpack: (webpackConfig, { dev, isServer }) => { if (dev && isServer && !this.isInitialized) { setImmediate(() => { this.setupDevModeTranslation(); }); this.isInitialized = true; } if (typeof nextConfig.webpack === "function") { return nextConfig.webpack(webpackConfig, { dev, isServer }); } return webpackConfig; }, // Add experimental support for Turbopack experimental: { ...nextConfig.experimental, turbo: { ...nextConfig.experimental?.turbo, // Ensure our plugin works with Turbopack rules: { ...nextConfig.experimental?.turbo?.rules } } } }; } async setupDevModeTranslation() { 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] No API key found, skipping translation setup"); return; } if (!config.targetLanguages || config.targetLanguages.length === 0) { console.warn("[Translatinator] No target languages specified, skipping translation setup"); return; } this.translatinator = new Translatinator2(config); await this.translatinator.initialize(); await this.translatinator.translateAll(); await this.setupFileWatcher(config); console.log("[Translatinator] Dev mode translation setup complete. Watching for changes..."); } catch (error) { console.error("[Translatinator] Failed to setup dev mode translation:", error); } } 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] Source file changed, updating translations..."); try { if (this.translatinator) { await this.translatinator.translateAll(); console.log("[Translatinator] Translations updated successfully"); } } catch (error) { console.error("[Translatinator] Auto-translation failed:", error); } }); process.on("SIGINT", async () => { if (this.watcher) { await this.watcher.close(); } }); process.on("SIGTERM", async () => { if (this.watcher) { await this.watcher.close(); } }); } }; function withTranslatinator(options = {}) { const plugin = new TranslatinatorNextPlugin(options); return (nextConfig = {}) => { return plugin.apply(nextConfig); }; } 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); } }; function withTranslatinatorDev(options = {}) { let devServer = null; return (nextConfig = {}) => { const config = { ...nextConfig, webpack: (webpackConfig, { dev, isServer }) => { if (dev && isServer && !devServer) { devServer = new TranslatinatorDevServer(options); setImmediate(() => { devServer?.start(); }); } if (typeof nextConfig.webpack === "function") { return nextConfig.webpack(webpackConfig, { dev, isServer }); } return webpackConfig; } }; if (typeof process !== "undefined") { const cleanup = async () => { if (devServer) { await devServer.stop(); } }; process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); process.on("beforeExit", cleanup); } return config; }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ConfigLoader, Translatinator, TranslatinatorDevServer, TranslatinatorNextPlugin, TranslatinatorWebpackPlugin, translate, withTranslatinator, withTranslatinatorDev }); //# sourceMappingURL=index.cjs.map