Add bilingual uses fields (food/medicinal/other _de/_en) to species

Backend: add 6 new columns to Species and CreateSpecies structs, update
INSERT/UPDATE queries, add migration 010.
Frontend: add fields to Species type, use pick_desc() for language-aware
display in species detail and cultivar species-info card.
This commit is contained in:
2026-03-15 17:09:25 +01:00
parent 5a37190fd3
commit 896b364b09
6 changed files with 58 additions and 24 deletions
@@ -0,0 +1,6 @@
ALTER TABLE species ADD COLUMN IF NOT EXISTS food_uses_de TEXT;
ALTER TABLE species ADD COLUMN IF NOT EXISTS food_uses_en TEXT;
ALTER TABLE species ADD COLUMN IF NOT EXISTS medicinal_uses_de TEXT;
ALTER TABLE species ADD COLUMN IF NOT EXISTS medicinal_uses_en TEXT;
ALTER TABLE species ADD COLUMN IF NOT EXISTS other_uses_de TEXT;
ALTER TABLE species ADD COLUMN IF NOT EXISTS other_uses_en TEXT;
+12
View File
@@ -101,8 +101,14 @@ pub struct Species {
pub salt_tolerance: Option<String>,
pub edibility_rating: Option<i16>,
pub food_uses: Option<String>,
pub food_uses_de: Option<String>,
pub food_uses_en: Option<String>,
pub medicinal_uses: Option<String>,
pub medicinal_uses_de: Option<String>,
pub medicinal_uses_en: Option<String>,
pub other_uses: Option<String>,
pub other_uses_de: Option<String>,
pub other_uses_en: Option<String>,
pub native_range: Option<String>,
pub invasiveness: Option<String>,
pub pollination_type: Option<String>,
@@ -164,8 +170,14 @@ pub struct CreateSpecies {
pub salt_tolerance: Option<String>,
pub edibility_rating: Option<i16>,
pub food_uses: Option<String>,
pub food_uses_de: Option<String>,
pub food_uses_en: Option<String>,
pub medicinal_uses: Option<String>,
pub medicinal_uses_de: Option<String>,
pub medicinal_uses_en: Option<String>,
pub other_uses: Option<String>,
pub other_uses_de: Option<String>,
pub other_uses_en: Option<String>,
pub native_range: Option<String>,
pub invasiveness: Option<String>,
pub pollination_type: Option<String>,
+28 -18
View File
@@ -106,7 +106,10 @@ pub async fn create(pool: &PgPool, req: &CreateSpecies) -> Result<Species> {
soil_moisture, drainage_requirement, ph_min, ph_max, soil_texture_preference,
hardiness_zone_usda, hardiness_zone_at, min_temp, max_temp,
drought_tolerance, salt_tolerance, edibility_rating,
food_uses, medicinal_uses, other_uses, native_range, invasiveness, pollination_type,
food_uses, food_uses_de, food_uses_en,
medicinal_uses, medicinal_uses_de, medicinal_uses_en,
other_uses, other_uses_de, other_uses_en,
native_range, invasiveness, pollination_type,
plant_layer, nitrogen_fixer, dynamic_accumulator, dynamic_accumulator_nutrients,
attracts_pollinators, attracts_beneficial_insects, wildlife_value, mulch_plant,
ground_cover_quality, allelopathic, guild_role, succession_stage, heavy_metal_tolerance,
@@ -116,10 +119,10 @@ pub async fn create(pool: &PgPool, req: &CreateSpecies) -> Result<Species> {
hoverfly_count, beetle_count, bird_count, mammal_count,
native_status, naturadb_tags)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,
$19,$20,$21,$22,$23,$24,$25,
$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,
$39,$40,$41,$42,$43,$44,$45,
$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,$58)
$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,
$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,
$45,$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,
$58,$59,$60,$61,$62,$63,$64)
RETURNING *"
)
.bind(id).bind(&slug).bind(req.family_id).bind(&req.name_scientific)
@@ -130,7 +133,9 @@ pub async fn create(pool: &PgPool, req: &CreateSpecies) -> Result<Species> {
.bind(&req.hardiness_zone_usda).bind(&req.hardiness_zone_at)
.bind(req.min_temp).bind(req.max_temp)
.bind(&req.drought_tolerance).bind(&req.salt_tolerance).bind(req.edibility_rating)
.bind(&req.food_uses).bind(&req.medicinal_uses).bind(&req.other_uses)
.bind(&req.food_uses).bind(&req.food_uses_de).bind(&req.food_uses_en)
.bind(&req.medicinal_uses).bind(&req.medicinal_uses_de).bind(&req.medicinal_uses_en)
.bind(&req.other_uses).bind(&req.other_uses_de).bind(&req.other_uses_en)
.bind(&req.native_range).bind(&req.invasiveness).bind(&req.pollination_type)
.bind(&req.plant_layer).bind(req.nitrogen_fixer).bind(req.dynamic_accumulator)
.bind(&req.dynamic_accumulator_nutrients)
@@ -165,17 +170,20 @@ pub async fn update(pool: &PgPool, id: Uuid, req: &CreateSpecies) -> Result<Spec
soil_texture_preference=$14, hardiness_zone_usda=$15, hardiness_zone_at=$16,
min_temp=$17, max_temp=$18,
drought_tolerance=$19, salt_tolerance=$20,
edibility_rating=$21, food_uses=$22, medicinal_uses=$23, other_uses=$24,
native_range=$25, invasiveness=$26, pollination_type=$27,
plant_layer=$28, nitrogen_fixer=$29, dynamic_accumulator=$30,
dynamic_accumulator_nutrients=$31, attracts_pollinators=$32, attracts_beneficial_insects=$33,
wildlife_value=$34, mulch_plant=$35, ground_cover_quality=$36, allelopathic=$37,
guild_role=$38, succession_stage=$39, heavy_metal_tolerance=$40,
wikidata_qid=$41, gbif_id=$42, eppo_code=$43, pfaf_url=$44, source_urls=$45,
nectar_value=$46, pollen_value=$47, wild_bee_count=$48, wild_bee_specialist_count=$49,
butterfly_moth_count=$50, caterpillar_host_count=$51, caterpillar_specialist_count=$52,
hoverfly_count=$53, beetle_count=$54, bird_count=$55, mammal_count=$56,
native_status=$57, naturadb_tags=$58,
edibility_rating=$21,
food_uses=$22, food_uses_de=$23, food_uses_en=$24,
medicinal_uses=$25, medicinal_uses_de=$26, medicinal_uses_en=$27,
other_uses=$28, other_uses_de=$29, other_uses_en=$30,
native_range=$31, invasiveness=$32, pollination_type=$33,
plant_layer=$34, nitrogen_fixer=$35, dynamic_accumulator=$36,
dynamic_accumulator_nutrients=$37, attracts_pollinators=$38, attracts_beneficial_insects=$39,
wildlife_value=$40, mulch_plant=$41, ground_cover_quality=$42, allelopathic=$43,
guild_role=$44, succession_stage=$45, heavy_metal_tolerance=$46,
wikidata_qid=$47, gbif_id=$48, eppo_code=$49, pfaf_url=$50, source_urls=$51,
nectar_value=$52, pollen_value=$53, wild_bee_count=$54, wild_bee_specialist_count=$55,
butterfly_moth_count=$56, caterpillar_host_count=$57, caterpillar_specialist_count=$58,
hoverfly_count=$59, beetle_count=$60, bird_count=$61, mammal_count=$62,
native_status=$63, naturadb_tags=$64,
updated_at=NOW()
WHERE id=$1 RETURNING *"
)
@@ -187,7 +195,9 @@ pub async fn update(pool: &PgPool, id: Uuid, req: &CreateSpecies) -> Result<Spec
.bind(&req.hardiness_zone_usda).bind(&req.hardiness_zone_at)
.bind(req.min_temp).bind(req.max_temp)
.bind(&req.drought_tolerance).bind(&req.salt_tolerance).bind(req.edibility_rating)
.bind(&req.food_uses).bind(&req.medicinal_uses).bind(&req.other_uses)
.bind(&req.food_uses).bind(&req.food_uses_de).bind(&req.food_uses_en)
.bind(&req.medicinal_uses).bind(&req.medicinal_uses_de).bind(&req.medicinal_uses_en)
.bind(&req.other_uses).bind(&req.other_uses_de).bind(&req.other_uses_en)
.bind(&req.native_range).bind(&req.invasiveness).bind(&req.pollination_type)
.bind(&req.plant_layer).bind(req.nitrogen_fixer).bind(req.dynamic_accumulator)
.bind(&req.dynamic_accumulator_nutrients)
+3 -3
View File
@@ -843,9 +843,9 @@ pub fn CultivarDetail(slug: String) -> Element {
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_food = opt_str(&sp.food_uses);
let sp_med = opt_str(&sp.medicinal_uses);
let sp_other = opt_str(&sp.other_uses);
let sp_food = pick_desc(&current_lang, &sp.food_uses_de, &sp.food_uses_en, &sp.food_uses);
let sp_med = pick_desc(&current_lang, &sp.medicinal_uses_de, &sp.medicinal_uses_en, &sp.medicinal_uses);
let sp_other = pick_desc(&current_lang, &sp.other_uses_de, &sp.other_uses_en, &sp.other_uses);
let sp_wildlife = opt_str(&sp.wildlife_value);
let sp_native = opt_str(&sp.native_range);
rsx! {
+3 -3
View File
@@ -496,9 +496,9 @@ pub fn SpeciesDetail(slug: String) -> Element {
let desc = pick_desc(&current_lang, &s.description_de, &s.description_en, &s.description);
// Uses
let food = os(&s.food_uses);
let med = os(&s.medicinal_uses);
let other = os(&s.other_uses);
let food = pick_desc(&current_lang, &s.food_uses_de, &s.food_uses_en, &s.food_uses);
let med = pick_desc(&current_lang, &s.medicinal_uses_de, &s.medicinal_uses_en, &s.medicinal_uses);
let other = pick_desc(&current_lang, &s.other_uses_de, &s.other_uses_en, &s.other_uses);
let edibility = match s.edibility_rating {
Some(r) => format!("{r}/5"),
None => em.to_string(),
+6
View File
@@ -43,8 +43,14 @@ pub struct Species {
pub salt_tolerance: Option<String>,
pub edibility_rating: Option<i16>,
pub food_uses: Option<String>,
pub food_uses_de: Option<String>,
pub food_uses_en: Option<String>,
pub medicinal_uses: Option<String>,
pub medicinal_uses_de: Option<String>,
pub medicinal_uses_en: Option<String>,
pub other_uses: Option<String>,
pub other_uses_de: Option<String>,
pub other_uses_en: Option<String>,
pub native_range: Option<String>,
pub plant_layer: Option<String>,
pub nitrogen_fixer: Option<bool>,