From a7c8fe10f295388257a5d6265af0e142aaa0f232 Mon Sep 17 00:00:00 2001 From: JortVlaming Date: Tue, 16 Dec 2025 19:55:28 +0100 Subject: [PATCH] Implement MySQL integration with translation history caching and management --- .env.example | 5 ++ bun.lock | 25 +++++++ docker-compose.yml | 27 +++++++ mysql-shell.sh | 23 ++++++ package.json | 1 + src/gemini.ts | 34 ++++++++- src/index.ts | 13 +++- src/mysql.ts | 182 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 305 insertions(+), 5 deletions(-) create mode 100644 mysql-shell.sh create mode 100644 src/mysql.ts diff --git a/.env.example b/.env.example index b027aa7..8d18c26 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,8 @@ PUBLIC_KEY=your_discord_public_key # Google AI Configuration GEMINI_API_KEY=your_google_genai_api_key + +MYSQL_ROOT_PASSWORD= +MYSQL_DATABASE= +MYSQL_USER= +MYSQL_PASSWORD= \ No newline at end of file diff --git a/bun.lock b/bun.lock index 35825f3..35217c1 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@google/genai": "^1.33.0", "discord-interactions": "^4.4.0", "discord.js": "^14.25.1", + "mysql2": "^3.15.3", "tweetnacl": "^1.0.3", }, "devDependencies": { @@ -57,6 +58,8 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], @@ -79,6 +82,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "discord-api-types": ["discord-api-types@0.38.37", "", {}, "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w=="], "discord-interactions": ["discord-interactions@4.4.0", "", {}, "sha512-jjJx8iwAeJcj8oEauV43fue9lNqkf38fy60aSs2+G8D1nJmDxUIrk08o3h0F3wgwuBWWJUZO+X/VgfXsxpCiJA=="], @@ -105,6 +110,8 @@ "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], @@ -115,8 +122,12 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -131,8 +142,12 @@ "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="], "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -141,6 +156,10 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -155,12 +174,18 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/docker-compose.yml b/docker-compose.yml index 2df8881..85956cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,26 @@ version: '3.8' services: + mysql: + image: mysql:8.0 + container_name: message-translator-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword} + MYSQL_DATABASE: ${MYSQL_DATABASE:-translator_db} + MYSQL_USER: ${MYSQL_USER:-translator} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-translator_password} + volumes: + - mysql-data:/var/lib/mysql + networks: + - app-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpassword}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + message-translator: build: context: . @@ -12,6 +32,9 @@ services: - "3000:3000" env_file: - .env + depends_on: + mysql: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s @@ -21,6 +44,10 @@ services: networks: - app-network +volumes: + mysql-data: + driver: local + networks: app-network: driver: bridge diff --git a/mysql-shell.sh b/mysql-shell.sh new file mode 100644 index 0000000..37b6c97 --- /dev/null +++ b/mysql-shell.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script to access MySQL container shell + +CONTAINER_NAME="message-translator-mysql" +MYSQL_USER="${MYSQL_USER:-translator}" +MYSQL_PASSWORD="${MYSQL_PASSWORD:-translator_password}" +MYSQL_DATABASE="${MYSQL_DATABASE:-translator_db}" + +echo "Connecting to MySQL container: $CONTAINER_NAME" +echo "Database: $MYSQL_DATABASE" +echo "User: $MYSQL_USER" +echo "" + +# Check if container is running +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Error: Container '$CONTAINER_NAME' is not running." + echo "Please start the container first with: docker-compose up -d" + exit 1 +fi + +# Connect to MySQL +docker exec -it $CONTAINER_NAME mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" diff --git a/package.json b/package.json index fb25b84..3504524 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@google/genai": "^1.33.0", "discord-interactions": "^4.4.0", "discord.js": "^14.25.1", + "mysql2": "^3.15.3", "tweetnacl": "^1.0.3" } } diff --git a/src/gemini.ts b/src/gemini.ts index 27562a4..a99cfe2 100644 --- a/src/gemini.ts +++ b/src/gemini.ts @@ -1,4 +1,8 @@ import { GoogleGenAI } from "@google/genai"; +import { + getTranslationFromHistory, + saveTranslationToHistory +} from "./mysql"; const ai = new GoogleGenAI({}); @@ -6,12 +10,29 @@ export type TranslationResult = { original_message: string; translated_message: string; detected_language: string; + from_cache?: boolean; }; export async function translate( message: string, language: string = "english" ): Promise { + // Check history first + const cachedTranslation = await getTranslationFromHistory(message, language); + + if (cachedTranslation) { + console.log("✨ Translation found in cache"); + return { + original_message: cachedTranslation.original_message, + translated_message: cachedTranslation.translated_message, + detected_language: cachedTranslation.detected_language || "unknown", + from_cache: true, + }; + } + + console.log("🔄 Requesting new translation from Gemini"); + + // Not in cache, use Gemini // Sanitize inputs to prevent prompt injection const sanitizedMessage = message.replace(/"/g, '\\"').slice(0, 2000); // Escape quotes and limit length const sanitizedLanguage = language.replace(/[^a-zA-Z\s-]/g, '').slice(0, 50); // Allow only letters, spaces, hyphens @@ -66,9 +87,20 @@ Do NOT include any extra text, commentary, markdown, or backticks. } } - return { + const result = { original_message: jsonResponse.original_message ?? message, translated_message: jsonResponse.translated_message ?? message, detected_language: jsonResponse.detected_language ?? "unknown", + from_cache: false, }; + + // Save to history for future use + await saveTranslationToHistory( + result.original_message, + language, + result.detected_language, + result.translated_message + ); + + return result; } diff --git a/src/index.ts b/src/index.ts index 72aac68..f0a9d35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import nacl from "tweetnacl"; import { register } from "./register"; import { translate } from "./gemini"; +import { initMySQLPool, initDatabase } from "./mysql"; // Debug logging helper const DEBUG_MODE = process.env.DEBUG_MODE === "1"; @@ -33,6 +34,10 @@ function validateEnv() { validateEnv(); +// Initialize MySQL +initMySQLPool(); +await initDatabase(); + const PUB_KEY = process.env.PUBLIC_KEY!; const DISCORD_TOKEN = process.env.DISCORD_TOKEN!; await register(); @@ -118,8 +123,8 @@ const commandHandlers: Record = { debug("Translation result:", translation); const embed = { - title: "Translation", - color: 0x1abc9c, + title: translation.from_cache ? "Translation (from cache ✨)" : "Translation", + color: translation.from_cache ? 0x3498db : 0x1abc9c, fields: [ { name: "Original Message", value: translation.original_message || "N/A" }, { name: "Detected Language", value: translation.detected_language || "N/A" }, @@ -175,8 +180,8 @@ const commandHandlers: Record = { debug("Translation result:", translation); const embed = { - title: `Translation (${language})`, - color: 0x1abc9c, + title: translation.from_cache ? `Translation (${language}) - from cache ✨` : `Translation (${language})`, + color: translation.from_cache ? 0x3498db : 0x1abc9c, fields: [ { name: "Original Message", value: translation.original_message || "N/A" }, { name: "Detected Language", value: translation.detected_language || "N/A" }, diff --git a/src/mysql.ts b/src/mysql.ts new file mode 100644 index 0000000..1a0e3b1 --- /dev/null +++ b/src/mysql.ts @@ -0,0 +1,182 @@ +import mysql from "mysql2/promise"; + +let pool: mysql.Pool | null = null; + +// Initialize MySQL connection pool +export function initMySQLPool() { + if (pool) return pool; + + pool = mysql.createPool({ + host: process.env.MYSQL_HOST || "mysql", + user: process.env.MYSQL_USER || "translator", + password: process.env.MYSQL_PASSWORD || "translator_password", + database: process.env.MYSQL_DATABASE || "translator_db", + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + }); + + console.log("✅ MySQL connection pool initialized"); + return pool; +} + +// Get the pool instance +export function getPool() { + if (!pool) { + throw new Error("MySQL pool not initialized. Call initMySQLPool() first."); + } + return pool; +} + +// Create tables if they don't exist +export async function initDatabase() { + const db = getPool(); + + const createTableSQL = ` + CREATE TABLE IF NOT EXISTS translation_history ( + id INT AUTO_INCREMENT PRIMARY KEY, + original_message TEXT NOT NULL, + original_message_hash VARCHAR(64) NOT NULL, + target_language VARCHAR(50) NOT NULL, + detected_language VARCHAR(50), + translated_message TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + use_count INT DEFAULT 1, + INDEX idx_hash_lang (original_message_hash, target_language), + INDEX idx_created_at (created_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `; + + try { + await db.execute(createTableSQL); + console.log("✅ Database tables initialized"); + } catch (error) { + console.error("❌ Failed to initialize database tables:", error); + throw error; + } +} + +// Hash function for messages (simple SHA-256 equivalent using Bun) +async function hashMessage(message: string): Promise { + const msgBuffer = new TextEncoder().encode(message.toLowerCase().trim()); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); +} + +export type TranslationHistoryEntry = { + id: number; + original_message: string; + original_message_hash: string; + target_language: string; + detected_language: string | null; + translated_message: string; + created_at: Date; + updated_at: Date; + use_count: number; +}; + +// Check if translation exists in history +export async function getTranslationFromHistory( + message: string, + targetLanguage: string +): Promise { + const db = getPool(); + const messageHash = await hashMessage(message); + + const [rows] = await db.execute( + `SELECT * FROM translation_history + WHERE original_message_hash = ? + AND target_language = ? + ORDER BY use_count DESC, updated_at DESC + LIMIT 1`, + [messageHash, targetLanguage.toLowerCase()] + ); + + if (rows.length === 0) { + return null; + } + + // Increment use count + await db.execute( + `UPDATE translation_history + SET use_count = use_count + 1, updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [rows[0].id] + ); + + return rows[0] as TranslationHistoryEntry; +} + +// Save translation to history +export async function saveTranslationToHistory( + originalMessage: string, + targetLanguage: string, + detectedLanguage: string, + translatedMessage: string +): Promise { + const db = getPool(); + const messageHash = await hashMessage(originalMessage); + + // Check if this exact translation already exists + const [existing] = await db.execute( + `SELECT id FROM translation_history + WHERE original_message_hash = ? AND target_language = ?`, + [messageHash, targetLanguage.toLowerCase()] + ); + + if (existing.length > 0) { + // Update existing entry + await db.execute( + `UPDATE translation_history + SET translated_message = ?, + detected_language = ?, + use_count = use_count + 1, + updated_at = CURRENT_TIMESTAMP + WHERE id = ?`, + [translatedMessage, detectedLanguage, existing[0].id] + ); + } else { + // Insert new entry + await db.execute( + `INSERT INTO translation_history + (original_message, original_message_hash, target_language, detected_language, translated_message) + VALUES (?, ?, ?, ?, ?)`, + [originalMessage, messageHash, targetLanguage.toLowerCase(), detectedLanguage, translatedMessage] + ); + } +} + +// Get translation statistics +export async function getTranslationStats(): Promise<{ + total_translations: number; + unique_messages: number; + languages_used: number; +}> { + const db = getPool(); + + const [rows] = await db.execute( + `SELECT + COUNT(*) as total_translations, + COUNT(DISTINCT original_message_hash) as unique_messages, + COUNT(DISTINCT target_language) as languages_used + FROM translation_history` + ); + + return rows[0] as any; +} + +// Clean old translations (optional maintenance function) +export async function cleanOldTranslations(daysOld: number = 90): Promise { + const db = getPool(); + + const [result] = await db.execute( + `DELETE FROM translation_history + WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY) + AND use_count = 1`, + [daysOld] + ); + + return result.affectedRows; +}