app

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

lib.rs (51728B)


      1 #![forbid(unsafe_code)]
      2 
      3 use mf2_i18n::{LanguageTag, negotiate_lookup};
      4 
      5 mod keys;
      6 
      7 pub use keys::AppTextKey;
      8 
      9 include!(concat!(env!("OUT_DIR"), "/app_i18n/generated_module.rs"));
     10 include!(concat!(env!("OUT_DIR"), "/app_i18n/generated_catalog.rs"));
     11 
     12 pub fn app_text(key: AppTextKey) -> String {
     13     generated::tr(key.id())
     14         .unwrap_or_else(|error| panic!("missing localized app text for key {}: {error}", key.id()))
     15 }
     16 
     17 pub fn default_locale() -> &'static str {
     18     DEFAULT_LOCALE_ID
     19 }
     20 
     21 pub fn supported_locales() -> &'static [&'static str] {
     22     SUPPORTED_LOCALE_IDS
     23 }
     24 
     25 pub fn resolve_locale_from_host(host_locale: &str) -> String {
     26     let normalized = normalize_host_locale(host_locale);
     27     let requested_locale = match LanguageTag::parse(&normalized) {
     28         Ok(locale) => locale,
     29         Err(_) => return DEFAULT_LOCALE_ID.to_owned(),
     30     };
     31     let supported = SUPPORTED_LOCALE_IDS
     32         .iter()
     33         .map(|locale| LanguageTag::parse(locale).expect("supported locale should parse"))
     34         .collect::<Vec<_>>();
     35     let default_locale =
     36         LanguageTag::parse(DEFAULT_LOCALE_ID).expect("default locale should parse");
     37 
     38     negotiate_lookup(&[requested_locale], &supported, &default_locale)
     39         .selected
     40         .normalized()
     41         .to_owned()
     42 }
     43 
     44 pub fn select_locale_from_host(host_locale: &str) -> String {
     45     let locale = resolve_locale_from_host(host_locale);
     46     generated::set_locale(&locale).unwrap_or_else(|_| DEFAULT_LOCALE_ID.to_owned())
     47 }
     48 
     49 fn normalize_host_locale(host_locale: &str) -> String {
     50     let trimmed = host_locale.trim();
     51     if trimmed.is_empty() {
     52         return DEFAULT_LOCALE_ID.to_owned();
     53     }
     54 
     55     let without_fallbacks = trimmed.split(':').next().unwrap_or(DEFAULT_LOCALE_ID);
     56     let without_encoding = without_fallbacks
     57         .split('.')
     58         .next()
     59         .unwrap_or(DEFAULT_LOCALE_ID)
     60         .split('@')
     61         .next()
     62         .unwrap_or(DEFAULT_LOCALE_ID)
     63         .trim();
     64     if without_encoding.is_empty() {
     65         return DEFAULT_LOCALE_ID.to_owned();
     66     }
     67 
     68     without_encoding.replace('_', "-")
     69 }
     70 
     71 #[cfg(test)]
     72 mod tests {
     73     use std::collections::BTreeSet;
     74 
     75     use super::{
     76         AppTextKey, app_text, default_locale, resolve_locale_from_host, supported_locales,
     77     };
     78 
     79     const RESERVED_PAYMENT_ACTION_TERMS: &[&str] = &[
     80         "checkout",
     81         "pay",
     82         "refund",
     83         "settlement",
     84         "wallet",
     85         "invoice",
     86         "bank",
     87         "card",
     88         "processor",
     89         "provider",
     90         "payment-provider",
     91         "payment provider",
     92     ];
     93 
     94     const FORBIDDEN_PAYMENT_DEFERRAL_COPY_PATTERNS: &[&str] = &[
     95         "payments are deferred",
     96         "payment is deferred",
     97         "payment deferred",
     98         "payments deferred",
     99         "deferred payment",
    100         "deferred payments",
    101         "checkout unavailable",
    102         "figure it out",
    103         "payment handling outside the app",
    104         "refund outside the app",
    105         "handle any refund outside the app",
    106         "settle outside the app",
    107     ];
    108 
    109     const FORBIDDEN_TRADE_WORKFLOW_LEAKAGE_PATTERNS: &[&str] = &[
    110         "state machine",
    111         "reducer",
    112         "event kind",
    113         "nostr",
    114         "protocol",
    115         "checkout",
    116         "payment provider",
    117         "payment-provider",
    118     ];
    119 
    120     #[test]
    121     fn generated_catalog_matches_typed_key_registry() {
    122         let catalog_keys = super::DEFAULT_CATALOG_KEY_IDS
    123             .iter()
    124             .copied()
    125             .collect::<BTreeSet<_>>();
    126         let typed_keys = AppTextKey::ALL
    127             .iter()
    128             .map(|key| key.id())
    129             .collect::<BTreeSet<_>>();
    130 
    131         assert_eq!(typed_keys, catalog_keys);
    132     }
    133 
    134     #[test]
    135     fn english_catalog_covers_all_defined_text_keys() {
    136         assert_eq!(super::generated::default_locale(), default_locale());
    137         assert_eq!(
    138             super::generated::supported_locales(),
    139             supported_locales()
    140                 .iter()
    141                 .map(|locale| (*locale).to_owned())
    142                 .collect::<Vec<_>>()
    143         );
    144 
    145         for key in AppTextKey::ALL {
    146             assert!(!app_text(*key).trim().is_empty());
    147         }
    148     }
    149 
    150     #[test]
    151     fn english_identity_copy_matches_the_macos_menu_contract() {
    152         assert_eq!(app_text(AppTextKey::AppName), "Radroots");
    153         assert_eq!(app_text(AppTextKey::MenuAbout), "About Radroots");
    154         assert_eq!(app_text(AppTextKey::MenuQuit), "Quit Radroots");
    155     }
    156 
    157     #[test]
    158     fn english_auth_copy_matches_the_local_account_workflow_contract() {
    159         assert_eq!(app_text(AppTextKey::AccountTitle), "Account");
    160         assert_eq!(app_text(AppTextKey::AccountTabProfile), "Profile");
    161         assert_eq!(app_text(AppTextKey::AccountTabFarmDetails), "Farm details");
    162         assert_eq!(app_text(AppTextKey::AccountTabPreferences), "Preferences");
    163         assert_eq!(app_text(AppTextKey::AccountTabSettings), "Settings");
    164         assert_eq!(
    165             app_text(AppTextKey::AccountNotImplemented),
    166             "Not implemented"
    167         );
    168         assert_eq!(app_text(AppTextKey::AccountFormSaveAction), "Save");
    169         assert_eq!(
    170             app_text(AppTextKey::AccountFormSaveDraftAction),
    171             "Save draft"
    172         );
    173         assert_eq!(
    174             app_text(AppTextKey::AccountProfilePersonalDetailsTitle),
    175             "Personal details"
    176         );
    177         assert_eq!(
    178             app_text(AppTextKey::AccountProfileChangePhotoAction),
    179             "Change photo"
    180         );
    181         assert_eq!(
    182             app_text(AppTextKey::AccountProfileRemovePhotoAction),
    183             "Remove"
    184         );
    185         assert_eq!(
    186             app_text(AppTextKey::AccountProfileFullNameValue),
    187             "Tyson Lupul"
    188         );
    189         assert_eq!(
    190             app_text(AppTextKey::AccountProfileEmailValue),
    191             "tyson@tysonsfarm.com"
    192         );
    193         assert_eq!(
    194             app_text(AppTextKey::AccountProfilePhoneValue),
    195             "+1 250 202 3030"
    196         );
    197         assert_eq!(
    198             app_text(AppTextKey::AccountFarmDetailsTitle),
    199             "Farm details"
    200         );
    201         assert_eq!(
    202             app_text(AppTextKey::AccountFarmDetailsTabProfile),
    203             "Profile"
    204         );
    205         assert_eq!(
    206             app_text(AppTextKey::AccountFarmDetailsTabLocation),
    207             "Location"
    208         );
    209         assert_eq!(
    210             app_text(AppTextKey::AccountFarmDetailsTabOperations),
    211             "Operations"
    212         );
    213         assert_eq!(
    214             app_text(AppTextKey::AccountFarmDetailsTabFulfilment),
    215             "Fulfilment"
    216         );
    217         assert_eq!(
    218             app_text(AppTextKey::AccountFarmDetailsFarmProfileTitle),
    219             "Farm profile"
    220         );
    221         assert_eq!(
    222             app_text(AppTextKey::AccountFarmDetailsContactEmailValue),
    223             "tyson@tysonsfarm.com"
    224         );
    225         assert_eq!(
    226             app_text(AppTextKey::AccountFarmDetailsPublicPhoneValue),
    227             "+1 250 202 3030"
    228         );
    229         assert_eq!(
    230             app_text(AppTextKey::AccountFarmDetailsFarmTypeVegetableFarm),
    231             "Vegetable farm"
    232         );
    233         assert_eq!(
    234             app_text(AppTextKey::AccountFarmDetailsLocationTitle),
    235             "Location & service area"
    236         );
    237         assert_eq!(
    238             app_text(AppTextKey::AccountFarmDetailsMapNotImplemented),
    239             "not implemented"
    240         );
    241         assert_eq!(
    242             app_text(AppTextKey::AccountFarmDetailsOperatingTitle),
    243             "Operating details"
    244         );
    245         assert_eq!(
    246             app_text(AppTextKey::AccountFarmDetailsPickupFulfillmentTitle),
    247             "Pickup & fulfilment"
    248         );
    249         assert_eq!(
    250             app_text(AppTextKey::AccountFarmDetailsOrderCutoffNoonValue),
    251             "12:00 PM (Noon)"
    252         );
    253         assert_eq!(app_text(AppTextKey::AccountSettingsTitle), "Settings");
    254         assert_eq!(
    255             app_text(AppTextKey::AccountSettingsNostrRelaysTitle),
    256             "Nostr relays"
    257         );
    258         assert_eq!(
    259             app_text(AppTextKey::AccountSettingsBlossomServerTitle),
    260             "Blossom server"
    261         );
    262         assert_eq!(
    263             app_text(AppTextKey::HomeTodayEmptySetupBody),
    264             "Add a local account to start using Radroots on this device."
    265         );
    266         assert_eq!(
    267             app_text(AppTextKey::SettingsAccountNoSelectionBody),
    268             "Add a local account to start using Radroots on this device."
    269         );
    270         assert_eq!(
    271             app_text(AppTextKey::SettingsAccountActivationLabel),
    272             "Farmer Activation"
    273         );
    274         assert_eq!(
    275             app_text(AppTextKey::SettingsAccountOpenWorkspaceAction),
    276             "Admin Console"
    277         );
    278         assert_eq!(
    279             app_text(AppTextKey::SettingsAccountImportFileAction),
    280             "Import from file"
    281         );
    282         assert_eq!(
    283             app_text(AppTextKey::SettingsAccountImportDatabaseAction),
    284             "Import from database"
    285         );
    286         assert_eq!(
    287             app_text(AppTextKey::SettingsAccountConnectRemoteBunkerAction),
    288             "Connect remote bunker"
    289         );
    290     }
    291 
    292     #[test]
    293     fn english_shell_reset_copy_matches_setup_and_utility_contract() {
    294         assert_eq!(
    295             app_text(AppTextKey::HomeSetupCreateAccountAction),
    296             "Create account"
    297         );
    298         assert_eq!(app_text(AppTextKey::SettingsTitle), "Radroots Settings");
    299         assert_eq!(
    300             app_text(AppTextKey::SettingsAccountNoSelectionTitle),
    301             "No account selected"
    302         );
    303         assert_eq!(
    304             app_text(AppTextKey::SettingsAccountNoSelectionBody),
    305             "Add a local account to start using Radroots on this device."
    306         );
    307         assert_eq!(
    308             app_text(AppTextKey::SettingsAccountStatusLoggedOut),
    309             "Logged Out"
    310         );
    311         assert_eq!(
    312             app_text(AppTextKey::SettingsAccountActivationInactive),
    313             "Not activated"
    314         );
    315         assert_eq!(
    316             app_text(AppTextKey::SettingsAccountAddAction),
    317             "Add Account..."
    318         );
    319         assert_eq!(app_text(AppTextKey::SettingsAccountLogOutAction), "Log Out");
    320         assert_eq!(
    321             app_text(AppTextKey::SettingsAccountOpenWorkspaceAction),
    322             "Admin Console"
    323         );
    324     }
    325 
    326     #[test]
    327     fn english_reminder_copy_matches_the_seller_surface_contract() {
    328         assert_eq!(app_text(AppTextKey::HomeTodayRemindersTitle), "Coming up");
    329         assert_eq!(app_text(AppTextKey::OrdersRemindersTitle), "Reminders");
    330         assert_eq!(
    331             app_text(AppTextKey::OrdersReminderLogTitle),
    332             "Reminder activity"
    333         );
    334         assert_eq!(
    335             app_text(AppTextKey::PackDayRemindersTitle),
    336             "Before this window"
    337         );
    338         assert_eq!(app_text(AppTextKey::ReminderDeadlineLabel), "Due");
    339         assert_eq!(app_text(AppTextKey::ReminderUrgencyDueSoon), "Due soon");
    340         assert_eq!(app_text(AppTextKey::ReminderUrgencyBlocking), "Blocking");
    341         assert_eq!(
    342             app_text(AppTextKey::ReminderPresentationTitle),
    343             "Needs attention now"
    344         );
    345         assert_eq!(
    346             app_text(AppTextKey::ReminderPresentationDismissAction),
    347             "Dismiss"
    348         );
    349         assert_eq!(
    350             app_text(AppTextKey::ReminderDeliveryStatePresented),
    351             "Presented"
    352         );
    353         assert_eq!(
    354             app_text(AppTextKey::ReminderDeliveryStateResolved),
    355             "Resolved"
    356         );
    357     }
    358 
    359     #[test]
    360     fn english_about_copy_matches_the_runtime_status_contract() {
    361         assert_eq!(
    362             app_text(AppTextKey::SettingsAboutCompanyName),
    363             "Radroots, Inc."
    364         );
    365         assert_eq!(app_text(AppTextKey::SettingsAboutVersionLabel), "Version");
    366         assert_eq!(
    367             app_text(AppTextKey::SettingsAboutVariantLabel),
    368             "Standalone local app"
    369         );
    370         assert_eq!(
    371             app_text(AppTextKey::SettingsAboutAcknowledgementsAction),
    372             "Acknowledgements"
    373         );
    374         assert_eq!(
    375             app_text(AppTextKey::SettingsAboutPrivacyPolicyAction),
    376             "Privacy Policy"
    377         );
    378         assert_eq!(
    379             app_text(AppTextKey::SettingsAboutTermsAction),
    380             "Terms of Service"
    381         );
    382         assert_eq!(
    383             app_text(AppTextKey::SettingsAboutReportIssueAction),
    384             "Report an Issue..."
    385         );
    386         assert_eq!(
    387             app_text(AppTextKey::SettingsAboutCopyrightNotice),
    388             "© 2026 Radroots, Inc. All rights reserved."
    389         );
    390         assert_eq!(
    391             app_text(AppTextKey::SettingsAboutTrademarkNotice),
    392             "Radroots is a trademark of Radroots, Inc."
    393         );
    394         assert_eq!(
    395             app_text(AppTextKey::SettingsAboutStatusSectionLabel),
    396             "Status"
    397         );
    398         assert_eq!(
    399             app_text(AppTextKey::SettingsAboutConflictReviewSectionLabel),
    400             "Conflict review"
    401         );
    402         assert_eq!(
    403             app_text(AppTextKey::SettingsAboutRuntimeSectionLabel),
    404             "Runtime"
    405         );
    406         assert_eq!(
    407             app_text(AppTextKey::SettingsAboutConflictReviewUnavailable),
    408             "Conflict review becomes available after you select an account."
    409         );
    410         assert_eq!(
    411             app_text(AppTextKey::SettingsAboutConflictReviewBlocking),
    412             "Blocking conflicts pause sync until you resolve them."
    413         );
    414         assert_eq!(
    415             app_text(AppTextKey::SettingsAboutRefreshAction),
    416             "Refresh sync"
    417         );
    418         assert_eq!(
    419             app_text(AppTextKey::SettingsAboutConflictAcceptLocalAction),
    420             "Accept local"
    421         );
    422         assert_eq!(
    423             app_text(AppTextKey::SettingsAboutConflictAcceptRemoteAction),
    424             "Accept remote"
    425         );
    426         assert_eq!(
    427             app_text(AppTextKey::SettingsAboutConflictDismissAction),
    428             "Dismiss"
    429         );
    430         assert_eq!(
    431             app_text(AppTextKey::MetadataSyncPendingWriteCount),
    432             "pending writes"
    433         );
    434         assert_eq!(
    435             app_text(AppTextKey::MetadataSyncBlockingConflictCount),
    436             "blocking conflict count"
    437         );
    438         assert_eq!(
    439             app_text(AppTextKey::MetadataSyncConflictAggregate),
    440             "aggregate"
    441         );
    442         assert_eq!(app_text(AppTextKey::MetadataSyncConflictKind), "kind");
    443         assert_eq!(
    444             app_text(AppTextKey::MetadataSyncConflictSeverity),
    445             "severity"
    446         );
    447         assert_eq!(
    448             app_text(AppTextKey::MetadataSyncConflictDetectedAt),
    449             "detected"
    450         );
    451         assert_eq!(
    452             app_text(AppTextKey::MetadataSyncConflictResolution),
    453             "resolution"
    454         );
    455         assert_eq!(
    456             app_text(AppTextKey::ValueSyncConflictAggregateFulfillmentWindow),
    457             "Fulfillment window"
    458         );
    459         assert_eq!(
    460             app_text(AppTextKey::ValueSyncConflictKindRevisionMismatch),
    461             "Revision mismatch"
    462         );
    463         assert_eq!(
    464             app_text(AppTextKey::ValueSyncConflictSeverityBlocking),
    465             "Blocking"
    466         );
    467         assert_eq!(
    468             app_text(AppTextKey::ValueSyncConflictResolutionAcceptedRemote),
    469             "Accepted remote"
    470         );
    471     }
    472 
    473     #[test]
    474     fn english_orders_copy_matches_the_queue_contract() {
    475         assert_eq!(app_text(AppTextKey::HomeNavOrders), "Orders");
    476         assert_eq!(
    477             app_text(AppTextKey::HomeTodayOpenInOrdersAction),
    478             "View all"
    479         );
    480         assert_eq!(
    481             app_text(AppTextKey::HomeTodayOpenInPackDayAction),
    482             "Open pack day"
    483         );
    484         assert_eq!(app_text(AppTextKey::OrdersTitle), "Orders");
    485         assert_eq!(
    486             app_text(AppTextKey::OrdersStatusNeedsAction),
    487             "Needs action"
    488         );
    489         assert_eq!(app_text(AppTextKey::OrdersStatusDeclined), "Declined");
    490         assert_eq!(app_text(AppTextKey::OrdersStatusScheduled), "Agreed");
    491         assert_eq!(app_text(AppTextKey::OrdersStatusInHandoff), "Agreed");
    492         assert_eq!(app_text(AppTextKey::OrdersStatusCompleted), "Agreed");
    493         assert_eq!(
    494             app_text(AppTextKey::OrdersStatusNeedsReview),
    495             "Needs review"
    496         );
    497         assert_eq!(app_text(AppTextKey::OrdersDetailTitle), "Order detail");
    498     }
    499 
    500     #[test]
    501     fn english_marketplace_detail_copy_matches_the_buyer_detail_contract() {
    502         assert_eq!(app_text(AppTextKey::PersonalDetailBackAction), "Back");
    503         assert_eq!(
    504             app_text(AppTextKey::PersonalDetailQuantityLabel),
    505             "Quantity"
    506         );
    507         assert_eq!(
    508             app_text(AppTextKey::PersonalDetailAddToCartAction),
    509             "Add to cart"
    510         );
    511         assert_eq!(
    512             app_text(AppTextKey::PersonalDetailReplaceCartAction),
    513             "Replace cart"
    514         );
    515         assert_eq!(
    516             app_text(AppTextKey::PersonalMarketplaceRefreshFailedNotice),
    517             "Couldn't refresh marketplace listings. Your saved local state is still here; try again in a moment."
    518         );
    519         assert_eq!(
    520             app_text(AppTextKey::PersonalDetailOpenFailedNotice),
    521             "Couldn't open that listing. Refresh the marketplace and try again."
    522         );
    523     }
    524 
    525     #[test]
    526     fn english_marketplace_order_review_copy_matches_the_local_order_contract() {
    527         assert_eq!(
    528             app_text(AppTextKey::PersonalCartReviewOrderAction),
    529             "Review order"
    530         );
    531         assert_eq!(
    532             app_text(AppTextKey::PersonalOrderReviewTitle),
    533             "Order review"
    534         );
    535         assert_eq!(
    536             app_text(AppTextKey::PersonalOrderReviewPlaceOrderAction),
    537             "Place order"
    538         );
    539         assert_eq!(
    540             app_text(AppTextKey::PersonalOrderReviewLocalOnlyBody),
    541             "Review the details before placing the order."
    542         );
    543         assert_eq!(
    544             app_text(AppTextKey::PersonalOrderPlaceFailedNotice),
    545             "Couldn't place that order. Nothing was sent; check the order and try again."
    546         );
    547         assert_eq!(
    548             app_text(AppTextKey::PersonalOrderCoordinationFailedNotice),
    549             "Order saved locally. It still needs to be shared with your order tools; open Orders and try again."
    550         );
    551     }
    552 
    553     #[test]
    554     fn english_payment_action_copy_remains_unspoken_for_reserved_workflow() {
    555         let action_keys = AppTextKey::ALL
    556             .iter()
    557             .copied()
    558             .filter(|key| is_visible_action_text_key(*key))
    559             .collect::<Vec<_>>();
    560 
    561         assert!(action_keys.contains(&AppTextKey::PersonalCartReviewOrderAction));
    562         assert!(action_keys.contains(&AppTextKey::PersonalOrderReviewPlaceOrderAction));
    563 
    564         for key in action_keys {
    565             let copy = app_text(key).to_lowercase();
    566             for term in RESERVED_PAYMENT_ACTION_TERMS {
    567                 assert!(
    568                     !contains_reserved_payment_action_term(&copy, term),
    569                     "{} contains reserved payment action term `{term}`",
    570                     key.id()
    571                 );
    572             }
    573         }
    574     }
    575 
    576     #[test]
    577     fn english_visible_copy_does_not_explain_payment_deferral() {
    578         for key in AppTextKey::ALL {
    579             let normalized_copy = app_text(*key).to_lowercase();
    580             for pattern in FORBIDDEN_PAYMENT_DEFERRAL_COPY_PATTERNS {
    581                 assert!(
    582                     !normalized_copy.contains(pattern),
    583                     "{} contains forbidden payment-deferral copy `{pattern}`",
    584                     key.id()
    585                 );
    586             }
    587         }
    588     }
    589 
    590     #[test]
    591     fn english_buyer_visible_copy_does_not_use_checkout_wording() {
    592         for key in AppTextKey::ALL
    593             .iter()
    594             .copied()
    595             .filter(|key| is_buyer_visible_text_key(*key))
    596         {
    597             let normalized_copy = app_text(key).to_lowercase();
    598             assert!(
    599                 !contains_reserved_payment_action_term(&normalized_copy, "checkout"),
    600                 "{} contains buyer-visible checkout wording",
    601                 key.id()
    602             );
    603         }
    604     }
    605 
    606     #[test]
    607     fn english_trade_workflow_copy_stays_compact_and_product_facing() {
    608         for key in AppTextKey::ALL
    609             .iter()
    610             .copied()
    611             .filter(|key| is_trade_workflow_text_key(*key))
    612         {
    613             let copy = app_text(key);
    614             let normalized_copy = copy.to_lowercase();
    615             assert!(
    616                 copy.split_whitespace().count() <= 4,
    617                 "{} is too long for a compact workflow badge",
    618                 key.id()
    619             );
    620             for pattern in FORBIDDEN_TRADE_WORKFLOW_LEAKAGE_PATTERNS {
    621                 assert!(
    622                     !normalized_copy.contains(pattern),
    623                     "{} contains workflow implementation copy `{pattern}`",
    624                     key.id()
    625                 );
    626             }
    627         }
    628     }
    629 
    630     #[test]
    631     fn english_trade_workflow_copy_matches_the_projection_contract() {
    632         assert_eq!(
    633             app_text(AppTextKey::TradeWorkflowAxisAgreement),
    634             "Agreement"
    635         );
    636         assert_eq!(app_text(AppTextKey::TradeWorkflowAxisSource), "Source");
    637         assert_eq!(
    638             app_text(AppTextKey::TradeWorkflowAgreementOrdered),
    639             "Ordered"
    640         );
    641         assert_eq!(
    642             app_text(AppTextKey::TradeWorkflowAgreementConfirmed),
    643             "Confirmed"
    644         );
    645         assert_eq!(
    646             app_text(AppTextKey::TradeWorkflowAgreementNeedsReview),
    647             "Needs review"
    648         );
    649         assert_eq!(
    650             app_text(AppTextKey::TradeWorkflowRevisionChangeProposed),
    651             "Change proposed"
    652         );
    653         assert_eq!(
    654             app_text(AppTextKey::TradeWorkflowRevisionKeptAsPlaced),
    655             "Kept as placed"
    656         );
    657         assert_eq!(
    658             app_text(AppTextKey::TradeWorkflowInventoryReserved),
    659             "Reserved"
    660         );
    661         assert_eq!(app_text(AppTextKey::TradeWorkflowProvenanceCli), "CLI");
    662         assert_eq!(
    663             app_text(AppTextKey::TradeWorkflowProvenanceLocalEvents),
    664             "Local events"
    665         );
    666     }
    667 
    668     #[test]
    669     fn validation_receipt_copy_covers_passive_evidence() {
    670         for (key, expected) in [
    671             (AppTextKey::TradeValidationReceiptSectionLabel, "Validation"),
    672             (AppTextKey::TradeValidationReceiptEventLabel, "Receipt"),
    673             (AppTextKey::TradeValidationReceiptTargetLabel, "Target"),
    674             (
    675                 AppTextKey::TradeValidationReceiptEventSetRootLabel,
    676                 "Evidence set",
    677             ),
    678             (
    679                 AppTextKey::TradeValidationReceiptReducerOutputRootLabel,
    680                 "Review output",
    681             ),
    682             (
    683                 AppTextKey::TradeValidationReceiptPublicValuesHashLabel,
    684                 "Verification values",
    685             ),
    686             (
    687                 AppTextKey::TradeValidationReceiptRecordedAtLabel,
    688                 "Recorded",
    689             ),
    690             (AppTextKey::TradeValidationReceiptResultValid, "Valid"),
    691             (
    692                 AppTextKey::TradeValidationReceiptResultNeedsReview,
    693                 "Needs review",
    694             ),
    695             (
    696                 AppTextKey::TradeValidationReceiptTypeListingValidation,
    697                 "Listing",
    698             ),
    699             (
    700                 AppTextKey::TradeValidationReceiptTypeTradeTransition,
    701                 "Trade",
    702             ),
    703             (
    704                 AppTextKey::TradeValidationReceiptTypeInventoryState,
    705                 "Stock",
    706             ),
    707             (
    708                 AppTextKey::TradeValidationReceiptTypeStateCheckpoint,
    709                 "State",
    710             ),
    711             (AppTextKey::TradeValidationReceiptProofNone, "None"),
    712             (AppTextKey::TradeValidationReceiptProofSp1Core, "Core proof"),
    713             (
    714                 AppTextKey::TradeValidationReceiptProofSp1Compressed,
    715                 "Compressed proof",
    716             ),
    717             (
    718                 AppTextKey::TradeValidationReceiptProofSp1Groth16,
    719                 "Groth16 proof",
    720             ),
    721             (
    722                 AppTextKey::TradeValidationReceiptProofSp1Plonk,
    723                 "Plonk proof",
    724             ),
    725         ] {
    726             assert_eq!(app_text(key), expected);
    727             assert!(
    728                 app_text(key).split_whitespace().count() <= 3,
    729                 "{} is too long for compact validation receipt evidence",
    730                 key.id()
    731             );
    732         }
    733     }
    734 
    735     #[test]
    736     fn english_marketplace_orders_copy_matches_the_buyer_history_contract() {
    737         assert_eq!(
    738             app_text(AppTextKey::PersonalOrdersSurfaceBody),
    739             "Review orders placed on this device."
    740         );
    741         assert_eq!(
    742             app_text(AppTextKey::PersonalOrdersEmptyTitle),
    743             "No orders yet"
    744         );
    745         assert_eq!(
    746             app_text(AppTextKey::PersonalOrdersListTitle),
    747             "Order history"
    748         );
    749         assert_eq!(
    750             app_text(AppTextKey::PersonalOrdersStatusPlaced),
    751             "Requested"
    752         );
    753         assert_eq!(
    754             app_text(AppTextKey::PersonalOrdersStatusScheduled),
    755             "Agreed"
    756         );
    757         assert_eq!(app_text(AppTextKey::PersonalOrdersStatusReady), "Agreed");
    758         assert_eq!(
    759             app_text(AppTextKey::PersonalOrdersStatusCompleted),
    760             "Agreed"
    761         );
    762         assert_eq!(
    763             app_text(AppTextKey::PersonalOrdersStatusDeclined),
    764             "Declined"
    765         );
    766         assert_eq!(
    767             app_text(AppTextKey::PersonalOrdersStatusNeedsReview),
    768             "Needs review"
    769         );
    770         assert_eq!(
    771             app_text(AppTextKey::PersonalOrdersDetailTitle),
    772             "Order detail"
    773         );
    774         assert_eq!(
    775             app_text(AppTextKey::PersonalOrdersDetailEmptyBody),
    776             "Select an order to review the details."
    777         );
    778         assert_eq!(
    779             app_text(AppTextKey::PersonalOrdersDetailFulfillmentLabel),
    780             "Pickup/delivery"
    781         );
    782         assert_eq!(
    783             app_text(AppTextKey::PersonalOrdersDetailNoteLabel),
    784             "Order note"
    785         );
    786         assert_eq!(
    787             app_text(AppTextKey::PersonalOrdersActionCancel),
    788             "Cancel order"
    789         );
    790         assert_eq!(
    791             app_text(AppTextKey::PersonalOrdersActionAcceptChange),
    792             "Accept change"
    793         );
    794         assert_eq!(
    795             app_text(AppTextKey::PersonalOrdersActionKeepOrder),
    796             "Keep order"
    797         );
    798         assert_eq!(
    799             app_text(AppTextKey::PersonalOrdersRepeatDemandTitle),
    800             "Reorder"
    801         );
    802         assert_eq!(
    803             app_text(AppTextKey::PersonalOrdersRepeatDemandActionEligible),
    804             "Reorder"
    805         );
    806         assert_eq!(
    807             app_text(AppTextKey::PersonalOrdersRepeatDemandActionPartial),
    808             "Reorder available items"
    809         );
    810         assert_eq!(
    811             app_text(AppTextKey::PersonalOrdersRepeatDemandNotePartialSingle),
    812             "One item from this order is currently unavailable to reorder."
    813         );
    814         assert_eq!(
    815             app_text(AppTextKey::PersonalOrdersRepeatDemandNotePartialMultiple),
    816             "Some items from this order are currently unavailable to reorder."
    817         );
    818         assert_eq!(
    819             app_text(AppTextKey::PersonalOrdersRepeatDemandNoteUnavailable),
    820             "Items from this order are currently unavailable to reorder."
    821         );
    822         assert_eq!(
    823             app_text(AppTextKey::PersonalOrdersCoordinationRetryTitle),
    824             "Finish sharing saved orders"
    825         );
    826         assert_eq!(
    827             app_text(AppTextKey::PersonalOrdersCoordinationRetryBody),
    828             "A saved order still needs to be shared with your order tools."
    829         );
    830         assert_eq!(
    831             app_text(AppTextKey::PersonalOrdersCoordinationRetryAction),
    832             "Try sharing again"
    833         );
    834     }
    835 
    836     #[test]
    837     fn english_pack_day_copy_matches_the_contextual_execution_contract() {
    838         assert_eq!(app_text(AppTextKey::PackDayTitle), "Pack day");
    839         assert_eq!(
    840             app_text(AppTextKey::PackDayWindowSummaryTitle),
    841             "Window summary"
    842         );
    843         assert_eq!(
    844             app_text(AppTextKey::PackDayTotalsTitle),
    845             "Totals by product"
    846         );
    847         assert_eq!(app_text(AppTextKey::PackDayPackListTitle), "Pack list");
    848         assert_eq!(
    849             app_text(AppTextKey::PackDayPickupRosterTitle),
    850             "Pickup roster"
    851         );
    852         assert_eq!(
    853             app_text(AppTextKey::PackDayEmptyTitle),
    854             "Nothing to pack yet"
    855         );
    856         assert_eq!(app_text(AppTextKey::PackDayExportTitle), "Export pack day");
    857         assert_eq!(
    858             app_text(AppTextKey::PackDayExportReadyTitle),
    859             "Ready to save locally"
    860         );
    861         assert_eq!(
    862             app_text(AppTextKey::PackDayExportUnavailableTitle),
    863             "Not ready yet"
    864         );
    865         assert_eq!(
    866             app_text(AppTextKey::PackDayExportRunningTitle),
    867             "Saving locally"
    868         );
    869         assert_eq!(
    870             app_text(AppTextKey::PackDayExportSucceededTitle),
    871             "Saved locally"
    872         );
    873         assert_eq!(
    874             app_text(AppTextKey::PackDayExportFailedTitle),
    875             "Couldn't save export"
    876         );
    877         assert_eq!(app_text(AppTextKey::PackDayExportAction), "Export pack day");
    878         assert_eq!(
    879             app_text(AppTextKey::PackDayExportActionRunning),
    880             "Exporting..."
    881         );
    882         assert_eq!(app_text(AppTextKey::PackDayExportFolderLabel), "Folder");
    883         assert_eq!(app_text(AppTextKey::PackDayExportFilesLabel), "Files");
    884         assert_eq!(app_text(AppTextKey::PackDayExportErrorLabel), "Error");
    885         assert_eq!(
    886             app_text(AppTextKey::PackDayPrintPackSheetAction),
    887             "Print pack sheet"
    888         );
    889         assert_eq!(
    890             app_text(AppTextKey::PackDayPrintPackSheetActionRunning),
    891             "Printing pack sheet..."
    892         );
    893         assert_eq!(
    894             app_text(AppTextKey::PackDayPrintPickupRosterAction),
    895             "Print pickup roster"
    896         );
    897         assert_eq!(
    898             app_text(AppTextKey::PackDayPrintPickupRosterActionRunning),
    899             "Printing pickup roster..."
    900         );
    901         assert_eq!(
    902             app_text(AppTextKey::PackDayPrintCustomerLabelsAction),
    903             "Print customer labels (Avery 5160)"
    904         );
    905         assert_eq!(
    906             app_text(AppTextKey::PackDayPrintCustomerLabelsActionRunning),
    907             "Printing customer labels (Avery 5160)..."
    908         );
    909         assert_eq!(
    910             app_text(AppTextKey::PackDayPrintUnavailableTitle),
    911             "Print not available yet"
    912         );
    913         assert_eq!(
    914             app_text(AppTextKey::PackDayPrintUnavailableBody),
    915             "Print actions become available after pack day files are saved locally."
    916         );
    917         assert_eq!(
    918             app_text(AppTextKey::PackDayPrintPackSheetQueuedTitle),
    919             "Queueing pack sheet"
    920         );
    921         assert_eq!(
    922             app_text(AppTextKey::PackDayPrintPackSheetSubmittedTitle),
    923             "Sent pack sheet to the printer"
    924         );
    925         assert_eq!(
    926             app_text(AppTextKey::PackDayPrintPackSheetFailedTitle),
    927             "Couldn't print pack sheet"
    928         );
    929         assert_eq!(
    930             app_text(AppTextKey::PackDayPrintPickupRosterQueuedTitle),
    931             "Queueing pickup roster"
    932         );
    933         assert_eq!(
    934             app_text(AppTextKey::PackDayPrintPickupRosterSubmittedTitle),
    935             "Sent pickup roster to the printer"
    936         );
    937         assert_eq!(
    938             app_text(AppTextKey::PackDayPrintPickupRosterFailedTitle),
    939             "Couldn't print pickup roster"
    940         );
    941         assert_eq!(
    942             app_text(AppTextKey::PackDayPrintCustomerLabelsQueuedTitle),
    943             "Queueing customer labels"
    944         );
    945         assert_eq!(
    946             app_text(AppTextKey::PackDayPrintCustomerLabelsSubmittedTitle),
    947             "Sent customer labels to the printer"
    948         );
    949         assert_eq!(
    950             app_text(AppTextKey::PackDayPrintCustomerLabelsFailedTitle),
    951             "Couldn't print customer labels"
    952         );
    953         assert_eq!(
    954             app_text(AppTextKey::PackDayPrintCustomerLabelsAvery5160OverflowFailedTitle),
    955             "Customer labels do not fit Avery 5160"
    956         );
    957         assert_eq!(app_text(AppTextKey::PackDayBatchPrintAction), "Print all");
    958         assert_eq!(
    959             app_text(AppTextKey::PackDayBatchPrintActionRunning),
    960             "Printing all..."
    961         );
    962         assert_eq!(
    963             app_text(AppTextKey::PackDayBatchPrintQueuedTitle),
    964             "Queueing pack day print run"
    965         );
    966         assert_eq!(
    967             app_text(AppTextKey::PackDayBatchPrintSucceededTitle),
    968             "Sent all pack day files to the printer"
    969         );
    970         assert_eq!(
    971             app_text(AppTextKey::PackDayBatchPrintFailedTitle),
    972             "Couldn't print all pack day files"
    973         );
    974         assert_eq!(
    975             app_text(AppTextKey::PackDayBatchPrintFailedPreflightTitle),
    976             "Pack day files are not ready to print"
    977         );
    978         assert_eq!(
    979             app_text(AppTextKey::PackDayBatchPrintFailedQueueLaunchTitle),
    980             "Couldn't start the print queue"
    981         );
    982         assert_eq!(
    983             app_text(AppTextKey::PackDayBatchPrintFailedQueueExitTitle),
    984             "Print queue stopped before the run finished"
    985         );
    986         assert_eq!(
    987             app_text(AppTextKey::PackDayBatchPrintCustomerLabelsAvery5160OverflowFailedTitle),
    988             "Customer labels do not fit Avery 5160"
    989         );
    990         assert_eq!(
    991             app_text(AppTextKey::PackDayHostHandoffRevealAction),
    992             "Show in Finder"
    993         );
    994         assert_eq!(
    995             app_text(AppTextKey::PackDayHostHandoffRevealActionRunning),
    996             "Showing in Finder..."
    997         );
    998         assert_eq!(
    999             app_text(AppTextKey::PackDayHostHandoffOpenPackSheetAction),
   1000             "Open pack sheet"
   1001         );
   1002         assert_eq!(
   1003             app_text(AppTextKey::PackDayHostHandoffOpenPackSheetActionRunning),
   1004             "Opening pack sheet..."
   1005         );
   1006         assert_eq!(
   1007             app_text(AppTextKey::PackDayHostHandoffOpenPickupRosterAction),
   1008             "Open pickup roster"
   1009         );
   1010         assert_eq!(
   1011             app_text(AppTextKey::PackDayHostHandoffOpenPickupRosterActionRunning),
   1012             "Opening pickup roster..."
   1013         );
   1014         assert_eq!(
   1015             app_text(AppTextKey::PackDayHostHandoffOpenCustomerLabelsAction),
   1016             "Open customer labels"
   1017         );
   1018         assert_eq!(
   1019             app_text(AppTextKey::PackDayHostHandoffOpenCustomerLabelsActionRunning),
   1020             "Opening customer labels..."
   1021         );
   1022         assert_eq!(
   1023             app_text(AppTextKey::PackDayHostHandoffRevealSucceededTitle),
   1024             "Shown in Finder"
   1025         );
   1026         assert_eq!(
   1027             app_text(AppTextKey::PackDayHostHandoffOpenPackSheetSucceededTitle),
   1028             "Opened pack sheet"
   1029         );
   1030         assert_eq!(
   1031             app_text(AppTextKey::PackDayHostHandoffOpenPickupRosterSucceededTitle),
   1032             "Opened pickup roster"
   1033         );
   1034         assert_eq!(
   1035             app_text(AppTextKey::PackDayHostHandoffOpenCustomerLabelsSucceededTitle),
   1036             "Opened customer labels"
   1037         );
   1038         assert_eq!(
   1039             app_text(AppTextKey::PackDayHostHandoffRevealFailedTitle),
   1040             "Couldn't show in Finder"
   1041         );
   1042         assert_eq!(
   1043             app_text(AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle),
   1044             "Couldn't open pack sheet"
   1045         );
   1046         assert_eq!(
   1047             app_text(AppTextKey::PackDayHostHandoffOpenPickupRosterFailedTitle),
   1048             "Couldn't open pickup roster"
   1049         );
   1050         assert_eq!(
   1051             app_text(AppTextKey::PackDayHostHandoffOpenCustomerLabelsFailedTitle),
   1052             "Couldn't open customer labels"
   1053         );
   1054     }
   1055 
   1056     #[test]
   1057     fn english_farm_rules_host_copy_matches_the_frozen_utility_window_inventory() {
   1058         assert_eq!(app_text(AppTextKey::SettingsNavFarm), "Farm");
   1059         assert_eq!(
   1060             app_text(AppTextKey::SettingsFarmPanelBody),
   1061             "Farm profile and pickup details stay local on this device."
   1062         );
   1063         assert_eq!(
   1064             app_text(AppTextKey::SettingsFarmUnavailableBody),
   1065             "Finish setting up a farm before editing farm settings on this device."
   1066         );
   1067         assert_eq!(app_text(AppTextKey::SettingsFarmSaveAction), "Save changes");
   1068         assert_eq!(
   1069             app_text(AppTextKey::SettingsFarmSaveSaved),
   1070             "Saved locally on this device."
   1071         );
   1072         assert_eq!(
   1073             app_text(AppTextKey::SettingsFarmSavePending),
   1074             "Save changes to keep this on this device."
   1075         );
   1076         assert_eq!(
   1077             app_text(AppTextKey::SettingsFarmSaveBlocked),
   1078             "Complete the highlighted fields before saving."
   1079         );
   1080         assert_eq!(
   1081             app_text(AppTextKey::SettingsFarmSaveFailed),
   1082             "Could not save farm settings on this device."
   1083         );
   1084         assert_eq!(
   1085             app_text(AppTextKey::SettingsPickupLocationsSectionLabel),
   1086             "Pickup locations"
   1087         );
   1088         assert_eq!(
   1089             app_text(AppTextKey::SettingsPickupLocationsEmptyBody),
   1090             "Add a pickup location so customers know where to collect orders."
   1091         );
   1092         assert_eq!(
   1093             app_text(AppTextKey::SettingsPickupLocationsAddAction),
   1094             "Add pickup location"
   1095         );
   1096         assert_eq!(
   1097             app_text(AppTextKey::SettingsPickupLocationsMakeDefaultAction),
   1098             "Make default"
   1099         );
   1100         assert_eq!(
   1101             app_text(AppTextKey::SettingsPickupLocationsDefaultBadge),
   1102             "Default"
   1103         );
   1104         assert_eq!(
   1105             app_text(AppTextKey::SettingsPickupLocationsRemoveAction),
   1106             "Remove"
   1107         );
   1108         assert_eq!(
   1109             app_text(AppTextKey::SettingsOperatingRulesSectionLabel),
   1110             "Operating rules"
   1111         );
   1112         assert_eq!(
   1113             app_text(AppTextKey::SettingsOperatingRulesInvalidPromiseLeadTime),
   1114             "Enter whole hours, for example 24."
   1115         );
   1116         assert_eq!(
   1117             app_text(AppTextKey::SettingsFulfillmentWindowsSectionLabel),
   1118             "Fulfillment windows"
   1119         );
   1120         assert_eq!(
   1121             app_text(AppTextKey::SettingsFulfillmentWindowsEmptyBody),
   1122             "Add a fulfillment window so customers know when orders are ready."
   1123         );
   1124         assert_eq!(
   1125             app_text(AppTextKey::SettingsFulfillmentWindowsPickupLocationsBody),
   1126             "Add a pickup location before saving a fulfillment window."
   1127         );
   1128         assert_eq!(
   1129             app_text(AppTextKey::SettingsFulfillmentWindowsAddAction),
   1130             "Add fulfillment window"
   1131         );
   1132         assert_eq!(
   1133             app_text(AppTextKey::SettingsFulfillmentWindowsItemLabel),
   1134             "Fulfillment window"
   1135         );
   1136         assert_eq!(
   1137             app_text(AppTextKey::SettingsBlackoutPeriodsSectionLabel),
   1138             "Blackout periods"
   1139         );
   1140         assert_eq!(
   1141             app_text(AppTextKey::SettingsBlackoutPeriodsEmptyBody),
   1142             "Add a blackout period for days when this farm is unavailable."
   1143         );
   1144         assert_eq!(
   1145             app_text(AppTextKey::SettingsBlackoutPeriodsAddAction),
   1146             "Add blackout period"
   1147         );
   1148         assert_eq!(
   1149             app_text(AppTextKey::SettingsBlackoutPeriodsItemLabel),
   1150             "Blackout period"
   1151         );
   1152         assert_eq!(
   1153             app_text(AppTextKey::SettingsReadinessSectionLabel),
   1154             "Readiness"
   1155         );
   1156         assert_eq!(
   1157             app_text(AppTextKey::SettingsReadinessFieldInvalidTimingConflicts),
   1158             "Invalid timing conflicts"
   1159         );
   1160         assert_eq!(
   1161             app_text(AppTextKey::SettingsReadinessFieldFulfillmentWindowEndsBeforeStart),
   1162             "A fulfillment window ends before it starts."
   1163         );
   1164         assert_eq!(
   1165             app_text(AppTextKey::SettingsReadinessFieldBlackoutOverlapsFulfillmentWindow),
   1166             "A blackout period overlaps a fulfillment window."
   1167         );
   1168         assert_eq!(app_text(AppTextKey::SettingsReadinessReady), "Ready");
   1169     }
   1170 
   1171     #[test]
   1172     fn startup_identity_choice_keys_remain_defined_in_the_typed_registry_source() {
   1173         let source = include_str!("keys.rs");
   1174 
   1175         for entry in [
   1176             "HomeSetupContinueAction => \"home.setup.continue_action\"",
   1177             "HomeSetupGenerateKeyAction => \"home.setup.generate_key_action\"",
   1178             "HomeSetupConnectSignerAction => \"home.setup.connect_signer_action\"",
   1179             "HomeSetupSignerSourcePlaceholder => \"home.setup.signer_source.placeholder\"",
   1180             "HomeSetupSignerConnectAction => \"home.setup.signer_connect_action\"",
   1181             "HomeSetupBackAction => \"home.setup.back_action\"",
   1182             "HomeSetupSignerReviewTitle => \"home.setup.signer.review_title\"",
   1183             "HomeSetupSignerSourceLabel => \"home.setup.signer.source_label\"",
   1184             "HomeSetupSignerSignerLabel => \"home.setup.signer.signer_label\"",
   1185             "HomeSetupSignerRelaysLabel => \"home.setup.signer.relays_label\"",
   1186             "HomeSetupSignerPermissionsLabel => \"home.setup.signer.permissions_label\"",
   1187             "HomeSetupSignerConnectingTitle => \"home.setup.signer.connecting_title\"",
   1188             "HomeSetupSignerPendingTitle => \"home.setup.signer.pending_title\"",
   1189             "HomeSetupSignerAuthChallengeTitle => \"home.setup.signer.auth_challenge_title\"",
   1190             "HomeSetupSignerApprovedTitle => \"home.setup.signer.approved_title\"",
   1191             "HomeSetupIssueUnavailableBody => \"home.setup.issue.unavailable_body\"",
   1192             "HomeSetupErrorStartupFailed => \"home.setup.error.startup_failed\"",
   1193             "HomeSetupSignerSourceValueBunkerUri => \"home.setup.signer.source_value.bunker_uri\"",
   1194             "HomeSetupSignerSourceValueDiscoveryUrl => \"home.setup.signer.source_value.discovery_url\"",
   1195             "HomeSetupSignerPermissionSignEventKind1 => \"home.setup.signer.permission.sign_event_kind_1\"",
   1196             "HomeSetupSignerPermissionSwitchRelays => \"home.setup.signer.permission.switch_relays\"",
   1197             "HomeSetupSignerPermissionAdditional => \"home.setup.signer.permission.additional\"",
   1198             "HomeSetupSignerErrorEnterSource => \"home.setup.signer.error.enter_source\"",
   1199             "HomeSetupSignerErrorUseSignerUri => \"home.setup.signer.error.use_signer_uri\"",
   1200             "HomeSetupSignerErrorMissingDiscoveryUri => \"home.setup.signer.error.missing_discovery_uri\"",
   1201             "HomeSetupSignerErrorInvalidDiscoveryUrl => \"home.setup.signer.error.invalid_discovery_url\"",
   1202             "HomeSetupSignerErrorInvalidRemoteSignerUri => \"home.setup.signer.error.invalid_remote_signer_uri\"",
   1203             "HomeSetupSignerErrorPendingApprovalExists => \"home.setup.signer.error.pending_approval_exists\"",
   1204             "HomeSetupSignerErrorConnectionFailed => \"home.setup.signer.error.connection_failed\"",
   1205         ] {
   1206             assert!(
   1207                 source.contains(entry),
   1208                 "typed startup identity-choice registry is missing {entry}"
   1209             );
   1210         }
   1211     }
   1212 
   1213     #[test]
   1214     fn english_startup_identity_choice_copy_matches_the_next_launcher_contract() {
   1215         assert_eq!(app_text(AppTextKey::HomeSetupContinueAction), "Continue");
   1216         assert_eq!(
   1217             app_text(AppTextKey::HomeSetupGenerateKeyAction),
   1218             "Generate key"
   1219         );
   1220         assert_eq!(
   1221             app_text(AppTextKey::HomeSetupConnectSignerAction),
   1222             "Connect signer"
   1223         );
   1224         assert_eq!(
   1225             app_text(AppTextKey::HomeSetupSignerSourcePlaceholder),
   1226             "Paste bunker URI or discovery URL"
   1227         );
   1228         assert_eq!(
   1229             app_text(AppTextKey::HomeSetupSignerConnectAction),
   1230             "Connect signer"
   1231         );
   1232         assert_eq!(app_text(AppTextKey::HomeSetupBackAction), "Back");
   1233         assert_eq!(
   1234             app_text(AppTextKey::HomeSetupSignerReviewTitle),
   1235             "Review signer details"
   1236         );
   1237         assert_eq!(app_text(AppTextKey::HomeSetupSignerSourceLabel), "Source");
   1238         assert_eq!(app_text(AppTextKey::HomeSetupSignerSignerLabel), "Signer");
   1239         assert_eq!(app_text(AppTextKey::HomeSetupSignerRelaysLabel), "Relays");
   1240         assert_eq!(
   1241             app_text(AppTextKey::HomeSetupSignerPermissionsLabel),
   1242             "Permissions"
   1243         );
   1244         assert_eq!(
   1245             app_text(AppTextKey::HomeSetupSignerConnectingTitle),
   1246             "Connecting to signer"
   1247         );
   1248         assert_eq!(
   1249             app_text(AppTextKey::HomeSetupSignerPendingTitle),
   1250             "Waiting for signer approval"
   1251         );
   1252         assert_eq!(
   1253             app_text(AppTextKey::HomeSetupSignerAuthChallengeTitle),
   1254             "Continue in your signer"
   1255         );
   1256         assert_eq!(
   1257             app_text(AppTextKey::HomeSetupSignerApprovedTitle),
   1258             "Signer approved"
   1259         );
   1260         assert_eq!(
   1261             app_text(AppTextKey::HomeSetupIssueUnavailableBody),
   1262             "Radroots couldn't start normally on this device. Check the local setup and try again."
   1263         );
   1264         assert_eq!(
   1265             app_text(AppTextKey::HomeSetupErrorStartupFailed),
   1266             "Couldn't finish startup right now. Check the connection and try again."
   1267         );
   1268         assert_eq!(
   1269             app_text(AppTextKey::HomeSetupSignerSourceValueBunkerUri),
   1270             "Bunker URI"
   1271         );
   1272         assert_eq!(
   1273             app_text(AppTextKey::HomeSetupSignerSourceValueDiscoveryUrl),
   1274             "Discovery URL"
   1275         );
   1276         assert_eq!(
   1277             app_text(AppTextKey::HomeSetupSignerPermissionSignEventKind1),
   1278             "Sign notes"
   1279         );
   1280         assert_eq!(
   1281             app_text(AppTextKey::HomeSetupSignerPermissionSwitchRelays),
   1282             "Switch relays"
   1283         );
   1284         assert_eq!(
   1285             app_text(AppTextKey::HomeSetupSignerPermissionAdditional),
   1286             "Additional permission"
   1287         );
   1288         assert_eq!(
   1289             app_text(AppTextKey::HomeSetupSignerErrorEnterSource),
   1290             "Paste a bunker URI or discovery URL from your signer to continue."
   1291         );
   1292         assert_eq!(
   1293             app_text(AppTextKey::HomeSetupSignerErrorUseSignerUri),
   1294             "Use a bunker URI or discovery URL from your signer."
   1295         );
   1296         assert_eq!(
   1297             app_text(AppTextKey::HomeSetupSignerErrorMissingDiscoveryUri),
   1298             "The discovery URL is missing the signer address."
   1299         );
   1300         assert_eq!(
   1301             app_text(AppTextKey::HomeSetupSignerErrorInvalidDiscoveryUrl),
   1302             "That discovery URL isn't valid. Check it and try again."
   1303         );
   1304         assert_eq!(
   1305             app_text(AppTextKey::HomeSetupSignerErrorInvalidRemoteSignerUri),
   1306             "That signer address isn't valid. Check it and try again."
   1307         );
   1308         assert_eq!(
   1309             app_text(AppTextKey::HomeSetupSignerErrorPendingApprovalExists),
   1310             "A signer connection is already waiting for approval."
   1311         );
   1312         assert_eq!(
   1313             app_text(AppTextKey::HomeSetupSignerErrorConnectionFailed),
   1314             "Couldn't continue with the signer. Check the signer and try again."
   1315         );
   1316     }
   1317 
   1318     #[test]
   1319     fn english_products_workflow_copy_matches_the_editor_contract() {
   1320         assert_eq!(app_text(AppTextKey::ProductsAddAction), "Add product");
   1321         assert_eq!(app_text(AppTextKey::ProductsEditorTitle), "Product details");
   1322         assert_eq!(
   1323             app_text(AppTextKey::ProductsEditorBody),
   1324             "Saved locally on this device."
   1325         );
   1326         assert_eq!(app_text(AppTextKey::ProductsEditorFieldTitle), "Name");
   1327         assert_eq!(app_text(AppTextKey::ProductsEditorFieldSubtitle), "Details");
   1328         assert_eq!(
   1329             app_text(AppTextKey::ProductsEditorFieldCategory),
   1330             "Category"
   1331         );
   1332         assert_eq!(app_text(AppTextKey::ProductsEditorFieldUnit), "Unit");
   1333         assert_eq!(
   1334             app_text(AppTextKey::ProductsEditorFieldPrice),
   1335             "Price (USD)"
   1336         );
   1337         assert_eq!(app_text(AppTextKey::ProductsEditorFieldStock), "Stock");
   1338         assert_eq!(app_text(AppTextKey::ProductsEditorFieldStatus), "Status");
   1339         assert_eq!(app_text(AppTextKey::ProductsEditorCloseAction), "Close");
   1340         assert_eq!(
   1341             app_text(AppTextKey::ProductsEditorSaveAction),
   1342             "Save changes"
   1343         );
   1344         assert_eq!(
   1345             app_text(AppTextKey::ProductsEditorPublishReadinessTitle),
   1346             "Publish readiness"
   1347         );
   1348         assert_eq!(
   1349             app_text(AppTextKey::ProductsEditorReady),
   1350             "This product is ready to publish."
   1351         );
   1352         assert_eq!(
   1353             app_text(AppTextKey::ProductsEditorBlockerAddProductName),
   1354             "Add a product name."
   1355         );
   1356         assert_eq!(
   1357             app_text(AppTextKey::ProductsEditorBlockerChooseCategory),
   1358             "Choose a category."
   1359         );
   1360         assert_eq!(
   1361             app_text(AppTextKey::ProductsEditorBlockerChooseUnit),
   1362             "Choose a unit."
   1363         );
   1364         assert_eq!(
   1365             app_text(AppTextKey::ProductsEditorBlockerSetPrice),
   1366             "Set a price."
   1367         );
   1368         assert_eq!(
   1369             app_text(AppTextKey::ProductsEditorBlockerSetStock),
   1370             "Set available stock."
   1371         );
   1372         assert_eq!(
   1373             app_text(AppTextKey::ProductsEditorBlockerAttachAvailability),
   1374             "Attach an availability window."
   1375         );
   1376         assert_eq!(
   1377             app_text(AppTextKey::ProductsUntitledDraft),
   1378             "Untitled draft"
   1379         );
   1380     }
   1381 
   1382     #[test]
   1383     fn host_locale_negotiation_reduces_to_supported_base_locale() {
   1384         assert_eq!(resolve_locale_from_host("en_US.UTF-8"), "en");
   1385         assert_eq!(resolve_locale_from_host("en-GB"), "en");
   1386         assert_eq!(resolve_locale_from_host("en:fr"), "en");
   1387         assert_eq!(resolve_locale_from_host(""), "en");
   1388         assert_eq!(resolve_locale_from_host("C.UTF-8"), "en");
   1389     }
   1390 
   1391     fn is_visible_action_text_key(key: AppTextKey) -> bool {
   1392         let id = key.id();
   1393         id.contains(".action") || id.contains("_action")
   1394     }
   1395 
   1396     fn is_buyer_visible_text_key(key: AppTextKey) -> bool {
   1397         key.id().starts_with("messages.personal.")
   1398     }
   1399 
   1400     fn is_trade_workflow_text_key(key: AppTextKey) -> bool {
   1401         key.id().starts_with("messages.trade.workflow.")
   1402     }
   1403 
   1404     fn contains_reserved_payment_action_term(value: &str, term: &str) -> bool {
   1405         if term.contains(' ') || term.contains('-') {
   1406             return value.contains(term);
   1407         }
   1408 
   1409         value.match_indices(term).any(|(start, _)| {
   1410             let end = start + term.len();
   1411             is_reserved_payment_term_boundary_before(value, start)
   1412                 && is_reserved_payment_term_boundary_after(value, end)
   1413         })
   1414     }
   1415 
   1416     fn is_reserved_payment_term_boundary_before(value: &str, index: usize) -> bool {
   1417         if index == 0 {
   1418             return true;
   1419         }
   1420 
   1421         is_reserved_payment_term_boundary_byte(value.as_bytes()[index - 1])
   1422     }
   1423 
   1424     fn is_reserved_payment_term_boundary_after(value: &str, index: usize) -> bool {
   1425         if index == value.len() {
   1426             return true;
   1427         }
   1428 
   1429         is_reserved_payment_term_boundary_byte(value.as_bytes()[index])
   1430     }
   1431 
   1432     fn is_reserved_payment_term_boundary_byte(byte: u8) -> bool {
   1433         !byte.is_ascii_alphanumeric() && byte != b'_' && byte != b'-'
   1434     }
   1435 }