1use once_cell::sync::Lazy;
2use std::collections::HashMap;
3use std::sync::RwLock;
4
5pub type LexiconMap = HashMap<String, String>;
6pub type WorldMap = HashMap<String, LexiconMap>;
7
8pub trait EnvProvider: Send + Sync {
10 fn var(&self, key: &str) -> Result<String, std::env::VarError>;
11}
12
13pub struct SystemEnvProvider;
15
16impl EnvProvider for SystemEnvProvider {
17 fn var(&self, key: &str) -> Result<String, std::env::VarError> {
18 std::env::var(key)
19 }
20}
21
22struct L10n {
24 world: RwLock<WorldMap>,
25 forced_language: RwLock<Option<String>>,
26 env_provider: Box<dyn EnvProvider>,
27}
28
29impl L10n {
30 fn new(env_provider: Box<dyn EnvProvider>) -> Self {
31 L10n {
32 world: RwLock::new(HashMap::new()),
33 forced_language: RwLock::new(None),
34 env_provider,
35 }
36 }
37
38 fn register(&self, lang: &str, lexicon: LexiconMap) {
39 let mut world = self.world.write().unwrap();
40 world.entry(lang.to_string()).or_default().extend(lexicon);
41 }
42
43 fn detect_language(&self) -> String {
44 if let Some(ref lang) = *self.forced_language.read().unwrap() {
46 return lang.clone();
47 }
48
49 if self.env_provider.var("L10N_TEST_MODE").is_ok() {
51 return "en".to_string();
52 }
53
54 let env_vars = ["LANGUAGE", "LC_ALL", "LC_MESSAGES", "LANG"];
56
57 for var in &env_vars {
58 if let Ok(value) = self.env_provider.var(var) {
59 if !value.is_empty() {
60 let lang = value.split('_').next().unwrap_or(&value);
62 let lang = lang.split('.').next().unwrap_or(lang);
63 return lang.to_string();
64 }
65 }
66 }
67
68 self.env_provider
70 .var("L10N_DEFAULT_LANGUAGE")
71 .unwrap_or_else(|_| "en".to_string())
72 }
73
74 fn translate(&self, phrase: &str) -> String {
75 let lang = self.detect_language();
76 let world = self.world.read().unwrap();
77
78 if let Some(lexicon) = world.get(&lang) {
79 if let Some(translation) = lexicon.get(phrase) {
80 return translation.clone();
81 }
82 }
83
84 phrase.to_string()
85 }
86
87 fn format(&self, phrase: &str, args: &[&str]) -> String {
88 let mut result = self.translate(phrase);
89
90 for arg in args {
92 if let Some(pos) = result.find("{}") {
93 result.replace_range(pos..pos + 2, arg);
94 }
95 }
96
97 result
98 }
99
100 fn force_language(&self, lang: &str) {
101 *self.forced_language.write().unwrap() = Some(lang.to_string());
102 }
103
104 fn reset_language(&self) {
105 *self.forced_language.write().unwrap() = None;
106 }
107}
108
109static GLOBAL_L10N: Lazy<L10n> = Lazy::new(|| L10n::new(Box::new(SystemEnvProvider)));
111
112pub fn register(lang: &str, lexicon: LexiconMap) {
114 GLOBAL_L10N.register(lang, lexicon);
115}
116
117pub fn t(phrase: &str) -> String {
118 GLOBAL_L10N.translate(phrase)
119}
120
121pub fn f(phrase: &str, args: &[&str]) -> String {
122 GLOBAL_L10N.format(phrase, args)
123}
124
125pub fn e(phrase: &str, args: &[&str]) -> String {
126 GLOBAL_L10N.format(phrase, args)
127}
128
129pub fn force_language(lang: &str) {
130 GLOBAL_L10N.force_language(lang);
131}
132
133pub fn reset_language() {
134 GLOBAL_L10N.reset_language();
135}
136
137pub fn detect_language() -> String {
138 GLOBAL_L10N.detect_language()
139}
140
141#[macro_export]
143macro_rules! t {
144 ($phrase:expr) => {
145 $crate::t($phrase)
146 };
147}
148
149#[macro_export]
150macro_rules! f {
151 ($phrase:expr, $($arg:expr),* $(,)?) => {
152 $crate::f($phrase, &[$($arg),*])
153 };
154}
155
156#[macro_export]
157macro_rules! e {
158 ($phrase:expr) => {
159 $crate::e($phrase, &[])
160 };
161 ($phrase:expr, $($arg:expr),* $(,)?) => {
162 $crate::e($phrase, &[$($arg),*])
163 };
164}
165
166#[macro_export]
168macro_rules! register_translations {
169 (
170 $(
171 $lang:ident: {
172 $($key:expr => $value:expr),* $(,)?
173 }
174 ),* $(,)?
175 ) => {
176 $(
177 {
178 let mut lexicon = $crate::LexiconMap::new();
179 $(
180 lexicon.insert($key.to_string(), $value.to_string());
181 )*
182 $crate::register(stringify!($lang), lexicon);
183 }
184 )*
185 };
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use std::collections::HashMap;
192 use std::sync::Mutex;
193
194 struct MockEnvProvider {
196 vars: Mutex<HashMap<String, String>>,
197 }
198
199 impl MockEnvProvider {
200 fn new() -> Self {
201 MockEnvProvider {
202 vars: Mutex::new(HashMap::new()),
203 }
204 }
205
206 fn set(&self, key: &str, value: &str) {
207 self.vars
208 .lock()
209 .unwrap()
210 .insert(key.to_string(), value.to_string());
211 }
212 }
213
214 impl EnvProvider for MockEnvProvider {
215 fn var(&self, key: &str) -> Result<String, std::env::VarError> {
216 self.vars
217 .lock()
218 .unwrap()
219 .get(key)
220 .cloned()
221 .ok_or(std::env::VarError::NotPresent)
222 }
223 }
224
225 #[test]
226 fn test_basic_translation() {
227 let env_provider = Box::new(MockEnvProvider::new());
228 let l10n = L10n::new(env_provider);
229
230 let mut ja_lexicon = LexiconMap::new();
231 ja_lexicon.insert("Hello".to_string(), "こんにちは".to_string());
232 l10n.register("ja", ja_lexicon);
233
234 l10n.force_language("ja");
235 assert_eq!(l10n.translate("Hello"), "こんにちは");
236
237 l10n.force_language("en");
238 assert_eq!(l10n.translate("Hello"), "Hello");
239 }
240
241 #[test]
242 fn test_environment_detection() {
243 let mock_env = Box::new(MockEnvProvider::new());
244 mock_env.set("LANGUAGE", "ja_JP.UTF-8");
245
246 let l10n = L10n::new(mock_env);
247 assert_eq!(l10n.detect_language(), "ja");
248 }
249
250 #[test]
251 fn test_format_translation() {
252 let env_provider = Box::new(MockEnvProvider::new());
253 let l10n = L10n::new(env_provider);
254
255 let mut ja_lexicon = LexiconMap::new();
256 ja_lexicon.insert("Hello, {}!".to_string(), "こんにちは、{}さん!".to_string());
257 l10n.register("ja", ja_lexicon);
258
259 l10n.force_language("ja");
260 assert_eq!(
261 l10n.format("Hello, {}!", &["Alice"]),
262 "こんにちは、Aliceさん!"
263 );
264 }
265
266 #[test]
267 fn test_force_and_reset_language() {
268 let mock_env = Box::new(MockEnvProvider::new());
269 mock_env.set("LANGUAGE", "ja");
270
271 let l10n = L10n::new(mock_env);
272
273 assert_eq!(l10n.detect_language(), "ja");
274
275 l10n.force_language("en");
276 assert_eq!(l10n.detect_language(), "en");
277
278 l10n.reset_language();
279 assert_eq!(l10n.detect_language(), "ja");
280 }
281
282 #[test]
283 fn test_register_translations_macro() {
284 register_translations! {
285 ja: {
286 "Yes" => "はい",
287 "No" => "いいえ",
288 },
289 es: {
290 "Yes" => "Sí",
291 "No" => "No",
292 }
293 }
294
295 force_language("ja");
296 assert_eq!(t("Yes"), "はい");
297 assert_eq!(t("No"), "いいえ");
298
299 force_language("es");
300 assert_eq!(t("Yes"), "Sí");
301 }
302
303 #[test]
304 fn test_format_macro() {
305 let mut ja_lexicon = LexiconMap::new();
306 ja_lexicon.insert("Welcome, {}!".to_string(), "ようこそ、{}さん!".to_string());
307 register("ja", ja_lexicon);
308
309 force_language("ja");
310 assert_eq!(f!("Welcome, {}!", "Bob"), "ようこそ、Bobさん!");
311 }
312}