Skip to main content

ferro_cli/commands/
make_lang.rs

1//! make:lang command — scaffold translation files for a new locale
2
3use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10    // Validate locale code: lowercase letters, optionally hyphen + more lowercase letters
11    if !is_valid_locale(&name) {
12        eprintln!(
13            "{} '{}' is not a valid locale code (expected format: en, fr, pt-br, zh-hans)",
14            style("Error:").red().bold(),
15            name
16        );
17        std::process::exit(1);
18    }
19
20    let lang_dir = Path::new("lang").join(&name);
21
22    // Check if locale directory already exists
23    if lang_dir.exists() {
24        eprintln!(
25            "{} Language directory '{}' already exists",
26            style("Error:").red().bold(),
27            lang_dir.display()
28        );
29        std::process::exit(1);
30    }
31
32    // Create locale directory
33    if let Err(e) = fs::create_dir_all(&lang_dir) {
34        eprintln!(
35            "{} Failed to create directory {}: {}",
36            style("Error:").red().bold(),
37            lang_dir.display(),
38            e
39        );
40        std::process::exit(1);
41    }
42
43    // Write validation.json
44    let validation_path = lang_dir.join("validation.json");
45    if let Err(e) = fs::write(&validation_path, templates::lang_validation_json()) {
46        eprintln!(
47            "{} Failed to write {}: {}",
48            style("Error:").red().bold(),
49            validation_path.display(),
50            e
51        );
52        std::process::exit(1);
53    }
54    println!(
55        "{} Created {}",
56        style("✓").green(),
57        validation_path.display()
58    );
59
60    // Write app.json
61    let app_path = lang_dir.join("app.json");
62    if let Err(e) = fs::write(&app_path, templates::lang_app_json()) {
63        eprintln!(
64            "{} Failed to write {}: {}",
65            style("Error:").red().bold(),
66            app_path.display(),
67            e
68        );
69        std::process::exit(1);
70    }
71    println!("{} Created {}", style("✓").green(), app_path.display());
72
73    println!();
74    println!(
75        "Language files for '{}' created successfully!",
76        style(&name).cyan().bold()
77    );
78    println!();
79    println!("Usage:");
80    println!(
81        "  {} Set APP_LOCALE={} in .env to use as default",
82        style("1.").dim(),
83        name
84    );
85    println!(
86        "  {} Translate the strings in lang/{}/*.json",
87        style("2.").dim(),
88        name
89    );
90    println!(
91        "  {} Use t(\"app.welcome\", &[(\"name\", \"Ferro\")]) in handlers",
92        style("3.").dim()
93    );
94    println!();
95}
96
97/// Validate locale code format: lowercase letters, optionally followed by hyphen
98/// and more lowercase letters (e.g., en, fr, pt-br, zh-hans).
99fn is_valid_locale(s: &str) -> bool {
100    if s.is_empty() {
101        return false;
102    }
103
104    let parts: Vec<&str> = s.split('-').collect();
105
106    // First part must be exactly 2 lowercase letters
107    if parts[0].len() != 2 || !parts[0].chars().all(|c| c.is_ascii_lowercase()) {
108        return false;
109    }
110
111    // Optional subsequent parts must be 2+ lowercase letters
112    for part in &parts[1..] {
113        if part.len() < 2 || !part.chars().all(|c| c.is_ascii_lowercase()) {
114            return false;
115        }
116    }
117
118    true
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_valid_locale_simple() {
127        assert!(is_valid_locale("en"));
128        assert!(is_valid_locale("fr"));
129        assert!(is_valid_locale("de"));
130        assert!(is_valid_locale("ja"));
131    }
132
133    #[test]
134    fn test_valid_locale_with_region() {
135        assert!(is_valid_locale("pt-br"));
136        assert!(is_valid_locale("zh-hans"));
137        assert!(is_valid_locale("en-us"));
138        assert!(is_valid_locale("en-gb"));
139    }
140
141    #[test]
142    fn test_valid_locale_multi_part() {
143        assert!(is_valid_locale("zh-hans-cn"));
144    }
145
146    #[test]
147    fn test_invalid_locale_empty() {
148        assert!(!is_valid_locale(""));
149    }
150
151    #[test]
152    fn test_invalid_locale_uppercase() {
153        assert!(!is_valid_locale("EN"));
154        assert!(!is_valid_locale("pt-BR"));
155        assert!(!is_valid_locale("Fr"));
156    }
157
158    #[test]
159    fn test_invalid_locale_numbers() {
160        assert!(!is_valid_locale("12"));
161        assert!(!is_valid_locale("en1"));
162        assert!(!is_valid_locale("e1"));
163    }
164
165    #[test]
166    fn test_invalid_locale_single_char() {
167        assert!(!is_valid_locale("e"));
168        assert!(!is_valid_locale("f"));
169    }
170
171    #[test]
172    fn test_invalid_locale_three_chars_base() {
173        assert!(!is_valid_locale("eng"));
174        assert!(!is_valid_locale("fra"));
175    }
176
177    #[test]
178    fn test_invalid_locale_single_char_subtag() {
179        assert!(!is_valid_locale("en-a"));
180        assert!(!is_valid_locale("pt-b"));
181    }
182
183    #[test]
184    fn test_invalid_locale_special_chars() {
185        assert!(!is_valid_locale("en_us"));
186        assert!(!is_valid_locale("en.us"));
187        assert!(!is_valid_locale("en/us"));
188    }
189}