Internationalize user-facing strings and export them for translations

Changelog-Added: Internationalized user-facing strings and exported them for translations
Signed-off-by: Terry Yiu <git@tyiu.xyz>
This commit is contained in:
Terry Yiu
2025-06-26 23:13:31 -04:00
committed by William Casarin
parent d07c3e9135
commit 3f5036bd32
37 changed files with 2198 additions and 437 deletions

View File

@@ -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}'
}

View File

@@ -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}'{"]"}
}

View File

@@ -233,7 +233,7 @@ impl Notedeck {
// Initialize localization // Initialize localization
let i18n_resource_dir = Path::new("assets/translations"); let i18n_resource_dir = Path::new("assets/translations");
let localization_manager = Arc::new( 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); error!("Failed to initialize localization manager: {}", e);
// Create a fallback manager with a temporary directory // Create a fallback manager with a temporary directory
LocalizationManager::new(&std::env::temp_dir().join("notedeck_i18n_fallback")) LocalizationManager::new(&std::env::temp_dir().join("notedeck_i18n_fallback"))

View File

@@ -78,7 +78,7 @@ impl LocalizationManager {
locale: &LanguageIdentifier, locale: &LanguageIdentifier,
) -> Result<Arc<FluentResource>, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<Arc<FluentResource>, Box<dyn std::error::Error + Send + Sync>> {
// Construct the path using the stored resource directory // 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 // Try to open the file directly
if let Err(e) = std::fs::File::open(&expected_path) { if let Err(e) = std::fs::File::open(&expected_path) {
@@ -87,16 +87,16 @@ impl LocalizationManager {
expected_path.display(), expected_path.display(),
e 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 // Load the FTL file directly instead of using ResourceManager
let ftl_string = std::fs::read_to_string(&expected_path) 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 // Parse the FTL content
let resource = FluentResource::try_new(ftl_string) 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!( tracing::debug!(
"Loaded and cached parsed FluentResource for locale: {}", "Loaded and cached parsed FluentResource for locale: {}",
@@ -182,15 +182,15 @@ impl LocalizationManager {
let mut bundle = FluentBundle::new(vec![locale.clone()]); let mut bundle = FluentBundle::new(vec![locale.clone()]);
bundle bundle
.add_resource(resource.as_ref()) .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 let message = bundle
.get_message(id) .get_message(id)
.ok_or_else(|| format!("Message not found: {}", id))?; .ok_or_else(|| format!("Message not found: {id}"))?;
let pattern = message let pattern = message
.value() .value()
.ok_or_else(|| format!("Message has no value: {}", id))?; .ok_or_else(|| format!("Message has no value: {id}"))?;
// Format the message // Format the message
let mut errors = Vec::new(); let mut errors = Vec::new();
@@ -243,16 +243,16 @@ impl LocalizationManager {
locale, locale,
self.available_locales 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 let mut current = self
.current_locale .current_locale
.write() .write()
.map_err(|e| format!("Lock error: {e}"))?; .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(); *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 // Clear caches when locale changes since they are locale-specific
let mut string_cache = self let mut string_cache = self
@@ -406,7 +406,7 @@ impl LocalizationContext {
pub fn get_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String { pub fn get_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String {
self.manager self.manager
.get_string_with_args(id, args) .get_string_with_args(id, args)
.unwrap_or_else(|_| format!("[MISSING: {}]", id)) .unwrap_or_else(|_| format!("[MISSING: {id}]"))
} }
/// Sets the current locale /// Sets the current locale
@@ -447,7 +447,7 @@ pub trait Localizable {
impl Localizable for LocalizationContext { impl Localizable for LocalizationContext {
fn get_localized_string(&self, id: &str) -> String { fn get_localized_string(&self, id: &str) -> String {
self.get_string(id) 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 { fn get_localized_string_with_args(&self, id: &str, args: Option<&FluentArgs>) -> String {

View File

@@ -54,7 +54,7 @@ fn simple_hash(s: &str) -> String {
pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String { pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
// Try to get from cache first // Try to get from cache first
let cache_key = if let Some(comment) = comment { let cache_key = if let Some(comment) = comment {
format!("{}:{}", key, comment) format!("{key}:{comment}")
} else { } else {
key.to_string() key.to_string()
}; };
@@ -76,8 +76,8 @@ pub fn normalize_ftl_key(key: &str, comment: Option<&str>) -> String {
result = result.trim_matches('_').to_string(); result = result.trim_matches('_').to_string();
// Ensure the key starts with a letter (Fluent requirement) // Ensure the key starts with a letter (Fluent requirement)
if !(result.len() > 0 && result.chars().next().unwrap().is_ascii_alphabetic()) { if result.is_empty() || !result.chars().next().unwrap().is_ascii_alphabetic() {
result = format!("k_{}", result); result = format!("k_{result}");
} }
// If we have a comment, append a hash of it to reduce collisions // If we have a comment, append a hash of it to reduce collisions

View File

@@ -153,8 +153,8 @@ impl From<nwc::Error> for NwcError {
impl Display for NwcError { impl Display for NwcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
NwcError::NIP47(err) => write!(f, "NIP47 error: {}", err), NwcError::NIP47(err) => write!(f, "NIP47 error: {err}"),
NwcError::Relay(err) => write!(f, "Relay error: {}", err), NwcError::Relay(err) => write!(f, "Relay error: {err}"),
NwcError::PrematureExit => write!(f, "Premature exit"), NwcError::PrematureExit => write!(f, "Premature exit"),
NwcError::Timeout => write!(f, "Request timed out"), NwcError::Timeout => write!(f, "Request timed out"),
} }

View File

@@ -5,7 +5,7 @@ use crate::app::NotedeckApp;
use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget}; use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference, Widget};
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType}; use notedeck::{tr, App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType};
use notedeck_columns::{ use notedeck_columns::{
column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus, column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus,
}; };
@@ -460,15 +460,16 @@ fn milestone_name() -> impl Widget {
); );
ui.add( ui.add(
Label::new( Label::new(
RichText::new("BETA") RichText::new(tr!("BETA", "Beta version label"))
.color(ui.style().visuals.noninteractive().fg_stroke.color) .color(ui.style().visuals.noninteractive().fg_stroke.color)
.font(font), .font(font),
) )
.selectable(false), .selectable(false),
) )
.on_hover_text( .on_hover_text(tr!(
"Notedeck is a beta product. Expect bugs and contact us when you run into issues.", "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) .on_hover_cursor(egui::CursorIcon::Help)
}) })
.inner .inner
@@ -719,7 +720,10 @@ fn bottomup_sidebar(
let resp = ui let resp = ui
.add(Button::new("").frame(false)) .add(Button::new("").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand) .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() { if resp.clicked() {
Some(ChromePanelAction::SaveTheme(ThemePreference::Light)) Some(ChromePanelAction::SaveTheme(ThemePreference::Light))
} else { } else {
@@ -730,7 +734,10 @@ fn bottomup_sidebar(
let resp = ui let resp = ui
.add(Button::new("🌙").frame(false)) .add(Button::new("🌙").frame(false))
.on_hover_cursor(egui::CursorIcon::PointingHand) .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() { if resp.clicked() {
Some(ChromePanelAction::SaveTheme(ThemePreference::Dark)) Some(ChromePanelAction::SaveTheme(ThemePreference::Dark))
} else { } else {

View File

@@ -19,7 +19,8 @@ use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::{ 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 notedeck_ui::{jobs::JobsCache, NoteOptions};
use std::collections::{BTreeSet, HashMap}; 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 mut account_to_decks: HashMap<Pubkey, Decks> = Default::default();
let decks = Decks::new(crate::decks::Deck::new_with_columns( let decks = Decks::new(crate::decks::Deck::new_with_columns(
crate::decks::Deck::default().icon, crate::decks::Deck::default().icon,
"My Deck".to_owned(), tr!("My Deck", "Title for the user's deck"),
cols, cols,
)); ));

View File

@@ -2,7 +2,7 @@ use std::collections::{hash_map::ValuesMut, HashMap};
use enostr::{Pubkey, RelayPool}; use enostr::{Pubkey, RelayPool};
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::{AppContext, FALLBACK_PUBKEY}; use notedeck::{tr, AppContext, FALLBACK_PUBKEY};
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
@@ -397,8 +397,8 @@ impl Deck {
'🇩' '🇩'
} }
pub fn default_name() -> &'static str { pub fn default_name() -> String {
"Default Deck" tr!("Default Deck", "Name of the default deck feed")
} }
pub fn new(icon: char, name: String) -> Self { pub fn new(icon: char, name: String) -> Self {

View File

@@ -2,6 +2,7 @@ use crate::key_parsing::perform_key_retrieval;
use crate::key_parsing::AcquireKeyError; use crate::key_parsing::AcquireKeyError;
use egui::{TextBuffer, TextEdit}; use egui::{TextBuffer, TextEdit};
use enostr::Keypair; use enostr::Keypair;
use notedeck::tr;
use poll_promise::Promise; use poll_promise::Promise;
/// The state data for acquiring a nostr key /// The state data for acquiring a nostr key
@@ -134,7 +135,8 @@ fn show_error(ui: &mut egui::Ui, err: &AcquireKeyError) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let error_label = match err { let error_label = match err {
AcquireKeyError::InvalidKey => egui::Label::new( 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) => { AcquireKeyError::Nip05Failed(e) => {
egui::Label::new(egui::RichText::new(e).color(ui.visuals().error_fg_color)) egui::Label::new(egui::RichText::new(e).color(ui.visuals().error_fg_color))

View File

@@ -31,8 +31,8 @@ use egui_nav::{Nav, NavAction, NavResponse, NavUiType, Percent, PopupResponse, P
use enostr::ProfileState; use enostr::ProfileState;
use nostrdb::{Filter, Ndb, Transaction}; use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{ use notedeck::{
get_current_default_msats, get_current_wallet, ui::is_narrow, Accounts, AppContext, NoteAction, get_current_default_msats, get_current_wallet, tr, ui::is_narrow, Accounts, AppContext,
NoteContext, RelayAction, NoteAction, NoteContext, RelayAction,
}; };
use tracing::error; use tracing::error;
@@ -572,14 +572,20 @@ fn render_nav_body(
let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) {
txn txn
} else { } else {
ui.label("Reply to unknown note"); ui.label(tr!(
"Reply to unknown note",
"Error message when reply note cannot be found"
));
return None; return None;
}; };
let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) { let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
note note
} else { } else {
ui.label("Reply to unknown note"); ui.label(tr!(
"Reply to unknown note",
"Error message when reply note cannot be found"
));
return None; 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()) { let note = if let Ok(note) = ctx.ndb.get_note_by_id(&txn, id.bytes()) {
note note
} else { } else {
ui.label("Quote of unknown note"); ui.label(tr!(
"Quote of unknown note",
"Error message when quote note cannot be found"
));
return None; return None;
}; };

View File

@@ -1,5 +1,5 @@
use enostr::{NoteId, Pubkey}; use enostr::{NoteId, Pubkey};
use notedeck::{NoteZapTargetOwned, RootNoteIdBuf, WalletType}; use notedeck::{tr, NoteZapTargetOwned, RootNoteIdBuf, WalletType};
use std::{ use std::{
fmt::{self}, fmt::{self},
ops::Range, ops::Range,
@@ -244,42 +244,85 @@ impl Route {
pub fn title(&self) -> ColumnTitle<'_> { pub fn title(&self) -> ColumnTitle<'_> {
match self { match self {
Route::Timeline(kind) => kind.to_title(), Route::Timeline(kind) => kind.to_title(),
Route::Thread(_) => ColumnTitle::simple("Thread"), Route::Thread(_) => {
Route::Reply(_id) => ColumnTitle::simple("Reply"), ColumnTitle::formatted(tr!("Thread", "Column title for note thread view"))
Route::Quote(_id) => ColumnTitle::simple("Quote"), }
Route::Relays => ColumnTitle::simple("Relays"), 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 { Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => ColumnTitle::simple("Accounts"), AccountsRoute::Accounts => {
AccountsRoute::AddAccount => ColumnTitle::simple("Add Account"), 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 { 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 { AddColumnRoute::Algo(r) => match r {
AddAlgoRoute::Base => ColumnTitle::simple("Add Algo Column"), AddAlgoRoute::Base => ColumnTitle::formatted(tr!(
AddAlgoRoute::LastPerPubkey => ColumnTitle::simple("Add Last Notes Column"), "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 => { AddColumnRoute::UndecidedNotification => ColumnTitle::formatted(tr!(
ColumnTitle::simple("Add Notifications Column") "Add Notifications Column",
} "Column title for adding notifications column"
AddColumnRoute::ExternalNotification => { )),
ColumnTitle::simple("Add External Notifications Column") AddColumnRoute::ExternalNotification => ColumnTitle::formatted(tr!(
} "Add External Notifications Column",
AddColumnRoute::Hashtag => ColumnTitle::simple("Add Hashtag Column"), "Column title for adding external notifications column"
AddColumnRoute::UndecidedIndividual => { )),
ColumnTitle::simple("Subscribe to someone's notes") AddColumnRoute::Hashtag => ColumnTitle::formatted(tr!(
} "Add Hashtag Column",
AddColumnRoute::ExternalIndividual => { "Column title for adding hashtag column"
ColumnTitle::simple("Subscribe to someone else's notes") )),
} 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::Support => {
Route::NewDeck => ColumnTitle::simple("Add Deck"), ColumnTitle::formatted(tr!("Damus Support", "Column title for support page"))
Route::EditDeck(_) => ColumnTitle::simple("Edit Deck"), }
Route::EditProfile(_) => ColumnTitle::simple("Edit Profile"), Route::NewDeck => {
Route::Search => ColumnTitle::simple("Search"), ColumnTitle::formatted(tr!("Add Deck", "Column title for adding new deck"))
Route::Wallet(_) => ColumnTitle::simple("Wallet"), }
Route::CustomizeZapAmount(_) => ColumnTitle::simple("Customize Zap Amount"), 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Route::Timeline(kind) => match kind { Route::Timeline(kind) => match kind {
TimelineKind::List(ListKind::Contact(_pk)) => write!(f, "Home"), TimelineKind::List(ListKind::Contact(_pk)) => {
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => { write!(f, "{}", tr!("Home", "Display name for home feed"))
write!(f, "Last Per Pubkey (Contact)") }
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => {
write!(
f,
"{}",
tr!(
"Last Per Pubkey (Contact)",
"Display name for last notes per contact"
)
)
}
TimelineKind::Notifications(_) => write!(
f,
"{}",
tr!("Notifications", "Display name for notifications")
),
TimelineKind::Universe => {
write!(f, "{}", tr!("Universe", "Display name for universe feed"))
}
TimelineKind::Generic(_) => {
write!(f, "{}", tr!("Custom", "Display name for custom timelines"))
}
TimelineKind::Search(_) => {
write!(f, "{}", tr!("Search", "Display name for search results"))
}
TimelineKind::Hashtag(ht) => write!(
f,
"{} ({})",
tr!("Hashtags", "Display name for hashtag feeds"),
ht.join(" ")
),
TimelineKind::Profile(_id) => {
write!(f, "{}", tr!("Profile", "Display name for user profiles"))
} }
TimelineKind::Notifications(_) => write!(f, "Notifications"),
TimelineKind::Universe => write!(f, "Universe"),
TimelineKind::Generic(_) => write!(f, "Custom"),
TimelineKind::Search(_) => write!(f, "Search"),
TimelineKind::Hashtag(ht) => write!(f, "Hashtags ({})", ht.join(" ")),
TimelineKind::Profile(_id) => write!(f, "Profile"),
}, },
Route::Thread(_) => write!(f, "Thread"), Route::Thread(_) => write!(f, "{}", tr!("Thread", "Display name for thread view")),
Route::Reply(_id) => write!(f, "Reply"), Route::Reply(_id) => {
Route::Quote(_id) => write!(f, "Quote"), write!(f, "{}", tr!("Reply", "Display name for reply composition"))
Route::Relays => write!(f, "Relays"), }
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 { Route::Accounts(amr) => match amr {
AccountsRoute::Accounts => write!(f, "Accounts"), AccountsRoute::Accounts => write!(
AccountsRoute::AddAccount => write!(f, "Add Account"), 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::ComposeNote => write!(
Route::AddColumn(_) => write!(f, "Add Column"), f,
Route::Support => write!(f, "Support"), "{}",
Route::NewDeck => write!(f, "Add Deck"), tr!("Compose Note", "Display name for note composition")
Route::EditDeck(_) => write!(f, "Edit Deck"), ),
Route::EditProfile(_) => write!(f, "Edit Profile"), Route::AddColumn(_) => {
Route::Search => write!(f, "Search"), write!(f, "{}", tr!("Add Column", "Display name for adding column"))
Route::Wallet(_) => write!(f, "Wallet"), }
Route::CustomizeZapAmount(_) => write!(f, "Customize Zap Amount"), 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")
),
} }
} }
} }

View File

@@ -6,7 +6,7 @@ use nostrdb::{Ndb, Transaction};
use notedeck::{ use notedeck::{
contacts::{contacts_filter, hybrid_contacts_filter}, contacts::{contacts_filter, hybrid_contacts_filter},
filter::{self, default_limit, default_remote_limit, HybridFilter}, filter::{self, default_limit, default_remote_limit, HybridFilter},
FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf, tr, FilterError, FilterState, NoteCache, RootIdError, RootNoteIdBuf,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@@ -257,14 +257,47 @@ impl AlgoTimeline {
impl Display for TimelineKind { impl Display for TimelineKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Home"), TimelineKind::List(ListKind::Contact(_src)) => write!(
TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => f.write_str("Last Notes"), f,
TimelineKind::Generic(_) => f.write_str("Timeline"), "{}",
TimelineKind::Notifications(_) => f.write_str("Notifications"), tr!("Home", "Timeline kind label for contact lists")
TimelineKind::Profile(_) => f.write_str("Profile"), ),
TimelineKind::Universe => f.write_str("Universe"), TimelineKind::Algo(AlgoTimeline::LastPerPubkey(_lk)) => write!(
TimelineKind::Hashtag(_) => f.write_str("Hashtags"), f,
TimelineKind::Search(_) => f.write_str("Search"), "{}",
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)) ColumnTitle::formatted(format!("Search \"{}\"", query.search))
} }
TimelineKind::List(list_kind) => match list_kind { 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 { 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::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Universe => ColumnTitle::simple("Universe"), TimelineKind::Universe => {
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"), 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()), TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
} }
} }

View File

@@ -9,8 +9,8 @@ use crate::{
use notedeck::{ use notedeck::{
contacts::hybrid_contacts_filter, contacts::hybrid_contacts_filter,
filter::{self, HybridFilter}, filter::{self, HybridFilter},
Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache, NoteRef, tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, NoteCache,
UnknownIds, NoteRef, UnknownIds,
}; };
use egui_virtual_list::VirtualList; use egui_virtual_list::VirtualList;
@@ -64,10 +64,12 @@ pub enum ViewFilter {
} }
impl ViewFilter { impl ViewFilter {
pub fn name(&self) -> &'static str { pub fn name(&self) -> String {
match self { match self {
ViewFilter::Notes => "Notes", ViewFilter::Notes => tr!("Notes", "Filter label for notes only view"),
ViewFilter::NotesAndReplies => "Notes & Replies", ViewFilter::NotesAndReplies => {
tr!("Notes & Replies", "Filter label for notes and replies view")
}
} }
} }

View File

@@ -6,7 +6,7 @@ use egui::{
}; };
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
use enostr::Keypair; use enostr::Keypair;
use notedeck::{fonts::get_font_size, AppAction, NotedeckTextStyle}; use notedeck::{fonts::get_font_size, tr, AppAction, NotedeckTextStyle};
use notedeck_ui::{ use notedeck_ui::{
app_images, app_images,
context_menu::{input_context, PasteBehavior}, context_menu::{input_context, PasteBehavior},
@@ -58,7 +58,7 @@ impl<'a> AccountLoginView<'a> {
ui.with_layout(Layout::left_to_right(Align::TOP), |ui| { ui.with_layout(Layout::left_to_right(Align::TOP), |ui| {
let help_text_style = NotedeckTextStyle::Small; let help_text_style = NotedeckTextStyle::Small;
ui.add(egui::Label::new( 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()) .text_style(help_text_style.text_style())
.size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()), .size(get_font_size(ui.ctx(), &help_text_style)).color(ui.visuals().weak_text_color()),
).wrap()) ).wrap())
@@ -73,13 +73,13 @@ impl<'a> AccountLoginView<'a> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label( 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) .color(ui.style().visuals.noninteractive().fg_stroke.color)
.text_style(NotedeckTextStyle::Body.text_style()), .text_style(NotedeckTextStyle::Body.text_style()),
); );
if ui 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() .clicked()
{ {
self.manager.should_create_new(); self.manager.should_create_new();
@@ -99,20 +99,20 @@ impl<'a> AccountLoginView<'a> {
} }
fn login_title_text() -> RichText { fn login_title_text() -> RichText {
RichText::new("Login") RichText::new(tr!("Login", "Login page title"))
.text_style(NotedeckTextStyle::Heading2.text_style()) .text_style(NotedeckTextStyle::Heading2.text_style())
.strong() .strong()
} }
fn login_textedit_info_text() -> RichText { 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() .strong()
.text_style(NotedeckTextStyle::Body.text_style()) .text_style(NotedeckTextStyle::Body.text_style())
} }
fn login_button() -> Button<'static> { fn login_button() -> Button<'static> {
Button::new( 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()) .text_style(NotedeckTextStyle::Body.text_style())
.strong(), .strong(),
) )
@@ -124,7 +124,11 @@ fn login_textedit(manager: &mut AcquireKeyState) -> TextEdit {
let create_textedit: fn(&mut dyn TextBuffer) -> TextEdit = |text| { let create_textedit: fn(&mut dyn TextBuffer) -> TextEdit = |text| {
egui::TextEdit::singleline(text) egui::TextEdit::singleline(text)
.hint_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) .vertical_align(Align::Center)
.min_size(Vec2::new(0.0, 40.0)) .min_size(Vec2::new(0.0, 40.0))

View File

@@ -3,7 +3,7 @@ use egui::{
}; };
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::{Accounts, Images}; use notedeck::{tr, Accounts, Images};
use notedeck_ui::colors::PINK; use notedeck_ui::colors::PINK;
use notedeck_ui::app_images; use notedeck_ui::app_images;
@@ -171,7 +171,7 @@ fn scroll_area() -> ScrollArea {
fn add_account_button() -> Button<'static> { fn add_account_button() -> Button<'static> {
Button::image_and_text( Button::image_and_text(
app_images::add_account_image().fit_to_exact_size(Vec2::new(48.0, 48.0)), 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) .size(16.0)
// TODO: this color should not be hard coded. Find some way to add it to the visuals // TODO: this color should not be hard coded. Find some way to add it to the visuals
.color(PINK), .color(PINK),
@@ -180,5 +180,8 @@ fn add_account_button() -> Button<'static> {
} }
fn sign_out_button() -> egui::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"
)))
} }

View File

@@ -17,7 +17,7 @@ use crate::{
Damus, Damus,
}; };
use notedeck::{AppContext, Images, NotedeckTextStyle, UserAccount}; use notedeck::{tr, AppContext, Images, NotedeckTextStyle, UserAccount};
use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images}; use notedeck_ui::{anim::ICON_EXPANSION_MULTIPLE, app_images};
use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter}; use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
@@ -229,8 +229,11 @@ impl<'a> AddColumnView<'a> {
deck_author: Pubkey, deck_author: Pubkey,
) -> Option<AddColumnResponse> { ) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData { let algo_option = ColumnOptionData {
title: "Contact List", title: tr!("Contact List", "Title for contact list column"),
description: "Source the last note for each user in your contact list", description: tr!(
"Source the last note for each user in your contact list",
"Description for contact list column"
),
icon: app_images::home_image(), icon: app_images::home_image(),
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Decided( option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Decided(
ListKind::contact_list(deck_author), ListKind::contact_list(deck_author),
@@ -245,8 +248,11 @@ impl<'a> AddColumnView<'a> {
fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> { fn algo_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
let algo_option = ColumnOptionData { let algo_option = ColumnOptionData {
title: "Last Note per User", title: tr!("Last Note per User", "Title for last note per user column"),
description: "Show the last note for each user from a list", description: tr!(
"Show the last note for each user from a list",
"Description for last note per user column"
),
icon: app_images::algo_image(), icon: app_images::algo_image(),
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)), option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)),
}; };
@@ -291,8 +297,11 @@ impl<'a> AddColumnView<'a> {
let text_edit = key_state.get_acquire_textedit(|text| { let text_edit = key_state.get_acquire_textedit(|text| {
egui::TextEdit::singleline(text) egui::TextEdit::singleline(text)
.hint_text( .hint_text(
RichText::new("Enter the user's key (npub, hex, nip05) here...") RichText::new(tr!(
.text_style(NotedeckTextStyle::Body.text_style()), "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) .vertical_align(Align::Center)
.desired_width(f32::INFINITY) .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) 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 animation_rect = helper.get_animation_rect();
let cur_icon_width = helper.scale_1d_pos(min_icon_width); 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> { fn get_base_options(&self, ui: &mut Ui) -> Vec<ColumnOptionData> {
let mut vec = Vec::new(); let mut vec = Vec::new();
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Home", title: tr!("Home", "Title for Home column"),
description: "See notes from your contacts", description: tr!(
"See notes from your contacts",
"Description for Home column"
),
icon: app_images::home_image(), icon: app_images::home_image(),
option: AddColumnOption::Contacts(if self.cur_account.key.secret_key.is_some() { option: AddColumnOption::Contacts(if self.cur_account.key.secret_key.is_some() {
PubkeySource::DeckAuthor PubkeySource::DeckAuthor
@@ -455,32 +468,47 @@ impl<'a> AddColumnView<'a> {
}), }),
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Notifications", title: tr!("Notifications", "Title for notifications column"),
description: "Stay up to date with notifications and mentions", description: tr!(
"Stay up to date with notifications and mentions",
"Description for notifications column"
),
icon: app_images::notifications_image(ui.visuals().dark_mode), icon: app_images::notifications_image(ui.visuals().dark_mode),
option: AddColumnOption::UndecidedNotification, option: AddColumnOption::UndecidedNotification,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Universe", title: tr!("Universe", "Title for universe column"),
description: "See the whole nostr universe", description: tr!(
"See the whole nostr universe",
"Description for universe column"
),
icon: app_images::universe_image(), icon: app_images::universe_image(),
option: AddColumnOption::Universe, option: AddColumnOption::Universe,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Hashtags", title: tr!("Hashtags", "Title for hashtags column"),
description: "Stay up to date with a certain hashtag", description: tr!(
"Stay up to date with a certain hashtag",
"Description for hashtags column"
),
icon: app_images::hashtag_image(), icon: app_images::hashtag_image(),
option: AddColumnOption::UndecidedHashtag, option: AddColumnOption::UndecidedHashtag,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Individual", title: tr!("Individual", "Title for individual user column"),
description: "Stay up to date with someone's notes & replies", description: tr!(
"Stay up to date with someone's notes & replies",
"Description for individual user column"
),
icon: app_images::profile_image(), icon: app_images::profile_image(),
option: AddColumnOption::UndecidedIndividual, option: AddColumnOption::UndecidedIndividual,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Algo", title: tr!("Algo", "Title for algorithmic feeds column"),
description: "Algorithmic feeds to aid in note discovery", description: tr!(
"Algorithmic feeds to aid in note discovery",
"Description for algorithmic feeds column"
),
icon: app_images::algo_image(), icon: app_images::algo_image(),
option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)), option: AddColumnOption::Algo(AlgoOption::LastPerPubkey(Decision::Undecided)),
}); });
@@ -498,15 +526,24 @@ impl<'a> AddColumnView<'a> {
}; };
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Your Notifications", title: tr!("Your Notifications", "Title for your notifications column"),
description: "Stay up to date with your notifications and mentions", 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), icon: app_images::notifications_image(ui.visuals().dark_mode),
option: AddColumnOption::Notification(source), option: AddColumnOption::Notification(source),
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Someone else's Notifications", title: tr!(
description: "Stay up to date with someone else's notifications and mentions", "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), icon: app_images::notifications_image(ui.visuals().dark_mode),
option: AddColumnOption::ExternalNotification, option: AddColumnOption::ExternalNotification,
}); });
@@ -524,15 +561,24 @@ impl<'a> AddColumnView<'a> {
}; };
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Your Notes", title: tr!("Your Notes", "Title for your notes column"),
description: "Keep track of your notes & replies", description: tr!(
"Keep track of your notes & replies",
"Description for your notes column"
),
icon: app_images::profile_image(), icon: app_images::profile_image(),
option: AddColumnOption::Individual(source), option: AddColumnOption::Individual(source),
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Someone else's Notes", title: tr!(
description: "Stay up to date with someone else's notes & replies", "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(), icon: app_images::profile_image(),
option: AddColumnOption::ExternalIndividual, option: AddColumnOption::ExternalIndividual,
}); });
@@ -542,11 +588,15 @@ impl<'a> AddColumnView<'a> {
} }
fn find_user_button() -> impl Widget { 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 { 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 { struct ColumnOptionData {
title: &'static str, title: String,
description: &'static str, description: String,
icon: Image<'static>, icon: Image<'static>,
option: AddColumnOption, option: AddColumnOption,
} }
@@ -648,7 +698,7 @@ pub fn render_add_column_routes(
} }
// We have a decision on where we want the last per pubkey // 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 // add it to our list of timelines
AlgoOption::LastPerPubkey(Decision::Decided(list_kind)) => { AlgoOption::LastPerPubkey(Decision::Decided(list_kind)) => {
let txn = Transaction::new(ctx.ndb).unwrap(); let txn = Transaction::new(ctx.ndb).unwrap();
@@ -734,8 +784,11 @@ pub fn hashtag_ui(
let text_edit = egui::TextEdit::singleline(text_buffer) let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text( .hint_text(
RichText::new("Enter the desired hashtags here (for multiple space-separated)") RichText::new(tr!(
.text_style(NotedeckTextStyle::Body.text_style()), "Enter the desired hashtags here (for multiple space-separated)",
"Placeholder for hashtag input field"
))
.text_style(NotedeckTextStyle::Body.text_style()),
) )
.vertical_align(Align::Center) .vertical_align(Align::Center)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
@@ -790,7 +843,7 @@ mod tests {
let data_str = "column:algo_selection:last_per_pubkey"; let data_str = "column:algo_selection:last_per_pubkey";
let data = &data_str.split(":").collect::<Vec<&str>>(); let data = &data_str.split(":").collect::<Vec<&str>>();
let mut token_writer = TokenWriter::default(); 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 parsed = AddColumnRoute::parse_from_tokens(&mut parser).unwrap();
let expected = AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey); let expected = AddColumnRoute::Algo(AddAlgoRoute::LastPerPubkey);
parsed.serialize_tokens(&mut token_writer); parsed.serialize_tokens(&mut token_writer);

View File

@@ -12,6 +12,7 @@ use crate::{
use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder}; use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder};
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, Transaction}; use nostrdb::{Ndb, Transaction};
use notedeck::tr;
use notedeck::{Images, NotedeckTextStyle}; use notedeck::{Images, NotedeckTextStyle};
use notedeck_ui::app_images; use notedeck_ui::app_images;
use notedeck_ui::{ use notedeck_ui::{
@@ -192,12 +193,16 @@ impl<'a> NavTitle<'a> {
if ui.data_mut(|d| *d.get_temp_mut_or_default(id)) { if ui.data_mut(|d| *d.get_temp_mut_or_default(id)) {
let mut confirm_pressed = false; let mut confirm_pressed = false;
delete_button_resp.show_tooltip_ui(|ui| { 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() { if confirm_resp.clicked() {
confirm_pressed = true; 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)); ui.data_mut(|d| d.insert_temp(id, false));
} }
}); });
@@ -206,7 +211,8 @@ impl<'a> NavTitle<'a> {
} }
confirm_pressed confirm_pressed
} else { } 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 false
} }
} }
@@ -220,7 +226,10 @@ impl<'a> NavTitle<'a> {
// showing the hover text while showing the move tooltip causes some weird visuals // 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()) { 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() { if move_resp.clicked() {

View File

@@ -1,5 +1,6 @@
use crate::{app_style::deck_icon_font_sized, deck_state::DeckState}; use crate::{app_style::deck_icon_font_sized, deck_state::DeckState};
use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget}; use egui::{vec2, Button, Color32, Label, RichText, Stroke, Ui, Widget};
use notedeck::tr;
use notedeck::{NamedFontFamily, NotedeckTextStyle}; use notedeck::{NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
@@ -17,18 +18,16 @@ pub struct ConfigureDeckResponse {
pub name: String, pub name: String,
} }
static CREATE_TEXT: &str = "Create Deck";
impl<'a> ConfigureDeckView<'a> { impl<'a> ConfigureDeckView<'a> {
pub fn new(state: &'a mut DeckState) -> Self { pub fn new(state: &'a mut DeckState) -> Self {
Self { Self {
state, 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 { pub fn with_create_text(mut self, text: String) -> Self {
self.create_button_text = text.to_owned(); self.create_button_text = text;
self self
} }
@@ -39,22 +38,28 @@ impl<'a> ConfigureDeckView<'a> {
); );
padding(16.0, ui, |ui| { padding(16.0, ui, |ui| {
ui.add(Label::new( 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.add_space(8.0);
ui.text_edit_singleline(&mut self.state.deck_name); ui.text_edit_singleline(&mut self.state.deck_name);
ui.add_space(8.0); ui.add_space(8.0);
ui.add(Label::new( ui.add(Label::new(
RichText::new("We recommend short names") RichText::new(tr!(
.color(ui.visuals().noninteractive().fg_stroke.color) "We recommend short names",
.size(notedeck::fonts::get_font_size( "Hint for deck name input field"
ui.ctx(), ))
&NotedeckTextStyle::Small, .color(ui.visuals().noninteractive().fg_stroke.color)
)), .size(notedeck::fonts::get_font_size(
ui.ctx(),
&NotedeckTextStyle::Small,
)),
)); ));
ui.add_space(32.0); 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 if ui
.add(deck_icon( .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) { fn show_warnings(ui: &mut Ui, warn_no_icon: bool, warn_no_title: bool) {
if warn_no_icon || warn_no_title { let warning = if warn_no_title && warn_no_icon {
let messages = [ tr!(
if warn_no_title { "Please create a name for the deck and select an icon.",
"create a name for the deck" "Error message for missing deck name and icon"
} else { )
"" } else if warn_no_title {
}, tr!(
if warn_no_icon { "select an icon" } else { "" }, "Please create a name for the deck.",
]; "Error message for missing deck name"
let message = messages )
.iter() } else if warn_no_icon {
.filter(|&&m| !m.is_empty()) tr!(
.copied() "Please select an icon.",
.collect::<Vec<_>>() "Error message for missing deck icon"
.join(" and "); )
} else {
String::new()
};
ui.add( if !warning.is_empty() {
egui::Label::new( ui.add(egui::Label::new(RichText::new(warning).color(ui.visuals().error_fg_color)).wrap());
RichText::new(format!("Please {message}.")).color(ui.visuals().error_fg_color),
)
.wrap(),
);
} }
} }

View File

@@ -3,14 +3,13 @@ use egui::Widget;
use crate::deck_state::DeckState; use crate::deck_state::DeckState;
use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView}; use super::configure_deck::{ConfigureDeckResponse, ConfigureDeckView};
use notedeck::tr;
use notedeck_ui::padding; use notedeck_ui::padding;
pub struct EditDeckView<'a> { pub struct EditDeckView<'a> {
config_view: ConfigureDeckView<'a>, config_view: ConfigureDeckView<'a>,
} }
static EDIT_TEXT: &str = "Edit Deck";
pub enum EditDeckResponse { pub enum EditDeckResponse {
Edit(ConfigureDeckResponse), Edit(ConfigureDeckResponse),
Delete, Delete,
@@ -18,7 +17,8 @@ pub enum EditDeckResponse {
impl<'a> EditDeckView<'a> { impl<'a> EditDeckView<'a> {
pub fn new(state: &'a mut DeckState) -> Self { 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 } Self { config_view }
} }
@@ -44,7 +44,7 @@ fn delete_button() -> impl Widget {
let size = egui::vec2(108.0, 40.0); let size = egui::vec2(108.0, 40.0);
ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| { ui.allocate_ui_with_layout(size, egui::Layout::top_down(egui::Align::Center), |ui| {
ui.add( 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) .fill(ui.visuals().error_fg_color)
.min_size(size), .min_size(size),
) )

View File

@@ -7,7 +7,7 @@ use egui::{
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{Ndb, ProfileRecord, Transaction}; use nostrdb::{Ndb, ProfileRecord, Transaction};
use notedeck::{ 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::{ use notedeck_ui::{
app_images, colors, profile::display_name_widget, widgets::styled_button_toggleable, 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)); ui.data_mut(|d| d.insert_temp(id, cur_amount));
let resp = ui.add(styled_button_toggleable( let resp = ui.add(styled_button_toggleable(
"Send", &tr!("Send", "Button label to send a zap"),
colors::PINK, colors::PINK,
is_valid_zap(maybe_sats), is_valid_zap(maybe_sats),
)); ));
@@ -158,7 +158,8 @@ fn show_title(ui: &mut egui::Ui) {
ui.add_space(8.0); ui.add_space(8.0);
ui.add(egui::Label::new( 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 painter = ui.painter();
let sats_galley = painter.layout_no_wrap( 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()), NotedeckTextStyle::Heading4.get_font_id(ui.ctx()),
ui.visuals().noninteractive().text_color(), 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); .font(user_input_font);
let amount_resp = ui.add(Label::new( 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()) .text_style(NotedeckTextStyle::Heading3.text_style())
.color(ui.visuals().noninteractive().text_color()), .color(ui.visuals().noninteractive().text_color()),
)); ));
@@ -398,11 +402,11 @@ impl Display for ZapSelectionButton {
ZapSelectionButton::First => write!(f, "69"), ZapSelectionButton::First => write!(f, "69"),
ZapSelectionButton::Second => write!(f, "100"), ZapSelectionButton::Second => write!(f, "100"),
ZapSelectionButton::Third => write!(f, "420"), ZapSelectionButton::Third => write!(f, "420"),
ZapSelectionButton::Fourth => write!(f, "5K"), 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, "10K"), 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, "20K"), 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, "50K"), 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, "100K"), ZapSelectionButton::Eighth => write!(f, "{}", tr!("100K", "Zap amount button for 100000 sats. Abbreviated because the button is too small to display the full amount.")),
} }
} }
} }

View File

@@ -25,7 +25,7 @@ use notedeck_ui::{
NoteOptions, ProfilePic, 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; use tracing::error;
pub struct PostView<'a, 'd> { pub struct PostView<'a, 'd> {
@@ -180,7 +180,13 @@ impl<'a, 'd> PostView<'a, 'd> {
}; };
let textedit = TextEdit::multiline(&mut self.draft.buffer) 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) .frame(false)
.desired_width(ui.available_width()) .desired_width(ui.available_width())
.layouter(&mut layouter); .layouter(&mut layouter);
@@ -605,7 +611,7 @@ fn render_post_view_media(
fn post_button(interactive: bool) -> impl egui::Widget { fn post_button(interactive: bool) -> impl egui::Widget {
move |ui: &mut egui::Ui| { 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 { if interactive {
ui.add(button) ui.add(button)
} else { } else {

View File

@@ -2,7 +2,7 @@ use core::f32;
use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit}; use egui::{vec2, Button, CornerRadius, Layout, Margin, RichText, ScrollArea, TextEdit};
use enostr::ProfileState; 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}; use notedeck_ui::{profile::banner, ProfilePic};
pub struct EditProfileView<'a> { pub struct EditProfileView<'a> {
@@ -32,7 +32,14 @@ impl<'a> EditProfileView<'a> {
notedeck_ui::padding(padding, ui, |ui| { notedeck_ui::padding(padding, ui, |ui| {
ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| { ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
if 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() .clicked()
{ {
save = true; save = true;
@@ -62,42 +69,66 @@ impl<'a> EditProfileView<'a> {
); );
in_frame(ui, |ui| { 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"))); ui.add(singleline_textedit(self.state.str_mut("display_name")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(singleline_textedit(self.state.str_mut("name")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(multiline_textedit(self.state.str_mut("picture")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(multiline_textedit(self.state.str_mut("banner")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(multiline_textedit(self.state.str_mut("about")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(singleline_textedit(self.state.str_mut("website")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(multiline_textedit(self.state.str_mut("lud16")));
}); });
in_frame(ui, |ui| { 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"))); ui.add(singleline_textedit(self.state.str_mut("nip05")));
let Some(nip05) = self.state.nip05() else { let Some(nip05) = self.state.nip05() else {
@@ -121,9 +152,18 @@ impl<'a> EditProfileView<'a> {
ui.colored_label( ui.colored_label(
ui.visuals().noninteractive().fg_stroke.color, ui.visuals().noninteractive().fg_stroke.color,
RichText::new(if use_domain { 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 { } 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
)
}), }),
); );
}); });

View File

@@ -4,6 +4,7 @@ pub use edit::EditProfileView;
use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke}; use egui::{vec2, Color32, CornerRadius, Layout, Rect, RichText, ScrollArea, Sense, Stroke};
use enostr::Pubkey; use enostr::Pubkey;
use nostrdb::{ProfileRecord, Transaction}; use nostrdb::{ProfileRecord, Transaction};
use notedeck::tr;
use notedeck_ui::profile::follow_button; use notedeck_ui::profile::follow_button;
use tracing::error; 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 edit_icon_size = vec2(16.0, 16.0);
let galley = painter.layout( let galley = painter.layout(
"Edit Profile".to_owned(), tr!("Edit Profile", "Button label to edit user profile"),
NotedeckTextStyle::Button.get_font_id(ui.ctx()), NotedeckTextStyle::Button.get_font_id(ui.ctx()),
ui.visuals().text_color(), ui.visuals().text_color(),
rect.width(), rect.width(),

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use crate::ui::{Preview, PreviewConfig}; use crate::ui::{Preview, PreviewConfig};
use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2}; use egui::{Align, Button, CornerRadius, Frame, Id, Layout, Margin, Rgba, RichText, Ui, Vec2};
use enostr::{RelayPool, RelayStatus}; use enostr::{RelayPool, RelayStatus};
use notedeck::{NotedeckTextStyle, RelayAction}; use notedeck::{tr, NotedeckTextStyle, RelayAction};
use notedeck_ui::app_images; use notedeck_ui::app_images;
use notedeck_ui::{colors::PINK, padding}; use notedeck_ui::{colors::PINK, padding};
use tracing::debug; use tracing::debug;
@@ -26,7 +26,7 @@ impl RelayView<'_> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| { ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
ui.label( ui.label(
RichText::new("Relays") RichText::new(tr!("Relays", "Label for relay list section"))
.text_style(NotedeckTextStyle::Heading2.text_style()), .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 is_enabled = self.pool.is_valid_url(text_buffer);
let text_edit = egui::TextEdit::singleline(text_buffer) let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text( .hint_text(
RichText::new("Enter the relay here") RichText::new(tr!(
.text_style(NotedeckTextStyle::Body.text_style()), "Enter the relay here",
"Placeholder for relay input field"
))
.text_style(NotedeckTextStyle::Body.text_style()),
) )
.vertical_align(Align::Center) .vertical_align(Align::Center)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
@@ -175,7 +178,7 @@ impl<'a> RelayView<'a> {
fn add_relay_button() -> Button<'static> { fn add_relay_button() -> Button<'static> {
Button::image_and_text( Button::image_and_text(
app_images::add_relay_image().fit_to_exact_size(Vec2::new(48.0, 48.0)), 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) .size(16.0)
// TODO: this color should not be hard coded. Find some way to add it to the visuals // TODO: this color should not be hard coded. Find some way to add it to the visuals
.color(PINK), .color(PINK),
@@ -185,7 +188,8 @@ fn add_relay_button() -> Button<'static> {
fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static { fn add_relay_button2(is_enabled: bool) -> impl egui::Widget + 'static {
move |ui: &mut egui::Ui| -> egui::Response { 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) 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 bg_color = egui::lerp(Rgba::from(fg_color)..=Rgba::BLACK, 0.8).into();
let label_text = match status { let label_text = match status {
RelayStatus::Connected => "Connected", RelayStatus::Connected => tr!("Connected", "Status label for connected relay"),
RelayStatus::Connecting => "Connecting...", RelayStatus::Connecting => tr!("Connecting...", "Status label for connecting relay"),
RelayStatus::Disconnected => "Not Connected", RelayStatus::Disconnected => tr!("Not Connected", "Status label for disconnected relay"),
}; };
let frame = Frame::new() let frame = Frame::new()

View File

@@ -5,7 +5,7 @@ use state::TypingType;
use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView}; use crate::{timeline::TimelineTab, ui::timeline::TimelineTabView};
use egui_winit::clipboard::Clipboard; use egui_winit::clipboard::Clipboard;
use nostrdb::{Filter, Ndb, Transaction}; use nostrdb::{Filter, Ndb, Transaction};
use notedeck::{NoteAction, NoteContext, NoteRef}; use notedeck::{tr, tr_plural, NoteAction, NoteContext, NoteRef};
use notedeck_ui::{ use notedeck_ui::{
context_menu::{input_context, PasteBehavior}, context_menu::{input_context, PasteBehavior},
icons::search_icon, icons::search_icon,
@@ -119,15 +119,21 @@ impl<'a, 'd> SearchView<'a, 'd> {
note_action = self.show_search_results(ui); note_action = self.show_search_results(ui);
} }
SearchState::Searched => { SearchState::Searched => {
ui.label(format!( ui.label(tr_plural!(
"Got {} results for '{}'", "Got {count} result for '{query}'", // one
self.query.notes.notes.len(), "Got {count} results for '{query}'", // other
&self.query.string "Search results count", // comment
self.query.notes.notes.len(), // count
query = &self.query.string
)); ));
note_action = self.show_search_results(ui); note_action = self.show_search_results(ui);
} }
SearchState::Typing(TypingType::AutoSearch) => { 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); note_action = self.show_search_results(ui);
} }
@@ -282,7 +288,13 @@ fn search_box(
let response = ui.add_sized( let response = ui.add_sized(
[ui.available_width(), search_height], [ui.available_width(), search_height],
TextEdit::singleline(input) 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) //.desired_width(available_width - 32.0)
//.font(egui::FontId::new(font_size, egui::FontFamily::Proportional)) //.font(egui::FontId::new(font_size, egui::FontFamily::Proportional))
.margin(vec2(0.0, 8.0)) .margin(vec2(0.0, 8.0))

View File

@@ -12,7 +12,7 @@ use crate::{
route::Route, route::Route,
}; };
use notedeck::{Accounts, UserAccount}; use notedeck::{tr, Accounts, UserAccount};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
app_images, colors, View, app_images, colors, View,
@@ -105,7 +105,7 @@ impl<'a> DesktopSidePanel<'a> {
ui.add_space(8.0); ui.add_space(8.0);
ui.add(egui::Label::new( ui.add(egui::Label::new(
RichText::new("DECKS") RichText::new(tr!("DECKS", "Label for decks section in side panel"))
.size(11.0) .size(11.0)
.color(ui.visuals().noninteractive().fg_stroke.color), .color(ui.visuals().noninteractive().fg_stroke.color),
)); ));

View File

@@ -1,5 +1,5 @@
use egui::{vec2, Button, Label, Layout, RichText}; use egui::{vec2, Button, Label, Layout, RichText};
use notedeck::{NamedFontFamily, NotedeckTextStyle}; use notedeck::{tr, NamedFontFamily, NotedeckTextStyle};
use notedeck_ui::{colors::PINK, padding}; use notedeck_ui::{colors::PINK, padding};
use tracing::error; use tracing::error;
@@ -21,10 +21,18 @@ impl<'a> SupportView<'a> {
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body), notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()), egui::FontFamily::Name(NamedFontFamily::Bold.as_str().into()),
); );
ui.add(Label::new(RichText::new("Running into a bug?").font(font))); ui.add(Label::new(
ui.label(RichText::new("Step 1").text_style(NotedeckTextStyle::Heading3.text_style())); 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| { 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); let size = vec2(120.0, 40.0);
ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| { ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
let font_size = let font_size =
@@ -47,16 +55,19 @@ impl<'a> SupportView<'a> {
if let Some(logs) = self.support.get_most_recent_log() { if let Some(logs) = self.support.get_most_recent_log() {
ui.label( 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 size = vec2(80.0, 40.0);
let copy_button = Button::new(RichText::new("Copy").size( let copy_button = Button::new(
notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body), RichText::new(tr!("Copy", "Button label to copy logs")).size(
)) notedeck::fonts::get_font_size(ui.ctx(), &NotedeckTextStyle::Body),
),
)
.fill(PINK) .fill(PINK)
.min_size(size); .min_size(size);
padding(8.0, ui, |ui| { 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| { ui.allocate_ui_with_layout(size, Layout::top_down(egui::Align::Center), |ui| {
if ui.add(copy_button).clicked() { if ui.add(copy_button).clicked() {
ui.ctx().copy_text(logs.to_string()); 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 { fn open_email_button(font_size: f32, size: egui::Vec2) -> impl egui::Widget {
Button::new(RichText::new("Open Email").size(font_size)) Button::new(
.fill(PINK) RichText::new(tr!("Open Email", "Button label to open email client")).size(font_size),
.min_size(size) )
.fill(PINK)
.min_size(size)
} }

View File

@@ -8,7 +8,7 @@ use std::f32::consts::PI;
use tracing::{error, warn}; use tracing::{error, warn};
use crate::timeline::{TimelineCache, TimelineKind, TimelineTab, ViewFilter}; 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::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
NoteOptions, NoteView, NoteOptions, NoteView,
@@ -281,17 +281,19 @@ pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usi
let ind = state.index(); let ind = state.index();
let txt = match views[ind as usize].filter { let txt = match views[ind as usize].filter {
ViewFilter::Notes => "Notes", ViewFilter::Notes => tr!("Notes", "Label for notes-only filter"),
ViewFilter::NotesAndReplies => "Notes & Replies", 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 // underline
if state.is_selected() { if state.is_selected() {
let rect = res.rect; let rect = res.rect;
let underline = 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)] #[allow(deprecated)]
let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5; let underline_y = ui.painter().round_to_pixel(rect.bottom()) - 1.5;
return (underline, underline_y); return (underline, underline_y);

View File

@@ -1,6 +1,6 @@
use egui::{vec2, CornerRadius, Layout}; use egui::{vec2, CornerRadius, Layout};
use notedeck::{ use notedeck::{
get_current_wallet, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle, get_current_wallet, tr, Accounts, DefaultZapMsats, GlobalWallet, NotedeckTextStyle,
PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet, PendingDefaultZapState, Wallet, WalletError, WalletUIState, ZapWallet,
}; };
@@ -202,8 +202,11 @@ fn show_no_wallet(
ui.horizontal_wrapped(|ui| 's: { ui.horizontal_wrapped(|ui| 's: {
let text_edit = egui::TextEdit::singleline(&mut state.buf) let text_edit = egui::TextEdit::singleline(&mut state.buf)
.hint_text( .hint_text(
egui::RichText::new("Paste your NWC URI here...") egui::RichText::new(tr!(
.text_style(notedeck::NotedeckTextStyle::Body.text_style()), "Paste your NWC URI here...",
"Placeholder text for NWC URI input"
))
.text_style(notedeck::NotedeckTextStyle::Body.text_style()),
) )
.vertical_align(egui::Align::Center) .vertical_align(egui::Align::Center)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
@@ -218,8 +221,14 @@ fn show_no_wallet(
}; };
let error_str = match error_msg { let error_str = match error_msg {
WalletError::InvalidURI => "Invalid NWC URI", WalletError::InvalidURI => tr!(
WalletError::NoWallet => "Add a wallet to continue", "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); ui.colored_label(ui.visuals().warn_fg_color, error_str);
}); });
@@ -229,15 +238,21 @@ fn show_no_wallet(
if show_local_only { if show_local_only {
ui.checkbox( ui.checkbox(
&mut state.for_local_only, &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.add_space(8.0);
} }
ui.with_layout(Layout::top_down(egui::Align::Center), |ui| { ui.with_layout(Layout::top_down(egui::Align::Center), |ui| {
ui.add(styled_button("Add Wallet", notedeck_ui::colors::PINK)) ui.add(styled_button(
.clicked() tr!("Add Wallet", "Button label to add a wallet").as_str(),
.then_some(WalletAction::SaveURI) notedeck_ui::colors::PINK,
))
.clicked()
.then_some(WalletAction::SaveURI)
}) })
.inner .inner
} }
@@ -268,7 +283,10 @@ fn show_with_wallet(
ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: { ui.with_layout(Layout::bottom_up(egui::Align::Min), |ui| 's: {
if ui 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() .clicked()
{ {
action = Some(WalletAction::Delete); action = Some(WalletAction::Delete);
@@ -280,7 +298,10 @@ fn show_with_wallet(
&& ui && ui
.checkbox( .checkbox(
&mut false, &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() .clicked()
{ {
@@ -308,7 +329,7 @@ fn show_default_zap(ui: &mut egui::Ui, state: &mut DefaultZapState) -> Option<Wa
vec2(ui.available_width(), 50.0), vec2(ui.available_width(), 50.0),
egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true), egui::Layout::left_to_right(egui::Align::Center).with_main_wrap(true),
|ui| { |ui| {
ui.label("Default amount per zap: "); ui.label(tr!("Default amount per zap: ", "Label for default zap amount input"));
match state { match state {
DefaultZapState::Pending(pending_default_zap_state) => { DefaultZapState::Pending(pending_default_zap_state) => {
let text = &mut pending_default_zap_state.amount_sats; 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.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 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() .clicked()
{ {
action = Some(WalletAction::SetDefaultZapSats(text.to_string())); 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) { if let Some(wallet_action) = show_valid_msats(ui, **msats) {
action = Some(wallet_action); 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 DefaultZapState::Pending(pending) = state {
if let Some(error_message) = &pending.error_message { if let Some(error_message) = &pending.error_message {
let msg_str = match 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); 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 let resp = resp
.on_hover_cursor(egui::CursorIcon::PointingHand) .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); let painter = ui.painter_at(resp.rect);

View File

@@ -4,7 +4,7 @@ use crate::{
}; };
use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers};
use nostrdb::{Ndb, Transaction}; 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}; use notedeck_ui::{app_images, icons::search_icon, jobs::JobsCache, NoteOptions, ProfilePic};
/// DaveUi holds all of the data it needs to render itself /// DaveUi holds all of the data it needs to render itself
@@ -138,7 +138,7 @@ impl<'a> DaveUi<'a> {
if self.trial { if self.trial {
ui.add(egui::Label::new( ui.add(egui::Label::new(
egui::RichText::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(), .weak(),
)); ));
@@ -308,7 +308,13 @@ impl<'a> DaveUi<'a> {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.with_layout(Layout::right_to_left(Align::Max), |ui| { ui.with_layout(Layout::right_to_left(Align::Max), |ui| {
let mut dave_response = DaveResponse::none(); 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(); dave_response = DaveResponse::send();
} }
@@ -322,7 +328,13 @@ impl<'a> DaveUi<'a> {
}, },
Key::Enter, 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), .frame(false),
); );

View File

@@ -1,6 +1,6 @@
use egui::{Rect, Vec2}; use egui::{Rect, Vec2};
use nostrdb::NoteKey; use nostrdb::NoteKey;
use notedeck::{BroadcastContext, NoteContextSelection}; use notedeck::{tr, BroadcastContext, NoteContextSelection};
pub struct NoteContextButton { pub struct NoteContextButton {
put_at: Option<Rect>, put_at: Option<Rect>,
@@ -109,31 +109,78 @@ impl NoteContextButton {
) -> Option<NoteContextSelection> { ) -> Option<NoteContextSelection> {
let mut context_selection: Option<NoteContextSelection> = None; 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| { stationary_arbitrary_menu_button(ui, button_response, |ui| {
ui.set_max_width(200.0); 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); context_selection = Some(NoteContextSelection::CopyText);
ui.close_menu(); 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); context_selection = Some(NoteContextSelection::CopyPubkey);
ui.close_menu(); 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); context_selection = Some(NoteContextSelection::CopyNoteId);
ui.close_menu(); 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); context_selection = Some(NoteContextSelection::CopyNoteJSON);
ui.close_menu(); 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( context_selection = Some(NoteContextSelection::Broadcast(
BroadcastContext::Everywhere, BroadcastContext::Everywhere,
)); ));
ui.close_menu(); 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( context_selection = Some(NoteContextSelection::Broadcast(
BroadcastContext::LocalNetwork, BroadcastContext::LocalNetwork,
)); ));

View File

@@ -6,7 +6,7 @@ use egui::{
}; };
use notedeck::{ use notedeck::{
fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url, 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, TexturedImage, TexturesCache, UrlMimes,
}; };
@@ -636,7 +636,10 @@ fn render_full_screen_media(
fn copy_link(url: &str, img_resp: &Response) { fn copy_link(url: &str, img_resp: &Response) {
img_resp.context_menu(|ui| { 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.ctx().copy_text(url.to_owned());
ui.close_menu(); 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(), text_style.font_family(),
); );
let info_galley = painter.layout( 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(), animation_fontid.clone(),
ui.visuals().text_color(), ui.visuals().text_color(),
render_rect.width() / 2.0, render_rect.width() / 2.0,
); );
let load_galley = painter.layout_no_wrap( 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, animation_fontid,
egui::Color32::BLACK, egui::Color32::BLACK,
// ui.visuals().widgets.inactive.bg_fill, // ui.visuals().widgets.inactive.bg_fill,

View File

@@ -27,7 +27,7 @@ use nostrdb::{Ndb, Note, NoteKey, ProfileRecord, Transaction};
use notedeck::{ use notedeck::{
name::get_display_name, name::get_display_name,
note::{NoteAction, NoteContext, ZapAction}, note::{NoteAction, NoteContext, ZapAction},
AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned, tr, AnyZapState, CachedNote, ContextSelection, NoteCache, NoteZapTarget, NoteZapTargetOwned,
NotedeckTextStyle, ZapTarget, Zaps, NotedeckTextStyle, ZapTarget, Zaps,
}; };
@@ -308,7 +308,7 @@ impl<'a, 'd> NoteView<'a, 'd> {
let color = ui.style().visuals.noninteractive().fg_stroke.color; let color = ui.style().visuals.noninteractive().fg_stroke.color;
ui.add_space(4.0); ui.add_space(4.0);
ui.label( ui.label(
RichText::new("Reposted") RichText::new(tr!("Reposted", "Label for reposted notes"))
.color(color) .color(color)
.text_style(style.text_style()), .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 let put_resp = ui
.put(rect, img.max_width(size)) .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) 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 let put_resp = ui
.put(rect, repost_icon(ui.visuals().dark_mode).max_width(size)) .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) 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 expand_size = 5.0; // from hover_expand_small
let rect = rect.translate(egui::vec2(-(expand_size / 2.0), 0.0)); 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) resp.union(put_resp)
} }

View File

@@ -1,9 +1,190 @@
use egui::{Label, RichText, Sense}; use egui::{Label, RichText, Sense};
use nostrdb::{Note, NoteReply, Transaction}; use nostrdb::{NoteReply, Transaction};
use super::NoteOptions; use super::NoteOptions;
use crate::{jobs::JobsCache, note::NoteView, Mention}; use crate::{jobs::JobsCache, note::NoteView, Mention};
use notedeck::{NoteAction, NoteContext}; use notedeck::{tr, NoteAction, NoteContext};
// Rich text segment types for internationalized rendering
#[derive(Debug, Clone)]
pub enum TextSegment {
Plain(String),
UserMention([u8; 32]), // pubkey
ThreadUserMention([u8; 32]), // pubkey
NoteLink([u8; 32]),
ThreadLink([u8; 32]),
}
// Helper function to parse i18n template strings with placeholders
fn parse_i18n_template(template: &str) -> Vec<TextSegment> {
let mut segments = Vec::new();
let mut current_text = String::new();
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
// Save any accumulated plain text
if !current_text.is_empty() {
segments.push(TextSegment::Plain(current_text.clone()));
current_text.clear();
}
// Parse placeholder
let mut placeholder = String::new();
for ch in chars.by_ref() {
if ch == '}' {
break;
}
placeholder.push(ch);
}
// Handle different placeholder types
match placeholder.as_str() {
// Placeholder values will be filled later.
"user" => segments.push(TextSegment::UserMention([0; 32])),
"thread_user" => segments.push(TextSegment::ThreadUserMention([0; 32])),
"note" => segments.push(TextSegment::NoteLink([0; 32])),
"thread" => segments.push(TextSegment::ThreadLink([0; 32])),
_ => {
// Unknown placeholder, treat as plain text
current_text.push_str(&format!("{{{placeholder}}}"));
}
}
} else {
current_text.push(ch);
}
}
// Add any remaining plain text
if !current_text.is_empty() {
segments.push(TextSegment::Plain(current_text));
}
segments
}
// Helper function to fill in the actual data for placeholders
fn fill_template_data(
mut segments: Vec<TextSegment>,
reply_pubkey: &[u8; 32],
reply_note_id: &[u8; 32],
root_pubkey: Option<&[u8; 32]>,
root_note_id: Option<&[u8; 32]>,
) -> Vec<TextSegment> {
for segment in &mut segments {
match segment {
TextSegment::UserMention(pubkey) if *pubkey == [0; 32] => {
*pubkey = *reply_pubkey;
}
TextSegment::ThreadUserMention(pubkey) if *pubkey == [0; 32] => {
*pubkey = *root_pubkey.unwrap_or(reply_pubkey);
}
TextSegment::NoteLink(note_id) if *note_id == [0; 32] => {
*note_id = *reply_note_id;
}
TextSegment::ThreadLink(note_id) if *note_id == [0; 32] => {
*note_id = *root_note_id.unwrap_or(reply_note_id);
}
_ => {}
}
}
segments
}
// Main rendering function for text segments
#[allow(clippy::too_many_arguments)]
fn render_text_segments(
ui: &mut egui::Ui,
segments: &[TextSegment],
txn: &Transaction,
note_context: &mut NoteContext,
note_options: NoteOptions,
jobs: &mut JobsCache,
size: f32,
selectable: bool,
) -> Option<NoteAction> {
let mut note_action: Option<NoteAction> = None;
let visuals = ui.visuals();
let color = visuals.noninteractive().fg_stroke.color;
let link_color = visuals.hyperlink_color;
for segment in segments {
match segment {
TextSegment::Plain(text) => {
ui.add(
Label::new(RichText::new(text).size(size).color(color)).selectable(selectable),
);
}
TextSegment::UserMention(pubkey) | TextSegment::ThreadUserMention(pubkey) => {
let action = Mention::new(note_context.ndb, note_context.img_cache, txn, pubkey)
.size(size)
.selectable(selectable)
.show(ui);
if action.is_some() {
note_action = action;
}
}
TextSegment::NoteLink(note_id) => {
if let Ok(note) = note_context.ndb.get_note_by_id(txn, note_id) {
let r = ui.add(
Label::new(
RichText::new(tr!("note", "Link text for note references"))
.size(size)
.color(link_color),
)
.sense(Sense::click())
.selectable(selectable),
);
if r.clicked() {
// TODO: jump to note
}
if r.hovered() {
r.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(400.0);
NoteView::new(note_context, &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);
});
}
}
}
}
}
note_action
}
#[must_use = "Please handle the resulting note action"] #[must_use = "Please handle the resulting note action"]
#[profiling::function] #[profiling::function]
@@ -15,163 +196,109 @@ pub fn reply_desc(
note_options: NoteOptions, note_options: NoteOptions,
jobs: &mut JobsCache, jobs: &mut JobsCache,
) -> Option<NoteAction> { ) -> Option<NoteAction> {
let mut note_action: Option<NoteAction> = None;
let size = 10.0; let size = 10.0;
let selectable = false; let selectable = false;
let visuals = ui.visuals();
let color = visuals.noninteractive().fg_stroke.color;
let link_color = visuals.hyperlink_color;
// note link renderer helper
let note_link = |ui: &mut egui::Ui,
note_context: &mut NoteContext,
text: &str,
note: &Note<'_>,
jobs: &mut JobsCache| {
let r = ui.add(
Label::new(RichText::new(text).size(size).color(link_color))
.sense(Sense::click())
.selectable(selectable),
);
if r.clicked() {
// TODO: jump to note
}
if r.hovered() {
r.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(400.0);
NoteView::new(note_context, note, note_options, jobs)
.actionbar(false)
.wide(true)
.show(ui);
});
}
};
ui.add(Label::new(RichText::new("replying to").size(size).color(color)).selectable(selectable));
let reply = note_reply.reply()?; let reply = note_reply.reply()?;
let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) { let reply_note = if let Ok(reply_note) = note_context.ndb.get_note_by_id(txn, reply.id) {
reply_note reply_note
} else { } else {
ui.add(Label::new(RichText::new("a note").size(size).color(color)).selectable(selectable)); // Handle case where reply note is not found
return None; 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() { let segments = if note_reply.is_reply_to_root() {
// We're replying to the root, let's show this // Template: "replying to {user}'s {thread}"
let action = Mention::new( let template = tr!(
note_context.ndb, "replying to {user}'s {thread}",
note_context.img_cache, "Template for replying to root thread",
txn, user = "{user}",
thread = "{thread}"
);
let segments = parse_i18n_template(&template);
fill_template_data(
segments,
reply_note.pubkey(), 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() { } 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 let Ok(root_note) = note_context.ndb.get_note_by_id(txn, root.id) {
if root_note.pubkey() == reply_note.pubkey() { if root_note.pubkey() == reply_note.pubkey() {
// simply "replying to bob's note" when replying to bob in his thread // Template: "replying to {user}'s {note}"
let action = Mention::new( let template = tr!(
note_context.ndb, "replying to {user}'s {note}",
note_context.img_cache, "Template for replying to user's note",
txn, user = "{user}",
reply_note.pubkey(), note = "{note}"
)
.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),
); );
let segments = parse_i18n_template(&template);
note_link(ui, note_context, "note", &reply_note, jobs); fill_template_data(segments, reply_note.pubkey(), reply.id, None, None)
} else { } else {
// replying to bob in alice's thread // Template: "replying to {reply_user}'s {note} in {thread_user}'s {thread}"
// This would need more sophisticated placeholder handling
let action = Mention::new( let template = tr!(
note_context.ndb, "replying to {user}'s {note} in {thread_user}'s {thread}",
note_context.img_cache, "Template for replying to note in different user's thread",
txn, user = "{user}",
note = "{note}",
thread_user = "{thread_user}",
thread = "{thread}"
);
let segments = parse_i18n_template(&template);
fill_template_data(
segments,
reply_note.pubkey(), reply_note.pubkey(),
reply.id,
Some(root_note.pubkey()),
Some(root.id),
) )
.size(size)
.selectable(selectable)
.show(ui);
if action.is_some() {
note_action = action;
}
ui.add(
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
);
note_link(ui, note_context, "note", &reply_note, jobs);
ui.add(
Label::new(RichText::new("in").size(size).color(color)).selectable(selectable),
);
let action = Mention::new(
note_context.ndb,
note_context.img_cache,
txn,
root_note.pubkey(),
)
.size(size)
.selectable(selectable)
.show(ui);
if action.is_some() {
note_action = action;
}
ui.add(
Label::new(RichText::new("'s").size(size).color(color)).selectable(selectable),
);
note_link(ui, note_context, "thread", &root_note, jobs);
} }
} else { } else {
let action = Mention::new( // Template: "replying to {user} in someone's thread"
note_context.ndb, let template = tr!(
note_context.img_cache, "replying to {user} in someone's thread",
txn, "Template for replying to user in unknown thread",
reply_note.pubkey(), user = "{user}"
)
.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),
); );
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,
)
} }

View File

@@ -3,7 +3,7 @@ use egui::{Frame, Label, RichText};
use egui_extras::Size; use egui_extras::Size;
use nostrdb::ProfileRecord; 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}; use super::{about_section_widget, banner, display_name_widget};
@@ -96,7 +96,7 @@ impl egui::Widget for SimpleProfilePreview<'_, '_> {
if !self.is_nsec { if !self.is_nsec {
ui.add( ui.add(
Label::new( Label::new(
RichText::new("Read only") RichText::new(tr!("Read only", "Label for read-only profile mode"))
.size(notedeck::fonts::get_font_size( .size(notedeck::fonts::get_font_size(
ui.ctx(), ui.ctx(),
&NotedeckTextStyle::Tiny, &NotedeckTextStyle::Tiny,

View File

@@ -1,6 +1,6 @@
use egui::{Color32, RichText, Widget}; use egui::{Color32, RichText, Widget};
use nostrdb::ProfileRecord; use nostrdb::ProfileRecord;
use notedeck::fonts::NamedFontFamily; use notedeck::{fonts::NamedFontFamily, tr};
pub struct Username<'a> { pub struct Username<'a> {
profile: Option<&'a ProfileRecord<'a>>, profile: Option<&'a ProfileRecord<'a>>,
@@ -52,7 +52,11 @@ impl Widget for Username<'_> {
} }
} }
} else { } 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 { if let Some(col) = color {
txt = txt.color(col) txt = txt.color(col)
} }