Skip to main content

ferro_lang/
config.rs

1/// Type-safe localization configuration.
2///
3/// Reads from environment variables with sensible defaults.
4/// Follows the same pattern as `AppConfig` and `ServerConfig`.
5///
6/// | Variable | Default | Description |
7/// |----------|---------|-------------|
8/// | `APP_LOCALE` | `"en"` | Default locale |
9/// | `APP_FALLBACK_LOCALE` | `"en"` | Fallback when key missing in requested locale |
10/// | `LANG_PATH` | `"lang"` | Directory containing translation files |
11#[derive(Debug, Clone)]
12pub struct LangConfig {
13    /// Default locale identifier (e.g. `"en"`, `"es"`).
14    pub locale: String,
15    /// Fallback locale used when a key is missing in the requested locale.
16    pub fallback_locale: String,
17    /// Path to the directory containing `{locale}/*.json` translation files.
18    pub path: String,
19}
20
21impl LangConfig {
22    /// Build config from environment variables with defaults.
23    pub fn from_env() -> Self {
24        Self {
25            locale: std::env::var("APP_LOCALE").unwrap_or_else(|_| "en".to_string()),
26            fallback_locale: std::env::var("APP_FALLBACK_LOCALE")
27                .unwrap_or_else(|_| "en".to_string()),
28            path: std::env::var("LANG_PATH").unwrap_or_else(|_| "lang".to_string()),
29        }
30    }
31
32    /// Create a builder for customizing config.
33    pub fn builder() -> LangConfigBuilder {
34        LangConfigBuilder::default()
35    }
36}
37
38impl Default for LangConfig {
39    fn default() -> Self {
40        Self::from_env()
41    }
42}
43
44/// Builder for `LangConfig`.
45///
46/// Unset fields fall back to environment variables (via `from_env()`).
47#[derive(Default)]
48pub struct LangConfigBuilder {
49    locale: Option<String>,
50    fallback_locale: Option<String>,
51    path: Option<String>,
52}
53
54impl LangConfigBuilder {
55    /// Set the default locale.
56    pub fn locale(mut self, locale: impl Into<String>) -> Self {
57        self.locale = Some(locale.into());
58        self
59    }
60
61    /// Set the fallback locale.
62    pub fn fallback_locale(mut self, fallback: impl Into<String>) -> Self {
63        self.fallback_locale = Some(fallback.into());
64        self
65    }
66
67    /// Set the translation files directory path.
68    pub fn path(mut self, path: impl Into<String>) -> Self {
69        self.path = Some(path.into());
70        self
71    }
72
73    /// Build the `LangConfig`, filling unset fields from environment.
74    pub fn build(self) -> LangConfig {
75        let default = LangConfig::from_env();
76        LangConfig {
77            locale: self.locale.unwrap_or(default.locale),
78            fallback_locale: self.fallback_locale.unwrap_or(default.fallback_locale),
79            path: self.path.unwrap_or(default.path),
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    // Env-var tests are combined into one test because set_var/remove_var
89    // are not thread-safe — parallel tests race on shared process state.
90    #[test]
91    fn from_env_and_defaults() {
92        // 1. Defaults when env vars are unset
93        std::env::remove_var("APP_LOCALE");
94        std::env::remove_var("APP_FALLBACK_LOCALE");
95        std::env::remove_var("LANG_PATH");
96
97        let config = LangConfig::from_env();
98        assert_eq!(config.locale, "en");
99        assert_eq!(config.fallback_locale, "en");
100        assert_eq!(config.path, "lang");
101
102        // 2. Reads env vars when set
103        std::env::set_var("APP_LOCALE", "es");
104        std::env::set_var("APP_FALLBACK_LOCALE", "fr");
105        std::env::set_var("LANG_PATH", "resources/lang");
106
107        let config = LangConfig::from_env();
108        assert_eq!(config.locale, "es");
109        assert_eq!(config.fallback_locale, "fr");
110        assert_eq!(config.path, "resources/lang");
111
112        // 3. Default trait delegates to from_env
113        let config = LangConfig::default();
114        assert_eq!(config.locale, "es");
115
116        // Clean up
117        std::env::remove_var("APP_LOCALE");
118        std::env::remove_var("APP_FALLBACK_LOCALE");
119        std::env::remove_var("LANG_PATH");
120    }
121
122    #[test]
123    fn builder_overrides_fields() {
124        let config = LangConfig::builder()
125            .locale("pt-br")
126            .fallback_locale("en")
127            .path("translations")
128            .build();
129
130        assert_eq!(config.locale, "pt-br");
131        assert_eq!(config.fallback_locale, "en");
132        assert_eq!(config.path, "translations");
133    }
134
135    #[test]
136    fn builder_fills_unset_from_defaults() {
137        let config = LangConfig::builder().locale("de").build();
138
139        assert_eq!(config.locale, "de");
140        // Fallback and path come from from_env() which uses defaults
141        // when env vars aren't set (or whatever the current env state is).
142        // We only verify the builder override worked.
143    }
144}