notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 3f5036bd325b89010556fd7373dc86271b57d572
parent d07c3e913530e1c21fa8e837fcdac3e782b18316
Author: Terry Yiu <git@tyiu.xyz>
Date:   Thu, 26 Jun 2025 23:13:31 -0400

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>

Diffstat:
Aassets/translations/en-US/main.ftl | 611+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aassets/translations/en-XA/main.ftl | 611+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/app.rs | 2+-
Mcrates/notedeck/src/i18n/manager.rs | 24++++++++++++------------
Mcrates/notedeck/src/i18n/mod.rs | 6+++---
Mcrates/notedeck/src/wallet.rs | 4++--
Mcrates/notedeck_chrome/src/chrome.rs | 19+++++++++++++------
Mcrates/notedeck_columns/src/app.rs | 5+++--
Mcrates/notedeck_columns/src/decks.rs | 6+++---
Mcrates/notedeck_columns/src/login_manager.rs | 4+++-
Mcrates/notedeck_columns/src/nav.rs | 19++++++++++++++-----
Mcrates/notedeck_columns/src/route.rs | 207++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/notedeck_columns/src/timeline/kind.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcrates/notedeck_columns/src/timeline/mod.rs | 12+++++++-----
Mcrates/notedeck_columns/src/ui/account_login_view.rs | 20++++++++++++--------
Mcrates/notedeck_columns/src/ui/accounts.rs | 9++++++---
Mcrates/notedeck_columns/src/ui/add_column.rs | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/notedeck_columns/src/ui/column/header.rs | 17+++++++++++++----
Mcrates/notedeck_columns/src/ui/configure_deck.rs | 74+++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/notedeck_columns/src/ui/edit_deck.rs | 8++++----
Mcrates/notedeck_columns/src/ui/note/custom_zap.rs | 24++++++++++++++----------
Mcrates/notedeck_columns/src/ui/note/post.rs | 12+++++++++---
Mcrates/notedeck_columns/src/ui/profile/edit.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 3++-
Mcrates/notedeck_columns/src/ui/relay.rs | 22+++++++++++++---------
Mcrates/notedeck_columns/src/ui/search/mod.rs | 26+++++++++++++++++++-------
Mcrates/notedeck_columns/src/ui/side_panel.rs | 4++--
Mcrates/notedeck_columns/src/ui/support.rs | 37+++++++++++++++++++++++++------------
Mcrates/notedeck_columns/src/ui/timeline.rs | 12+++++++-----
Mcrates/notedeck_columns/src/ui/wallet.rs | 55++++++++++++++++++++++++++++++++++++++-----------------
Mcrates/notedeck_dave/src/ui/dave.rs | 20++++++++++++++++----
Mcrates/notedeck_ui/src/note/context.rs | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/notedeck_ui/src/note/media.rs | 15+++++++++++----
Mcrates/notedeck_ui/src/note/mod.rs | 12+++++++-----
Mcrates/notedeck_ui/src/note/reply_description.rs | 395++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/notedeck_ui/src/profile/preview.rs | 4++--
Mcrates/notedeck_ui/src/username.rs | 8++++++--
37 files changed, 2195 insertions(+), 434 deletions(-)

