i18next-mcp-server
Version:
A comprehensive Model Context Protocol (MCP) server for i18next translation management, health checking, and automated translation workflows
1,064 lines • 59.4 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { ConfigManager } from './core/config.js';
import { FileManager } from './core/file-manager.js';
import { HealthChecker } from './health/health-checker.js';
import { I18nextScannerIntegration } from './automation/scanner-integration.js';
import { getProjectInfoSchema, healthCheckSchema, validateFilesSchema, listFilesSchema, coverageReportSchema, qualityAnalysisSchema, usageAnalysisSchema, exportDataSchema, syncMissingKeysSchema, addTranslationKeySchema, syncFromSourceSchema, syncAllMissingSchema, getMissingKeysSchema, scanCodeForMissingKeysSchema } from './schemas.js';
export class I18nTranslationServer {
server;
configManager;
fileManager = null;
healthChecker = null;
keyManager = null;
analyticsEngine = null;
scannerIntegration = null;
isInitializing = false;
initializationAttempts = 0;
maxInitializationAttempts = 3;
operationTimeout = 30000; // 30 seconds
constructor() {
this.server = new Server({
name: 'i18next-translation-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.configManager = new ConfigManager();
this.setupHandlers();
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_project_info',
description: 'Get information about the translation project configuration and status',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'health_check',
description: 'Perform comprehensive health check on translation files',
inputSchema: {
type: 'object',
properties: {
languages: {
type: 'array',
items: { type: 'string' },
description: 'Specific languages to check (optional)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Specific namespaces to check (optional)'
},
detailed: {
type: 'boolean',
description: 'Include detailed analysis',
default: false
},
summary: {
type: 'boolean',
description: 'Return only high-level summary for AI assistants (overrides detailed)',
default: false
}
}
}
},
{
name: 'validate_files',
description: 'Validate translation files for JSON syntax and structure issues',
inputSchema: {
type: 'object',
properties: {
fix: {
type: 'boolean',
description: 'Attempt to fix issues automatically',
default: false
}
}
}
},
{
name: 'list_files',
description: 'List all translation files with their basic information',
inputSchema: {
type: 'object',
properties: {
language: {
type: 'string',
description: 'Filter by specific language'
},
namespace: {
type: 'string',
description: 'Filter by specific namespace'
}
}
}
},
{
name: 'coverage_report',
description: 'Generate comprehensive coverage report for translations',
inputSchema: {
type: 'object',
properties: {
languages: {
type: 'array',
items: { type: 'string' },
description: 'Specific languages to analyze (optional)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Specific namespaces to analyze (optional)'
}
}
}
},
{
name: 'quality_analysis',
description: 'Analyze translation quality across files',
inputSchema: {
type: 'object',
properties: {
languages: {
type: 'array',
items: { type: 'string' },
description: 'Specific languages to analyze (optional)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Specific namespaces to analyze (optional)'
}
}
}
},
{
name: 'usage_analysis',
description: 'Generate detailed usage analysis of translation keys',
inputSchema: {
type: 'object',
properties: {
sourceCodePaths: {
type: 'array',
items: { type: 'string' },
description: 'Paths to source code directories to scan',
default: ['src/']
}
}
}
},
{
name: 'export_data',
description: 'Export translation data in various formats',
inputSchema: {
type: 'object',
properties: {
format: {
type: 'string',
enum: ['json', 'csv', 'xlsx', 'gettext'],
description: 'Export format'
},
languages: {
type: 'array',
items: { type: 'string' },
description: 'Specific languages to export (optional)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Specific namespaces to export (optional)'
}
},
required: ['format']
}
},
{
name: 'sync_missing_keys',
description: 'Synchronize missing translation keys across all languages from a source language, maintaining sorted structure',
inputSchema: {
type: 'object',
properties: {
sourceLanguage: {
type: 'string',
description: 'Source language to copy keys from (default: configured default language)',
default: 'en'
},
targetLanguages: {
type: 'array',
items: { type: 'string' },
description: 'Target languages to sync keys to (optional, defaults to all languages except source)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Specific namespaces to sync (optional, defaults to all namespaces)'
},
placeholder: {
type: 'string',
description: 'Placeholder text for missing translations (default: empty string)',
default: ''
},
dryRun: {
type: 'boolean',
description: 'Preview changes without applying them',
default: false
},
createBackup: {
type: 'boolean',
description: 'Create backup before making changes',
default: true
}
}
}
},
{
name: 'add_translation_key',
description: 'Add a specific translation key with values to specified languages and namespaces',
inputSchema: {
type: 'object',
properties: {
key: {
type: 'string',
description: 'Translation key to add (supports nested keys with dot notation)',
examples: ['dashboard.activity.title', 'galleries.photos.upload']
},
translations: {
type: 'object',
description: 'Object with language codes as keys and translation values',
examples: [{ 'en': 'Activity', 'es': 'Actividad', 'ca': 'Activitat' }]
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Target namespaces (optional, defaults to all namespaces)'
},
overwrite: {
type: 'boolean',
description: 'Overwrite existing keys',
default: false
},
createBackup: {
type: 'boolean',
description: 'Create backup before making changes',
default: true
}
},
required: ['key', 'translations']
}
},
{
name: 'sync_from_source',
description: 'Sync specific missing keys from source language to target languages',
inputSchema: {
type: 'object',
properties: {
keys: {
type: 'array',
items: { type: 'string' },
description: 'Specific keys to sync'
},
sourceLanguage: {
type: 'string',
description: 'Source language to copy from',
default: 'en'
},
targetLanguages: {
type: 'array',
items: { type: 'string' },
description: 'Target languages to sync to'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Target namespaces'
},
copyValues: {
type: 'boolean',
description: 'Copy source values as placeholders or leave empty',
default: false
},
createBackup: {
type: 'boolean',
description: 'Create backup before making changes',
default: true
}
},
required: ['keys', 'targetLanguages']
}
},
{
name: 'sync_all_missing',
description: 'Comprehensive sync to add all missing keys across all languages, ensuring complete coverage',
inputSchema: {
type: 'object',
properties: {
placeholder: {
type: 'string',
description: 'Placeholder text for missing translations',
default: ''
},
createBackup: {
type: 'boolean',
description: 'Create backup before making changes',
default: true
},
dryRun: {
type: 'boolean',
description: 'Preview changes without applying them',
default: false
}
}
}
},
{
name: 'get_missing_keys',
description: 'Get a detailed list of missing keys by language and namespace for targeted translation work',
inputSchema: {
type: 'object',
properties: {
sourceLanguage: {
type: 'string',
description: 'Reference language to compare against',
default: 'en'
},
targetLanguages: {
type: 'array',
items: { type: 'string' },
description: 'Languages to check for missing keys (optional)'
},
namespaces: {
type: 'array',
items: { type: 'string' },
description: 'Namespaces to check (optional)'
},
format: {
type: 'string',
enum: ['detailed', 'summary', 'flat'],
description: 'Output format for missing keys',
default: 'detailed'
}
}
}
},
{
name: 'scan_code_for_missing_keys',
description: 'Scan code for missing keys using the scanner integration',
inputSchema: {
type: 'object',
properties: {
sourceCodePaths: {
type: 'array',
items: { type: 'string' },
description: 'Paths to source code directories to scan',
default: ['src/']
}
}
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
await this.ensureInitialized();
switch (name) {
case 'get_project_info': {
getProjectInfoSchema.parse(args || {});
return await this.handleGetProjectInfo();
}
case 'health_check': {
const validatedArgs = healthCheckSchema.parse(args || {});
return await this.handleHealthCheck(validatedArgs);
}
case 'validate_files': {
const validatedArgs = validateFilesSchema.parse(args || {});
return await this.handleValidateFiles(validatedArgs);
}
case 'list_files': {
const validatedArgs = listFilesSchema.parse(args || {});
return await this.handleListFiles(validatedArgs);
}
case 'coverage_report': {
const validatedArgs = coverageReportSchema.parse(args || {});
return await this.handleCoverageReport(validatedArgs);
}
case 'quality_analysis': {
const validatedArgs = qualityAnalysisSchema.parse(args || {});
return await this.handleQualityAnalysis(validatedArgs);
}
case 'usage_analysis': {
const validatedArgs = usageAnalysisSchema.parse(args || {});
return await this.handleUsageAnalysis(validatedArgs);
}
case 'export_data': {
const validatedArgs = exportDataSchema.parse(args || {});
return await this.handleExportData(validatedArgs);
}
case 'sync_missing_keys': {
const validatedArgs = syncMissingKeysSchema.parse(args || {});
return await this.handleSyncMissingKeys(validatedArgs);
}
case 'add_translation_key': {
const validatedArgs = addTranslationKeySchema.parse(args || {});
return await this.handleAddTranslationKey(validatedArgs);
}
case 'sync_from_source': {
const validatedArgs = syncFromSourceSchema.parse(args || {});
return await this.handleSyncFromSource(validatedArgs);
}
case 'sync_all_missing': {
const validatedArgs = syncAllMissingSchema.parse(args || {});
return await this.handleSyncAllMissing(validatedArgs);
}
case 'get_missing_keys': {
const validatedArgs = getMissingKeysSchema.parse(args || {});
return await this.handleGetMissingKeys(validatedArgs);
}
case 'scan_code_for_missing_keys': {
const validatedArgs = scanCodeForMissingKeysSchema.parse(args || {});
return await this.handleScanCodeForMissingKeys(validatedArgs);
}
default:
return {
content: [
{
type: 'text',
text: `Unknown tool: ${name}`
}
],
isError: true
};
}
}
catch (error) {
console.error(`Tool execution error (${name}):`, error);
return {
content: [
{
type: 'text',
text: `Error executing tool "${name}": ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
};
}
});
}
async withTimeout(operation, timeoutMs = this.operationTimeout) {
return Promise.race([
operation,
new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs))
]);
}
async ensureInitialized() {
// Prevent multiple concurrent initialization attempts
if (this.isInitializing) {
throw new Error('Initialization already in progress');
}
if (!this.fileManager) {
// Check initialization attempt limit
if (this.initializationAttempts >= this.maxInitializationAttempts) {
throw new Error(`Failed to initialize after ${this.maxInitializationAttempts} attempts`);
}
this.isInitializing = true;
this.initializationAttempts++;
try {
console.error(`Initialization attempt ${this.initializationAttempts}`);
// Load configuration
console.error('Loading configuration...');
const config = await this.configManager.loadConfig();
console.error('Configuration loaded:', {
projectRoot: config.projectRoot,
localesPath: config.localesPath,
languages: config.languages,
namespaces: config.namespaces
});
// Initialize managers
console.error('Initializing FileManager...');
this.fileManager = new FileManager(config);
console.error('Initializing HealthChecker...');
this.healthChecker = new HealthChecker(config, this.fileManager);
// Initialize KeyManager
console.error('Initializing KeyManager...');
const { KeyManager } = await import('./management/key-manager.js');
this.keyManager = new KeyManager(config, this.fileManager);
// Dynamic import for AnalyticsEngine to avoid circular dependencies
console.error('Initializing AnalyticsEngine...');
const { AnalyticsEngine } = await import('./reporting/analytics.js');
this.analyticsEngine = new AnalyticsEngine(config, this.fileManager, this.healthChecker);
// Initialize scanner integration
console.error('Initializing ScannerIntegration...');
this.scannerIntegration = new I18nextScannerIntegration(config);
// Ensure directory structure exists
console.error('Ensuring directory structure...');
await this.fileManager.ensureDirectoryStructure();
console.error('Initialization completed successfully');
// Reset attempts on successful initialization
this.initializationAttempts = 0;
}
catch (error) {
console.error('Initialization failed:', error);
// Reset state on failure
this.fileManager = null;
this.healthChecker = null;
this.keyManager = null;
this.analyticsEngine = null;
this.scannerIntegration = null;
throw error;
}
finally {
this.isInitializing = false;
}
}
}
async handleGetProjectInfo() {
if (!this.fileManager)
throw new Error('File manager not initialized');
const config = this.configManager.getConfig();
const fileStats = await this.fileManager.getFileStats();
const validation = await this.configManager.validateProject();
const projectInfo = {
config: {
projectRoot: config.projectRoot,
localesPath: config.localesPath,
languages: config.languages,
namespaces: config.namespaces,
defaultLanguage: config.defaultLanguage,
defaultNamespace: config.defaultNamespace
},
fileStats,
validation,
server: {
name: 'i18next-translation-server',
version: '1.0.0',
capabilities: ['file_management', 'validation', 'project_info', 'health_check']
}
};
return {
content: [
{
type: 'text',
text: JSON.stringify(projectInfo, null, 2)
}
]
};
}
async handleHealthCheck(args) {
if (!this.healthChecker)
throw new Error('Health checker not initialized');
const parsed = z.object({
languages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
detailed: z.boolean().default(false),
summary: z.boolean().default(false)
}).parse(args || {});
const results = await this.healthChecker.performHealthCheck(parsed.languages, parsed.namespaces, parsed.detailed);
// If summary mode, return condensed output
if (parsed.summary) {
const summaryOutput = {
overall: {
score: results.summary.score,
grade: this.getGradeFromScore(results.summary.score),
totalIssues: results.summary.totalIssues,
breakdown: {
errors: results.summary.errors,
warnings: results.summary.warnings,
info: results.summary.info
}
},
files: Object.fromEntries(Object.entries(results.files).map(([fileKey, fileData]) => [
fileKey,
{
score: fileData.qualityScore.score,
grade: fileData.qualityScore.grade,
keys: fileData.keyCount,
issues: fileData.issueCount,
criticalIssues: fileData.issues.filter(i => i.severity === 'error').length
}
])),
topIssues: results.issues
.filter(i => i.severity === 'error')
.slice(0, 5)
.map(i => ({ type: i.type, severity: i.severity, message: i.message, file: i.file })),
recommendations: results.recommendations.slice(0, 3)
};
return {
content: [
{
type: 'text',
text: JSON.stringify(summaryOutput, null, 2)
}
]
};
}
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2)
}
]
};
}
async handleValidateFiles(args) {
if (!this.fileManager)
throw new Error('File manager not initialized');
const parsed = z.object({
fix: z.boolean().default(false)
}).parse(args || {});
// Basic file validation
const files = await this.fileManager.loadAllTranslationFiles();
const results = {
valid: true,
issues: [],
totalFiles: files.length,
validFiles: 0,
fixedIssues: parsed.fix ? [] : undefined
};
for (const file of files) {
try {
// File loaded successfully means JSON is valid
results.validFiles++;
}
catch (error) {
results.valid = false;
results.issues.push({
file: file.path,
issue: error instanceof Error ? error.message : String(error),
severity: 'error'
});
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2)
}
]
};
}
async handleListFiles(args) {
if (!this.fileManager)
throw new Error('File manager not initialized');
const schema = z.object({
language: z.string().optional(),
namespace: z.string().optional()
});
const parsedArgs = schema.parse(args);
const allFiles = await this.fileManager.loadAllTranslationFiles();
let filteredFiles = allFiles;
if (parsedArgs.language) {
filteredFiles = filteredFiles.filter(f => f.language === parsedArgs.language);
}
if (parsedArgs.namespace) {
filteredFiles = filteredFiles.filter(f => f.namespace === parsedArgs.namespace);
}
const fileInfo = filteredFiles.map(f => ({
path: f.path,
language: f.language,
namespace: f.namespace,
size: f.size,
lastModified: f.lastModified.toISOString()
}));
return {
content: [{ type: 'text', text: JSON.stringify(fileInfo, null, 2) }]
};
}
async handleCoverageReport(args) {
if (!this.analyticsEngine)
throw new Error('Analytics engine not initialized');
const schema = z.object({
languages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
});
const parsedArgs = schema.parse(args);
const report = await this.analyticsEngine.generateCoverageReport(parsedArgs.languages, parsedArgs.namespaces);
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
}
async handleQualityAnalysis(args) {
if (!this.analyticsEngine)
throw new Error('Analytics engine not initialized');
const schema = z.object({
languages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
});
const parsedArgs = schema.parse(args);
const report = await this.analyticsEngine.generateQualityAnalysis(parsedArgs.languages, parsedArgs.namespaces);
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
}
async handleUsageAnalysis(args) {
if (!this.analyticsEngine)
throw new Error('Analytics engine not initialized');
const schema = z.object({
sourceCodePaths: z.array(z.string()).optional(),
});
const parsedArgs = schema.parse(args);
const report = await this.analyticsEngine.generateUsageAnalysis(parsedArgs.sourceCodePaths);
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }] };
}
async handleExportData(args) {
if (!this.analyticsEngine)
throw new Error('Analytics engine not initialized');
const schema = z.object({
format: z.enum(['json', 'csv', 'xlsx', 'gettext']),
languages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
});
const parsedArgs = schema.parse(args);
const result = await this.analyticsEngine.exportData(parsedArgs.format, parsedArgs.languages, parsedArgs.namespaces);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
}
getGradeFromScore(score) {
if (score >= 90)
return 'A';
if (score >= 80)
return 'B';
if (score >= 70)
return 'C';
if (score >= 60)
return 'D';
return 'F';
}
async handleSyncMissingKeys(args) {
if (!this.fileManager || !this.keyManager)
throw new Error('Managers not initialized');
const parsed = z.object({
sourceLanguage: z.string().default('en'),
targetLanguages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
placeholder: z.string().default(''),
dryRun: z.boolean().default(false),
createBackup: z.boolean().default(true)
}).parse(args || {});
const config = this.configManager.getConfig();
const targetLanguages = parsed.targetLanguages || config.languages.filter(l => l !== parsed.sourceLanguage);
const targetNamespaces = parsed.namespaces || config.namespaces;
const operations = [];
const errors = [];
for (const namespace of targetNamespaces) {
try {
// Load source file
const sourceFile = await this.fileManager.loadTranslationFile(parsed.sourceLanguage, namespace);
const sourceKeys = this.getAllKeys(sourceFile.content);
for (const targetLanguage of targetLanguages) {
try {
const targetFile = await this.fileManager.loadTranslationFile(targetLanguage, namespace);
const targetKeys = new Set(this.getAllKeys(targetFile.content));
const missingKeys = sourceKeys.filter(key => !targetKeys.has(key));
if (missingKeys.length > 0) {
operations.push({
language: targetLanguage,
namespace,
missingKeys,
action: parsed.dryRun ? 'preview' : 'sync'
});
if (!parsed.dryRun) {
// Add missing keys
for (const key of missingKeys) {
const sourceValue = this.getNestedValue(sourceFile.content, key);
const value = parsed.placeholder || (typeof sourceValue === 'string' ? sourceValue : '');
await this.keyManager.addKey({
type: 'add',
key,
value,
defaultValue: value,
languages: [targetLanguage],
namespaces: [namespace],
overwrite: false
});
}
}
}
}
catch {
// Target file doesn't exist - create with all source keys
operations.push({
language: targetLanguage,
namespace,
missingKeys: sourceKeys,
action: parsed.dryRun ? 'preview' : 'create_file'
});
if (!parsed.dryRun) {
// Create the target directory and file structure
const newContent = {};
for (const key of sourceKeys) {
const sourceValue = this.getNestedValue(sourceFile.content, key);
const value = parsed.placeholder || (typeof sourceValue === 'string' ? sourceValue : '');
this.setNestedValue(newContent, key, value);
}
await this.fileManager.saveTranslationFile(targetLanguage, namespace, newContent, parsed.createBackup);
}
}
}
}
catch (error) {
errors.push(`Failed to process ${parsed.sourceLanguage}/${namespace}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const result = {
success: errors.length === 0,
dryRun: parsed.dryRun,
operations,
errors,
summary: {
totalOperations: operations.length,
totalKeysToSync: operations.reduce((sum, op) => sum + op.missingKeys.length, 0),
targetLanguages: targetLanguages.length,
namespaces: targetNamespaces.length
}
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && typeof current === 'object' ? current[key] : undefined;
}, obj);
}
setNestedValue(obj, path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
if (!lastKey)
return;
let current = obj;
for (const key of keys) {
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
current[key] = {};
}
current = current[key];
}
current[lastKey] = value;
}
async handleAddTranslationKey(args) {
if (!this.keyManager)
throw new Error('Key manager not initialized');
const parsed = z.object({
key: z.string(),
translations: z.record(z.string()),
namespaces: z.array(z.string()).optional(),
overwrite: z.boolean().default(false),
createBackup: z.boolean().default(true)
}).parse(args);
const config = this.configManager.getConfig();
const targetNamespaces = parsed.namespaces || config.namespaces;
const languages = Object.keys(parsed.translations);
const results = [];
const errors = [];
for (const namespace of targetNamespaces) {
for (const [language, value] of Object.entries(parsed.translations)) {
try {
const result = await this.keyManager.addKey({
type: 'add',
key: parsed.key,
value,
defaultValue: value,
languages: [language],
namespaces: [namespace],
overwrite: parsed.overwrite
});
results.push({
language,
namespace,
key: parsed.key,
value,
success: result.success,
conflicts: result.conflicts,
errors: result.errors
});
}
catch (error) {
errors.push(`Failed to add key "${parsed.key}" to ${language}/${namespace}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
const summary = {
success: errors.length === 0,
totalOperations: results.length,
successfulOperations: results.filter(r => r.success).length,
languages: languages.length,
namespaces: targetNamespaces.length,
key: parsed.key
};
return {
content: [
{
type: 'text',
text: JSON.stringify({ summary, results, errors }, null, 2)
}
]
};
}
async handleSyncFromSource(args) {
if (!this.fileManager || !this.keyManager)
throw new Error('Managers not initialized');
const parsed = z.object({
keys: z.array(z.string()),
sourceLanguage: z.string().default('en'),
targetLanguages: z.array(z.string()),
namespaces: z.array(z.string()).optional(),
copyValues: z.boolean().default(false),
createBackup: z.boolean().default(true)
}).parse(args);
const config = this.configManager.getConfig();
const targetNamespaces = parsed.namespaces || config.namespaces;
const results = [];
const errors = [];
for (const namespace of targetNamespaces) {
try {
// Load source file
const sourceFile = await this.fileManager.loadTranslationFile(parsed.sourceLanguage, namespace);
for (const targetLanguage of parsed.targetLanguages) {
for (const key of parsed.keys) {
try {
const sourceValue = this.getNestedValue(sourceFile.content, key);
if (sourceValue === undefined) {
errors.push(`Key "${key}" not found in source ${parsed.sourceLanguage}/${namespace}`);
continue;
}
const value = parsed.copyValues && typeof sourceValue === 'string' ? sourceValue : '';
const result = await this.keyManager.addKey({
type: 'add',
key,
value,
defaultValue: value,
languages: [targetLanguage],
namespaces: [namespace],
overwrite: false
});
results.push({
key,
language: targetLanguage,
namespace,
value,
success: result.success,
conflicts: result.conflicts,
errors: result.errors
});
}
catch (error) {
errors.push(`Failed to sync key "${key}" to ${targetLanguage}/${namespace}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
}
catch (error) {
errors.push(`Failed to load source file ${parsed.sourceLanguage}/${namespace}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const summary = {
success: errors.length === 0,
totalOperations: results.length,
successfulOperations: results.filter(r => r.success).length,
keys: parsed.keys.length,
targetLanguages: parsed.targetLanguages.length,
namespaces: targetNamespaces.length
};
return {
content: [
{
type: 'text',
text: JSON.stringify({ summary, results, errors }, null, 2)
}
]
};
}
async handleSyncAllMissing(args) {
const parsed = z.object({
placeholder: z.string().default(''),
createBackup: z.boolean().default(true),
dryRun: z.boolean().default(false)
}).parse(args || {});
// Use the sync_missing_keys handler with all languages
const config = this.configManager.getConfig();
const syncArgs = {
sourceLanguage: config.defaultLanguage,
targetLanguages: config.languages.filter(l => l !== config.defaultLanguage),
namespaces: config.namespaces,
placeholder: parsed.placeholder,
dryRun: parsed.dryRun,
createBackup: parsed.createBackup
};
return await this.handleSyncMissingKeys(syncArgs);
}
async handleGetMissingKeys(args) {
if (!this.fileManager)
throw new Error('File manager not initialized');
const parsed = z.object({
sourceLanguage: z.string().default('en'),
targetLanguages: z.array(z.string()).optional(),
namespaces: z.array(z.string()).optional(),
format: z.enum(['detailed', 'summary', 'flat']).default('detailed')
}).parse(args || {});
const config = this.configManager.getConfig();
const targetLanguages = parsed.targetLanguages || config.languages.filter(l => l !== parsed.sourceLanguage);
const targetNamespaces = parsed.namespaces || config.namespaces;
const missingKeys = {};
for (const namespace of targetNamespaces) {
try {
// Load source file
const sourceFile = await this.fileManager.loadTranslationFile(parsed.sourceLanguage, namespace);
const sourceKeys = this.getAllKeys(sourceFile.content);
for (const targetLanguage of targetLanguages) {
try {
const targetFile = await this.fileManager.loadTranslationFile(targetLanguage, namespace);
const targetKeys = new Set(this.getAllKeys(targetFile.content));
const missing = sourceKeys.filter(key => !targetKeys.has(key));
if (missing.length > 0) {
if (!missingKeys[targetLanguage])
missingKeys[targetLanguage] = {};
missingKeys[targetLanguage][namespace] = missing;
}
}
catch {
// Target file doesn't exist - all source keys are missing
if (!missingKeys[targetLanguage])
missingKeys[targetLanguage] = {};
missingKeys[targetLanguage][namespace] = sourceKeys;
}
}
}
catch {
console.warn(`Could not load source file ${parsed.sourceLanguage}/${namespace}.json`);
}
}
let output;
if (parsed.format === 'summary') {
const summary = Object.entries(missingKeys).map(([lang, namespaces]) => ({
language: lang,
totalMissing: Object.values(namespaces).flat().length,
namespaces: Object.keys(namespaces).length
}));
output = { summary, totalLanguages: targetLanguages.length };
}
else if (parsed.format === 'flat') {
const flatKeys = {};
for (const [lang, namespaces] of Object.entries(missingKeys)) {
flatKeys[lang] = Object.values(namespaces).flat();
}
output = flatKeys;
}
else {
output = missingKeys;
}
return {
conte