app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit cc1bb85b257dbf308780b2721ea2d27d23261ca5
parent ec8162461c197159ed60f648c21ef0a93c75f81d
Author: triesap <tyson@radroots.org>
Date:   Sun,  7 Jun 2026 14:55:56 -0700

ui: bootstrap account settings view

Diffstat:
Mcrates/desktop/src/source_guards.rs | 35++++++++++++++++++++++++++++++++++-
Mcrates/desktop/src/window.rs | 356+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/i18n/src/keys.rs | 22+++++++++++++++++++++-
Mcrates/i18n/src/lib.rs | 15++++++++++++++-
Mi18n/locales/en/messages.json | 22+++++++++++++++++++++-
5 files changed, 438 insertions(+), 12 deletions(-)

diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -43,6 +43,16 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "account-more", "account-profile-change-photo", "account-profile-remove-photo", + "account-settings-add-relay", + "account-settings-blossom-product-photos", + "account-settings-blossom-profile-farm-media", + "account-settings-relay-localhost-8080", + "account-settings-relay-localhost-8081", + "account-settings-remove-relay-localhost-8080", + "account-settings-remove-relay-localhost-8081", + "account-settings-reset-blossom", + "account-settings-reset-relays", + "account-settings-save", "account-farm-continue-location", "account-farm-save-draft", "account-farm-view-profile", @@ -184,6 +194,7 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "home-today-open-products-low-stock", "home-products-scroll", "home-today-scroll", + "http://localhost:8082", "today-reminder-chip", "https://auth.example/challenge", "identity", @@ -342,6 +353,8 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "switch_relays", "startup-title-radroots", "startup-title-starting", + "ws://localhost:8080", + "ws://localhost:8081", "wss://relay.example", "wss://relay.radroots.example", "{currency_code} {dollars}.{cents:02}", @@ -363,7 +376,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::AccountTabProfile", "AppTextKey::AccountTabFarmDetails", "AppTextKey::AccountTabPreferences", - "AppTextKey::AccountTabSecurity", + "AppTextKey::AccountTabSettings", "AppTextKey::AccountNotImplemented", "AppTextKey::AccountProfilePersonalDetailsTitle", "AppTextKey::AccountProfilePictureLabel", @@ -440,6 +453,26 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::AccountFarmDetailsFarmTypeNurseryPlantFarm", "AppTextKey::AccountFarmDetailsFarmTypeMixedFarm", "AppTextKey::AccountFarmDetailsFarmTypeOther", + "AppTextKey::AccountSettingsTitle", + "AppTextKey::AccountSettingsNostrRelaysTitle", + "AppTextKey::AccountSettingsNostrRelaysHelper", + "AppTextKey::AccountSettingsRelayAccessReadWrite", + "AppTextKey::AccountSettingsRelayAccessReadOnly", + "AppTextKey::AccountSettingsRemoveRelayAction", + "AppTextKey::AccountSettingsAddRelayLabel", + "AppTextKey::AccountSettingsAddRelayPlaceholder", + "AppTextKey::AccountSettingsAddRelayAction", + "AppTextKey::AccountSettingsResetRelaysAction", + "AppTextKey::AccountSettingsDefaultRelaysNote", + "AppTextKey::AccountSettingsBlossomServerTitle", + "AppTextKey::AccountSettingsBlossomServerHelper", + "AppTextKey::AccountSettingsBlossomServerUrlLabel", + "AppTextKey::AccountSettingsBlossomProductPhotosLabel", + "AppTextKey::AccountSettingsBlossomProfileFarmMediaLabel", + "AppTextKey::AccountSettingsResetBlossomServerAction", + "AppTextKey::AccountSettingsBlossomConnectionHealthy", + "AppTextKey::AccountSettingsBlossomUploadsAvailable", + "AppTextKey::AccountSettingsSaveChangesAction", "AppTextKey::HomeSetupBackAction", "AppTextKey::HomeSetupBrowseMarketplaceAction", "AppTextKey::HomeSetupConnectSignerAction", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -30,12 +30,12 @@ use radroots_app_sync::{ SyncConflictKind, SyncConflictResolutionStatus, SyncConflictSeverity, }; use radroots_app_ui::{ - APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, + APP_UI_THEME, AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, AppSegmentButtonIconSpec as IconSegmentButtonSpec, AppUnderlineTabSpec, LabelValueRow, SettingsPreferencesGeneralRowState, app_button_account_selector_row as account_selector_row, app_button_card, app_button_choice as choice_button, - app_button_compact as action_button_compact, app_button_list_row as list_row_button, - app_button_primary as action_button_primary, + app_button_compact as action_button_compact, app_button_icon as action_icon_button, + app_button_list_row as list_row_button, app_button_primary as action_button_primary, app_button_primary_disabled as action_button_primary_disabled, app_button_primary_full_width as action_button_primary_full_width, app_button_secondary as action_button, app_button_secondary_disabled as action_button_disabled, @@ -177,7 +177,7 @@ enum AccountTab { Profile, FarmDetails, Preferences, - Security, + Settings, } impl AccountTab { @@ -185,7 +185,7 @@ impl AccountTab { Self::Profile, Self::FarmDetails, Self::Preferences, - Self::Security, + Self::Settings, ]; const fn text_key(self) -> AppTextKey { @@ -193,14 +193,15 @@ impl AccountTab { Self::Profile => AppTextKey::AccountTabProfile, Self::FarmDetails => AppTextKey::AccountTabFarmDetails, Self::Preferences => AppTextKey::AccountTabPreferences, - Self::Security => AppTextKey::AccountTabSecurity, + Self::Settings => AppTextKey::AccountTabSettings, } } const fn panel_text_key(self) -> AppTextKey { match self { Self::Profile | Self::FarmDetails => self.text_key(), - Self::Preferences | Self::Security => AppTextKey::AccountNotImplemented, + Self::Preferences => AppTextKey::AccountNotImplemented, + Self::Settings => AppTextKey::AccountSettingsTitle, } } @@ -398,6 +399,26 @@ impl AccountFarmProfileFormState { } } +#[derive(Clone)] +struct AccountSettingsFormState { + add_relay_input: Entity<InputState>, + blossom_server_input: Entity<InputState>, +} + +impl AccountSettingsFormState { + fn new(window: &mut Window, cx: &mut Context<HomeView>) -> Self { + Self { + add_relay_input: cx.new(|cx| { + InputState::new(window, cx) + .placeholder(app_text(AppTextKey::AccountSettingsAddRelayPlaceholder)) + }), + blossom_server_input: cx.new(|cx| { + InputState::new(window, cx).default_value(ACCOUNT_SETTINGS_DEFAULT_BLOSSOM_SERVER) + }), + } + } +} + fn account_farm_profile_select_state( value_keys: &[AppTextKey], window: &mut Window, @@ -547,6 +568,7 @@ pub struct HomeView { selected_account_tab: AccountTab, account_profile_form: Option<AccountProfileFormState>, account_farm_profile_form: Option<AccountFarmProfileFormState>, + account_settings_form: Option<AccountSettingsFormState>, account_farm_profile_textarea_wrap_ready: bool, account_farm_profile_textarea_wrap_requested: bool, relay_client: Option<RadrootsNostrClient>, @@ -671,6 +693,7 @@ impl HomeView { selected_account_tab: AccountTab::default(), account_profile_form: None, account_farm_profile_form: None, + account_settings_form: None, account_farm_profile_textarea_wrap_ready: false, account_farm_profile_textarea_wrap_requested: false, relay_client: None, @@ -5236,9 +5259,13 @@ impl HomeView { account_farm_profile_panel(&form, self.account_farm_profile_textarea_wrap_ready, cx) .into_any_element() } - AccountTab::Preferences | AccountTab::Security => { + AccountTab::Preferences => { account_placeholder_panel(selected_tab.panel_text_key()).into_any_element() } + AccountTab::Settings => { + let form = self.account_settings_form(window, cx).clone(); + account_settings_panel(&form, cx).into_any_element() + } }; app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) @@ -5292,6 +5319,21 @@ impl HomeView { form } + fn account_settings_form( + &mut self, + window: &mut Window, + cx: &mut Context<Self>, + ) -> &AccountSettingsFormState { + if self.account_settings_form.is_none() { + self.account_settings_form = Some(AccountSettingsFormState::new(window, cx)); + } + + let Some(form) = self.account_settings_form.as_ref() else { + unreachable!(); + }; + form + } + fn prepare_account_farm_profile_textarea_wrap( &mut self, window: &mut Window, @@ -9278,6 +9320,9 @@ fn account_profile_labeled_control( const ACCOUNT_FORM_CONTROL_HEIGHT_PX: f32 = 28.0; const ACCOUNT_FORM_CONTROL_RADIUS_PX: f32 = 8.0; +const ACCOUNT_SETTINGS_DEFAULT_BLOSSOM_SERVER: &str = "http://localhost:8082"; +const ACCOUNT_SETTINGS_RELAY_LOCALHOST_8080: &str = "ws://localhost:8080"; +const ACCOUNT_SETTINGS_RELAY_LOCALHOST_8081: &str = "ws://localhost:8081"; fn account_form_text_input(input: &Entity<InputState>) -> impl IntoElement { gpui::Styled::h( @@ -9816,6 +9861,301 @@ fn account_farm_profile_action_row(cx: &mut Context<HomeView>) -> impl IntoEleme ) } +fn account_settings_panel( + form: &AccountSettingsFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(account_section_heading(AppTextKey::AccountSettingsTitle)) + .child( + div() + .w_full() + .flex() + .items_start() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child( + div() + .flex_1() + .min_w_0() + .child(account_settings_nostr_relays_card(form, cx)), + ) + .child( + div() + .flex_basis(relative(0.4)) + .min_w(px(320.0)) + .child(account_settings_blossom_server_card(form, cx)), + ), + ) + .child(action_button_primary_full_width( + "account-settings-save", + app_shared_text(AppTextKey::AccountSettingsSaveChangesAction), + |_, _, _| {}, + cx, + )) +} + +fn account_settings_nostr_relays_card( + form: &AccountSettingsFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { + account_settings_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(account_farm_profile_title_block( + AppTextKey::AccountSettingsNostrRelaysTitle, + AppTextKey::AccountSettingsNostrRelaysHelper, + )) + .child(account_settings_relay_list(cx)) + .child(account_settings_add_relay_controls(form, cx)) + .child(div().w(px(160.0)).child(action_button_full_width( + "account-settings-reset-relays", + app_shared_text(AppTextKey::AccountSettingsResetRelaysAction), + |_, _, _| {}, + cx, + ))) + .child(account_settings_helper_note( + AppTextKey::AccountSettingsDefaultRelaysNote, + )), + ) +} + +fn account_settings_relay_list(cx: &mut Context<HomeView>) -> impl IntoElement { + app_stack_v(0.0) + .w_full() + .border_1() + .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) + .overflow_hidden() + .child(account_settings_relay_row( + "account-settings-relay-localhost-8080", + "account-settings-remove-relay-localhost-8080", + ACCOUNT_SETTINGS_RELAY_LOCALHOST_8080, + AppTextKey::AccountSettingsRelayAccessReadWrite, + true, + cx, + )) + .child(account_settings_relay_row( + "account-settings-relay-localhost-8081", + "account-settings-remove-relay-localhost-8081", + ACCOUNT_SETTINGS_RELAY_LOCALHOST_8081, + AppTextKey::AccountSettingsRelayAccessReadOnly, + true, + cx, + )) +} + +fn account_settings_relay_row( + checkbox_id: &'static str, + remove_id: &'static str, + relay_url: &'static str, + access_key: AppTextKey, + enabled: bool, + cx: &mut Context<HomeView>, +) -> impl IntoElement { + div() + .w_full() + .min_h(px(52.0)) + .px(px(APP_UI_THEME.foundation.spacing.medium_px)) + .py(px(APP_UI_THEME.foundation.spacing.small_px)) + .flex() + .items_center() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child(div().flex_1().min_w_0().child(app_checkbox_field( + AppCheckboxFieldSpec::new(checkbox_id, relay_url, Option::<SharedString>::None), + enabled, + cx, + |_, _, _| {}, + ))) + .child( + div() + .w(px(104.0)) + .text_size(px(APP_UI_THEME.foundation.typography.settings_row_text_px)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(app_shared_text(access_key)), + ) + .child(action_icon_button( + AppIconButtonSpec::new( + remove_id, + app_shared_text(AppTextKey::AccountSettingsRemoveRelayAction), + IconName::Delete, + ), + |_, _, _| {}, + cx, + )) +} + +fn account_settings_add_relay_controls( + form: &AccountSettingsFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { + app_stack_v(APP_UI_THEME.foundation.spacing.tight_px) + .w_full() + .child( + div() + .w_full() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(app_shared_text(AppTextKey::AccountSettingsAddRelayLabel)), + ) + .child( + div() + .w_full() + .flex() + .items_center() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child( + div() + .flex_1() + .min_w_0() + .child(account_form_text_input(&form.add_relay_input)), + ) + .child(action_button( + "account-settings-add-relay", + app_shared_text(AppTextKey::AccountSettingsAddRelayAction), + |_, _, _| {}, + cx, + )), + ) +} + +fn account_settings_blossom_server_card( + form: &AccountSettingsFormState, + cx: &mut Context<HomeView>, +) -> impl IntoElement { + account_settings_card( + app_stack_v(APP_UI_THEME.shells.home_stack_gap_px) + .w_full() + .child(account_farm_profile_title_block( + AppTextKey::AccountSettingsBlossomServerTitle, + AppTextKey::AccountSettingsBlossomServerHelper, + )) + .child(account_profile_labeled_control( + AppTextKey::AccountSettingsBlossomServerUrlLabel, + account_form_text_input(&form.blossom_server_input), + )) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "account-settings-blossom-product-photos", + app_shared_text(AppTextKey::AccountSettingsBlossomProductPhotosLabel), + Option::<SharedString>::None, + ), + true, + cx, + |_, _, _| {}, + )) + .child(app_checkbox_field( + AppCheckboxFieldSpec::new( + "account-settings-blossom-profile-farm-media", + app_shared_text(AppTextKey::AccountSettingsBlossomProfileFarmMediaLabel), + Option::<SharedString>::None, + ), + true, + cx, + |_, _, _| {}, + )) + .child(div().w(px(172.0)).child(action_button_full_width( + "account-settings-reset-blossom", + app_shared_text(AppTextKey::AccountSettingsResetBlossomServerAction), + |_, _, _| {}, + cx, + ))) + .child(account_settings_blossom_health_card()), + ) +} + +fn account_settings_blossom_health_card() -> impl IntoElement { + div() + .w_full() + .border_1() + .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) + .rounded(px(APP_UI_THEME.foundation.radii.medium_px)) + .p(px(APP_UI_THEME.foundation.spacing.large_px)) + .flex() + .items_center() + .justify_between() + .gap(px(APP_UI_THEME.shells.home_stack_gap_px)) + .child( + div() + .flex() + .items_center() + .gap(px(APP_UI_THEME.foundation.spacing.medium_px)) + .child(status_indicator( + APP_UI_THEME.components.app_status_indicator.online, + )) + .child( + app_stack_v(APP_UI_THEME.foundation.spacing.micro_px) + .min_w_0() + .child( + div() + .text_size(px(APP_UI_THEME + .foundation + .typography + .settings_row_text_px)) + .font_weight(gpui::FontWeight::MEDIUM) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(app_shared_text( + AppTextKey::AccountSettingsBlossomConnectionHealthy, + )), + ) + .child( + div() + .text_size(px(APP_UI_THEME + .foundation + .typography + .settings_row_text_px)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(app_shared_text( + AppTextKey::AccountSettingsBlossomUploadsAvailable, + )), + ), + ), + ) + .child( + Icon::new(IconName::CircleCheck) + .with_size(gpui_component::Size::Size(px(18.0))) + .text_color(rgb(APP_UI_THEME.components.app_status_indicator.online)), + ) +} + +fn account_settings_card(content: impl IntoElement) -> impl IntoElement { + div() + .w_full() + .border_1() + .border_color(rgb(APP_UI_THEME.foundation.surfaces.divider)) + .rounded(px(APP_UI_THEME.foundation.radii.large_px)) + .bg(transparent_black()) + .child( + div() + .w_full() + .p(px(APP_UI_THEME.shells.home_card_padding_px)) + .child(content), + ) +} + +fn account_settings_helper_note(text_key: AppTextKey) -> impl IntoElement { + div() + .w_full() + .flex() + .items_center() + .gap(px(APP_UI_THEME.foundation.spacing.small_px)) + .child( + Icon::new(IconName::Info) + .with_size(gpui_component::Size::Size(px(14.0))) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)), + ) + .child( + div() + .flex_1() + .min_w_0() + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .line_height(relative(1.25)) + .text_color(rgb(APP_UI_THEME.foundation.text.secondary)) + .child(app_shared_text(text_key)), + ) +} + fn buyer_listings_feed( section: PersonalSection, rows: &[BuyerListingRow], diff --git a/crates/i18n/src/keys.rs b/crates/i18n/src/keys.rs @@ -31,7 +31,7 @@ define_app_text_keys! { AccountTabProfile => "account.tab.profile", AccountTabFarmDetails => "account.tab.farm_details", AccountTabPreferences => "account.tab.preferences", - AccountTabSecurity => "account.tab.security", + AccountTabSettings => "account.tab.settings", AccountNotImplemented => "account.not_implemented", AccountProfilePersonalDetailsTitle => "account.profile.personal_details.title", AccountProfilePictureLabel => "account.profile.picture.label", @@ -108,6 +108,26 @@ define_app_text_keys! { AccountFarmDetailsFarmTypeNurseryPlantFarm => "account.farm_details.farm_type.nursery_plant_farm", AccountFarmDetailsFarmTypeMixedFarm => "account.farm_details.farm_type.mixed_farm", AccountFarmDetailsFarmTypeOther => "account.farm_details.farm_type.other", + AccountSettingsTitle => "account.settings.title", + AccountSettingsNostrRelaysTitle => "account.settings.nostr_relays.title", + AccountSettingsNostrRelaysHelper => "account.settings.nostr_relays.helper", + AccountSettingsRelayAccessReadWrite => "account.settings.relay_access.read_write", + AccountSettingsRelayAccessReadOnly => "account.settings.relay_access.read_only", + AccountSettingsRemoveRelayAction => "account.settings.remove_relay.action", + AccountSettingsAddRelayLabel => "account.settings.add_relay.label", + AccountSettingsAddRelayPlaceholder => "account.settings.add_relay.placeholder", + AccountSettingsAddRelayAction => "account.settings.add_relay.action", + AccountSettingsResetRelaysAction => "account.settings.reset_relays.action", + AccountSettingsDefaultRelaysNote => "account.settings.default_relays.note", + AccountSettingsBlossomServerTitle => "account.settings.blossom_server.title", + AccountSettingsBlossomServerHelper => "account.settings.blossom_server.helper", + AccountSettingsBlossomServerUrlLabel => "account.settings.blossom_server_url.label", + AccountSettingsBlossomProductPhotosLabel => "account.settings.blossom_product_photos.label", + AccountSettingsBlossomProfileFarmMediaLabel => "account.settings.blossom_profile_farm_media.label", + AccountSettingsResetBlossomServerAction => "account.settings.reset_blossom_server.action", + AccountSettingsBlossomConnectionHealthy => "account.settings.blossom_connection.healthy", + AccountSettingsBlossomUploadsAvailable => "account.settings.blossom_uploads.available", + AccountSettingsSaveChangesAction => "account.settings.save_changes.action", HomeNavBrowse => "home.nav.browse", HomeNavSearch => "home.nav.search", HomeNavCart => "home.nav.cart", diff --git a/crates/i18n/src/lib.rs b/crates/i18n/src/lib.rs @@ -160,7 +160,7 @@ mod tests { assert_eq!(app_text(AppTextKey::AccountTabProfile), "Profile"); assert_eq!(app_text(AppTextKey::AccountTabFarmDetails), "Farm details"); assert_eq!(app_text(AppTextKey::AccountTabPreferences), "Preferences"); - assert_eq!(app_text(AppTextKey::AccountTabSecurity), "Security"); + assert_eq!(app_text(AppTextKey::AccountTabSettings), "Settings"); assert_eq!( app_text(AppTextKey::AccountNotImplemented), "Not implemented" @@ -209,6 +209,19 @@ mod tests { app_text(AppTextKey::AccountFarmDetailsFarmTypeVegetableFarm), "Vegetable farm" ); + assert_eq!(app_text(AppTextKey::AccountSettingsTitle), "Settings"); + assert_eq!( + app_text(AppTextKey::AccountSettingsNostrRelaysTitle), + "Nostr relays" + ); + assert_eq!( + app_text(AppTextKey::AccountSettingsBlossomServerTitle), + "Blossom server" + ); + assert_eq!( + app_text(AppTextKey::AccountSettingsSaveChangesAction), + "Save changes" + ); assert_eq!( app_text(AppTextKey::AccountFarmDetailsSaveDraftAction), "Save draft" diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -10,7 +10,7 @@ "account.tab.profile": "Profile", "account.tab.farm_details": "Farm details", "account.tab.preferences": "Preferences", - "account.tab.security": "Security", + "account.tab.settings": "Settings", "account.not_implemented": "Not implemented", "account.profile.personal_details.title": "Personal details", "account.profile.picture.label": "Profile picture", @@ -87,6 +87,26 @@ "account.farm_details.farm_type.nursery_plant_farm": "Nursery & plant farm", "account.farm_details.farm_type.mixed_farm": "Mixed farm", "account.farm_details.farm_type.other": "Other farm type", + "account.settings.title": "Settings", + "account.settings.nostr_relays.title": "Nostr relays", + "account.settings.nostr_relays.helper": "Manage the relays used to publish and read events.", + "account.settings.relay_access.read_write": "Read & write", + "account.settings.relay_access.read_only": "Read only", + "account.settings.remove_relay.action": "Remove relay", + "account.settings.add_relay.label": "Add relay", + "account.settings.add_relay.placeholder": "wss://your-relay.example", + "account.settings.add_relay.action": "Add relay", + "account.settings.reset_relays.action": "Reset to defaults", + "account.settings.default_relays.note": "Default relays are selected automatically when your account is created.", + "account.settings.blossom_server.title": "Blossom server", + "account.settings.blossom_server.helper": "Choose where media uploads are stored.", + "account.settings.blossom_server_url.label": "Server URL", + "account.settings.blossom_product_photos.label": "Use this server for product photos", + "account.settings.blossom_profile_farm_media.label": "Use this server for profile and farm media", + "account.settings.reset_blossom_server.action": "Reset default server", + "account.settings.blossom_connection.healthy": "Connection status: Healthy", + "account.settings.blossom_uploads.available": "Uploads available", + "account.settings.save_changes.action": "Save changes", "home.nav.browse": "Browse", "home.nav.search": "Search", "home.nav.cart": "Cart",