feat(seed-saving): source attribution + advisory note

Record where the guidance comes from (copyright/attribution) and make
clear it is general, not local gospel — kept light:
- seed_saving.json gains a top-level sources list (Seed to Seed / Seed
  Savers Exchange / Organic Seed Alliance); parsed into
  SeedSavingData.sources, exposed via SeedSavingCatalog.sources.
- The detail section shows a small muted footer: an italic 'advisory —
  adapt to your climate' line plus a 'Source: …' credit.
- i18n seedSaving.advisory + sourcePrefix (en/es/pt/ast).
- Tests: asset carries sources; the section renders advisory + credit.
This commit is contained in:
vjrj 2026-07-11 07:52:28 +02:00
parent d897d3934c
commit 89addb1ed7
15 changed files with 93 additions and 8 deletions

View file

@ -86,12 +86,21 @@ class SeedSavingGuide {
}
}
/// An attribution for the bundled guidance (the facts are drawn from these).
class SeedSavingSource {
const SeedSavingSource({required this.title, this.author, this.url});
final String title;
final String? author;
final String? url;
}
/// The whole bundled dataset: family defaults plus per-taxon overrides (keyed by
/// scientific name OR bare genus). Lookups are case-insensitive.
class SeedSavingData {
SeedSavingData({
required Map<String, SeedSavingGuide> families,
required Map<String, SeedSavingGuide> taxa,
this.sources = const [],
}) : _families = {
for (final e in families.entries) e.key.toLowerCase().trim(): e.value,
},
@ -102,6 +111,9 @@ class SeedSavingData {
final Map<String, SeedSavingGuide> _families;
final Map<String, SeedSavingGuide> _taxa;
/// Where the guidance comes from shown as a small credit under the section.
final List<SeedSavingSource> sources;
/// The best guide for a crop: the family default, overlaid by a per-species
/// (or per-genus) entry when one exists. Returns null when nothing matches.
SeedSavingGuide? guideFor({String? scientificName, String? family}) {
@ -151,5 +163,18 @@ SeedSavingData parseSeedSaving(String jsonString) {
);
}
return SeedSavingData(families: section('families'), taxa: section('species'));
final sources = [
for (final s in (root['sources'] as List? ?? const []).cast<Map>())
SeedSavingSource(
title: s['title'] as String,
author: s['author'] as String?,
url: s['url'] as String?,
),
];
return SeedSavingData(
families: section('families'),
taxa: section('species'),
sources: sources,
);
}