import { promises as fs } from "fs";
import { watch, FSWatcher } from "fs";
import path from "path";
import { TranslationEntry } from "./types.js";
// Global variables to store found translations file info and index
export let translationsFilePath: string | null = null;
export let translationsIndex: Map<string, TranslationEntry> = new Map();
let fileWatcher: FSWatcher | null = null;
// Function to discover translation files - now supports explicit path from config
export async function discoverTranslationsFile(
configuredPath?: string
): Promise<void> {
console.error(`🔍 Translation discovery starting...`);
if (configuredPath) {
// Use explicitly configured path
console.error(
`📁 Using configured translation file path: ${configuredPath}`
);
try {
// Support both absolute and relative paths
const resolvedPath = path.isAbsolute(configuredPath)
? configuredPath
: path.resolve(process.cwd(), configuredPath);
console.error(`📍 Resolved path: ${resolvedPath}`);
// Check if the file exists
await fs.access(resolvedPath);
await loadTranslationFile(resolvedPath);
setupFileWatcher(resolvedPath);
return;
} catch (error) {
console.error(`❌ Error loading configured translation file: ${error}`);
console.error(`❌ Configured path: ${configuredPath}`);
console.error(
`❌ Resolved path: ${
path.isAbsolute(configuredPath)
? configuredPath
: path.resolve(process.cwd(), configuredPath)
}`
);
throw error;
}
}
// Fall back to automatic discovery if no configured path
const currentDir = process.cwd();
console.error(
`📂 No configured path provided, searching automatically from: ${currentDir}`
);
try {
const found = await searchForTranslationsFile(currentDir);
if (found) {
console.error(`✅ Auto-discovered translation file: ${found}`);
await loadTranslationFile(found);
setupFileWatcher(found);
} else {
console.error(
"⚠️ No translation.json or translations.json file found in any 'en' folder"
);
}
} catch (error) {
console.error(`❌ Error searching for translation file: ${error}`);
}
}
// Function to load and parse a translation file
async function loadTranslationFile(filePath: string): Promise<void> {
try {
translationsFilePath = filePath;
const content = await fs.readFile(filePath, "utf-8");
const translationsData = JSON.parse(content);
// Clear and rebuild the translation index
translationsIndex.clear();
buildTranslationIndex(translationsData);
console.error(`✅ Loaded translation file: ${filePath}`);
console.error(`📊 Indexed ${translationsIndex.size} translation entries`);
} catch (parseError) {
console.error(`❌ Error parsing translation file: ${parseError}`);
console.error(`📄 File: ${filePath}`);
// Clear the index if parsing fails
translationsIndex.clear();
}
}
// Function to manually refresh translations (for MCP tool)
export async function refreshTranslations(): Promise<{
success: boolean;
message: string;
entriesCount: number;
}> {
if (!translationsFilePath) {
return {
success: false,
message:
"No translation file path available. Run initial discovery first.",
entriesCount: 0,
};
}
try {
const oldCount = translationsIndex.size;
await loadTranslationFile(translationsFilePath);
const newCount = translationsIndex.size;
return {
success: true,
message: `Successfully refreshed translations. Entries: ${oldCount} → ${newCount}`,
entriesCount: newCount,
};
} catch (error) {
return {
success: false,
message: `Failed to refresh translations: ${
error instanceof Error ? error.message : String(error)
}`,
entriesCount: translationsIndex.size,
};
}
}
// Function to setup file watching
function setupFileWatcher(filePath: string): void {
// Clean up existing watcher
if (fileWatcher) {
fileWatcher.close();
}
try {
fileWatcher = watch(
filePath,
{ persistent: false },
async (eventType, filename) => {
if (eventType === "change") {
console.error(`🔄 Translation file changed, reloading: ${filename}`);
await loadTranslationFile(filePath);
}
}
);
console.error(`👁️ Watching translation file for changes: ${filePath}`);
} catch (error) {
console.error(`⚠️ Could not setup file watcher: ${error}`);
}
}
// Recursive function to search for translation files in "en" folders
async function searchForTranslationsFile(
dir: string,
depth: number = 0
): Promise<string | null> {
// Limit search depth to avoid infinite recursion (increased for deeper structures)
if (depth > 8) return null;
try {
const items = await fs.readdir(dir, { withFileTypes: true });
// First, check if current directory is named "en" and contains translation files
if (path.basename(dir) === "en") {
// Try multiple common file names
const possibleFiles = [
"translation.json",
"translations.json",
"common.json",
"messages.json",
];
console.error(`📁 Checking "en" folder: ${dir}`);
for (const fileName of possibleFiles) {
const translationPath = path.join(dir, fileName);
try {
await fs.access(translationPath);
return translationPath;
} catch {
// File doesn't exist, try next one
}
}
}
// Then search in subdirectories
for (const item of items) {
if (item.isDirectory() && !shouldSkipDirectory(item.name)) {
const subDir = path.join(dir, item.name);
const found = await searchForTranslationsFile(subDir, depth + 1);
if (found) return found;
}
}
} catch (error) {
// Skip directories we can't read
}
return null;
}
// Helper function to determine if we should skip a directory
function shouldSkipDirectory(dirName: string): boolean {
const skipDirs = [
"node_modules",
".git",
".vs",
".vscode",
"bin",
"obj",
"dist",
"build",
"coverage",
".nyc_output",
"temp",
"tmp",
];
return dirName.startsWith(".") || skipDirs.includes(dirName);
}
// Function to recursively build translation index from nested objects
function buildTranslationIndex(obj: any, currentPath: string = ""): void {
for (const [key, value] of Object.entries(obj)) {
const fullPath = currentPath ? `${currentPath}.${key}` : key;
if (typeof value === "string") {
// This is a leaf node - add to index
translationsIndex.set(fullPath.toLowerCase(), {
path: fullPath,
value: value as string,
});
// Also index by the value for reverse lookup
const valueLower = (value as string).toLowerCase();
if (!translationsIndex.has(`__value__${valueLower}`)) {
translationsIndex.set(`__value__${valueLower}`, {
path: fullPath,
value: value as string,
});
}
} else if (typeof value === "object" && value !== null) {
// This is a nested object - recurse
buildTranslationIndex(value, fullPath);
}
}
}
// Function to find translation paths and values
export function findTranslations(
query: string,
exact: boolean = false
): TranslationEntry[] {
const queryLower = query.toLowerCase();
const results: TranslationEntry[] = [];
const seen = new Set<string>();
for (const [indexKey, entry] of translationsIndex) {
// Skip value-based index entries for path searches
if (indexKey.startsWith("__value__")) continue;
const matches = exact
? entry.path.toLowerCase() === queryLower ||
entry.value.toLowerCase() === queryLower
: entry.path.toLowerCase().includes(queryLower) ||
entry.value.toLowerCase().includes(queryLower);
if (matches && !seen.has(entry.path)) {
results.push(entry);
seen.add(entry.path);
}
}
// Also search by value if not exact match
if (!exact) {
const valueKey = `__value__${queryLower}`;
if (translationsIndex.has(valueKey)) {
const entry = translationsIndex.get(valueKey)!;
if (!seen.has(entry.path)) {
results.push(entry);
}
}
}
return results.slice(0, 20); // Limit results to prevent overwhelming output
}