app

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

commit 1d9857abf0b672f7abd79ebc01c5dad1cfa1176b
parent 0c8c5faae96f808d30f9d666c7b4dfd63c9a8e75
Author: triesap <tyson@radroots.org>
Date:   Tue, 21 Apr 2026 06:25:46 +0000

app: localize startup copy boundaries

- map startup notices and issue summaries through localized app text keys
- localize signer source and permission preview labels in the startup shell
- extend source guards and launcher tests for the keyed startup copy contract
- keep runtime fallback copy calm while preserving technical matcher literals

Diffstat:
Mcrates/launchers/desktop/src/source_guards.rs | 32++++++++++++++++++++++++++++----
Mcrates/launchers/desktop/src/window.rs | 181++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mcrates/shared/i18n/src/keys.rs | 14++++++++++++++
Mcrates/shared/i18n/src/lib.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mi18n/locales/en/messages.json | 14++++++++++++++
5 files changed, 277 insertions(+), 34 deletions(-)

diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -27,7 +27,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "6.500", "Salad mix", "USD", - "Untitled draft", "/tmp/radroots/data/apps/app", "/tmp/radroots/logs/apps/app", "{}.{:02}", @@ -59,10 +58,10 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "buyer.detail_open_failed", "buyer.order_open_failed", "buyer.repeat_demand_failed", - "bunker uri", "bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.radroots.example", "buyer.fulfillment_filter_update_failed", "buyer.search_query_update_failed", + "desktop runtime roots require HOME for macos", "failed to add buyer product to cart", "failed to open buyer order detail", "failed to place buyer order", @@ -138,7 +137,6 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "today-reminder-chip", "https://auth.example/challenge", "identity", - "none", "npub1", "guest", "orders", @@ -208,9 +206,20 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "products.search_query_update_failed", "products.stock_update_failed", "products.sort_update_failed", + "discovery url does not contain a remote signer uri", + "enter a bunker or discovery url to continue", + "enter a bunker or discovery url from the signer; raw nostrconnect client uris are signer-side only", + "invalid discovery url:", + "invalid discovery url: relative URL without a base", + "invalid remote signer uri:", + "invalid remote signer uri: invalid public key", + "a remote signer connection is already pending approval", + "raw nostrconnect client uris are signer-side only", + "remote signer", "remote signer connection failed: relay refused the request", "remote signer did not respond yet", "runtime unavailable", + "sign_event:kind:1", "shell", "shell-account-entry", "shell-account-label", @@ -247,11 +256,12 @@ const ALLOWED_WINDOW_LITERALS: &[&str] = &[ "settings.about.conflict_resolution_failed", "failed to refresh sync from the about panel", "failed to resolve sync conflict from the about panel", - "sign_event:kind:1, switch_relays", + "switch_relays", "startup-title-radroots", "startup-title-starting", "wss://relay.radroots.example", "{currency_code} {dollars}.{cents:02}", + "{}, {}", "{}: {}", "{} {} {}.", "{quantity} {unit_label}", @@ -280,6 +290,20 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::HomeSetupSignerPendingTitle", "AppTextKey::HomeSetupSignerAuthChallengeTitle", "AppTextKey::HomeSetupSignerApprovedTitle", + "AppTextKey::HomeSetupIssueUnavailableBody", + "AppTextKey::HomeSetupErrorStartupFailed", + "AppTextKey::HomeSetupSignerSourceValueBunkerUri", + "AppTextKey::HomeSetupSignerSourceValueDiscoveryUrl", + "AppTextKey::HomeSetupSignerPermissionSignEventKind1", + "AppTextKey::HomeSetupSignerPermissionSwitchRelays", + "AppTextKey::HomeSetupSignerPermissionAdditional", + "AppTextKey::HomeSetupSignerErrorEnterSource", + "AppTextKey::HomeSetupSignerErrorUseSignerUri", + "AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri", + "AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl", + "AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri", + "AppTextKey::HomeSetupSignerErrorPendingApprovalExists", + "AppTextKey::HomeSetupSignerErrorConnectionFailed", "AppTextKey::HomeFarmSetupOnboardingTitle", "AppTextKey::HomeFarmSetupOnboardingBody", "AppTextKey::HomeFarmSetupOnboardingAction", diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -30,7 +30,8 @@ use radroots_app_models::{ }; use radroots_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome, - RadrootsAppRemoteSignerPendingSession, radroots_app_remote_signer_connect_pending, + RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSource, + radroots_app_remote_signer_connect_pending, radroots_app_remote_signer_poll_pending_session_with_progress, radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions, }; @@ -6881,7 +6882,8 @@ fn about_status_rows(runtime: &DesktopAppRuntimeSummary) -> Vec<LabelValueRow> { app_shared_text(AppTextKey::MetadataStartupIssue), runtime .startup_issue - .clone() + .as_deref() + .map(startup_issue_summary_text) .unwrap_or_else(|| app_text(AppTextKey::ValueNone)), )); @@ -8509,6 +8511,7 @@ fn startup_home_shell( cx: &App, ) -> impl IntoElement { let surface = startup_home_surface(runtime); + let startup_notice = startup_notice.map(startup_notice_text); app_window_shell( APP_UI_THEME.foundation.surfaces.window_background, @@ -8552,7 +8555,7 @@ fn startup_home_shell( on_browse_marketplace, cx, )) - .when_some(startup_notice, |this, error| { + .when_some(startup_notice, |this, error: String| { this.child( div() .w_full() @@ -8580,7 +8583,7 @@ fn startup_home_shell( on_connect_signer, cx, )) - .when_some(startup_notice, |this, error| { + .when_some(startup_notice, |this, error: String| { this.child( div().w_full().text_center().child( home_body_text(error.to_owned()), @@ -8661,7 +8664,7 @@ fn startup_home_tagline() -> impl IntoElement { fn startup_signer_entry_surface( signer_entry: Option<&StartupSignerEntryState>, connect_state: &StartupSignerConnectState, - startup_notice: Option<&str>, + startup_notice: Option<String>, on_submit_signer: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, on_back: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, cx: &App, @@ -8676,7 +8679,10 @@ fn startup_signer_entry_surface( { None } else { - preview.as_ref().err().cloned() + preview + .as_ref() + .err() + .map(|error| startup_notice_text(error)) }; let submit_enabled = preview.is_ok() && matches!(connect_state, StartupSignerConnectState::Idle); @@ -8771,25 +8777,19 @@ fn startup_signer_entry_surface( on_back, cx, )) - .when_some(startup_notice, |this, notice| { - this.child( - div() - .w_full() - .text_center() - .child(home_body_text(notice.to_owned())), - ) + .when_some(startup_notice, |this, notice: String| { + this.child(div().w_full().text_center().child(home_body_text(notice))) }) } fn startup_signer_preview_summary(input: &str) -> Result<StartupSignerPreviewSummary, String> { let target = radroots_app_remote_signer_preview(input).map_err(|error| error.to_string())?; - let requested_permissions = target.requested_permission_labels(); Ok(StartupSignerPreviewSummary { - source_label: target.source_label().to_owned(), + source_label: startup_signer_source_text(target.source), signer_npub: target.signer_identity.public_key_npub.clone(), relays_label: startup_signer_csv_or_none(target.relays.as_slice()), - permissions_label: startup_signer_csv_or_none(requested_permissions.as_slice()), + permissions_label: startup_signer_permissions_label(target.requested_permission_labels()), }) } @@ -8844,7 +8844,7 @@ fn startup_signer_source_input_is_editable(connect_state: &StartupSignerConnectS fn startup_signer_csv_or_none(values: &[String]) -> String { if values.is_empty() { - return "none".to_owned(); + return app_text(AppTextKey::ValueNone); } values.join(", ") @@ -8861,7 +8861,15 @@ fn startup_signer_requested_permissions_label() -> String { } fn startup_signer_permissions_label(permissions: Vec<String>) -> String { - startup_signer_csv_or_none(permissions.as_slice()) + if permissions.is_empty() { + return app_text(AppTextKey::ValueNone); + } + + permissions + .into_iter() + .map(|permission| startup_signer_permission_text(permission.as_str())) + .collect::<Vec<_>>() + .join(", ") } fn startup_signer_status_spec( @@ -8894,11 +8902,57 @@ fn startup_signer_transport_failure_requires_notice(message: &str) -> bool { message != "remote signer did not respond yet" } +fn startup_issue_summary_text(_startup_issue: &str) -> String { + app_text(AppTextKey::HomeSetupIssueUnavailableBody) +} + +fn startup_signer_source_text(source: RadrootsAppRemoteSignerSource) -> String { + app_text(match source { + RadrootsAppRemoteSignerSource::BunkerUri => AppTextKey::HomeSetupSignerSourceValueBunkerUri, + RadrootsAppRemoteSignerSource::DiscoveryUrl => { + AppTextKey::HomeSetupSignerSourceValueDiscoveryUrl + } + }) +} + +fn startup_signer_permission_text(permission: &str) -> String { + app_text(match permission { + "sign_event:kind:1" => AppTextKey::HomeSetupSignerPermissionSignEventKind1, + "switch_relays" => AppTextKey::HomeSetupSignerPermissionSwitchRelays, + _ => AppTextKey::HomeSetupSignerPermissionAdditional, + }) +} + +fn startup_notice_text(message: &str) -> String { + app_text(match message { + "enter a bunker or discovery url to continue" => { + AppTextKey::HomeSetupSignerErrorEnterSource + } + "discovery url does not contain a remote signer uri" => { + AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri + } + "a remote signer connection is already pending approval" => { + AppTextKey::HomeSetupSignerErrorPendingApprovalExists + } + _ if message.contains("raw nostrconnect client uris are signer-side only") => { + AppTextKey::HomeSetupSignerErrorUseSignerUri + } + _ if message.starts_with("invalid discovery url:") => { + AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl + } + _ if message.starts_with("invalid remote signer uri:") => { + AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri + } + _ if message.contains("remote signer") => AppTextKey::HomeSetupSignerErrorConnectionFailed, + _ => AppTextKey::HomeSetupErrorStartupFailed, + }) +} + fn startup_home_body(runtime: &DesktopAppRuntimeSummary) -> impl IntoElement { - let body = runtime - .startup_issue - .clone() - .unwrap_or_else(|| app_shared_text(AppTextKey::HomeTodayEmptySetupBody).to_string()); + let body = runtime.startup_issue.as_deref().map_or_else( + || app_shared_text(AppTextKey::HomeTodayEmptySetupBody).to_string(), + startup_issue_summary_text, + ); div().w_full().text_center().child(home_body_text(body)) } @@ -11605,9 +11659,9 @@ mod tests { presented_farmer_reminder, product_display_title, reminder_action_target, reminder_deadline_text, reminder_delivery_state_key, reminder_urgency_color, reminder_urgency_key, settings_auto_focus_target, startup_home_surface, - startup_signer_preview_summary, startup_signer_preview_summary_for_connect_state, - startup_signer_source_input_is_editable, startup_signer_status_spec, - startup_signer_transport_failure_requires_notice, + startup_issue_summary_text, startup_notice_text, startup_signer_preview_summary, + startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable, + startup_signer_status_spec, startup_signer_transport_failure_requires_notice, }; use crate::runtime::{ DesktopAppRuntimeMetadataSummary, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary, @@ -12275,8 +12329,14 @@ mod tests { #[test] fn blank_product_titles_fall_back_to_the_untitled_copy() { - assert_eq!(product_display_title(""), "Untitled draft"); - assert_eq!(product_display_title(" "), "Untitled draft"); + assert_eq!( + product_display_title(""), + app_text(AppTextKey::ProductsUntitledDraft) + ); + assert_eq!( + product_display_title(" "), + app_text(AppTextKey::ProductsUntitledDraft) + ); assert_eq!(product_display_title("Salad mix"), "Salad mix"); } @@ -12287,12 +12347,19 @@ mod tests { ) .expect("preview"); - assert_eq!(preview.source_label, "bunker uri"); + assert_eq!( + preview.source_label, + app_text(AppTextKey::HomeSetupSignerSourceValueBunkerUri) + ); assert!(preview.signer_npub.starts_with("npub1")); assert_eq!(preview.relays_label, "wss://relay.radroots.example"); assert_eq!( preview.permissions_label, - "sign_event:kind:1, switch_relays" + format!( + "{}, {}", + app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1), + app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays) + ) ); } @@ -12389,7 +12456,11 @@ mod tests { assert_eq!(preview.relays_label, "wss://relay.radroots.example"); assert_eq!( preview.permissions_label, - "sign_event:kind:1, switch_relays" + format!( + "{}, {}", + app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1), + app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays) + ) ); } @@ -12404,6 +12475,56 @@ mod tests { } #[test] + fn startup_signer_notice_copy_maps_known_signer_failures() { + assert_eq!( + startup_notice_text("enter a bunker or discovery url to continue"), + app_text(AppTextKey::HomeSetupSignerErrorEnterSource) + ); + assert_eq!( + startup_notice_text( + "enter a bunker or discovery url from the signer; raw nostrconnect client uris are signer-side only" + ), + app_text(AppTextKey::HomeSetupSignerErrorUseSignerUri) + ); + assert_eq!( + startup_notice_text("discovery url does not contain a remote signer uri"), + app_text(AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri) + ); + assert_eq!( + startup_notice_text("invalid discovery url: relative URL without a base"), + app_text(AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl) + ); + assert_eq!( + startup_notice_text("invalid remote signer uri: invalid public key"), + app_text(AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri) + ); + assert_eq!( + startup_notice_text("a remote signer connection is already pending approval"), + app_text(AppTextKey::HomeSetupSignerErrorPendingApprovalExists) + ); + assert_eq!( + startup_notice_text("remote signer connection failed: relay refused the request"), + app_text(AppTextKey::HomeSetupSignerErrorConnectionFailed) + ); + assert_eq!( + startup_notice_text("failed to add relay `{relay_url}`: {error}"), + app_text(AppTextKey::HomeSetupErrorStartupFailed) + ); + } + + #[test] + fn startup_issue_copy_fails_closed_to_a_localized_summary() { + assert_eq!( + startup_issue_summary_text("runtime unavailable"), + app_text(AppTextKey::HomeSetupIssueUnavailableBody) + ); + assert_eq!( + startup_issue_summary_text("desktop runtime roots require HOME for macos"), + app_text(AppTextKey::HomeSetupIssueUnavailableBody) + ); + } + + #[test] fn reminder_action_target_prefers_order_detail_before_pack_day() { let order_id = radroots_app_models::OrderId::new(); let fulfillment_window_id = FulfillmentWindowId::new(); diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -75,6 +75,20 @@ define_app_text_keys! { HomeSetupSignerPendingTitle => "home.setup.signer.pending_title", HomeSetupSignerAuthChallengeTitle => "home.setup.signer.auth_challenge_title", HomeSetupSignerApprovedTitle => "home.setup.signer.approved_title", + HomeSetupIssueUnavailableBody => "home.setup.issue.unavailable_body", + HomeSetupErrorStartupFailed => "home.setup.error.startup_failed", + HomeSetupSignerSourceValueBunkerUri => "home.setup.signer.source_value.bunker_uri", + HomeSetupSignerSourceValueDiscoveryUrl => "home.setup.signer.source_value.discovery_url", + HomeSetupSignerPermissionSignEventKind1 => "home.setup.signer.permission.sign_event_kind_1", + HomeSetupSignerPermissionSwitchRelays => "home.setup.signer.permission.switch_relays", + HomeSetupSignerPermissionAdditional => "home.setup.signer.permission.additional", + HomeSetupSignerErrorEnterSource => "home.setup.signer.error.enter_source", + HomeSetupSignerErrorUseSignerUri => "home.setup.signer.error.use_signer_uri", + HomeSetupSignerErrorMissingDiscoveryUri => "home.setup.signer.error.missing_discovery_uri", + HomeSetupSignerErrorInvalidDiscoveryUrl => "home.setup.signer.error.invalid_discovery_url", + HomeSetupSignerErrorInvalidRemoteSignerUri => "home.setup.signer.error.invalid_remote_signer_uri", + HomeSetupSignerErrorPendingApprovalExists => "home.setup.signer.error.pending_approval_exists", + HomeSetupSignerErrorConnectionFailed => "home.setup.signer.error.connection_failed", HomeFarmSetupOnboardingTitle => "home.farm_setup.onboarding.title", HomeFarmSetupOnboardingBody => "home.farm_setup.onboarding.body", HomeFarmSetupOnboardingAction => "home.farm_setup.onboarding.action", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -583,6 +583,20 @@ mod tests { "HomeSetupSignerPendingTitle => \"home.setup.signer.pending_title\"", "HomeSetupSignerAuthChallengeTitle => \"home.setup.signer.auth_challenge_title\"", "HomeSetupSignerApprovedTitle => \"home.setup.signer.approved_title\"", + "HomeSetupIssueUnavailableBody => \"home.setup.issue.unavailable_body\"", + "HomeSetupErrorStartupFailed => \"home.setup.error.startup_failed\"", + "HomeSetupSignerSourceValueBunkerUri => \"home.setup.signer.source_value.bunker_uri\"", + "HomeSetupSignerSourceValueDiscoveryUrl => \"home.setup.signer.source_value.discovery_url\"", + "HomeSetupSignerPermissionSignEventKind1 => \"home.setup.signer.permission.sign_event_kind_1\"", + "HomeSetupSignerPermissionSwitchRelays => \"home.setup.signer.permission.switch_relays\"", + "HomeSetupSignerPermissionAdditional => \"home.setup.signer.permission.additional\"", + "HomeSetupSignerErrorEnterSource => \"home.setup.signer.error.enter_source\"", + "HomeSetupSignerErrorUseSignerUri => \"home.setup.signer.error.use_signer_uri\"", + "HomeSetupSignerErrorMissingDiscoveryUri => \"home.setup.signer.error.missing_discovery_uri\"", + "HomeSetupSignerErrorInvalidDiscoveryUrl => \"home.setup.signer.error.invalid_discovery_url\"", + "HomeSetupSignerErrorInvalidRemoteSignerUri => \"home.setup.signer.error.invalid_remote_signer_uri\"", + "HomeSetupSignerErrorPendingApprovalExists => \"home.setup.signer.error.pending_approval_exists\"", + "HomeSetupSignerErrorConnectionFailed => \"home.setup.signer.error.connection_failed\"", ] { assert!( source.contains(entry), @@ -638,6 +652,62 @@ mod tests { app_text(AppTextKey::HomeSetupSignerApprovedTitle), "Signer approved" ); + assert_eq!( + app_text(AppTextKey::HomeSetupIssueUnavailableBody), + "Radroots couldn't start normally on this device. Check the local setup and try again." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupErrorStartupFailed), + "Couldn't finish startup right now. Check the connection and try again." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerSourceValueBunkerUri), + "Bunker URI" + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerSourceValueDiscoveryUrl), + "Discovery URL" + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1), + "Sign notes" + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays), + "Switch relays" + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerPermissionAdditional), + "Additional permission" + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorEnterSource), + "Paste a bunker URI or discovery URL from your signer to continue." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorUseSignerUri), + "Use a bunker URI or discovery URL from your signer." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri), + "The discovery URL is missing the signer address." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl), + "That discovery URL isn't valid. Check it and try again." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri), + "That signer address isn't valid. Check it and try again." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorPendingApprovalExists), + "A signer connection is already waiting for approval." + ); + assert_eq!( + app_text(AppTextKey::HomeSetupSignerErrorConnectionFailed), + "Couldn't continue with the signer. Check the signer and try again." + ); } #[test] diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -55,6 +55,20 @@ "home.setup.signer.pending_title": "Waiting for signer approval", "home.setup.signer.auth_challenge_title": "Continue in your signer", "home.setup.signer.approved_title": "Signer approved", + "home.setup.issue.unavailable_body": "Radroots couldn't start normally on this device. Check the local setup and try again.", + "home.setup.error.startup_failed": "Couldn't finish startup right now. Check the connection and try again.", + "home.setup.signer.source_value.bunker_uri": "Bunker URI", + "home.setup.signer.source_value.discovery_url": "Discovery URL", + "home.setup.signer.permission.sign_event_kind_1": "Sign notes", + "home.setup.signer.permission.switch_relays": "Switch relays", + "home.setup.signer.permission.additional": "Additional permission", + "home.setup.signer.error.enter_source": "Paste a bunker URI or discovery URL from your signer to continue.", + "home.setup.signer.error.use_signer_uri": "Use a bunker URI or discovery URL from your signer.", + "home.setup.signer.error.missing_discovery_uri": "The discovery URL is missing the signer address.", + "home.setup.signer.error.invalid_discovery_url": "That discovery URL isn't valid. Check it and try again.", + "home.setup.signer.error.invalid_remote_signer_uri": "That signer address isn't valid. Check it and try again.", + "home.setup.signer.error.pending_approval_exists": "A signer connection is already waiting for approval.", + "home.setup.signer.error.connection_failed": "Couldn't continue with the signer. Check the signer and try again.", "home.farm_setup.onboarding.title": "Set up your farm", "home.farm_setup.onboarding.body": "Add the basics now. You can change them later.", "home.farm_setup.onboarding.action": "Set up your farm",