Internationalize user-facing strings and export them for translations
Changelog-Added: Internationalized user-facing strings and exported them for translations Signed-off-by: Terry Yiu <git@tyiu.xyz>
This commit is contained in:
@@ -233,7 +233,7 @@ impl Notedeck {
|
||||
// Initialize localization
|
||||
let i18n_resource_dir = Path::new("assets/translations");
|
||||
let localization_manager = Arc::new(
|
||||
LocalizationManager::new(&i18n_resource_dir).unwrap_or_else(|e| {
|
||||
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"))
|
||||
|
||||
@@ -78,7 +78,7 @@ impl LocalizationManager {
|
||||
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!("{}/main.ftl", locale));
|
||||
let expected_path = self.resource_dir.join(format!("{locale}/main.ftl"));
|
||||
|
||||
// Try to open the file directly
|
||||
if let Err(e) = std::fs::File::open(&expected_path) {
|
||||
@@ -87,16 +87,16 @@ impl LocalizationManager {
|
||||
expected_path.display(),
|
||||
e
|
||||
);
|
||||
return Err(format!("Failed to open FTL file: {}", e).into());
|
||||
return Err(format!("Failed to open FTL file: {e}").into());
|
||||
}
|
||||
|
||||
// Load the FTL file directly instead of using ResourceManager
|
||||
let ftl_string = std::fs::read_to_string(&expected_path)
|
||||
.map_err(|e| format!("Failed to read FTL file: {}", e))?;
|
||||
.map_err(|e| format!("Failed to read FTL file: {e}"))?;
|
||||
|
||||
// Parse the FTL content
|
||||
let resource = FluentResource::try_new(ftl_string)
|
||||
.map_err(|e| format!("Failed to parse FTL content: {:?}", e))?;
|
||||
.map_err(|e| format!("Failed to parse FTL content: {e:?}"))?;
|
||||
|
||||
tracing::debug!(
|
||||
"Loaded and cached parsed FluentResource for locale: {}",
|
||||
@@ -182,15 +182,15 @@ impl LocalizationManager {
|
||||
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))?;
|
||||
.map_err(|e| format!("Failed to add resource to bundle: {e:?}"))?;
|
||||
|
||||
let message = bundle
|
||||
.get_message(id)
|
||||
.ok_or_else(|| format!("Message not found: {}", id))?;
|
||||
.ok_or_else(|| format!("Message not found: {id}"))?;
|
||||
|
||||
let pattern = message
|
||||
.value()
|
||||
.ok_or_else(|| format!("Message has no value: {}", id))?;
|
||||
.ok_or_else(|| format!("Message has no value: {id}"))?;
|
||||
|
||||
// Format the message
|
||||
let mut errors = Vec::new();
|
||||
@@ -243,16 +243,16 @@ impl LocalizationManager {
|
||||
locale,
|
||||
self.available_locales
|
||||
);
|
||||
return Err(format!("Locale {} is not available", locale).into());
|
||||
return Err(format!("Locale {locale} is not available").into());
|
||||
}
|
||||
|
||||
let mut current = self
|
||||
.current_locale
|
||||
.write()
|
||||
.map_err(|e| format!("Lock error: {e}"))?;
|
||||
tracing::info!("Switching locale from {} to {}", *current, locale);
|
||||
tracing::info!("Switching locale from {} to {locale}", *current);
|
||||
*current = locale.clone();
|
||||
tracing::info!("Successfully set locale to: {}", locale);
|
||||
tracing::info!("Successfully set locale to: {locale}");
|
||||
|
||||
// Clear caches when locale changes since they are locale-specific
|
||||
let mut string_cache = self
|
||||
@@ -406,7 +406,7 @@ impl LocalizationContext {
|
||||
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))
|
||||
.unwrap_or_else(|_| format!("[MISSING: {id}]"))
|
||||
}
|
||||
|
||||
/// Sets the current locale
|
||||
@@ -447,7 +447,7 @@ pub trait Localizable {
|
||||
impl Localizable for LocalizationContext {
|
||||
fn get_localized_string(&self, id: &str) -> String {
|
||||
self.get_string(id)
|
||||
.unwrap_or_else(|| format!("[MISSING: {}]", id))
|
||||
.unwrap_or_else(|| format!("[MISSING: {id}]"))
|
||||
}
|
||||
|
||||
fn get_localized_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String {
|
||||
|
||||
@@ -54,7 +54,7 @@ fn simple_hash(s: &str) -> String {
|
||||
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)
|
||||
format!("{key}:{comment}")
|
||||
} else {
|
||||
key.to_string()
|
||||
};
|
||||
@@ -76,8 +76,8 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
|
||||
result = result.trim_matches('_').to_string();
|
||||
|
||||
// Ensure the key starts with a letter (Fluent requirement)
|
||||
if !(result.len() > 0 && result.chars().next().unwrap().is_ascii_alphabetic()) {
|
||||
result = format!("k_{}", result);
|
||||
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
|
||||
|
||||
@@ -153,8 +153,8 @@ impl From<nwc::Error> for NwcError {
|
||||
impl Display for NwcError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NwcError::NIP47(err) => write!(f, "NIP47 error: {}", err),
|
||||
NwcError::Relay(err) => write!(f, "Relay error: {}", err),
|
||||
NwcError::NIP47(err) => write!(f, "NIP47 error: {err}"),
|
||||
NwcError::Relay(err) => write!(f, "Relay error: {err}"),
|
||||
NwcError::PrematureExit => write!(f, "Premature exit"),
|
||||
NwcError::Timeout => write!(f, "Request timed out"),
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ 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::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType};
|
||||
use notedeck::{tr, App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType};
|
||||
use notedeck_columns::{
|
||||
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
|
||||
};
|
||||
@@ -460,15 +460,16 @@ fn milestone_name() -> impl Widget {
|
||||
);
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new("BETA")
|
||||
RichText::new(tr!("BETA", "Beta version label"))
|
||||
.color(ui.style().visuals.noninteractive().fg_stroke.color)
|
||||
.font(font),
|
||||
)
|
||||
.selectable(false),
|
||||
)
|
||||
.on_hover_text(
|
||||
.on_hover_text(tr!(
|
||||
"Notedeck is a beta product. Expect bugs and contact us when you run into issues.",
|
||||
)
|
||||
"Beta product warning message"
|
||||
))
|
||||
.on_hover_cursor(egui::CursorIcon::Help)
|
||||
})
|
||||
.inner
|
||||
@@ -719,7 +720,10 @@ fn bottomup_sidebar(
|
||||
let resp = ui
|
||||
.add(Button::new("☀").frame(false))
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.on_hover_text("Switch to light mode");
|
||||
.on_hover_text(tr!(
|
||||
"Switch to light mode",
|
||||
"Hover text for light mode toggle button"
|
||||
));
|
||||
if resp.clicked() {
|
||||
Some(ChromePanelAction::SaveTheme(ThemePreference::Light))
|
||||
} else {
|
||||
@@ -730,7 +734,10 @@ fn bottomup_sidebar(
|
||||
let resp = ui
|
||||
.add(Button::new("🌙").frame(false))
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.on_hover_text("Switch to dark mode");
|
||||
.on_hover_text(tr!(
|
||||
"Switch to dark mode",
|
||||
"Hover text for dark mode toggle button"
|
||||
));
|
||||
if resp.clicked() {
|
||||
Some(ChromePanelAction::SaveTheme(ThemePreference::Dark))
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,8 @@ use egui_extras::{Size, StripBuilder};
|
||||
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{
|
||||
ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds,
|
||||
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState,
|
||||
UnknownIds,
|
||||
};
|
||||
use notedeck_ui::{jobs::JobsCache, NoteOptions};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
@@ -848,7 +849,7 @@ fn columns_to_decks_cache(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,
|
||||
"My Deck".to_owned(),
|
||||
tr!("My Deck", "Title for the user's deck"),
|
||||
cols,
|
||||
));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::{hash_map::ValuesMut, HashMap};
|
||||
|
||||
use enostr::{Pubkey, RelayPool};
|
||||
use nostrdb::Transaction;
|
||||
use notedeck::{AppContext, FALLBACK_PUBKEY};
|
||||
use notedeck::{tr, AppContext, FALLBACK_PUBKEY};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
@@ -397,8 +397,8 @@ impl Deck {
|
||||
'🇩'
|
||||
}
|
||||
|
||||
pub fn default_name() -> &'static str {
|
||||
"Default Deck"
|
||||
pub fn default_name() -> String {
|
||||
tr!("Default Deck", "Name of the default deck feed")
|
||||
}
|
||||
|
||||
pub fn new(icon: char, name: String) -> Self {
|
||||
|
||||
@@ -2,6 +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 poll_promise::Promise;
|
||||
|
||||
/// The state data for acquiring a nostr key
|
||||
@@ -134,7 +135,8 @@ fn show_error(ui: &mut egui::Ui, err: &AcquireKeyError) {
|
||||
ui.horizontal(|ui| {
|
||||
let error_label = match err {
|
||||
AcquireKeyError::InvalidKey => egui::Label::new(
|
||||
egui::RichText::new("Invalid key.").color(ui.visuals().error_fg_color),
|
||||
egui::RichText::new(tr!("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))
|
||||
|
||||
@@ -31,8 +31,8 @@ use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, P
|
||||
use enostr::ProfileState;
|
||||
use nostrdb::{Filter, Ndb, Transaction};
|
||||
use notedeck::{
|
||||
get_current_default_msats, get_current_wallet, ui::is_narrow, Accounts, AppContext, NoteAction,
|
||||
NoteContext, RelayAction,
|
||||
get_current_default_msats, get_current_wallet, tr, ui::is_narrow, Accounts, AppContext,
|
||||
NoteAction, NoteContext, RelayAction,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
@@ -572,14 +572,20 @@ fn render_nav_body(
|
||||
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
|
||||
txn
|
||||
} else {
|
||||
ui.label("Reply to unknown note");
|
||||
ui.label(tr!(
|
||||
"Reply to unknown note",
|
||||
"Error message when reply note cannot be found"
|
||||
));
|
||||
return None;
|
||||
};
|
||||
|
||||
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
|
||||
note
|
||||
} else {
|
||||
ui.label("Reply to unknown note");
|
||||
ui.label(tr!(
|
||||
"Reply to unknown note",
|
||||
"Error message when reply note cannot be found"
|
||||
));
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -616,7 +622,10 @@ fn render_nav_body(
|
||||
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
|
||||
note
|
||||
} else {
|
||||
ui.label("Quote of unknown note");
|
||||
ui.label(tr!(
|
||||
"Quote of unknown note",
|
||||
"Error message when quote note cannot be found"
|
||||
));
|
||||
return None;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use enostr::{NoteId, Pubkey};
|
||||
use notedeck::{NoteZapTargetOwned, RootNoteIdBuf, WalletType};
|
||||
use notedeck::{tr, NoteZapTargetOwned, RootNoteIdBuf, WalletType};
|
||||
use std::{
|
||||
fmt::{self},
|
||||
ops::Range,
|
||||
@@ -244,42 +244,85 @@ impl Route {
|
||||
pub fn title(&self) -> ColumnTitle<'_> {
|
||||
match self {
|
||||
Route::Timeline(kind) => kind.to_title(),
|
||||
Route::Thread(_) => ColumnTitle::simple("Thread"),
|
||||
Route::Reply(_id) => ColumnTitle::simple("Reply"),
|
||||
Route::Quote(_id) => ColumnTitle::simple("Quote"),
|
||||
Route::Relays => ColumnTitle::simple("Relays"),
|
||||
Route::Thread(_) => {
|
||||
ColumnTitle::formatted(tr!("Thread", "Column title for note thread view"))
|
||||
}
|
||||
Route::Reply(_id) => {
|
||||
ColumnTitle::formatted(tr!("Reply", "Column title for reply composition"))
|
||||
}
|
||||
Route::Quote(_id) => {
|
||||
ColumnTitle::formatted(tr!("Quote", "Column title for quote composition"))
|
||||
}
|
||||
Route::Relays => {
|
||||
ColumnTitle::formatted(tr!("Relays", "Column title for relay management"))
|
||||
}
|
||||
Route::Accounts(amr) => match amr {
|
||||
AccountsRoute::Accounts => ColumnTitle::simple("Accounts"),
|
||||
AccountsRoute::AddAccount => ColumnTitle::simple("Add Account"),
|
||||
AccountsRoute::Accounts => {
|
||||
ColumnTitle::formatted(tr!("Accounts", "Column title for account management"))
|
||||
}
|
||||
AccountsRoute::AddAccount => ColumnTitle::formatted(tr!(
|
||||
"Add Account",
|
||||
"Column title for adding new account"
|
||||
)),
|
||||
},
|
||||
Route::ComposeNote => ColumnTitle::simple("Compose Note"),
|
||||
Route::ComposeNote => {
|
||||
ColumnTitle::formatted(tr!("Compose Note", "Column title for note composition"))
|
||||
}
|
||||
Route::AddColumn(c) => match c {
|
||||
AddColumnRoute::Base => ColumnTitle::simple("Add Column"),
|
||||
AddColumnRoute::Base => {
|
||||
ColumnTitle::formatted(tr!("Add Column", "Column title for adding new column"))
|
||||
}
|
||||
AddColumnRoute::Algo(r) => match r {
|
||||
AddAlgoRoute::Base => ColumnTitle::simple("Add Algo Column"),
|
||||
AddAlgoRoute::LastPerPubkey => ColumnTitle::simple("Add Last Notes Column"),
|
||||
AddAlgoRoute::Base => ColumnTitle::formatted(tr!(
|
||||
"Add Algo Column",
|
||||
"Column title for adding algorithm column"
|
||||
)),
|
||||
AddAlgoRoute::LastPerPubkey => ColumnTitle::formatted(tr!(
|
||||
"Add Last Notes Column",
|
||||
"Column title for adding last notes column"
|
||||
)),
|
||||
},
|
||||
AddColumnRoute::UndecidedNotification => {
|
||||
ColumnTitle::simple("Add Notifications Column")
|
||||
}
|
||||
AddColumnRoute::ExternalNotification => {
|
||||
ColumnTitle::simple("Add External Notifications Column")
|
||||
}
|
||||
AddColumnRoute::Hashtag => ColumnTitle::simple("Add Hashtag Column"),
|
||||
AddColumnRoute::UndecidedIndividual => {
|
||||
ColumnTitle::simple("Subscribe to someone's notes")
|
||||
}
|
||||
AddColumnRoute::ExternalIndividual => {
|
||||
ColumnTitle::simple("Subscribe to someone else's notes")
|
||||
}
|
||||
AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!(
|
||||
"Add Notifications Column",
|
||||
"Column title for adding notifications column"
|
||||
)),
|
||||
AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!(
|
||||
"Add External Notifications Column",
|
||||
"Column title for adding external notifications column"
|
||||
)),
|
||||
AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!(
|
||||
"Add Hashtag Column",
|
||||
"Column title for adding hashtag column"
|
||||
)),
|
||||
AddColumnRoute::UndecidedIndividual => ColumnTitle::formatted(tr!(
|
||||
"Subscribe to someone's notes",
|
||||
"Column title for subscribing to individual user"
|
||||
)),
|
||||
AddColumnRoute::ExternalIndividual => ColumnTitle::formatted(tr!(
|
||||
"Subscribe to someone else's notes",
|
||||
"Column title for subscribing to external user"
|
||||
)),
|
||||
},
|
||||
Route::Support => ColumnTitle::simple("Damus Support"),
|
||||
Route::NewDeck => ColumnTitle::simple("Add Deck"),
|
||||
Route::EditDeck(_) => ColumnTitle::simple("Edit Deck"),
|
||||
Route::EditProfile(_) => ColumnTitle::simple("Edit Profile"),
|
||||
Route::Search => ColumnTitle::simple("Search"),
|
||||
Route::Wallet(_) => ColumnTitle::simple("Wallet"),
|
||||
Route::CustomizeZapAmount(_) => ColumnTitle::simple("Customize Zap Amount"),
|
||||
Route::Support => {
|
||||
ColumnTitle::formatted(tr!("Damus Support", "Column title for support page"))
|
||||
}
|
||||
Route::NewDeck => {
|
||||
ColumnTitle::formatted(tr!("Add Deck", "Column title for adding new deck"))
|
||||
}
|
||||
Route::EditDeck(_) => {
|
||||
ColumnTitle::formatted(tr!("Edit Deck", "Column title for editing deck"))
|
||||
}
|
||||
Route::EditProfile(_) => {
|
||||
ColumnTitle::formatted(tr!("Edit Profile", "Column title for profile editing"))
|
||||
}
|
||||
Route::Search => ColumnTitle::formatted(tr!("Search", "Column title for search page")),
|
||||
Route::Wallet(_) => {
|
||||
ColumnTitle::formatted(tr!("Wallet", "Column title for wallet management"))
|
||||
}
|
||||
Route::CustomizeZapAmount(_) => ColumnTitle::formatted(tr!(
|
||||
"Customize Zap Amount",
|
||||
"Column title for zap amount customization"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,34 +496,90 @@ 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, "Home"),
|
||||
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => {
|
||||
write!(f, "Last Per Pubkey (Contact)")
|
||||
TimelineKind::List(ListKind::Contact(_pk)) => {
|
||||
write!(f, "{}", tr!("Home", "Display name for home feed"))
|
||||
}
|
||||
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!(
|
||||
"Last Per Pubkey (Contact)",
|
||||
"Display name for last notes per contact"
|
||||
)
|
||||
)
|
||||
}
|
||||
TimelineKind::Notifications(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Notifications", "Display name for notifications")
|
||||
),
|
||||
TimelineKind::Universe => {
|
||||
write!(f, "{}", tr!("Universe", "Display name for universe feed"))
|
||||
}
|
||||
TimelineKind::Generic(_) => {
|
||||
write!(f, "{}", tr!("Custom", "Display name for custom timelines"))
|
||||
}
|
||||
TimelineKind::Search(_) => {
|
||||
write!(f, "{}", tr!("Search", "Display name for search results"))
|
||||
}
|
||||
TimelineKind::Hashtag(ht) => write!(
|
||||
f,
|
||||
"{} ({})",
|
||||
tr!("Hashtags", "Display name for hashtag feeds"),
|
||||
ht.join(" ")
|
||||
),
|
||||
TimelineKind::Profile(_id) => {
|
||||
write!(f, "{}", tr!("Profile", "Display name for user profiles"))
|
||||
}
|
||||
TimelineKind::Notifications(_) => write!(f, "Notifications"),
|
||||
TimelineKind::Universe => write!(f, "Universe"),
|
||||
TimelineKind::Generic(_) => write!(f, "Custom"),
|
||||
TimelineKind::Search(_) => write!(f, "Search"),
|
||||
TimelineKind::Hashtag(ht) => write!(f, "Hashtags ({})", ht.join(" ")),
|
||||
TimelineKind::Profile(_id) => write!(f, "Profile"),
|
||||
},
|
||||
Route::Thread(_) => write!(f, "Thread"),
|
||||
Route::Reply(_id) => write!(f, "Reply"),
|
||||
Route::Quote(_id) => write!(f, "Quote"),
|
||||
Route::Relays => write!(f, "Relays"),
|
||||
Route::Thread(_) => write!(f, "{}", tr!("Thread", "Display name for thread view")),
|
||||
Route::Reply(_id) => {
|
||||
write!(f, "{}", tr!("Reply", "Display name for reply composition"))
|
||||
}
|
||||
Route::Quote(_id) => {
|
||||
write!(f, "{}", tr!("Quote", "Display name for quote composition"))
|
||||
}
|
||||
Route::Relays => write!(f, "{}", tr!("Relays", "Display name for relay management")),
|
||||
Route::Accounts(amr) => match amr {
|
||||
AccountsRoute::Accounts => write!(f, "Accounts"),
|
||||
AccountsRoute::AddAccount => write!(f, "Add Account"),
|
||||
AccountsRoute::Accounts => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Accounts", "Display name for account management")
|
||||
),
|
||||
AccountsRoute::AddAccount => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Add Account", "Display name for adding account")
|
||||
),
|
||||
},
|
||||
Route::ComposeNote => write!(f, "Compose Note"),
|
||||
Route::AddColumn(_) => write!(f, "Add Column"),
|
||||
Route::Support => write!(f, "Support"),
|
||||
Route::NewDeck => write!(f, "Add Deck"),
|
||||
Route::EditDeck(_) => write!(f, "Edit Deck"),
|
||||
Route::EditProfile(_) => write!(f, "Edit Profile"),
|
||||
Route::Search => write!(f, "Search"),
|
||||
Route::Wallet(_) => write!(f, "Wallet"),
|
||||
Route::CustomizeZapAmount(_) => write!(f, "Customize Zap Amount"),
|
||||
Route::ComposeNote => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Compose Note", "Display name for note composition")
|
||||
),
|
||||
Route::AddColumn(_) => {
|
||||
write!(f, "{}", tr!("Add Column", "Display name for adding column"))
|
||||
}
|
||||
Route::Support => write!(f, "{}", tr!("Support", "Display name for support page")),
|
||||
Route::NewDeck => write!(f, "{}", tr!("Add Deck", "Display name for adding deck")),
|
||||
Route::EditDeck(_) => {
|
||||
write!(f, "{}", tr!("Edit Deck", "Display name for editing deck"))
|
||||
}
|
||||
Route::EditProfile(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Edit Profile", "Display name for profile editing")
|
||||
),
|
||||
Route::Search => write!(f, "{}", tr!("Search", "Display name for search page")),
|
||||
Route::Wallet(_) => {
|
||||
write!(f, "{}", tr!("Wallet", "Display name for wallet management"))
|
||||
}
|
||||
Route::CustomizeZapAmount(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Customize Zap Amount", "Display name for zap customization")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{
|
||||
contacts::{contacts_filter, hybrid_contacts_filter},
|
||||
filter::{self, default_limit, default_remote_limit, HybridFilter},
|
||||
FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf,
|
||||
tr, FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::{Hash, Hasher};
|
||||
@@ -257,14 +257,47 @@ impl AlgoTimeline {
|
||||
impl Display for TimelineKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Home"),
|
||||
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => f.write_str("Last Notes"),
|
||||
TimelineKind::Generic(_) => f.write_str("Timeline"),
|
||||
TimelineKind::Notifications(_) => f.write_str("Notifications"),
|
||||
TimelineKind::Profile(_) => f.write_str("Profile"),
|
||||
TimelineKind::Universe => f.write_str("Universe"),
|
||||
TimelineKind::Hashtag(_) => f.write_str("Hashtags"),
|
||||
TimelineKind::Search(_) => f.write_str("Search"),
|
||||
TimelineKind::List(ListKind::Contact(_src)) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Home", "Timeline kind label for contact lists")
|
||||
),
|
||||
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!(
|
||||
"Last Notes",
|
||||
"Timeline kind label for last notes per pubkey"
|
||||
)
|
||||
),
|
||||
TimelineKind::Generic(_) => {
|
||||
write!(f, "{}", tr!("Timeline", "Generic timeline kind label"))
|
||||
}
|
||||
TimelineKind::Notifications(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Notifications", "Timeline kind label for notifications")
|
||||
),
|
||||
TimelineKind::Profile(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Profile", "Timeline kind label for user profiles")
|
||||
),
|
||||
TimelineKind::Universe => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Universe", "Timeline kind label for universe feed")
|
||||
),
|
||||
TimelineKind::Hashtag(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Hashtag", "Timeline kind label for hashtag feeds")
|
||||
),
|
||||
TimelineKind::Search(_) => write!(
|
||||
f,
|
||||
"{}",
|
||||
tr!("Search", "Timeline kind label for search results")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -567,15 +600,26 @@ impl TimelineKind {
|
||||
ColumnTitle::formatted(format!("Search \"{}\"", query.search))
|
||||
}
|
||||
TimelineKind::List(list_kind) => match list_kind {
|
||||
ListKind::Contact(_pubkey_source) => ColumnTitle::simple("Contacts"),
|
||||
ListKind::Contact(_pubkey_source) => {
|
||||
ColumnTitle::formatted(tr!("Contacts", "Column title for contact lists"))
|
||||
}
|
||||
},
|
||||
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(list_kind)) => match list_kind {
|
||||
ListKind::Contact(_pubkey_source) => ColumnTitle::simple("Contacts (last notes)"),
|
||||
ListKind::Contact(_pubkey_source) => ColumnTitle::formatted(tr!(
|
||||
"Contacts (last notes)",
|
||||
"Column title for last notes per contact"
|
||||
)),
|
||||
},
|
||||
TimelineKind::Notifications(_pubkey_source) => ColumnTitle::simple("Notifications"),
|
||||
TimelineKind::Notifications(_pubkey_source) => {
|
||||
ColumnTitle::formatted(tr!("Notifications", "Column title for notifications"))
|
||||
}
|
||||
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
|
||||
TimelineKind::Universe => ColumnTitle::simple("Universe"),
|
||||
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
|
||||
TimelineKind::Universe => {
|
||||
ColumnTitle::formatted(tr!("Universe", "Column title for universe feed"))
|
||||
}
|
||||
TimelineKind::Generic(_) => {
|
||||
ColumnTitle::formatted(tr!("Custom", "Column title for custom timelines"))
|
||||
}
|
||||
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ use crate::{
|
||||
use notedeck::{
|
||||
contacts::hybrid_contacts_filter,
|
||||
filter::{self, HybridFilter},
|
||||
Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache, NoteRef,
|
||||
UnknownIds,
|
||||
tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache,
|
||||
NoteRef, UnknownIds,
|
||||
};
|
||||
|
||||
use egui_virtual_list::VirtualList;
|
||||
@@ -64,10 +64,12 @@ pub enum ViewFilter {
|
||||
}
|
||||
|
||||
impl ViewFilter {
|
||||
pub fn name(&self) -> &'static str {
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
ViewFilter::Notes => "Notes",
|
||||
ViewFilter::NotesAndReplies => "Notes & Replies",
|
||||
ViewFilter::Notes => tr!("Notes", "Filter label for notes only view"),
|
||||
ViewFilter::NotesAndReplies => {
|
||||
tr!("Notes & Replies", "Filter label for notes and replies view")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use egui::{
|
||||
};
|
||||
use egui_winit::clipboard::Clipboard;
|
||||
use enostr::Keypair;
|
||||
use notedeck::{fonts::get_font_size, AppAction, NotedeckTextStyle};
|
||||
use notedeck::{fonts::get_font_size, tr, AppAction, NotedeckTextStyle};
|
||||
use notedeck_ui::{
|
||||
app_images,
|
||||
context_menu::{input_context, PasteBehavior},
|
||||
@@ -58,7 +58,7 @@ 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("Enter your public key (npub), nostr address (e.g. vrod@damus.io), or private key (nsec). You must enter your private key to be able to post, reply, etc.")
|
||||
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"))
|
||||
.text_style(help_text_style.text_style())
|
||||
.size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()),
|
||||
).wrap())
|
||||
@@ -73,13 +73,13 @@ impl<'a> AccountLoginView<'a> {
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new("New to Nostr?")
|
||||
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."))
|
||||
.color(ui.style().visuals.noninteractive().fg_stroke.color)
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
);
|
||||
|
||||
if ui
|
||||
.add(Button::new(RichText::new("Create Account")).frame(false))
|
||||
.add(Button::new(RichText::new(tr!("Create Account", "Button to create a new account"))).frame(false))
|
||||
.clicked()
|
||||
{
|
||||
self.manager.should_create_new();
|
||||
@@ -99,20 +99,20 @@ impl<'a> AccountLoginView<'a> {
|
||||
}
|
||||
|
||||
fn login_title_text() -> RichText {
|
||||
RichText::new("Login")
|
||||
RichText::new(tr!("Login", "Login page title"))
|
||||
.text_style(NotedeckTextStyle::Heading2.text_style())
|
||||
.strong()
|
||||
}
|
||||
|
||||
fn login_textedit_info_text() -> RichText {
|
||||
RichText::new("Enter your key")
|
||||
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)."))
|
||||
.strong()
|
||||
.text_style(NotedeckTextStyle::Body.text_style())
|
||||
}
|
||||
|
||||
fn login_button() -> Button<'static> {
|
||||
Button::new(
|
||||
RichText::new("Login now — let's do this!")
|
||||
RichText::new(tr!("Login now — let's do this!", "Login button text"))
|
||||
.text_style(NotedeckTextStyle::Body.text_style())
|
||||
.strong(),
|
||||
)
|
||||
@@ -124,7 +124,11 @@ fn login_textedit(manager: &mut AcquireKeyState) -> TextEdit {
|
||||
let create_textedit: fn(&mut dyn TextBuffer) -> TextEdit = |text| {
|
||||
egui::TextEdit::singleline(text)
|
||||
.hint_text(
|
||||
RichText::new("Your key here...").text_style(NotedeckTextStyle::Body.text_style()),
|
||||
RichText::new(tr!(
|
||||
"Your key here...",
|
||||
"Placeholder text for key input field"
|
||||
))
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
)
|
||||
.vertical_align(Align::Center)
|
||||
.min_size(Vec2::new(0.0, 40.0))
|
||||
|
||||
@@ -3,7 +3,7 @@ use egui::{
|
||||
};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, Images};
|
||||
use notedeck::{tr, Accounts, Images};
|
||||
use notedeck_ui::colors::PINK;
|
||||
|
||||
use notedeck_ui::app_images;
|
||||
@@ -171,7 +171,7 @@ fn scroll_area() -> ScrollArea {
|
||||
fn add_account_button() -> Button<'static> {
|
||||
Button::image_and_text(
|
||||
app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
|
||||
RichText::new(" Add account")
|
||||
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),
|
||||
@@ -180,5 +180,8 @@ fn add_account_button() -> Button<'static> {
|
||||
}
|
||||
|
||||
fn sign_out_button() -> egui::Button<'static> {
|
||||
egui::Button::new(RichText::new("Sign out"))
|
||||
egui::Button::new(RichText::new(tr!(
|
||||
"Sign out",
|
||||
"Button label to sign out of account"
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::{
|
||||
Damus,
|
||||
};
|
||||
|
||||
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount};
|
||||
use notedeck::{tr, AppContext, Images, NotedeckTextStyle, UserAccount};
|
||||
use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images};
|
||||
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
|
||||
|
||||
@@ -229,8 +229,11 @@ impl<'a> AddColumnView<'a> {
|
||||
deck_author: Pubkey,
|
||||
) -> Option<AddColumnResponse> {
|
||||
let algo_option = ColumnOptionData {
|
||||
title: "Contact List",
|
||||
description: "Source the last note for each user in your contact list",
|
||||
title: tr!("Contact List", "Title for contact list column"),
|
||||
description: tr!(
|
||||
"Source the last note for each user in your contact list",
|
||||
"Description for contact list column"
|
||||
),
|
||||
icon: app_images::home_image(),
|
||||
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Decided(
|
||||
ListKind::contact_list(deck_author),
|
||||
@@ -245,8 +248,11 @@ impl<'a> AddColumnView<'a> {
|
||||
|
||||
fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
|
||||
let algo_option = ColumnOptionData {
|
||||
title: "Last Note per User",
|
||||
description: "Show the last note for each user from a list",
|
||||
title: tr!("Last Note per User", "Title for last note per user column"),
|
||||
description: tr!(
|
||||
"Show the last note for each user from a list",
|
||||
"Description for last note per user column"
|
||||
),
|
||||
icon: app_images::algo_image(),
|
||||
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)),
|
||||
};
|
||||
@@ -291,8 +297,11 @@ impl<'a> AddColumnView<'a> {
|
||||
let text_edit = key_state.get_acquire_textedit(|text| {
|
||||
egui::TextEdit::singleline(text)
|
||||
.hint_text(
|
||||
RichText::new("Enter the user's key (npub, hex, nip05) here...")
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
RichText::new(tr!(
|
||||
"Enter the user's key (npub, hex, nip05) here...",
|
||||
"Hint text to prompt entering the user's public key."
|
||||
))
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
)
|
||||
.vertical_align(Align::Center)
|
||||
.desired_width(f32::INFINITY)
|
||||
@@ -386,7 +395,8 @@ impl<'a> AddColumnView<'a> {
|
||||
title_font_max_size + inter_text_padding + desc_font_max_size + (2.0 * height_padding)
|
||||
};
|
||||
|
||||
let helper = AnimationHelper::new(ui, data.title, vec2(max_width, max_height));
|
||||
let title = data.title.clone();
|
||||
let helper = AnimationHelper::new(ui, title.clone(), vec2(max_width, max_height));
|
||||
let animation_rect = helper.get_animation_rect();
|
||||
|
||||
let cur_icon_width = helper.scale_1d_pos(min_icon_width);
|
||||
@@ -445,8 +455,11 @@ impl<'a> AddColumnView<'a> {
|
||||
fn get_base_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> {
|
||||
let mut vec = Vec::new();
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Home",
|
||||
description: "See notes from your contacts",
|
||||
title: tr!("Home", "Title for Home column"),
|
||||
description: tr!(
|
||||
"See notes from your contacts",
|
||||
"Description for Home column"
|
||||
),
|
||||
icon: app_images::home_image(),
|
||||
option: AddColumnOption::Contacts(if self.cur_account.key.secret_key.is_some() {
|
||||
PubkeySource::DeckAuthor
|
||||
@@ -455,32 +468,47 @@ impl<'a> AddColumnView<'a> {
|
||||
}),
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Notifications",
|
||||
description: "Stay up to date with notifications and mentions",
|
||||
title: tr!("Notifications", "Title for notifications column"),
|
||||
description: tr!(
|
||||
"Stay up to date with notifications and mentions",
|
||||
"Description for notifications column"
|
||||
),
|
||||
icon: app_images::notifications_image(ui.visuals().dark_mode),
|
||||
option: AddColumnOption::UndecidedNotification,
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Universe",
|
||||
description: "See the whole nostr universe",
|
||||
title: tr!("Universe", "Title for universe column"),
|
||||
description: tr!(
|
||||
"See the whole nostr universe",
|
||||
"Description for universe column"
|
||||
),
|
||||
icon: app_images::universe_image(),
|
||||
option: AddColumnOption::Universe,
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Hashtags",
|
||||
description: "Stay up to date with a certain hashtag",
|
||||
title: tr!("Hashtags", "Title for hashtags column"),
|
||||
description: tr!(
|
||||
"Stay up to date with a certain hashtag",
|
||||
"Description for hashtags column"
|
||||
),
|
||||
icon: app_images::hashtag_image(),
|
||||
option: AddColumnOption::UndecidedHashtag,
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Individual",
|
||||
description: "Stay up to date with someone's notes & replies",
|
||||
title: tr!("Individual", "Title for individual user column"),
|
||||
description: tr!(
|
||||
"Stay up to date with someone's notes & replies",
|
||||
"Description for individual user column"
|
||||
),
|
||||
icon: app_images::profile_image(),
|
||||
option: AddColumnOption::UndecidedIndividual,
|
||||
});
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Algo",
|
||||
description: "Algorithmic feeds to aid in note discovery",
|
||||
title: tr!("Algo", "Title for algorithmic feeds column"),
|
||||
description: tr!(
|
||||
"Algorithmic feeds to aid in note discovery",
|
||||
"Description for algorithmic feeds column"
|
||||
),
|
||||
icon: app_images::algo_image(),
|
||||
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)),
|
||||
});
|
||||
@@ -498,15 +526,24 @@ impl<'a> AddColumnView<'a> {
|
||||
};
|
||||
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Your Notifications",
|
||||
description: "Stay up to date with your notifications and mentions",
|
||||
title: tr!("Your Notifications", "Title for your notifications column"),
|
||||
description: tr!(
|
||||
"Stay up to date with your notifications and mentions",
|
||||
"Description for your notifications column"
|
||||
),
|
||||
icon: app_images::notifications_image(ui.visuals().dark_mode),
|
||||
option: AddColumnOption::Notification(source),
|
||||
});
|
||||
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Someone else's Notifications",
|
||||
description: "Stay up to date with someone else's notifications and mentions",
|
||||
title: tr!(
|
||||
"Someone else's Notifications",
|
||||
"Title for someone else's notifications column"
|
||||
),
|
||||
description: tr!(
|
||||
"Stay up to date with someone else's notifications and mentions",
|
||||
"Description for someone else's notifications column"
|
||||
),
|
||||
icon: app_images::notifications_image(ui.visuals().dark_mode),
|
||||
option: AddColumnOption::ExternalNotification,
|
||||
});
|
||||
@@ -524,15 +561,24 @@ impl<'a> AddColumnView<'a> {
|
||||
};
|
||||
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Your Notes",
|
||||
description: "Keep track of your notes & replies",
|
||||
title: tr!("Your Notes", "Title for your notes column"),
|
||||
description: tr!(
|
||||
"Keep track of your notes & replies",
|
||||
"Description for your notes column"
|
||||
),
|
||||
icon: app_images::profile_image(),
|
||||
option: AddColumnOption::Individual(source),
|
||||
});
|
||||
|
||||
vec.push(ColumnOptionData {
|
||||
title: "Someone else's Notes",
|
||||
description: "Stay up to date with someone else's notes & replies",
|
||||
title: tr!(
|
||||
"Someone else's Notes",
|
||||
"Title for someone else's notes column"
|
||||
),
|
||||
description: tr!(
|
||||
"Stay up to date with someone else's notes & replies",
|
||||
"Description for someone else's notes column"
|
||||
),
|
||||
icon: app_images::profile_image(),
|
||||
option: AddColumnOption::ExternalIndividual,
|
||||
});
|
||||
@@ -542,11 +588,15 @@ impl<'a> AddColumnView<'a> {
|
||||
}
|
||||
|
||||
fn find_user_button() -> impl Widget {
|
||||
styled_button("Find User", notedeck_ui::colors::PINK)
|
||||
let label = tr!("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 {
|
||||
styled_button("Add", notedeck_ui::colors::PINK)
|
||||
let label = tr!("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)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -571,8 +621,8 @@ pub(crate) fn sized_button(text: &str) -> impl Widget + '_ {
|
||||
*/
|
||||
|
||||
struct ColumnOptionData {
|
||||
title: &'static str,
|
||||
description: &'static str,
|
||||
title: String,
|
||||
description: String,
|
||||
icon: Image<'static>,
|
||||
option: AddColumnOption,
|
||||
}
|
||||
@@ -648,7 +698,7 @@ pub fn render_add_column_routes(
|
||||
}
|
||||
|
||||
// We have a decision on where we want the last per pubkey
|
||||
// source to be, so let;s create a timeline from that and
|
||||
// source to be, so let's create a timeline from that and
|
||||
// add it to our list of timelines
|
||||
AlgoOption::LastPerPubkey(Decision::Decided(list_kind)) => {
|
||||
let txn = Transaction::new(ctx.ndb).unwrap();
|
||||
@@ -734,8 +784,11 @@ pub fn hashtag_ui(
|
||||
|
||||
let text_edit = egui::TextEdit::singleline(text_buffer)
|
||||
.hint_text(
|
||||
RichText::new("Enter the desired hashtags here (for multiple space-separated)")
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
RichText::new(tr!(
|
||||
"Enter the desired hashtags here (for multiple space-separated)",
|
||||
"Placeholder for hashtag input field"
|
||||
))
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
)
|
||||
.vertical_align(Align::Center)
|
||||
.desired_width(f32::INFINITY)
|
||||
@@ -790,7 +843,7 @@ mod tests {
|
||||
let data_str = "column:algo_selection:last_per_pubkey";
|
||||
let data = &data_str.split(":").collect::<Vec<&str>>();
|
||||
let mut token_writer = TokenWriter::default();
|
||||
let mut parser = TokenParser::new(&data);
|
||||
let mut parser = TokenParser::new(data);
|
||||
let parsed = AddColumnRoute::parse_from_tokens(&mut parser).unwrap();
|
||||
let expected = AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey);
|
||||
parsed.serialize_tokens(&mut token_writer);
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::tr;
|
||||
use notedeck::{Images, NotedeckTextStyle};
|
||||
use notedeck_ui::app_images;
|
||||
use notedeck_ui::{
|
||||
@@ -192,12 +193,16 @@ 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("Confirm");
|
||||
let confirm_resp = ui.button(tr!("Confirm", "Button label to confirm an action"));
|
||||
if confirm_resp.clicked() {
|
||||
confirm_pressed = true;
|
||||
}
|
||||
|
||||
if confirm_resp.clicked() || ui.button("Cancel").clicked() {
|
||||
if confirm_resp.clicked()
|
||||
|| ui
|
||||
.button(tr!("Cancel", "Button label to cancel an action"))
|
||||
.clicked()
|
||||
{
|
||||
ui.data_mut(|d| d.insert_temp(id, false));
|
||||
}
|
||||
});
|
||||
@@ -206,7 +211,8 @@ impl<'a> NavTitle<'a> {
|
||||
}
|
||||
confirm_pressed
|
||||
} else {
|
||||
delete_button_resp.on_hover_text("Delete this column");
|
||||
delete_button_resp
|
||||
.on_hover_text(tr!("Delete this column", "Tooltip for deleting a column"));
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -220,7 +226,10 @@ 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("Moves this column to another positon");
|
||||
move_resp = move_resp.on_hover_text(tr!(
|
||||
"Moves this column to another position",
|
||||
"Tooltip for moving a column"
|
||||
));
|
||||
}
|
||||
|
||||
if move_resp.clicked() {
|
||||
|
||||
@@ -1,5 +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::{NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
@@ -17,18 +18,16 @@ pub struct ConfigureDeckResponse {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
static CREATE_TEXT: &str = "Create Deck";
|
||||
|
||||
impl<'a> ConfigureDeckView<'a> {
|
||||
pub fn new(state: &'a mut DeckState) -> Self {
|
||||
Self {
|
||||
state,
|
||||
create_button_text: CREATE_TEXT.to_owned(),
|
||||
create_button_text: tr!("Create Deck", "Button label to create a new deck"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_create_text(mut self, text: &str) -> Self {
|
||||
self.create_button_text = text.to_owned();
|
||||
pub fn with_create_text(mut self, text: String) -> Self {
|
||||
self.create_button_text = text;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -39,22 +38,28 @@ impl<'a> ConfigureDeckView<'a> {
|
||||
);
|
||||
padding(16.0, ui, |ui| {
|
||||
ui.add(Label::new(
|
||||
RichText::new("Deck name").font(title_font.clone()),
|
||||
RichText::new(tr!("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("We recommend short names")
|
||||
.color(ui.visuals().noninteractive().fg_stroke.color)
|
||||
.size(notedeck::fonts::get_font_size(
|
||||
ui.ctx(),
|
||||
&NotedeckTextStyle::Small,
|
||||
)),
|
||||
RichText::new(tr!(
|
||||
"We recommend short names",
|
||||
"Hint for deck name input field"
|
||||
))
|
||||
.color(ui.visuals().noninteractive().fg_stroke.color)
|
||||
.size(notedeck::fonts::get_font_size(
|
||||
ui.ctx(),
|
||||
&NotedeckTextStyle::Small,
|
||||
)),
|
||||
));
|
||||
|
||||
ui.add_space(32.0);
|
||||
ui.add(Label::new(RichText::new("Icon").font(title_font)));
|
||||
ui.add(Label::new(
|
||||
RichText::new(tr!("Icon", "Label for deck icon selection")).font(title_font),
|
||||
));
|
||||
|
||||
if ui
|
||||
.add(deck_icon(
|
||||
@@ -121,28 +126,27 @@ impl<'a> ConfigureDeckView<'a> {
|
||||
}
|
||||
|
||||
fn show_warnings(ui: &mut Ui, warn_no_icon: bool, warn_no_title: bool) {
|
||||
if warn_no_icon || warn_no_title {
|
||||
let messages = [
|
||||
if warn_no_title {
|
||||
"create a name for the deck"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if warn_no_icon { "select an icon" } else { "" },
|
||||
];
|
||||
let message = messages
|
||||
.iter()
|
||||
.filter(|&&m| !m.is_empty())
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" and ");
|
||||
let warning = if warn_no_title && warn_no_icon {
|
||||
tr!(
|
||||
"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!(
|
||||
"Please create a name for the deck.",
|
||||
"Error message for missing deck name"
|
||||
)
|
||||
} else if warn_no_icon {
|
||||
tr!(
|
||||
"Please select an icon.",
|
||||
"Error message for missing deck icon"
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
ui.add(
|
||||
egui::Label::new(
|
||||
RichText::new(format!("Please {message}.")).color(ui.visuals().error_fg_color),
|
||||
)
|
||||
.wrap(),
|
||||
);
|
||||
if !warning.is_empty() {
|
||||
ui.add(egui::Label::new(RichText::new(warning).color(ui.visuals().error_fg_color)).wrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ use egui::Widget;
|
||||
use crate::deck_state::DeckState;
|
||||
|
||||
use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView};
|
||||
use notedeck::tr;
|
||||
use notedeck_ui::padding;
|
||||
|
||||
pub struct EditDeckView<'a> {
|
||||
config_view: ConfigureDeckView<'a>,
|
||||
}
|
||||
|
||||
static EDIT_TEXT: &str = "Edit Deck";
|
||||
|
||||
pub enum EditDeckResponse {
|
||||
Edit(ConfigureDeckResponse),
|
||||
Delete,
|
||||
@@ -18,7 +17,8 @@ pub enum EditDeckResponse {
|
||||
|
||||
impl<'a> EditDeckView<'a> {
|
||||
pub fn new(state: &'a mut DeckState) -> Self {
|
||||
let config_view = ConfigureDeckView::new(state).with_create_text(EDIT_TEXT);
|
||||
let config_view = ConfigureDeckView::new(state)
|
||||
.with_create_text(tr!("Edit Deck", "Button label to edit a deck"));
|
||||
Self { config_view }
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ fn delete_button() -> impl Widget {
|
||||
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("Delete Deck")
|
||||
egui::Button::new(tr!("Delete Deck", "Button label to delete a deck"))
|
||||
.fill(ui.visuals().error_fg_color)
|
||||
.min_size(size),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ use egui::{
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::{Ndb, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, get_profile_url, name::get_display_name, Images, NotedeckTextStyle,
|
||||
fonts::get_font_size, get_profile_url, name::get_display_name, tr, Images, NotedeckTextStyle,
|
||||
};
|
||||
use notedeck_ui::{
|
||||
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable,
|
||||
@@ -110,7 +110,7 @@ impl<'a> CustomZapView<'a> {
|
||||
ui.data_mut(|d| d.insert_temp(id, cur_amount));
|
||||
|
||||
let resp = ui.add(styled_button_toggleable(
|
||||
"Send",
|
||||
&tr!("Send", "Button label to send a zap"),
|
||||
colors::PINK,
|
||||
is_valid_zap(maybe_sats),
|
||||
));
|
||||
@@ -158,7 +158,8 @@ fn show_title(ui: &mut egui::Ui) {
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.add(egui::Label::new(
|
||||
egui::RichText::new("Zap").text_style(NotedeckTextStyle::Heading2.text_style()),
|
||||
egui::RichText::new(tr!("Zap", "Heading for zap (tip) action"))
|
||||
.text_style(NotedeckTextStyle::Heading2.text_style()),
|
||||
));
|
||||
},
|
||||
);
|
||||
@@ -190,7 +191,10 @@ fn show_amount(ui: &mut egui::Ui, id: egui::Id, user_input: &mut String, width:
|
||||
let painter = ui.painter();
|
||||
|
||||
let sats_galley = painter.layout_no_wrap(
|
||||
"SATS".to_owned(),
|
||||
tr!(
|
||||
"SATS",
|
||||
"Label for satoshis (Bitcoin unit) for custom zap amount input field"
|
||||
),
|
||||
NotedeckTextStyle::Heading4.get_font_id(ui.ctx()),
|
||||
ui.visuals().noninteractive().text_color(),
|
||||
);
|
||||
@@ -215,7 +219,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("Amount")
|
||||
egui::RichText::new(tr!("Amount", "Label for zap amount input field"))
|
||||
.text_style(NotedeckTextStyle::Heading3.text_style())
|
||||
.color(ui.visuals().noninteractive().text_color()),
|
||||
));
|
||||
@@ -398,11 +402,11 @@ impl Display for ZapSelectionButton {
|
||||
ZapSelectionButton::First => write!(f, "69"),
|
||||
ZapSelectionButton::Second => write!(f, "100"),
|
||||
ZapSelectionButton::Third => write!(f, "420"),
|
||||
ZapSelectionButton::Fourth => write!(f, "5K"),
|
||||
ZapSelectionButton::Fifth => write!(f, "10K"),
|
||||
ZapSelectionButton::Sixth => write!(f, "20K"),
|
||||
ZapSelectionButton::Seventh => write!(f, "50K"),
|
||||
ZapSelectionButton::Eighth => write!(f, "100K"),
|
||||
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.")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use notedeck_ui::{
|
||||
NoteOptions, ProfilePic,
|
||||
};
|
||||
|
||||
use notedeck::{name::get_display_name, supported_mime_hosted_at_url, NoteAction, NoteContext};
|
||||
use notedeck::{name::get_display_name, supported_mime_hosted_at_url, tr, NoteAction, NoteContext};
|
||||
use tracing::error;
|
||||
|
||||
pub struct PostView<'a, 'd> {
|
||||
@@ -180,7 +180,13 @@ impl<'a, 'd> PostView<'a, 'd> {
|
||||
};
|
||||
|
||||
let textedit = TextEdit::multiline(&mut self.draft.buffer)
|
||||
.hint_text(egui::RichText::new("Write a banger note here...").weak())
|
||||
.hint_text(
|
||||
egui::RichText::new(tr!(
|
||||
"Write a banger note here...",
|
||||
"Placeholder for note input field"
|
||||
))
|
||||
.weak(),
|
||||
)
|
||||
.frame(false)
|
||||
.desired_width(ui.available_width())
|
||||
.layouter(&mut layouter);
|
||||
@@ -605,7 +611,7 @@ fn render_post_view_media(
|
||||
|
||||
fn post_button(interactive: bool) -> impl egui::Widget {
|
||||
move |ui: &mut egui::Ui| {
|
||||
let button = egui::Button::new("Post now");
|
||||
let button = egui::Button::new(tr!("Post now", "Button label to post a note"));
|
||||
if interactive {
|
||||
ui.add(button)
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@ use core::f32;
|
||||
|
||||
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
|
||||
use enostr::ProfileState;
|
||||
use notedeck::{profile::unwrap_profile_url, Images, NotedeckTextStyle};
|
||||
use notedeck::{profile::unwrap_profile_url, tr, Images, NotedeckTextStyle};
|
||||
use notedeck_ui::{profile::banner, ProfilePic};
|
||||
|
||||
pub struct EditProfileView<'a> {
|
||||
@@ -32,7 +32,14 @@ impl<'a> EditProfileView<'a> {
|
||||
notedeck_ui::padding(padding, ui, |ui| {
|
||||
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui
|
||||
.add(button("Save changes", 119.0).fill(notedeck_ui::colors::PINK))
|
||||
.add(
|
||||
button(
|
||||
tr!("Save changes", "Button label to save profile changes")
|
||||
.as_str(),
|
||||
119.0,
|
||||
)
|
||||
.fill(notedeck_ui::colors::PINK),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
save = true;
|
||||
@@ -62,42 +69,66 @@ impl<'a> EditProfileView<'a> {
|
||||
);
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Display name"));
|
||||
ui.add(label(
|
||||
tr!("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("Username"));
|
||||
ui.add(label(
|
||||
tr!("Username", "Profile username field label").as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("name")));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Profile picture"));
|
||||
ui.add(label(
|
||||
tr!("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("Banner"));
|
||||
ui.add(label(
|
||||
tr!("Banner", "Profile banner URL field label").as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("banner")));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("About"));
|
||||
ui.add(label(
|
||||
tr!("About", "Profile about/bio field label").as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("about")));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Website"));
|
||||
ui.add(label(
|
||||
tr!("Website", "Profile website field label").as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("website")));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Lightning network address (lud16)"));
|
||||
ui.add(label(
|
||||
tr!(
|
||||
"Lightning network address (lud16)",
|
||||
"Bitcoin Lightning network address field label"
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(multiline_textedit(self.state.str_mut("lud16")));
|
||||
});
|
||||
|
||||
in_frame(ui, |ui| {
|
||||
ui.add(label("Nostr address (NIP-05 identity)"));
|
||||
ui.add(label(
|
||||
tr!(
|
||||
"Nostr address (NIP-05 identity)",
|
||||
"NIP-05 identity field label"
|
||||
)
|
||||
.as_str(),
|
||||
));
|
||||
ui.add(singleline_textedit(self.state.str_mut("nip05")));
|
||||
|
||||
let Some(nip05) = self.state.nip05() else {
|
||||
@@ -121,9 +152,18 @@ impl<'a> EditProfileView<'a> {
|
||||
ui.colored_label(
|
||||
ui.visuals().noninteractive().fg_stroke.color,
|
||||
RichText::new(if use_domain {
|
||||
format!("\"{suffix}\" will be used for identification")
|
||||
tr!(
|
||||
"\"{domain}\" will be used for identification",
|
||||
"Domain identification message",
|
||||
domain = suffix
|
||||
)
|
||||
} else {
|
||||
format!("\"{prefix}\" at \"{suffix}\" will be used for identification")
|
||||
tr!(
|
||||
"\"{username}\" at \"{domain}\" will be used for identification",
|
||||
"Username and domain identification message",
|
||||
username = prefix,
|
||||
domain = suffix
|
||||
)
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +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_ui::profile::follow_button;
|
||||
use tracing::error;
|
||||
|
||||
@@ -362,7 +363,7 @@ fn edit_profile_button() -> impl egui::Widget + 'static {
|
||||
|
||||
let edit_icon_size = vec2(16.0, 16.0);
|
||||
let galley = painter.layout(
|
||||
"Edit Profile".to_owned(),
|
||||
tr!("Edit Profile", "Button label to edit user profile"),
|
||||
NotedeckTextStyle::Button.get_font_id(ui.ctx()),
|
||||
ui.visuals().text_color(),
|
||||
rect.width(),
|
||||
|
||||
@@ -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::{NotedeckTextStyle, RelayAction};
|
||||
use notedeck::{tr, NotedeckTextStyle, RelayAction};
|
||||
use notedeck_ui::app_images;
|
||||
use notedeck_ui::{colors::PINK, padding};
|
||||
use tracing::debug;
|
||||
@@ -26,7 +26,7 @@ impl RelayView<'_> {
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
|
||||
ui.label(
|
||||
RichText::new("Relays")
|
||||
RichText::new(tr!("Relays", "Label for relay list section"))
|
||||
.text_style(NotedeckTextStyle::Heading2.text_style()),
|
||||
);
|
||||
});
|
||||
@@ -150,8 +150,11 @@ impl<'a> RelayView<'a> {
|
||||
let is_enabled = self.pool.is_valid_url(text_buffer);
|
||||
let text_edit = egui::TextEdit::singleline(text_buffer)
|
||||
.hint_text(
|
||||
RichText::new("Enter the relay here")
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
RichText::new(tr!(
|
||||
"Enter the relay here",
|
||||
"Placeholder for relay input field"
|
||||
))
|
||||
.text_style(NotedeckTextStyle::Body.text_style()),
|
||||
)
|
||||
.vertical_align(Align::Center)
|
||||
.desired_width(f32::INFINITY)
|
||||
@@ -175,7 +178,7 @@ impl<'a> RelayView<'a> {
|
||||
fn add_relay_button() -> Button<'static> {
|
||||
Button::image_and_text(
|
||||
app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)),
|
||||
RichText::new(" Add relay")
|
||||
RichText::new(tr!("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),
|
||||
@@ -185,7 +188,8 @@ fn add_relay_button() -> Button<'static> {
|
||||
|
||||
fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static {
|
||||
move |ui: &mut egui::Ui| -> egui::Response {
|
||||
let button_widget = styled_button("Add", notedeck_ui::colors::PINK);
|
||||
let add_text = tr!("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)
|
||||
}
|
||||
}
|
||||
@@ -224,9 +228,9 @@ 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 => "Connected",
|
||||
RelayStatus::Connecting => "Connecting...",
|
||||
RelayStatus::Disconnected => "Not Connected",
|
||||
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"),
|
||||
};
|
||||
|
||||
let frame = Frame::new()
|
||||
|
||||
@@ -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::{NoteAction, NoteContext, NoteRef};
|
||||
use notedeck::{tr, tr_plural, NoteAction, NoteContext, NoteRef};
|
||||
use notedeck_ui::{
|
||||
context_menu::{input_context, PasteBehavior},
|
||||
icons::search_icon,
|
||||
@@ -119,15 +119,21 @@ impl<'a, 'd> SearchView<'a, 'd> {
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
SearchState::Searched => {
|
||||
ui.label(format!(
|
||||
"Got {} results for '{}'",
|
||||
self.query.notes.notes.len(),
|
||||
&self.query.string
|
||||
ui.label(tr_plural!(
|
||||
"Got {count} result for '{query}'", // one
|
||||
"Got {count} results for '{query}'", // other
|
||||
"Search results count", // comment
|
||||
self.query.notes.notes.len(), // count
|
||||
query = &self.query.string
|
||||
));
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
SearchState::Typing(TypingType::AutoSearch) => {
|
||||
ui.label(format!("Searching for '{}'", &self.query.string));
|
||||
ui.label(tr!(
|
||||
"Searching for '{query}'",
|
||||
"Search in progress message",
|
||||
query = &self.query.string
|
||||
));
|
||||
|
||||
note_action = self.show_search_results(ui);
|
||||
}
|
||||
@@ -282,7 +288,13 @@ fn search_box(
|
||||
let response = ui.add_sized(
|
||||
[ui.available_width(), search_height],
|
||||
TextEdit::singleline(input)
|
||||
.hint_text(RichText::new("Search notes...").weak())
|
||||
.hint_text(
|
||||
RichText::new(tr!(
|
||||
"Search notes...",
|
||||
"Placeholder for search notes input field"
|
||||
))
|
||||
.weak(),
|
||||
)
|
||||
//.desired_width(available_width - 32.0)
|
||||
//.font(egui::FontId::new(font_size, egui::FontFamily::Proportional))
|
||||
.margin(vec2(0.0, 8.0))
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
route::Route,
|
||||
};
|
||||
|
||||
use notedeck::{Accounts, UserAccount};
|
||||
use notedeck::{tr, Accounts, UserAccount};
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
app_images, colors, View,
|
||||
@@ -105,7 +105,7 @@ impl<'a> DesktopSidePanel<'a> {
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.add(egui::Label::new(
|
||||
RichText::new("DECKS")
|
||||
RichText::new(tr!("DECKS", "Label for decks section in side panel"))
|
||||
.size(11.0)
|
||||
.color(ui.visuals().noninteractive().fg_stroke.color),
|
||||
));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use egui::{vec2, Button, Label, Layout, RichText};
|
||||
use notedeck::{NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck::{tr, NamedFontFamily, NotedeckTextStyle};
|
||||
use notedeck_ui::{colors::PINK, padding};
|
||||
use tracing::error;
|
||||
|
||||
@@ -21,10 +21,18 @@ impl<'a> SupportView<'a> {
|
||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
||||
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
|
||||
);
|
||||
ui.add(Label::new(RichText::new("Running into a bug?").font(font)));
|
||||
ui.label(RichText::new("Step 1").text_style(NotedeckTextStyle::Heading3.text_style()));
|
||||
ui.add(Label::new(
|
||||
RichText::new(tr!("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()),
|
||||
);
|
||||
padding(8.0, ui, |ui| {
|
||||
ui.label("Open your default email client to get help from the Damus team");
|
||||
ui.label(tr!(
|
||||
"Open your default email client to get help from the Damus team",
|
||||
"Instruction to open email client"
|
||||
));
|
||||
let size = vec2(120.0, 40.0);
|
||||
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
|
||||
let font_size =
|
||||
@@ -47,16 +55,19 @@ impl<'a> SupportView<'a> {
|
||||
|
||||
if let Some(logs) = self.support.get_most_recent_log() {
|
||||
ui.label(
|
||||
RichText::new("Step 2").text_style(NotedeckTextStyle::Heading3.text_style()),
|
||||
RichText::new(tr!("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("Copy").size(
|
||||
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
|
||||
))
|
||||
let copy_button = Button::new(
|
||||
RichText::new(tr!("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("Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email.").wrap());
|
||||
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.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());
|
||||
@@ -76,7 +87,9 @@ impl<'a> SupportView<'a> {
|
||||
}
|
||||
|
||||
fn open_email_button(font_size: f32, size: egui::Vec2) -> impl egui::Widget {
|
||||
Button::new(RichText::new("Open Email").size(font_size))
|
||||
.fill(PINK)
|
||||
.min_size(size)
|
||||
Button::new(
|
||||
RichText::new(tr!("Open Email", "Button label to open email client")).size(font_size),
|
||||
)
|
||||
.fill(PINK)
|
||||
.min_size(size)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ 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, NoteAction, NoteContext, ScrollInfo};
|
||||
use notedeck::{note::root_note_id_from_selected_id, tr, NoteAction, NoteContext, ScrollInfo};
|
||||
use notedeck_ui::{
|
||||
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
|
||||
NoteOptions, NoteView,
|
||||
@@ -281,17 +281,19 @@ 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 => "Notes",
|
||||
ViewFilter::NotesAndReplies => "Notes & Replies",
|
||||
ViewFilter::Notes => tr!("Notes", "Label for notes-only filter"),
|
||||
ViewFilter::NotesAndReplies => {
|
||||
tr!("Notes & Replies", "Label for notes and replies filter")
|
||||
}
|
||||
};
|
||||
|
||||
let res = ui.add(egui::Label::new(txt).selectable(false));
|
||||
let res = ui.add(egui::Label::new(txt.clone()).selectable(false));
|
||||
|
||||
// underline
|
||||
if state.is_selected() {
|
||||
let rect = res.rect;
|
||||
let underline =
|
||||
shrink_range_to_width(rect.x_range(), get_label_width(ui, txt) * 1.15);
|
||||
shrink_range_to_width(rect.x_range(), get_label_width(ui, &txt) * 1.15);
|
||||
#[allow(deprecated)]
|
||||
let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5;
|
||||
return (underline, underline_y);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::{vec2, CornerRadius, Layout};
|
||||
use notedeck::{
|
||||
get_current_wallet, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle,
|
||||
get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle,
|
||||
PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
|
||||
};
|
||||
|
||||
@@ -202,8 +202,11 @@ fn show_no_wallet(
|
||||
ui.horizontal_wrapped(|ui| 's: {
|
||||
let text_edit = egui::TextEdit::singleline(&mut state.buf)
|
||||
.hint_text(
|
||||
egui::RichText::new("Paste your NWC URI here...")
|
||||
.text_style(notedeck::NotedeckTextStyle::Body.text_style()),
|
||||
egui::RichText::new(tr!(
|
||||
"Paste your NWC URI here...",
|
||||
"Placeholder text for NWC URI input"
|
||||
))
|
||||
.text_style(notedeck::NotedeckTextStyle::Body.text_style()),
|
||||
)
|
||||
.vertical_align(egui::Align::Center)
|
||||
.desired_width(f32::INFINITY)
|
||||
@@ -218,8 +221,14 @@ fn show_no_wallet(
|
||||
};
|
||||
|
||||
let error_str = match error_msg {
|
||||
WalletError::InvalidURI => "Invalid NWC URI",
|
||||
WalletError::NoWallet => "Add a wallet to continue",
|
||||
WalletError::InvalidURI => tr!(
|
||||
"Invalid NWC URI",
|
||||
"Error message for invalid Nostr Wallet Connect URI"
|
||||
),
|
||||
WalletError::NoWallet => tr!(
|
||||
"Add a wallet to continue",
|
||||
"Error message for missing wallet"
|
||||
),
|
||||
};
|
||||
ui.colored_label(ui.visuals().warn_fg_color, error_str);
|
||||
});
|
||||
@@ -229,15 +238,21 @@ fn show_no_wallet(
|
||||
if show_local_only {
|
||||
ui.checkbox(
|
||||
&mut state.for_local_only,
|
||||
"Use this wallet for the current account only",
|
||||
tr!(
|
||||
"Use this wallet for the current account only",
|
||||
"Checkbox label for using wallet only for current account"
|
||||
),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
|
||||
ui.add(styled_button("Add Wallet", notedeck_ui::colors::PINK))
|
||||
.clicked()
|
||||
.then_some(WalletAction::SaveURI)
|
||||
ui.add(styled_button(
|
||||
tr!("Add Wallet", "Button label to add a wallet").as_str(),
|
||||
notedeck_ui::colors::PINK,
|
||||
))
|
||||
.clicked()
|
||||
.then_some(WalletAction::SaveURI)
|
||||
})
|
||||
.inner
|
||||
}
|
||||
@@ -268,7 +283,10 @@ fn show_with_wallet(
|
||||
|
||||
ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: {
|
||||
if ui
|
||||
.add(styled_button("Delete Wallet", ui.visuals().window_fill))
|
||||
.add(styled_button(
|
||||
tr!("Delete Wallet", "Button label to delete a wallet").as_str(),
|
||||
ui.visuals().window_fill,
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
action = Some(WalletAction::Delete);
|
||||
@@ -280,7 +298,10 @@ fn show_with_wallet(
|
||||
&& ui
|
||||
.checkbox(
|
||||
&mut false,
|
||||
"Add a different wallet that will only be used for this account",
|
||||
tr!(
|
||||
"Add a different wallet that will only be used for this account",
|
||||
"Button label to add a different wallet"
|
||||
),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
@@ -308,7 +329,7 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
|
||||
vec2(ui.available_width(), 50.0),
|
||||
egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|
||||
|ui| {
|
||||
ui.label("Default amount per zap: ");
|
||||
ui.label(tr!("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;
|
||||
@@ -340,10 +361,10 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
|
||||
|
||||
ui.memory_mut(|m| m.request_focus(id));
|
||||
|
||||
ui.label(" sats");
|
||||
ui.label(tr!("sats", "Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings."));
|
||||
|
||||
if ui
|
||||
.add(styled_button("Save", ui.visuals().widgets.active.bg_fill))
|
||||
.add(styled_button(tr!("Save", "Button to save default zap amount").as_str(), ui.visuals().widgets.active.bg_fill))
|
||||
.clicked()
|
||||
{
|
||||
action = Some(WalletAction::SetDefaultZapSats(text.to_string()));
|
||||
@@ -353,14 +374,14 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
|
||||
if let Some(wallet_action) = show_valid_msats(ui, **msats) {
|
||||
action = Some(wallet_action);
|
||||
}
|
||||
ui.label(" sats");
|
||||
ui.label(tr!("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 => "Invalid amount",
|
||||
notedeck::DefaultZapError::InvalidUserInput => tr!("Invalid amount", "Error message for invalid zap amount"),
|
||||
};
|
||||
|
||||
ui.colored_label(ui.visuals().warn_fg_color, msg_str);
|
||||
@@ -388,7 +409,7 @@ 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("Click to edit");
|
||||
.on_hover_text_at_pointer(tr!("Click to edit", "Hover text for editable zap amount"));
|
||||
|
||||
let painter = ui.painter_at(resp.rect);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
};
|
||||
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
|
||||
use nostrdb::{Ndb, Transaction};
|
||||
use notedeck::{Accounts, AppContext, Images, NoteAction, NoteContext};
|
||||
use notedeck::{tr, Accounts, AppContext, Images, 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
|
||||
@@ -138,7 +138,7 @@ impl<'a> DaveUi<'a> {
|
||||
if self.trial {
|
||||
ui.add(egui::Label::new(
|
||||
egui::RichText::new(
|
||||
"The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon!",
|
||||
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"),
|
||||
)
|
||||
.weak(),
|
||||
));
|
||||
@@ -308,7 +308,13 @@ impl<'a> DaveUi<'a> {
|
||||
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("Ask")).clicked() {
|
||||
if ui
|
||||
.add(egui::Button::new(tr!(
|
||||
"Ask",
|
||||
"Button to send message to Dave AI assistant"
|
||||
)))
|
||||
.clicked()
|
||||
{
|
||||
dave_response = DaveResponse::send();
|
||||
}
|
||||
|
||||
@@ -322,7 +328,13 @@ impl<'a> DaveUi<'a> {
|
||||
},
|
||||
Key::Enter,
|
||||
))
|
||||
.hint_text(egui::RichText::new("Ask dave anything...").weak())
|
||||
.hint_text(
|
||||
egui::RichText::new(tr!(
|
||||
"Ask dave anything...",
|
||||
"Placeholder text for Dave AI input field"
|
||||
))
|
||||
.weak(),
|
||||
)
|
||||
.frame(false),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::{Rect, Vec2};
|
||||
use nostrdb::NoteKey;
|
||||
use notedeck::{BroadcastContext, NoteContextSelection};
|
||||
use notedeck::{tr, BroadcastContext, NoteContextSelection};
|
||||
|
||||
pub struct NoteContextButton {
|
||||
put_at: Option<Rect>,
|
||||
@@ -109,31 +109,78 @@ impl NoteContextButton {
|
||||
) -> 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);
|
||||
if ui.button("Copy text").clicked() {
|
||||
|
||||
// Debug: Check what the tr! macro returns
|
||||
let copy_text = tr!(
|
||||
"Copy Text",
|
||||
"Copy the text content of the note to clipboard"
|
||||
);
|
||||
tracing::debug!("Copy Text translation: '{}'", copy_text);
|
||||
|
||||
if ui.button(copy_text).clicked() {
|
||||
context_selection = Some(NoteContextSelection::CopyText);
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Copy user public key").clicked() {
|
||||
if ui
|
||||
.button(tr!(
|
||||
"Copy Pubkey",
|
||||
"Copy the author's public key to clipboard"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
context_selection = Some(NoteContextSelection::CopyPubkey);
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Copy note id").clicked() {
|
||||
if ui
|
||||
.button(tr!(
|
||||
"Copy Note ID",
|
||||
"Copy the unique note identifier to clipboard"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
context_selection = Some(NoteContextSelection::CopyNoteId);
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Copy note json").clicked() {
|
||||
if ui
|
||||
.button(tr!(
|
||||
"Copy Note JSON",
|
||||
"Copy the raw note data in JSON format to clipboard"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
context_selection = Some(NoteContextSelection::CopyNoteJSON);
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Broadcast").clicked() {
|
||||
if ui
|
||||
.button(tr!(
|
||||
"Broadcast",
|
||||
"Broadcast the note to all connected relays"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
context_selection = Some(NoteContextSelection::Broadcast(
|
||||
BroadcastContext::Everywhere,
|
||||
));
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("Broadcast to local network").clicked() {
|
||||
if ui
|
||||
.button(tr!(
|
||||
"Broadcast Local",
|
||||
"Broadcast the note only to local network relays"
|
||||
))
|
||||
.clicked()
|
||||
{
|
||||
context_selection = Some(NoteContextSelection::Broadcast(
|
||||
BroadcastContext::LocalNetwork,
|
||||
));
|
||||
|
||||
@@ -6,7 +6,7 @@ use egui::{
|
||||
};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url,
|
||||
GifState, GifStateMap, Images, JobPool, MediaCache, MediaCacheType, NotedeckTextStyle,
|
||||
tr, GifState, GifStateMap, Images, JobPool, MediaCache, MediaCacheType, NotedeckTextStyle,
|
||||
TexturedImage, TexturesCache, UrlMimes,
|
||||
};
|
||||
|
||||
@@ -636,7 +636,10 @@ fn render_full_screen_media(
|
||||
|
||||
fn copy_link(url: &str, img_resp: &Response) {
|
||||
img_resp.context_menu(|ui| {
|
||||
if ui.button("Copy Link").clicked() {
|
||||
if ui
|
||||
.button(tr!("Copy Link", "Button to copy media link to clipboard"))
|
||||
.clicked()
|
||||
{
|
||||
ui.ctx().copy_text(url.to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
@@ -722,14 +725,18 @@ fn render_blur_text(ui: &mut egui::Ui, url: &str, render_rect: egui::Rect) -> eg
|
||||
text_style.font_family(),
|
||||
);
|
||||
let info_galley = painter.layout(
|
||||
"Media from someone you don't follow".to_owned(),
|
||||
tr!(
|
||||
"Media from someone you don't follow",
|
||||
"Text shown on blurred media from unfollowed users"
|
||||
)
|
||||
.to_owned(),
|
||||
animation_fontid.clone(),
|
||||
ui.visuals().text_color(),
|
||||
render_rect.width() / 2.0,
|
||||
);
|
||||
|
||||
let load_galley = painter.layout_no_wrap(
|
||||
"Tap to Load".to_owned(),
|
||||
tr!("Tap to Load", "Button text to load blurred media").to_owned(),
|
||||
animation_fontid,
|
||||
egui::Color32::BLACK,
|
||||
// ui.visuals().widgets.inactive.bg_fill,
|
||||
|
||||
@@ -27,7 +27,7 @@ use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
|
||||
use notedeck::{
|
||||
name::get_display_name,
|
||||
note::{NoteAction, NoteContext, ZapAction},
|
||||
AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned,
|
||||
tr, AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned,
|
||||
NotedeckTextStyle, ZapTarget, Zaps,
|
||||
};
|
||||
|
||||
@@ -308,7 +308,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
|
||||
let color = ui.style().visuals.noninteractive().fg_stroke.color;
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new("Reposted")
|
||||
RichText::new(tr!("Reposted", "Label for reposted notes"))
|
||||
.color(color)
|
||||
.text_style(style.text_style()),
|
||||
);
|
||||
@@ -864,7 +864,7 @@ fn reply_button(ui: &mut egui::Ui, note_key: NoteKey) -> egui::Response {
|
||||
|
||||
let put_resp = ui
|
||||
.put(rect, img.max_width(size))
|
||||
.on_hover_text("Reply to this note");
|
||||
.on_hover_text(tr!("Reply to this note", "Hover text for reply button"));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
@@ -889,7 +889,7 @@ 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("Repost this note");
|
||||
.on_hover_text(tr!("Repost this note", "Hover text for repost button"));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
@@ -927,7 +927,9 @@ 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("Zap this note");
|
||||
let put_resp = ui
|
||||
.put(rect, img)
|
||||
.on_hover_text(tr!("Zap this note", "Hover text for zap button"));
|
||||
|
||||
resp.union(put_resp)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,190 @@
|
||||
use egui::{Label, RichText, Sense};
|
||||
use nostrdb::{Note, NoteReply, Transaction};
|
||||
use nostrdb::{NoteReply, Transaction};
|
||||
|
||||
use super::NoteOptions;
|
||||
use crate::{jobs::JobsCache, note::NoteView, Mention};
|
||||
use notedeck::{NoteAction, NoteContext};
|
||||
use notedeck::{tr, NoteAction, NoteContext};
|
||||
|
||||
// Rich text segment types for internationalized rendering
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TextSegment {
|
||||
Plain(String),
|
||||
UserMention([u8; 32]), // pubkey
|
||||
ThreadUserMention([u8; 32]), // pubkey
|
||||
NoteLink([u8; 32]),
|
||||
ThreadLink([u8; 32]),
|
||||
}
|
||||
|
||||
// Helper function to parse i18n template strings with placeholders
|
||||
fn parse_i18n_template(template: &str) -> Vec<TextSegment> {
|
||||
let mut segments = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut chars = template.chars().peekable();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '{' {
|
||||
// Save any accumulated plain text
|
||||
if !current_text.is_empty() {
|
||||
segments.push(TextSegment::Plain(current_text.clone()));
|
||||
current_text.clear();
|
||||
}
|
||||
|
||||
// Parse placeholder
|
||||
let mut placeholder = String::new();
|
||||
for ch in chars.by_ref() {
|
||||
if ch == '}' {
|
||||
break;
|
||||
}
|
||||
placeholder.push(ch);
|
||||
}
|
||||
|
||||
// Handle different placeholder types
|
||||
match placeholder.as_str() {
|
||||
// Placeholder values will be filled later.
|
||||
"user" => segments.push(TextSegment::UserMention([0; 32])),
|
||||
"thread_user" => segments.push(TextSegment::ThreadUserMention([0; 32])),
|
||||
"note" => segments.push(TextSegment::NoteLink([0; 32])),
|
||||
"thread" => segments.push(TextSegment::ThreadLink([0; 32])),
|
||||
_ => {
|
||||
// Unknown placeholder, treat as plain text
|
||||
current_text.push_str(&format!("{{{placeholder}}}"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_text.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining plain text
|
||||
if !current_text.is_empty() {
|
||||
segments.push(TextSegment::Plain(current_text));
|
||||
}
|
||||
|
||||
segments
|
||||
}
|
||||
|
||||
// Helper function to fill in the actual data for placeholders
|
||||
fn fill_template_data(
|
||||
mut segments: Vec<TextSegment>,
|
||||
reply_pubkey: &[u8; 32],
|
||||
reply_note_id: &[u8; 32],
|
||||
root_pubkey: Option<&[u8; 32]>,
|
||||
root_note_id: Option<&[u8; 32]>,
|
||||
) -> Vec<TextSegment> {
|
||||
for segment in &mut segments {
|
||||
match segment {
|
||||
TextSegment::UserMention(pubkey) if *pubkey == [0; 32] => {
|
||||
*pubkey = *reply_pubkey;
|
||||
}
|
||||
TextSegment::ThreadUserMention(pubkey) if *pubkey == [0; 32] => {
|
||||
*pubkey = *root_pubkey.unwrap_or(reply_pubkey);
|
||||
}
|
||||
TextSegment::NoteLink(note_id) if *note_id == [0; 32] => {
|
||||
*note_id = *reply_note_id;
|
||||
}
|
||||
TextSegment::ThreadLink(note_id) if *note_id == [0; 32] => {
|
||||
*note_id = *root_note_id.unwrap_or(reply_note_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
segments
|
||||
}
|
||||
|
||||
// Main rendering function for text segments
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_text_segments(
|
||||
ui: &mut egui::Ui,
|
||||
segments: &[TextSegment],
|
||||
txn: &Transaction,
|
||||
note_context: &mut NoteContext,
|
||||
note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
size: f32,
|
||||
selectable: bool,
|
||||
) -> Option<NoteAction> {
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
let visuals = ui.visuals();
|
||||
let color = visuals.noninteractive().fg_stroke.color;
|
||||
let link_color = visuals.hyperlink_color;
|
||||
|
||||
for segment in segments {
|
||||
match segment {
|
||||
TextSegment::Plain(text) => {
|
||||
ui.add(
|
||||
Label::new(RichText::new(text).size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
}
|
||||
TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => {
|
||||
let action = Mention::new(note_context.ndb, note_context.img_cache, txn, pubkey)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
}
|
||||
TextSegment::NoteLink(note_id) => {
|
||||
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),
|
||||
)
|
||||
.sense(Sense::click())
|
||||
.selectable(selectable),
|
||||
);
|
||||
|
||||
if r.clicked() {
|
||||
// TODO: jump to note
|
||||
}
|
||||
|
||||
if r.hovered() {
|
||||
r.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(400.0);
|
||||
NoteView::new(note_context, ¬e, note_options, jobs)
|
||||
.actionbar(false)
|
||||
.wide(true)
|
||||
.show(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TextSegment::ThreadLink(note_id) => {
|
||||
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),
|
||||
)
|
||||
.sense(Sense::click())
|
||||
.selectable(selectable),
|
||||
);
|
||||
|
||||
if r.clicked() {
|
||||
// TODO: jump to note
|
||||
}
|
||||
|
||||
if r.hovered() {
|
||||
r.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(400.0);
|
||||
NoteView::new(note_context, ¬e, note_options, jobs)
|
||||
.actionbar(false)
|
||||
.wide(true)
|
||||
.show(ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
note_action
|
||||
}
|
||||
|
||||
#[must_use = "Please handle the resulting note action"]
|
||||
#[profiling::function]
|
||||
@@ -15,163 +196,109 @@ pub fn reply_desc(
|
||||
note_options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
) -> Option<NoteAction> {
|
||||
let mut note_action: Option<NoteAction> = None;
|
||||
let size = 10.0;
|
||||
let selectable = false;
|
||||
let visuals = ui.visuals();
|
||||
let color = visuals.noninteractive().fg_stroke.color;
|
||||
let link_color = visuals.hyperlink_color;
|
||||
|
||||
// note link renderer helper
|
||||
let note_link = |ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
text: &str,
|
||||
note: &Note<'_>,
|
||||
jobs: &mut JobsCache| {
|
||||
let r = ui.add(
|
||||
Label::new(RichText::new(text).size(size).color(link_color))
|
||||
.sense(Sense::click())
|
||||
.selectable(selectable),
|
||||
);
|
||||
|
||||
if r.clicked() {
|
||||
// TODO: jump to note
|
||||
}
|
||||
|
||||
if r.hovered() {
|
||||
r.on_hover_ui_at_pointer(|ui| {
|
||||
ui.set_max_width(400.0);
|
||||
NoteView::new(note_context, note, note_options, jobs)
|
||||
.actionbar(false)
|
||||
.wide(true)
|
||||
.show(ui);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ui.add(Label::new(RichText::new("replying to").size(size).color(color)).selectable(selectable));
|
||||
|
||||
let reply = note_reply.reply()?;
|
||||
|
||||
let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) {
|
||||
reply_note
|
||||
} else {
|
||||
ui.add(Label::new(RichText::new("a note").size(size).color(color)).selectable(selectable));
|
||||
return None;
|
||||
// Handle case where reply note is not found
|
||||
let template = tr!(
|
||||
"replying to a note",
|
||||
"Fallback text when reply note is not found"
|
||||
);
|
||||
let segments = parse_i18n_template(&template);
|
||||
return render_text_segments(
|
||||
ui,
|
||||
&segments,
|
||||
txn,
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
size,
|
||||
selectable,
|
||||
);
|
||||
};
|
||||
|
||||
if note_reply.is_reply_to_root() {
|
||||
// We're replying to the root, let's show this
|
||||
let action = Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
let segments = if note_reply.is_reply_to_root() {
|
||||
// Template: "replying to {user}'s {thread}"
|
||||
let template = tr!(
|
||||
"replying to {user}'s {thread}",
|
||||
"Template for replying to root thread",
|
||||
user = "{user}",
|
||||
thread = "{thread}"
|
||||
);
|
||||
let segments = parse_i18n_template(&template);
|
||||
fill_template_data(
|
||||
segments,
|
||||
reply_note.pubkey(),
|
||||
reply.id,
|
||||
None,
|
||||
Some(reply.id),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable));
|
||||
|
||||
note_link(ui, note_context, "thread", &reply_note, jobs);
|
||||
} else if let Some(root) = note_reply.root() {
|
||||
// replying to another post in a thread, not the root
|
||||
|
||||
if let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) {
|
||||
if root_note.pubkey() == reply_note.pubkey() {
|
||||
// simply "replying to bob's note" when replying to bob in his thread
|
||||
let action = Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
// Template: "replying to {user}'s {note}"
|
||||
let template = tr!(
|
||||
"replying to {user}'s {note}",
|
||||
"Template for replying to user's note",
|
||||
user = "{user}",
|
||||
note = "{note}"
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "note", &reply_note, jobs);
|
||||
let segments = parse_i18n_template(&template);
|
||||
fill_template_data(segments, reply_note.pubkey(), reply.id, None, None)
|
||||
} else {
|
||||
// replying to bob in alice's thread
|
||||
|
||||
let action = Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
// Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}"
|
||||
// This would need more sophisticated placeholder handling
|
||||
let template = tr!(
|
||||
"replying to {user}'s {note} in {thread_user}'s {thread}",
|
||||
"Template for replying to note in different user's thread",
|
||||
user = "{user}",
|
||||
note = "{note}",
|
||||
thread_user = "{thread_user}",
|
||||
thread = "{thread}"
|
||||
);
|
||||
let segments = parse_i18n_template(&template);
|
||||
fill_template_data(
|
||||
segments,
|
||||
reply_note.pubkey(),
|
||||
reply.id,
|
||||
Some(root_note.pubkey()),
|
||||
Some(root.id),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "note", &reply_note, jobs);
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("in").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
let action = Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
root_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
|
||||
);
|
||||
|
||||
note_link(ui, note_context, "thread", &root_note, jobs);
|
||||
}
|
||||
} else {
|
||||
let action = Mention::new(
|
||||
note_context.ndb,
|
||||
note_context.img_cache,
|
||||
txn,
|
||||
reply_note.pubkey(),
|
||||
)
|
||||
.size(size)
|
||||
.selectable(selectable)
|
||||
.show(ui);
|
||||
|
||||
if action.is_some() {
|
||||
note_action = action;
|
||||
}
|
||||
|
||||
ui.add(
|
||||
Label::new(RichText::new("in someone's thread").size(size).color(color))
|
||||
.selectable(selectable),
|
||||
// Template: "replying to {user} in someone's thread"
|
||||
let template = tr!(
|
||||
"replying to {user} in someone's thread",
|
||||
"Template for replying to user in unknown thread",
|
||||
user = "{user}"
|
||||
);
|
||||
let segments = parse_i18n_template(&template);
|
||||
fill_template_data(segments, reply_note.pubkey(), reply.id, None, None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback
|
||||
let template = tr!(
|
||||
"replying to {user}",
|
||||
"Fallback template for replying to user",
|
||||
user = "{user}"
|
||||
);
|
||||
let segments = parse_i18n_template(&template);
|
||||
fill_template_data(segments, reply_note.pubkey(), reply.id, None, None)
|
||||
};
|
||||
|
||||
note_action
|
||||
render_text_segments(
|
||||
ui,
|
||||
&segments,
|
||||
txn,
|
||||
note_context,
|
||||
note_options,
|
||||
jobs,
|
||||
size,
|
||||
selectable,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use egui::{Frame, Label, RichText};
|
||||
use egui_extras::Size;
|
||||
use nostrdb::ProfileRecord;
|
||||
|
||||
use notedeck::{name::get_display_name, profile::get_profile_url, Images, NotedeckTextStyle};
|
||||
use notedeck::{name::get_display_name, profile::get_profile_url, tr, Images, NotedeckTextStyle};
|
||||
|
||||
use super::{about_section_widget, banner, display_name_widget};
|
||||
|
||||
@@ -96,7 +96,7 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> {
|
||||
if !self.is_nsec {
|
||||
ui.add(
|
||||
Label::new(
|
||||
RichText::new("Read only")
|
||||
RichText::new(tr!("Read only", "Label for read-only profile mode"))
|
||||
.size(notedeck::fonts::get_font_size(
|
||||
ui.ctx(),
|
||||
&NotedeckTextStyle::Tiny,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use egui::{Color32, RichText, Widget};
|
||||
use nostrdb::ProfileRecord;
|
||||
use notedeck::fonts::NamedFontFamily;
|
||||
use notedeck::{fonts::NamedFontFamily, tr};
|
||||
|
||||
pub struct Username<'a> {
|
||||
profile: Option<&'a ProfileRecord<'a>>,
|
||||
@@ -52,7 +52,11 @@ impl Widget for Username<'_> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut txt = RichText::new("nostrich").family(NamedFontFamily::Medium.as_family());
|
||||
let mut txt = RichText::new(tr!(
|
||||
"nostrich",
|
||||
"Default username when profile is not available"
|
||||
))
|
||||
.family(NamedFontFamily::Medium.as_family());
|
||||
if let Some(col) = color {
|
||||
txt = txt.color(col)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user