diff --git a/assets/translations/en-US/main.ftl b/assets/translations/en-US/main.ftl @@ -0,0 +1,611 @@ +# Main translation file for Notedeck +# This file contains common UI strings used throughout the application +# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY + +# Regular strings + +# Profile about/bio field label +About_00c0 = About + +# Display name for account management +Accounts_e233 = Accounts + +# Column title for account management +Accounts_f018 = Accounts + +# Button label to add a relay +Add_269d = Add + +# Label for add column button +Add_47df = Add + +# Button label to add a different wallet +Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = Add a different wallet that will only be used for this account + +# Error message for missing wallet +Add_a_wallet_to_continue_d170 = Add a wallet to continue + +# Button label to add a new account +Add_account_1cfc = Add account + +# Column title for adding new account +Add_Account_d06c = Add Account + +# Display name for adding account +Add_Account_d715 = Add Account + +# Column title for adding algorithm column +Add_Algo_Column_0d75 = Add Algo Column + +# Display name for adding column +Add_Column_c6ff = Add Column + +# Column title for adding new column +Add_Column_c764 = Add Column + +# Display name for adding deck +Add_Deck_6e5f = Add Deck + +# Column title for adding new deck +Add_Deck_fabf = Add Deck + +# Column title for adding external notifications column +Add_External_Notifications_Column_41ae = Add External Notifications Column + +# Column title for adding hashtag column +Add_Hashtag_Column_ebf4 = Add Hashtag Column + +# Column title for adding last notes column +Add_Last_Notes_Column_bbad = Add Last Notes Column + +# Column title for adding notifications column +Add_Notifications_Column_79f8 = Add Notifications Column + +# Button label to add a relay +Add_relay_269d = Add relay + +# Button label to add a wallet +Add_Wallet_d1be = Add Wallet + +# Title for algorithmic feeds column +Algo_2452 = Algo + +# Description for algorithmic feeds column +Algorithmic_feeds_to_aid_in_note_discovery_d344 = Algorithmic feeds to aid in note discovery + +# Label for zap amount input field +Amount_70f0 = Amount + +# Button to send message to Dave AI assistant +Ask_b7f4 = Ask + +# Placeholder text for Dave AI input field +Ask_dave_anything_33d1 = Ask dave anything... + +# Profile banner URL field label +Banner_52ef = Banner + +# Beta version label +BETA_8e5d = BETA + +# Broadcast the note to all connected relays +Broadcast_fe43 = Broadcast + +# Broadcast the note only to local network relays +Broadcast_Local_7e50 = Broadcast Local + +# Button label to cancel an action +Cancel_ed3b = Cancel + +# Hover text for editable zap amount +Click_to_edit_0414 = Click to edit + +# Display name for note composition +Compose_Note_ad11 = Compose Note + +# Column title for note composition +Compose_Note_c094 = Compose Note + +# Button label to confirm an action +Confirm_f8a6 = Confirm + +# Status label for connected relay +Connected_f8cc = Connected + +# Status label for connecting relay +Connecting_6b7e = Connecting... + +# Title for contact list column +Contact_List_f85a = Contact List + +# Column title for contact lists +Contacts_7533 = Contacts + +# Timeline kind label for contact lists +Contacts_8b98 = Contacts + +# Column title for last notes per contact +Contacts__last_notes_3f84 = Contacts (last notes) + +# Button label to copy logs +Copy_a688 = Copy + +# Button to copy media link to clipboard +Copy_Link_dc7c = Copy Link + +# Copy the unique note identifier to clipboard +Copy_Note_ID_6b45 = Copy Note ID + +# Copy the raw note data in JSON format to clipboard +Copy_Note_JSON_9e4e = Copy Note JSON + +# Copy the author's public key to clipboard +Copy_Pubkey_9cc4 = Copy Pubkey + +# Copy the text content of the note to clipboard +Copy_Text_f81c = Copy Text + +# Button to create a new account +Create_Account_6994 = Create Account + +# Button label to create a new deck +Create_Deck_16b7 = Create Deck + +# Column title for custom timelines +Custom_a69e = Custom + +# Display name for custom timelines +Custom_cb4f = Custom + +# Column title for zap amount customization +Customize_Zap_Amount_cfc4 = Customize Zap Amount + +# Display name for zap customization +Customize_Zap_Amount_ed29 = Customize Zap Amount + +# Column title for support page +Damus_Support_27c0 = Damus Support + +# Label for deck name input field +Deck_name_cd32 = Deck name + +# Label for decks section in side panel +DECKS_1fad = DECKS + +# Label for default zap amount input +Default_amount_per_zap_399d = Default amount per zap: + +# Name of the default deck feed +Default_Deck_fcca = Default Deck + +# Button label to delete a deck +Delete_Deck_bb29 = Delete Deck + +# Tooltip for deleting a column +Delete_this_column_8d5a = Delete this column + +# Button label to delete a wallet +Delete_Wallet_d1d4 = Delete Wallet + +# Profile display name field label +Display_name_f9d9 = Display name + +# Domain identification message +domain___will_be_used_for_identification_b67e = "{$domain}" will be used for identification + +# Column title for editing deck +Edit_Deck_4018 = Edit Deck + +# Display name for editing deck +Edit_Deck_c9ba = Edit Deck + +# Button label to edit a deck +Edit_Deck_fd93 = Edit Deck + +# Button label to edit user profile +Edit_Profile_49e6 = Edit Profile + +# Display name for profile editing +Edit_Profile_6699 = Edit Profile + +# Column title for profile editing +Edit_Profile_8ad4 = Edit Profile + +# Placeholder for hashtag input field +Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = Enter the desired hashtags here (for multiple space-separated) + +# Placeholder for relay input field +Enter_the_relay_here_1c8b = Enter the relay here + +# Hint text to prompt entering the user's public key. +Enter_the_user_s_key__npub__hex__nip05__here_650c = Enter the user's key (npub, hex, nip05) here... + +# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05). +Enter_your_key_0fca = Enter your key + +# Instructions for entering Nostr credentials +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_48e9 = 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. + +# Label for find user button +Find_User_bd12 = Find User + +# Timeline kind label for hashtag feeds +Hashtag_a0ab = Hashtag + +# Display name for hashtag feeds +Hashtags_617e = Hashtags + +# Title for hashtags column +Hashtags_f8e0 = Hashtags + +# Display name for home feed +Home_3efc = Home + +# Title for Home column +Home_8c19 = Home + +# Label for deck icon selection +Icon_b0ab = Icon + +# Title for individual user column +Individual_b776 = Individual + +# Error message for invalid zap amount +Invalid_amount_6630 = Invalid amount + +# Error message for invalid key input +Invalid_key_4726 = Invalid key. + +# Error message for invalid Nostr Wallet Connect URI +Invalid_NWC_URI_031b = Invalid NWC URI + +# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount. +k_100K_686c = 100K + +# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount. +k_10K_f7e6 = 10K + +# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount. +k_20K_4977 = 20K + +# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount. +k_50K_c2dc = 50K + +# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount. +k_5K_f7e6 = 5K + +# Description for your notes column +Keep_track_of_your_notes___replies_a334 = Keep track of your notes & replies + +# Title for last note per user column +Last_Note_per_User_17ad = Last Note per User + +# Timeline kind label for last notes per pubkey +Last_Notes_aefe = Last Notes + +# Display name for last notes per contact +Last_Per_Pubkey__Contact_33ce = Last Per Pubkey (Contact) + +# Bitcoin Lightning network address field label +Lightning_network_address__lud16_ea51 = Lightning network address (lud16) + +# Login page title +Login_9eef = Login + +# Login button text +Login_now___let_s_do_this_5630 = Login now — let's do this! + +# Text shown on blurred media from unfollowed users +Media_from_someone_you_don_t_follow_5611 = Media from someone you don't follow + +# Tooltip for moving a column +Moves_this_column_to_another_position_0d4b = Moves this column to another position + +# Title for the user's deck +My_Deck_4ac5 = My Deck + +# Label asking if the user is new to Nostr. Underneath this label is a button to create an account. +New_to_Nostr_a2fd = New to Nostr? + +# NIP-05 identity field label +Nostr_address__NIP-05_identity_74a2 = Nostr address (NIP-05 identity) + +# Default username when profile is not available +nostrich_df29 = nostrich + +# Status label for disconnected relay +Not_Connected_6292 = Not Connected + +# Link text for note references +note_cad6 = note + +# Beta product warning message +Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = Notedeck is a beta product. Expect bugs and contact us when you run into issues. + +# Filter label for notes only view +Notes_03fb = Notes + +# Label for notes-only filter +Notes_60d2 = Notes + +# Filter label for notes and replies view +Notes___Replies_1ec2 = Notes & Replies + +# Label for notes and replies filter +Notes___Replies_6e3b = Notes & Replies + +# Timeline kind label for notifications +Notifications_6228 = Notifications + +# Display name for notifications +Notifications_8029 = Notifications + +# Column title for notifications +Notifications_d673 = Notifications + +# Title for notifications column +Notifications_ef56 = Notifications + +# Button label to open email client +Open_Email_25e9 = Open Email + +# Instruction to open email client +Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = Open your default email client to get help from the Damus team + +# Placeholder text for NWC URI input +Paste_your_NWC_URI_here_b471 = Paste your NWC URI here... + +# Error message for missing deck name +Please_create_a_name_for_the_deck_38e7 = Please create a name for the deck. + +# Error message for missing deck name and icon +Please_create_a_name_for_the_deck_and_select_an_icon_0add = Please create a name for the deck and select an icon. + +# Error message for missing deck icon +Please_select_an_icon_655b = Please select an icon. + +# Button label to post a note +Post_now_8a49 = Post now + +# Instruction for copying logs +Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = Press the button below to copy your most recent logs to your system's clipboard. Then paste it into your email. + +# Display name for user profiles +Profile_2478 = Profile + +# Timeline kind label for user profiles +Profile_9027 = Profile + +# Profile picture URL field label +Profile_picture_81ff = Profile picture + +# Column title for quote composition +Quote_475c = Quote + +# Display name for quote composition +Quote_a38e = Quote + +# Error message when quote note cannot be found +Quote_of_unknown_note_e4f0 = Quote of unknown note + +# Label for read-only profile mode +Read_only_82ff = Read only + +# Display name for relay management +Relays_7335 = Relays + +# Column title for relay management +Relays_9d89 = Relays + +# Label for relay list section +Relays_ad5e = Relays + +# Column title for reply composition +Reply_3bf1 = Reply + +# Display name for reply composition +Reply_b40f = Reply + +# Hover text for reply button +Reply_to_this_note_f5de = Reply to this note + +# Error message when reply note cannot be found +Reply_to_unknown_note_4401 = Reply to unknown note + +# Fallback template for replying to user +replying_to__user_15ab = replying to {$user} + +# Template for replying to user in unknown thread +replying_to__user__in_someone_s_thread_e148 = replying to {$user} in someone's thread + +# Template for replying to note in different user's thread +replying_to__user__s__note__in__thread_user__s__thread_daa8 = replying to {$user}'s {$note} in {$thread_user}'s {$thread} + +# Template for replying to user's note +replying_to__user__s__note_ccba = replying to {$user}'s {$note} + +# Template for replying to root thread +replying_to__user__s__thread_444d = replying to {$user}'s {$thread} + +# Fallback text when reply note is not found +replying_to_a_note_e0bc = replying to a note + +# Hover text for repost button +Repost_this_note_8e56 = Repost this note + +# Label for reposted notes +Reposted_61c8 = Reposted + +# Heading for support section +Running_into_a_bug_1796 = Running into a bug? + +# Label for satoshis (Bitcoin unit) for custom zap amount input field +SATS_45d7 = SATS + +# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings. +sats_e5ec = sats + +# Button to save default zap amount +Save_6f7c = Save + +# Button label to save profile changes +Save_changes_00db = Save changes + +# Display name for search results +Search_0aa0 = Search + +# Display name for search page +Search_4503 = Search + +# Timeline kind label for search results +Search_a0b8 = Search + +# Column title for search page +Search_c573 = Search + +# Placeholder for search notes input field +Search_notes_42a6 = Search notes... + +# Search in progress message +Searching_for___query_5d18 = Searching for '{$query}' + +# Description for Home column +See_notes_from_your_contacts_ac16 = See notes from your contacts + +# Description for universe column +See_the_whole_nostr_universe_7694 = See the whole nostr universe + +# Button label to send a zap +Send_1ea4 = Send + +# Description for last note per user column +Show_the_last_note_for_each_user_from_a_list_50e7 = Show the last note for each user from a list + +# Button label to sign out of account +Sign_out_337b = Sign out + +# Title for someone else's notes column +Someone_else_s_Notes_7e5f = Someone else's Notes + +# Title for someone else's notifications column +Someone_else_s_Notifications_82e6 = Someone else's Notifications + +# Description for contact list column +Source_the_last_note_for_each_user_in_your_contact_list_e157 = Source the last note for each user in your contact list + +# Description for hashtags column +Stay_up_to_date_with_a_certain_hashtag_88e3 = Stay up to date with a certain hashtag + +# Description for notifications column +Stay_up_to_date_with_notifications_and_mentions_6f4e = Stay up to date with notifications and mentions + +# Description for someone else's notes column +Stay_up_to_date_with_someone_else_s_notes___replies_464c = Stay up to date with someone else's notes & replies + +# Description for someone else's notifications column +Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = Stay up to date with someone else's notifications and mentions + +# Description for individual user column +Stay_up_to_date_with_someone_s_notes___replies_aa78 = Stay up to date with someone's notes & replies + +# Description for your notifications column +Stay_up_to_date_with_your_notifications_and_mentions_e73e = Stay up to date with your notifications and mentions + +# Step 1 label in support instructions +Step_1_8656 = Step 1 + +# Step 2 label in support instructions +Step_2_d08d = Step 2 + +# Column title for subscribing to external user +Subscribe_to_someone_else_s_notes_d1e9 = Subscribe to someone else's notes + +# Column title for subscribing to individual user +Subscribe_to_someone_s_notes_b3c8 = Subscribe to someone's notes + +# Display name for support page +Support_a4b4 = Support + +# Hover text for dark mode toggle button +Switch_to_dark_mode_4dec = Switch to dark mode + +# Hover text for light mode toggle button +Switch_to_light_mode_72ce = Switch to light mode + +# Button text to load blurred media +Tap_to_Load_4b05 = Tap to Load + +# Message shown when Dave trial period has ended +The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = The Dave Nostr AI assistant trial has ended :(. Thanks for testing! Zap-enabled Dave coming soon! + +# Column title for note thread view +Thread_0f20 = Thread + +# Display name for thread view +Thread_9957 = Thread + +# Link text for thread references +thread_ad1f = thread + +# Generic timeline kind label +Timeline_b0fc = Timeline + +# Timeline kind label for universe feed +Universe_0a3e = Universe + +# Display name for universe feed +Universe_d47e = Universe + +# Title for universe column +Universe_e01e = Universe + +# Column title for universe feed +Universe_ffaa = Universe + +# Checkbox label for using wallet only for current account +Use_this_wallet_for_the_current_account_only_61dc = Use this wallet for the current account only + +# Username and domain identification message +username___at___domain___will_be_used_for_identification_a4fd = "{$username}" at "{$domain}" will be used for identification + +# Profile username field label +Username_daa7 = Username + +# Column title for wallet management +Wallet_5e50 = Wallet + +# Display name for wallet management +Wallet_cdca = Wallet + +# Hint for deck name input field +We_recommend_short_names_083e = We recommend short names + +# Profile website field label +Website_7980 = Website + +# Placeholder for note input field +Write_a_banger_note_here_bad2 = Write a banger note here... + +# Placeholder text for key input field +Your_key_here_81bd = Your key here... + +# Title for your notes column +Your_Notes_f6db = Your Notes + +# Title for your notifications column +Your_Notifications_080d = Your Notifications + +# Heading for zap (tip) action +Zap_16b4 = Zap + +# Hover text for zap button +Zap_this_note_42b2 = Zap this note + +# Pluralized strings + +# Search results count +Got__count__results_for___query_85fb = + { $count -> + [one] Got {$count} result for '{$query}' + *[other] Got {$count} results for '{$query}' + } diff --git a/assets/translations/en-XA/main.ftl b/assets/translations/en-XA/main.ftl @@ -0,0 +1,611 @@ +# Main translation file for Notedeck +# This file contains common UI strings used throughout the application +# Auto-generated by extract_i18n.py - DO NOT EDIT MANUALLY + +# Regular strings + +# Profile about/bio field label +About_00c0 = {"["}Àbóút{"]"} + +# Display name for account management +Accounts_e233 = {"["}Àççóúñts{"]"} + +# Column title for account management +Accounts_f018 = {"["}Àççóúñts{"]"} + +# Button label to add a relay +Add_269d = {"["}Àdd{"]"} + +# Label for add column button +Add_47df = {"["}Àdd{"]"} + +# Button label to add a different wallet +Add_a_different_wallet_that_will_only_be_used_for_this_account_de8d = {"["}Àdd à dífféréñt wàllét thàt wíll óñly bé úséd fór thís àççóúñt{"]"} + +# Error message for missing wallet +Add_a_wallet_to_continue_d170 = {"["}Àdd à wàllét tó çóñtíñúé{"]"} + +# Button label to add a new account +Add_account_1cfc = {"["}Àdd àççóúñt{"]"} + +# Column title for adding new account +Add_Account_d06c = {"["}Àdd Àççóúñt{"]"} + +# Display name for adding account +Add_Account_d715 = {"["}Àdd Àççóúñt{"]"} + +# Column title for adding algorithm column +Add_Algo_Column_0d75 = {"["}Àdd Àlgó Çólúmñ{"]"} + +# Display name for adding column +Add_Column_c6ff = {"["}Àdd Çólúmñ{"]"} + +# Column title for adding new column +Add_Column_c764 = {"["}Àdd Çólúmñ{"]"} + +# Display name for adding deck +Add_Deck_6e5f = {"["}Àdd Déçk{"]"} + +# Column title for adding new deck +Add_Deck_fabf = {"["}Àdd Déçk{"]"} + +# Column title for adding external notifications column +Add_External_Notifications_Column_41ae = {"["}Àdd Éxtérñàl Ñótífíçàtíóñs Çólúmñ{"]"} + +# Column title for adding hashtag column +Add_Hashtag_Column_ebf4 = {"["}Àdd Hàshtàg Çólúmñ{"]"} + +# Column title for adding last notes column +Add_Last_Notes_Column_bbad = {"["}Àdd Làst Ñótés Çólúmñ{"]"} + +# Column title for adding notifications column +Add_Notifications_Column_79f8 = {"["}Àdd Ñótífíçàtíóñs Çólúmñ{"]"} + +# Button label to add a relay +Add_relay_269d = {"["}Àdd rélày{"]"} + +# Button label to add a wallet +Add_Wallet_d1be = {"["}Àdd Wàllét{"]"} + +# Title for algorithmic feeds column +Algo_2452 = {"["}Àlgó{"]"} + +# Description for algorithmic feeds column +Algorithmic_feeds_to_aid_in_note_discovery_d344 = {"["}Àlgóríthmíç fééds tó àíd íñ ñóté dísçóvéry{"]"} + +# Label for zap amount input field +Amount_70f0 = {"["}Àmóúñt{"]"} + +# Button to send message to Dave AI assistant +Ask_b7f4 = {"["}Àsk{"]"} + +# Placeholder text for Dave AI input field +Ask_dave_anything_33d1 = {"["}Àsk dàvé àñythíñg...{"]"} + +# Profile banner URL field label +Banner_52ef = {"["}Bàññér{"]"} + +# Beta version label +BETA_8e5d = {"["}BÉTÀ{"]"} + +# Broadcast the note to all connected relays +Broadcast_fe43 = {"["}Bróàdçàst{"]"} + +# Broadcast the note only to local network relays +Broadcast_Local_7e50 = {"["}Bróàdçàst Lóçàl{"]"} + +# Button label to cancel an action +Cancel_ed3b = {"["}Çàñçél{"]"} + +# Hover text for editable zap amount +Click_to_edit_0414 = {"["}Çlíçk tó édít{"]"} + +# Display name for note composition +Compose_Note_ad11 = {"["}Çómpósé Ñóté{"]"} + +# Column title for note composition +Compose_Note_c094 = {"["}Çómpósé Ñóté{"]"} + +# Button label to confirm an action +Confirm_f8a6 = {"["}Çóñfírm{"]"} + +# Status label for connected relay +Connected_f8cc = {"["}Çóññéçtéd{"]"} + +# Status label for connecting relay +Connecting_6b7e = {"["}Çóññéçtíñg...{"]"} + +# Title for contact list column +Contact_List_f85a = {"["}Çóñtàçt Líst{"]"} + +# Column title for contact lists +Contacts_7533 = {"["}Çóñtàçts{"]"} + +# Timeline kind label for contact lists +Contacts_8b98 = {"["}Çóñtàçts{"]"} + +# Column title for last notes per contact +Contacts__last_notes_3f84 = {"["}Çóñtàçts (làst ñótés){"]"} + +# Button label to copy logs +Copy_a688 = {"["}Çópy{"]"} + +# Button to copy media link to clipboard +Copy_Link_dc7c = {"["}Çópy Líñk{"]"} + +# Copy the unique note identifier to clipboard +Copy_Note_ID_6b45 = {"["}Çópy Ñóté ÍD{"]"} + +# Copy the raw note data in JSON format to clipboard +Copy_Note_JSON_9e4e = {"["}Çópy Ñóté JSÓÑ{"]"} + +# Copy the author's public key to clipboard +Copy_Pubkey_9cc4 = {"["}Çópy Púbkéy{"]"} + +# Copy the text content of the note to clipboard +Copy_Text_f81c = {"["}Çópy Téxt{"]"} + +# Button to create a new account +Create_Account_6994 = {"["}Çréàté Àççóúñt{"]"} + +# Button label to create a new deck +Create_Deck_16b7 = {"["}Çréàté Déçk{"]"} + +# Column title for custom timelines +Custom_a69e = {"["}Çústóm{"]"} + +# Display name for custom timelines +Custom_cb4f = {"["}Çústóm{"]"} + +# Column title for zap amount customization +Customize_Zap_Amount_cfc4 = {"["}Çústómízé Zàp Àmóúñt{"]"} + +# Display name for zap customization +Customize_Zap_Amount_ed29 = {"["}Çústómízé Zàp Àmóúñt{"]"} + +# Column title for support page +Damus_Support_27c0 = {"["}Dàmús Súppórt{"]"} + +# Label for deck name input field +Deck_name_cd32 = {"["}Déçk ñàmé{"]"} + +# Label for decks section in side panel +DECKS_1fad = {"["}DÉÇKS{"]"} + +# Label for default zap amount input +Default_amount_per_zap_399d = {"["}Défàúlt àmóúñt pér zàp:{"]"} + +# Name of the default deck feed +Default_Deck_fcca = {"["}Défàúlt Déçk{"]"} + +# Button label to delete a deck +Delete_Deck_bb29 = {"["}Délété Déçk{"]"} + +# Tooltip for deleting a column +Delete_this_column_8d5a = {"["}Délété thís çólúmñ{"]"} + +# Button label to delete a wallet +Delete_Wallet_d1d4 = {"["}Délété Wàllét{"]"} + +# Profile display name field label +Display_name_f9d9 = {"["}Dísplày ñàmé{"]"} + +# Domain identification message +domain___will_be_used_for_identification_b67e = {"["}"{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"} + +# Column title for editing deck +Edit_Deck_4018 = {"["}Édít Déçk{"]"} + +# Display name for editing deck +Edit_Deck_c9ba = {"["}Édít Déçk{"]"} + +# Button label to edit a deck +Edit_Deck_fd93 = {"["}Édít Déçk{"]"} + +# Button label to edit user profile +Edit_Profile_49e6 = {"["}Édít Prófílé{"]"} + +# Display name for profile editing +Edit_Profile_6699 = {"["}Édít Prófílé{"]"} + +# Column title for profile editing +Edit_Profile_8ad4 = {"["}Édít Prófílé{"]"} + +# Placeholder for hashtag input field +Enter_the_desired_hashtags_here__for_multiple_space-separated_7a69 = {"["}Éñtér thé désíréd hàshtàgs héré (fór múltíplé spàçé-sépàràtéd){"]"} + +# Placeholder for relay input field +Enter_the_relay_here_1c8b = {"["}Éñtér thé rélày héré{"]"} + +# Hint text to prompt entering the user's public key. +Enter_the_user_s_key__npub__hex__nip05__here_650c = {"["}Éñtér thé úsér's kéy (ñpúb, héx, ñíp05) héré...{"]"} + +# Label for key input field. Key can be public key (npub), private key (nsec), or Nostr address (NIP-05). +Enter_your_key_0fca = {"["}Éñtér yóúr kéy{"]"} + +# Instructions for entering Nostr credentials +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_48e9 = {"["}Éñtér yóúr públíç kéy (ñpúb), ñóstr àddréss (é.g. {$address}), ór prívàté kéy (ñséç). Yóú múst éñtér yóúr prívàté kéy tó bé àblé tó póst, réply, étç.{"]"} + +# Label for find user button +Find_User_bd12 = {"["}Fíñd Úsér{"]"} + +# Timeline kind label for hashtag feeds +Hashtag_a0ab = {"["}Hàshtàg{"]"} + +# Display name for hashtag feeds +Hashtags_617e = {"["}Hàshtàgs{"]"} + +# Title for hashtags column +Hashtags_f8e0 = {"["}Hàshtàgs{"]"} + +# Display name for home feed +Home_3efc = {"["}Hómé{"]"} + +# Title for Home column +Home_8c19 = {"["}Hómé{"]"} + +# Label for deck icon selection +Icon_b0ab = {"["}Íçóñ{"]"} + +# Title for individual user column +Individual_b776 = {"["}Íñdívídúàl{"]"} + +# Error message for invalid zap amount +Invalid_amount_6630 = {"["}Íñvàlíd àmóúñt{"]"} + +# Error message for invalid key input +Invalid_key_4726 = {"["}Íñvàlíd kéy.{"]"} + +# Error message for invalid Nostr Wallet Connect URI +Invalid_NWC_URI_031b = {"["}Íñvàlíd ÑWÇ ÚRÍ{"]"} + +# Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount. +k_100K_686c = {"["}100K{"]"} + +# Zap amount button for 10000 sats. Abbreviated because the button is too small to display the full amount. +k_10K_f7e6 = {"["}10K{"]"} + +# Zap amount button for 20000 sats. Abbreviated because the button is too small to display the full amount. +k_20K_4977 = {"["}20K{"]"} + +# Zap amount button for 50000 sats. Abbreviated because the button is too small to display the full amount. +k_50K_c2dc = {"["}50K{"]"} + +# Zap amount button for 5000 sats. Abbreviated because the button is too small to display the full amount. +k_5K_f7e6 = {"["}5K{"]"} + +# Description for your notes column +Keep_track_of_your_notes___replies_a334 = {"["}Kéép tràçk óf yóúr ñótés & réplíés{"]"} + +# Title for last note per user column +Last_Note_per_User_17ad = {"["}Làst Ñóté pér Úsér{"]"} + +# Timeline kind label for last notes per pubkey +Last_Notes_aefe = {"["}Làst Ñótés{"]"} + +# Display name for last notes per contact +Last_Per_Pubkey__Contact_33ce = {"["}Làst Pér Púbkéy (Çóñtàçt){"]"} + +# Bitcoin Lightning network address field label +Lightning_network_address__lud16_ea51 = {"["}Líghtñíñg ñétwórk àddréss (lúd16){"]"} + +# Login page title +Login_9eef = {"["}Lógíñ{"]"} + +# Login button text +Login_now___let_s_do_this_5630 = {"["}Lógíñ ñów — lét's dó thís!{"]"} + +# Text shown on blurred media from unfollowed users +Media_from_someone_you_don_t_follow_5611 = {"["}Médíà fróm sóméóñé yóú dóñ't fóllów{"]"} + +# Tooltip for moving a column +Moves_this_column_to_another_position_0d4b = {"["}Móvés thís çólúmñ tó àñóthér pósítíóñ{"]"} + +# Title for the user's deck +My_Deck_4ac5 = {"["}My Déçk{"]"} + +# Label asking if the user is new to Nostr. Underneath this label is a button to create an account. +New_to_Nostr_a2fd = {"["}Ñéw tó Ñóstr?{"]"} + +# NIP-05 identity field label +Nostr_address__NIP-05_identity_74a2 = {"["}Ñóstr àddréss (ÑÍP-05 ídéñtíty){"]"} + +# Default username when profile is not available +nostrich_df29 = {"["}ñóstríçh{"]"} + +# Status label for disconnected relay +Not_Connected_6292 = {"["}Ñót Çóññéçtéd{"]"} + +# Link text for note references +note_cad6 = {"["}ñóté{"]"} + +# Beta product warning message +Notedeck_is_a_beta_product__Expect_bugs_and_contact_us_when_you_run_into_issues_a671 = {"["}Ñótédéçk ís à bétà pródúçt. Éxpéçt búgs àñd çóñtàçt ús whéñ yóú rúñ íñtó íssúés.{"]"} + +# Filter label for notes only view +Notes_03fb = {"["}Ñótés{"]"} + +# Label for notes-only filter +Notes_60d2 = {"["}Ñótés{"]"} + +# Filter label for notes and replies view +Notes___Replies_1ec2 = {"["}Ñótés & Réplíés{"]"} + +# Label for notes and replies filter +Notes___Replies_6e3b = {"["}Ñótés & Réplíés{"]"} + +# Timeline kind label for notifications +Notifications_6228 = {"["}Ñótífíçàtíóñs{"]"} + +# Display name for notifications +Notifications_8029 = {"["}Ñótífíçàtíóñs{"]"} + +# Column title for notifications +Notifications_d673 = {"["}Ñótífíçàtíóñs{"]"} + +# Title for notifications column +Notifications_ef56 = {"["}Ñótífíçàtíóñs{"]"} + +# Button label to open email client +Open_Email_25e9 = {"["}Ópéñ Émàíl{"]"} + +# Instruction to open email client +Open_your_default_email_client_to_get_help_from_the_Damus_team_68dc = {"["}Ópéñ yóúr défàúlt émàíl çlíéñt tó gét hélp fróm thé Dàmús téàm{"]"} + +# Placeholder text for NWC URI input +Paste_your_NWC_URI_here_b471 = {"["}Pàsté yóúr ÑWÇ ÚRÍ héré...{"]"} + +# Error message for missing deck name +Please_create_a_name_for_the_deck_38e7 = {"["}Pléàsé çréàté à ñàmé fór thé déçk.{"]"} + +# Error message for missing deck name and icon +Please_create_a_name_for_the_deck_and_select_an_icon_0add = {"["}Pléàsé çréàté à ñàmé fór thé déçk àñd séléçt àñ íçóñ.{"]"} + +# Error message for missing deck icon +Please_select_an_icon_655b = {"["}Pléàsé séléçt àñ íçóñ.{"]"} + +# Button label to post a note +Post_now_8a49 = {"["}Póst ñów{"]"} + +# Instruction for copying logs +Press_the_button_below_to_copy_your_most_recent_logs_to_your_system_s_clipboard__Then_paste_it_into_your_email_322e = {"["}Préss thé búttóñ bélów tó çópy yóúr móst réçéñt lógs tó yóúr systém's çlípbóàrd. Théñ pàsté ít íñtó yóúr émàíl.{"]"} + +# Display name for user profiles +Profile_2478 = {"["}Prófílé{"]"} + +# Timeline kind label for user profiles +Profile_9027 = {"["}Prófílé{"]"} + +# Profile picture URL field label +Profile_picture_81ff = {"["}Prófílé píçtúré{"]"} + +# Column title for quote composition +Quote_475c = {"["}Qúóté{"]"} + +# Display name for quote composition +Quote_a38e = {"["}Qúóté{"]"} + +# Error message when quote note cannot be found +Quote_of_unknown_note_e4f0 = {"["}Qúóté óf úñkñówñ ñóté{"]"} + +# Label for read-only profile mode +Read_only_82ff = {"["}Réàd óñly{"]"} + +# Display name for relay management +Relays_7335 = {"["}Rélàys{"]"} + +# Column title for relay management +Relays_9d89 = {"["}Rélàys{"]"} + +# Label for relay list section +Relays_ad5e = {"["}Rélàys{"]"} + +# Column title for reply composition +Reply_3bf1 = {"["}Réply{"]"} + +# Display name for reply composition +Reply_b40f = {"["}Réply{"]"} + +# Hover text for reply button +Reply_to_this_note_f5de = {"["}Réply tó thís ñóté{"]"} + +# Error message when reply note cannot be found +Reply_to_unknown_note_4401 = {"["}Réply tó úñkñówñ ñóté{"]"} + +# Fallback template for replying to user +replying_to__user_15ab = {"["}réplyíñg tó {$user}{"]"} + +# Template for replying to user in unknown thread +replying_to__user__in_someone_s_thread_e148 = {"["}réplyíñg tó {$user} íñ sóméóñé's thréàd{"]"} + +# Template for replying to note in different user's thread +replying_to__user__s__note__in__thread_user__s__thread_daa8 = {"["}réplyíñg tó {$user}'s {$note} íñ {$thread_user}'s {$thread}{"]"} + +# Template for replying to user's note +replying_to__user__s__note_ccba = {"["}réplyíñg tó {$user}'s {$note}{"]"} + +# Template for replying to root thread +replying_to__user__s__thread_444d = {"["}réplyíñg tó {$user}'s {$thread}{"]"} + +# Fallback text when reply note is not found +replying_to_a_note_e0bc = {"["}réplyíñg tó à ñóté{"]"} + +# Hover text for repost button +Repost_this_note_8e56 = {"["}Répóst thís ñóté{"]"} + +# Label for reposted notes +Reposted_61c8 = {"["}Répóstéd{"]"} + +# Heading for support section +Running_into_a_bug_1796 = {"["}Rúññíñg íñtó à búg?{"]"} + +# Label for satoshis (Bitcoin unit) for custom zap amount input field +SATS_45d7 = {"["}SÀTS{"]"} + +# Unit label for satoshis (Bitcoin unit) for configuring default zap amount in wallet settings. +sats_e5ec = {"["}sàts{"]"} + +# Button to save default zap amount +Save_6f7c = {"["}Sàvé{"]"} + +# Button label to save profile changes +Save_changes_00db = {"["}Sàvé çhàñgés{"]"} + +# Display name for search results +Search_0aa0 = {"["}Séàrçh{"]"} + +# Display name for search page +Search_4503 = {"["}Séàrçh{"]"} + +# Timeline kind label for search results +Search_a0b8 = {"["}Séàrçh{"]"} + +# Column title for search page +Search_c573 = {"["}Séàrçh{"]"} + +# Placeholder for search notes input field +Search_notes_42a6 = {"["}Séàrçh ñótés...{"]"} + +# Search in progress message +Searching_for___query_5d18 = {"["}Séàrçhíñg fór '{$query}'{"]"} + +# Description for Home column +See_notes_from_your_contacts_ac16 = {"["}Séé ñótés fróm yóúr çóñtàçts{"]"} + +# Description for universe column +See_the_whole_nostr_universe_7694 = {"["}Séé thé whólé ñóstr úñívérsé{"]"} + +# Button label to send a zap +Send_1ea4 = {"["}Séñd{"]"} + +# Description for last note per user column +Show_the_last_note_for_each_user_from_a_list_50e7 = {"["}Shów thé làst ñóté fór éàçh úsér fróm à líst{"]"} + +# Button label to sign out of account +Sign_out_337b = {"["}Sígñ óút{"]"} + +# Title for someone else's notes column +Someone_else_s_Notes_7e5f = {"["}Sóméóñé élsé's Ñótés{"]"} + +# Title for someone else's notifications column +Someone_else_s_Notifications_82e6 = {"["}Sóméóñé élsé's Ñótífíçàtíóñs{"]"} + +# Description for contact list column +Source_the_last_note_for_each_user_in_your_contact_list_e157 = {"["}Sóúrçé thé làst ñóté fór éàçh úsér íñ yóúr çóñtàçt líst{"]"} + +# Description for hashtags column +Stay_up_to_date_with_a_certain_hashtag_88e3 = {"["}Stày úp tó dàté wíth à çértàíñ hàshtàg{"]"} + +# Description for notifications column +Stay_up_to_date_with_notifications_and_mentions_6f4e = {"["}Stày úp tó dàté wíth ñótífíçàtíóñs àñd méñtíóñs{"]"} + +# Description for someone else's notes column +Stay_up_to_date_with_someone_else_s_notes___replies_464c = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótés & réplíés{"]"} + +# Description for someone else's notifications column +Stay_up_to_date_with_someone_else_s_notifications_and_mentions_3473 = {"["}Stày úp tó dàté wíth sóméóñé élsé's ñótífíçàtíóñs àñd méñtíóñs{"]"} + +# Description for individual user column +Stay_up_to_date_with_someone_s_notes___replies_aa78 = {"["}Stày úp tó dàté wíth sóméóñé's ñótés & réplíés{"]"} + +# Description for your notifications column +Stay_up_to_date_with_your_notifications_and_mentions_e73e = {"["}Stày úp tó dàté wíth yóúr ñótífíçàtíóñs àñd méñtíóñs{"]"} + +# Step 1 label in support instructions +Step_1_8656 = {"["}Stép 1{"]"} + +# Step 2 label in support instructions +Step_2_d08d = {"["}Stép 2{"]"} + +# Column title for subscribing to external user +Subscribe_to_someone_else_s_notes_d1e9 = {"["}Súbsçríbé tó sóméóñé élsé's ñótés{"]"} + +# Column title for subscribing to individual user +Subscribe_to_someone_s_notes_b3c8 = {"["}Súbsçríbé tó sóméóñé's ñótés{"]"} + +# Display name for support page +Support_a4b4 = {"["}Súppórt{"]"} + +# Hover text for dark mode toggle button +Switch_to_dark_mode_4dec = {"["}Swítçh tó dàrk módé{"]"} + +# Hover text for light mode toggle button +Switch_to_light_mode_72ce = {"["}Swítçh tó líght módé{"]"} + +# Button text to load blurred media +Tap_to_Load_4b05 = {"["}Tàp tó Lóàd{"]"} + +# Message shown when Dave trial period has ended +The_Dave_Nostr_AI_assistant_trial_has_ended_____Thanks_for_testing__Zap-enabled_Dave_coming_soon_c6c7 = {"["}Thé Dàvé Ñóstr ÀÍ àssístàñt tríàl hàs éñdéd :(. Thàñks fór téstíñg! Zàp-éñàbléd Dàvé çómíñg sóóñ!{"]"} + +# Column title for note thread view +Thread_0f20 = {"["}Thréàd{"]"} + +# Display name for thread view +Thread_9957 = {"["}Thréàd{"]"} + +# Link text for thread references +thread_ad1f = {"["}thréàd{"]"} + +# Generic timeline kind label +Timeline_b0fc = {"["}Tímélíñé{"]"} + +# Timeline kind label for universe feed +Universe_0a3e = {"["}Úñívérsé{"]"} + +# Display name for universe feed +Universe_d47e = {"["}Úñívérsé{"]"} + +# Title for universe column +Universe_e01e = {"["}Úñívérsé{"]"} + +# Column title for universe feed +Universe_ffaa = {"["}Úñívérsé{"]"} + +# Checkbox label for using wallet only for current account +Use_this_wallet_for_the_current_account_only_61dc = {"["}Úsé thís wàllét fór thé çúrréñt àççóúñt óñly{"]"} + +# Username and domain identification message +username___at___domain___will_be_used_for_identification_a4fd = {"["}"{$username}" àt "{$domain}" wíll bé úséd fór ídéñtífíçàtíóñ{"]"} + +# Profile username field label +Username_daa7 = {"["}Úsérñàmé{"]"} + +# Column title for wallet management +Wallet_5e50 = {"["}Wàllét{"]"} + +# Display name for wallet management +Wallet_cdca = {"["}Wàllét{"]"} + +# Hint for deck name input field +We_recommend_short_names_083e = {"["}Wé réçómméñd shórt ñàmés{"]"} + +# Profile website field label +Website_7980 = {"["}Wébsíté{"]"} + +# Placeholder for note input field +Write_a_banger_note_here_bad2 = {"["}Wríté à bàñgér ñóté héré...{"]"} + +# Placeholder text for key input field +Your_key_here_81bd = {"["}Yóúr kéy héré...{"]"} + +# Title for your notes column +Your_Notes_f6db = {"["}Yóúr Ñótés{"]"} + +# Title for your notifications column +Your_Notifications_080d = {"["}Yóúr Ñótífíçàtíóñs{"]"} + +# Heading for zap (tip) action +Zap_16b4 = {"["}Zàp{"]"} + +# Hover text for zap button +Zap_this_note_42b2 = {"["}Zàp thís ñóté{"]"} + +# Pluralized strings + +# Search results count +Got__count__results_for___query_85fb = + { $count -> + [one] {"["}Gót {$count} résúlt fór '{$query}'{"]"} + *[other] {"["}Gót {$count} résúlts fór '{$query}'{"]"} + } diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -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")) diff --git a/crates/notedeck/src/i18n/manager.rs b/crates/notedeck/src/i18n/manager.rs @@ -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 { diff --git a/crates/notedeck/src/i18n/mod.rs b/crates/notedeck/src/i18n/mod.rs @@ -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 diff --git a/crates/notedeck/src/wallet.rs b/crates/notedeck/src/wallet.rs @@ -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"), } diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -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 { diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -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, )); diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs @@ -2,7 +2,7 @@ use std::collections::{hash_map::ValuesMut, HashMap}; use enostr::{Pubkey, RelayPool}; use nostrdb::Transaction; -use notedeck::{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 { diff --git a/crates/notedeck_columns/src/login_manager.rs b/crates/notedeck_columns/src/login_manager.rs @@ -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)) diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -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; }; diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs @@ -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::List(ListKind::Contact(_pk)) => { + write!(f, "{}", tr!("Home", "Display name for home feed")) + } TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => { - write!(f, "Last Per Pubkey (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") + ), } } } diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs @@ -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()), } } diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -9,8 +9,8 @@ use crate::{ use notedeck::{ contacts::hybrid_contacts_filter, filter::{self, HybridFilter}, - 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") + } } } diff --git a/crates/notedeck_columns/src/ui/account_login_view.rs b/crates/notedeck_columns/src/ui/account_login_view.rs @@ -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)) diff --git a/crates/notedeck_columns/src/ui/accounts.rs b/crates/notedeck_columns/src/ui/accounts.rs @@ -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" + ))) } diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs @@ -17,7 +17,7 @@ use crate::{ Damus, }; -use notedeck::{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); diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs @@ -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() { diff --git a/crates/notedeck_columns/src/ui/configure_deck.rs b/crates/notedeck_columns/src/ui/configure_deck.rs @@ -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 "); - - ui.add( - egui::Label::new( - RichText::new(format!("Please {message}.")).color(ui.visuals().error_fg_color), - ) - .wrap(), - ); + 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() + }; + + if !warning.is_empty() { + ui.add(egui::Label::new(RichText::new(warning).color(ui.visuals().error_fg_color)).wrap()); } } diff --git a/crates/notedeck_columns/src/ui/edit_deck.rs b/crates/notedeck_columns/src/ui/edit_deck.rs @@ -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), ) diff --git a/crates/notedeck_columns/src/ui/note/custom_zap.rs b/crates/notedeck_columns/src/ui/note/custom_zap.rs @@ -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.")), } } } diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -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 { diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -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 + ) }), ); }); diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -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(), diff --git a/crates/notedeck_columns/src/ui/relay.rs b/crates/notedeck_columns/src/ui/relay.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::ui::{Preview, PreviewConfig}; use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2}; use enostr::{RelayPool, RelayStatus}; -use notedeck::{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() diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs @@ -5,7 +5,7 @@ use state::TypingType; use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView}; use egui_winit::clipboard::Clipboard; use nostrdb::{Filter, Ndb, Transaction}; -use notedeck::{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)) diff --git a/crates/notedeck_columns/src/ui/side_panel.rs b/crates/notedeck_columns/src/ui/side_panel.rs @@ -12,7 +12,7 @@ use crate::{ route::Route, }; -use notedeck::{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), )); diff --git a/crates/notedeck_columns/src/ui/support.rs b/crates/notedeck_columns/src/ui/support.rs @@ -1,5 +1,5 @@ use egui::{vec2, Button, Label, Layout, RichText}; -use notedeck::{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) } diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -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); diff --git a/crates/notedeck_columns/src/ui/wallet.rs b/crates/notedeck_columns/src/ui/wallet.rs @@ -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); diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -4,7 +4,7 @@ use crate::{ }; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use nostrdb::{Ndb, Transaction}; -use notedeck::{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), ); diff --git a/crates/notedeck_ui/src/note/context.rs b/crates/notedeck_ui/src/note/context.rs @@ -1,6 +1,6 @@ use egui::{Rect, Vec2}; use nostrdb::NoteKey; -use notedeck::{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, )); diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs @@ -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, diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -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) } diff --git a/crates/notedeck_ui/src/note/reply_description.rs b/crates/notedeck_ui/src/note/reply_description.rs @@ -1,177 +1,304 @@ 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}; -#[must_use = "Please handle the resulting note action"] -#[profiling::function] -pub fn reply_desc( +// 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_reply: &NoteReply, note_context: &mut NoteContext, note_options: NoteOptions, jobs: &mut JobsCache, + size: f32, + selectable: bool, ) -> 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), - ); + 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 r.clicked() { - // TODO: jump to note - } + 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.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); - }); + 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); + }); + } + } + } + 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, &note, note_options, jobs) + .actionbar(false) + .wide(true) + .show(ui); + }); + } + } + } } - }; + } - ui.add(Label::new(RichText::new("replying to").size(size).color(color)).selectable(selectable)); + note_action +} + +#[must_use = "Please handle the resulting note action"] +#[profiling::function] +pub fn reply_desc( + ui: &mut egui::Ui, + txn: &Transaction, + note_reply: &NoteReply, + note_context: &mut NoteContext, + note_options: NoteOptions, + jobs: &mut JobsCache, +) -> Option<NoteAction> { + let size = 10.0; + let selectable = false; 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, - 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 {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}" ); - - 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(), + 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, "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, + ) } diff --git a/crates/notedeck_ui/src/profile/preview.rs b/crates/notedeck_ui/src/profile/preview.rs @@ -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, diff --git a/crates/notedeck_ui/src/username.rs b/crates/notedeck_ui/src/username.rs @@ -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) }