commit 3d4db820b49f30c292790b3e7668b8c239f0cd37
parent d1e222f732e7004bf1a92b8efc85e32b26ba6a2f
Author: William Casarin <jb55@jb55.com>
Date: Sun, 29 Jun 2025 11:05:31 -0700
i18n: make localization context non-global
- Simplify Localization{Context,Manager} to just Localization
- Fixed a bunch of lifetime issueo
- Removed all Arcs and Locks
- Removed globals
* widgets now need access to &mut Localization for i18n
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
47 files changed, 1405 insertions(+), 1157 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -4340,6 +4340,12 @@ dependencies = [
]
[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6064,6 +6070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
dependencies = [
"unic-langid-impl",
+ "unic-langid-macros",
]
[[package]]
@@ -6076,6 +6083,30 @@ dependencies = [
]
[[package]]
+name = "unic-langid-macros"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25"
+dependencies = [
+ "proc-macro-hack",
+ "tinystr",
+ "unic-langid-impl",
+ "unic-langid-macros-impl",
+]
+
+[[package]]
+name = "unic-langid-macros-impl"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5"
+dependencies = [
+ "proc-macro-hack",
+ "quote",
+ "syn 2.0.104",
+ "unic-langid-impl",
+]
+
+[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -65,7 +65,7 @@ tracing = { version = "0.1.40", features = ["log"] }
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tempfile = "3.13.0"
-unic-langid = "0.9.6"
+unic-langid = { version = "0.9.6", features = ["macros"] }
url = "2.5.2"
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4"] }
diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs
@@ -1,5 +1,5 @@
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;
@@ -18,7 +18,6 @@ use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
-use std::sync::Arc;
use tracing::{error, info};
pub enum AppAction {
@@ -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
@@ -231,19 +230,10 @@ impl Notedeck {
let job_pool = JobPool::default();
// Initialize localization
- let i18n_resource_dir = Path::new("assets/translations");
- let localization_manager = Arc::new(
- LocalizationManager::new(i18n_resource_dir).unwrap_or_else(|e| {
- error!("Failed to initialize localization manager: {}", e);
- // Create a fallback manager with a temporary directory
- LocalizationManager::new(&std::env::temp_dir().join("notedeck_i18n_fallback"))
- .expect("Failed to create fallback localization manager")
- }),
- );
- let i18n = LocalizationContext::new(localization_manager);
+ let i18n = Localization::new();
// Initialize global i18n context
- crate::i18n::init_global_i18n(i18n.clone());
+ //crate::i18n::init_global_i18n(i18n.clone());
Self {
ndb,
@@ -289,7 +279,7 @@ impl Notedeck {
zaps: &mut self.zaps,
frame_history: &mut self.frame_history,
job_pool: &mut self.job_pool,
- i18n: &self.i18n,
+ i18n: &mut self.i18n,
}
}
diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs
@@ -1,5 +1,5 @@
use crate::{
- account::accounts::Accounts, frame_history::FrameHistory, i18n::LocalizationContext,
+ account::accounts::Accounts, frame_history::FrameHistory, i18n::Localization,
wallet::GlobalWallet, zaps::Zaps, Args, DataPath, Images, JobPool, NoteCache, ThemeHandler,
UnknownIds,
};
@@ -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 mut Localization,
}
diff --git a/crates/notedeck/src/i18n/error.rs b/crates/notedeck/src/i18n/error.rs
@@ -0,0 +1,24 @@
+use super::IntlKeyBuf;
+use unic_langid::LanguageIdentifier;
+
+/// App related errors
+#[derive(thiserror::Error, Debug)]
+pub enum IntlError {
+ #[error("message not found: {0}")]
+ NotFound(IntlKeyBuf),
+
+ #[error("message has no value: {0}")]
+ NoValue(IntlKeyBuf),
+
+ #[error("Locale({0}) parse error: {1}")]
+ LocaleParse(LanguageIdentifier, String),
+
+ #[error("locale not available: {0}")]
+ LocaleNotAvailable(LanguageIdentifier),
+
+ #[error("FTL for '{0}' is not available")]
+ NoFtl(LanguageIdentifier),
+
+ #[error("Bundle for '{0}' is not available")]
+ NoBundle(LanguageIdentifier),
+}
diff --git a/crates/notedeck/src/i18n/key.rs b/crates/notedeck/src/i18n/key.rs
@@ -0,0 +1,47 @@
+use std::fmt;
+
+/// An owned key used to lookup i18n translations. Mostly used for errors
+#[derive(Eq, PartialEq, Clone, Debug)]
+pub struct IntlKeyBuf(String);
+
+/// A key used to lookup i18n translations
+#[derive(Eq, PartialEq, Clone, Copy, Debug)]
+pub struct IntlKey<'a>(&'a str);
+
+impl fmt::Display for IntlKey<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // Use `self.number` to refer to each positional data point.
+ write!(f, "{}", self.0)
+ }
+}
+
+impl fmt::Display for IntlKeyBuf {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // Use `self.number` to refer to each positional data point.
+ write!(f, "{}", &self.0)
+ }
+}
+
+impl IntlKeyBuf {
+ pub fn new(string: impl Into<String>) -> Self {
+ IntlKeyBuf(string.into())
+ }
+
+ pub fn borrow<'a>(&'a self) -> IntlKey<'a> {
+ IntlKey::new(&self.0)
+ }
+}
+
+impl<'a> IntlKey<'a> {
+ pub fn new(string: &'a str) -> IntlKey<'a> {
+ IntlKey(string)
+ }
+
+ pub fn to_owned(&self) -> IntlKeyBuf {
+ IntlKeyBuf::new(self.0)
+ }
+
+ pub fn as_str(&self) -> &'a str {
+ self.0
+ }
+}
diff --git a/crates/notedeck/src/i18n/manager.rs b/crates/notedeck/src/i18n/manager.rs
@@ -1,35 +1,55 @@
-use fluent::FluentArgs;
-use fluent::{FluentBundle, FluentResource};
+use super::{IntlError, IntlKey, IntlKeyBuf};
+use fluent::{FluentArgs, FluentBundle, FluentResource};
use fluent_langneg::negotiate_languages;
+use std::borrow::Cow;
use std::collections::HashMap;
-use std::path::Path;
-use std::sync::{Arc, RwLock};
-use unic_langid::LanguageIdentifier;
+use unic_langid::{langid, LanguageIdentifier};
+
+const EN_XA: LanguageIdentifier = langid!("en-XA");
+const EN_US: LanguageIdentifier = langid!("en-US");
+const NUM_FTLS: usize = 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"),
+ },
+];
+
+type Bundle = FluentBundle<FluentResource>;
/// Manages localization resources and provides localized strings
-pub struct LocalizationManager {
+pub struct Localization {
/// Current locale
- current_locale: RwLock<LanguageIdentifier>,
+ current_locale: LanguageIdentifier,
/// Available locales
available_locales: Vec<LanguageIdentifier>,
/// Fallback locale
fallback_locale: LanguageIdentifier,
- /// Resource directory path
- resource_dir: std::path::PathBuf,
- /// Cached parsed FluentResource per locale
- resource_cache: RwLock<HashMap<LanguageIdentifier, Arc<FluentResource>>>,
+
/// Cached string results per locale (only for strings without arguments)
- string_cache: RwLock<HashMap<LanguageIdentifier, HashMap<String, String>>>,
+ string_cache: HashMap<LanguageIdentifier, HashMap<String, String>>,
+ /// Cached normalized keys
+ normalized_key_cache: HashMap<String, IntlKeyBuf>,
+ /// Bundles
+ bundles: HashMap<LanguageIdentifier, Bundle>,
}
-impl LocalizationManager {
- /// Creates a new LocalizationManager with the specified resource directory
- pub fn new(resource_dir: &Path) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
+impl Default for Localization {
+ fn default() -> Self {
// 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();
+ let mut current_locale = default_locale.clone();
// Check if pseudolocale is enabled via environment variable
let enable_pseudolocale = std::env::var("NOTEDECK_PSEUDOLOCALE").is_ok();
@@ -39,200 +59,243 @@ 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);
+ available_locales.push(EN_XA.clone());
+ current_locale = EN_XA.clone();
tracing::info!(
"Pseudolocale (en-XA) enabled via NOTEDECK_PSEUDOLOCALE environment variable"
);
}
- Ok(Self {
- current_locale: RwLock::new(default_locale),
+ Self {
+ current_locale,
available_locales,
fallback_locale,
- resource_dir: resource_dir.to_path_buf(),
- resource_cache: RwLock::new(HashMap::new()),
- string_cache: RwLock::new(HashMap::new()),
- })
+ normalized_key_cache: HashMap::new(),
+ string_cache: HashMap::new(),
+ bundles: HashMap::new(),
+ }
+ }
+}
+
+pub enum StringCacheResult<'a> {
+ Hit(Cow<'a, str>),
+ NeedsInsert(IntlKeyBuf, String),
+}
+
+impl Localization {
+ /// Creates a new Localization with the specified resource directory
+ pub fn new() -> Self {
+ Localization::default()
}
/// Gets a localized string by its ID
- pub fn get_string(&self, id: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
- 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 {
- tracing::error!("Failed to get string '{}': {}", id, e);
+ pub fn get_string(&mut self, id: IntlKey<'_>) -> Result<String, IntlError> {
+ self.get_cached_string(id, None)
+ }
+
+ /// Load a fluent bundle given a language identifier. Only looks in the static
+ /// ftl files baked into the binary
+ fn load_bundle(lang: &LanguageIdentifier) -> Result<Bundle, IntlError> {
+ for ftl in &FTLS {
+ if &ftl.identifier == lang {
+ let mut bundle = FluentBundle::new(vec![lang.to_owned()]);
+ let resource = FluentResource::try_new(ftl.ftl.to_string());
+ match resource {
+ Err((resource, errors)) => {
+ for error in errors {
+ tracing::error!("load_bundle ({lang}): {error}");
+ }
+
+ tracing::warn!("load_bundle ({}: loading bundle with errors", lang);
+ if let Err(errs) = bundle.add_resource(resource) {
+ for err in errs {
+ tracing::error!("adding resource: {err}");
+ }
+ }
+ }
+
+ Ok(resource) => {
+ tracing::info!("loaded {} bundle OK!", lang);
+ if let Err(errs) = bundle.add_resource(resource) {
+ for err in errs {
+ tracing::error!("adding resource 2: {err}");
+ }
+ }
+ }
+ }
+
+ return Ok(bundle);
+ }
}
- result
+
+ // no static ftl for this LanguageIdentifier
+ Err(IntlError::NoFtl(lang.to_owned()))
}
- /// Loads and caches a parsed FluentResource for the given locale
- fn load_resource_for_locale(
- &self,
- locale: &LanguageIdentifier,
- ) -> Result<Arc<FluentResource>, Box<dyn std::error::Error + Send + Sync>> {
- // Construct the path using the stored resource directory
- let expected_path = self.resource_dir.join(format!("{locale}/main.ftl"));
+ fn get_bundle<'a>(&'a self, lang: &LanguageIdentifier) -> &'a Bundle {
+ self.bundles
+ .get(lang)
+ .expect("make sure to call ensure_bundle!")
+ }
- // Try to open the file directly
- if let Err(e) = std::fs::File::open(&expected_path) {
- tracing::error!(
- "Direct file open failed: {} ({})",
- expected_path.display(),
- e
- );
- return Err(format!("Failed to open FTL file: {e}").into());
+ fn has_bundle(&self, lang: &LanguageIdentifier) -> bool {
+ self.bundles.contains_key(lang)
+ }
+
+ fn try_load_bundle(&mut self, lang: &LanguageIdentifier) -> Result<(), IntlError> {
+ self.bundles
+ .insert(lang.to_owned(), Self::load_bundle(lang)?);
+ Ok(())
+ }
+
+ pub fn normalized_ftl_key(&mut self, key: &str, comment: &str) -> IntlKeyBuf {
+ match self.get_ftl_key(key) {
+ Some(intl_key) => intl_key,
+ None => {
+ self.insert_ftl_key(key, comment);
+ self.get_ftl_key(key).unwrap()
+ }
}
+ }
- // 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}"))?;
+ fn get_ftl_key(&self, cache_key: &str) -> Option<IntlKeyBuf> {
+ self.normalized_key_cache.get(cache_key).cloned()
+ }
+
+ fn insert_ftl_key(&mut self, cache_key: &str, comment: &str) {
+ let mut result = fixup_key(cache_key);
+
+ // Ensure the key starts with a letter (Fluent requirement)
+ if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
+ result = format!("k_{result}");
+ }
- // Parse the FTL content
- let resource = FluentResource::try_new(ftl_string)
- .map_err(|e| format!("Failed to parse FTL content: {e:?}"))?;
+ // If we have a comment, append a hash of it to reduce collisions
+ let hash_str = format!("_{}", simple_hash(comment));
+ result.push_str(&hash_str);
tracing::debug!(
- "Loaded and cached parsed FluentResource for locale: {}",
- locale
+ "normalize_ftl_key: original='{}', final='{}'",
+ cache_key,
+ result
);
- Ok(Arc::new(resource))
- }
-
- /// Gets cached parsed FluentResource for the current locale, loading it if necessary
- fn get_cached_resource(
- &self,
- ) -> Result<Arc<FluentResource>, Box<dyn std::error::Error + Send + Sync>> {
- let locale = self
- .current_locale
- .read()
- .map_err(|e| format!("Lock error: {e}"))?;
-
- // Try to get from cache first
- {
- let cache = self
- .resource_cache
- .read()
- .map_err(|e| format!("Cache lock error: {e}"))?;
- if let Some(resource) = cache.get(&locale) {
- tracing::debug!("Using cached parsed FluentResource for locale: {}", locale);
- return Ok(resource.clone());
+
+ self.normalized_key_cache
+ .insert(cache_key.to_owned(), IntlKeyBuf::new(result));
+ }
+
+ fn get_cached_string_no_args<'key>(
+ &'key self,
+ lang: &LanguageIdentifier,
+ id: IntlKey<'key>,
+ ) -> Result<Cow<'key, str>, IntlError> {
+ // Try to get from string cache first
+ if let Some(locale_cache) = self.string_cache.get(lang) {
+ if let Some(cached_string) = locale_cache.get(id.as_str()) {
+ /*
+ tracing::trace!(
+ "Using cached string result for '{}' in locale: {}",
+ id,
+ &lang
+ );
+ */
+
+ return Ok(Cow::Borrowed(cached_string));
}
}
- // Not in cache, load and cache it
- let resource = self.load_resource_for_locale(&locale)?;
-
- // Store in cache
- {
- let mut cache = self
- .resource_cache
- .write()
- .map_err(|e| format!("Cache lock error: {e}"))?;
- cache.insert(locale.clone(), resource.clone());
- tracing::debug!("Cached parsed FluentResource for locale: {}", locale);
+ Err(IntlError::NotFound(id.to_owned()))
+ }
+
+ fn ensure_bundle(&mut self) -> Result<(), IntlError> {
+ let locale = self.current_locale.clone();
+ if !self.has_bundle(&locale) {
+ match self.try_load_bundle(&locale) {
+ Err(err) => {
+ tracing::warn!(
+ "tried to load bundle {} but failed with '{err}'. using fallback {}",
+ &locale,
+ &self.fallback_locale
+ );
+ self.try_load_bundle(&locale)
+ .expect("failed to load fallback bundle!?");
+ Ok(())
+ }
+
+ Ok(()) => Ok(()),
+ }
+ } else {
+ Ok(())
+ }
+ }
+
+ fn get_current_bundle(&self) -> &Bundle {
+ if self.has_bundle(&self.current_locale) {
+ return self.get_bundle(&self.current_locale);
}
- Ok(resource)
+ self.get_bundle(&self.fallback_locale)
}
/// Gets cached string result, or formats it and caches the result
- fn get_cached_string(
- &self,
- id: &str,
+ pub fn get_cached_string(
+ &mut self,
+ id: IntlKey<'_>,
args: Option<&FluentArgs>,
- ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
- let locale = self
- .current_locale
- .read()
- .map_err(|e| format!("Lock error: {e}"))?;
+ ) -> Result<String, IntlError> {
+ self.ensure_bundle()?;
- // 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() {
- // Try to get from string cache first
- {
- let cache = self
- .string_cache
- .read()
- .map_err(|e| format!("String cache lock error: {e}"))?;
- if let Some(locale_cache) = cache.get(&locale) {
- if let Some(cached_string) = locale_cache.get(id) {
- tracing::debug!(
- "Using cached string result for '{}' in locale: {}",
- id,
- locale
- );
- return Ok(cached_string.clone());
- }
- }
+ if let Ok(result) = self.get_cached_string_no_args(&self.current_locale, id) {
+ return Ok(result.to_string());
}
}
- // Not in cache or has arguments, format it using cached resource
- 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![locale.clone()]);
- bundle
- .add_resource(resource.as_ref())
- .map_err(|e| format!("Failed to add resource to bundle: {e:?}"))?;
+ let result = {
+ let bundle = self.get_current_bundle();
- let message = bundle
- .get_message(id)
- .ok_or_else(|| format!("Message not found: {id}"))?;
+ let message = bundle
+ .get_message(id.as_str())
+ .ok_or_else(|| IntlError::NotFound(id.to_owned()))?;
- let pattern = message
- .value()
- .ok_or_else(|| format!("Message has no value: {id}"))?;
+ let pattern = message
+ .value()
+ .ok_or_else(|| IntlError::NoValue(id.to_owned()))?;
- // Format the message
- let mut errors = Vec::new();
- let result = bundle.format_pattern(pattern, args, &mut errors);
+ let mut errors = Vec::with_capacity(0);
+ let result = bundle.format_pattern(pattern, args, &mut errors);
- if !errors.is_empty() {
- tracing::warn!("Localization errors for {}: {:?}", id, errors);
- }
+ if !errors.is_empty() {
+ tracing::warn!("Localization errors for {}: {:?}", id, &errors);
+ }
- let result_string = result.into_owned();
+ result.to_string()
+ };
// Only cache simple strings without arguments
// This prevents caching issues when the same message ID is used with different arguments
if args.is_none() {
- let mut cache = self
- .string_cache
- .write()
- .map_err(|e| format!("String cache lock error: {e}"))?;
- let locale_cache = cache.entry(locale.clone()).or_insert_with(HashMap::new);
- locale_cache.insert(id.to_string(), result_string.clone());
- tracing::debug!("Cached string result for '{}' in locale: {}", id, locale);
+ self.cache_string(self.current_locale.clone(), id, result.as_str());
+ tracing::debug!(
+ "Cached string result for '{}' in locale: {}",
+ id,
+ &self.current_locale
+ );
} else {
- tracing::debug!("Not caching string '{}' due to arguments", id);
+ tracing::trace!("Not caching string '{}' due to arguments", id);
}
- Ok(result_string)
+ Ok(result)
}
- /// Gets a localized string by its ID with optional arguments
- pub fn get_string_with_args(
- &self,
- id: &str,
- args: Option<&FluentArgs>,
- ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
- self.get_cached_string(id, args)
+ pub fn cache_string<'a>(&mut self, locale: LanguageIdentifier, id: IntlKey<'a>, result: &str) {
+ tracing::debug!("Cached string result for '{}' in locale: {}", id, &locale);
+ let locale_cache = self.string_cache.entry(locale).or_default();
+ locale_cache.insert(id.to_owned().to_string(), result.to_owned());
}
/// Sets the current locale
- pub fn set_locale(
- &self,
- locale: LanguageIdentifier,
- ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ 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);
@@ -243,56 +306,37 @@ impl LocalizationManager {
locale,
self.available_locales
);
- return Err(format!("Locale {locale} is not available").into());
+ return Err(IntlError::LocaleNotAvailable(locale));
}
- let mut current = self
- .current_locale
- .write()
- .map_err(|e| format!("Lock error: {e}"))?;
- tracing::info!("Switching locale from {} to {locale}", *current);
- *current = locale.clone();
- tracing::info!("Successfully set locale to: {locale}");
+ tracing::info!(
+ "Switching locale from {} to {}",
+ &self.current_locale,
+ &locale
+ );
+ self.current_locale = locale;
// Clear caches when locale changes since they are locale-specific
- let mut string_cache = self
- .string_cache
- .write()
- .map_err(|e| format!("String cache lock error: {e}"))?;
- string_cache.clear();
+ self.string_cache.clear();
tracing::debug!("String cache cleared due to locale change");
Ok(())
}
/// Clears the parsed FluentResource cache (useful for development when FTL files change)
- pub fn clear_cache(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
- let mut cache = self
- .resource_cache
- .write()
- .map_err(|e| format!("Cache lock error: {e}"))?;
- cache.clear();
- tracing::info!("Parsed FluentResource cache cleared");
-
- let mut string_cache = self
- .string_cache
- .write()
- .map_err(|e| format!("String cache lock error: {e}"))?;
- string_cache.clear();
- tracing::info!("String result cache cleared");
+ pub fn clear_cache(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ self.bundles.clear();
+ tracing::debug!("Parsed FluentResource cache cleared");
+
+ self.string_cache.clear();
+ tracing::debug!("String result cache cleared");
Ok(())
}
/// Gets the current locale
- pub fn get_current_locale(
- &self,
- ) -> Result<LanguageIdentifier, Box<dyn std::error::Error + Send + Sync>> {
- let current = self
- .current_locale
- .read()
- .map_err(|e| format!("Lock error: {e}"))?;
- Ok(current.clone())
+ pub fn get_current_locale(&self) -> &LanguageIdentifier {
+ &self.current_locale
}
/// Gets all available locales
@@ -307,38 +351,24 @@ impl LocalizationManager {
/// Gets cache statistics for monitoring performance
pub fn get_cache_stats(&self) -> Result<CacheStats, Box<dyn std::error::Error + Send + Sync>> {
- let resource_cache = self
- .resource_cache
- .read()
- .map_err(|e| format!("Cache lock error: {e}"))?;
- let string_cache = self
- .string_cache
- .read()
- .map_err(|e| format!("String cache lock error: {e}"))?;
-
let mut total_strings = 0;
- for locale_cache in string_cache.values() {
+ for locale_cache in self.string_cache.values() {
total_strings += locale_cache.len();
}
Ok(CacheStats {
- resource_cache_size: resource_cache.len(),
+ resource_cache_size: self.bundles.len(),
string_cache_size: total_strings,
- cached_locales: resource_cache.keys().cloned().collect(),
+ cached_locales: self.bundles.keys().cloned().collect(),
})
}
/// Limits the string cache size to prevent memory growth
pub fn limit_string_cache_size(
- &self,
+ &mut self,
max_strings_per_locale: usize,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
- let mut string_cache = self
- .string_cache
- .write()
- .map_err(|e| format!("String cache lock error: {e}"))?;
-
- for locale_cache in string_cache.values_mut() {
+ for locale_cache in self.string_cache.values_mut() {
if locale_cache.len() > max_strings_per_locale {
// Remove oldest entries (simple approach: just clear and let it rebuild)
// In a more sophisticated implementation, you might use an LRU cache
@@ -365,96 +395,6 @@ impl LocalizationManager {
}
}
-/// Context for sharing localization across the application
-#[derive(Clone)]
-pub struct LocalizationContext {
- /// The localization manager
- manager: Arc<LocalizationManager>,
-}
-
-impl LocalizationContext {
- /// Creates a new LocalizationContext
- pub fn new(manager: Arc<LocalizationManager>) -> 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::<LanguageIdentifier>() {
- 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<String> {
- 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<dyn std::error::Error + Send + Sync>> {
- self.manager.set_locale(locale)
- }
-
- /// Gets the current locale
- pub fn get_current_locale(
- &self,
- ) -> Result<LanguageIdentifier, Box<dyn std::error::Error + Send + Sync>> {
- self.manager.get_current_locale()
- }
-
- /// Clears the resource cache (useful for development when FTL files change)
- pub fn clear_cache(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
- self.manager.clear_cache()
- }
-
- /// Gets the underlying manager
- pub fn manager(&self) -> &Arc<LocalizationManager> {
- &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 {
@@ -468,266 +408,162 @@ mod tests {
use super::*;
#[test]
- fn test_localization_manager_creation() {
- 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);
- assert!(manager.is_ok());
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
- }
-
- #[test]
fn test_locale_management() {
- 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 i18n = Localization::default();
// Test default locale
- let current = manager.get_current_locale().unwrap();
+ let current = i18n.get_current_locale();
assert_eq!(current.to_string(), "en-US");
// Test available locales
- let available = manager.get_available_locales();
+ let available = i18n.get_available_locales();
assert_eq!(available.len(), 1);
assert_eq!(available[0].to_string(), "en-US");
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_ftl_caching() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test3");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 mut i18n = Localization::default();
// First call should load and cache the FTL content
- let result1 = manager.get_string("test_key");
+ let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.as_ref().unwrap(), "Test Value");
// Second call should use cached FTL content
- let result2 = manager.get_string("test_key");
+ let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test another key from the same FTL content
- let result3 = manager.get_string("another_key");
+ let result3 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Another Value");
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_cache_clearing() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test4");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 mut i18n = Localization::default();
// Load and cache the FTL content
- let result1 = manager.get_string("test_key");
+ let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
// Clear the cache
- let clear_result = manager.clear_cache();
+ let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache (will reload)
- let result2 = manager.get_string("test_key");
+ let result2 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_context_caching() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test5");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 context = LocalizationContext::new(manager);
+ let mut i18n = Localization::default();
// Debug: check what the normalized key should be
- let normalized_key = crate::i18n::normalize_ftl_key("test_key", None);
+ let normalized_key = i18n.normalized_ftl_key("test_key", "comment");
println!("Normalized key: '{}'", normalized_key);
// First call should load and cache the FTL content
- let result1 = context.get_string("test_key");
+ let result1 = i18n.get_string(normalized_key.borrow());
println!("First result: {:?}", result1);
- assert!(result1.is_some());
+ assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached FTL content
- let result2 = context.get_string("test_key");
- assert!(result2.is_some());
+ let result2 = i18n.get_string(normalized_key.borrow());
+ assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Test cache clearing through context
- let clear_result = context.clear_cache();
+ let clear_result = i18n.clear_cache();
assert!(clear_result.is_ok());
// Should still work after clearing cache
- let result3 = context.get_string("test_key");
- assert!(result3.is_some());
+ let result3 = i18n.get_string(normalized_key.borrow());
+ assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Test Value");
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_bundle_caching() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test6");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 mut i18n = Localization::default();
// First call should create bundle and cache the resource
- let result1 = manager.get_string("test_key");
+ let result1 = i18n.get_string(IntlKeyBuf::new("test_key").borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached resource but create new bundle
- let result2 = manager.get_string("another_key");
+ let result2 = i18n.get_string(IntlKeyBuf::new("another_key").borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Another Value");
// Check cache stats
- let stats = manager.get_cache_stats().unwrap();
+ let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.resource_cache_size, 1);
assert_eq!(stats.string_cache_size, 2); // Both strings should be cached
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_string_caching() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test7");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 mut i18n = Localization::default();
+ let key = i18n.normalized_ftl_key("test_key", "comment");
// First call should format and cache the string
- let result1 = manager.get_string("test_key");
+ let result1 = i18n.get_string(key.borrow());
assert!(result1.is_ok());
assert_eq!(result1.unwrap(), "Test Value");
// Second call should use cached string
- let result2 = manager.get_string("test_key");
+ let result2 = i18n.get_string(key.borrow());
assert!(result2.is_ok());
assert_eq!(result2.unwrap(), "Test Value");
// Check cache stats
- let stats = manager.get_cache_stats().unwrap();
+ let stats = i18n.get_cache_stats().unwrap();
assert_eq!(stats.string_cache_size, 1);
-
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_cache_clearing_on_locale_change() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test8");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create test FTL files for two locales
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- std::fs::write(en_us_dir.join("main.ftl"), "test_key = Test Value").unwrap();
-
- let en_xa_dir = temp_dir.join("en-XA");
- std::fs::create_dir_all(&en_xa_dir).unwrap();
- std::fs::write(en_xa_dir.join("main.ftl"), "test_key = Test Value XA").unwrap();
-
// Enable pseudolocale for this test
std::env::set_var("NOTEDECK_PSEUDOLOCALE", "1");
- let manager = LocalizationManager::new(&temp_dir).unwrap();
-
- // Load some strings in en-US
- let result1 = manager.get_string("test_key");
- assert!(result1.is_ok());
+ let mut i18n = Localization::default();
// Check that caches are populated
- let stats1 = manager.get_cache_stats().unwrap();
+ let stats1 = i18n.get_cache_stats().unwrap();
assert!(stats1.resource_cache_size > 0);
assert!(stats1.string_cache_size > 0);
// Switch to en-XA
- let en_xa: LanguageIdentifier = "en-XA".parse().unwrap();
- manager.set_locale(en_xa).unwrap();
+ let en_xa: LanguageIdentifier = langid!("en-XA");
+ i18n.set_locale(en_xa).unwrap();
// Check that string cache is cleared (resource cache remains for both locales)
- let stats2 = manager.get_cache_stats().unwrap();
+ let stats2 = i18n.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
// Cleanup
std::env::remove_var("NOTEDECK_PSEUDOLOCALE");
- std::fs::remove_dir_all(&temp_dir).unwrap();
}
#[test]
fn test_string_caching_with_arguments() {
- let temp_dir = std::env::temp_dir().join("notedeck_i18n_test9");
- std::fs::create_dir_all(&temp_dir).unwrap();
-
- // Create a test FTL file with a message that takes arguments
- let en_us_dir = temp_dir.join("en-US");
- std::fs::create_dir_all(&en_us_dir).unwrap();
- 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 mut manager = Localization::default();
// First call with arguments should not be cached
let mut args = fluent::FluentArgs::new();
args.set("name", "Alice");
- let result1 = manager.get_string_with_args("welcome_message", Some(&args));
- assert!(result1.is_ok());
- // Note: Fluent may add bidirectional text control characters, so we check contains
- let result1_str = result1.unwrap();
- assert!(result1_str.contains("Alice"));
+ let key = IntlKeyBuf::new("welcome_message");
+ let result1 = manager
+ .get_cached_string(key.borrow(), Some(&args))
+ .unwrap();
+ assert!(result1.contains("Alice"));
// Check that it's not in the string cache
let stats1 = manager.get_cache_stats().unwrap();
@@ -736,7 +572,7 @@ mod tests {
// Second call with different arguments should work correctly
let mut args2 = fluent::FluentArgs::new();
args2.set("name", "Bob");
- let result2 = manager.get_string_with_args("welcome_message", Some(&args2));
+ let result2 = manager.get_cached_string(key.borrow(), Some(&args2));
assert!(result2.is_ok());
let result2_str = result2.unwrap();
assert!(result2_str.contains("Bob"));
@@ -745,22 +581,35 @@ mod tests {
let stats2 = manager.get_cache_stats().unwrap();
assert_eq!(stats2.string_cache_size, 0);
- // Test a simple string without arguments - should be cached
- let ftl_content_simple = "simple_message = Hello World";
- std::fs::write(en_us_dir.join("main.ftl"), ftl_content_simple).unwrap();
-
// Clear cache to start fresh
manager.clear_cache().unwrap();
- let result3 = manager.get_string("simple_message");
+ let result3 = manager.get_string(key.borrow());
assert!(result3.is_ok());
assert_eq!(result3.unwrap(), "Hello World");
// Check that simple string is cached
let stats3 = manager.get_cache_stats().unwrap();
assert_eq!(stats3.string_cache_size, 1);
+ }
+}
- // Cleanup
- std::fs::remove_dir_all(&temp_dir).unwrap();
+/// Replace each invalid character with exactly one underscore
+/// This matches the behavior of the Python extraction script
+pub fn fixup_key(s: &str) -> String {
+ let mut out = String::with_capacity(s.len());
+ for ch in s.chars() {
+ match ch {
+ 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' => out.push(ch),
+ _ => out.push('_'), // always push
+ }
}
+ let trimmed = out.trim_matches('_');
+ trimmed.to_owned()
+}
+
+fn simple_hash(s: &str) -> String {
+ let digest = md5::compute(s.as_bytes());
+ // Take the first 2 bytes and convert to 4 hex characters
+ format!("{:02x}{:02x}", digest[0], digest[1])
}
diff --git a/crates/notedeck/src/i18n/mod.rs b/crates/notedeck/src/i18n/mod.rs
@@ -4,104 +4,22 @@
//! 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;
+pub use manager::StringCacheResult;
/// Re-export commonly used types for convenience
pub use fluent::FluentArgs;
pub use fluent::FluentValue;
pub use unic_langid::LanguageIdentifier;
-use md5;
-use once_cell::sync::OnceCell;
-use regex::Regex;
-use std::collections::HashMap;
-use std::sync::Arc;
-use std::sync::Mutex;
-use tracing::info;
-
-/// Global localization manager for easy access from anywhere
-static GLOBAL_I18N: OnceCell<Arc<LocalizationManager>> = OnceCell::new();
-
-/// Cache for normalized FTL keys to avoid repeated normalization
-static NORMALIZED_KEY_CACHE: OnceCell<Mutex<HashMap<String, String>>> = OnceCell::new();
-
-/// Initialize the global localization context
-pub fn init_global_i18n(context: LocalizationContext) {
- info!("Initializing global i18n context");
- let _ = GLOBAL_I18N.set(context.manager().clone());
-
- // Initialize the normalized key cache
- let _ = NORMALIZED_KEY_CACHE.set(Mutex::new(HashMap::new()));
-
- info!("Global i18n context initialized successfully");
-}
-
-/// Get the global localization manager
-pub fn get_global_i18n() -> Option<Arc<LocalizationManager>> {
- GLOBAL_I18N.get().cloned()
-}
-
-fn simple_hash(s: &str) -> String {
- let digest = md5::compute(s.as_bytes());
- // Take the first 2 bytes and convert to 4 hex characters
- format!("{:02x}{:02x}", digest[0], digest[1])
-}
-
-pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
- // Try to get from cache first
- let cache_key = if let Some(comment) = comment {
- format!("{key}:{comment}")
- } else {
- key.to_string()
- };
-
- if let Some(cache) = NORMALIZED_KEY_CACHE.get() {
- if let Ok(cache) = cache.lock() {
- if let Some(cached) = cache.get(&cache_key) {
- return cached.clone();
- }
- }
- }
-
- // Replace each invalid character with exactly one underscore
- // This matches the behavior of the Python extraction script
- let re = Regex::new(r"[^a-zA-Z0-9_-]").unwrap();
- let mut result = re.replace_all(key, "_").to_string();
-
- // Remove leading/trailing underscores
- result = result.trim_matches('_').to_string();
-
- // Ensure the key starts with a letter (Fluent requirement)
- if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
- result = format!("k_{result}");
- }
-
- // If we have a comment, append a hash of it to reduce collisions
- if let Some(comment) = comment {
- let hash_str = format!("_{}", simple_hash(comment));
- 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
-}
-
/// Macro for getting localized strings with format-like syntax
///
/// Syntax: tr!("message", comment)
@@ -114,53 +32,36 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
/// All placeholders must be named and start with a letter (a-zA-Z).
#[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()
- }
+ let key = $i18n.normalized_ftl_key($message, $comment);
+ match $i18n.get_string(key.borrow()) {
+ Ok(r) => r,
+ Err(_err) => {
+ $message.to_string()
}
- } else {
- tracing::warn!("FALLBACK: Global i18n not initialized, using key '{}' as string", $message);
- $message.to_string()
}
}
};
// Case with named parameters: message, comment, param=value, ...
- ($message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
+ ($i18n:expr, $message:expr, $comment:expr, $($param:ident = $value:expr),*) => {
{
- let norm_key = $crate::i18n::normalize_ftl_key($message, Some($comment));
- if let Some(i18n) = $crate::i18n::get_global_i18n() {
- let mut args = $crate::i18n::FluentArgs::new();
- $(
- args.set(stringify!($param), $value);
- )*
- match i18n.get_string_with_args(&norm_key, Some(&args)) {
- Ok(s) => s,
- Err(_) => {
- // Fallback: replace placeholders with values
- let mut result = $message.to_string();
- $(
- result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
- )*
- result
- }
+ let key = $i18n.normalized_ftl_key($message, $comment);
+ let mut args = $crate::i18n::FluentArgs::new();
+ $(
+ args.set(stringify!($param), $value);
+ )*
+ match $i18n.get_cached_string(key.borrow(), Some(&args)) {
+ Ok(r) => r,
+ Err(_) => {
+ // Fallback: replace placeholders with values
+ let mut result = $message.to_string();
+ $(
+ result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
+ )*
+ result
}
- } else {
- // Fallback: replace placeholders with values
- let mut result = $message.to_string();
- $(
- result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());
- )*
- result
}
}
};
@@ -177,42 +78,27 @@ macro_rules! tr {
#[macro_export]
macro_rules! tr_plural {
// With named parameters
- ($one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
- let norm_key = $crate::i18n::normalize_ftl_key($other, Some($comment));
- if let Some(i18n) = $crate::i18n::get_global_i18n() {
- let mut args = $crate::i18n::FluentArgs::new();
- args.set("count", $count);
- $(args.set(stringify!($param), $value);)*
- match i18n.get_string_with_args(&norm_key, Some(&args)) {
- Ok(s) => s,
- Err(_) => {
- // Fallback: use simple pluralization
- if $count == 1 {
- let mut result = $one.to_string();
- $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
- result = result.replace("{count}", &$count.to_string());
- result
- } else {
- let mut result = $other.to_string();
- $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
- result = result.replace("{count}", &$count.to_string());
- result
- }
+ ($i18n:expr, $one:expr, $other:expr, $comment:expr, $count:expr, $($param:ident = $value:expr),*) => {{
+ let norm_key = $i18n.normalized_ftl_key($other, $comment);
+ let mut args = $crate::i18n::FluentArgs::new();
+ args.set("count", $count);
+ $(args.set(stringify!($param), $value);)*
+ match $i18n.get_cached_string(norm_key.borrow(), Some(&args)) {
+ Ok(s) => s,
+ Err(_) => {
+ // Fallback: use simple pluralization
+ if $count == 1 {
+ let mut result = $one.to_string();
+ $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
+ result = result.replace("{count}", &$count.to_string());
+ result
+ } else {
+ let mut result = $other.to_string();
+ $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
+ result = result.replace("{count}", &$count.to_string());
+ result
}
}
- } else {
- // Fallback: use simple pluralization
- if $count == 1 {
- let mut result = $one.to_string();
- $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
- result = result.replace("{count}", &$count.to_string());
- result
- } else {
- let mut result = $other.to_string();
- $(result = result.replace(&format!("{{{}}}", stringify!($param)), &$value.to_string());)*
- result = result.replace("{count}", &$count.to_string());
- result
- }
}
}};
// Without named parameters
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -45,11 +45,7 @@ pub use context::AppContext;
pub use error::{show_one_error_message, Error, FilterError, ZapError};
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,
@@ -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/src/note/mod.rs b/crates/notedeck/src/note/mod.rs
@@ -6,6 +6,7 @@ pub use context::{BroadcastContext, ContextSelection, NoteContextSelection};
use crate::Accounts;
use crate::JobPool;
+use crate::Localization;
use crate::UnknownIds;
use crate::{notecache::NoteCache, zaps::Zaps, Images};
use enostr::{NoteId, RelayPool};
@@ -19,6 +20,7 @@ use std::fmt;
pub struct NoteContext<'d> {
pub ndb: &'d Ndb,
pub accounts: &'d Accounts,
+ pub i18n: &'d mut Localization,
pub img_cache: &'d mut Images,
pub note_cache: &'d mut NoteCache,
pub zaps: &'d mut Zaps,
diff --git a/crates/notedeck/src/notecache.rs b/crates/notedeck/src/notecache.rs
@@ -1,7 +1,5 @@
-use crate::{time_ago_since, TimeCached};
use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf};
use std::collections::HashMap;
-use std::time::Duration;
#[derive(Default)]
pub struct NoteCache {
@@ -32,7 +30,7 @@ impl NoteCache {
#[derive(Clone)]
pub struct CachedNote {
- reltime: TimeCached<String>,
+ //reltime: TimeCached<String>,
pub client: Option<String>,
pub reply: NoteReplyBuf,
}
@@ -41,22 +39,25 @@ impl CachedNote {
pub fn new(note: &Note) -> Self {
use crate::note::event_tag;
+ /*
let created_at = note.created_at();
let reltime = TimeCached::new(
Duration::from_secs(1),
- Box::new(move || time_ago_since(created_at)),
+ Box::new(move || time_ago_since(i18n, created_at)),
);
+ */
let reply = NoteReply::new(note.tags()).to_owned();
let client = event_tag(note, "client");
CachedNote {
client: client.map(|c| c.to_string()),
- reltime,
+ // reltime,
reply,
}
}
+ /*
pub fn reltime_str_mut(&mut self) -> &str {
self.reltime.get_mut()
}
@@ -64,4 +65,5 @@ impl CachedNote {
pub fn reltime_str(&self) -> Option<&str> {
self.reltime.get().map(|x| x.as_str())
}
+ */
}
diff --git a/crates/notedeck/src/time.rs b/crates/notedeck/src/time.rs
@@ -1,4 +1,4 @@
-use crate::tr;
+use crate::{tr, Localization};
use std::time::{SystemTime, UNIX_EPOCH};
// Time duration constants in seconds
@@ -18,7 +18,7 @@ const MAX_SECONDS_FOR_WEEKS: u64 = ONE_MONTH_IN_SECONDS - 1;
const MAX_SECONDS_FOR_MONTHS: u64 = ONE_YEAR_IN_SECONDS - 1;
/// Calculate relative time between two timestamps
-fn time_ago_between(timestamp: u64, now: u64) -> String {
+fn time_ago_between(i18n: &mut Localization, timestamp: u64, now: u64) -> String {
// Determine if the timestamp is in the future or the past
let duration = if now >= timestamp {
now.saturating_sub(timestamp)
@@ -28,36 +28,48 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
let time_str = match duration {
0..=2 => tr!(
+ i18n,
"now",
"Relative time for very recent events (less than 3 seconds)"
),
- 3..=MAX_SECONDS => tr!("{count}s", "Relative time in seconds", count = duration),
+ 3..=MAX_SECONDS => tr!(
+ i18n,
+ "{count}s",
+ "Relative time in seconds",
+ count = duration
+ ),
ONE_MINUTE_IN_SECONDS..=MAX_SECONDS_FOR_MINUTES => tr!(
+ i18n,
"{count}m",
"Relative time in minutes",
count = duration / ONE_MINUTE_IN_SECONDS
),
ONE_HOUR_IN_SECONDS..=MAX_SECONDS_FOR_HOURS => tr!(
+ i18n,
"{count}h",
"Relative time in hours",
count = duration / ONE_HOUR_IN_SECONDS
),
ONE_DAY_IN_SECONDS..=MAX_SECONDS_FOR_DAYS => tr!(
+ i18n,
"{count}d",
"Relative time in days",
count = duration / ONE_DAY_IN_SECONDS
),
ONE_WEEK_IN_SECONDS..=MAX_SECONDS_FOR_WEEKS => tr!(
+ i18n,
"{count}w",
"Relative time in weeks",
count = duration / ONE_WEEK_IN_SECONDS
),
ONE_MONTH_IN_SECONDS..=MAX_SECONDS_FOR_MONTHS => tr!(
+ i18n,
"{count}mo",
"Relative time in months",
count = duration / ONE_MONTH_IN_SECONDS
),
_ => tr!(
+ i18n,
"{count}y",
"Relative time in years",
count = duration / ONE_YEAR_IN_SECONDS
@@ -65,19 +77,19 @@ fn time_ago_between(timestamp: u64, now: u64) -> String {
};
if timestamp > now {
- format!("+{}", time_str)
+ format!("+{time_str}")
} else {
time_str
}
}
-pub fn time_ago_since(timestamp: u64) -> String {
+pub fn time_ago_since(i18n: &mut Localization, timestamp: u64) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
- time_ago_between(timestamp, now)
+ time_ago_between(i18n, timestamp, now)
}
#[cfg(test)]
@@ -95,9 +107,10 @@ mod tests {
#[test]
fn test_now_condition() {
let now = get_current_timestamp();
+ let mut intl = Localization::default();
// Test 0 seconds ago
- let result = time_ago_between(now, now);
+ let result = time_ago_between(&mut intl, now, now);
assert_eq!(
result, "now",
"Expected 'now' for 0 seconds, got: {}",
@@ -105,7 +118,7 @@ mod tests {
);
// Test 1 second ago
- let result = time_ago_between(now - 1, now);
+ let result = time_ago_between(&mut intl, now - 1, now);
assert_eq!(
result, "now",
"Expected 'now' for 1 second, got: {}",
@@ -113,7 +126,7 @@ mod tests {
);
// Test 2 seconds ago
- let result = time_ago_between(now - 2, now);
+ let result = time_ago_between(&mut intl, now - 2, now);
assert_eq!(
result, "now",
"Expected 'now' for 2 seconds, got: {}",
@@ -124,13 +137,14 @@ mod tests {
#[test]
fn test_seconds_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 3 seconds ago
- let result = time_ago_between(now - 3, now);
+ let result = time_ago_between(&mut i18n, now - 3, now);
assert_eq!(result, "3s", "Expected '3s' for 3 seconds, got: {}", result);
// Test 30 seconds ago
- let result = time_ago_between(now - 30, now);
+ let result = time_ago_between(&mut i18n, now - 30, now);
assert_eq!(
result, "30s",
"Expected '30s' for 30 seconds, got: {}",
@@ -138,7 +152,7 @@ mod tests {
);
// Test 59 seconds ago (max for seconds)
- let result = time_ago_between(now - 59, now);
+ let result = time_ago_between(&mut i18n, now - 59, now);
assert_eq!(
result, "59s",
"Expected '59s' for 59 seconds, got: {}",
@@ -149,13 +163,14 @@ mod tests {
#[test]
fn test_minutes_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 minute ago
- let result = time_ago_between(now - ONE_MINUTE_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_MINUTE_IN_SECONDS, now);
assert_eq!(result, "1m", "Expected '1m' for 1 minute, got: {}", result);
// Test 30 minutes ago
- let result = time_ago_between(now - 30 * ONE_MINUTE_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 30 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "30m",
"Expected '30m' for 30 minutes, got: {}",
@@ -163,7 +178,7 @@ mod tests {
);
// Test 59 minutes ago (max for minutes)
- let result = time_ago_between(now - 59 * ONE_MINUTE_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 59 * ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "59m",
"Expected '59m' for 59 minutes, got: {}",
@@ -174,13 +189,14 @@ mod tests {
#[test]
fn test_hours_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 hour ago
- let result = time_ago_between(now - ONE_HOUR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_HOUR_IN_SECONDS, now);
assert_eq!(result, "1h", "Expected '1h' for 1 hour, got: {}", result);
// Test 12 hours ago
- let result = time_ago_between(now - 12 * ONE_HOUR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 12 * ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "12h",
"Expected '12h' for 12 hours, got: {}",
@@ -188,7 +204,7 @@ mod tests {
);
// Test 23 hours ago (max for hours)
- let result = time_ago_between(now - 23 * ONE_HOUR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 23 * ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "23h",
"Expected '23h' for 23 hours, got: {}",
@@ -199,43 +215,46 @@ mod tests {
#[test]
fn test_days_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 day ago
- let result = time_ago_between(now - ONE_DAY_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "1d", "Expected '1d' for 1 day, got: {}", result);
// Test 3 days ago
- let result = time_ago_between(now - 3 * ONE_DAY_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 3 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "3d", "Expected '3d' for 3 days, got: {}", result);
// Test 6 days ago (max for days, before weeks)
- let result = time_ago_between(now - 6 * ONE_DAY_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 6 * ONE_DAY_IN_SECONDS, now);
assert_eq!(result, "6d", "Expected '6d' for 6 days, got: {}", result);
}
#[test]
fn test_weeks_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 week ago
- let result = time_ago_between(now - ONE_WEEK_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "1w", "Expected '1w' for 1 week, got: {}", result);
// Test 4 weeks ago
- let result = time_ago_between(now - 4 * ONE_WEEK_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 4 * ONE_WEEK_IN_SECONDS, now);
assert_eq!(result, "4w", "Expected '4w' for 4 weeks, got: {}", result);
}
#[test]
fn test_months_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 month ago
- let result = time_ago_between(now - ONE_MONTH_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_MONTH_IN_SECONDS, now);
assert_eq!(result, "1mo", "Expected '1mo' for 1 month, got: {}", result);
// Test 11 months ago (max for months, before years)
- let result = time_ago_between(now - 11 * ONE_MONTH_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 11 * ONE_MONTH_IN_SECONDS, now);
assert_eq!(
result, "11mo",
"Expected '11mo' for 11 months, got: {}",
@@ -246,17 +265,18 @@ mod tests {
#[test]
fn test_years_condition() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 year ago
- let result = time_ago_between(now - ONE_YEAR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "1y", "Expected '1y' for 1 year, got: {}", result);
// Test 5 years ago
- let result = time_ago_between(now - 5 * ONE_YEAR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 5 * ONE_YEAR_IN_SECONDS, now);
assert_eq!(result, "5y", "Expected '5y' for 5 years, got: {}", result);
// Test 10 years ago (reduced from 100 to avoid overflow)
- let result = time_ago_between(now - 10 * ONE_YEAR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now - 10 * ONE_YEAR_IN_SECONDS, now);
assert_eq!(
result, "10y",
"Expected '10y' for 10 years, got: {}",
@@ -267,9 +287,10 @@ mod tests {
#[test]
fn test_future_timestamps() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test 1 minute in the future
- let result = time_ago_between(now + ONE_MINUTE_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now + ONE_MINUTE_IN_SECONDS, now);
assert_eq!(
result, "+1m",
"Expected '+1m' for 1 minute in future, got: {}",
@@ -277,7 +298,7 @@ mod tests {
);
// Test 1 hour in the future
- let result = time_ago_between(now + ONE_HOUR_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now + ONE_HOUR_IN_SECONDS, now);
assert_eq!(
result, "+1h",
"Expected '+1h' for 1 hour in future, got: {}",
@@ -285,7 +306,7 @@ mod tests {
);
// Test 1 day in the future
- let result = time_ago_between(now + ONE_DAY_IN_SECONDS, now);
+ let result = time_ago_between(&mut i18n, now + ONE_DAY_IN_SECONDS, now);
assert_eq!(
result, "+1d",
"Expected '+1d' for 1 day in future, got: {}",
@@ -296,9 +317,10 @@ mod tests {
#[test]
fn test_boundary_conditions() {
let now = get_current_timestamp();
+ let mut i18n = Localization::default();
// Test boundary between seconds and minutes
- let result = time_ago_between(now - 60, now);
+ let result = time_ago_between(&mut i18n, now - 60, now);
assert_eq!(
result, "1m",
"Expected '1m' for exactly 60 seconds, got: {}",
@@ -306,7 +328,7 @@ mod tests {
);
// Test boundary between minutes and hours
- let result = time_ago_between(now - 3600, now);
+ let result = time_ago_between(&mut i18n, now - 3600, now);
assert_eq!(
result, "1h",
"Expected '1h' for exactly 3600 seconds, got: {}",
@@ -314,7 +336,7 @@ mod tests {
);
// Test boundary between hours and days
- let result = time_ago_between(now - 86400, now);
+ let result = time_ago_between(&mut i18n, now - 86400, now);
assert_eq!(
result, "1d",
"Expected '1d' for exactly 86400 seconds, got: {}",
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -5,7 +5,9 @@ use crate::app::NotedeckApp;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction};
-use notedeck::{tr, App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType};
+use notedeck::{
+ tr, App, AppAction, AppContext, Localization, NotedeckTextStyle, UserAccount, WalletType,
+};
use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
};
@@ -59,14 +61,17 @@ pub enum ChromePanelAction {
}
impl ChromePanelAction {
- fn columns_switch(ctx: &AppContext, chrome: &mut Chrome, kind: &TimelineKind) {
+ fn columns_switch(ctx: &mut AppContext, chrome: &mut Chrome, kind: &TimelineKind) {
chrome.switch_to_columns();
let Some(columns_app) = chrome.get_columns_app() else {
return;
};
- if let Some(active_columns) = columns_app.decks_cache.active_columns_mut(ctx.accounts) {
+ if let Some(active_columns) = columns_app
+ .decks_cache
+ .active_columns_mut(ctx.i18n, ctx.accounts)
+ {
match active_columns.select_by_kind(kind) {
SelectionResult::NewSelection(_index) => {
// great! no need to go to top yet
@@ -85,13 +90,14 @@ impl ChromePanelAction {
}
}
- fn columns_navigate(ctx: &AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
+ fn columns_navigate(ctx: &mut AppContext, chrome: &mut Chrome, route: notedeck_columns::Route) {
chrome.switch_to_columns();
- if let Some(c) = chrome
- .get_columns_app()
- .and_then(|columns| columns.decks_cache.selected_column_mut(ctx.accounts))
- {
+ if let Some(c) = chrome.get_columns_app().and_then(|columns| {
+ columns
+ .decks_cache
+ .selected_column_mut(ctx.i18n, ctx.accounts)
+ }) {
if c.router().routes().iter().any(|r| r == &route) {
// return if we are already routing to accounts
c.router_mut().go_back();
@@ -102,7 +108,7 @@ impl ChromePanelAction {
};
}
- fn process(&self, ctx: &AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
+ fn process(&self, ctx: &mut AppContext, chrome: &mut Chrome, ui: &mut egui::Ui) {
match self {
Self::SaveTheme(theme) => {
ui.ctx().options_mut(|o| {
@@ -244,7 +250,7 @@ impl Chrome {
.vertical(|mut vstrip| {
vstrip.cell(|ui| {
_ = ui.vertical_centered(|ui| {
- self.topdown_sidebar(ui);
+ self.topdown_sidebar(ui, app_ctx.i18n);
})
});
vstrip.cell(|ui| {
@@ -401,7 +407,7 @@ impl Chrome {
}
}
- fn topdown_sidebar(&mut self, ui: &mut egui::Ui) {
+ fn topdown_sidebar(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
// macos needs a bit of space to make room for window
// minimize/close buttons
if cfg!(target_os = "macos") {
@@ -417,7 +423,7 @@ impl Chrome {
}
ui.add_space(4.0);
- ui.add(milestone_name());
+ ui.add(milestone_name(i18n));
ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
{
@@ -451,7 +457,7 @@ impl notedeck::App for Chrome {
}
}
-fn milestone_name() -> impl Widget {
+fn milestone_name<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response {
ui.vertical_centered(|ui| {
let font = egui::FontId::new(
@@ -460,13 +466,14 @@ fn milestone_name() -> impl Widget {
);
ui.add(
Label::new(
- RichText::new(tr!("BETA", "Beta version label"))
+ RichText::new(tr!(i18n, "BETA", "Beta version label"))
.color(ui.style().visuals.noninteractive().fg_stroke.color)
.font(font),
)
.selectable(false),
)
.on_hover_text(tr!(
+ i18n,
"Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
"Beta product warning message"
))
@@ -657,7 +664,7 @@ fn chrome_handle_app_action(
let cols = columns
.decks_cache
- .active_columns_mut(ctx.accounts)
+ .active_columns_mut(ctx.i18n, ctx.accounts)
.unwrap();
let m_action = notedeck_columns::actionbar::execute_and_process_note_action(
note_action,
@@ -721,6 +728,7 @@ fn bottomup_sidebar(
.add(Button::new("☀").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!(
+ ctx.i18n,
"Switch to light mode",
"Hover text for light mode toggle button"
));
@@ -735,6 +743,7 @@ fn bottomup_sidebar(
.add(Button::new("🌙").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(tr!(
+ ctx.i18n,
"Switch to dark mode",
"Hover text for dark mode toggle button"
));
diff --git a/crates/notedeck_columns/src/accounts/mod.rs b/crates/notedeck_columns/src/accounts/mod.rs
@@ -1,7 +1,7 @@
use enostr::{FullKeypair, Pubkey};
use nostrdb::{Ndb, Transaction};
-use notedeck::{Accounts, AppContext, SingleUnkIdAction, UnknownIds};
+use notedeck::{Accounts, AppContext, Localization, SingleUnkIdAction, UnknownIds};
use crate::app::get_active_columns_mut;
use crate::decks::DecksCache;
@@ -72,23 +72,34 @@ pub fn render_accounts_route(
route: AccountsRoute,
) -> AddAccountAction {
let resp = match route {
- AccountsRoute::Accounts => {
- AccountsView::new(app_ctx.ndb, app_ctx.accounts, app_ctx.img_cache)
+ AccountsRoute::Accounts => AccountsView::new(
+ app_ctx.ndb,
+ app_ctx.accounts,
+ app_ctx.img_cache,
+ app_ctx.i18n,
+ )
+ .ui(ui)
+ .inner
+ .map(AccountsRouteResponse::Accounts),
+
+ AccountsRoute::AddAccount => {
+ AccountLoginView::new(login_state, app_ctx.clipboard, app_ctx.i18n)
.ui(ui)
.inner
- .map(AccountsRouteResponse::Accounts)
+ .map(AccountsRouteResponse::AddAccount)
}
-
- AccountsRoute::AddAccount => AccountLoginView::new(login_state, app_ctx.clipboard)
- .ui(ui)
- .inner
- .map(AccountsRouteResponse::AddAccount),
};
if let Some(resp) = resp {
match resp {
AccountsRouteResponse::Accounts(response) => {
- let action = process_accounts_view_response(app_ctx.accounts, decks, col, response);
+ let action = process_accounts_view_response(
+ app_ctx.i18n,
+ app_ctx.accounts,
+ decks,
+ col,
+ response,
+ );
AddAccountAction {
accounts_action: action,
unk_id_action: SingleUnkIdAction::no_action(),
@@ -98,7 +109,7 @@ pub fn render_accounts_route(
let action =
process_login_view_response(app_ctx, timeline_cache, decks, col, response);
*login_state = Default::default();
- let router = get_active_columns_mut(app_ctx.accounts, decks)
+ let router = get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, decks)
.column_mut(col)
.router_mut();
router.go_back();
@@ -114,12 +125,13 @@ pub fn render_accounts_route(
}
pub fn process_accounts_view_response(
+ i18n: &mut Localization,
accounts: &mut Accounts,
decks: &mut DecksCache,
col: usize,
response: AccountsViewResponse,
) -> Option<AccountsAction> {
- let router = get_active_columns_mut(accounts, decks)
+ let router = get_active_columns_mut(i18n, accounts, decks)
.column_mut(col)
.router_mut();
let mut action = None;
diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs
@@ -284,7 +284,7 @@ impl NewNotes {
let timeline = if let Some(profile) = timeline_cache.get_mut(&self.id) {
profile
} else {
- error!("NewNotes: could not get timeline for key {}", self.id);
+ error!("NewNotes: could not get timeline for key {:?}", self.id);
return;
};
diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs
@@ -20,7 +20,7 @@ use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPo
use nostrdb::Transaction;
use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
- UnknownIds,
+ Localization, UnknownIds,
};
use notedeck_ui::{jobs::JobsCache, NoteOptions};
use std::collections::{BTreeSet, HashMap};
@@ -92,7 +92,8 @@ fn try_process_event(
app_ctx: &mut AppContext<'_>,
ctx: &egui::Context,
) -> Result<()> {
- let current_columns = get_active_columns_mut(app_ctx.accounts, &mut damus.decks_cache);
+ let current_columns =
+ get_active_columns_mut(app_ctx.i18n, app_ctx.accounts, &mut damus.decks_cache);
ctx.input(|i| handle_key_events(i, current_columns));
let ctx2 = ctx.clone();
@@ -187,7 +188,9 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con
app_ctx.img_cache.urls.cache.handle_io();
if damus.columns(app_ctx.accounts).columns().is_empty() {
- damus.columns_mut(app_ctx.accounts).new_column_picker();
+ damus
+ .columns_mut(app_ctx.i18n, app_ctx.accounts)
+ .new_column_picker();
}
match damus.state {
@@ -262,7 +265,7 @@ fn handle_eose(
tl
} else {
error!(
- "timeline uid:{} not found for FetchingContactList",
+ "timeline uid:{:?} not found for FetchingContactList",
timeline_uid
);
return Ok(());
@@ -427,9 +430,9 @@ impl Damus {
}
}
- columns_to_decks_cache(columns, account)
+ columns_to_decks_cache(ctx.i18n, columns, account)
} else if let Some(decks_cache) =
- crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache)
+ crate::storage::load_decks_cache(ctx.path, ctx.ndb, &mut timeline_cache, ctx.i18n)
{
info!(
"DecksCache: loading from disk {}",
@@ -495,8 +498,8 @@ impl Damus {
self.options.insert(AppOptions::ScrollToTop)
}
- pub fn columns_mut(&mut self, accounts: &Accounts) -> &mut Columns {
- get_active_columns_mut(accounts, &mut self.decks_cache)
+ pub fn columns_mut(&mut self, i18n: &mut Localization, accounts: &Accounts) -> &mut Columns {
+ get_active_columns_mut(i18n, accounts, &mut self.decks_cache)
}
pub fn columns(&self, accounts: &Accounts) -> &Columns {
@@ -512,7 +515,8 @@ impl Damus {
}
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
- let decks_cache = DecksCache::default();
+ let mut i18n = Localization::default();
+ let decks_cache = DecksCache::default_decks_cache(&mut i18n);
let path = DataPath::new(&data_path);
let imgcache_dir = path.path(DataPathType::Cache);
@@ -567,7 +571,7 @@ fn render_damus_mobile(
let rect = ui.available_rect_before_wrap();
let mut app_action: Option<AppAction> = None;
- let active_col = app.columns_mut(app_ctx.accounts).selected as usize;
+ let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
if !app.columns(app_ctx.accounts).columns().is_empty() {
let r = nav::render_nav(
active_col,
@@ -623,6 +627,7 @@ fn hovering_post_button(
&mut app.decks_cache,
app_ctx.accounts,
SidePanelAction::ComposeNote,
+ app_ctx.i18n,
);
}
}
@@ -715,9 +720,12 @@ fn timelines_view(
.horizontal(|mut strip| {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
- let side_panel =
- DesktopSidePanel::new(ctx.accounts.get_selected_account(), &app.decks_cache)
- .show(ui);
+ let side_panel = DesktopSidePanel::new(
+ ctx.accounts.get_selected_account(),
+ &app.decks_cache,
+ ctx.i18n,
+ )
+ .show(ui);
if let Some(side_panel) = side_panel {
if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
@@ -725,6 +733,7 @@ fn timelines_view(
&mut app.decks_cache,
ctx.accounts,
side_panel.action,
+ ctx.i18n,
) {
side_panel_action = Some(action);
}
@@ -833,27 +842,32 @@ pub fn get_decks<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a De
}
pub fn get_active_columns_mut<'a>(
+ i18n: &mut Localization,
accounts: &Accounts,
decks_cache: &'a mut DecksCache,
) -> &'a mut Columns {
- get_decks_mut(accounts, decks_cache)
+ get_decks_mut(i18n, accounts, decks_cache)
.active_mut()
.columns_mut()
}
-pub fn get_decks_mut<'a>(accounts: &Accounts, decks_cache: &'a mut DecksCache) -> &'a mut Decks {
- decks_cache.decks_mut(accounts.selected_account_pubkey())
+pub fn get_decks_mut<'a>(
+ i18n: &mut Localization,
+ accounts: &Accounts,
+ decks_cache: &'a mut DecksCache,
+) -> &'a mut Decks {
+ decks_cache.decks_mut(i18n, accounts.selected_account_pubkey())
}
-fn columns_to_decks_cache(cols: Columns, key: &[u8; 32]) -> DecksCache {
+fn columns_to_decks_cache(i18n: &mut Localization, cols: Columns, key: &[u8; 32]) -> DecksCache {
let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
let decks = Decks::new(crate::decks::Deck::new_with_columns(
- crate::decks::Deck::default().icon,
- tr!("My Deck", "Title for the user's deck"),
+ crate::decks::Deck::default_icon(),
+ tr!(i18n, "My Deck", "Title for the user's deck"),
cols,
));
let account = Pubkey::new(*key);
account_to_decks.insert(account, decks);
- DecksCache::new(account_to_decks)
+ DecksCache::new(account_to_decks, i18n)
}
diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs
@@ -2,7 +2,7 @@ use std::collections::{hash_map::ValuesMut, HashMap};
use enostr::{Pubkey, RelayPool};
use nostrdb::Transaction;
-use notedeck::{tr, AppContext, FALLBACK_PUBKEY};
+use notedeck::{tr, AppContext, Localization, FALLBACK_PUBKEY};
use tracing::{error, info};
use crate::{
@@ -21,18 +21,20 @@ pub struct DecksCache {
fallback_pubkey: Pubkey,
}
-impl Default for DecksCache {
- fn default() -> Self {
+impl DecksCache {
+ pub fn default_decks_cache(i18n: &mut Localization) -> Self {
let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
- account_to_decks.insert(FALLBACK_PUBKEY(), Decks::default());
- DecksCache::new(account_to_decks)
+ account_to_decks.insert(FALLBACK_PUBKEY(), Decks::default_decks(i18n));
+ DecksCache::new(account_to_decks, i18n)
}
-}
-impl DecksCache {
/// Gets the first column in the currently active user's active deck
- pub fn selected_column_mut(&mut self, accounts: ¬edeck::Accounts) -> Option<&mut Column> {
- self.active_columns_mut(accounts)
+ pub fn selected_column_mut(
+ &mut self,
+ i18n: &mut Localization,
+ accounts: ¬edeck::Accounts,
+ ) -> Option<&mut Column> {
+ self.active_columns_mut(i18n, accounts)
.and_then(|ad| ad.selected_mut())
}
@@ -45,10 +47,14 @@ impl DecksCache {
}
/// Gets a mutable reference to the active columns
- pub fn active_columns_mut(&mut self, accounts: ¬edeck::Accounts) -> Option<&mut Columns> {
+ pub fn active_columns_mut(
+ &mut self,
+ i18n: &mut Localization,
+ accounts: ¬edeck::Accounts,
+ ) -> Option<&mut Columns> {
let account = accounts.get_selected_account();
- self.decks_mut(&account.key.pubkey)
+ self.decks_mut(i18n, &account.key.pubkey)
.active_deck_mut()
.map(|ad| ad.columns_mut())
}
@@ -62,9 +68,11 @@ impl DecksCache {
.map(|ad| ad.columns())
}
- pub fn new(mut account_to_decks: HashMap<Pubkey, Decks>) -> Self {
+ pub fn new(mut account_to_decks: HashMap<Pubkey, Decks>, i18n: &mut Localization) -> Self {
let fallback_pubkey = FALLBACK_PUBKEY();
- account_to_decks.entry(fallback_pubkey).or_default();
+ account_to_decks
+ .entry(fallback_pubkey)
+ .or_insert_with(|| Decks::default_decks(i18n));
Self {
account_to_decks,
@@ -79,7 +87,7 @@ impl DecksCache {
fallback_pubkey,
demo_decks(fallback_pubkey, timeline_cache, ctx),
);
- DecksCache::new(account_to_decks)
+ DecksCache::new(account_to_decks, ctx.i18n)
}
pub fn decks(&self, key: &Pubkey) -> &Decks {
@@ -88,8 +96,10 @@ impl DecksCache {
.unwrap_or_else(|| self.fallback())
}
- pub fn decks_mut(&mut self, key: &Pubkey) -> &mut Decks {
- self.account_to_decks.entry(*key).or_default()
+ pub fn decks_mut(&mut self, i18n: &mut Localization, key: &Pubkey) -> &mut Decks {
+ self.account_to_decks
+ .entry(*key)
+ .or_insert_with(|| Decks::default_decks(i18n))
}
pub fn fallback(&self) -> &Decks {
@@ -110,7 +120,7 @@ impl DecksCache {
timeline_cache: &mut TimelineCache,
pubkey: Pubkey,
) {
- let mut decks = Decks::default();
+ let mut decks = Decks::default_decks(ctx.i18n);
// add home and notifications for new accounts
add_demo_columns(
@@ -157,6 +167,7 @@ impl DecksCache {
pub fn remove(
&mut self,
+ i18n: &mut Localization,
key: &Pubkey,
timeline_cache: &mut TimelineCache,
ndb: &mut nostrdb::Ndb,
@@ -171,7 +182,7 @@ impl DecksCache {
if !self.account_to_decks.contains_key(&self.fallback_pubkey) {
self.account_to_decks
- .insert(self.fallback_pubkey, Decks::default());
+ .insert(self.fallback_pubkey, Decks::default_decks(i18n));
}
}
@@ -194,13 +205,11 @@ pub struct Decks {
decks: Vec<Deck>,
}
-impl Default for Decks {
- fn default() -> Self {
- Decks::new(Deck::default())
+impl Decks {
+ pub fn default_decks(i18n: &mut Localization) -> Self {
+ Decks::new(Deck::default_deck(i18n))
}
-}
-impl Decks {
pub fn new(deck: Deck) -> Self {
let decks = vec![deck];
@@ -381,24 +390,22 @@ pub struct Deck {
columns: Columns,
}
-impl Default for Deck {
- fn default() -> Self {
+impl Deck {
+ pub fn default_icon() -> char {
+ '🇩'
+ }
+
+ fn default_deck(i18n: &mut Localization) -> Self {
let columns = Columns::default();
Self {
columns,
icon: Deck::default_icon(),
- name: Deck::default_name().to_string(),
+ name: Deck::default_name(i18n).to_string(),
}
}
-}
-
-impl Deck {
- pub fn default_icon() -> char {
- '🇩'
- }
- pub fn default_name() -> String {
- tr!("Default Deck", "Name of the default deck feed")
+ pub fn default_name(i18n: &mut Localization) -> String {
+ tr!(i18n, "Default Deck", "Name of the default deck feed")
}
pub fn new(icon: char, name: String) -> Self {
@@ -482,7 +489,7 @@ pub fn demo_decks(
Deck {
icon: Deck::default_icon(),
- name: Deck::default_name().to_string(),
+ name: Deck::default_name(ctx.i18n).to_string(),
columns,
}
};
diff --git a/crates/notedeck_columns/src/login_manager.rs b/crates/notedeck_columns/src/login_manager.rs
@@ -2,7 +2,7 @@ use crate::key_parsing::perform_key_retrieval;
use crate::key_parsing::AcquireKeyError;
use egui::{TextBuffer, TextEdit};
use enostr::Keypair;
-use notedeck::tr;
+use notedeck::{tr, Localization};
use poll_promise::Promise;
/// The state data for acquiring a nostr key
@@ -24,7 +24,7 @@ impl<'a> AcquireKeyState {
/// Get the textedit for the UI without exposing the key variable
pub fn get_acquire_textedit(
&'a mut self,
- textedit_closure: fn(&'a mut dyn TextBuffer) -> TextEdit<'a>,
+ textedit_closure: impl FnOnce(&'a mut dyn TextBuffer) -> TextEdit<'a>,
) -> TextEdit<'a> {
textedit_closure(&mut self.desired_key)
}
@@ -106,7 +106,7 @@ impl<'a> AcquireKeyState {
self.should_create_new
}
- pub fn loading_and_error_ui(&mut self, ui: &mut egui::Ui) {
+ pub fn loading_and_error_ui(&mut self, ui: &mut egui::Ui, i18n: &mut Localization) {
ui.add_space(8.0);
ui.vertical_centered(|ui| {
@@ -116,7 +116,7 @@ impl<'a> AcquireKeyState {
});
if let Some(err) = self.check_for_error() {
- show_error(ui, err);
+ show_error(ui, i18n, err);
}
ui.add_space(8.0);
@@ -131,12 +131,16 @@ impl<'a> AcquireKeyState {
}
}
-fn show_error(ui: &mut egui::Ui, err: &AcquireKeyError) {
+fn show_error(ui: &mut egui::Ui, i18n: &mut Localization, err: &AcquireKeyError) {
ui.horizontal(|ui| {
let error_label = match err {
AcquireKeyError::InvalidKey => egui::Label::new(
- egui::RichText::new(tr!("Invalid key.", "Error message for invalid key input"))
- .color(ui.visuals().error_fg_color),
+ egui::RichText::new(tr!(
+ i18n,
+ "Invalid key.",
+ "Error message for invalid key input"
+ ))
+ .color(ui.visuals().error_fg_color),
),
AcquireKeyError::Nip05Failed(e) => {
egui::Label::new(egui::RichText::new(e).color(ui.visuals().error_fg_color))
diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs
@@ -89,7 +89,7 @@ impl SwitchingAction {
ui_ctx,
);
// pop nav after switch
- get_active_columns_mut(ctx.accounts, decks_cache)
+ get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
.column_mut(switch_action.source_column)
.router_mut()
.go_back();
@@ -102,13 +102,13 @@ impl SwitchingAction {
break 's;
}
- decks_cache.remove(to_remove, timeline_cache, ctx.ndb, ctx.pool);
+ decks_cache.remove(ctx.i18n, to_remove, timeline_cache, ctx.ndb, ctx.pool);
}
},
SwitchingAction::Columns(columns_action) => match *columns_action {
ColumnsAction::Remove(index) => {
- let kinds_to_pop =
- get_active_columns_mut(ctx.accounts, decks_cache).delete_column(index);
+ let kinds_to_pop = get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache)
+ .delete_column(index);
for kind in &kinds_to_pop {
if let Err(err) = timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
error!("error popping timeline: {err}");
@@ -117,15 +117,15 @@ impl SwitchingAction {
}
ColumnsAction::Switch(from, to) => {
- get_active_columns_mut(ctx.accounts, decks_cache).move_col(from, to);
+ get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache).move_col(from, to);
}
},
SwitchingAction::Decks(decks_action) => match *decks_action {
DecksAction::Switch(index) => {
- get_decks_mut(ctx.accounts, decks_cache).set_active(index)
+ get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).set_active(index)
}
DecksAction::Removing(index) => {
- get_decks_mut(ctx.accounts, decks_cache).remove_deck(
+ get_decks_mut(ctx.i18n, ctx.accounts, decks_cache).remove_deck(
index,
timeline_cache,
ctx.ndb,
@@ -206,10 +206,10 @@ fn process_popup_resp(
}
if let Some(NavAction::Returned(_)) = action.action {
- let column = app.columns_mut(ctx.accounts).column_mut(col);
+ let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
column.sheet_router.clear();
} else if let Some(NavAction::Navigating) = action.action {
- let column = app.columns_mut(ctx.accounts).column_mut(col);
+ let column = app.columns_mut(ctx.i18n, ctx.accounts).column_mut(col);
column.sheet_router.navigating = false;
}
@@ -235,7 +235,7 @@ fn process_nav_resp(
match action {
NavAction::Returned(return_type) => {
let r = app
- .columns_mut(ctx.accounts)
+ .columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.pop();
@@ -260,7 +260,10 @@ fn process_nav_resp(
}
NavAction::Navigated => {
- let cur_router = app.columns_mut(ctx.accounts).column_mut(col).router_mut();
+ let cur_router = app
+ .columns_mut(ctx.i18n, ctx.accounts)
+ .column_mut(col)
+ .router_mut();
cur_router.navigating = false;
if cur_router.is_replacing() {
cur_router.remove_previous_routes();
@@ -414,7 +417,7 @@ fn process_render_nav_action(
RenderNavAction::Back => Some(RouterAction::GoBack),
RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked),
RenderNavAction::RemoveColumn => {
- let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col);
+ let kinds_to_pop = app.columns_mut(ctx.i18n, ctx.accounts).delete_column(col);
for kind in &kinds_to_pop {
if let Err(err) = app.timeline_cache.pop(kind, ctx.ndb, ctx.pool) {
@@ -439,7 +442,7 @@ fn process_render_nav_action(
crate::actionbar::execute_and_process_note_action(
note_action,
ctx.ndb,
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
col,
&mut app.timeline_cache,
&mut app.threads,
@@ -480,7 +483,8 @@ fn process_render_nav_action(
};
if let Some(action) = router_action {
- let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col);
+ let cols =
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache).column_mut(col);
let router = &mut cols.router;
let sheet_router = &mut cols.sheet_router;
@@ -511,6 +515,7 @@ fn render_nav_body(
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
current_account_has_wallet,
+ i18n: ctx.i18n,
};
match top {
Route::Timeline(kind) => {
@@ -565,7 +570,7 @@ fn render_nav_body(
.accounts_action
.map(|f| RenderNavAction::SwitchingAction(SwitchingAction::Accounts(f)))
}
- Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map)
+ Route::Relays => RelayView::new(ctx.pool, &mut app.view_state.id_string_map, ctx.i18n)
.ui(ui)
.map(RenderNavAction::RelayAction),
Route::Reply(id) => {
@@ -573,6 +578,7 @@ fn render_nav_body(
txn
} else {
ui.label(tr!(
+ note_context.i18n,
"Reply to unknown note",
"Error message when reply note cannot be found"
));
@@ -583,6 +589,7 @@ fn render_nav_body(
note
} else {
ui.label(tr!(
+ note_context.i18n,
"Reply to unknown note",
"Error message when reply note cannot be found"
));
@@ -623,6 +630,7 @@ fn render_nav_body(
note
} else {
ui.label(tr!(
+ note_context.i18n,
"Quote of unknown note",
"Error message when quote note cannot be found"
));
@@ -676,15 +684,16 @@ fn render_nav_body(
None
}
Route::Support => {
- SupportView::new(&mut app.support).show(ui);
+ SupportView::new(&mut app.support, ctx.i18n).show(ui);
None
}
Route::Search => {
let id = ui.id().with(("search", depth, col));
- let navigating = get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
- .column(col)
- .router()
- .navigating;
+ let navigating =
+ get_active_columns_mut(note_context.i18n, ctx.accounts, &mut app.decks_cache)
+ .column(col)
+ .router()
+ .navigating;
let search_buffer = app.view_state.searches.entry(id).or_default();
let txn = Transaction::new(ctx.ndb).expect("txn");
@@ -711,13 +720,13 @@ fn render_nav_body(
let id = ui.id().with("new-deck");
let new_deck_state = app.view_state.id_to_deck_state.entry(id).or_default();
let mut resp = None;
- if let Some(config_resp) = ConfigureDeckView::new(new_deck_state).ui(ui) {
+ if let Some(config_resp) = ConfigureDeckView::new(new_deck_state, ctx.i18n).ui(ui) {
let cur_acc = ctx.accounts.selected_account_pubkey();
app.decks_cache
.add_deck(*cur_acc, Deck::new(config_resp.icon, config_resp.name));
// set new deck as active
- let cur_index = get_decks_mut(ctx.accounts, &mut app.decks_cache)
+ let cur_index = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.decks()
.len()
- 1;
@@ -726,7 +735,7 @@ fn render_nav_body(
)));
new_deck_state.clear();
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router()
.go_back();
}
@@ -734,7 +743,7 @@ fn render_nav_body(
}
Route::EditDeck(index) => {
let mut action = None;
- let cur_deck = get_decks_mut(ctx.accounts, &mut app.decks_cache)
+ let cur_deck = get_decks_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.decks_mut()
.get_mut(*index)
.expect("index wasn't valid");
@@ -746,7 +755,7 @@ fn render_nav_body(
.id_to_deck_state
.entry(id)
.or_insert_with(|| DeckState::from_deck(cur_deck));
- if let Some(resp) = EditDeckView::new(deck_state).ui(ui) {
+ if let Some(resp) = EditDeckView::new(deck_state, ctx.i18n).ui(ui) {
match resp {
EditDeckResponse::Edit(configure_deck_response) => {
cur_deck.edit(configure_deck_response);
@@ -757,7 +766,7 @@ fn render_nav_body(
)));
}
}
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.get_first_router()
.go_back();
}
@@ -778,7 +787,7 @@ fn render_nav_body(
return action;
};
- if EditProfileView::new(state, ctx.img_cache).ui(ui) {
+ if EditProfileView::new(ctx.i18n, state, ctx.img_cache).ui(ui) {
if let Some(state) = app.view_state.pubkey_to_profile_state.get(kp.pubkey) {
action = Some(RenderNavAction::ProfileAction(ProfileAction::SaveChanges(
SaveProfileChanges::new(kp.to_full(), state.clone()),
@@ -833,7 +842,7 @@ fn render_nav_body(
}
};
- WalletView::new(state)
+ WalletView::new(state, ctx.i18n)
.ui(ui)
.map(RenderNavAction::WalletAction)
}
@@ -841,6 +850,7 @@ fn render_nav_body(
let txn = Transaction::new(ctx.ndb).expect("txn");
let default_msats = get_current_default_msats(ctx.accounts, ctx.global_wallet);
CustomZapView::new(
+ ctx.i18n,
ctx.img_cache,
ctx.ndb,
&txn,
@@ -849,7 +859,7 @@ fn render_nav_body(
)
.ui(ui)
.map(|msats| {
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache)
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache)
.column_mut(col)
.router_mut()
.go_back();
@@ -904,9 +914,10 @@ pub fn render_nav(
NavUiType::Title => NavTitle::new(
ctx.ndb,
ctx.img_cache,
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
&[route.clone()],
col,
+ ctx.i18n,
)
.show_move_button(!narrow)
.show_delete_button(!narrow)
@@ -926,13 +937,13 @@ pub fn render_nav(
.clone(),
)
.navigating(
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.navigating,
)
.returning(
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.returning,
@@ -942,9 +953,10 @@ pub fn render_nav(
NavUiType::Title => NavTitle::new(
ctx.ndb,
ctx.img_cache,
- get_active_columns_mut(ctx.accounts, &mut app.decks_cache),
+ get_active_columns_mut(ctx.i18n, ctx.accounts, &mut app.decks_cache),
nav.routes(),
col,
+ ctx.i18n,
)
.show_move_button(!narrow)
.show_delete_button(!narrow)
diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs
@@ -1,16 +1,10 @@
use enostr::{NoteId, Pubkey};
-use notedeck::{tr, NoteZapTargetOwned, RootNoteIdBuf, WalletType};
-use std::{
- fmt::{self},
- ops::Range,
-};
+use notedeck::{tr, Localization, NoteZapTargetOwned, RootNoteIdBuf, WalletType};
+use std::ops::Range;
use crate::{
accounts::AccountsRoute,
- timeline::{
- kind::{AlgoTimeline, ColumnTitle, ListKind},
- ThreadSelection, TimelineKind,
- },
+ timeline::{kind::ColumnTitle, ThreadSelection, TimelineKind},
ui::add_column::{AddAlgoRoute, AddColumnRoute},
};
@@ -241,85 +235,104 @@ impl Route {
)
}
- pub fn title(&self) -> ColumnTitle<'_> {
+ pub fn title(&self, i18n: &mut Localization) -> ColumnTitle<'_> {
match self {
- Route::Timeline(kind) => kind.to_title(),
+ Route::Timeline(kind) => kind.to_title(i18n),
Route::Thread(_) => {
- ColumnTitle::formatted(tr!("Thread", "Column title for note thread view"))
+ ColumnTitle::formatted(tr!(i18n, "Thread", "Column title for note thread view"))
}
Route::Reply(_id) => {
- ColumnTitle::formatted(tr!("Reply", "Column title for reply composition"))
+ ColumnTitle::formatted(tr!(i18n, "Reply", "Column title for reply composition"))
}
Route::Quote(_id) => {
- ColumnTitle::formatted(tr!("Quote", "Column title for quote composition"))
+ ColumnTitle::formatted(tr!(i18n, "Quote", "Column title for quote composition"))
}
Route::Relays => {
- ColumnTitle::formatted(tr!("Relays", "Column title for relay management"))
+ ColumnTitle::formatted(tr!(i18n, "Relays", "Column title for relay management"))
}
Route::Accounts(amr) => match amr {
- AccountsRoute::Accounts => {
- ColumnTitle::formatted(tr!("Accounts", "Column title for account management"))
- }
+ AccountsRoute::Accounts => ColumnTitle::formatted(tr!(
+ i18n,
+ "Accounts",
+ "Column title for account management"
+ )),
AccountsRoute::AddAccount => ColumnTitle::formatted(tr!(
+ i18n,
"Add Account",
"Column title for adding new account"
)),
},
- Route::ComposeNote => {
- ColumnTitle::formatted(tr!("Compose Note", "Column title for note composition"))
- }
+ Route::ComposeNote => ColumnTitle::formatted(tr!(
+ i18n,
+ "Compose Note",
+ "Column title for note composition"
+ )),
Route::AddColumn(c) => match c {
- AddColumnRoute::Base => {
- ColumnTitle::formatted(tr!("Add Column", "Column title for adding new column"))
- }
+ AddColumnRoute::Base => ColumnTitle::formatted(tr!(
+ i18n,
+ "Add Column",
+ "Column title for adding new column"
+ )),
AddColumnRoute::Algo(r) => match r {
AddAlgoRoute::Base => ColumnTitle::formatted(tr!(
+ i18n,
"Add Algo Column",
"Column title for adding algorithm column"
)),
AddAlgoRoute::LastPerPubkey => ColumnTitle::formatted(tr!(
+ i18n,
"Add Last Notes Column",
"Column title for adding last notes column"
)),
},
AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!(
+ i18n,
"Add Notifications Column",
"Column title for adding notifications column"
)),
AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!(
+ i18n,
"Add External Notifications Column",
"Column title for adding external notifications column"
)),
AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!(
+ i18n,
"Add Hashtag Column",
"Column title for adding hashtag column"
)),
AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!(
+ i18n,
"Subscribe to someone's notes",
"Column title for subscribing to individual user"
)),
AddColumnRoute::ExternalIndividual => ColumnTitle::formatted(tr!(
+ i18n,
"Subscribe to someone else's notes",
"Column title for subscribing to external user"
)),
},
Route::Support => {
- ColumnTitle::formatted(tr!("Damus Support", "Column title for support page"))
+ ColumnTitle::formatted(tr!(i18n, "Damus Support", "Column title for support page"))
}
Route::NewDeck => {
- ColumnTitle::formatted(tr!("Add Deck", "Column title for adding new deck"))
+ ColumnTitle::formatted(tr!(i18n, "Add Deck", "Column title for adding new deck"))
}
Route::EditDeck(_) => {
- ColumnTitle::formatted(tr!("Edit Deck", "Column title for editing deck"))
+ ColumnTitle::formatted(tr!(i18n, "Edit Deck", "Column title for editing deck"))
}
- Route::EditProfile(_) => {
- ColumnTitle::formatted(tr!("Edit Profile", "Column title for profile editing"))
+ Route::EditProfile(_) => ColumnTitle::formatted(tr!(
+ i18n,
+ "Edit Profile",
+ "Column title for profile editing"
+ )),
+ Route::Search => {
+ ColumnTitle::formatted(tr!(i18n, "Search", "Column title for search page"))
}
- Route::Search => ColumnTitle::formatted(tr!("Search", "Column title for search page")),
Route::Wallet(_) => {
- ColumnTitle::formatted(tr!("Wallet", "Column title for wallet management"))
+ ColumnTitle::formatted(tr!(i18n, "Wallet", "Column title for wallet management"))
}
Route::CustomizeZapAmount(_) => ColumnTitle::formatted(tr!(
+ i18n,
"Customize Zap Amount",
"Column title for zap amount customization"
)),
@@ -492,12 +505,13 @@ impl<R: Clone> Router<R> {
}
}
+/*
impl fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Route::Timeline(kind) => match kind {
TimelineKind::List(ListKind::Contact(_pk)) => {
- write!(f, "{}", tr!("Home", "Display name for home feed"))
+ write!(f, "{}", i18n, "Home", "Display name for home feed"))
}
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => {
write!(
@@ -583,6 +597,7 @@ impl fmt::Display for Route {
}
}
}
+*/
#[derive(Clone, Debug)]
pub struct SingletonRouter<R: Clone> {
diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs
@@ -13,7 +13,7 @@ use crate::{
Error,
};
-use notedeck::{storage, DataPath, DataPathType, Directory};
+use notedeck::{storage, DataPath, DataPathType, Directory, Localization};
use tokenator::{ParseError, TokenParser, TokenWriter};
pub static DECKS_CACHE_FILE: &str = "decks_cache.json";
@@ -22,6 +22,7 @@ pub fn load_decks_cache(
path: &DataPath,
ndb: &Ndb,
timeline_cache: &mut TimelineCache,
+ i18n: &mut Localization,
) -> Option<DecksCache> {
let data_path = path.path(DataPathType::Setting);
@@ -40,7 +41,7 @@ pub fn load_decks_cache(
serde_json::from_str::<SerializableDecksCache>(&decks_cache_str).ok()?;
serializable_decks_cache
- .decks_cache(ndb, timeline_cache)
+ .decks_cache(ndb, timeline_cache, i18n)
.ok()
}
@@ -91,6 +92,7 @@ impl SerializableDecksCache {
self,
ndb: &Ndb,
timeline_cache: &mut TimelineCache,
+ i18n: &mut Localization,
) -> Result<DecksCache, Error> {
let account_to_decks = self
.decks_cache
@@ -102,7 +104,7 @@ impl SerializableDecksCache {
})
.collect::<Result<HashMap<Pubkey, Decks>, Error>>()?;
- Ok(DecksCache::new(account_to_decks))
+ Ok(DecksCache::new(account_to_decks, i18n))
}
}
diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs
@@ -6,11 +6,11 @@ use nostrdb::{Ndb, Transaction};
use notedeck::{
contacts::{contacts_filter, hybrid_contacts_filter},
filter::{self, default_limit, default_remote_limit, HybridFilter},
- tr, FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf,
+ tr, FilterError, FilterState, Localization, NoteCache, RootIdError, RootNoteIdBuf,
};
use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
use std::hash::{Hash, Hasher};
-use std::{borrow::Cow, fmt::Display};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
use tracing::{error, warn};
@@ -254,6 +254,7 @@ impl AlgoTimeline {
}
}
+/*
impl Display for TimelineKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -301,6 +302,7 @@ impl Display for TimelineKind {
}
}
}
+*/
impl TimelineKind {
pub fn pubkey(&self) -> Option<&Pubkey> {
@@ -594,31 +596,32 @@ impl TimelineKind {
}
}
- pub fn to_title(&self) -> ColumnTitle<'_> {
+ pub fn to_title(&self, i18n: &mut Localization) -> ColumnTitle<'_> {
match self {
TimelineKind::Search(query) => {
ColumnTitle::formatted(format!("Search \"{}\"", query.search))
}
TimelineKind::List(list_kind) => match list_kind {
ListKind::Contact(_pubkey_source) => {
- ColumnTitle::formatted(tr!("Contacts", "Column title for contact lists"))
+ ColumnTitle::formatted(tr!(i18n, "Contacts", "Column title for contact lists"))
}
},
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind {
ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!(
+ i18n,
"Contacts (last notes)",
"Column title for last notes per contact"
)),
},
TimelineKind::Notifications(_pubkey_source) => {
- ColumnTitle::formatted(tr!("Notifications", "Column title for notifications"))
+ ColumnTitle::formatted(tr!(i18n, "Notifications", "Column title for notifications"))
}
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Universe => {
- ColumnTitle::formatted(tr!("Universe", "Column title for universe feed"))
+ ColumnTitle::formatted(tr!(i18n, "Universe", "Column title for universe feed"))
}
TimelineKind::Generic(_) => {
- ColumnTitle::formatted(tr!("Custom", "Column title for custom timelines"))
+ ColumnTitle::formatted(tr!(i18n, "Custom", "Column title for custom timelines"))
}
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
}
diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs
@@ -9,8 +9,8 @@ use crate::{
use notedeck::{
contacts::hybrid_contacts_filter,
filter::{self, HybridFilter},
- tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache,
- NoteRef, UnknownIds,
+ tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization,
+ NoteCache, NoteRef, UnknownIds,
};
use egui_virtual_list::VirtualList;
@@ -64,11 +64,15 @@ pub enum ViewFilter {
}
impl ViewFilter {
- pub fn name(&self) -> String {
+ pub fn name(&self, i18n: &mut Localization) -> String {
match self {
- ViewFilter::Notes => tr!("Notes", "Filter label for notes only view"),
+ ViewFilter::Notes => tr!(i18n, "Notes", "Filter label for notes only view"),
ViewFilter::NotesAndReplies => {
- tr!("Notes & Replies", "Filter label for notes and replies view")
+ tr!(
+ i18n,
+ "Notes & Replies",
+ "Filter label for notes and replies view"
+ )
}
}
}
diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs
@@ -1,12 +1,11 @@
use crate::login_manager::AcquireKeyState;
use crate::ui::{Preview, PreviewConfig};
use egui::{
- Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextBuffer, TextEdit,
- Vec2,
+ Align, Button, Color32, Frame, InnerResponse, Layout, Margin, RichText, TextEdit, Vec2,
};
use egui_winit::clipboard::Clipboard;
use enostr::Keypair;
-use notedeck::{fonts::get_font_size, tr, AppAction, NotedeckTextStyle};
+use notedeck::{fonts::get_font_size, tr, AppAction, Localization, NotedeckTextStyle};
use notedeck_ui::{
app_images,
context_menu::{input_context, PasteBehavior},
@@ -15,6 +14,7 @@ use notedeck_ui::{
pub struct AccountLoginView<'a> {
manager: &'a mut AcquireKeyState,
clipboard: &'a mut Clipboard,
+ i18n: &'a mut Localization,
}
pub enum AccountLoginResponse {
@@ -23,8 +23,16 @@ pub enum AccountLoginResponse {
}
impl<'a> AccountLoginView<'a> {
- pub fn new(manager: &'a mut AcquireKeyState, clipboard: &'a mut Clipboard) -> Self {
- AccountLoginView { manager, clipboard }
+ pub fn new(
+ manager: &'a mut AcquireKeyState,
+ clipboard: &'a mut Clipboard,
+ i18n: &'a mut Localization,
+ ) -> Self {
+ AccountLoginView {
+ manager,
+ clipboard,
+ i18n,
+ }
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> InnerResponse<Option<AccountLoginResponse>> {
@@ -35,11 +43,11 @@ impl<'a> AccountLoginView<'a> {
ui.vertical(|ui| {
ui.vertical_centered(|ui| {
ui.add_space(32.0);
- ui.label(login_title_text());
+ ui.label(login_title_text(self.i18n));
});
ui.horizontal(|ui| {
- ui.label(login_textedit_info_text());
+ ui.label(login_textedit_info_text(self.i18n));
});
ui.vertical_centered_justified(|ui| {
@@ -48,7 +56,7 @@ impl<'a> AccountLoginView<'a> {
let button_width = 32.0;
let text_edit_width = available_width - button_width;
- let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager));
+ let textedit_resp = ui.add_sized([text_edit_width, 40.0], login_textedit(self.manager, self.i18n));
input_context(&textedit_resp, self.clipboard, self.manager.input_buffer(), PasteBehavior::Clear);
if eye_button(ui, self.manager.password_visible()).clicked() {
@@ -58,28 +66,28 @@ impl<'a> AccountLoginView<'a> {
ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
let help_text_style = NotedeckTextStyle::Small;
ui.add(egui::Label::new(
- RichText::new(tr!("Enter your public key (npub), nostr address (e.g. {address}), or private key (nsec). You must enter your private key to be able to post, reply, etc.", "Instructions for entering Nostr credentials", address="vrod@damus.io"))
+ RichText::new(tr!(self.i18n, "Enter your public key (npub), nostr address (e.g. {address}), or private key (nsec). You must enter your private key to be able to post, reply, etc.", "Instructions for entering Nostr credentials", address="vrod@damus.io"))
.text_style(help_text_style.text_style())
.size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()),
).wrap())
});
- self.manager.loading_and_error_ui(ui);
+ self.manager.loading_and_error_ui(ui, self.i18n);
- if ui.add(login_button()).clicked() {
+ if ui.add(login_button(self.i18n)).clicked() {
self.manager.apply_acquire();
}
});
ui.horizontal(|ui| {
ui.label(
- RichText::new(tr!("New to Nostr?", "Label asking if the user is new to Nostr. Underneath this label is a button to create an account."))
+ RichText::new(tr!(self.i18n,"New to Nostr?", "Label asking if the user is new to Nostr. Underneath this label is a button to create an account."))
.color(ui.style().visuals.noninteractive().fg_stroke.color)
.text_style(NotedeckTextStyle::Body.text_style()),
);
if ui
- .add(Button::new(RichText::new(tr!("Create Account", "Button to create a new account"))).frame(false))
+ .add(Button::new(RichText::new(tr!(self.i18n,"Create Account", "Button to create a new account"))).frame(false))
.clicked()
{
self.manager.should_create_new();
@@ -98,21 +106,21 @@ impl<'a> AccountLoginView<'a> {
}
}
-fn login_title_text() -> RichText {
- RichText::new(tr!("Login", "Login page title"))
+fn login_title_text(i18n: &mut Localization) -> RichText {
+ RichText::new(tr!(i18n, "Login", "Login page title"))
.text_style(NotedeckTextStyle::Heading2.text_style())
.strong()
}
-fn login_textedit_info_text() -> RichText {
- RichText::new(tr!("Enter your key", "Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05)."))
+fn login_textedit_info_text(i18n: &mut Localization) -> RichText {
+ RichText::new(tr!(i18n, "Enter your key", "Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05)."))
.strong()
.text_style(NotedeckTextStyle::Body.text_style())
}
-fn login_button() -> Button<'static> {
+fn login_button(i18n: &mut Localization) -> Button<'static> {
Button::new(
- RichText::new(tr!("Login now — let's do this!", "Login button text"))
+ RichText::new(tr!(i18n, "Login now — let's do this!", "Login button text"))
.text_style(NotedeckTextStyle::Body.text_style())
.strong(),
)
@@ -120,11 +128,15 @@ fn login_button() -> Button<'static> {
.min_size(Vec2::new(0.0, 40.0))
}
-fn login_textedit(manager: &mut AcquireKeyState) -> TextEdit {
- let create_textedit: fn(&mut dyn TextBuffer) -> TextEdit = |text| {
+fn login_textedit<'a>(
+ manager: &'a mut AcquireKeyState,
+ i18n: &'a mut Localization,
+) -> TextEdit<'a> {
+ let create_textedit = |text| {
egui::TextEdit::singleline(text)
.hint_text(
RichText::new(tr!(
+ i18n,
"Your key here...",
"Placeholder text for key input field"
))
@@ -167,7 +179,7 @@ mod preview {
impl App for AccountLoginPreview {
fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
- AccountLoginView::new(&mut self.manager, ctx.clipboard).ui(ui);
+ AccountLoginView::new(&mut self.manager, ctx.clipboard, ctx.i18n).ui(ui);
None
}
diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs
@@ -3,16 +3,17 @@ use egui::{
};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
-use notedeck::{tr, Accounts, Images};
+use notedeck::{tr, Accounts, Images, Localization};
use notedeck_ui::colors::PINK;
+use notedeck_ui::profile::preview::SimpleProfilePreview;
use notedeck_ui::app_images;
-use notedeck_ui::profile::preview::SimpleProfilePreview;
pub struct AccountsView<'a> {
ndb: &'a Ndb,
accounts: &'a Accounts,
img_cache: &'a mut Images,
+ i18n: &'a mut Localization,
}
#[derive(Clone, Debug)]
@@ -29,24 +30,30 @@ enum ProfilePreviewAction {
}
impl<'a> AccountsView<'a> {
- pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut Images) -> Self {
+ pub fn new(
+ ndb: &'a Ndb,
+ accounts: &'a Accounts,
+ img_cache: &'a mut Images,
+ i18n: &'a mut Localization,
+ ) -> Self {
AccountsView {
ndb,
accounts,
img_cache,
+ i18n,
}
}
pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> {
Frame::new().outer_margin(12.0).show(ui, |ui| {
- if let Some(resp) = Self::top_section_buttons_widget(ui).inner {
+ if let Some(resp) = Self::top_section_buttons_widget(ui, self.i18n).inner {
return Some(resp);
}
ui.add_space(8.0);
scroll_area()
.show(ui, |ui| {
- Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache)
+ Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache, self.i18n)
})
.inner
})
@@ -57,6 +64,7 @@ impl<'a> AccountsView<'a> {
accounts: &Accounts,
ndb: &Ndb,
img_cache: &mut Images,
+ i18n: &mut Localization,
) -> Option<AccountsViewResponse> {
let mut return_op: Option<AccountsViewResponse> = None;
ui.allocate_ui_with_layout(
@@ -79,8 +87,12 @@ impl<'a> AccountsView<'a> {
let max_size = egui::vec2(ui.available_width(), 77.0);
let resp = ui.allocate_response(max_size, egui::Sense::click());
ui.allocate_new_ui(UiBuilder::new().max_rect(resp.rect), |ui| {
- let preview =
- SimpleProfilePreview::new(profile.as_ref(), img_cache, has_nsec);
+ let preview = SimpleProfilePreview::new(
+ profile.as_ref(),
+ img_cache,
+ i18n,
+ has_nsec,
+ );
show_profile_card(ui, preview, max_size, is_selected, resp)
})
.inner
@@ -104,12 +116,13 @@ impl<'a> AccountsView<'a> {
fn top_section_buttons_widget(
ui: &mut egui::Ui,
+ i18n: &mut Localization,
) -> InnerResponse<Option<AccountsViewResponse>> {
ui.allocate_ui_with_layout(
Vec2::new(ui.available_size_before_wrap().x, 32.0),
Layout::left_to_right(egui::Align::Center),
|ui| {
- if ui.add(add_account_button()).clicked() {
+ if ui.add(add_account_button(i18n)).clicked() {
Some(AccountsViewResponse::RouteToLogin)
} else {
None
@@ -141,16 +154,14 @@ fn show_profile_card(
.inner_margin(8.0)
.show(ui, |ui| {
ui.horizontal(|ui| {
+ let btn = sign_out_button(preview.i18n);
ui.add(preview);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if card_resp.clicked() {
op = Some(ProfilePreviewAction::SwitchTo);
}
- if ui
- .add_sized(egui::Vec2::new(84.0, 32.0), sign_out_button())
- .clicked()
- {
+ if ui.add_sized(egui::Vec2::new(84.0, 32.0), btn).clicked() {
op = Some(ProfilePreviewAction::RemoveAccount)
}
});
@@ -168,19 +179,24 @@ fn scroll_area() -> ScrollArea {
.auto_shrink([false; 2])
}
-fn add_account_button() -> Button<'static> {
+fn add_account_button(i18n: &mut Localization) -> Button<'static> {
Button::image_and_text(
app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
- RichText::new(tr!("Add account", "Button label to add a new account"))
- .size(16.0)
- // TODO: this color should not be hard coded. Find some way to add it to the visuals
- .color(PINK),
+ RichText::new(tr!(
+ i18n,
+ "Add account",
+ "Button label to add a new account"
+ ))
+ .size(16.0)
+ // TODO: this color should not be hard coded. Find some way to add it to the visuals
+ .color(PINK),
)
.frame(false)
}
-fn sign_out_button() -> egui::Button<'static> {
+fn sign_out_button(i18n: &mut Localization) -> egui::Button<'static> {
egui::Button::new(RichText::new(tr!(
+ i18n,
"Sign out",
"Button label to sign out of account"
)))
diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs
@@ -17,7 +17,7 @@ use crate::{
Damus,
};
-use notedeck::{tr, AppContext, Images, NotedeckTextStyle, UserAccount};
+use notedeck::{tr, AppContext, Images, Localization, NotedeckTextStyle, UserAccount};
use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
@@ -167,6 +167,7 @@ pub struct AddColumnView<'a> {
ndb: &'a Ndb,
img_cache: &'a mut Images,
cur_account: &'a UserAccount,
+ i18n: &'a mut Localization,
}
impl<'a> AddColumnView<'a> {
@@ -175,12 +176,14 @@ impl<'a> AddColumnView<'a> {
ndb: &'a Ndb,
img_cache: &'a mut Images,
cur_account: &'a UserAccount,
+ i18n: &'a mut Localization,
) -> Self {
Self {
key_state_map,
ndb,
img_cache,
cur_account,
+ i18n,
}
}
@@ -229,8 +232,9 @@ impl<'a> AddColumnView<'a> {
deck_author: Pubkey,
) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData {
- title: tr!("Contact List", "Title for contact list column"),
+ title: tr!(self.i18n, "Contact List", "Title for contact list column"),
description: tr!(
+ self.i18n,
"Source the last note for each user in your contact list",
"Description for contact list column"
),
@@ -248,8 +252,13 @@ impl<'a> AddColumnView<'a> {
fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData {
- title: tr!("Last Note per User", "Title for last note per user column"),
+ title: tr!(
+ self.i18n,
+ "Last Note per User",
+ "Title for last note per user column"
+ ),
description: tr!(
+ self.i18n,
"Show the last note for each user from a list",
"Description for last note per user column"
),
@@ -298,6 +307,7 @@ impl<'a> AddColumnView<'a> {
egui::TextEdit::singleline(text)
.hint_text(
RichText::new(tr!(
+ self.i18n,
"Enter the user's key (npub, hex, nip05) here...",
"Hint text to prompt entering the user's public key."
))
@@ -312,9 +322,11 @@ impl<'a> AddColumnView<'a> {
ui.add(text_edit);
key_state.handle_input_change_after_acquire();
- key_state.loading_and_error_ui(ui);
+ key_state.loading_and_error_ui(ui, self.i18n);
- if key_state.get_login_keypair().is_none() && ui.add(find_user_button()).clicked() {
+ if key_state.get_login_keypair().is_none()
+ && ui.add(find_user_button(self.i18n)).clicked()
+ {
key_state.apply_acquire();
}
@@ -337,7 +349,7 @@ impl<'a> AddColumnView<'a> {
}
}
- ui.add(add_column_button())
+ ui.add(add_column_button(self.i18n))
.clicked()
.then(|| to_option(keypair.pubkey).take_as_response(self.cur_account))
} else {
@@ -452,11 +464,12 @@ impl<'a> AddColumnView<'a> {
helper.take_animation_response()
}
- fn get_base_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> {
+ fn get_base_options(&mut self, ui: &mut Ui) -> Vec<ColumnOptionData> {
let mut vec = Vec::new();
vec.push(ColumnOptionData {
- title: tr!("Home", "Title for Home column"),
+ title: tr!(self.i18n, "Home", "Title for Home column"),
description: tr!(
+ self.i18n,
"See notes from your contacts",
"Description for Home column"
),
@@ -468,8 +481,9 @@ impl<'a> AddColumnView<'a> {
}),
});
vec.push(ColumnOptionData {
- title: tr!("Notifications", "Title for notifications column"),
+ title: tr!(self.i18n, "Notifications", "Title for notifications column"),
description: tr!(
+ self.i18n,
"Stay up to date with notifications and mentions",
"Description for notifications column"
),
@@ -477,8 +491,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedNotification,
});
vec.push(ColumnOptionData {
- title: tr!("Universe", "Title for universe column"),
+ title: tr!(self.i18n, "Universe", "Title for universe column"),
description: tr!(
+ self.i18n,
"See the whole nostr universe",
"Description for universe column"
),
@@ -486,8 +501,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::Universe,
});
vec.push(ColumnOptionData {
- title: tr!("Hashtags", "Title for hashtags column"),
+ title: tr!(self.i18n, "Hashtags", "Title for hashtags column"),
description: tr!(
+ self.i18n,
"Stay up to date with a certain hashtag",
"Description for hashtags column"
),
@@ -495,8 +511,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedHashtag,
});
vec.push(ColumnOptionData {
- title: tr!("Individual", "Title for individual user column"),
+ title: tr!(self.i18n, "Individual", "Title for individual user column"),
description: tr!(
+ self.i18n,
"Stay up to date with someone's notes & replies",
"Description for individual user column"
),
@@ -504,8 +521,9 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedIndividual,
});
vec.push(ColumnOptionData {
- title: tr!("Algo", "Title for algorithmic feeds column"),
+ title: tr!(self.i18n, "Algo", "Title for algorithmic feeds column"),
description: tr!(
+ self.i18n,
"Algorithmic feeds to aid in note discovery",
"Description for algorithmic feeds column"
),
@@ -516,7 +534,7 @@ impl<'a> AddColumnView<'a> {
vec
}
- fn get_notifications_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> {
+ fn get_notifications_options(&mut self, ui: &mut Ui) -> Vec<ColumnOptionData> {
let mut vec = Vec::new();
let source = if self.cur_account.key.secret_key.is_some() {
@@ -526,8 +544,13 @@ impl<'a> AddColumnView<'a> {
};
vec.push(ColumnOptionData {
- title: tr!("Your Notifications", "Title for your notifications column"),
+ title: tr!(
+ self.i18n,
+ "Your Notifications",
+ "Title for your notifications column"
+ ),
description: tr!(
+ self.i18n,
"Stay up to date with your notifications and mentions",
"Description for your notifications column"
),
@@ -537,10 +560,12 @@ impl<'a> AddColumnView<'a> {
vec.push(ColumnOptionData {
title: tr!(
+ self.i18n,
"Someone else's Notifications",
"Title for someone else's notifications column"
),
description: tr!(
+ self.i18n,
"Stay up to date with someone else's notifications and mentions",
"Description for someone else's notifications column"
),
@@ -551,7 +576,7 @@ impl<'a> AddColumnView<'a> {
vec
}
- fn get_individual_options(&self) -> Vec<ColumnOptionData> {
+ fn get_individual_options(&mut self) -> Vec<ColumnOptionData> {
let mut vec = Vec::new();
let source = if self.cur_account.key.secret_key.is_some() {
@@ -561,8 +586,9 @@ impl<'a> AddColumnView<'a> {
};
vec.push(ColumnOptionData {
- title: tr!("Your Notes", "Title for your notes column"),
+ title: tr!(self.i18n, "Your Notes", "Title for your notes column"),
description: tr!(
+ self.i18n,
"Keep track of your notes & replies",
"Description for your notes column"
),
@@ -572,10 +598,12 @@ impl<'a> AddColumnView<'a> {
vec.push(ColumnOptionData {
title: tr!(
+ self.i18n,
"Someone else's Notes",
"Title for someone else's notes column"
),
description: tr!(
+ self.i18n,
"Stay up to date with someone else's notes & replies",
"Description for someone else's notes column"
),
@@ -587,14 +615,14 @@ impl<'a> AddColumnView<'a> {
}
}
-fn find_user_button() -> impl Widget {
- let label = tr!("Find User", "Label for find user button");
+fn find_user_button(i18n: &mut Localization) -> impl Widget {
+ let label = tr!(i18n, "Find User", "Label for find user button");
let color = notedeck_ui::colors::PINK;
move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui)
}
-fn add_column_button() -> impl Widget {
- let label = tr!("Add", "Label for add column button");
+fn add_column_button(i18n: &mut Localization) -> impl Widget {
+ let label = tr!(i18n, "Add", "Label for add column button");
let color = notedeck_ui::colors::PINK;
move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui)
}
@@ -639,6 +667,7 @@ pub fn render_add_column_routes(
ctx.ndb,
ctx.img_cache,
ctx.accounts.get_selected_account(),
+ ctx.i18n,
);
let resp = match route {
AddColumnRoute::Base => add_column_view.ui(ui),
@@ -649,7 +678,7 @@ pub fn render_add_column_routes(
},
AddColumnRoute::UndecidedNotification => add_column_view.notifications_ui(ui),
AddColumnRoute::ExternalNotification => add_column_view.external_notification_ui(ui),
- AddColumnRoute::Hashtag => hashtag_ui(ui, &mut app.view_state.id_string_map),
+ AddColumnRoute::Hashtag => hashtag_ui(ui, ctx.i18n, &mut app.view_state.id_string_map),
AddColumnRoute::UndecidedIndividual => add_column_view.individual_ui(ui),
AddColumnRoute::ExternalIndividual => add_column_view.external_individual_ui(ui),
};
@@ -677,7 +706,7 @@ pub fn render_add_column_routes(
ctx.accounts,
);
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone()));
@@ -689,7 +718,7 @@ pub fn render_add_column_routes(
// If we are undecided, we simply route to the LastPerPubkey
// algo route selection
AlgoOption::LastPerPubkey(Decision::Undecided) => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(Route::AddColumn(AddColumnRoute::Algo(
@@ -717,7 +746,7 @@ pub fn render_add_column_routes(
ctx.accounts,
);
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to_replaced(Route::timeline(timeline.kind.clone()));
@@ -735,13 +764,13 @@ pub fn render_add_column_routes(
},
AddColumnResponse::UndecidedNotification => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(Route::AddColumn(AddColumnRoute::UndecidedNotification));
}
AddColumnResponse::ExternalNotification => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(crate::route::Route::AddColumn(
@@ -749,13 +778,13 @@ pub fn render_add_column_routes(
));
}
AddColumnResponse::Hashtag => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(crate::route::Route::AddColumn(AddColumnRoute::Hashtag));
}
AddColumnResponse::UndecidedIndividual => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(crate::route::Route::AddColumn(
@@ -763,7 +792,7 @@ pub fn render_add_column_routes(
));
}
AddColumnResponse::ExternalIndividual => {
- app.columns_mut(ctx.accounts)
+ app.columns_mut(ctx.i18n, ctx.accounts)
.column_mut(col)
.router_mut()
.route_to(crate::route::Route::AddColumn(
@@ -776,6 +805,7 @@ pub fn render_add_column_routes(
pub fn hashtag_ui(
ui: &mut Ui,
+ i18n: &mut Localization,
id_string_map: &mut HashMap<Id, String>,
) -> Option<AddColumnResponse> {
padding(16.0, ui, |ui| {
@@ -785,6 +815,7 @@ pub fn hashtag_ui(
let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text(
RichText::new(tr!(
+ i18n,
"Enter the desired hashtags here (for multiple space-separated)",
"Placeholder for hashtag input field"
))
@@ -801,7 +832,7 @@ pub fn hashtag_ui(
let mut handle_user_input = false;
if ui.input(|i| i.key_released(egui::Key::Enter))
|| ui
- .add_sized(egui::vec2(50.0, 40.0), add_column_button())
+ .add_sized(egui::vec2(50.0, 40.0), add_column_button(i18n))
.clicked()
{
handle_user_input = true;
diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs
@@ -13,7 +13,7 @@ use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
use enostr::Pubkey;
use nostrdb::{Ndb, Transaction};
use notedeck::tr;
-use notedeck::{Images, NotedeckTextStyle};
+use notedeck::{Images, Localization, NotedeckTextStyle};
use notedeck_ui::app_images;
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -27,6 +27,7 @@ pub struct NavTitle<'a> {
routes: &'a [Route],
col_id: usize,
options: u32,
+ i18n: &'a mut Localization,
}
impl<'a> NavTitle<'a> {
@@ -40,6 +41,7 @@ impl<'a> NavTitle<'a> {
columns: &'a Columns,
routes: &'a [Route],
col_id: usize,
+ i18n: &'a mut Localization,
) -> Self {
let options = Self::SHOW_MOVE | Self::SHOW_DELETE;
NavTitle {
@@ -49,6 +51,7 @@ impl<'a> NavTitle<'a> {
routes,
col_id,
options,
+ i18n,
}
}
@@ -129,7 +132,7 @@ impl<'a> NavTitle<'a> {
// NOTE(jb55): include graphic in back label as well because why
// not it looks cool
let pfp_resp = self.title_pfp(ui, prev, 32.0);
- let column_title = prev.title();
+ let column_title = prev.title(self.i18n);
let back_resp = match &column_title {
ColumnTitle::Simple(title) => ui.add(Self::back_label(title, color)),
@@ -182,7 +185,7 @@ impl<'a> NavTitle<'a> {
animation_resp
}
- fn delete_button_section(&self, ui: &mut egui::Ui) -> bool {
+ fn delete_button_section(&mut self, ui: &mut egui::Ui) -> bool {
let id = ui.id().with("title");
let delete_button_resp = self.delete_column_button(ui, 32.0);
@@ -193,14 +196,18 @@ impl<'a> NavTitle<'a> {
if ui.data_mut(|d| *d.get_temp_mut_or_default(id)) {
let mut confirm_pressed = false;
delete_button_resp.show_tooltip_ui(|ui| {
- let confirm_resp = ui.button(tr!("Confirm", "Button label to confirm an action"));
+ let confirm_resp = ui.button(tr!(
+ self.i18n,
+ "Confirm",
+ "Button label to confirm an action"
+ ));
if confirm_resp.clicked() {
confirm_pressed = true;
}
if confirm_resp.clicked()
|| ui
- .button(tr!("Cancel", "Button label to cancel an action"))
+ .button(tr!(self.i18n, "Cancel", "Button label to cancel an action"))
.clicked()
{
ui.data_mut(|d| d.insert_temp(id, false));
@@ -211,8 +218,11 @@ impl<'a> NavTitle<'a> {
}
confirm_pressed
} else {
- delete_button_resp
- .on_hover_text(tr!("Delete this column", "Tooltip for deleting a column"));
+ delete_button_resp.on_hover_text(tr!(
+ self.i18n,
+ "Delete this column",
+ "Tooltip for deleting a column"
+ ));
false
}
}
@@ -227,6 +237,7 @@ impl<'a> NavTitle<'a> {
// showing the hover text while showing the move tooltip causes some weird visuals
if ui.data(|d| d.get_temp::<bool>(cur_id).is_none()) {
move_resp = move_resp.on_hover_text(tr!(
+ self.i18n,
"Moves this column to another position",
"Tooltip for moving a column"
));
@@ -522,8 +533,8 @@ impl<'a> NavTitle<'a> {
.selectable(false)
}
- fn title_label(&self, ui: &mut egui::Ui, top: &Route) {
- let column_title = top.title();
+ fn title_label(&mut self, ui: &mut egui::Ui, top: &Route) {
+ let column_title = top.title(self.i18n);
match &column_title {
ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)),
diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs
@@ -1,6 +1,6 @@
use crate::{app_style::deck_icon_font_sized, deck_state::DeckState};
use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
-use notedeck::tr;
+use notedeck::{tr, Localization};
use notedeck::{NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -11,6 +11,7 @@ use notedeck_ui::{
pub struct ConfigureDeckView<'a> {
state: &'a mut DeckState,
create_button_text: String,
+ pub i18n: &'a mut Localization,
}
pub struct ConfigureDeckResponse {
@@ -19,10 +20,11 @@ pub struct ConfigureDeckResponse {
}
impl<'a> ConfigureDeckView<'a> {
- pub fn new(state: &'a mut DeckState) -> Self {
+ pub fn new(state: &'a mut DeckState, i18n: &'a mut Localization) -> Self {
Self {
state,
- create_button_text: tr!("Create Deck", "Button label to create a new deck"),
+ create_button_text: tr!(i18n, "Create Deck", "Button label to create a new deck"),
+ i18n,
}
}
@@ -38,14 +40,19 @@ impl<'a> ConfigureDeckView<'a> {
);
padding(16.0, ui, |ui| {
ui.add(Label::new(
- RichText::new(tr!("Deck name", "Label for deck name input field"))
- .font(title_font.clone()),
+ RichText::new(tr!(
+ self.i18n,
+ "Deck name",
+ "Label for deck name input field"
+ ))
+ .font(title_font.clone()),
));
ui.add_space(8.0);
ui.text_edit_singleline(&mut self.state.deck_name);
ui.add_space(8.0);
ui.add(Label::new(
RichText::new(tr!(
+ self.i18n,
"We recommend short names",
"Hint for deck name input field"
))
@@ -58,7 +65,8 @@ impl<'a> ConfigureDeckView<'a> {
ui.add_space(32.0);
ui.add(Label::new(
- RichText::new(tr!("Icon", "Label for deck icon selection")).font(title_font),
+ RichText::new(tr!(self.i18n, "Icon", "Label for deck icon selection"))
+ .font(title_font),
));
if ui
@@ -97,7 +105,12 @@ impl<'a> ConfigureDeckView<'a> {
self.state.warn_no_title = false;
}
- show_warnings(ui, self.state.warn_no_icon, self.state.warn_no_title);
+ show_warnings(
+ ui,
+ self.i18n,
+ self.state.warn_no_icon,
+ self.state.warn_no_title,
+ );
let mut resp = None;
if ui
@@ -125,19 +138,22 @@ impl<'a> ConfigureDeckView<'a> {
}
}
-fn show_warnings(ui: &mut Ui, warn_no_icon: bool, warn_no_title: bool) {
+fn show_warnings(ui: &mut Ui, i18n: &mut Localization, warn_no_icon: bool, warn_no_title: bool) {
let warning = if warn_no_title && warn_no_icon {
tr!(
+ i18n,
"Please create a name for the deck and select an icon.",
"Error message for missing deck name and icon"
)
} else if warn_no_title {
tr!(
+ i18n,
"Please create a name for the deck.",
"Error message for missing deck name"
)
} else if warn_no_icon {
tr!(
+ i18n,
"Please select an icon.",
"Error message for missing deck icon"
)
@@ -320,12 +336,8 @@ mod preview {
}
impl App for ConfigureDeckPreview {
- fn update(
- &mut self,
- _app_ctx: &mut AppContext<'_>,
- ui: &mut egui::Ui,
- ) -> Option<AppAction> {
- ConfigureDeckView::new(&mut self.state).ui(ui);
+ fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
+ ConfigureDeckView::new(&mut self.state, ctx.i18n).ui(ui);
None
}
diff --git a/crates/notedeck_columns/src/ui/edit_deck.rs b/crates/notedeck_columns/src/ui/edit_deck.rs
@@ -3,7 +3,7 @@ use egui::Widget;
use crate::deck_state::DeckState;
use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView};
-use notedeck::tr;
+use notedeck::{tr, Localization};
use notedeck_ui::padding;
pub struct EditDeckView<'a> {
@@ -16,9 +16,9 @@ pub enum EditDeckResponse {
}
impl<'a> EditDeckView<'a> {
- pub fn new(state: &'a mut DeckState) -> Self {
- let config_view = ConfigureDeckView::new(state)
- .with_create_text(tr!("Edit Deck", "Button label to edit a deck"));
+ pub fn new(state: &'a mut DeckState, i18n: &'a mut Localization) -> Self {
+ let txt = tr!(i18n, "Edit Deck", "Button label to edit a deck");
+ let config_view = ConfigureDeckView::new(state, i18n).with_create_text(txt);
Self { config_view }
}
@@ -26,7 +26,7 @@ impl<'a> EditDeckView<'a> {
let mut edit_deck_resp = None;
padding(egui::Margin::symmetric(16, 4), ui, |ui| {
- if ui.add(delete_button()).clicked() {
+ if ui.add(delete_button(self.config_view.i18n)).clicked() {
edit_deck_resp = Some(EditDeckResponse::Delete);
}
});
@@ -39,12 +39,12 @@ impl<'a> EditDeckView<'a> {
}
}
-fn delete_button() -> impl Widget {
+fn delete_button<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| {
let size = egui::vec2(108.0, 40.0);
ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| {
ui.add(
- egui::Button::new(tr!("Delete Deck", "Button label to delete a deck"))
+ egui::Button::new(tr!(i18n, "Delete Deck", "Button label to delete a deck"))
.fill(ui.visuals().error_fg_color)
.min_size(size),
)
@@ -75,12 +75,8 @@ mod preview {
}
impl App for EditDeckPreview {
- fn update(
- &mut self,
- _app_ctx: &mut AppContext<'_>,
- ui: &mut egui::Ui,
- ) -> Option<AppAction> {
- EditDeckView::new(&mut self.state).ui(ui);
+ fn update(&mut self, ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
+ EditDeckView::new(&mut self.state, ctx.i18n).ui(ui);
None
}
}
diff --git a/crates/notedeck_columns/src/ui/note/custom_zap.rs b/crates/notedeck_columns/src/ui/note/custom_zap.rs
@@ -1,5 +1,3 @@
-use std::fmt::Display;
-
use egui::{
emath::GuiRounding, pos2, vec2, Color32, CornerRadius, FontId, Frame, Label, Layout, Slider,
Stroke,
@@ -7,7 +5,8 @@ use egui::{
use enostr::Pubkey;
use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{
- fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, NotedeckTextStyle,
+ fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, Localization,
+ NotedeckTextStyle,
};
use notedeck_ui::{
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
@@ -20,11 +19,13 @@ pub struct CustomZapView<'a> {
txn: &'a Transaction,
target_pubkey: &'a Pubkey,
default_msats: u64,
+ i18n: &'a mut Localization,
}
#[allow(clippy::new_without_default)]
impl<'a> CustomZapView<'a> {
pub fn new(
+ i18n: &'a mut Localization,
images: &'a mut Images,
ndb: &'a Ndb,
txn: &'a Transaction,
@@ -37,6 +38,7 @@ impl<'a> CustomZapView<'a> {
ndb,
txn,
default_msats,
+ i18n,
}
}
@@ -48,7 +50,7 @@ impl<'a> CustomZapView<'a> {
}
fn ui_internal(&mut self, ui: &mut egui::Ui) -> Option<u64> {
- show_title(ui);
+ show_title(ui, self.i18n);
ui.add_space(16.0);
@@ -82,7 +84,7 @@ impl<'a> CustomZapView<'a> {
} else {
(self.default_msats / 1000).to_string()
};
- show_amount(ui, id, &mut cur_amount, slider_width);
+ show_amount(ui, self.i18n, id, &mut cur_amount, slider_width);
let mut maybe_sats = cur_amount.parse::<u64>().ok();
let prev_slider_sats = maybe_sats.unwrap_or(default_sats).clamp(1, 100000);
@@ -102,7 +104,7 @@ impl<'a> CustomZapView<'a> {
maybe_sats = Some(slider_sats);
}
- if let Some(selection) = show_selection_buttons(ui, maybe_sats) {
+ if let Some(selection) = show_selection_buttons(ui, maybe_sats, self.i18n) {
cur_amount = selection.to_string();
maybe_sats = Some(selection);
}
@@ -110,7 +112,7 @@ impl<'a> CustomZapView<'a> {
ui.data_mut(|d| d.insert_temp(id, cur_amount));
let resp = ui.add(styled_button_toggleable(
- &tr!("Send", "Button label to send a zap"),
+ &tr!(self.i18n, "Send", "Button label to send a zap"),
colors::PINK,
is_valid_zap(maybe_sats),
));
@@ -129,7 +131,7 @@ fn is_valid_zap(amount: Option<u64>) -> bool {
amount.is_some_and(|sats| sats > 0)
}
-fn show_title(ui: &mut egui::Ui) {
+fn show_title(ui: &mut egui::Ui, i18n: &mut Localization) {
let max_size = 32.0;
ui.allocate_ui_with_layout(
vec2(ui.available_width(), max_size),
@@ -158,7 +160,7 @@ fn show_title(ui: &mut egui::Ui) {
ui.add_space(8.0);
ui.add(egui::Label::new(
- egui::RichText::new(tr!("Zap", "Heading for zap (tip) action"))
+ egui::RichText::new(tr!(i18n, "Zap", "Heading for zap (tip) action"))
.text_style(NotedeckTextStyle::Heading2.text_style()),
));
},
@@ -177,7 +179,13 @@ fn show_profile(ui: &mut egui::Ui, images: &mut Images, profile: Option<&Profile
);
}
-fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width: f32) {
+fn show_amount(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ id: egui::Id,
+ user_input: &mut String,
+ width: f32,
+) {
let user_input_font = NotedeckTextStyle::Heading.get_bolded_font(ui.ctx());
let user_input_id = id.with("sats_amount");
@@ -192,6 +200,7 @@ fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width:
let sats_galley = painter.layout_no_wrap(
tr!(
+ i18n,
"SATS",
"Label for satoshis (Bitcoin unit) for custom zap amount input field"
),
@@ -219,7 +228,7 @@ fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width:
.font(user_input_font);
let amount_resp = ui.add(Label::new(
- egui::RichText::new(tr!("Amount", "Label for zap amount input field"))
+ egui::RichText::new(tr!(i18n, "Amount", "Label for zap amount input field"))
.text_style(NotedeckTextStyle::Heading3.text_style())
.color(ui.visuals().noninteractive().text_color()),
));
@@ -300,7 +309,11 @@ const SELECTION_BUTTONS: [ZapSelectionButton; 8] = [
ZapSelectionButton::Eighth,
];
-fn show_selection_buttons(ui: &mut egui::Ui, sats_selection: Option<u64>) -> Option<u64> {
+fn show_selection_buttons(
+ ui: &mut egui::Ui,
+ sats_selection: Option<u64>,
+ i18n: &mut Localization,
+) -> Option<u64> {
let mut our_selection = None;
ui.allocate_ui_with_layout(
vec2(224.0, 116.0),
@@ -309,7 +322,8 @@ fn show_selection_buttons(ui: &mut egui::Ui, sats_selection: Option<u64>) -> Opt
ui.spacing_mut().item_spacing = vec2(8.0, 8.0);
for button in SELECTION_BUTTONS {
- our_selection = our_selection.or(show_selection_button(ui, sats_selection, button));
+ our_selection =
+ our_selection.or(show_selection_button(ui, sats_selection, button, i18n));
}
},
);
@@ -321,6 +335,7 @@ fn show_selection_button(
ui: &mut egui::Ui,
sats_selection: Option<u64>,
button: ZapSelectionButton,
+ i18n: &mut Localization,
) -> Option<u64> {
let (rect, _) = ui.allocate_exact_size(vec2(50.0, 50.0), egui::Sense::click());
let helper = AnimationHelper::new_from_rect(ui, ("zap_selection_button", &button), rect);
@@ -353,7 +368,11 @@ fn show_selection_button(
NotedeckTextStyle::Body.font_family(),
);
- let galley = painter.layout_no_wrap(button.to_string(), fontid, ui.visuals().text_color());
+ let galley = painter.layout_no_wrap(
+ button.to_desc_string(i18n),
+ fontid,
+ ui.visuals().text_color(),
+ );
let text_rect = {
let mut galley_rect = galley.rect;
galley_rect.set_center(rect.center());
@@ -394,19 +413,17 @@ impl ZapSelectionButton {
ZapSelectionButton::Eighth => 100_000,
}
}
-}
-impl Display for ZapSelectionButton {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ pub fn to_desc_string(&self, i18n: &mut Localization) -> String {
match self {
- ZapSelectionButton::First => write!(f, "69"),
- ZapSelectionButton::Second => write!(f, "100"),
- ZapSelectionButton::Third => write!(f, "420"),
- ZapSelectionButton::Fourth => write!(f, "{}", tr!("5K", "Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount.")),
- ZapSelectionButton::Fifth => write!(f, "{}", tr!("10K", "Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount.")),
- ZapSelectionButton::Sixth => write!(f, "{}", tr!("20K", "Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount.")),
- ZapSelectionButton::Seventh => write!(f, "{}", tr!("50K", "Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount.")),
- ZapSelectionButton::Eighth => write!(f, "{}", tr!("100K", "Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.")),
+ ZapSelectionButton::First => "69".to_string(),
+ ZapSelectionButton::Second => "100".to_string(),
+ ZapSelectionButton::Third => "420".to_string(),
+ ZapSelectionButton::Fourth => tr!(i18n, "5K", "Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount."),
+ ZapSelectionButton::Fifth => tr!(i18n, "10K", "Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount."),
+ ZapSelectionButton::Sixth => tr!(i18n, "20K", "Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount."),
+ ZapSelectionButton::Seventh => tr!(i18n, "50K", "Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount."),
+ ZapSelectionButton::Eighth => tr!(i18n, "100K", "Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount."),
}
}
}
diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs
@@ -25,7 +25,9 @@ use notedeck_ui::{
NoteOptions, ProfilePic,
};
-use notedeck::{name::get_display_name, supported_mime_hosted_at_url, tr, NoteAction, NoteContext};
+use notedeck::{
+ name::get_display_name, supported_mime_hosted_at_url, tr, Localization, NoteAction, NoteContext,
+};
use tracing::error;
pub struct PostView<'a, 'd> {
@@ -182,6 +184,7 @@ impl<'a, 'd> PostView<'a, 'd> {
let textedit = TextEdit::multiline(&mut self.draft.buffer)
.hint_text(
egui::RichText::new(tr!(
+ self.note_context.i18n,
"Write a banger note here...",
"Placeholder for note input field"
))
@@ -411,7 +414,10 @@ impl<'a, 'd> PostView<'a, 'd> {
ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| {
let post_button_clicked = ui
- .add_sized([91.0, 32.0], post_button(!self.draft.buffer.is_empty()))
+ .add_sized(
+ [91.0, 32.0],
+ post_button(self.note_context.i18n, !self.draft.buffer.is_empty()),
+ )
.clicked();
let shortcut_pressed = ui.input(|i| {
@@ -609,9 +615,9 @@ fn render_post_view_media(
}
}
-fn post_button(interactive: bool) -> impl egui::Widget {
+fn post_button<'a>(i18n: &'a mut Localization, interactive: bool) -> impl egui::Widget + 'a {
move |ui: &mut egui::Ui| {
- let button = egui::Button::new(tr!("Post now", "Button label to post a note"));
+ let button = egui::Button::new(tr!(i18n, "Post now", "Button label to post a note"));
if interactive {
ui.add(button)
} else {
@@ -804,6 +810,7 @@ mod preview {
unknown_ids: app.unknown_ids,
current_account_has_wallet: false,
clipboard: app.clipboard,
+ i18n: app.i18n,
};
PostView::new(
diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs
@@ -2,17 +2,26 @@ use core::f32;
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
use enostr::ProfileState;
-use notedeck::{profile::unwrap_profile_url, tr, Images, NotedeckTextStyle};
+use notedeck::{profile::unwrap_profile_url, tr, Images, Localization, NotedeckTextStyle};
use notedeck_ui::{profile::banner, ProfilePic};
pub struct EditProfileView<'a> {
state: &'a mut ProfileState,
img_cache: &'a mut Images,
+ i18n: &'a mut Localization,
}
impl<'a> EditProfileView<'a> {
- pub fn new(state: &'a mut ProfileState, img_cache: &'a mut Images) -> Self {
- Self { state, img_cache }
+ pub fn new(
+ i18n: &'a mut Localization,
+ state: &'a mut ProfileState,
+ img_cache: &'a mut Images,
+ ) -> Self {
+ Self {
+ i18n,
+ state,
+ img_cache,
+ }
}
// return true to save
@@ -34,8 +43,12 @@ impl<'a> EditProfileView<'a> {
if ui
.add(
button(
- tr!("Save changes", "Button label to save profile changes")
- .as_str(),
+ tr!(
+ self.i18n,
+ "Save changes",
+ "Button label to save profile changes"
+ )
+ .as_str(),
119.0,
)
.fill(notedeck_ui::colors::PINK),
@@ -70,42 +83,52 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| {
ui.add(label(
- tr!("Display name", "Profile display name field label").as_str(),
+ tr!(
+ self.i18n,
+ "Display name",
+ "Profile display name field label"
+ )
+ .as_str(),
));
ui.add(singleline_textedit(self.state.str_mut("display_name")));
});
in_frame(ui, |ui| {
ui.add(label(
- tr!("Username", "Profile username field label").as_str(),
+ tr!(self.i18n, "Username", "Profile username field label").as_str(),
));
ui.add(singleline_textedit(self.state.str_mut("name")));
});
in_frame(ui, |ui| {
ui.add(label(
- tr!("Profile picture", "Profile picture URL field label").as_str(),
+ tr!(
+ self.i18n,
+ "Profile picture",
+ "Profile picture URL field label"
+ )
+ .as_str(),
));
ui.add(multiline_textedit(self.state.str_mut("picture")));
});
in_frame(ui, |ui| {
ui.add(label(
- tr!("Banner", "Profile banner URL field label").as_str(),
+ tr!(self.i18n, "Banner", "Profile banner URL field label").as_str(),
));
ui.add(multiline_textedit(self.state.str_mut("banner")));
});
in_frame(ui, |ui| {
ui.add(label(
- tr!("About", "Profile about/bio field label").as_str(),
+ tr!(self.i18n, "About", "Profile about/bio field label").as_str(),
));
ui.add(multiline_textedit(self.state.str_mut("about")));
});
in_frame(ui, |ui| {
ui.add(label(
- tr!("Website", "Profile website field label").as_str(),
+ tr!(self.i18n, "Website", "Profile website field label").as_str(),
));
ui.add(singleline_textedit(self.state.str_mut("website")));
});
@@ -113,6 +136,7 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| {
ui.add(label(
tr!(
+ self.i18n,
"Lightning network address (lud16)",
"Bitcoin Lightning network address field label"
)
@@ -124,6 +148,7 @@ impl<'a> EditProfileView<'a> {
in_frame(ui, |ui| {
ui.add(label(
tr!(
+ self.i18n,
"Nostr address (NIP-05 identity)",
"NIP-05 identity field label"
)
@@ -153,12 +178,14 @@ impl<'a> EditProfileView<'a> {
ui.visuals().noninteractive().fg_stroke.color,
RichText::new(if use_domain {
tr!(
+ self.i18n,
"\"{domain}\" will be used for identification",
"Domain identification message",
domain = suffix
)
} else {
tr!(
+ self.i18n,
"\"{username}\" at \"{domain}\" will be used for identification",
"Username and domain identification message",
username = prefix,
diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs
@@ -4,7 +4,7 @@ pub use edit::EditProfileView;
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction};
-use notedeck::tr;
+use notedeck::{tr, Localization};
use notedeck_ui::profile::follow_button;
use tracing::error;
@@ -91,8 +91,12 @@ impl<'a, 'd> ProfileView<'a, 'd> {
)
.get_ptr();
- profile_timeline.selected_view =
- tabs_ui(ui, profile_timeline.selected_view, &profile_timeline.views);
+ profile_timeline.selected_view = tabs_ui(
+ ui,
+ self.note_context.i18n,
+ profile_timeline.selected_view,
+ &profile_timeline.views,
+ );
let reversed = false;
// poll for new notes and insert them into our existing notes
@@ -184,7 +188,10 @@ impl<'a, 'd> ProfileView<'a, 'd> {
match profile_type {
ProfileType::MyProfile => {
- if ui.add(edit_profile_button()).clicked() {
+ if ui
+ .add(edit_profile_button(self.note_context.i18n))
+ .clicked()
+ {
action = Some(ProfileViewAction::EditProfile);
}
}
@@ -334,7 +341,7 @@ fn copy_key_widget(pfp_rect: &egui::Rect) -> impl egui::Widget + '_ {
}
}
-fn edit_profile_button() -> impl egui::Widget + 'static {
+fn edit_profile_button<'a>(i18n: &'a mut Localization) -> impl egui::Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response {
let (rect, resp) = ui.allocate_exact_size(vec2(124.0, 32.0), Sense::click());
let painter = ui.painter_at(rect);
@@ -363,7 +370,7 @@ fn edit_profile_button() -> impl egui::Widget + 'static {
let edit_icon_size = vec2(16.0, 16.0);
let galley = painter.layout(
- tr!("Edit Profile", "Button label to edit user profile"),
+ tr!(i18n, "Edit Profile", "Button label to edit user profile"),
NotedeckTextStyle::Button.get_font_id(ui.ctx()),
ui.visuals().text_color(),
rect.width(),
diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use crate::ui::{Preview, PreviewConfig};
use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2};
use enostr::{RelayPool, RelayStatus};
-use notedeck::{tr, NotedeckTextStyle, RelayAction};
+use notedeck::{tr, Localization, NotedeckTextStyle, RelayAction};
use notedeck_ui::app_images;
use notedeck_ui::{colors::PINK, padding};
use tracing::debug;
@@ -13,6 +13,7 @@ use super::widgets::styled_button;
pub struct RelayView<'a> {
pool: &'a RelayPool,
id_string_map: &'a mut HashMap<Id, String>,
+ i18n: &'a mut Localization,
}
impl RelayView<'_> {
@@ -26,7 +27,7 @@ impl RelayView<'_> {
ui.horizontal(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.label(
- RichText::new(tr!("Relays", "Label for relay list section"))
+ RichText::new(tr!(self.i18n, "Relays", "Label for relay list section"))
.text_style(NotedeckTextStyle::Heading2.text_style()),
);
});
@@ -53,10 +54,15 @@ impl RelayView<'_> {
}
impl<'a> RelayView<'a> {
- pub fn new(pool: &'a RelayPool, id_string_map: &'a mut HashMap<Id, String>) -> Self {
+ pub fn new(
+ pool: &'a RelayPool,
+ id_string_map: &'a mut HashMap<Id, String>,
+ i18n: &'a mut Localization,
+ ) -> Self {
RelayView {
pool,
id_string_map,
+ i18n,
}
}
@@ -65,7 +71,7 @@ impl<'a> RelayView<'a> {
}
/// Show the current relays and return a relay the user selected to delete
- fn show_relays(&'a self, ui: &mut Ui) -> Option<String> {
+ fn show_relays(&mut self, ui: &mut Ui) -> Option<String> {
let mut relay_to_remove = None;
for (index, relay_info) in get_relay_infos(self.pool).iter().enumerate() {
ui.add_space(8.0);
@@ -107,7 +113,7 @@ impl<'a> RelayView<'a> {
relay_to_remove = Some(relay_info.relay_url.to_string());
};
- show_connection_status(ui, relay_info.status);
+ show_connection_status(ui, self.i18n, relay_info.status);
});
});
});
@@ -123,7 +129,7 @@ impl<'a> RelayView<'a> {
match self.id_string_map.get(&id) {
None => {
ui.with_layout(Layout::top_down(Align::Min), |ui| {
- let relay_button = add_relay_button();
+ let relay_button = add_relay_button(self.i18n);
if ui.add(relay_button).clicked() {
debug!("add relay clicked");
self.id_string_map
@@ -151,6 +157,7 @@ impl<'a> RelayView<'a> {
let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text(
RichText::new(tr!(
+ self.i18n,
"Enter the relay here",
"Placeholder for relay input field"
))
@@ -163,7 +170,10 @@ impl<'a> RelayView<'a> {
ui.add(text_edit);
ui.add_space(8.0);
if ui
- .add_sized(egui::vec2(50.0, 40.0), add_relay_button2(is_enabled))
+ .add_sized(
+ egui::vec2(50.0, 40.0),
+ add_relay_button2(self.i18n, is_enabled),
+ )
.clicked()
{
self.id_string_map.remove(&id) // remove and return the value
@@ -175,10 +185,10 @@ impl<'a> RelayView<'a> {
}
}
-fn add_relay_button() -> Button<'static> {
+fn add_relay_button(i18n: &mut Localization) -> Button<'static> {
Button::image_and_text(
app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
- RichText::new(tr!("Add relay", "Button label to add a relay"))
+ RichText::new(tr!(i18n, "Add relay", "Button label to add a relay"))
.size(16.0)
// TODO: this color should not be hard coded. Find some way to add it to the visuals
.color(PINK),
@@ -186,9 +196,9 @@ fn add_relay_button() -> Button<'static> {
.frame(false)
}
-fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static {
+fn add_relay_button2<'a>(i18n: &'a mut Localization, is_enabled: bool) -> impl egui::Widget + 'a {
move |ui: &mut egui::Ui| -> egui::Response {
- let add_text = tr!("Add", "Button label to add a relay");
+ let add_text = tr!(i18n, "Add", "Button label to add a relay");
let button_widget = styled_button(add_text.as_str(), notedeck_ui::colors::PINK);
ui.add_enabled(is_enabled, button_widget)
}
@@ -219,7 +229,7 @@ fn relay_frame(ui: &mut Ui) -> Frame {
.stroke(ui.style().visuals.noninteractive().bg_stroke)
}
-fn show_connection_status(ui: &mut Ui, status: RelayStatus) {
+fn show_connection_status(ui: &mut Ui, i18n: &mut Localization, status: RelayStatus) {
let fg_color = match status {
RelayStatus::Connected => ui.visuals().selection.bg_fill,
RelayStatus::Connecting => ui.visuals().warn_fg_color,
@@ -228,9 +238,11 @@ fn show_connection_status(ui: &mut Ui, status: RelayStatus) {
let bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into();
let label_text = match status {
- RelayStatus::Connected => tr!("Connected", "Status label for connected relay"),
- RelayStatus::Connecting => tr!("Connecting...", "Status label for connecting relay"),
- RelayStatus::Disconnected => tr!("Not Connected", "Status label for disconnected relay"),
+ RelayStatus::Connected => tr!(i18n, "Connected", "Status label for connected relay"),
+ RelayStatus::Connecting => tr!(i18n, "Connecting...", "Status label for connecting relay"),
+ RelayStatus::Disconnected => {
+ tr!(i18n, "Not Connected", "Status label for disconnected relay")
+ }
};
let frame = Frame::new()
@@ -290,7 +302,7 @@ mod preview {
fn update(&mut self, app: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option<AppAction> {
self.pool.try_recv();
let mut id_string_map = HashMap::new();
- RelayView::new(app.pool, &mut id_string_map).ui(ui);
+ RelayView::new(app.pool, &mut id_string_map, app.i18n).ui(ui);
None
}
}
diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs
@@ -5,7 +5,7 @@ use state::TypingType;
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
use egui_winit::clipboard::Clipboard;
use nostrdb::{Filter, Ndb, Transaction};
-use notedeck::{tr, tr_plural, NoteAction, NoteContext, NoteRef};
+use notedeck::{tr, tr_plural, Localization, NoteAction, NoteContext, NoteRef};
use notedeck_ui::{
context_menu::{input_context, PasteBehavior},
icons::search_icon,
@@ -54,6 +54,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
ui.spacing_mut().item_spacing = egui::vec2(0.0, 12.0);
let search_resp = search_box(
+ self.note_context.i18n,
&mut self.query.string,
self.query.focus_state.clone(),
ui,
@@ -120,6 +121,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
}
SearchState::Searched => {
ui.label(tr_plural!(
+ self.note_context.i18n,
"Got {count} result for '{query}'", // one
"Got {count} results for '{query}'", // other
"Search results count", // comment
@@ -130,6 +132,7 @@ impl<'a, 'd> SearchView<'a, 'd> {
}
SearchState::Typing(TypingType::AutoSearch) => {
ui.label(tr!(
+ self.note_context.i18n,
"Searching for '{query}'",
"Search in progress message",
query = &self.query.string
@@ -247,6 +250,7 @@ impl SearchResponse {
}
fn search_box(
+ i18n: &mut Localization,
input: &mut String,
focus_state: FocusState,
ui: &mut egui::Ui,
@@ -290,6 +294,7 @@ fn search_box(
TextEdit::singleline(input)
.hint_text(
RichText::new(tr!(
+ i18n,
"Search notes...",
"Placeholder for search notes input field"
))
diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs
@@ -12,7 +12,7 @@ use crate::{
route::Route,
};
-use notedeck::{tr, Accounts, UserAccount};
+use notedeck::{tr, Accounts, Localization, UserAccount};
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
app_images, colors, View,
@@ -26,6 +26,7 @@ static ICON_WIDTH: f32 = 40.0;
pub struct DesktopSidePanel<'a> {
selected_account: &'a UserAccount,
decks_cache: &'a DecksCache,
+ i18n: &'a mut Localization,
}
impl View for DesktopSidePanel<'_> {
@@ -58,10 +59,15 @@ impl SidePanelResponse {
}
impl<'a> DesktopSidePanel<'a> {
- pub fn new(selected_account: &'a UserAccount, decks_cache: &'a DecksCache) -> Self {
+ pub fn new(
+ selected_account: &'a UserAccount,
+ decks_cache: &'a DecksCache,
+ i18n: &'a mut Localization,
+ ) -> Self {
Self {
selected_account,
decks_cache,
+ i18n,
}
}
@@ -105,9 +111,13 @@ impl<'a> DesktopSidePanel<'a> {
ui.add_space(8.0);
ui.add(egui::Label::new(
- RichText::new(tr!("DECKS", "Label for decks section in side panel"))
- .size(11.0)
- .color(ui.visuals().noninteractive().fg_stroke.color),
+ RichText::new(tr!(
+ self.i18n,
+ "DECKS",
+ "Label for decks section in side panel"
+ ))
+ .size(11.0)
+ .color(ui.visuals().noninteractive().fg_stroke.color),
));
ui.add_space(8.0);
let add_deck_resp = ui.add(add_deck_button());
@@ -175,8 +185,9 @@ impl<'a> DesktopSidePanel<'a> {
decks_cache: &mut DecksCache,
accounts: &Accounts,
action: SidePanelAction,
+ i18n: &mut Localization,
) -> Option<SwitchingAction> {
- let router = get_active_columns_mut(accounts, decks_cache).get_first_router();
+ let router = get_active_columns_mut(i18n, accounts, decks_cache).get_first_router();
let mut switching_response = None;
match action {
/*
@@ -218,7 +229,7 @@ impl<'a> DesktopSidePanel<'a> {
{
router.go_back();
} else {
- get_active_columns_mut(accounts, decks_cache).new_column_picker();
+ get_active_columns_mut(i18n, accounts, decks_cache).new_column_picker();
}
}
SidePanelAction::ComposeNote => {
@@ -263,7 +274,7 @@ impl<'a> DesktopSidePanel<'a> {
switching_response = Some(crate::nav::SwitchingAction::Decks(
DecksAction::Switch(index),
));
- if let Some(edit_deck) = get_decks_mut(accounts, decks_cache)
+ if let Some(edit_deck) = get_decks_mut(i18n, accounts, decks_cache)
.decks_mut()
.get_mut(index)
{
diff --git a/crates/notedeck_columns/src/ui/support.rs b/crates/notedeck_columns/src/ui/support.rs
@@ -1,5 +1,5 @@
use egui::{vec2, Button, Label, Layout, RichText};
-use notedeck::{tr, NamedFontFamily, NotedeckTextStyle};
+use notedeck::{tr, Localization, NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{colors::PINK, padding};
use tracing::error;
@@ -7,11 +7,12 @@ use crate::support::Support;
pub struct SupportView<'a> {
support: &'a mut Support,
+ i18n: &'a mut Localization,
}
impl<'a> SupportView<'a> {
- pub fn new(support: &'a mut Support) -> Self {
- Self { support }
+ pub fn new(support: &'a mut Support, i18n: &'a mut Localization) -> Self {
+ Self { support, i18n }
}
pub fn show(&mut self, ui: &mut egui::Ui) {
@@ -22,14 +23,24 @@ impl<'a> SupportView<'a> {
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
);
ui.add(Label::new(
- RichText::new(tr!("Running into a bug?", "Heading for support section")).font(font),
+ RichText::new(tr!(
+ self.i18n,
+ "Running into a bug?",
+ "Heading for support section"
+ ))
+ .font(font),
));
ui.label(
- RichText::new(tr!("Step 1", "Step 1 label in support instructions"))
- .text_style(NotedeckTextStyle::Heading3.text_style()),
+ RichText::new(tr!(
+ self.i18n,
+ "Step 1",
+ "Step 1 label in support instructions"
+ ))
+ .text_style(NotedeckTextStyle::Heading3.text_style()),
);
padding(8.0, ui, |ui| {
ui.label(tr!(
+ self.i18n,
"Open your default email client to get help from the Damus team",
"Instruction to open email client"
));
@@ -37,7 +48,7 @@ impl<'a> SupportView<'a> {
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
let font_size =
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
- let button_resp = ui.add(open_email_button(font_size, size));
+ let button_resp = ui.add(open_email_button(self.i18n, font_size, size));
if button_resp.clicked() {
if let Err(e) = open::that(self.support.get_mailto_url()) {
error!(
@@ -55,19 +66,23 @@ impl<'a> SupportView<'a> {
if let Some(logs) = self.support.get_most_recent_log() {
ui.label(
- RichText::new(tr!("Step 2", "Step 2 label in support instructions"))
- .text_style(NotedeckTextStyle::Heading3.text_style()),
+ RichText::new(tr!(
+ self.i18n,
+ "Step 2",
+ "Step 2 label in support instructions"
+ ))
+ .text_style(NotedeckTextStyle::Heading3.text_style()),
);
let size = vec2(80.0, 40.0);
let copy_button = Button::new(
- RichText::new(tr!("Copy", "Button label to copy logs")).size(
+ RichText::new(tr!(self.i18n, "Copy", "Button label to copy logs")).size(
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
),
)
.fill(PINK)
.min_size(size);
padding(8.0, ui, |ui| {
- ui.add(Label::new(RichText::new(tr!("Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.", "Instruction for copying logs"))).wrap());
+ ui.add(Label::new(RichText::new(tr!(self.i18n,"Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.", "Instruction for copying logs"))).wrap());
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
if ui.add(copy_button).clicked() {
ui.ctx().copy_text(logs.to_string());
@@ -86,9 +101,13 @@ impl<'a> SupportView<'a> {
}
}
-fn open_email_button(font_size: f32, size: egui::Vec2) -> impl egui::Widget {
+fn open_email_button(
+ i18n: &mut Localization,
+ font_size: f32,
+ size: egui::Vec2,
+) -> impl egui::Widget {
Button::new(
- RichText::new(tr!("Open Email", "Button label to open email client")).size(font_size),
+ RichText::new(tr!(i18n, "Open Email", "Button label to open email client")).size(font_size),
)
.fill(PINK)
.min_size(size)
diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs
@@ -8,7 +8,9 @@ use std::f32::consts::PI;
use tracing::{error, warn};
use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter};
-use notedeck::{note::root_note_id_from_selected_id, tr, NoteAction, NoteContext, ScrollInfo};
+use notedeck::{
+ note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo,
+};
use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
NoteOptions, NoteView,
@@ -103,7 +105,12 @@ fn timeline_ui(
return None;
};
- timeline.selected_view = tabs_ui(ui, timeline.selected_view, &timeline.views);
+ timeline.selected_view = tabs_ui(
+ ui,
+ note_context.i18n,
+ timeline.selected_view,
+ &timeline.views,
+ );
// need this for some reason??
ui.add_space(3.0);
@@ -263,7 +270,12 @@ fn goto_top_button(center: Pos2) -> impl egui::Widget {
}
}
-pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usize {
+pub fn tabs_ui(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ selected: usize,
+ views: &[TimelineTab],
+) -> usize {
ui.spacing_mut().item_spacing.y = 0.0;
let tab_res = egui_tabs::Tabs::new(views.len() as i32)
@@ -281,9 +293,13 @@ pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usi
let ind = state.index();
let txt = match views[ind as usize].filter {
- ViewFilter::Notes => tr!("Notes", "Label for notes-only filter"),
+ ViewFilter::Notes => tr!(i18n, "Notes", "Label for notes-only filter"),
ViewFilter::NotesAndReplies => {
- tr!("Notes & Replies", "Label for notes and replies filter")
+ tr!(
+ i18n,
+ "Notes & Replies",
+ "Label for notes and replies filter"
+ )
}
};
diff --git a/crates/notedeck_columns/src/ui/wallet.rs b/crates/notedeck_columns/src/ui/wallet.rs
@@ -1,7 +1,7 @@
use egui::{vec2, CornerRadius, Layout};
use notedeck::{
- get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle,
- PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
+ get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, Localization,
+ NotedeckTextStyle, PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
};
use crate::{nav::RouterAction, route::Route};
@@ -153,11 +153,12 @@ impl WalletAction {
pub struct WalletView<'a> {
state: WalletState<'a>,
+ i18n: &'a mut Localization,
}
impl<'a> WalletView<'a> {
- pub fn new(state: WalletState<'a>) -> Self {
- Self { state }
+ pub fn new(state: WalletState<'a>, i18n: &'a mut Localization) -> Self {
+ Self { state, i18n }
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<WalletAction> {
@@ -173,11 +174,17 @@ impl<'a> WalletView<'a> {
wallet,
default_zap_state,
can_create_local_wallet,
- } => show_with_wallet(ui, wallet, default_zap_state, *can_create_local_wallet),
+ } => show_with_wallet(
+ ui,
+ self.i18n,
+ wallet,
+ default_zap_state,
+ *can_create_local_wallet,
+ ),
WalletState::NoWallet {
state,
show_local_only,
- } => show_no_wallet(ui, state, *show_local_only),
+ } => show_no_wallet(ui, self.i18n, state, *show_local_only),
}
}
}
@@ -196,6 +203,7 @@ fn try_create_wallet(state: &mut WalletUIState) -> Option<Wallet> {
fn show_no_wallet(
ui: &mut egui::Ui,
+ i18n: &mut Localization,
state: &mut WalletUIState,
show_local_only: bool,
) -> Option<WalletAction> {
@@ -203,6 +211,7 @@ fn show_no_wallet(
let text_edit = egui::TextEdit::singleline(&mut state.buf)
.hint_text(
egui::RichText::new(tr!(
+ i18n,
"Paste your NWC URI here...",
"Placeholder text for NWC URI input"
))
@@ -222,10 +231,12 @@ fn show_no_wallet(
let error_str = match error_msg {
WalletError::InvalidURI => tr!(
+ i18n,
"Invalid NWC URI",
"Error message for invalid Nostr Wallet Connect URI"
),
WalletError::NoWallet => tr!(
+ i18n,
"Add a wallet to continue",
"Error message for missing wallet"
),
@@ -239,6 +250,7 @@ fn show_no_wallet(
ui.checkbox(
&mut state.for_local_only,
tr!(
+ i18n,
"Use this wallet for the current account only",
"Checkbox label for using wallet only for current account"
),
@@ -248,7 +260,7 @@ fn show_no_wallet(
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
ui.add(styled_button(
- tr!("Add Wallet", "Button label to add a wallet").as_str(),
+ tr!(i18n, "Add Wallet", "Button label to add a wallet").as_str(),
notedeck_ui::colors::PINK,
))
.clicked()
@@ -259,6 +271,7 @@ fn show_no_wallet(
fn show_with_wallet(
ui: &mut egui::Ui,
+ i18n: &mut Localization,
wallet: &mut Wallet,
default_zap_state: &mut DefaultZapState,
can_create_local_wallet: bool,
@@ -279,12 +292,12 @@ fn show_with_wallet(
}
});
- let mut action = show_default_zap(ui, default_zap_state);
+ let mut action = show_default_zap(ui, i18n, default_zap_state);
ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: {
if ui
.add(styled_button(
- tr!("Delete Wallet", "Button label to delete a wallet").as_str(),
+ tr!(i18n, "Delete Wallet", "Button label to delete a wallet").as_str(),
ui.visuals().window_fill,
))
.clicked()
@@ -299,6 +312,7 @@ fn show_with_wallet(
.checkbox(
&mut false,
tr!(
+ i18n,
"Add a different wallet that will only be used for this account",
"Button label to add a different wallet"
),
@@ -323,13 +337,17 @@ fn show_balance(ui: &mut egui::Ui, msats: u64) -> egui::Response {
.inner
}
-fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<WalletAction> {
+fn show_default_zap(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ state: &mut DefaultZapState,
+) -> Option<WalletAction> {
let mut action = None;
ui.allocate_ui_with_layout(
vec2(ui.available_width(), 50.0),
egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|ui| {
- ui.label(tr!("Default amount per zap: ", "Label for default zap amount input"));
+ ui.label(tr!(i18n, "Default amount per zap: ", "Label for default zap amount input"));
match state {
DefaultZapState::Pending(pending_default_zap_state) => {
let text = &mut pending_default_zap_state.amount_sats;
@@ -361,27 +379,27 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
ui.memory_mut(|m| m.request_focus(id));
- ui.label(tr!("sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
+ ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
if ui
- .add(styled_button(tr!("Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill))
+ .add(styled_button(tr!(i18n, "Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill))
.clicked()
{
action = Some(WalletAction::SetDefaultZapSats(text.to_string()));
}
}
DefaultZapState::Valid(msats) => {
- if let Some(wallet_action) = show_valid_msats(ui, **msats) {
+ if let Some(wallet_action) = show_valid_msats(ui, i18n, **msats) {
action = Some(wallet_action);
}
- ui.label(tr!("sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
+ ui.label(tr!(i18n, "sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
}
}
if let DefaultZapState::Pending(pending) = state {
if let Some(error_message) = &pending.error_message {
let msg_str = match error_message {
- notedeck::DefaultZapError::InvalidUserInput => tr!("Invalid amount", "Error message for invalid zap amount"),
+ notedeck::DefaultZapError::InvalidUserInput => tr!(i18n, "Invalid amount", "Error message for invalid zap amount"),
};
ui.colored_label(ui.visuals().warn_fg_color, msg_str);
@@ -393,7 +411,11 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
action
}
-fn show_valid_msats(ui: &mut egui::Ui, msats: u64) -> Option<WalletAction> {
+fn show_valid_msats(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ msats: u64,
+) -> Option<WalletAction> {
let galley = {
let painter = ui.painter();
@@ -409,7 +431,11 @@ fn show_valid_msats(ui: &mut egui::Ui, msats: u64) -> Option<WalletAction> {
let resp = resp
.on_hover_cursor(egui::CursorIcon::PointingHand)
- .on_hover_text_at_pointer(tr!("Click to edit", "Hover text for editable zap amount"));
+ .on_hover_text_at_pointer(tr!(
+ i18n,
+ "Click to edit",
+ "Hover text for editable zap amount"
+ ));
let painter = ui.painter_at(resp.rect);
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -4,7 +4,7 @@ use crate::{
};
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
use nostrdb::{Ndb, Transaction};
-use notedeck::{tr, Accounts, AppContext, Images, NoteAction, NoteContext};
+use notedeck::{tr, Accounts, AppContext, Images, Localization, NoteAction, NoteContext};
use notedeck_ui::{app_images, icons::search_icon, jobs::JobsCache, NoteOptions, ProfilePic};
/// DaveUi holds all of the data it needs to render itself
@@ -107,7 +107,7 @@ impl<'a> DaveUi<'a> {
.inner_margin(egui::Margin::same(8))
.fill(ui.visuals().extreme_bg_color)
.corner_radius(12.0)
- .show(ui, |ui| self.inputbox(ui))
+ .show(ui, |ui| self.inputbox(app_ctx.i18n, ui))
.inner;
let note_action = egui::ScrollArea::vertical()
@@ -134,11 +134,11 @@ impl<'a> DaveUi<'a> {
.or(DaveResponse { action })
}
- fn error_chat(&self, err: &str, ui: &mut egui::Ui) {
+ fn error_chat(&self, i18n: &mut Localization, err: &str, ui: &mut egui::Ui) {
if self.trial {
ui.add(egui::Label::new(
egui::RichText::new(
- tr!("The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!", "Message shown when Dave trial period has ended"),
+ tr!(i18n, "The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!", "Message shown when Dave trial period has ended"),
)
.weak(),
));
@@ -160,7 +160,7 @@ impl<'a> DaveUi<'a> {
for message in self.chat {
let r = match message {
Message::Error(err) => {
- self.error_chat(err, ui);
+ self.error_chat(ctx.i18n, err, ui);
None
}
Message::User(msg) => {
@@ -220,6 +220,7 @@ impl<'a> DaveUi<'a> {
unknown_ids: ctx.unknown_ids,
clipboard: ctx.clipboard,
current_account_has_wallet: false,
+ i18n: ctx.i18n,
};
let txn = Transaction::new(note_context.ndb).unwrap();
@@ -303,13 +304,14 @@ impl<'a> DaveUi<'a> {
note_action
}
- fn inputbox(&mut self, ui: &mut egui::Ui) -> DaveResponse {
+ fn inputbox(&mut self, i18n: &mut Localization, ui: &mut egui::Ui) -> DaveResponse {
//ui.add_space(Self::chat_margin(ui.ctx()) as f32);
ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
let mut dave_response = DaveResponse::none();
if ui
.add(egui::Button::new(tr!(
+ i18n,
"Ask",
"Button to send message to Dave AI assistant"
)))
@@ -330,6 +332,7 @@ impl<'a> DaveUi<'a> {
))
.hint_text(
egui::RichText::new(tr!(
+ i18n,
"Ask dave anything...",
"Placeholder text for Dave AI input field"
))
diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs
@@ -323,6 +323,7 @@ pub fn render_note_contents(
&supported_medias,
carousel_id,
trusted_media,
+ note_context.i18n,
);
ui.add_space(2.0);
}
diff --git a/crates/notedeck_ui/src/note/context.rs b/crates/notedeck_ui/src/note/context.rs
@@ -1,6 +1,6 @@
use egui::{Rect, Vec2};
use nostrdb::NoteKey;
-use notedeck::{tr, BroadcastContext, NoteContextSelection};
+use notedeck::{tr, BroadcastContext, Localization, NoteContextSelection};
pub struct NoteContextButton {
put_at: Option<Rect>,
@@ -105,24 +105,17 @@ impl NoteContextButton {
#[profiling::function]
pub fn menu(
ui: &mut egui::Ui,
+ i18n: &mut Localization,
button_response: egui::Response,
) -> Option<NoteContextSelection> {
let mut context_selection: Option<NoteContextSelection> = 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);
// Debug: Check what the tr! macro returns
let copy_text = tr!(
+ i18n,
"Copy Text",
"Copy the text content of the note to clipboard"
);
@@ -134,6 +127,7 @@ impl NoteContextButton {
}
if ui
.button(tr!(
+ i18n,
"Copy Pubkey",
"Copy the author's public key to clipboard"
))
@@ -144,6 +138,7 @@ impl NoteContextButton {
}
if ui
.button(tr!(
+ i18n,
"Copy Note ID",
"Copy the unique note identifier to clipboard"
))
@@ -154,6 +149,7 @@ impl NoteContextButton {
}
if ui
.button(tr!(
+ i18n,
"Copy Note JSON",
"Copy the raw note data in JSON format to clipboard"
))
@@ -164,6 +160,7 @@ impl NoteContextButton {
}
if ui
.button(tr!(
+ i18n,
"Broadcast",
"Broadcast the note to all connected relays"
))
@@ -176,6 +173,7 @@ impl NoteContextButton {
}
if ui
.button(tr!(
+ i18n,
"Broadcast Local",
"Broadcast the note only to local network relays"
))
diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs
@@ -6,8 +6,8 @@ use egui::{
};
use notedeck::{
fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url,
- tr, GifState, GifStateMap, Images, JobPool, MediaCache, MediaCacheType, NotedeckTextStyle,
- TexturedImage, TexturesCache, UrlMimes,
+ tr, GifState, GifStateMap, Images, JobPool, Localization, MediaCache, MediaCacheType,
+ NotedeckTextStyle, TexturedImage, TexturesCache, UrlMimes,
};
use crate::{
@@ -20,6 +20,7 @@ use crate::{
AnimationHelper, PulseAlpha,
};
+#[allow(clippy::too_many_arguments)]
pub(crate) fn image_carousel(
ui: &mut egui::Ui,
img_cache: &mut Images,
@@ -28,6 +29,7 @@ pub(crate) fn image_carousel(
medias: &[RenderableMedia],
carousel_id: egui::Id,
trusted_media: bool,
+ i18n: &mut Localization,
) -> Option<MediaAction> {
// let's make sure everything is within our area
@@ -69,9 +71,14 @@ pub(crate) fn image_carousel(
blur_type.clone(),
);
- if let Some(cur_action) =
- render_media(ui, &mut img_cache.gif_states, media_state, url, height)
- {
+ if let Some(cur_action) = render_media(
+ ui,
+ &mut img_cache.gif_states,
+ media_state,
+ url,
+ height,
+ i18n,
+ ) {
// clicked the media, lets set the active index
if let MediaUIAction::Clicked = cur_action {
set_show_popup(ui, popup_id(carousel_id), true);
@@ -100,7 +107,14 @@ pub(crate) fn image_carousel(
let current_image_index = update_selected_image_index(ui, carousel_id, medias.len() as i32);
- show_full_screen_media(ui, medias, current_image_index, img_cache, carousel_id);
+ show_full_screen_media(
+ ui,
+ medias,
+ current_image_index,
+ img_cache,
+ carousel_id,
+ i18n,
+ );
}
action
}
@@ -163,6 +177,7 @@ fn show_full_screen_media(
index: usize,
img_cache: &mut Images,
carousel_id: egui::Id,
+ i18n: &mut Localization,
) {
Window::new("image_popup")
.title_bar(false)
@@ -201,6 +216,7 @@ fn show_full_screen_media(
cur_state.gifs,
image_url,
carousel_id,
+ i18n,
);
})
});
@@ -363,6 +379,7 @@ fn select_next_media(
next as usize
}
+#[allow(clippy::too_many_arguments)]
fn render_full_screen_media(
ui: &mut egui::Ui,
num_urls: usize,
@@ -371,6 +388,7 @@ fn render_full_screen_media(
gifs: &mut HashMap<String, GifState>,
image_url: &str,
carousel_id: egui::Id,
+ i18n: &mut Localization,
) {
const TOP_BAR_HEIGHT: f32 = 30.0;
const BOTTOM_BAR_HEIGHT: f32 = 60.0;
@@ -631,13 +649,17 @@ fn render_full_screen_media(
});
}
- copy_link(image_url, &response);
+ copy_link(i18n, image_url, &response);
}
-fn copy_link(url: &str, img_resp: &Response) {
+fn copy_link(i18n: &mut Localization, url: &str, img_resp: &Response) {
img_resp.context_menu(|ui| {
if ui
- .button(tr!("Copy Link", "Button to copy media link to clipboard"))
+ .button(tr!(
+ i18n,
+ "Copy Link",
+ "Button to copy media link to clipboard"
+ ))
.clicked()
{
ui.ctx().copy_text(url.to_owned());
@@ -653,10 +675,11 @@ fn render_media(
render_state: MediaRenderState,
url: &str,
height: f32,
+ i18n: &mut Localization,
) -> Option<MediaUIAction> {
match render_state {
MediaRenderState::ActualImage(image) => {
- if render_success_media(ui, url, image, gifs, height).clicked() {
+ if render_success_media(ui, url, image, gifs, height, i18n).clicked() {
Some(MediaUIAction::Clicked)
} else {
None
@@ -695,9 +718,9 @@ fn render_media(
let resp = match obfuscated_texture {
ObfuscatedTexture::Blur(texture_handle) => {
let resp = ui.add(texture_to_image(texture_handle, height));
- render_blur_text(ui, url, resp.rect)
+ render_blur_text(ui, i18n, url, resp.rect)
}
- ObfuscatedTexture::Default => render_default_blur(ui, height, url),
+ ObfuscatedTexture::Default => render_default_blur(ui, i18n, height, url),
};
if resp
@@ -712,7 +735,12 @@ fn render_media(
}
}
-fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> egui::Response {
+fn render_blur_text(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ url: &str,
+ render_rect: egui::Rect,
+) -> egui::Response {
let helper = AnimationHelper::new_from_rect(ui, ("show_media", url), render_rect);
let painter = ui.painter_at(helper.get_animation_rect());
@@ -726,6 +754,7 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
);
let info_galley = painter.layout(
tr!(
+ i18n,
"Media from someone you don't follow",
"Text shown on blurred media from unfollowed users"
)
@@ -736,7 +765,7 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
);
let load_galley = painter.layout_no_wrap(
- tr!("Tap to Load", "Button text to load blurred media").to_owned(),
+ tr!(i18n, "Tap to Load", "Button text to load blurred media"),
animation_fontid,
egui::Color32::BLACK,
// ui.visuals().widgets.inactive.bg_fill,
@@ -792,9 +821,14 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
helper.take_animation_response()
}
-fn render_default_blur(ui: &mut egui::Ui, height: f32, url: &str) -> egui::Response {
+fn render_default_blur(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ height: f32,
+ url: &str,
+) -> egui::Response {
let rect = render_default_blur_bg(ui, height, url, false);
- render_blur_text(ui, url, rect)
+ render_blur_text(ui, i18n, url, rect)
}
fn render_default_blur_bg(ui: &mut egui::Ui, height: f32, url: &str, shimmer: bool) -> egui::Rect {
@@ -883,12 +917,13 @@ fn render_success_media(
tex: &mut TexturedImage,
gifs: &mut GifStateMap,
height: f32,
+ i18n: &mut Localization,
) -> Response {
let texture = handle_repaint(ui, retrieve_latest_texture(url, gifs, tex));
let img = texture_to_image(texture, height);
let img_resp = ui.add(Button::image(img).frame(false));
- copy_link(url, &img_resp);
+ copy_link(i18n, url, &img_resp);
img_resp
}
diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs
@@ -17,6 +17,7 @@ use notedeck::note::MediaAction;
use notedeck::note::ZapTargetAmount;
use notedeck::ui::is_narrow;
use notedeck::Images;
+use notedeck::Localization;
pub use options::NoteOptions;
pub use reply_description::reply_desc;
@@ -27,8 +28,8 @@ use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{
name::get_display_name,
note::{NoteAction, NoteContext, ZapAction},
- tr, AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned,
- NotedeckTextStyle, ZapTarget, Zaps,
+ tr, AnyZapState, ContextSelection, NoteZapTarget, NoteZapTargetOwned, NotedeckTextStyle,
+ ZapTarget, Zaps,
};
pub struct NoteView<'a, 'd> {
@@ -194,7 +195,6 @@ impl<'a, 'd> NoteView<'a, 'd> {
}
fn textmode_ui(&mut self, ui: &mut egui::Ui) -> egui::Response {
- let note_key = self.note.key().expect("todo: implement non-db notes");
let txn = self.note.txn().expect("todo: implement non-db notes");
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
@@ -206,23 +206,22 @@ impl<'a, 'd> NoteView<'a, 'd> {
//ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
- let cached_note = self
- .note_context
- .note_cache
- .cached_note_or_insert_mut(note_key, self.note);
-
let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| {
- render_reltime(ui, cached_note, false).response
+ render_reltime(ui, self.note_context.i18n, self.note.created_at(), false).response
});
let (_id, rect) = ui.allocate_space(egui::vec2(150.0, 20.0));
ui.allocate_rect(rect, Sense::hover());
ui.put(rect, |ui: &mut egui::Ui| {
ui.add(
- Username::new(profile.as_ref().ok(), self.note.pubkey())
- .abbreviated(6)
- .pk_colored(true),
+ Username::new(
+ self.note_context.i18n,
+ profile.as_ref().ok(),
+ self.note.pubkey(),
+ )
+ .abbreviated(6)
+ .pk_colored(true),
)
});
@@ -308,9 +307,13 @@ impl<'a, 'd> NoteView<'a, 'd> {
let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add_space(4.0);
ui.label(
- RichText::new(tr!("Reposted", "Label for reposted notes"))
- .color(color)
- .text_style(style.text_style()),
+ RichText::new(tr!(
+ self.note_context.i18n,
+ "Reposted",
+ "Label for reposted notes"
+ ))
+ .color(color)
+ .text_style(style.text_style()),
);
});
NoteView::new(self.note_context, ¬e_to_repost, self.flags, self.jobs).show(ui)
@@ -348,20 +351,17 @@ impl<'a, 'd> NoteView<'a, 'd> {
#[profiling::function]
fn note_header(
ui: &mut egui::Ui,
- note_cache: &mut NoteCache,
+ i18n: &mut Localization,
note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
show_unread_indicator: bool,
) {
- let note_key = note.key().unwrap();
-
let horiz_resp = ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = if is_narrow(ui.ctx()) { 1.0 } else { 2.0 };
- ui.add(Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20));
+ ui.add(Username::new(i18n, profile.as_ref().ok(), note.pubkey()).abbreviated(20));
- let cached_note = note_cache.cached_note_or_insert_mut(note_key, note);
- render_reltime(ui, cached_note, true);
+ render_reltime(ui, i18n, note.created_at(), true);
})
.response;
@@ -405,7 +405,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
ui.horizontal_centered(|ui| {
NoteView::note_header(
ui,
- self.note_context.note_cache,
+ self.note_context.i18n,
self.note,
profile,
self.show_unread_indicator,
@@ -460,10 +460,16 @@ impl<'a, 'd> NoteView<'a, 'd> {
cur_acc: cur_acc.keypair(),
})
};
- note_action =
- render_note_actionbar(ui, zapper, self.note.id(), self.note.pubkey(), note_key)
- .inner
- .or(note_action);
+ note_action = render_note_actionbar(
+ ui,
+ zapper,
+ self.note.id(),
+ self.note.pubkey(),
+ note_key,
+ self.note_context.i18n,
+ )
+ .inner
+ .or(note_action);
}
NoteUiResponse {
@@ -489,7 +495,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
NoteView::note_header(
ui,
- self.note_context.note_cache,
+ self.note_context.i18n,
self.note,
profile,
self.show_unread_indicator,
@@ -542,6 +548,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
self.note.id(),
self.note.pubkey(),
note_key,
+ self.note_context.i18n,
)
.inner
.or(note_action);
@@ -588,7 +595,8 @@ impl<'a, 'd> NoteView<'a, 'd> {
};
let resp = ui.add(NoteContextButton::new(note_key).place_at(context_pos));
- if let Some(action) = NoteContextButton::menu(ui, resp.clone()) {
+ if let Some(action) = NoteContextButton::menu(ui, self.note_context.i18n, resp.clone())
+ {
note_action = Some(NoteAction::Context(ContextSelection { note_key, action }));
}
}
@@ -765,11 +773,13 @@ fn render_note_actionbar(
note_id: &[u8; 32],
note_pubkey: &[u8; 32],
note_key: NoteKey,
+ i18n: &mut Localization,
) -> egui::InnerResponse<Option<NoteAction>> {
ui.horizontal(|ui| 's: {
- let reply_resp = reply_button(ui, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
+ let reply_resp =
+ reply_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
let quote_resp =
- quote_repost_button(ui, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
+ quote_repost_button(ui, i18n, note_key).on_hover_cursor(egui::CursorIcon::PointingHand);
let to_noteid = |id: &[u8; 32]| NoteId::new(*id);
if reply_resp.clicked() {
@@ -804,7 +814,7 @@ fn render_note_actionbar(
cur_acc.secret_key.as_ref()?;
match zap_state {
- Ok(any_zap_state) => ui.add(zap_button(any_zap_state, note_id)),
+ Ok(any_zap_state) => ui.add(zap_button(i18n, any_zap_state, note_id)),
Err(err) => {
let (rect, _) =
ui.allocate_at_least(egui::vec2(10.0, 10.0), egui::Sense::click());
@@ -832,7 +842,8 @@ fn render_note_actionbar(
#[profiling::function]
fn render_reltime(
ui: &mut egui::Ui,
- note_cache: &mut CachedNote,
+ i18n: &mut Localization,
+ created_at: u64,
before: bool,
) -> egui::InnerResponse<()> {
ui.horizontal(|ui| {
@@ -840,7 +851,7 @@ fn render_reltime(
secondary_label(ui, "⋅");
}
- secondary_label(ui, note_cache.reltime_str_mut());
+ secondary_label(ui, notedeck::time_ago_since(i18n, created_at));
if !before {
secondary_label(ui, "⋅");
@@ -848,7 +859,7 @@ fn render_reltime(
})
}
-fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
+fn reply_button(ui: &mut egui::Ui, i18n: &mut Localization, note_key: NoteKey) -> egui::Response {
let img = if ui.style().visuals.dark_mode {
app_images::reply_dark_image()
} else {
@@ -862,9 +873,11 @@ fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
let expand_size = 5.0; // from hover_expand_small
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
- let put_resp = ui
- .put(rect, img.max_width(size))
- .on_hover_text(tr!("Reply to this note", "Hover text for reply button"));
+ let put_resp = ui.put(rect, img.max_width(size)).on_hover_text(tr!(
+ i18n,
+ "Reply to this note",
+ "Hover text for reply button"
+ ));
resp.union(put_resp)
}
@@ -877,7 +890,11 @@ fn repost_icon(dark_mode: bool) -> egui::Image<'static> {
}
}
-fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
+fn quote_repost_button(
+ ui: &mut egui::Ui,
+ i18n: &mut Localization,
+ note_key: NoteKey,
+) -> egui::Response {
let size = 14.0;
let expand_size = 5.0;
let anim_speed = 0.05;
@@ -889,12 +906,20 @@ fn quote_repost_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
let put_resp = ui
.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size))
- .on_hover_text(tr!("Repost this note", "Hover text for repost button"));
+ .on_hover_text(tr!(
+ i18n,
+ "Repost this note",
+ "Hover text for repost button"
+ ));
resp.union(put_resp)
}
-fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<'_> {
+fn zap_button<'a>(
+ i18n: &'a mut Localization,
+ state: AnyZapState,
+ noteid: &'a [u8; 32],
+) -> impl egui::Widget + use<'a> {
move |ui: &mut egui::Ui| -> egui::Response {
let (rect, size, resp) = crate::anim::hover_expand_small(ui, ui.id().with("zap"));
@@ -927,9 +952,11 @@ fn zap_button(state: AnyZapState, noteid: &[u8; 32]) -> impl egui::Widget + use<
let expand_size = 5.0; // from hover_expand_small
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0));
- let put_resp = ui
- .put(rect, img)
- .on_hover_text(tr!("Zap this note", "Hover text for zap button"));
+ let put_resp = ui.put(rect, img).on_hover_text(tr!(
+ i18n,
+ "Zap this note",
+ "Hover text for zap button"
+ ));
resp.union(put_resp)
}
diff --git a/crates/notedeck_ui/src/note/reply_description.rs b/crates/notedeck_ui/src/note/reply_description.rs
@@ -130,9 +130,13 @@ fn render_text_segments(
if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) {
let r = ui.add(
Label::new(
- RichText::new(tr!("note", "Link text for note references"))
- .size(size)
- .color(link_color),
+ RichText::new(tr!(
+ note_context.i18n,
+ "note",
+ "Link text for note references"
+ ))
+ .size(size)
+ .color(link_color),
)
.sense(Sense::click())
.selectable(selectable),
@@ -157,9 +161,13 @@ fn render_text_segments(
if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) {
let r = ui.add(
Label::new(
- RichText::new(tr!("thread", "Link text for thread references"))
- .size(size)
- .color(link_color),
+ RichText::new(tr!(
+ note_context.i18n,
+ "thread",
+ "Link text for thread references"
+ ))
+ .size(size)
+ .color(link_color),
)
.sense(Sense::click())
.selectable(selectable),
@@ -206,6 +214,7 @@ pub fn reply_desc(
} else {
// Handle case where reply note is not found
let template = tr!(
+ note_context.i18n,
"replying to a note",
"Fallback text when reply note is not found"
);
@@ -225,6 +234,7 @@ pub fn reply_desc(
let segments = if note_reply.is_reply_to_root() {
// Template: "replying to {user}'s {thread}"
let template = tr!(
+ note_context.i18n,
"replying to {user}'s {thread}",
"Template for replying to root thread",
user = "{user}",
@@ -243,6 +253,7 @@ pub fn reply_desc(
if root_note.pubkey() == reply_note.pubkey() {
// Template: "replying to {user}'s {note}"
let template = tr!(
+ note_context.i18n,
"replying to {user}'s {note}",
"Template for replying to user's note",
user = "{user}",
@@ -254,6 +265,7 @@ pub fn reply_desc(
// Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}"
// This would need more sophisticated placeholder handling
let template = tr!(
+ note_context.i18n,
"replying to {user}'s {note} in {thread_user}'s {thread}",
"Template for replying to note in different user's thread",
user = "{user}",
@@ -273,6 +285,7 @@ pub fn reply_desc(
} else {
// Template: "replying to {user} in someone's thread"
let template = tr!(
+ note_context.i18n,
"replying to {user} in someone's thread",
"Template for replying to user in unknown thread",
user = "{user}"
@@ -283,6 +296,7 @@ pub fn reply_desc(
} else {
// Fallback
let template = tr!(
+ note_context.i18n,
"replying to {user}",
"Fallback template for replying to user",
user = "{user}"
diff --git a/crates/notedeck_ui/src/profile/preview.rs b/crates/notedeck_ui/src/profile/preview.rs
@@ -3,7 +3,9 @@ use egui::{Frame, Label, RichText};
use egui_extras::Size;
use nostrdb::ProfileRecord;
-use notedeck::{name::get_display_name, profile::get_profile_url, tr, Images, NotedeckTextStyle};
+use notedeck::{
+ name::get_display_name, profile::get_profile_url, tr, Images, Localization, NotedeckTextStyle,
+};
use super::{about_section_widget, banner, display_name_widget};
@@ -68,6 +70,7 @@ impl egui::Widget for ProfilePreview<'_, '_> {
pub struct SimpleProfilePreview<'a, 'cache> {
profile: Option<&'a ProfileRecord<'a>>,
+ pub i18n: &'cache mut Localization,
cache: &'cache mut Images,
is_nsec: bool,
}
@@ -76,12 +79,14 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new(
profile: Option<&'a ProfileRecord<'a>>,
cache: &'cache mut Images,
+ i18n: &'cache mut Localization,
is_nsec: bool,
) -> Self {
SimpleProfilePreview {
profile,
cache,
is_nsec,
+ i18n,
}
}
}
@@ -96,12 +101,16 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> {
if !self.is_nsec {
ui.add(
Label::new(
- RichText::new(tr!("Read only", "Label for read-only profile mode"))
- .size(notedeck::fonts::get_font_size(
- ui.ctx(),
- &NotedeckTextStyle::Tiny,
- ))
- .color(ui.visuals().warn_fg_color),
+ RichText::new(tr!(
+ self.i18n,
+ "Read only",
+ "Label for read-only profile mode"
+ ))
+ .size(notedeck::fonts::get_font_size(
+ ui.ctx(),
+ &NotedeckTextStyle::Tiny,
+ ))
+ .color(ui.visuals().warn_fg_color),
)
.selectable(false),
);
diff --git a/crates/notedeck_ui/src/username.rs b/crates/notedeck_ui/src/username.rs
@@ -1,8 +1,9 @@
use egui::{Color32, RichText, Widget};
use nostrdb::ProfileRecord;
-use notedeck::{fonts::NamedFontFamily, tr};
+use notedeck::{fonts::NamedFontFamily, tr, Localization};
pub struct Username<'a> {
+ i18n: &'a mut Localization,
profile: Option<&'a ProfileRecord<'a>>,
pk: &'a [u8; 32],
pk_colored: bool,
@@ -20,10 +21,15 @@ impl<'a> Username<'a> {
self
}
- pub fn new(profile: Option<&'a ProfileRecord>, pk: &'a [u8; 32]) -> Self {
+ pub fn new(
+ i18n: &'a mut Localization,
+ profile: Option<&'a ProfileRecord>,
+ pk: &'a [u8; 32],
+ ) -> Self {
let pk_colored = false;
let abbrev: usize = 1000;
Username {
+ i18n,
profile,
pk,
pk_colored,
@@ -53,6 +59,7 @@ impl Widget for Username<'_> {
}
} else {
let mut txt = RichText::new(tr!(
+ self.i18n,
"nostrich",
"Default username when profile is not available"
))