Files
herbapi/herbapi-ui/src/pages/families.rs
T
florian.berthold aa36c846d7 Add full DE/EN UI label translations across all pages
Add t() translation function to i18n.rs with ~200 key/value pairs
covering navigation, card headers, field labels, buttons, filters,
planting calendar rows, and general UI text in both German and English.

Updated all 11 source files to use t(&lang, "key") instead of
hardcoded English strings: app.rs (sidebar nav), all 7 page files
(species, cultivars, families, suppliers, home, search, sources),
and all 3 component files (planting_calendar, table_controls,
plant_card). Boolean values (Yes/No/Annual) are also translated.
2026-03-15 17:20:51 +01:00

220 lines
9.8 KiB
Rust

use dioxus::prelude::*;
use crate::api;
use crate::app::{Lang, Route};
use crate::components::table_controls::*;
use crate::i18n::{pick_name, t};
const STORAGE_KEY_COLS: &str = "herbapi_families_cols";
const STORAGE_KEY_PP: &str = "herbapi_families_pp";
fn family_columns() -> Vec<ColumnDef> {
vec![
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
ColumnDef { key: "common_name", label: "Common Name", default_visible: true },
ColumnDef { key: "name_en", label: "English", default_visible: false },
ColumnDef { key: "name_de", label: "German", default_visible: false },
ColumnDef { key: "description", label: "Description", default_visible: false },
]
}
#[component]
pub fn FamilyList() -> Element {
let lang = use_context::<Lang>().0;
let columns = family_columns();
let mut page = use_signal(|| 1i64);
let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25));
let mut search = use_signal(|| String::new());
let visible_cols = use_signal(|| load_visible_columns(STORAGE_KEY_COLS, &family_columns()));
let families = use_resource(move || {
let s = search.read().clone();
let p = *page.read();
let pp = *per_page.read();
async move {
let q = if s.is_empty() { None } else { Some(s.as_str()) };
api::list_families(p, pp, q).await
}
});
let current_page = *page.read();
let l = lang.read().clone();
rsx! {
div { class: "page",
h1 { "{t(&l, \"page.families\")}" }
div { class: "table-toolbar",
div { class: "search-bar",
input {
r#type: "text",
placeholder: "{t(&l, \"search.placeholder_families\")}",
value: "{search}",
oninput: move |e| {
search.set(e.value());
page.set(1);
},
}
}
PerPageSelector {
per_page: per_page,
page: page,
storage_key: STORAGE_KEY_PP.to_string(),
}
}
ColumnToggle {
columns: columns.clone(),
visible: visible_cols,
storage_key: STORAGE_KEY_COLS.to_string(),
}
match &*families.read() {
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} {t(&l, \"nav.families\").to_lowercase()}" }
div { class: "table-wrap",
table {
thead {
tr {
if is_col_visible(&vis, "name_scientific") {
th { "{t(&l, \"field.scientific_name\")}" }
}
if is_col_visible(&vis, "common_name") {
th { "{t(&l, \"field.common_name\")}" }
}
if is_col_visible(&vis, "name_en") {
th { "{t(&l, \"field.name_en\")}" }
}
if is_col_visible(&vis, "name_de") {
th { "{t(&l, \"field.name_de\")}" }
}
if is_col_visible(&vis, "description") {
th { "{t(&l, \"field.description\")}" }
}
}
}
tbody {
for f in data.data.iter() {
{
let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific);
rsx! {
tr {
if is_col_visible(&vis, "name_scientific") {
td {
Link { to: Route::FamilyDetail { slug: f.slug.clone() },
em { "{f.name_scientific}" }
}
}
}
if is_col_visible(&vis, "common_name") {
td { "{common}" }
}
if is_col_visible(&vis, "name_en") {
td { "{f.name_en.as_deref().unwrap_or(\"-\")}" }
}
if is_col_visible(&vis, "name_de") {
td { "{f.name_de.as_deref().unwrap_or(\"-\")}" }
}
if is_col_visible(&vis, "description") {
td { class: "cell-truncated",
"{f.description.as_deref().map(|d| truncate(d, 80)).unwrap_or_else(|| \"-\".to_string())}"
}
}
}
}
}
}
}
}
}
if data.total > data.per_page {
div { class: "pagination",
button {
disabled: current_page <= 1,
onclick: move |_| page.set(current_page - 1),
"{t(&l, \"btn.previous\")}"
}
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),
"{t(&l, \"btn.next\")}"
}
}
}
}
},
}
}
}
}
#[component]
pub fn FamilyDetail(slug: String) -> Element {
let lang = use_context::<Lang>().0;
let slug_clone = slug.clone();
let family = use_resource(move || {
let s = slug_clone.clone();
async move { api::get_family(&s).await }
});
let slug_for_species = slug.clone();
let species = use_resource(move || {
let s = slug_for_species.clone();
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 { "{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);
rsx! {
h1 { em { "{f.name_scientific}" } }
if common != f.name_scientific {
p { class: "name-common", "{common}" }
}
if let Some(ref desc) = f.description {
p { "{desc}" }
}
}
},
}
h2 { "{t(&l, \"page.species_in_family\")}" }
match &*species.read() {
None => rsx! { p { "{t(&l, \"loading\")}" } },
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
Some(Ok(data)) => rsx! {
div { class: "card-grid",
for s in data.data.iter() {
{
let sp_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
let show_common = if sp_common == s.name_scientific { None } else { Some(sp_common) };
rsx! {
div { class: "plant-card",
Link { to: Route::SpeciesDetail { slug: s.slug.clone() },
em { "{s.name_scientific}" }
}
if let Some(ref common) = show_common {
p { class: "card-common", "{common}" }
}
}
}
}
}
}
},
}
}
}
}