use std::env; use std::fs; use std::path::Path; use std::process; use std::collections::HashMap; fn main() { #[cfg(feature = "embed")] { minijinja_embed::embed_templates!("templates"); } // Only run i18n validation in debug builds or when explicitly requested if env::var("CARGO_CFG_DEBUG_ASSERTIONS").is_ok() || env::var("VALIDATE_I18N").is_ok() { validate_i18n_files(); } } fn validate_i18n_files() { let i18n_dir = Path::new("i18n"); if !i18n_dir.exists() { return; // Skip if no i18n directory } println!("cargo:rerun-if-changed=i18n/"); // Check for duplicate keys for entry in fs::read_dir(i18n_dir).unwrap() { let lang_dir = entry.unwrap().path(); if lang_dir.is_dir() { if check_for_duplicates(&lang_dir) { eprintln!("❌ Build failed: Duplicate translation keys found!"); process::exit(1); } } } // Check synchronization between en-us and fr-ca if check_synchronization() { eprintln!("❌ Build failed: Translation files are not synchronized!"); process::exit(1); } println!("✅ i18n validation passed"); } fn check_for_duplicates(dir: &Path) -> bool { let mut has_duplicates = false; for entry in fs::read_dir(dir).unwrap() { let file = entry.unwrap().path(); if file.extension().and_then(|s| s.to_str()) == Some("ftl") { if let Ok(content) = fs::read_to_string(&file) { let mut seen_keys = HashMap::new(); for (line_num, line) in content.lines().enumerate() { if let Some(key) = parse_translation_key(line) { if let Some(prev_line) = seen_keys.insert(key.clone(), line_num + 1) { eprintln!( "Duplicate key '{}' in {}: line {} and line {}", key, file.display(), prev_line, line_num + 1 ); has_duplicates = true; } } } } } } has_duplicates } fn check_synchronization() -> bool { let files = ["ui.ftl", "common.ftl", "actions.ftl", "errors.ftl", "forms.ftl"]; let mut has_sync_issues = false; for file in files.iter() { let en_file = Path::new("i18n/en-us").join(file); let fr_file = Path::new("i18n/fr-ca").join(file); if en_file.exists() && fr_file.exists() { let en_count = count_translation_keys(&en_file); let fr_count = count_translation_keys(&fr_file); if en_count != fr_count { eprintln!( "Key count mismatch in {}: EN={}, FR={}", file, en_count, fr_count ); has_sync_issues = true; } } } has_sync_issues } fn count_translation_keys(file: &Path) -> usize { if let Ok(content) = fs::read_to_string(file) { content .lines() .filter(|line| parse_translation_key(line).is_some()) .count() } else { 0 } } fn parse_translation_key(line: &str) -> Option { let trimmed = line.trim(); // Skip comments and empty lines if trimmed.starts_with('#') || trimmed.is_empty() { return None; } // Look for pattern: key = value if let Some(eq_pos) = trimmed.find(" =") { let key = &trimmed[..eq_pos]; // Validate key format: alphanumeric, hyphens, underscores only if key.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') && !key.is_empty() { return Some(key.to_string()); } } None }