translatinator
Version:
Automated translation management for web applications. Supports multiple translation engines (Google, DeepL, Yandex, LibreTranslate)
604 lines (597 loc) • 21.3 kB
JavaScript
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