inspector_gguf/localization/
manager.rs

1use crate::localization::{
2    Language, LocalizationError, SettingsManager, SystemLocaleDetector, TranslationLoader,
3};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Type alias for translation data structure containing nested key-value pairs.
8///
9/// Translation maps store hierarchical translation data where keys can be accessed
10/// using dot notation (e.g., "buttons.load" maps to `translations["buttons"]["load"]`).
11pub type TranslationMap = HashMap<String, Value>;
12
13/// Central manager for all localization operations in Inspector GGUF.
14///
15/// The `LocalizationManager` coordinates translation loading, language switching,
16/// and text retrieval with automatic fallback mechanisms. It serves as the primary
17/// interface for all internationalization needs in the application.
18///
19/// The manager integrates with [`TranslationLoader`] for file operations, [`SystemLocaleDetector`]
20/// for automatic language detection, [`SettingsManager`] for persistent preferences, and
21/// [`Language`] for supported language variants.
22///
23/// # Features
24///
25/// - **Automatic Language Detection**: Detects system locale on initialization
26/// - **Fallback System**: Falls back to English, then to key names if translations are missing
27/// - **Persistent Settings**: Integrates with settings system for user preferences
28/// - **Thread-Safe Design**: Can be safely shared across threads when wrapped appropriately
29/// - **Validation**: Ensures translation completeness and format correctness
30///
31/// # Examples
32///
33/// ## Basic Usage
34///
35/// ```rust
36/// use inspector_gguf::localization::{LocalizationManager, Language};
37///
38/// // Create manager with automatic language detection
39/// let mut manager = LocalizationManager::new()?;
40///
41/// // Get translated text
42/// let app_title = manager.get_text("app.title");
43/// let load_button = manager.get_text("buttons.load");
44///
45/// // Switch language
46/// manager.set_language(Language::Russian)?;
47/// let russian_title = manager.get_text("app.title");
48/// # Ok::<(), Box<dyn std::error::Error>>(())
49/// ```
50///
51/// ## With Persistent Settings
52///
53/// ```rust
54/// use inspector_gguf::localization::{LocalizationManager, Language};
55///
56/// let mut manager = LocalizationManager::new()?;
57///
58/// // Change language and save preference
59/// manager.set_language_with_persistence(Language::PortugueseBrazilian)?;
60///
61/// // Language preference will be restored on next startup
62/// # Ok::<(), Box<dyn std::error::Error>>(())
63/// ```
64pub struct LocalizationManager {
65    current_language: Language,
66    translations: HashMap<Language, TranslationMap>,
67}
68
69impl LocalizationManager {
70    /// Creates a new LocalizationManager with automatic language detection and translation loading.
71    ///
72    /// This constructor performs several initialization steps:
73    /// 1. Loads all available translation files
74    /// 2. Detects system locale or loads saved language preference
75    /// 3. Sets up fallback mechanisms for missing translations
76    ///
77    /// # Returns
78    ///
79    /// Returns a configured `LocalizationManager` ready for use, or a `LocalizationError`
80    /// if critical translation files (especially English) cannot be loaded.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if:
85    /// - The English translation file is missing or corrupted (required for fallback)
86    /// - Translation files have invalid JSON format
87    /// - Required translation sections are missing
88    ///
89    /// # Examples
90    ///
91    /// ```rust
92    /// use inspector_gguf::localization::LocalizationManager;
93    ///
94    /// let manager = LocalizationManager::new()?;
95    /// println!("Current language: {:?}", manager.get_current_language());
96    /// # Ok::<(), Box<dyn std::error::Error>>(())
97    /// ```
98    pub fn new() -> Result<Self, LocalizationError> {
99        let mut manager = LocalizationManager {
100            current_language: Language::English,
101            translations: HashMap::new(),
102        };
103
104        // Load translations for all supported languages
105        let loader = TranslationLoader::new();
106        for language in [
107            Language::English,
108            Language::Russian,
109            Language::PortugueseBrazilian,
110        ] {
111            match loader.load_translation(language) {
112                Ok(translations) => {
113                    manager.translations.insert(language, translations);
114                }
115                Err(e) => {
116                    eprintln!(
117                        "Warning: Failed to load translations for {:?}: {}",
118                        language, e
119                    );
120                    // Insert empty map as fallback
121                    manager.translations.insert(language, HashMap::new());
122                }
123            }
124        }
125
126        // Determine initial language from settings or system locale
127        let settings_manager = SettingsManager::new().unwrap_or_default();
128        let initial_language = settings_manager
129            .load_language_preference()
130            .or_else(SystemLocaleDetector::detect)
131            .unwrap_or(Language::English);
132
133        manager.current_language = initial_language;
134
135        Ok(manager)
136    }
137
138    /// Retrieves translated text for the specified key with automatic fallback.
139    ///
140    /// This method implements a three-tier fallback system:
141    /// 1. Try current language translation
142    /// 2. Fall back to English if key is missing in current language
143    /// 3. Return the key itself if no translation is found
144    ///
145    /// Keys use dot notation to access nested translation structures
146    /// (e.g., "buttons.load" accesses `translations["buttons"]["load"]`).
147    ///
148    /// # Arguments
149    ///
150    /// * `key` - Translation key in dot notation (e.g., "app.title", "buttons.load")
151    ///
152    /// # Returns
153    ///
154    /// Returns the translated string, or the key itself if no translation is available.
155    /// This method never panics and always returns a valid string.
156    ///
157    /// # Examples
158    ///
159    /// ```rust
160    /// use inspector_gguf::localization::{LocalizationManager, Language};
161    ///
162    /// let mut manager = LocalizationManager::new()?;
163    /// manager.set_language(Language::English)?;
164    ///
165    /// // Get simple translation
166    /// let title = manager.get_text("app.title");
167    /// assert_eq!(title, "Inspector GGUF");
168    ///
169    /// // Get nested translation
170    /// let load_button = manager.get_text("buttons.load");
171    /// assert_eq!(load_button, "Load");
172    ///
173    /// // Non-existent key returns the key itself
174    /// let missing = manager.get_text("non.existent.key");
175    /// assert_eq!(missing, "non.existent.key");
176    /// # Ok::<(), Box<dyn std::error::Error>>(())
177    /// ```
178    pub fn get_text(&self, key: &str) -> String {
179        // Try to get translation from current language
180        if let Some(translation_map) = self.translations.get(&self.current_language)
181            && let Some(value) = self.get_nested_value(translation_map, key)
182            && let Some(text) = value.as_str()
183        {
184            return text.to_string();
185        }
186
187        // Fallback to English if current language doesn't have the key
188        if self.current_language != Language::English
189            && let Some(translation_map) = self.translations.get(&Language::English)
190            && let Some(value) = self.get_nested_value(translation_map, key)
191            && let Some(text) = value.as_str()
192        {
193            return text.to_string();
194        }
195
196        // Final fallback: return the key itself
197        key.to_string()
198    }
199
200    /// Sets the current language without persisting the preference.
201    ///
202    /// Changes the active language for translation lookups. This change is temporary
203    /// and will not be saved to user settings. Use [`set_language_with_persistence`]
204    /// if you want to save the language preference.
205    ///
206    /// # Arguments
207    ///
208    /// * `language` - The language to switch to
209    ///
210    /// # Returns
211    ///
212    /// Returns `Ok(())` on success, or a `LocalizationError` if the language
213    /// is not supported or translations are not available.
214    ///
215    /// # Examples
216    ///
217    /// ```rust
218    /// use inspector_gguf::localization::{LocalizationManager, Language};
219    ///
220    /// let mut manager = LocalizationManager::new()?;
221    ///
222    /// // Temporarily switch to Russian
223    /// manager.set_language(Language::Russian)?;
224    /// let russian_title = manager.get_text("app.title");
225    ///
226    /// // Language preference is not saved to disk
227    /// # Ok::<(), Box<dyn std::error::Error>>(())
228    /// ```
229    ///
230    /// See also [`set_language_with_persistence`] for persistent language changes,
231    /// [`SystemLocaleDetector::detect`] for automatic detection, and [`SettingsManager`]
232    /// for settings management.
233    ///
234    /// [`set_language_with_persistence`]: LocalizationManager::set_language_with_persistence
235    pub fn set_language(&mut self, language: Language) -> Result<(), LocalizationError> {
236        self.current_language = language;
237        Ok(())
238    }
239
240    /// Sets the current language and saves the preference to persistent storage.
241    ///
242    /// This method changes the active language and attempts to save the preference
243    /// to the user's settings file. The saved preference will be restored when
244    /// the application is restarted.
245    ///
246    /// # Arguments
247    ///
248    /// * `language` - The language to switch to and save as preference
249    ///
250    /// # Returns
251    ///
252    /// Returns `Ok(())` on success. If the language change succeeds but saving
253    /// the preference fails, the method still returns `Ok(())` but logs a warning.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the language is not supported or translations are not available.
258    /// Settings save failures are logged but do not cause the method to fail.
259    ///
260    /// # Examples
261    ///
262    /// ```rust
263    /// use inspector_gguf::localization::{LocalizationManager, Language};
264    ///
265    /// let mut manager = LocalizationManager::new()?;
266    ///
267    /// // Switch to Portuguese and save preference
268    /// manager.set_language_with_persistence(Language::PortugueseBrazilian)?;
269    ///
270    /// // Preference will be restored on next application startup
271    /// # Ok::<(), Box<dyn std::error::Error>>(())
272    /// ```
273    pub fn set_language_with_persistence(
274        &mut self,
275        language: Language,
276    ) -> Result<(), LocalizationError> {
277        self.current_language = language;
278
279        // Persist the language preference to settings
280        let settings_manager = SettingsManager::new().unwrap_or_default();
281        if let Err(e) = settings_manager.save_language_preference(language) {
282            eprintln!("Warning: Failed to save language preference: {}", e);
283            // Don't fail the language change if we can't save settings
284        }
285
286        Ok(())
287    }
288
289    /// Returns the currently active language.
290    ///
291    /// # Returns
292    ///
293    /// The currently selected language for translation lookups.
294    ///
295    /// # Examples
296    ///
297    /// ```rust
298    /// use inspector_gguf::localization::{LocalizationManager, Language};
299    ///
300    /// let manager = LocalizationManager::new()?;
301    /// let current = manager.get_current_language();
302    /// println!("Current language: {:?}", current);
303    /// # Ok::<(), Box<dyn std::error::Error>>(())
304    /// ```
305    pub fn get_current_language(&self) -> Language {
306        self.current_language
307    }
308
309    /// Returns a list of all supported languages.
310    ///
311    /// This method returns all languages that the application supports,
312    /// regardless of whether their translation files are currently loaded.
313    ///
314    /// # Returns
315    ///
316    /// A vector containing all supported language variants.
317    ///
318    /// # Examples
319    ///
320    /// ```rust
321    /// use inspector_gguf::localization::LocalizationManager;
322    ///
323    /// let manager = LocalizationManager::new()?;
324    /// let languages = manager.get_available_languages();
325    ///
326    /// for lang in languages {
327    ///     println!("Supported: {} ({})", lang.display_name(), lang.to_code());
328    /// }
329    /// # Ok::<(), Box<dyn std::error::Error>>(())
330    /// ```
331    pub fn get_available_languages(&self) -> Vec<Language> {
332        vec![
333            Language::English,
334            Language::Russian,
335            Language::PortugueseBrazilian,
336        ]
337    }
338
339    /// Loads or replaces translations for a specific language.
340    ///
341    /// This method allows manual loading of translation data, which can be useful
342    /// for testing, dynamic translation loading, or custom translation sources.
343    ///
344    /// # Arguments
345    ///
346    /// * `language` - The language to load translations for
347    /// * `translations` - The translation data as a nested HashMap structure
348    ///
349    /// # Examples
350    ///
351    /// ```rust
352    /// use inspector_gguf::localization::{LocalizationManager, Language};
353    /// use std::collections::HashMap;
354    /// use serde_json::json;
355    ///
356    /// let mut manager = LocalizationManager::new()?;
357    ///
358    /// // Create custom translations
359    /// let mut custom_translations = HashMap::new();
360    /// custom_translations.insert("app".to_string(), json!({"title": "Custom Title"}));
361    ///
362    /// // Load custom translations
363    /// manager.load_translations(Language::English, custom_translations);
364    /// # Ok::<(), Box<dyn std::error::Error>>(())
365    /// ```
366    pub fn load_translations(&mut self, language: Language, translations: TranslationMap) {
367        self.translations.insert(language, translations);
368    }
369
370    /// Retrieves nested values from translation map using dot notation.
371    ///
372    /// This helper method navigates through nested JSON objects using dot-separated
373    /// key paths. For example, "buttons.load" accesses `translations["buttons"]["load"]`.
374    ///
375    /// # Arguments
376    ///
377    /// * `map` - The translation map to search in
378    /// * `key` - Dot-separated key path (e.g., "section.subsection.key")
379    ///
380    /// # Returns
381    ///
382    /// Returns a reference to the JSON value if found, or `None` if the path
383    /// doesn't exist or encounters a non-object value along the way.
384    ///
385    /// # Examples
386    ///
387    /// For a translation structure like:
388    /// ```json
389    /// {
390    ///   "buttons": {
391    ///     "load": "Load File",
392    ///     "save": "Save File"
393    ///   }
394    /// }
395    /// ```
396    ///
397    /// - `get_nested_value(map, "buttons.load")` returns `Some("Load File")`
398    /// - `get_nested_value(map, "buttons.nonexistent")` returns `None`
399    /// - `get_nested_value(map, "nonexistent.key")` returns `None`
400    fn get_nested_value<'a>(&self, map: &'a TranslationMap, key: &str) -> Option<&'a Value> {
401        let parts: Vec<&str> = key.split('.').collect();
402        let mut current_value = None;
403
404        // Start with the root map
405        for (i, part) in parts.iter().enumerate() {
406            if i == 0 {
407                // First part - get from root map
408                current_value = map.get(*part);
409            } else {
410                // Subsequent parts - navigate deeper into nested objects
411                if let Some(Value::Object(obj)) = current_value {
412                    current_value = obj.get(*part);
413                } else {
414                    return None;
415                }
416            }
417        }
418
419        current_value
420    }
421}
422
423impl Default for LocalizationManager {
424    fn default() -> Self {
425        Self::new().unwrap_or_else(|_| LocalizationManager {
426            current_language: Language::English,
427            translations: HashMap::new(),
428        })
429    }
430}