forked from
smokesignal.events/smokesignal
Fork i18n + search + filtering- v0.2
1use std::env;
2use std::fs;
3use std::path::Path;
4use std::process;
5use std::collections::HashMap;
6
7fn main() {
8 #[cfg(feature = "embed")]
9 {
10 minijinja_embed::embed_templates!("templates");
11 }
12
13 // Only run i18n validation in debug builds or when explicitly requested
14 if env::var("CARGO_CFG_DEBUG_ASSERTIONS").is_ok() || env::var("VALIDATE_I18N").is_ok() {
15 validate_i18n_files();
16 }
17}
18
19fn validate_i18n_files() {
20 let i18n_dir = Path::new("i18n");
21 if !i18n_dir.exists() {
22 return; // Skip if no i18n directory
23 }
24
25 println!("cargo:rerun-if-changed=i18n/");
26
27 // Check for duplicate keys
28 for entry in fs::read_dir(i18n_dir).unwrap() {
29 let lang_dir = entry.unwrap().path();
30 if lang_dir.is_dir() {
31 if check_for_duplicates(&lang_dir) {
32 eprintln!("❌ Build failed: Duplicate translation keys found!");
33 process::exit(1);
34 }
35 }
36 }
37
38 // Check synchronization between en-us and fr-ca
39 if check_synchronization() {
40 eprintln!("❌ Build failed: Translation files are not synchronized!");
41 process::exit(1);
42 }
43
44 println!("✅ i18n validation passed");
45}
46
47fn check_for_duplicates(dir: &Path) -> bool {
48 let mut has_duplicates = false;
49
50 for entry in fs::read_dir(dir).unwrap() {
51 let file = entry.unwrap().path();
52 if file.extension().and_then(|s| s.to_str()) == Some("ftl") {
53 if let Ok(content) = fs::read_to_string(&file) {
54 let mut seen_keys = HashMap::new();
55
56 for (line_num, line) in content.lines().enumerate() {
57 if let Some(key) = parse_translation_key(line) {
58 if let Some(prev_line) = seen_keys.insert(key.clone(), line_num + 1) {
59 eprintln!(
60 "Duplicate key '{}' in {}: line {} and line {}",
61 key,
62 file.display(),
63 prev_line,
64 line_num + 1
65 );
66 has_duplicates = true;
67 }
68 }
69 }
70 }
71 }
72 }
73
74 has_duplicates
75}
76
77fn check_synchronization() -> bool {
78 let files = ["ui.ftl", "common.ftl", "actions.ftl", "errors.ftl", "forms.ftl"];
79 let mut has_sync_issues = false;
80
81 for file in files.iter() {
82 let en_file = Path::new("i18n/en-us").join(file);
83 let fr_file = Path::new("i18n/fr-ca").join(file);
84
85 if en_file.exists() && fr_file.exists() {
86 let en_count = count_translation_keys(&en_file);
87 let fr_count = count_translation_keys(&fr_file);
88
89 if en_count != fr_count {
90 eprintln!(
91 "Key count mismatch in {}: EN={}, FR={}",
92 file, en_count, fr_count
93 );
94 has_sync_issues = true;
95 }
96 }
97 }
98
99 has_sync_issues
100}
101
102fn count_translation_keys(file: &Path) -> usize {
103 if let Ok(content) = fs::read_to_string(file) {
104 content
105 .lines()
106 .filter(|line| parse_translation_key(line).is_some())
107 .count()
108 } else {
109 0
110 }
111}
112
113fn parse_translation_key(line: &str) -> Option<String> {
114 let trimmed = line.trim();
115
116 // Skip comments and empty lines
117 if trimmed.starts_with('#') || trimmed.is_empty() {
118 return None;
119 }
120
121 // Look for pattern: key = value
122 if let Some(eq_pos) = trimmed.find(" =") {
123 let key = &trimmed[..eq_pos];
124 // Validate key format: alphanumeric, hyphens, underscores only
125 if key.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') && !key.is_empty() {
126 return Some(key.to_string());
127 }
128 }
129
130 None
131}