ferro_cli/commands/
make_lang.rs1use console::style;
4use std::fs;
5use std::path::Path;
6
7use crate::templates;
8
9pub fn run(name: String) {
10 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 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 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 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 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
97fn 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 if parts[0].len() != 2 || !parts[0].chars().all(|c| c.is_ascii_lowercase()) {
108 return false;
109 }
110
111 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}