patois 0.2.1

A lightweight, domain-aware i18n library for Rust desktop applications.
Documentation
//! wxWidgets-specific helpers for apps that use `patois` for translations, gated behind
//! the `ui` feature. Generalizes the patois+wxDragon wiring an app would otherwise have to
//! reimplement itself: activating a locale (both the app's own catalog and wxWidgets' own
//! `wxstd` UI strings via [`crate::embed_wx_translations!`]), and populating a language
//! selector control.

use wxdragon::{translations::Translations, widgets::choice::Choice};

use crate::{LanguageInfo, LanguageManager};

/// Owns runtime language state for a patois domain in a wxDragon app: which languages are
/// available, which one is active, and keeping `wxdragon::translations::Translations` (both
/// the app's own catalog and wxWidgets' own `wxstd` strings) in sync with it.
///
/// Construct once per app (typically behind a `OnceLock`/`Mutex` singleton owned by the
/// app, since that lifecycle is app state, not something this type manages itself) and call
/// [`Self::initialize`] before creating any UI.
pub struct WxTranslationManager {
	domain: String,
	current_language: String,
	available_languages: Vec<LanguageInfo>,
	initialized: bool,
}

impl WxTranslationManager {
	/// Create a manager for the given patois domain (e.g. `"paperback"`).
	#[must_use]
	pub fn new(domain: &str) -> Self {
		Self {
			domain: domain.to_string(),
			current_language: "en".to_string(),
			available_languages: vec![LanguageInfo { code: "en".to_string(), name: "English".to_string() }],
			initialized: false,
		}
	}

	/// Detects the system language (falling back to English if unavailable), activates it for
	/// both the app's own domain and wxWidgets' own strings, and installs `wxstd_loader` so
	/// `Translations::add_std_catalog` can find them. Idempotent: subsequent calls are no-ops.
	///
	/// `wxstd_loader` is normally the `WxStdCatalogLoader` type generated by
	/// [`crate::embed_wx_translations!`] in the calling crate.
	pub fn initialize(&mut self, wxstd_loader: impl wxdragon::translations::TranslationsLoader + 'static) -> bool {
		if self.initialized {
			return true;
		}
		let translations = Translations::new();
		// Must be installed before add_std_catalog so wx consults it for the language.
		translations.set_loader(wxstd_loader);
		let mgr = LanguageManager::new(&self.domain);
		self.available_languages = mgr.available();
		let raw_sys_lang = LanguageManager::system_language();
		let sys_lang = raw_sys_lang.split('_').next().unwrap_or(&raw_sys_lang).to_string();
		self.current_language = if self.is_language_available(&sys_lang) { sys_lang } else { "en".to_string() };
		if self.current_language != "en" {
			translations.set_language_str(&self.current_language);
		}
		translations.add_std_catalog();
		Translations::set_global(translations);
		crate::set_default_domain(&self.domain);
		crate::set_locale(&self.current_language);
		self.initialized = true;
		true
	}

	/// Switches the active language, replacing the global `Translations` (a fresh instance
	/// needs its own loader installed before `add_std_catalog` picks the language catalog).
	/// Returns `false` if not yet initialized or `language_code` isn't available.
	pub fn set_language(
		&mut self,
		language_code: &str,
		wxstd_loader: impl wxdragon::translations::TranslationsLoader + 'static,
	) -> bool {
		if !self.initialized || !self.is_language_available(language_code) {
			return false;
		}
		self.current_language = language_code.to_string();
		let translations = Translations::new();
		translations.set_loader(wxstd_loader);
		translations.set_language_str(language_code);
		translations.add_std_catalog();
		Translations::set_global(translations);
		crate::set_locale(language_code);
		true
	}

	#[must_use]
	pub fn current_language(&self) -> String {
		self.current_language.clone()
	}

	#[must_use]
	pub fn available_languages(&self) -> Vec<LanguageInfo> {
		self.available_languages.clone()
	}

	#[must_use]
	pub fn is_language_available(&self, language_code: &str) -> bool {
		self.available_languages.iter().any(|lang| lang.code == language_code)
	}
}

/// Populates `combo` with each language's display name and returns the parallel list of
/// codes (same index as the combo's items), for later lookup via [`resolve_language_choice`].
pub fn populate_language_choice(combo: &Choice, languages: &[LanguageInfo]) -> Vec<String> {
	let mut codes = Vec::with_capacity(languages.len());
	for lang in languages {
		combo.append(&lang.name);
		codes.push(lang.code.clone());
	}
	codes
}

/// Resolves `combo`'s current selection back to a language code via the parallel `codes`
/// list returned by [`populate_language_choice`]. `None` if nothing is selected or the
/// selection index is out of range.
#[must_use]
pub fn resolve_language_choice(combo: &Choice, codes: &[String]) -> Option<String> {
	combo.get_selection().and_then(|index| usize::try_from(index).ok()).and_then(|index| codes.get(index).cloned())
}

#[cfg(test)]
mod tests {
	use std::borrow::Cow;

	use wxdragon::translations::TranslationsLoader;

	use super::*;

	/// A no-op loader for exercising `WxTranslationManager`'s state machine without needing
	/// a real macro-generated `WxStdCatalogLoader` (which only exists in a consuming app
	/// crate that actually calls `embed_wx_translations!()`).
	struct NoopLoader;

	impl TranslationsLoader for NoopLoader {
		fn load_catalog(&self, _domain: &str, _lang: &str) -> Option<Cow<'_, [u8]>> {
			None
		}

		fn available_translations(&self, _domain: &str) -> Vec<String> {
			Vec::new()
		}
	}

	#[test]
	fn new_manager_has_english_available_by_default() {
		let manager = WxTranslationManager::new("test_domain");
		assert_eq!(manager.current_language(), "en");
		assert!(manager.is_language_available("en"));
		assert!(!manager.is_language_available("zz"));
	}

	#[test]
	fn set_language_fails_when_not_initialized() {
		let mut manager = WxTranslationManager::new("test_domain");
		assert!(!manager.set_language("en", NoopLoader));
		assert_eq!(manager.current_language(), "en");
	}

	#[test]
	fn available_languages_returns_clone() {
		let manager = WxTranslationManager::new("test_domain");
		let mut langs = manager.available_languages();
		langs.push(LanguageInfo { code: "xx".to_string(), name: "Fake".to_string() });
		assert!(!manager.is_language_available("xx"));
	}
}