diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs index 5ebc2c9fb96c..bc0ea5bb0a67 100644 --- a/crates/notedeck/src/app.rs +++ b/crates/notedeck/src/app.rs @@ -1,13 +1,12 @@ +use crate::JobPool; use crate::account::FALLBACK_PUBKEY; -use crate::i18n::{LocalizationContext, LocalizationManager}; +use crate::i18n::Localization; use crate::persist::{AppSizeHandler, ZoomHandler}; use crate::wallet::GlobalWallet; use crate::zaps::Zaps; -use crate::JobPool; use crate::{ - frame_history::FrameHistory, AccountStorage, Accounts, AppContext, Args, DataPath, - DataPathType, Directory, Images, NoteAction, NoteCache, RelayDebugView, ThemeHandler, - UnknownIds, + AccountStorage, Accounts, AppContext, Args, DataPath, DataPathType, Directory, Images, + NoteAction, NoteCache, RelayDebugView, ThemeHandler, UnknownIds, frame_history::FrameHistory, }; use egui::Margin; use egui::ThemePreference; @@ -50,7 +49,7 @@ pub struct Notedeck { zaps: Zaps, frame_history: FrameHistory, job_pool: JobPool, - i18n: LocalizationContext, + i18n: Localization, } /// Our chrome, which is basically nothing @@ -243,7 +242,7 @@ impl Notedeck { let i18n = LocalizationContext::new(localization_manager); // Initialize global i18n context - crate::i18n::init_global_i18n(i18n.clone()); + //crate::i18n::init_global_i18n(i18n.clone()); Self { ndb, diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs index 5e63821383cf..f03199f07b9d 100644 --- a/crates/notedeck/src/context.rs +++ b/crates/notedeck/src/context.rs @@ -1,7 +1,7 @@ use crate::{ - account::accounts::Accounts, frame_history::FrameHistory, i18n::LocalizationContext, - wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler, - UnknownIds, + Args, DataPath, Images, JobPool, NoteCache, ThemeHandler, UnknownIds, + account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization, + wallet::GlobalWallet, zaps::Zaps, }; use egui_winit::clipboard::Clipboard; @@ -25,5 +25,5 @@ pub struct AppContext<'a> { pub zaps: &'a mut Zaps, pub frame_history: &'a mut FrameHistory, pub job_pool: &'a mut JobPool, - pub i18n: &'a LocalizationContext, + pub i18n: &'a Localization, } diff --git a/crates/notedeck/src/i18n/manager.rs b/crates/notedeck/src/i18n/manager.rs index d6bfb570a4f7..f1527976a274 100644 --- a/crates/notedeck/src/i18n/manager.rs +++ b/crates/notedeck/src/i18n/manager.rs @@ -1,3 +1,4 @@ +use super::{IntlError, IntlKey, IntlKeyBuf}; use fluent::FluentArgs; use fluent::{FluentBundle, FluentResource}; use fluent_langneg::negotiate_languages; @@ -6,29 +7,48 @@ use std::path::Path; use std::sync::Arc; use unic_langid::LanguageIdentifier; +const EN_XA = lang!("en-XA"); +const EN_US = lang!("en-US"); + +const FTL_EN_XA: u8 = 0; +const FTL_EN_US: u8 = 1; +const NUM_FTLS: u8 = 2; + +struct StaticBundle { + identifier: LanguageIdentifier, + ftl: &'static str +} + +const FTLS: &[StaticBundle; NUM_FTLS] = [ + StaticBundle { identifier: EN_XA, ftl: include_str!("../../../../assets/translations/en-XA/main.ftl") }, + StaticBundle { identifier: EN_US, ftl: include_str!("../../../../assets/translations/en-US/main.ftl") }, +]; + /// Manages localization resources and provides localized strings -pub struct LocalizationManager { +pub struct Localization { /// Current locale current_locale: LanguageIdentifier, /// Available locales available_locales: Vec, /// Fallback locale fallback_locale: LanguageIdentifier, - /// Resource directory path - resource_dir: std::path::PathBuf, + /// Cached parsed FluentResource per locale - resource_cache: HashMap>, + ///resource_cache: HashMap, + /// Cached string results per locale (only for strings without arguments) string_cache: HashMap>, + /// Cached normalized keys + normalized_key_cache: HashMap, + /// Bundles + bundles: HashMap } -impl LocalizationManager { - /// Creates a new LocalizationManager with the specified resource directory - pub fn new(resource_dir: &Path) -> Result> { +impl Localization { + /// Creates a new Localization with the specified resource directory + pub fn new(resource_dir: &Path) -> Result { // Default to English (US) - let default_locale: LanguageIdentifier = "en-US" - .parse() - .map_err(|e| format!("Locale parse error: {e:?}"))?; + let default_locale = EN_US; let fallback_locale = default_locale.clone(); // Check if pseudolocale is enabled via environment variable @@ -39,10 +59,8 @@ impl LocalizationManager { // Add en-XA if pseudolocale is enabled if enable_pseudolocale { - let pseudolocale: LanguageIdentifier = "en-XA" - .parse() - .map_err(|e| format!("Pseudolocale parse error: {e:?}"))?; - available_locales.push(pseudolocale); + let pseudolocale = EN_XA; + available_locales.push(pseudolocale.clone()); tracing::info!( "Pseudolocale (en-XA) enabled via NOTEDECK_PSEUDOLOCALE environment variable" ); @@ -52,31 +70,30 @@ impl LocalizationManager { current_locale: default_locale, available_locales, fallback_locale, - resource_dir: resource_dir.to_path_buf(), - resource_cache: HashMap::new(), string_cache: HashMap::new(), }) } /// Gets a localized string by its ID - pub fn get_string(&self, id: &str) -> String { + pub fn get_string<'a>(&'a self, id: IntlKey<'a>) -> &'a str { tracing::debug!( "Getting string '{}' for locale '{}'", id, self.get_current_locale() ); + let result = self.get_string_with_args(id, None); - if let Err(ref e) = result { + if let Err(e) = result { tracing::error!("Failed to get string '{}': {}", id, e); } - result + result.unwrap_or(id.as_str()) } /// Loads and caches a parsed FluentResource for the given locale fn load_resource_for_locale( &self, locale: &LanguageIdentifier, - ) -> Result, Box> { + ) -> Result { // Construct the path using the stored resource directory let expected_path = self.resource_dir.join(format!("{locale}/main.ftl")); @@ -87,16 +104,16 @@ impl LocalizationManager { expected_path.display(), e ); - return Err(format!("Failed to open FTL file: {e}").into()); + return Err(IntlError::ReadFtl(format!("{e}"))); } // Load the FTL file directly instead of using ResourceManager let ftl_string = std::fs::read_to_string(&expected_path) - .map_err(|e| format!("Failed to read FTL file: {e}"))?; + .map_err(|e| IntlError::ReadFtl(format!("{e}")))?; // Parse the FTL content let resource = FluentResource::try_new(ftl_string) - .map_err(|e| format!("Failed to parse FTL content: {e:?}"))?; + .map_err(|e| IntlError::ReadFtl(format!("failed to parse FTL content: {e:?}")))?; tracing::debug!( "Loaded and cached parsed FluentResource for locale: {}", @@ -106,9 +123,7 @@ impl LocalizationManager { } /// Gets cached parsed FluentResource for the current locale, loading it if necessary - fn get_cached_resource( - &mut self, - ) -> Result, Box> { + fn get_cached_resource(&mut self) -> Result, IntlError> { // Try to get from cache first { if let Some(resource) = self.resource_cache.get(&self.current_locale) { @@ -134,12 +149,23 @@ impl LocalizationManager { Ok(resource) } + fn load_bundle(lang: LanguageIdentifier) -> Result { + let mut bundle = FluentBundle::new(lang); + for ftl in FTLS { + if ftl.identifier == lang { + break; + } + } + + Err(IntlError::No) + } + /// Gets cached string result, or formats it and caches the result - fn get_cached_string( + fn get_cached_string<'intl, 'key>( &mut self, - id: &str, + id: IntlKey<'key>, args: Option<&FluentArgs>, - ) -> Result> { + ) -> Result<&'intl , IntlError> { // Only cache simple strings without arguments // For strings with arguments, we can't cache the final result since args may vary if args.is_none() { @@ -160,18 +186,27 @@ impl LocalizationManager { let resource = self.get_cached_resource()?; // Create a bundle for this request (not cached due to thread-safety issues) - let mut bundle = FluentBundle::new(vec![self.current_locale.clone()]); + + let bundle = if let Some(bundle) = self.bundles.get(&self.current_locale) else { + bundle + } else { + let mut bundle = FluentBundle::new(vec![self.current_locale.clone()]); + bundle.add_resource(resource.as_ref()); + self.bundles.insert() + }; + + let mut bundle = self.bundles.entry() FluentBundle::new(vec![self.current_locale.clone()]); bundle .add_resource(resource.as_ref()) .map_err(|e| format!("Failed to add resource to bundle: {e:?}"))?; let message = bundle .get_message(id) - .ok_or_else(|| format!("Message not found: {id}"))?; + .ok_or_else(|| IntlError::NotFound(IntlKey::new(id)))?; let pattern = message .value() - .ok_or_else(|| format!("Message has no value: {id}"))?; + .ok_or_else(|| IntlError::NoValue(IntlKey::new(id)))?; // Format the message let mut errors = Vec::new(); @@ -181,8 +216,6 @@ impl LocalizationManager { tracing::warn!("Localization errors for {}: {:?}", id, errors); } - let result_string = result.into_owned(); - // Only cache simple strings without arguments // This prevents caching issues when the same message ID is used with different arguments if args.is_none() { @@ -190,7 +223,7 @@ impl LocalizationManager { .string_cache .entry(self.current_locale.clone()) .or_insert_with(HashMap::new); - locale_cache.insert(id.to_string(), result_string.clone()); + locale_cache.insert(id.to_string(), result_string.to_owned()); tracing::debug!( "Cached string result for '{}' in locale: {}", id, @@ -204,19 +237,16 @@ impl LocalizationManager { } /// Gets a localized string by its ID with optional arguments - pub fn get_string_with_args( - &self, - id: &str, + pub fn get_string_with_args<'intl, 'key>( + &'intl self, + id: IntlKey<'key>, args: Option<&FluentArgs>, - ) -> Result<&str, Box> { + ) -> Result<&'intl str, IntlError> { self.get_cached_string(id, args) } /// Sets the current locale - pub fn set_locale( - &mut self, - locale: LanguageIdentifier, - ) -> Result<(), Box> { + pub fn set_locale(&mut self, locale: LanguageIdentifier) -> Result<(), IntlError> { tracing::info!("Attempting to set locale to: {}", locale); tracing::info!("Available locales: {:?}", self.available_locales); @@ -227,7 +257,7 @@ impl LocalizationManager { locale, self.available_locales ); - return Err(format!("Locale {locale} is not available").into()); + return Err(IntlError::LocaleNotAvailable(locale)); } tracing::info!("Switching locale from {} to {locale}", &self.current_locale,); @@ -313,94 +343,6 @@ impl LocalizationManager { } } -/// Context for sharing localization across the application -#[derive(Clone)] -pub struct LocalizationContext { - /// The localization manager - manager: Arc, -} - -impl LocalizationContext { - /// Creates a new LocalizationContext - pub fn new(manager: Arc) -> Self { - let context = Self { manager }; - - // Auto-switch to pseudolocale if environment variable is set - if std::env::var("NOTEDECK_PSEUDOLOCALE").is_ok() { - tracing::info!("NOTEDECK_PSEUDOLOCALE environment variable detected"); - if let Ok(pseudolocale) = "en-XA".parse::() { - tracing::info!("Attempting to switch to pseudolocale: {}", pseudolocale); - if let Err(e) = context.set_locale(pseudolocale) { - tracing::warn!("Failed to switch to pseudolocale: {}", e); - } else { - tracing::info!("Automatically switched to pseudolocale (en-XA)"); - } - } else { - tracing::error!("Failed to parse en-XA as LanguageIdentifier"); - } - } else { - tracing::info!("NOTEDECK_PSEUDOLOCALE environment variable not set"); - } - - context - } - - /// Gets a localized string by its ID - pub fn get_string(&self, id: &str) -> Option { - self.manager.get_string(id).ok() - } - - /// Gets a localized string by its ID with optional arguments - pub fn get_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String { - self.manager - .get_string_with_args(id, args) - .unwrap_or_else(|_| format!("[MISSING: {id}]")) - } - - /// Sets the current locale - pub fn set_locale( - &self, - locale: LanguageIdentifier, - ) -> Result<(), Box> { - self.manager.set_locale(locale) - } - - /// Gets the current locale - pub fn get_current_locale(&self) -> &LanguageIdentifier { - self.manager.get_current_locale() - } - - /// Clears the resource cache (useful for development when FTL files change) - pub fn clear_cache(&self) -> Result<(), Box> { - self.manager.clear_cache() - } - - /// Gets the underlying manager - pub fn manager(&self) -> &Arc { - &self.manager - } -} - -/// Trait for objects that can be localized -pub trait Localizable { - /// Gets a localized string by its ID - fn get_localized_string(&self, id: &str) -> String; - - /// Gets a localized string by its ID with optional arguments - fn get_localized_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String; -} - -impl Localizable for LocalizationContext { - fn get_localized_string(&self, id: &str) -> String { - self.get_string(id) - .unwrap_or_else(|| format!("[MISSING: {id}]")) - } - - fn get_localized_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String { - self.get_string_with_args(id, args) - } -} - /// Statistics about cache usage #[derive(Debug, Clone)] pub struct CacheStats { @@ -418,7 +360,7 @@ mod tests { let temp_dir = std::env::temp_dir().join("notedeck_i18n_test"); std::fs::create_dir_all(&temp_dir).unwrap(); - let manager = LocalizationManager::new(&temp_dir); + let manager = Localization::new(&temp_dir); assert!(manager.is_ok()); // Cleanup @@ -430,7 +372,7 @@ mod tests { let temp_dir = std::env::temp_dir().join("notedeck_i18n_test2"); std::fs::create_dir_all(&temp_dir).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // Test default locale let current = manager.get_current_locale().unwrap(); @@ -456,7 +398,7 @@ mod tests { let ftl_content = "test_key = Test Value\nanother_key = Another Value"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // First call should load and cache the FTL content let result1 = manager.get_string("test_key"); @@ -488,7 +430,7 @@ mod tests { let ftl_content = "test_key = Test Value"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // Load and cache the FTL content let result1 = manager.get_string("test_key"); @@ -518,7 +460,7 @@ mod tests { let ftl_content = "test_key = Test Value"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = Arc::new(LocalizationManager::new(&temp_dir).unwrap()); + let manager = Arc::new(Localization::new(&temp_dir).unwrap()); let context = LocalizationContext::new(manager); // Debug: check what the normalized key should be @@ -560,7 +502,7 @@ mod tests { let ftl_content = "test_key = Test Value\nanother_key = Another Value"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // First call should create bundle and cache the resource let result1 = manager.get_string("test_key"); @@ -592,7 +534,7 @@ mod tests { let ftl_content = "test_key = Test Value"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // First call should format and cache the string let result1 = manager.get_string("test_key"); @@ -629,7 +571,7 @@ mod tests { // Enable pseudolocale for this test std::env::set_var("NOTEDECK_PSEUDOLOCALE", "1"); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // Check that caches are populated let stats1 = manager.get_cache_stats().unwrap(); @@ -660,7 +602,7 @@ mod tests { let ftl_content = "welcome_message = Welcome {$name}!"; std::fs::write(en_us_dir.join("main.ftl"), ftl_content).unwrap(); - let manager = LocalizationManager::new(&temp_dir).unwrap(); + let manager = Localization::new(&temp_dir).unwrap(); // First call with arguments should not be cached let mut args = fluent::FluentArgs::new(); diff --git a/crates/notedeck/src/i18n/mod.rs b/crates/notedeck/src/i18n/mod.rs index d3a4eb23750b..8f65f8ef30c0 100644 --- a/crates/notedeck/src/i18n/mod.rs +++ b/crates/notedeck/src/i18n/mod.rs @@ -4,11 +4,15 @@ //! It handles loading translation files, managing locales, and providing //! localized strings throughout the application. +mod error; +mod key; pub mod manager; +pub use error::IntlError; +pub use key::{IntlKey, IntlKeyBuf}; + pub use manager::CacheStats; -pub use manager::LocalizationContext; -pub use manager::LocalizationManager; +pub use manager::Localization; /// Re-export commonly used types for convenience pub use fluent::FluentArgs; @@ -23,8 +27,9 @@ use std::sync::Arc; use std::sync::Mutex; use tracing::info; +/* /// Global localization manager for easy access from anywhere -static GLOBAL_I18N: OnceCell> = OnceCell::new(); +static GLOBAL_I18N: OnceCell> = OnceCell::new(); /// Cache for normalized FTL keys to avoid repeated normalization static NORMALIZED_KEY_CACHE: OnceCell>> = OnceCell::new(); @@ -39,11 +44,7 @@ pub fn init_global_i18n(context: LocalizationContext) { info!("Global i18n context initialized successfully"); } - -/// Get the global localization manager -pub fn get_global_i18n() -> Option> { - GLOBAL_I18N.get().cloned() -} +*/ fn simple_hash(s: &str) -> String { let digest = md5::compute(s.as_bytes()); @@ -51,15 +52,19 @@ fn simple_hash(s: &str) -> String { format!("{:02x}{:02x}", digest[0], digest[1]) } -pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String { +pub fn normalize_ftl_key<'a>( + manager: &Localization, + key: &str, + comment: Option<&str>, +) -> IntlKey<'a> { // Try to get from cache first let cache_key = if let Some(comment) = comment { format!("{key}:{comment}") } else { - key.to_string() + IntlKey::new(key) }; - if let Some(cache) = NORMALIZED_KEY_CACHE.get() { + if let Some(cache) = manager.normalized_key_cache.get() { if let Ok(cache) = cache.lock() { if let Some(cached) = cache.get(&cache_key) { return cached.clone(); @@ -86,20 +91,25 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String { result.push_str(&hash_str); } - // Cache the result - if let Some(cache) = NORMALIZED_KEY_CACHE.get() { - if let Ok(mut cache) = cache.lock() { - cache.insert(cache_key, result.clone()); - } - } - tracing::debug!( "normalize_ftl_key: original='{}', comment='{:?}', final='{}'", key, comment, result ); - result + + if let Some(key) = manager.normalized_key_cache.get(&cache_key) { + key.borrow() + } else { + manager + .normalized_key_cache + .insert(cache_key, result.clone()); + manager + .normalized_key_cache + .get(cache_key) + .unwrap() + .borrow() + } } /// Macro for getting localized strings with format-like syntax @@ -115,22 +125,9 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String { #[macro_export] macro_rules! tr { // Simple case: just message and comment - ($message:expr, $comment:expr) => { + ($i18n:expr, $message:expr, $comment:expr) => { { - let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment)); - if let Some(i18n) = $crate::i18n::get_global_i18n() { - let result = i18n.get_string(&norm_key); - match result { - Ok(ref s) if s != $message => s.clone(), - _ => { - tracing::warn!("FALLBACK: Using key '{}' as string (not found in FTL)", $message); - $message.to_string() - } - } - } else { - tracing::warn!("FALLBACK: Global i18n not initialized, using key '{}' as string", $message); - $message.to_string() - } + i18n.get_string($crate::i18n::normalize_ftl_key($message, Some($comment))); } }; diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs index a079f01abd76..58c88bfc2eca 100644 --- a/crates/notedeck/src/lib.rs +++ b/crates/notedeck/src/lib.rs @@ -35,21 +35,17 @@ mod user_account; mod wallet; mod zaps; +pub use account::FALLBACK_PUBKEY; pub use account::accounts::{AccountData, AccountSubs, Accounts}; pub use account::contacts::{ContactState, IsFollowing}; pub use account::relay::RelayAction; -pub use account::FALLBACK_PUBKEY; pub use app::{App, AppAction, Notedeck}; pub use args::Args; pub use context::AppContext; -pub use error::{show_one_error_message, Error, FilterError, ZapError}; +pub use error::{Error, FilterError, ZapError, show_one_error_message}; pub use filter::{FilterState, FilterStates, UnifiedSubscription}; pub use fonts::NamedFontFamily; -pub use i18n::manager::Localizable; -pub use i18n::{ - CacheStats, FluentArgs, FluentValue, LanguageIdentifier, LocalizationContext, - LocalizationManager, -}; +pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization}; pub use imgcache::{ Animation, GifState, GifStateMap, ImageFrame, Images, LoadableTextureState, MediaCache, MediaCacheType, TextureFrame, TextureState, TexturedImage, TexturesCache, @@ -72,16 +68,16 @@ pub use style::NotedeckTextStyle; pub use theme::ColorTheme; pub use time::time_ago_since; pub use timecache::TimeCached; -pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds}; -pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes}; +pub use unknowns::{NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds, get_unknown_note_ids}; +pub use urls::{SupportedMimeType, UrlMimes, supported_mime_hosted_at_url}; pub use user_account::UserAccount; pub use wallet::{ - get_current_wallet, get_wallet_for, GlobalWallet, Wallet, WalletError, WalletType, - WalletUIState, ZapWallet, + GlobalWallet, Wallet, WalletError, WalletType, WalletUIState, ZapWallet, get_current_wallet, + get_wallet_for, }; pub use zaps::{ - get_current_default_msats, AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget, - NoteZapTargetOwned, PendingDefaultZapState, ZapTarget, ZapTargetOwned, ZappingError, + AnyZapState, DefaultZapError, DefaultZapMsats, NoteZapTarget, NoteZapTargetOwned, + PendingDefaultZapState, ZapTarget, ZapTargetOwned, ZappingError, get_current_default_msats, }; // export libs @@ -89,5 +85,3 @@ pub use enostr; pub use nostrdb; pub use zaps::Zaps; - -pub use crate::i18n::{get_global_i18n, init_global_i18n}; diff --git a/crates/notedeck_ui/src/note/context.rs b/crates/notedeck_ui/src/note/context.rs index 4f59f6e79a4c..fdf19a51a681 100644 --- a/crates/notedeck_ui/src/note/context.rs +++ b/crates/notedeck_ui/src/note/context.rs @@ -109,15 +109,6 @@ impl NoteContextButton { ) -> Option { let mut context_selection: Option = None; - // Debug: Check if global i18n is available - if let Some(i18n) = notedeck::i18n::get_global_i18n() { - if let Ok(locale) = i18n.get_current_locale() { - tracing::debug!("Current locale in context menu: {}", locale); - } - } else { - tracing::warn!("Global i18n context not available in context menu"); - } - stationary_arbitrary_menu_button(ui, button_response, |ui| { ui.set_max_width(200.0);