Add DE/EN language toggle with bilingual descriptions

- Language switcher in sidebar (DE/EN buttons, persists to localStorage)
- i18n module with pick_desc/pick_name helpers for language-aware fallback
- All detail/list pages use language context for names and descriptions
- Species/Cultivar types updated with description_de/description_en
- Common Name column added to species/families lists
This commit is contained in:
2026-03-15 13:01:38 +01:00
parent efa05b2d44
commit 3ecfdfadf2
9 changed files with 244 additions and 63 deletions
+51 -18
View File
@@ -4,9 +4,10 @@ use std::collections::HashMap;
use uuid::Uuid;
use crate::api;
use crate::app::Route;
use crate::app::{Lang, Route};
use crate::components::plant_card::PlantCard;
use crate::components::table_controls::*;
use crate::i18n::{pick_desc, pick_name};
const STORAGE_KEY_COLS: &str = "herbapi_species_cols";
const STORAGE_KEY_PP: &str = "herbapi_species_pp";
@@ -15,7 +16,8 @@ const STORAGE_KEY_VIEW: &str = "herbapi_species_view";
fn species_columns() -> Vec<ColumnDef> {
vec![
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
ColumnDef { key: "name_de", label: "German", default_visible: true },
ColumnDef { key: "common_name", label: "Common Name", default_visible: true },
ColumnDef { key: "name_de", label: "German", default_visible: false },
ColumnDef { key: "name_en", label: "English", default_visible: false },
ColumnDef { key: "family", label: "Family", default_visible: true },
ColumnDef { key: "plant_layer", label: "Layer", default_visible: true },
@@ -33,6 +35,7 @@ fn species_columns() -> Vec<ColumnDef> {
#[component]
pub fn SpeciesList() -> Element {
let lang = use_context::<Lang>().0;
let columns = species_columns();
let mut page = use_signal(|| 1i64);
let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25));
@@ -222,6 +225,9 @@ pub fn SpeciesList() -> Element {
if is_col_visible(&vis, "name_scientific") {
th { "Scientific Name" }
}
if is_col_visible(&vis, "common_name") {
th { "Common Name" }
}
if is_col_visible(&vis, "name_de") {
th { "German" }
}
@@ -267,6 +273,7 @@ pub fn SpeciesList() -> Element {
for s in data.data.iter() {
{
let family_name: &str = fm.get(&s.family_id).map(String::as_str).unwrap_or("-");
let common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
rsx! {
tr {
if is_col_visible(&vis, "name_scientific") {
@@ -276,6 +283,9 @@ pub fn SpeciesList() -> Element {
}
}
}
if is_col_visible(&vis, "common_name") {
td { "{common}" }
}
if is_col_visible(&vis, "name_de") {
td { "{s.name_de.as_deref().unwrap_or(\"-\")}" }
}
@@ -369,12 +379,18 @@ pub fn SpeciesList() -> Element {
} else {
div { class: "card-grid",
for s in data.data.iter() {
PlantCard {
key: "{s.id}",
slug: s.slug.clone(),
name: s.name_scientific.clone(),
name_common: s.name_en.clone(),
entity_type: "species".to_string(),
{
let card_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
let show_common = if card_common == s.name_scientific { None } else { Some(card_common) };
rsx! {
PlantCard {
key: "{s.id}",
slug: s.slug.clone(),
name: s.name_scientific.clone(),
name_common: show_common,
entity_type: "species".to_string(),
}
}
}
}
}
@@ -404,6 +420,7 @@ pub fn SpeciesList() -> Element {
#[component]
pub fn SpeciesDetail(slug: String) -> Element {
let lang = use_context::<Lang>().0;
let slug_clone = slug.clone();
let species = use_resource(move || {
let s = slug_clone.clone();
@@ -436,6 +453,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
Some(Ok(s)) => {
let species_slug = s.slug.clone();
let current_lang = lang.read().clone();
// Helper closures to format fields
let os = |v: &Option<String>| -> String {
@@ -460,9 +478,10 @@ pub fn SpeciesDetail(slug: String) -> Element {
let em = "\u{2014}";
let common_name = pick_name(&current_lang, &s.name_de, &s.name_en, &s.name_scientific);
let name_en = os(&s.name_en);
let name_de = os(&s.name_de);
let desc = os(&s.description);
let desc = pick_desc(&current_lang, &s.description_de, &s.description_en, &s.description);
// Uses
let food = os(&s.food_uses);
@@ -511,6 +530,9 @@ pub fn SpeciesDetail(slug: String) -> Element {
rsx! {
h1 { em { "{s.name_scientific}" } }
if common_name != s.name_scientific {
p { class: "name-common", "{common_name}" }
}
div { class: "detail-row",
// === LEFT COLUMN ===
@@ -525,6 +547,10 @@ pub fn SpeciesDetail(slug: String) -> Element {
th { "Scientific Name" }
td { em { "{s.name_scientific}" } }
}
tr {
th { "Common Name" }
td { "{common_name}" }
}
tr {
th { "Name EN" }
td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" }
@@ -894,6 +920,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
#[component]
fn CultivarListForSpecies(species_slug: String) -> Element {
let lang = use_context::<Lang>().0;
let slug = species_slug.clone();
let cultivars = use_resource(move || {
let s = slug.clone();
@@ -911,15 +938,21 @@ fn CultivarListForSpecies(species_slug: String) -> Element {
rsx! {
div { class: "card-grid",
for c in data.data.iter() {
div { class: "plant-card",
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
strong { "{c.name}" }
}
if let Some(ref en) = c.name_en {
p { class: "card-common", "{en}" }
}
if c.is_organic {
span { class: "badge organic", "Organic" }
{
let cv_common = pick_name(&lang.read(), &c.name_de, &c.name_en, &c.name);
let show_common = if cv_common == c.name { None } else { Some(cv_common) };
rsx! {
div { class: "plant-card",
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
strong { "{c.name}" }
}
if let Some(ref common) = show_common {
p { class: "card-common", "{common}" }
}
if c.is_organic {
span { class: "badge organic", "Organic" }
}
}
}
}
}