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}