rust_l10n/
lib.rs

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
8// 環境変数取得を抽象化するトレイト(テスト可能にするため)
9pub trait EnvProvider: Send + Sync {
10    fn var(&self, key: &str) -> Result<String, std::env::VarError>;
11}
12
13// 実際の環境変数を読むプロバイダ
14pub 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
22// グローバルな翻訳ストレージ
23struct 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        // 強制言語設定があればそれを使用
45        if let Some(ref lang) = *self.forced_language.read().unwrap() {
46            return lang.clone();
47        }
48
49        // テストモード: L10N_TEST_MODE が設定されていれば英語に固定
50        if self.env_provider.var("L10N_TEST_MODE").is_ok() {
51            return "en".to_string();
52        }
53
54        // 環境変数から言語を検出
55        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                    // 言語コードを抽出 (例: "ja_JP.UTF-8" -> "ja")
61                    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        // デフォルト言語
69        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        // シンプルな置換実装 ({}を順番に置き換える)
91        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
109// グローバルインスタンス
110static GLOBAL_L10N: Lazy<L10n> = Lazy::new(|| L10n::new(Box::new(SystemEnvProvider)));
111
112// パブリックAPI
113pub 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// マクロ定義
142#[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// 翻訳登録用マクロ
167#[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    // テスト用の環境変数プロバイダ
195    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}