diff --git a/herbapi-ui/src/app.rs b/herbapi-ui/src/app.rs index 92c6b0f..4107e8f 100644 --- a/herbapi-ui/src/app.rs +++ b/herbapi-ui/src/app.rs @@ -2,6 +2,7 @@ use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; use crate::api; +use crate::i18n::t; use crate::types::MeResponse; /// Global language signal shared via context. Values: "de" or "en". @@ -61,6 +62,8 @@ fn Layout() -> Element { let auth = use_resource(|| async { api::get_current_user().await.ok() }); let user: Option = auth.read().as_ref().and_then(|r| r.clone()); + let l = &*current_lang; + rsx! { div { class: "app-layout", nav { class: "sidebar", @@ -68,17 +71,17 @@ fn Layout() -> Element { span { class: "brand-icon", "\u{1F33F}" } div { class: "brand-text-group", span { class: "brand-text", "HerbAPI" } - span { class: "brand-sub", "Plant Database" } + span { class: "brand-sub", "{t(l, \"brand.sub\")}" } } } div { class: "sidebar-nav", - NavLink { to: Route::Home {}, label: "Home" } - NavLink { to: Route::FamilyList {}, label: "Families" } - NavLink { to: Route::SpeciesList {}, label: "Species" } - NavLink { to: Route::CultivarList {}, label: "Cultivars" } - NavLink { to: Route::SupplierList {}, label: "Suppliers" } - NavLink { to: Route::SearchPage {}, label: "Search" } - NavLink { to: Route::Sources {}, label: "Sources" } + NavLink { to: Route::Home {}, label: t(l, "nav.home") } + NavLink { to: Route::FamilyList {}, label: t(l, "nav.families") } + NavLink { to: Route::SpeciesList {}, label: t(l, "nav.species") } + NavLink { to: Route::CultivarList {}, label: t(l, "nav.cultivars") } + NavLink { to: Route::SupplierList {}, label: t(l, "nav.suppliers") } + NavLink { to: Route::SearchPage {}, label: t(l, "nav.search") } + NavLink { to: Route::Sources {}, label: t(l, "nav.sources") } } div { class: "sidebar-lang", div { class: "lang-toggle", @@ -103,9 +106,9 @@ fn Layout() -> Element { div { class: "sidebar-user", if let Some(ref u) = user { span { class: "user-name", "{u.nickname.as_deref().or(u.name.as_deref()).unwrap_or(&u.email)}" } - a { class: "logout-link", href: "/auth/oidc/logout", "Logout" } + a { class: "logout-link", href: "/auth/oidc/logout", "{t(l, \"btn.logout\")}" } } else { - a { class: "login-link", href: "/auth/oidc/login", "Login" } + a { class: "login-link", href: "/auth/oidc/login", "{t(l, \"btn.login\")}" } } } } diff --git a/herbapi-ui/src/components/planting_calendar.rs b/herbapi-ui/src/components/planting_calendar.rs index 9958108..2f5c6db 100644 --- a/herbapi-ui/src/components/planting_calendar.rs +++ b/herbapi-ui/src/components/planting_calendar.rs @@ -1,5 +1,8 @@ use dioxus::prelude::*; +use crate::app::Lang; +use crate::i18n::t; + /// Month boundaries as week ranges (approximate ISO weeks). /// Jan=1-4, Feb=5-8, Mar=9-13, Apr=14-17, May=18-22, Jun=23-26, /// Jul=27-30, Aug=31-35, Sep=36-39, Oct=40-44, Nov=45-48, Dec=49-52 @@ -54,17 +57,22 @@ pub fn PlantingCalendar( glasshouse_weeks: Option>, harvesting_weeks: Option>, ) -> Element { - let rows: Vec<(&str, &str, Option>)> = vec![ - ("Indoor Sowing", "cal-indoor", resolve_weeks(&indoor_sowing_weeks, &indoor_sowing_months)), - ("Direct Sowing", "cal-direct", resolve_weeks(&direct_sowing_weeks, &direct_sowing_months)), - ("Transplanting", "cal-transplant", resolve_weeks(&transplanting_weeks, &transplanting_months)), - ("Glasshouse", "cal-glass", resolve_weeks(&glasshouse_weeks, &glasshouse_months)), - ("Harvesting", "cal-harvest", resolve_weeks(&harvesting_weeks, &harvesting_months)), + let lang = use_context::().0; + let l = lang.read().clone(); + + let row_indoor = (t(&l, "cal.indoor_sowing").to_string(), "cal-indoor".to_string(), resolve_weeks(&indoor_sowing_weeks, &indoor_sowing_months)); + let row_direct = (t(&l, "cal.direct_sowing").to_string(), "cal-direct".to_string(), resolve_weeks(&direct_sowing_weeks, &direct_sowing_months)); + let row_transplant = (t(&l, "cal.transplanting").to_string(), "cal-transplant".to_string(), resolve_weeks(&transplanting_weeks, &transplanting_months)); + let row_glass = (t(&l, "cal.glasshouse").to_string(), "cal-glass".to_string(), resolve_weeks(&glasshouse_weeks, &glasshouse_months)); + let row_harvest = (t(&l, "cal.harvesting").to_string(), "cal-harvest".to_string(), resolve_weeks(&harvesting_weeks, &harvesting_months)); + + let rows: Vec<(String, String, Option>)> = vec![ + row_indoor, row_direct, row_transplant, row_glass, row_harvest, ]; let has_data = rows.iter().any(|(_, _, w)| w.is_some()); if !has_data { - return rsx! { p { class: "empty", "No planting calendar data." } }; + return rsx! { p { class: "empty", "{t(&l, \"no_planting_data\")}" } }; } rsx! { @@ -111,23 +119,23 @@ pub fn PlantingCalendar( div { class: "wcal-legend", div { class: "wcal-legend-item", div { class: "wcal-legend-swatch cal-indoor" } - span { "Indoor Sowing" } + span { "{t(&l, \"cal.indoor_sowing\")}" } } div { class: "wcal-legend-item", div { class: "wcal-legend-swatch cal-direct" } - span { "Direct Sowing" } + span { "{t(&l, \"cal.direct_sowing\")}" } } div { class: "wcal-legend-item", div { class: "wcal-legend-swatch cal-transplant" } - span { "Transplanting" } + span { "{t(&l, \"cal.transplanting\")}" } } div { class: "wcal-legend-item", div { class: "wcal-legend-swatch cal-glass" } - span { "Glasshouse" } + span { "{t(&l, \"cal.glasshouse\")}" } } div { class: "wcal-legend-item", div { class: "wcal-legend-swatch cal-harvest" } - span { "Harvesting" } + span { "{t(&l, \"cal.harvesting\")}" } } } } diff --git a/herbapi-ui/src/components/table_controls.rs b/herbapi-ui/src/components/table_controls.rs index 649f936..1a7cb07 100644 --- a/herbapi-ui/src/components/table_controls.rs +++ b/herbapi-ui/src/components/table_controls.rs @@ -2,6 +2,9 @@ use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; use std::collections::HashSet; +use crate::app::Lang; +use crate::i18n::t; + /// Column definition for configurable tables #[derive(Clone, PartialEq)] pub struct ColumnDef { @@ -36,9 +39,12 @@ pub fn ColumnToggle( visible: Signal>, storage_key: String, ) -> Element { + let lang = use_context::().0; + let l = lang.read().clone(); + rsx! { div { class: "column-toggle", - span { class: "column-toggle-label", "Columns:" } + span { class: "column-toggle-label", "{t(&l, \"table.columns\")}" } for col in columns.iter() { { let key = col.key.to_string(); @@ -74,11 +80,13 @@ pub fn PerPageSelector( page: Signal, storage_key: String, ) -> Element { + let lang = use_context::().0; + let l = lang.read().clone(); let current = *per_page.read(); rsx! { div { class: "per-page-selector", - label { "Show " } + label { "{t(&l, \"table.show\")} " } select { value: "{current}", onchange: move |e| { @@ -93,7 +101,7 @@ pub fn PerPageSelector( option { value: "50", "50" } option { value: "100", "100" } } - label { " per page" } + label { " {t(&l, \"table.per_page\")}" } } } } diff --git a/herbapi-ui/src/i18n.rs b/herbapi-ui/src/i18n.rs index deac8b9..17f9534 100644 --- a/herbapi-ui/src/i18n.rs +++ b/herbapi-ui/src/i18n.rs @@ -23,3 +23,369 @@ pub fn pick_name(lang: &str, de: &Option, en: &Option, scientifi .unwrap_or(scientific).to_string(), } } + +/// Translate a UI label key into the appropriate language. +/// All keys should be covered; unknown keys return "???". +pub fn t(lang: &str, key: &str) -> &'static str { + match (lang, key) { + // Navigation + ("de", "nav.home") => "Startseite", + ("de", "nav.families") => "Familien", + ("de", "nav.species") => "Arten", + ("de", "nav.cultivars") => "Sorten", + ("de", "nav.suppliers") => "Lieferanten", + ("de", "nav.search") => "Suche", + ("de", "nav.sources") => "Quellen", + + // Sidebar + ("de", "brand.sub") => "Pflanzendatenbank", + + // Page headings + ("de", "page.species") => "Arten", + ("de", "page.cultivars") => "Sorten", + ("de", "page.families") => "Pflanzenfamilien", + ("de", "page.suppliers") => "Lieferanten", + ("de", "page.search") => "Suche", + ("de", "page.sources") => "Datenquellen", + ("de", "page.recent_species") => "Neueste Arten", + ("de", "page.species_in_family") => "Arten in dieser Familie", + + // Card headers + ("de", "card.species_details") => "Artdetails", + ("de", "card.cultivar_details") => "Sortendetails", + ("de", "card.growing_info") => "Anbauinformationen", + ("de", "card.growing_requirements") => "Anbauanforderungen", + ("de", "card.planting_schedule") => "Pflanzkalender", + ("de", "card.planting_calendar") => "Pflanzkalender", + ("de", "card.germination") => "Keimung", + ("de", "card.climate") => "Klima & Umgebung", + ("de", "card.where_to_buy") => "Bezugsquellen", + ("de", "card.uses") => "Nutzung", + ("de", "card.ecology") => "\u{00d6}kologie", + ("de", "card.permaculture") => "Permakultur", + ("de", "card.external_links") => "Externe Links", + ("de", "card.wildlife") => "Wildtiere & \u{00d6}kologie", + ("de", "card.image") => "Bild", + ("de", "card.species_info") => "Artinformationen", + ("de", "card.cultivars") => "Sorten", + + // Field labels + ("de", "field.scientific_name") => "Wissenschaftlicher Name", + ("de", "field.common_name") => "Allgemeiner Name", + ("de", "field.name") => "Name", + ("de", "field.name_en") => "Name EN", + ("de", "field.name_de") => "Name DE", + ("de", "field.family") => "Familie", + ("de", "field.description") => "Beschreibung", + ("de", "field.organic") => "Bio", + ("de", "field.perennial") => "Mehrj\u{00e4}hrig", + ("de", "field.growing_time") => "Kulturzeit", + ("de", "field.planting_depth") => "Pflanztiefe", + ("de", "field.row_spacing") => "Reihenabstand", + ("de", "field.plant_spacing") => "Pflanzabstand", + ("de", "field.propagation") => "Vermehrung", + ("de", "field.days_to_germ") => "Tage bis Keimung", + ("de", "field.germ_temp") => "Keimtemperatur", + ("de", "field.light_req") => "Lichtbedarf", + ("de", "field.stratification") => "Stratifikation erforderlich", + ("de", "field.stratification_days") => "Stratifikationstage", + ("de", "field.scarification") => "Skarifizierung erforderlich", + ("de", "field.min_temp") => "Min. Temperatur", + ("de", "field.max_temp") => "Max. Temperatur", + ("de", "field.humidity") => "Luftfeuchtigkeit", + ("de", "field.light") => "Licht", + ("de", "field.frost_tolerance") => "Frosttoleranz", + ("de", "field.drought_tolerance") => "Trockenheitstoleranz", + ("de", "field.usda_zone") => "USDA-Zone", + ("de", "field.at_zone") => "AT-Zone", + ("de", "field.ph_range") => "pH-Bereich", + ("de", "field.soil_moisture") => "Bodenfeuchte", + ("de", "field.salt_tolerance") => "Salztoleranz", + ("de", "field.plant_layer") => "Pflanzenschicht", + ("de", "field.nitrogen_fixer") => "Stickstoffbinder", + ("de", "field.dynamic_acc") => "Dynamischer N\u{00e4}hrstoffsammler", + ("de", "field.food_uses") => "Nahrungsnutzung", + ("de", "field.medicinal_uses") => "Medizinische Nutzung", + ("de", "field.other_uses") => "Sonstige Nutzung", + ("de", "field.wildlife_value") => "Wildtierwert", + ("de", "field.native_range") => "Herkunftsgebiet", + ("de", "field.edibility") => "Essbarkeit", + ("de", "field.pollination") => "Best\u{00e4}ubung", + ("de", "field.species") => "Art", + ("de", "field.supplier") => "Lieferant", + ("de", "field.sku") => "Art.-Nr.", + ("de", "field.price") => "Preis", + ("de", "field.link") => "Link", + ("de", "field.nectar") => "Nektar", + ("de", "field.pollen") => "Pollen", + ("de", "field.wild_bees") => "Wildbienen", + ("de", "field.butterflies") => "Schmetterlinge & Falter", + ("de", "field.native_status") => "Heimatstatus", + ("de", "field.succession") => "Sukzessionsstufe", + ("de", "field.country") => "Land", + ("de", "field.website") => "Website", + ("de", "field.notes") => "Anmerkungen", + ("de", "field.attracts_pollinators") => "Zieht Best\u{00e4}uber an", + ("de", "field.attracts_beneficial") => "Zieht N\u{00fc}tzlinge an", + ("de", "field.mulch_plant") => "Mulchpflanze", + ("de", "field.ground_cover_quality") => "Bodendeckerqualit\u{00e4}t", + ("de", "field.allelopathic") => "Allelopathisch", + ("de", "field.guild_role") => "Gildenrolle", + ("de", "field.wikidata_qid") => "Wikidata QID", + ("de", "field.gbif_id") => "GBIF ID", + ("de", "field.eppo_code") => "EPPO-Code", + ("de", "field.pfaf_url") => "PFAF URL", + ("de", "field.hoverflies") => "Schwebfliegen", + ("de", "field.beetles") => "K\u{00e4}fer", + ("de", "field.birds") => "V\u{00f6}gel", + ("de", "field.mammals") => "S\u{00e4}ugetiere", + ("de", "field.naturadb_tags") => "NaturaDB Tags", + ("de", "field.caterpillar_specialists") => "Raupen-Spezialisten", + ("de", "field.succession_planting") => "Staffelpflanzung", + ("de", "field.planting_notes") => "Pflanzhinweise", + ("de", "field.min_light_hours") => "Min. Lichtstunden/Tag", + ("de", "field.opt_light_hours") => "Opt. Lichtstunden/Tag", + ("de", "field.usda_hardiness_zone") => "USDA-Winterh\u{00e4}rtezone", + ("de", "field.n_fixer") => "N-Binder", + ("de", "field.dyn_accum") => "Dyn. Sammler", + ("de", "field.layer") => "Schicht", + ("de", "field.drought_tol") => "Trockentol.", + ("de", "field.frost_tol") => "Frosttol.", + ("de", "field.days_germ") => "Tage Keimung", + ("de", "field.data_used") => "Verwendete Daten", + ("de", "field.license") => "Lizenz", + ("de", "field.view_on_pfaf") => "Auf PFAF ansehen", + ("de", "field.source") => "Quelle", + + // Planting calendar + ("de", "cal.indoor_sowing") => "Voranzucht", + ("de", "cal.direct_sowing") => "Direktsaat", + ("de", "cal.transplanting") => "Auspflanzen", + ("de", "cal.glasshouse") => "Gew\u{00e4}chshaus", + ("de", "cal.harvesting") => "Ernte", + + // Buttons + ("de", "btn.previous") => "Zur\u{00fc}ck", + ("de", "btn.next") => "Weiter", + ("de", "btn.view") => "Ansehen", + ("de", "btn.search") => "Suchen", + ("de", "btn.table_view") => "Tabellenansicht", + ("de", "btn.card_view") => "Kartenansicht", + ("de", "btn.logout") => "Abmelden", + ("de", "btn.login") => "Anmelden", + + // Table controls + ("de", "table.columns") => "Spalten:", + ("de", "table.show") => "Zeige", + ("de", "table.per_page") => "pro Seite", + + // Filters + ("de", "filter.layer") => "Schicht", + ("de", "filter.drought_tol") => "Trockentol.", + ("de", "filter.all") => "Alle", + + // General + ("de", "yes") => "Ja", + ("de", "no") => "Nein", + ("de", "annual") => "Einj\u{00e4}hrig", + ("de", "loading") => "Laden\u{2026}", + ("de", "estimated_note") => "Mit ~ markierte Werte sind von der Art gesch\u{00e4}tzt", + ("de", "page") => "Seite", + ("de", "of") => "von", + ("de", "no_cultivars") => "Noch keine Sorten.", + ("de", "no_suppliers_linked") => "Keine Lieferanten verkn\u{00fc}pft", + ("de", "no_planting_data") => "Keine Pflanzkalenderdaten.", + ("de", "results") => "Ergebnisse", + ("de", "search.placeholder") => "Pflanzen suchen\u{2026}", + ("de", "search.placeholder_species") => "Arten suchen\u{2026}", + ("de", "search.placeholder_cultivars") => "Sorten suchen\u{2026}", + ("de", "search.placeholder_families") => "Familien suchen\u{2026}", + ("de", "search.placeholder_full") => "Pflanzen, Familien, Sorten suchen\u{2026}", + ("de", "subtitle") => "Dreisprachige Pflanzen-Referenzdatenbank", + ("de", "sources.intro") => "HerbAPI aggregiert Pflanzendaten aus mehreren offenen Quellen. Wir danken diesen Projekten, dass sie botanisches Wissen frei zug\u{00e4}nglich machen.", + ("de", "species_species") => "Arten", + ("de", "loading_species_data") => "Lade Artdaten\u{2026}", + + // ── English defaults ────────────────────────────────────── + + // Navigation + (_, "nav.home") => "Home", + (_, "nav.families") => "Families", + (_, "nav.species") => "Species", + (_, "nav.cultivars") => "Cultivars", + (_, "nav.suppliers") => "Suppliers", + (_, "nav.search") => "Search", + (_, "nav.sources") => "Sources", + + // Sidebar + (_, "brand.sub") => "Plant Database", + + // Page headings + (_, "page.species") => "Species", + (_, "page.cultivars") => "Cultivars", + (_, "page.families") => "Plant Families", + (_, "page.suppliers") => "Suppliers", + (_, "page.search") => "Search", + (_, "page.sources") => "Data Sources", + (_, "page.recent_species") => "Recent Species", + (_, "page.species_in_family") => "Species in this Family", + + // Card headers + (_, "card.species_details") => "Species Details", + (_, "card.cultivar_details") => "Cultivar Details", + (_, "card.growing_info") => "Growing Information", + (_, "card.growing_requirements") => "Growing Requirements", + (_, "card.planting_schedule") => "Planting Schedule", + (_, "card.planting_calendar") => "Planting Calendar", + (_, "card.germination") => "Germination", + (_, "card.climate") => "Climate & Environment", + (_, "card.where_to_buy") => "Where to Buy", + (_, "card.uses") => "Uses", + (_, "card.ecology") => "Ecology", + (_, "card.permaculture") => "Permaculture", + (_, "card.external_links") => "External Links", + (_, "card.wildlife") => "Wildlife & Ecology", + (_, "card.image") => "Image", + (_, "card.species_info") => "Species Information", + (_, "card.cultivars") => "Cultivars", + + // Field labels + (_, "field.scientific_name") => "Scientific Name", + (_, "field.common_name") => "Common Name", + (_, "field.name") => "Name", + (_, "field.name_en") => "Name EN", + (_, "field.name_de") => "Name DE", + (_, "field.family") => "Family", + (_, "field.description") => "Description", + (_, "field.organic") => "Organic", + (_, "field.perennial") => "Perennial", + (_, "field.growing_time") => "Growing Time", + (_, "field.planting_depth") => "Planting Depth", + (_, "field.row_spacing") => "Row Spacing", + (_, "field.plant_spacing") => "Plant Spacing", + (_, "field.propagation") => "Propagation Methods", + (_, "field.days_to_germ") => "Days to Germination", + (_, "field.germ_temp") => "Germination Temp", + (_, "field.light_req") => "Light Requirement", + (_, "field.stratification") => "Stratification Required", + (_, "field.stratification_days") => "Stratification Days", + (_, "field.scarification") => "Scarification Required", + (_, "field.min_temp") => "Min Temp", + (_, "field.max_temp") => "Max Temp", + (_, "field.humidity") => "Humidity", + (_, "field.light") => "Light", + (_, "field.frost_tolerance") => "Frost Tolerance", + (_, "field.drought_tolerance") => "Drought Tolerance", + (_, "field.usda_zone") => "USDA Zone", + (_, "field.at_zone") => "AT Zone", + (_, "field.ph_range") => "pH Range", + (_, "field.soil_moisture") => "Soil Moisture", + (_, "field.salt_tolerance") => "Salt Tolerance", + (_, "field.plant_layer") => "Plant Layer", + (_, "field.nitrogen_fixer") => "Nitrogen Fixer", + (_, "field.dynamic_acc") => "Dynamic Accumulator", + (_, "field.food_uses") => "Food Uses", + (_, "field.medicinal_uses") => "Medicinal Uses", + (_, "field.other_uses") => "Other Uses", + (_, "field.wildlife_value") => "Wildlife Value", + (_, "field.native_range") => "Native Range", + (_, "field.edibility") => "Edibility Rating", + (_, "field.pollination") => "Pollination Type", + (_, "field.species") => "Species", + (_, "field.supplier") => "Supplier", + (_, "field.sku") => "SKU", + (_, "field.price") => "Price", + (_, "field.link") => "Link", + (_, "field.nectar") => "Nectar Value", + (_, "field.pollen") => "Pollen Value", + (_, "field.wild_bees") => "Wild Bees", + (_, "field.butterflies") => "Butterflies & Moths", + (_, "field.native_status") => "Native Status", + (_, "field.succession") => "Succession Stage", + (_, "field.country") => "Country", + (_, "field.website") => "Website", + (_, "field.notes") => "Notes", + (_, "field.attracts_pollinators") => "Attracts Pollinators", + (_, "field.attracts_beneficial") => "Attracts Beneficial Insects", + (_, "field.mulch_plant") => "Mulch Plant", + (_, "field.ground_cover_quality") => "Ground Cover Quality", + (_, "field.allelopathic") => "Allelopathic", + (_, "field.guild_role") => "Guild Role", + (_, "field.wikidata_qid") => "Wikidata QID", + (_, "field.gbif_id") => "GBIF ID", + (_, "field.eppo_code") => "EPPO Code", + (_, "field.pfaf_url") => "PFAF URL", + (_, "field.hoverflies") => "Hoverflies", + (_, "field.beetles") => "Beetles", + (_, "field.birds") => "Birds", + (_, "field.mammals") => "Mammals", + (_, "field.naturadb_tags") => "NaturaDB Tags", + (_, "field.caterpillar_specialists") => "Caterpillar Specialists", + (_, "field.succession_planting") => "Succession Planting", + (_, "field.planting_notes") => "Planting Notes", + (_, "field.min_light_hours") => "Min Light Hours/Day", + (_, "field.opt_light_hours") => "Optimal Light Hours/Day", + (_, "field.usda_hardiness_zone") => "USDA Hardiness Zone", + (_, "field.n_fixer") => "N-Fixer", + (_, "field.dyn_accum") => "Dyn. Accum.", + (_, "field.layer") => "Layer", + (_, "field.drought_tol") => "Drought Tol.", + (_, "field.frost_tol") => "Frost Tol.", + (_, "field.days_germ") => "Days to Germ.", + (_, "field.data_used") => "Data used", + (_, "field.license") => "License", + (_, "field.view_on_pfaf") => "View on PFAF", + (_, "field.source") => "Source", + + // Planting calendar + (_, "cal.indoor_sowing") => "Indoor Sowing", + (_, "cal.direct_sowing") => "Direct Sowing", + (_, "cal.transplanting") => "Transplanting", + (_, "cal.glasshouse") => "Glasshouse", + (_, "cal.harvesting") => "Harvesting", + + // Buttons + (_, "btn.previous") => "Previous", + (_, "btn.next") => "Next", + (_, "btn.view") => "View", + (_, "btn.search") => "Search", + (_, "btn.table_view") => "Table View", + (_, "btn.card_view") => "Card View", + (_, "btn.logout") => "Logout", + (_, "btn.login") => "Login", + + // Table controls + (_, "table.columns") => "Columns:", + (_, "table.show") => "Show", + (_, "table.per_page") => "per page", + + // Filters + (_, "filter.layer") => "Layer", + (_, "filter.drought_tol") => "Drought Tol.", + (_, "filter.all") => "All", + + // General + (_, "yes") => "Yes", + (_, "no") => "No", + (_, "annual") => "Annual", + (_, "loading") => "Loading...", + (_, "estimated_note") => "Values marked ~ species are estimated from species-level data", + (_, "page") => "Page", + (_, "of") => "of", + (_, "no_cultivars") => "No cultivars yet.", + (_, "no_suppliers_linked") => "No suppliers linked", + (_, "no_planting_data") => "No planting calendar data.", + (_, "results") => "results", + (_, "search.placeholder") => "Search plants...", + (_, "search.placeholder_species") => "Search species...", + (_, "search.placeholder_cultivars") => "Search cultivars...", + (_, "search.placeholder_families") => "Search families...", + (_, "search.placeholder_full") => "Search plants, families, cultivars...", + (_, "subtitle") => "Trilingual plant reference database", + (_, "sources.intro") => "HerbAPI aggregates plant data from multiple open sources. We are grateful to these projects for making botanical knowledge freely available.", + (_, "species_species") => "species", + (_, "loading_species_data") => "Loading species data\u{2026}", + + _ => "???", + } +} diff --git a/herbapi-ui/src/pages/cultivars.rs b/herbapi-ui/src/pages/cultivars.rs index 8467d07..3551452 100644 --- a/herbapi-ui/src/pages/cultivars.rs +++ b/herbapi-ui/src/pages/cultivars.rs @@ -6,7 +6,7 @@ use crate::api; use crate::app::{Lang, Route}; use crate::components::planting_calendar::PlantingCalendar; use crate::components::table_controls::*; -use crate::i18n::{pick_desc, pick_name}; +use crate::i18n::{pick_desc, pick_name, t}; /// Convert a month-number array (1=Jan..12=Dec) to a comma-separated string of abbreviations. fn months_display(months: &Option>) -> String { @@ -48,18 +48,18 @@ fn opt_i32_suffix(val: Option, suffix: &str) -> String { } } -/// Format an Option as Yes / No / em dash. -fn opt_bool(val: Option) -> String { +/// Format an Option as Yes / No / em dash (translated). +fn opt_bool_t(val: Option, lang: &str) -> String { match val { - Some(true) => "Yes".to_string(), - Some(false) => "No".to_string(), + Some(true) => t(lang, "yes").to_string(), + Some(false) => t(lang, "no").to_string(), None => "\u{2014}".to_string(), } } -/// Bool field: always present (non-Option). -fn bool_display(val: bool) -> &'static str { - if val { "Yes" } else { "No" } +/// Bool field: always present (non-Option), translated. +fn bool_display_t(val: bool, lang: &str) -> String { + if val { t(lang, "yes").to_string() } else { t(lang, "no").to_string() } } /// Format an Option> as comma-separated or em dash. @@ -109,6 +109,7 @@ fn cultivar_columns() -> Vec { #[component] pub fn CultivarList() -> Element { + let lang = use_context::().0; let columns = cultivar_columns(); let mut page = use_signal(|| 1i64); let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25)); @@ -146,16 +147,17 @@ pub fn CultivarList() -> Element { }); let current_page = *page.read(); + let l = lang.read().clone(); rsx! { div { class: "page", - h1 { "Cultivars" } + h1 { "{t(&l, \"page.cultivars\")}" } div { class: "table-toolbar", div { class: "search-bar", input { r#type: "text", - placeholder: "Search cultivars...", + placeholder: "{t(&l, \"search.placeholder_cultivars\")}", value: "{search}", oninput: move |e| { search.set(e.value()); @@ -177,7 +179,7 @@ pub fn CultivarList() -> Element { } match &*cultivars.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { let smap = species_map.read(); @@ -188,34 +190,34 @@ pub fn CultivarList() -> Element { }; let vis = visible_cols.read(); rsx! { - p { class: "result-count", "{data.total} cultivars" } + p { class: "result-count", "{data.total} {t(&l, \"nav.cultivars\").to_lowercase()}" } div { class: "table-wrap", table { thead { tr { if is_col_visible(&vis, "name") { - th { "Name" } + th { "{t(&l, \"field.name\")}" } } if is_col_visible(&vis, "species") { - th { "Species" } + th { "{t(&l, \"field.species\")}" } } if is_col_visible(&vis, "organic") { - th { "Organic" } + th { "{t(&l, \"field.organic\")}" } } if is_col_visible(&vis, "perennial") { - th { "Perennial" } + th { "{t(&l, \"field.perennial\")}" } } if is_col_visible(&vis, "description") { - th { "Description" } + th { "{t(&l, \"field.description\")}" } } if is_col_visible(&vis, "frost_tolerance") { - th { "Frost Tol." } + th { "{t(&l, \"field.frost_tol\")}" } } if is_col_visible(&vis, "growing_time") { - th { "Growing Time" } + th { "{t(&l, \"field.growing_time\")}" } } if is_col_visible(&vis, "days_germ") { - th { "Days to Germ." } + th { "{t(&l, \"field.days_germ\")}" } } } } @@ -238,14 +240,14 @@ pub fn CultivarList() -> Element { if is_col_visible(&vis, "organic") { td { if c.is_organic { - span { class: "badge organic", "Yes" } + span { class: "badge organic", "{t(&l, \"yes\")}" } } else { "-" } } } if is_col_visible(&vis, "perennial") { - td { if c.perennial { "Yes" } else { "Annual" } } + td { if c.perennial { "{t(&l, \"yes\")}" } else { "{t(&l, \"annual\")}" } } } if is_col_visible(&vis, "description") { td { class: "cell-truncated", @@ -283,13 +285,13 @@ pub fn CultivarList() -> Element { button { disabled: current_page <= 1, onclick: move |_| page.set(current_page - 1), - "Previous" + "{t(&l, \"btn.previous\")}" } - span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" } + span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" } button { disabled: current_page * data.per_page >= data.total, onclick: move |_| page.set(current_page + 1), - "Next" + "{t(&l, \"btn.next\")}" } } } @@ -359,10 +361,11 @@ pub fn CultivarDetail(slug: String) -> Element { rsx! { div { class: "page cultivar-detail", match &*cultivar.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&lang.read(), \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(c)) => { let current_lang = lang.read().clone(); + let l = &*current_lang; // Pre-compute display strings outside of rsx let common_name = pick_name(¤t_lang, &c.name_de, &c.name_en, &c.name); @@ -370,8 +373,8 @@ pub fn CultivarDetail(slug: String) -> Element { let name_de = opt_str(&c.name_de); let name_sci = opt_str(&c.name_scientific); let desc = pick_desc(¤t_lang, &c.description_de, &c.description_en, &c.description); - let organic = bool_display(c.is_organic); - let perennial = bool_display(c.perennial); + let organic = bool_display_t(c.is_organic, l); + let perennial = bool_display_t(c.perennial, l); // Planting schedule let indoor = months_display(&c.indoor_sowing_months); @@ -393,9 +396,9 @@ pub fn CultivarDetail(slug: String) -> Element { let days_germ = opt_i32_suffix(c.days_to_germination, ""); let germ_temp = opt_f64_suffix(c.germination_temp_c, " \u{00b0}C"); let light_req = opt_str(&c.light_requirement); - let strat_req = opt_bool(c.stratification_required); + let strat_req = opt_bool_t(c.stratification_required, l); let strat_days = opt_i32_suffix(c.stratification_days, ""); - let scar_req = opt_bool(c.scarification_required); + let scar_req = opt_bool_t(c.scarification_required, l); // Climate let min_temp = opt_f64_suffix(c.min_temp, " \u{00b0}C"); @@ -407,8 +410,6 @@ pub fn CultivarDetail(slug: String) -> Element { let opt_light_h = opt_f64_suffix(c.optimal_light_hours_day, " h"); // --- Species fallback values --- - // When a cultivar field is empty, we show species-level - // data styled as an estimation. let (sp_frost_fb, sp_light_fb, sp_drought_fb, sp_usda_fb) = { let sp_read = species_data.read(); match &*sp_read { @@ -433,7 +434,7 @@ pub fn CultivarDetail(slug: String) -> Element { || sp_drought_fb.is_some() || sp_usda_fb.is_some(); - // Species image: prefer primary from images API, else primary_image_key + // Species image let sp_primary_img: Option = { let imgs = species_images.read(); match &*imgs { @@ -460,7 +461,7 @@ pub fn CultivarDetail(slug: String) -> Element { // Week-based planting calendar (full width) div { class: "detail-card", style: "margin-top: 1.25rem;", - div { class: "detail-card-header", "Planting Calendar" } + div { class: "detail-card-header", "{t(l, \"card.planting_calendar\")}" } div { style: "padding: 0.75rem;", PlantingCalendar { indoor_sowing_months: c.indoor_sowing_months.clone(), @@ -483,27 +484,27 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 1: Cultivar Details div { class: "detail-card", - div { class: "detail-card-header", "Cultivar Details" } + div { class: "detail-card-header", "{t(l, \"card.cultivar_details\")}" } table { class: "attr-table", tbody { tr { - th { "Name" } + th { "{t(l, \"field.name\")}" } td { "{c.name}" } } tr { - th { "Common Name" } + th { "{t(l, \"field.common_name\")}" } td { "{common_name}" } } tr { - th { "Name EN" } + th { "{t(l, \"field.name_en\")}" } td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" } } tr { - th { "Name DE" } + th { "{t(l, \"field.name_de\")}" } td { class: if name_de == "\u{2014}" { "placeholder" } else { "" }, "{name_de}" } } tr { - th { "Scientific Name" } + th { "{t(l, \"field.scientific_name\")}" } td { class: if name_sci == "\u{2014}" { "placeholder" } else { "" }, if name_sci != "\u{2014}" { em { "{name_sci}" } @@ -513,7 +514,7 @@ pub fn CultivarDetail(slug: String) -> Element { } } tr { - th { "Species" } + th { "{t(l, \"field.species\")}" } td { if let Some(Some(ref sp)) = *species_data.read() { Link { to: Route::SpeciesDetail { slug: sp.slug.clone() }, @@ -530,15 +531,15 @@ pub fn CultivarDetail(slug: String) -> Element { } } tr { - th { "Description" } + th { "{t(l, \"field.description\")}" } td { class: if desc == "\u{2014}" { "placeholder" } else { "" }, "{desc}" } } tr { - th { "Organic" } + th { "{t(l, \"field.organic\")}" } td { "{organic}" } } tr { - th { "Perennial" } + th { "{t(l, \"field.perennial\")}" } td { "{perennial}" } } } @@ -547,35 +548,35 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 2: Planting Schedule div { class: "detail-card", - div { class: "detail-card-header", "Planting Schedule" } + div { class: "detail-card-header", "{t(l, \"card.planting_schedule\")}" } table { class: "attr-table", tbody { tr { - th { "Indoor Sowing" } + th { "{t(l, \"cal.indoor_sowing\")}" } td { class: if indoor == "\u{2014}" { "placeholder" } else { "" }, "{indoor}" } } tr { - th { "Direct Sowing" } + th { "{t(l, \"cal.direct_sowing\")}" } td { class: if direct == "\u{2014}" { "placeholder" } else { "" }, "{direct}" } } tr { - th { "Transplanting" } + th { "{t(l, \"cal.transplanting\")}" } td { class: if transplant == "\u{2014}" { "placeholder" } else { "" }, "{transplant}" } } tr { - th { "Glasshouse" } + th { "{t(l, \"cal.glasshouse\")}" } td { class: if glasshouse == "\u{2014}" { "placeholder" } else { "" }, "{glasshouse}" } } tr { - th { "Harvesting" } + th { "{t(l, \"cal.harvesting\")}" } td { class: if harvest == "\u{2014}" { "placeholder" } else { "" }, "{harvest}" } } tr { - th { "Succession Planting" } + th { "{t(l, \"field.succession_planting\")}" } td { class: if succession == "\u{2014}" { "placeholder" } else { "" }, "{succession}" } } tr { - th { "Planting Notes" } + th { "{t(l, \"field.planting_notes\")}" } td { class: if planting_notes == "\u{2014}" { "placeholder" } else { "" }, "{planting_notes}" } } } @@ -584,7 +585,7 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 3: Where to Buy div { class: "detail-card", - div { class: "detail-card-header", "Where to Buy" } + div { class: "detail-card-header", "{t(l, \"card.where_to_buy\")}" } { let sup_links = suppliers_data.read(); let sups = all_suppliers.read(); @@ -598,10 +599,10 @@ pub fn CultivarDetail(slug: String) -> Element { table { class: "attr-table", thead { tr { - th { "Supplier" } - th { "SKU" } - th { "Price" } - th { "Link" } + th { "{t(l, \"field.supplier\")}" } + th { "{t(l, \"field.sku\")}" } + th { "{t(l, \"field.price\")}" } + th { "{t(l, \"field.link\")}" } } } tbody { @@ -625,7 +626,7 @@ pub fn CultivarDetail(slug: String) -> Element { td { if let Some(ref u) = url { if !u.is_empty() { - a { href: "{u}", target: "_blank", class: "external-link", "View" } + a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"btn.view\")}" } } else { span { class: "placeholder", "\u{2014}" } } @@ -642,7 +643,7 @@ pub fn CultivarDetail(slug: String) -> Element { } }, _ => rsx! { - p { class: "placeholder detail-card-empty", "\u{2014} No suppliers linked" } + p { class: "placeholder detail-card-empty", "\u{2014} {t(l, \"no_suppliers_linked\")}" } }, } } @@ -655,7 +656,7 @@ pub fn CultivarDetail(slug: String) -> Element { // Image card if let Some(ref key) = sp_img_key { div { class: "detail-card", - div { class: "detail-card-header", "Image" } + div { class: "detail-card-header", "{t(l, \"card.image\")}" } div { class: "image-card-body", img { class: "species-image", src: "/img/{key}", alt: "{c.name}" } if let Some(ref img) = sp_primary_img { @@ -673,7 +674,7 @@ pub fn CultivarDetail(slug: String) -> Element { if img.caption.is_some() || img.license.is_some() { " | " } - a { href: "{url}", target: "_blank", class: "attribution-link", "Source" } + a { href: "{url}", target: "_blank", class: "attribution-link", "{t(l, \"field.source\")}" } } } } @@ -683,27 +684,27 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 4: Growing Information div { class: "detail-card", - div { class: "detail-card-header", "Growing Information" } + div { class: "detail-card-header", "{t(l, \"card.growing_info\")}" } table { class: "attr-table", tbody { tr { - th { "Growing Time" } + th { "{t(l, \"field.growing_time\")}" } td { class: if growing_time == "\u{2014}" { "placeholder" } else { "" }, "{growing_time}" } } tr { - th { "Planting Depth" } + th { "{t(l, \"field.planting_depth\")}" } td { class: if planting_depth == "\u{2014}" { "placeholder" } else { "" }, "{planting_depth}" } } tr { - th { "Row Spacing" } + th { "{t(l, \"field.row_spacing\")}" } td { class: if row_spacing == "\u{2014}" { "placeholder" } else { "" }, "{row_spacing}" } } tr { - th { "Plant Spacing" } + th { "{t(l, \"field.plant_spacing\")}" } td { class: if plant_spacing == "\u{2014}" { "placeholder" } else { "" }, "{plant_spacing}" } } tr { - th { "Propagation Methods" } + th { "{t(l, \"field.propagation\")}" } td { class: if propagation == "\u{2014}" { "placeholder" } else { "" }, "{propagation}" } } } @@ -712,19 +713,19 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 5: Germination div { class: "detail-card", - div { class: "detail-card-header", "Germination" } + div { class: "detail-card-header", "{t(l, \"card.germination\")}" } table { class: "attr-table", tbody { tr { - th { "Days to Germination" } + th { "{t(l, \"field.days_to_germ\")}" } td { class: if days_germ == "\u{2014}" { "placeholder" } else { "" }, "{days_germ}" } } tr { - th { "Germination Temp" } + th { "{t(l, \"field.germ_temp\")}" } td { class: if germ_temp == "\u{2014}" { "placeholder" } else { "" }, "{germ_temp}" } } tr { - th { "Light Requirement" } + th { "{t(l, \"field.light_req\")}" } if light_req != "\u{2014}" { td { "{light_req}" } } else if let Some(ref fb) = sp_light_fb { @@ -737,15 +738,15 @@ pub fn CultivarDetail(slug: String) -> Element { } } tr { - th { "Stratification Required" } + th { "{t(l, \"field.stratification\")}" } td { class: if strat_req == "\u{2014}" { "placeholder" } else { "" }, "{strat_req}" } } tr { - th { "Stratification Days" } + th { "{t(l, \"field.stratification_days\")}" } td { class: if strat_days == "\u{2014}" { "placeholder" } else { "" }, "{strat_days}" } } tr { - th { "Scarification Required" } + th { "{t(l, \"field.scarification\")}" } td { class: if scar_req == "\u{2014}" { "placeholder" } else { "" }, "{scar_req}" } } } @@ -754,23 +755,23 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 6: Climate & Environment div { class: "detail-card", - div { class: "detail-card-header", "Climate & Environment" } + div { class: "detail-card-header", "{t(l, \"card.climate\")}" } table { class: "attr-table", tbody { tr { - th { "Min Temp" } + th { "{t(l, \"field.min_temp\")}" } td { class: if min_temp == "\u{2014}" { "placeholder" } else { "" }, "{min_temp}" } } tr { - th { "Max Temp" } + th { "{t(l, \"field.max_temp\")}" } td { class: if max_temp == "\u{2014}" { "placeholder" } else { "" }, "{max_temp}" } } tr { - th { "Humidity" } + th { "{t(l, \"field.humidity\")}" } td { class: if humidity == "\u{2014}" { "placeholder" } else { "" }, "{humidity}" } } tr { - th { "Light" } + th { "{t(l, \"field.light\")}" } if light != "\u{2014}" { td { "{light}" } } else if let Some(ref fb) = sp_light_fb { @@ -783,7 +784,7 @@ pub fn CultivarDetail(slug: String) -> Element { } } tr { - th { "Frost Tolerance" } + th { "{t(l, \"field.frost_tolerance\")}" } if frost_tol != "\u{2014}" { td { "{frost_tol}" } } else if let Some(ref fb) = sp_frost_fb { @@ -796,16 +797,16 @@ pub fn CultivarDetail(slug: String) -> Element { } } tr { - th { "Min Light Hours/Day" } + th { "{t(l, \"field.min_light_hours\")}" } td { class: if min_light_h == "\u{2014}" { "placeholder" } else { "" }, "{min_light_h}" } } tr { - th { "Optimal Light Hours/Day" } + th { "{t(l, \"field.opt_light_hours\")}" } td { class: if opt_light_h == "\u{2014}" { "placeholder" } else { "" }, "{opt_light_h}" } } if let Some(ref fb) = sp_drought_fb { tr { - th { "Drought Tolerance" } + th { "{t(l, \"field.drought_tolerance\")}" } td { span { class: "estimated-value", "{fb}" } span { class: "estimated-tag", "~ species" } @@ -814,7 +815,7 @@ pub fn CultivarDetail(slug: String) -> Element { } if let Some(ref fb) = sp_usda_fb { tr { - th { "USDA Hardiness Zone" } + th { "{t(l, \"field.usda_hardiness_zone\")}" } td { span { class: "estimated-value", "{fb}" } span { class: "estimated-tag", "~ species" } @@ -827,7 +828,7 @@ pub fn CultivarDetail(slug: String) -> Element { // Card 7: Species Information div { class: "detail-card", - div { class: "detail-card-header", "Species Information" } + div { class: "detail-card-header", "{t(l, \"card.species_info\")}" } { let sp_read = species_data.read(); match &*sp_read { @@ -841,8 +842,8 @@ pub fn CultivarDetail(slug: String) -> Element { let sp_layer = opt_str(&sp.plant_layer); let sp_drought = opt_str(&sp.drought_tolerance); let sp_usda = opt_str(&sp.hardiness_zone_usda); - let sp_nfix = opt_bool(sp.nitrogen_fixer); - let sp_dynacc = opt_bool(sp.dynamic_accumulator); + let sp_nfix = opt_bool_t(sp.nitrogen_fixer, l); + let sp_dynacc = opt_bool_t(sp.dynamic_accumulator, l); let sp_food = pick_desc(¤t_lang, &sp.food_uses_de, &sp.food_uses_en, &sp.food_uses); let sp_med = pick_desc(¤t_lang, &sp.medicinal_uses_de, &sp.medicinal_uses_en, &sp.medicinal_uses); let sp_other = pick_desc(¤t_lang, &sp.other_uses_de, &sp.other_uses_en, &sp.other_uses); @@ -852,47 +853,47 @@ pub fn CultivarDetail(slug: String) -> Element { table { class: "attr-table", tbody { tr { - th { "Plant Layer" } + th { "{t(l, \"field.plant_layer\")}" } td { class: if sp_layer == "\u{2014}" { "placeholder" } else { "" }, "{sp_layer}" } } tr { - th { "Drought Tolerance" } + th { "{t(l, \"field.drought_tolerance\")}" } td { class: if sp_drought == "\u{2014}" { "placeholder" } else { "" }, "{sp_drought}" } } tr { - th { "USDA Zone" } + th { "{t(l, \"field.usda_zone\")}" } td { class: if sp_usda == "\u{2014}" { "placeholder" } else { "" }, "{sp_usda}" } } tr { - th { "pH Range" } + th { "{t(l, \"field.ph_range\")}" } td { class: if ph_range == "\u{2014}" { "placeholder" } else { "" }, "{ph_range}" } } tr { - th { "Nitrogen Fixer" } + th { "{t(l, \"field.nitrogen_fixer\")}" } td { class: if sp_nfix == "\u{2014}" { "placeholder" } else { "" }, "{sp_nfix}" } } tr { - th { "Dynamic Accumulator" } + th { "{t(l, \"field.dynamic_acc\")}" } td { class: if sp_dynacc == "\u{2014}" { "placeholder" } else { "" }, "{sp_dynacc}" } } tr { - th { "Food Uses" } + th { "{t(l, \"field.food_uses\")}" } td { class: if sp_food == "\u{2014}" { "placeholder" } else { "" }, "{sp_food}" } } tr { - th { "Medicinal Uses" } + th { "{t(l, \"field.medicinal_uses\")}" } td { class: if sp_med == "\u{2014}" { "placeholder" } else { "" }, "{sp_med}" } } tr { - th { "Other Uses" } + th { "{t(l, \"field.other_uses\")}" } td { class: if sp_other == "\u{2014}" { "placeholder" } else { "" }, "{sp_other}" } } tr { - th { "Wildlife Value" } + th { "{t(l, \"field.wildlife_value\")}" } td { class: if sp_wildlife == "\u{2014}" { "placeholder" } else { "" }, "{sp_wildlife}" } } tr { - th { "Native Range" } + th { "{t(l, \"field.native_range\")}" } td { class: if sp_native == "\u{2014}" { "placeholder" } else { "" }, "{sp_native}" } } } @@ -900,7 +901,7 @@ pub fn CultivarDetail(slug: String) -> Element { } }, _ => rsx! { - p { class: "placeholder detail-card-empty", "Loading species data\u{2026}" } + p { class: "placeholder detail-card-empty", "{t(l, \"loading_species_data\")}" } }, } } @@ -910,9 +911,7 @@ pub fn CultivarDetail(slug: String) -> Element { if has_any_fallback { p { class: "species-fallback-note", - "Values marked " - span { class: "estimated-tag", "~ species" } - " are estimated from species-level data." + "{t(l, \"estimated_note\")}" } } } diff --git a/herbapi-ui/src/pages/families.rs b/herbapi-ui/src/pages/families.rs index f22bb83..12be4ba 100644 --- a/herbapi-ui/src/pages/families.rs +++ b/herbapi-ui/src/pages/families.rs @@ -3,7 +3,7 @@ use dioxus::prelude::*; use crate::api; use crate::app::{Lang, Route}; use crate::components::table_controls::*; -use crate::i18n::pick_name; +use crate::i18n::{pick_name, t}; const STORAGE_KEY_COLS: &str = "herbapi_families_cols"; const STORAGE_KEY_PP: &str = "herbapi_families_pp"; @@ -38,16 +38,17 @@ pub fn FamilyList() -> Element { }); let current_page = *page.read(); + let l = lang.read().clone(); rsx! { div { class: "page", - h1 { "Plant Families" } + h1 { "{t(&l, \"page.families\")}" } div { class: "table-toolbar", div { class: "search-bar", input { r#type: "text", - placeholder: "Search families...", + placeholder: "{t(&l, \"search.placeholder_families\")}", value: "{search}", oninput: move |e| { search.set(e.value()); @@ -69,30 +70,30 @@ pub fn FamilyList() -> Element { } match &*families.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { let vis = visible_cols.read(); rsx! { - p { class: "result-count", "{data.total} families" } + p { class: "result-count", "{data.total} {t(&l, \"nav.families\").to_lowercase()}" } div { class: "table-wrap", table { thead { tr { if is_col_visible(&vis, "name_scientific") { - th { "Scientific Name" } + th { "{t(&l, \"field.scientific_name\")}" } } if is_col_visible(&vis, "common_name") { - th { "Common Name" } + th { "{t(&l, \"field.common_name\")}" } } if is_col_visible(&vis, "name_en") { - th { "English" } + th { "{t(&l, \"field.name_en\")}" } } if is_col_visible(&vis, "name_de") { - th { "German" } + th { "{t(&l, \"field.name_de\")}" } } if is_col_visible(&vis, "description") { - th { "Description" } + th { "{t(&l, \"field.description\")}" } } } } @@ -135,13 +136,13 @@ pub fn FamilyList() -> Element { button { disabled: current_page <= 1, onclick: move |_| page.set(current_page - 1), - "Previous" + "{t(&l, \"btn.previous\")}" } - span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" } + span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" } button { disabled: current_page * data.per_page >= data.total, onclick: move |_| page.set(current_page + 1), - "Next" + "{t(&l, \"btn.next\")}" } } } @@ -167,10 +168,12 @@ pub fn FamilyDetail(slug: String) -> Element { async move { api::list_species(1, 100, Some(&s), None).await } }); + let l = lang.read().clone(); + rsx! { div { class: "page", match &*family.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(f)) => { let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific); @@ -186,9 +189,9 @@ pub fn FamilyDetail(slug: String) -> Element { }, } - h2 { "Species in this Family" } + h2 { "{t(&l, \"page.species_in_family\")}" } match &*species.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => rsx! { div { class: "card-grid", diff --git a/herbapi-ui/src/pages/home.rs b/herbapi-ui/src/pages/home.rs index 6a17d68..eb55d39 100644 --- a/herbapi-ui/src/pages/home.rs +++ b/herbapi-ui/src/pages/home.rs @@ -3,23 +3,24 @@ use dioxus::prelude::*; use crate::api; use crate::app::{Lang, Route}; use crate::components::plant_card::PlantCard; -use crate::i18n::pick_name; +use crate::i18n::{pick_name, t}; #[component] pub fn Home() -> Element { let lang = use_context::().0; let mut search_query = use_signal(|| String::new()); let species = use_resource(|| async { api::list_species(1, 12, None, None).await }); + let l = lang.read().clone(); rsx! { div { class: "page home-page", h1 { "HerbAPI" } - p { class: "subtitle", "Trilingual plant reference database" } + p { class: "subtitle", "{t(&l, \"subtitle\")}" } div { class: "search-bar", input { r#type: "text", - placeholder: "Search plants...", + placeholder: "{t(&l, \"search.placeholder\")}", value: "{search_query}", oninput: move |e| search_query.set(e.value()), onkeydown: move |e| { @@ -31,9 +32,9 @@ pub fn Home() -> Element { } } - h2 { "Recent Species" } + h2 { "{t(&l, \"page.recent_species\")}" } match &*species.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => rsx! { div { class: "card-grid", diff --git a/herbapi-ui/src/pages/search.rs b/herbapi-ui/src/pages/search.rs index c9829e7..94791e7 100644 --- a/herbapi-ui/src/pages/search.rs +++ b/herbapi-ui/src/pages/search.rs @@ -1,10 +1,12 @@ use dioxus::prelude::*; use crate::api; -use crate::app::Route; +use crate::app::{Lang, Route}; +use crate::i18n::t; #[component] pub fn SearchPage() -> Element { + let lang = use_context::().0; let mut query = use_signal(|| String::new()); let mut results = use_signal(|| None::, String>>); @@ -18,14 +20,16 @@ pub fn SearchPage() -> Element { } }; + let l = lang.read().clone(); + rsx! { div { class: "page search-page", - h1 { "Search" } + h1 { "{t(&l, \"page.search\")}" } div { class: "search-bar", input { r#type: "text", - placeholder: "Search plants, families, cultivars...", + placeholder: "{t(&l, \"search.placeholder_full\")}", value: "{query}", oninput: move |e| query.set(e.value()), onkeydown: move |e| { @@ -34,14 +38,14 @@ pub fn SearchPage() -> Element { } }, } - button { onclick: move |_| trigger_search(), "Search" } + button { onclick: move |_| trigger_search(), "{t(&l, \"btn.search\")}" } } match &*results.read() { None => rsx! {}, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => rsx! { - p { class: "result-count", "{data.len()} results" } + p { class: "result-count", "{data.len()} {t(&l, \"results\")}" } div { class: "search-results", for r in data.iter() { div { class: "search-result", diff --git a/herbapi-ui/src/pages/sources.rs b/herbapi-ui/src/pages/sources.rs index 232fe88..02e83d2 100644 --- a/herbapi-ui/src/pages/sources.rs +++ b/herbapi-ui/src/pages/sources.rs @@ -1,5 +1,8 @@ use dioxus::prelude::*; +use crate::app::Lang; +use crate::i18n::t; + struct DataSource { name: &'static str, url: &'static str, @@ -83,11 +86,14 @@ const SOURCES: &[DataSource] = &[ #[component] pub fn Sources() -> Element { + let lang = use_context::().0; + let l = lang.read().clone(); + rsx! { div { class: "page sources-page", - h1 { "Data Sources" } + h1 { "{t(&l, \"page.sources\")}" } p { class: "sources-intro", - "HerbAPI aggregates plant data from multiple open sources. We are grateful to these projects for making botanical knowledge freely available." + "{t(&l, \"sources.intro\")}" } div { class: "sources-grid", for source in SOURCES.iter() { @@ -105,11 +111,11 @@ pub fn Sources() -> Element { p { class: "source-description", "{source.description}" } div { class: "source-details", div { class: "source-detail", - span { class: "source-detail-label", "Data used" } + span { class: "source-detail-label", "{t(&l, \"field.data_used\")}" } span { class: "source-detail-value", "{source.data_used}" } } div { class: "source-detail", - span { class: "source-detail-label", "License" } + span { class: "source-detail-label", "{t(&l, \"field.license\")}" } span { class: "source-detail-value source-license", "{source.license}" } } } diff --git a/herbapi-ui/src/pages/species.rs b/herbapi-ui/src/pages/species.rs index 707c873..13291a3 100644 --- a/herbapi-ui/src/pages/species.rs +++ b/herbapi-ui/src/pages/species.rs @@ -7,7 +7,7 @@ use crate::api; use crate::app::{Lang, Route}; use crate::components::plant_card::PlantCard; use crate::components::table_controls::*; -use crate::i18n::{pick_desc, pick_name}; +use crate::i18n::{pick_desc, pick_name, t}; const STORAGE_KEY_COLS: &str = "herbapi_species_cols"; const STORAGE_KEY_PP: &str = "herbapi_species_pp"; @@ -93,16 +93,17 @@ pub fn SpeciesList() -> Element { let current_page = *page.read(); let is_table = *table_view.read(); + let l = lang.read().clone(); rsx! { div { class: "page", - h1 { "Species" } + h1 { "{t(&l, \"page.species\")}" } div { class: "table-toolbar", div { class: "search-bar", input { r#type: "text", - placeholder: "Search species...", + placeholder: "{t(&l, \"search.placeholder_species\")}", value: "{search}", oninput: move |e| { search.set(e.value()); @@ -118,7 +119,7 @@ pub fn SpeciesList() -> Element { table_view.set(new_val); let _ = LocalStorage::set(STORAGE_KEY_VIEW, new_val); }, - if is_table { "Card View" } else { "Table View" } + if is_table { "{t(&l, \"btn.card_view\")}" } else { "{t(&l, \"btn.table_view\")}" } } PerPageSelector { per_page: per_page, @@ -131,14 +132,14 @@ pub fn SpeciesList() -> Element { // Filter bar div { class: "filter-bar", div { class: "filter-group", - label { "Layer" } + label { "{t(&l, \"filter.layer\")}" } select { value: "{filter_layer}", onchange: move |e| { filter_layer.set(e.value()); page.set(1); }, - option { value: "", "All" } + option { value: "", "{t(&l, \"filter.all\")}" } option { value: "canopy", "Canopy" } option { value: "understory", "Understory" } option { value: "shrub", "Shrub" } @@ -149,14 +150,14 @@ pub fn SpeciesList() -> Element { } } div { class: "filter-group", - label { "Drought Tol." } + label { "{t(&l, \"filter.drought_tol\")}" } select { value: "{filter_drought}", onchange: move |e| { filter_drought.set(e.value()); page.set(1); }, - option { value: "", "All" } + option { value: "", "{t(&l, \"filter.all\")}" } option { value: "none", "None" } option { value: "low", "Low" } option { value: "moderate", "Moderate" } @@ -174,7 +175,7 @@ pub fn SpeciesList() -> Element { page.set(1); }, } - "N-Fixer" + "{t(&l, \"field.n_fixer\")}" } } div { class: "filter-group filter-checkbox", @@ -187,7 +188,7 @@ pub fn SpeciesList() -> Element { page.set(1); }, } - "Dyn. Accumulator" + "{t(&l, \"field.dyn_accum\")}" } } } @@ -201,7 +202,7 @@ pub fn SpeciesList() -> Element { } match &*species.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { let fmap_read = family_map.read(); @@ -212,7 +213,7 @@ pub fn SpeciesList() -> Element { }; rsx! { - p { class: "result-count", "{data.total} species" } + p { class: "result-count", "{data.total} {t(&l, \"nav.species\").to_lowercase()}" } if is_table { { @@ -223,49 +224,49 @@ pub fn SpeciesList() -> Element { thead { tr { if is_col_visible(&vis, "name_scientific") { - th { "Scientific Name" } + th { "{t(&l, \"field.scientific_name\")}" } } if is_col_visible(&vis, "common_name") { - th { "Common Name" } + th { "{t(&l, \"field.common_name\")}" } } if is_col_visible(&vis, "name_de") { - th { "German" } + th { "{t(&l, \"field.name_de\")}" } } if is_col_visible(&vis, "name_en") { - th { "English" } + th { "{t(&l, \"field.name_en\")}" } } if is_col_visible(&vis, "family") { - th { "Family" } + th { "{t(&l, \"field.family\")}" } } if is_col_visible(&vis, "plant_layer") { - th { "Layer" } + th { "{t(&l, \"field.layer\")}" } } if is_col_visible(&vis, "nitrogen_fixer") { - th { "N-Fixer" } + th { "{t(&l, \"field.n_fixer\")}" } } if is_col_visible(&vis, "dynamic_accumulator") { - th { "Dyn. Accum." } + th { "{t(&l, \"field.dyn_accum\")}" } } if is_col_visible(&vis, "food_uses") { - th { "Food Uses" } + th { "{t(&l, \"field.food_uses\")}" } } if is_col_visible(&vis, "edibility_rating") { - th { "Edibility" } + th { "{t(&l, \"field.edibility\")}" } } if is_col_visible(&vis, "drought_tolerance") { - th { "Drought Tol." } + th { "{t(&l, \"field.drought_tol\")}" } } if is_col_visible(&vis, "hardiness_zone_usda") { - th { "USDA Zone" } + th { "{t(&l, \"field.usda_zone\")}" } } if is_col_visible(&vis, "nectar_value") { - th { "Nectar" } + th { "{t(&l, \"field.nectar\")}" } } if is_col_visible(&vis, "wild_bee_count") { - th { "Wild Bees" } + th { "{t(&l, \"field.wild_bees\")}" } } if is_col_visible(&vis, "native_status") { - th { "Native Status" } + th { "{t(&l, \"field.native_status\")}" } } } } @@ -301,7 +302,7 @@ pub fn SpeciesList() -> Element { if is_col_visible(&vis, "nitrogen_fixer") { td { match s.nitrogen_fixer { - Some(true) => rsx! { span { class: "badge organic", "Yes" } }, + Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } }, Some(false) => rsx! { "-" }, None => rsx! { "-" }, } @@ -310,7 +311,7 @@ pub fn SpeciesList() -> Element { if is_col_visible(&vis, "dynamic_accumulator") { td { match s.dynamic_accumulator { - Some(true) => rsx! { span { class: "badge organic", "Yes" } }, + Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } }, Some(false) => rsx! { "-" }, None => rsx! { "-" }, } @@ -401,13 +402,13 @@ pub fn SpeciesList() -> Element { button { disabled: current_page <= 1, onclick: move |_| page.set(current_page - 1), - "Previous" + "{t(&l, \"btn.previous\")}" } - span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" } + span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" } button { disabled: current_page * data.per_page >= data.total, onclick: move |_| page.set(current_page + 1), - "Next" + "{t(&l, \"btn.next\")}" } } } @@ -461,11 +462,12 @@ pub fn SpeciesDetail(slug: String) -> Element { rsx! { div { class: "page species-detail", match &*species.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&lang.read(), \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(s)) => { let species_slug = s.slug.clone(); let current_lang = lang.read().clone(); + let l = &*current_lang; // Helper closures to format fields let os = |v: &Option| -> String { @@ -476,8 +478,8 @@ pub fn SpeciesDetail(slug: String) -> Element { }; let ob = |v: Option| -> String { match v { - Some(true) => "Yes".to_string(), - Some(false) => "No".to_string(), + Some(true) => t(l, "yes").to_string(), + Some(false) => t(l, "no").to_string(), None => "\u{2014}".to_string(), } }; @@ -565,27 +567,27 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 1: Species Details div { class: "detail-card", - div { class: "detail-card-header", "Species Details" } + div { class: "detail-card-header", "{t(l, \"card.species_details\")}" } table { class: "attr-table", tbody { tr { - th { "Scientific Name" } + th { "{t(l, \"field.scientific_name\")}" } td { em { "{s.name_scientific}" } } } tr { - th { "Common Name" } + th { "{t(l, \"field.common_name\")}" } td { "{common_name}" } } tr { - th { "Name EN" } + th { "{t(l, \"field.name_en\")}" } td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" } } tr { - th { "Name DE" } + th { "{t(l, \"field.name_de\")}" } td { class: if name_de == em { "placeholder" } else { "" }, "{name_de}" } } tr { - th { "Family" } + th { "{t(l, \"field.family\")}" } td { if let Some(Some(ref fam)) = *family_data.read() { Link { to: Route::FamilyDetail { slug: fam.slug.clone() }, @@ -597,7 +599,7 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "Description" } + th { "{t(l, \"field.description\")}" } td { class: if desc == em { "placeholder" } else { "" }, "{desc}" } } } @@ -606,23 +608,23 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 2: Uses div { class: "detail-card", - div { class: "detail-card-header", "Uses" } + div { class: "detail-card-header", "{t(l, \"card.uses\")}" } table { class: "attr-table", tbody { tr { - th { "Food Uses" } + th { "{t(l, \"field.food_uses\")}" } td { class: if food == em { "placeholder" } else { "" }, "{food}" } } tr { - th { "Medicinal Uses" } + th { "{t(l, \"field.medicinal_uses\")}" } td { class: if med == em { "placeholder" } else { "" }, "{med}" } } tr { - th { "Other Uses" } + th { "{t(l, \"field.other_uses\")}" } td { class: if other == em { "placeholder" } else { "" }, "{other}" } } tr { - th { "Edibility Rating" } + th { "{t(l, \"field.edibility\")}" } td { class: if edibility == em { "placeholder" } else { "" }, "{edibility}" } } } @@ -631,27 +633,27 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 3: Ecology div { class: "detail-card", - div { class: "detail-card-header", "Ecology" } + div { class: "detail-card-header", "{t(l, \"card.ecology\")}" } table { class: "attr-table", tbody { tr { - th { "Plant Layer" } + th { "{t(l, \"field.plant_layer\")}" } td { class: if layer == em { "placeholder" } else { "" }, "{layer}" } } tr { - th { "Succession Stage" } + th { "{t(l, \"field.succession\")}" } td { class: if succession == em { "placeholder" } else { "" }, "{succession}" } } tr { - th { "Wildlife Value" } + th { "{t(l, \"field.wildlife_value\")}" } td { class: if wildlife == em { "placeholder" } else { "" }, "{wildlife}" } } tr { - th { "Native Range" } + th { "{t(l, \"field.native_range\")}" } td { class: if native == em { "placeholder" } else { "" }, "{native}" } } tr { - th { "Pollination Type" } + th { "{t(l, \"field.pollination\")}" } td { class: if pollination == em { "placeholder" } else { "" }, "{pollination}" } } } @@ -665,7 +667,7 @@ pub fn SpeciesDetail(slug: String) -> Element { // Image card if let Some(ref key) = img_key { div { class: "detail-card", - div { class: "detail-card-header", "Image" } + div { class: "detail-card-header", "{t(l, \"card.image\")}" } div { class: "image-card-body", img { class: "species-image", src: "/img/{key}", alt: "{s.name_scientific}" } if let Some(ref img) = primary_img { @@ -683,7 +685,7 @@ pub fn SpeciesDetail(slug: String) -> Element { if img.caption.is_some() || img.license.is_some() { " | " } - a { href: "{url}", target: "_blank", class: "attribution-link", "Source" } + a { href: "{url}", target: "_blank", class: "attribution-link", "{t(l, \"field.source\")}" } } } } @@ -693,31 +695,31 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 4: Growing Requirements div { class: "detail-card", - div { class: "detail-card-header", "Growing Requirements" } + div { class: "detail-card-header", "{t(l, \"card.growing_requirements\")}" } table { class: "attr-table", tbody { tr { - th { "Soil Moisture" } + th { "{t(l, \"field.soil_moisture\")}" } td { class: if soil_moist == em { "placeholder" } else { "" }, "{soil_moist}" } } tr { - th { "pH Range" } + th { "{t(l, \"field.ph_range\")}" } td { class: if ph_range == em { "placeholder" } else { "" }, "{ph_range}" } } tr { - th { "Drought Tolerance" } + th { "{t(l, \"field.drought_tolerance\")}" } td { class: if drought == em { "placeholder" } else { "" }, "{drought}" } } tr { - th { "Salt Tolerance" } + th { "{t(l, \"field.salt_tolerance\")}" } td { class: if salt == em { "placeholder" } else { "" }, "{salt}" } } tr { - th { "USDA Zone" } + th { "{t(l, \"field.usda_zone\")}" } td { class: if usda == em { "placeholder" } else { "" }, "{usda}" } } tr { - th { "AT Zone" } + th { "{t(l, \"field.at_zone\")}" } td { class: if at_zone == em { "placeholder" } else { "" }, "{at_zone}" } } } @@ -726,39 +728,39 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 5: Permaculture div { class: "detail-card", - div { class: "detail-card-header", "Permaculture" } + div { class: "detail-card-header", "{t(l, \"card.permaculture\")}" } table { class: "attr-table", tbody { tr { - th { "Nitrogen Fixer" } + th { "{t(l, \"field.nitrogen_fixer\")}" } td { class: if n_fixer == em { "placeholder" } else { "" }, "{n_fixer}" } } tr { - th { "Dynamic Accumulator" } + th { "{t(l, \"field.dynamic_acc\")}" } td { class: if dyn_acc == em { "placeholder" } else { "" }, "{dyn_acc}" } } tr { - th { "Attracts Pollinators" } + th { "{t(l, \"field.attracts_pollinators\")}" } td { class: if pollinators == em { "placeholder" } else { "" }, "{pollinators}" } } tr { - th { "Attracts Beneficial Insects" } + th { "{t(l, \"field.attracts_beneficial\")}" } td { class: if beneficial == em { "placeholder" } else { "" }, "{beneficial}" } } tr { - th { "Mulch Plant" } + th { "{t(l, \"field.mulch_plant\")}" } td { class: if mulch == em { "placeholder" } else { "" }, "{mulch}" } } tr { - th { "Ground Cover Quality" } + th { "{t(l, \"field.ground_cover_quality\")}" } td { class: if gc_quality == em { "placeholder" } else { "" }, "{gc_quality}" } } tr { - th { "Allelopathic" } + th { "{t(l, \"field.allelopathic\")}" } td { class: if allelo == em { "placeholder" } else { "" }, "{allelo}" } } tr { - th { "Guild Role" } + th { "{t(l, \"field.guild_role\")}" } td { class: if guild == em { "placeholder" } else { "" }, "{guild}" } } } @@ -767,11 +769,11 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 6: Wildlife & Ecology div { class: "detail-card", - div { class: "detail-card-header", "Wildlife & Ecology" } + div { class: "detail-card-header", "{t(l, \"card.wildlife\")}" } table { class: "attr-table", tbody { tr { - th { "Nectar Value" } + th { "{t(l, \"field.nectar\")}" } td { match s.nectar_value { Some(v) => rsx! { @@ -788,7 +790,7 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "Pollen Value" } + th { "{t(l, \"field.pollen\")}" } td { match s.pollen_value { Some(v) => rsx! { @@ -805,69 +807,69 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "Wild Bees" } + th { "{t(l, \"field.wild_bees\")}" } td { match (s.wild_bee_count, s.wild_bee_specialist_count) { - (Some(total), Some(spec)) => rsx! { "{total} species ({spec} specialists)" }, - (Some(total), None) => rsx! { "{total} species" }, + (Some(total), Some(spec)) => rsx! { "{total} {t(l, \"species_species\")} ({spec} specialists)" }, + (Some(total), None) => rsx! { "{total} {t(l, \"species_species\")}" }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { - th { "Butterflies & Moths" } + th { "{t(l, \"field.butterflies\")}" } td { match (s.butterfly_moth_count, s.caterpillar_host_count) { - (Some(bm), Some(ch)) => rsx! { "{bm} species ({ch} caterpillar hosts)" }, - (Some(bm), None) => rsx! { "{bm} species" }, + (Some(bm), Some(ch)) => rsx! { "{bm} {t(l, \"species_species\")} ({ch} caterpillar hosts)" }, + (Some(bm), None) => rsx! { "{bm} {t(l, \"species_species\")}" }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } if s.caterpillar_specialist_count.is_some() { tr { - th { "Caterpillar Specialists" } + th { "{t(l, \"field.caterpillar_specialists\")}" } td { "{s.caterpillar_specialist_count.unwrap()}" } } } tr { - th { "Hoverflies" } + th { "{t(l, \"field.hoverflies\")}" } td { match s.hoverfly_count { - Some(v) => rsx! { "{v} species" }, + Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { - th { "Beetles" } + th { "{t(l, \"field.beetles\")}" } td { match s.beetle_count { - Some(v) => rsx! { "{v} species" }, + Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { - th { "Birds" } + th { "{t(l, \"field.birds\")}" } td { match s.bird_count { - Some(v) => rsx! { "{v} species" }, + Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { - th { "Mammals" } + th { "{t(l, \"field.mammals\")}" } td { match s.mammal_count { - Some(v) => rsx! { "{v} species" }, + Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { - th { "Native Status" } + th { "{t(l, \"field.native_status\")}" } td { match &s.native_status { Some(ns) if !ns.is_empty() => { @@ -884,7 +886,7 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "NaturaDB Tags" } + th { "{t(l, \"field.naturadb_tags\")}" } td { match &s.naturadb_tags { Some(tags) if !tags.is_empty() => { @@ -907,11 +909,11 @@ pub fn SpeciesDetail(slug: String) -> Element { // Card 7: External Links div { class: "detail-card", - div { class: "detail-card-header", "External Links" } + div { class: "detail-card-header", "{t(l, \"card.external_links\")}" } table { class: "attr-table", tbody { tr { - th { "Wikidata QID" } + th { "{t(l, \"field.wikidata_qid\")}" } td { if let Some(ref q) = qid { if !q.is_empty() { @@ -925,7 +927,7 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "GBIF ID" } + th { "{t(l, \"field.gbif_id\")}" } td { if let Some(ref g) = gbif { if !g.is_empty() { @@ -939,15 +941,15 @@ pub fn SpeciesDetail(slug: String) -> Element { } } tr { - th { "EPPO Code" } + th { "{t(l, \"field.eppo_code\")}" } td { class: if eppo == em { "placeholder" } else { "" }, "{eppo}" } } tr { - th { "PFAF URL" } + th { "{t(l, \"field.pfaf_url\")}" } td { if let Some(ref u) = pfaf { if !u.is_empty() { - a { href: "{u}", target: "_blank", class: "external-link", "View on PFAF" } + a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"field.view_on_pfaf\")}" } } else { span { class: "placeholder", "\u{2014}" } } @@ -963,7 +965,7 @@ pub fn SpeciesDetail(slug: String) -> Element { } // Cultivars for this species (below the two-column layout) - h2 { "Cultivars" } + h2 { "{t(l, \"card.cultivars\")}" } CultivarListForSpecies { species_slug: species_slug } } }, @@ -981,13 +983,15 @@ fn CultivarListForSpecies(species_slug: String) -> Element { async move { api::list_cultivars(1, 100, Some(&s), None).await } }); + let l = lang.read().clone(); + rsx! { match &*cultivars.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { if data.data.is_empty() { - rsx! { p { class: "empty", "No cultivars yet." } } + rsx! { p { class: "empty", "{t(&l, \"no_cultivars\")}" } } } else { rsx! { div { class: "card-grid", @@ -1004,7 +1008,7 @@ fn CultivarListForSpecies(species_slug: String) -> Element { p { class: "card-common", "{common}" } } if c.is_organic { - span { class: "badge organic", "Organic" } + span { class: "badge organic", "{t(&l, \"field.organic\")}" } } } } diff --git a/herbapi-ui/src/pages/suppliers.rs b/herbapi-ui/src/pages/suppliers.rs index befbc81..05a2a65 100644 --- a/herbapi-ui/src/pages/suppliers.rs +++ b/herbapi-ui/src/pages/suppliers.rs @@ -1,8 +1,9 @@ use dioxus::prelude::*; use crate::api; -use crate::app::Route; +use crate::app::{Lang, Route}; use crate::components::table_controls::*; +use crate::i18n::t; const STORAGE_KEY_COLS: &str = "herbapi_suppliers_cols"; @@ -19,14 +20,17 @@ fn supplier_columns() -> Vec { #[component] pub fn SupplierList() -> Element { + let lang = use_context::().0; let columns = supplier_columns(); let visible_cols = use_signal(|| load_visible_columns(STORAGE_KEY_COLS, &supplier_columns())); let suppliers = use_resource(|| async { api::list_suppliers().await }); + let l = lang.read().clone(); + rsx! { div { class: "page", - h1 { "Suppliers" } + h1 { "{t(&l, \"page.suppliers\")}" } ColumnToggle { columns: columns.clone(), @@ -35,33 +39,33 @@ pub fn SupplierList() -> Element { } match &*suppliers.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { let vis = visible_cols.read(); rsx! { - p { class: "result-count", "{data.len()} suppliers" } + p { class: "result-count", "{data.len()} {t(&l, \"nav.suppliers\").to_lowercase()}" } div { class: "table-wrap", table { thead { tr { if is_col_visible(&vis, "name") { - th { "Name" } + th { "{t(&l, \"field.name\")}" } } if is_col_visible(&vis, "country") { - th { "Country" } + th { "{t(&l, \"field.country\")}" } } if is_col_visible(&vis, "organic") { - th { "Organic" } + th { "{t(&l, \"field.organic\")}" } } if is_col_visible(&vis, "demeter") { th { "Demeter" } } if is_col_visible(&vis, "website") { - th { "Website" } + th { "{t(&l, \"field.website\")}" } } if is_col_visible(&vis, "notes") { - th { "Notes" } + th { "{t(&l, \"field.notes\")}" } } } } @@ -81,7 +85,7 @@ pub fn SupplierList() -> Element { if is_col_visible(&vis, "organic") { td { if s.is_organic { - span { class: "badge organic", "Organic" } + span { class: "badge organic", "{t(&l, \"field.organic\")}" } } else { "-" } @@ -127,16 +131,19 @@ pub fn SupplierList() -> Element { #[component] pub fn SupplierDetail(slug: String) -> Element { + let lang = use_context::().0; let slug_clone = slug.clone(); let supplier = use_resource(move || { let s = slug_clone.clone(); async move { api::get_supplier(&s).await } }); + let l = lang.read().clone(); + rsx! { div { class: "page", match &*supplier.read() { - None => rsx! { p { "Loading..." } }, + None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(s)) => rsx! { h1 { "{s.name}" } @@ -145,7 +152,7 @@ pub fn SupplierDetail(slug: String) -> Element { } div { class: "badges", if s.is_organic { - span { class: "badge organic", "Organic" } + span { class: "badge organic", "{t(&l, \"field.organic\")}" } } if s.is_demeter { span { class: "badge demeter", "Demeter" }