From 89addb1ed7a8e6e399c4a2d99ce9c288eab50db9 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 11 Jul 2026 07:52:28 +0200 Subject: [PATCH] feat(seed-saving): source attribution + advisory note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../app_seeds/assets/catalog/seed_saving.json | 7 ++++- .../lib/data/seed_saving_catalog.dart | 3 +++ apps/app_seeds/lib/domain/seed_saving.dart | 27 ++++++++++++++++++- apps/app_seeds/lib/i18n/ast.i18n.json | 4 ++- apps/app_seeds/lib/i18n/en.i18n.json | 4 ++- apps/app_seeds/lib/i18n/es.i18n.json | 4 ++- apps/app_seeds/lib/i18n/pt.i18n.json | 4 ++- apps/app_seeds/lib/i18n/strings.g.dart | 4 +-- apps/app_seeds/lib/i18n/strings_ast.g.dart | 4 +++ apps/app_seeds/lib/i18n/strings_en.g.dart | 8 ++++++ apps/app_seeds/lib/i18n/strings_es.g.dart | 4 +++ apps/app_seeds/lib/i18n/strings_pt.g.dart | 4 +++ .../lib/ui/variety_detail_screen.dart | 15 +++++++++++ .../test/data/seed_saving_asset_test.dart | 5 ++++ .../test/ui/seed_saving_section_test.dart | 4 +++ 15 files changed, 93 insertions(+), 8 deletions(-) diff --git a/apps/app_seeds/assets/catalog/seed_saving.json b/apps/app_seeds/assets/catalog/seed_saving.json index a8b7568..30d1433 100644 --- a/apps/app_seeds/assets/catalog/seed_saving.json +++ b/apps/app_seeds/assets/catalog/seed_saving.json @@ -1,6 +1,11 @@ { "version": 1, - "note": "Curated seed-saving guidance (public-domain horticultural knowledge, e.g. Ashworth 'Seed to Seed', Seed Savers Exchange). Keyed by botanical family (defaults) with per-species/genus overrides that win. Distances in metres are conservative home-garden ranges for keeping a variety reasonably true; plant counts keep enough genetic diversity. `note` is a locale-keyed map, extensible to any language. This is a starter set, not a ceiling.", + "note": "Curated seed-saving guidance. Values are widely-published horticultural FACTS (isolation distances, pollination habit, population sizes) — not copied text — drawn from the sources below and cross-checked for consistency. Keyed by botanical family (defaults) with per-species/genus overrides that win. Distances in metres are conservative home-garden ranges for keeping a variety reasonably true; plant counts keep enough genetic diversity. `note` is a locale-keyed map, extensible to any language. Advisory only — adapt to local climate. A starter set, not a ceiling.", + "sources": [ + { "title": "Seed to Seed", "author": "Suzanne Ashworth", "publisher": "Seed Savers Exchange" }, + { "title": "Seed Savers Exchange", "url": "https://seedsavers.org" }, + { "title": "Organic Seed Alliance", "url": "https://seedalliance.org" } + ], "families": { "Solanaceae": { "lifeCycle": "annual", "pollination": "self", "vector": "self", "isolationMinM": 3, "isolationMaxM": 15, "minPlants": 1, "recommendedPlants": 6, "processing": "wet", "difficulty": "easy" }, "Fabaceae": { "lifeCycle": "annual", "pollination": "self", "vector": "self", "isolationMinM": 3, "isolationMaxM": 20, "minPlants": 1, "recommendedPlants": 10, "processing": "dry", "difficulty": "easy" }, diff --git a/apps/app_seeds/lib/data/seed_saving_catalog.dart b/apps/app_seeds/lib/data/seed_saving_catalog.dart index f27b68a..9f6b6bd 100644 --- a/apps/app_seeds/lib/data/seed_saving_catalog.dart +++ b/apps/app_seeds/lib/data/seed_saving_catalog.dart @@ -29,4 +29,7 @@ class SeedSavingCatalog { /// matches. See [SeedSavingData.guideFor]. static SeedSavingGuide? guideFor({String? scientificName, String? family}) => data?.guideFor(scientificName: scientificName, family: family); + + /// The attributions for the guidance (empty before load). + static List get sources => data?.sources ?? const []; } diff --git a/apps/app_seeds/lib/domain/seed_saving.dart b/apps/app_seeds/lib/domain/seed_saving.dart index 2b86a68..4ed4646 100644 --- a/apps/app_seeds/lib/domain/seed_saving.dart +++ b/apps/app_seeds/lib/domain/seed_saving.dart @@ -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 families, required Map 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 _families; final Map _taxa; + /// Where the guidance comes from — shown as a small credit under the section. + final List 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()) + 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, + ); } diff --git a/apps/app_seeds/lib/i18n/ast.i18n.json b/apps/app_seeds/lib/i18n/ast.i18n.json index 6e4c487..50702f7 100644 --- a/apps/app_seeds/lib/i18n/ast.i18n.json +++ b/apps/app_seeds/lib/i18n/ast.i18n.json @@ -23,7 +23,9 @@ "difficulty": "Dificultá", "diffEasy": "Fácil", "diffMedium": "Media", - "diffHard": "Difícil" + "diffHard": "Difícil", + "advisory": "Orientativu — afaílu al to clima y variedá.", + "sourcePrefix": "Fonte" }, "calendar": { "title": "Esti mes", diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 8b09e7e..6ae843d 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -23,7 +23,9 @@ "difficulty": "Difficulty", "diffEasy": "Easy", "diffMedium": "Medium", - "diffHard": "Hard" + "diffHard": "Hard", + "advisory": "General guidance — adapt it to your climate and variety.", + "sourcePrefix": "Source" }, "calendar": { "title": "This month", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index 2913bc4..f5baf51 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -23,7 +23,9 @@ "difficulty": "Dificultad", "diffEasy": "Fácil", "diffMedium": "Media", - "diffHard": "Difícil" + "diffHard": "Difícil", + "advisory": "Orientativo — adáptalo a tu clima y variedad.", + "sourcePrefix": "Fuente" }, "calendar": { "title": "Este mes", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index 4f9fb7d..ab2e91d 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -23,7 +23,9 @@ "difficulty": "Dificuldade", "diffEasy": "Fácil", "diffMedium": "Média", - "diffHard": "Difícil" + "diffHard": "Difícil", + "advisory": "Orientativo — adapte-o ao seu clima e variedade.", + "sourcePrefix": "Fonte" }, "calendar": { "title": "Este mês", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index 94b2ec3..d97213a 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 4 -/// Strings: 1960 (490 per locale) +/// Strings: 1968 (492 per locale) /// -/// Built on 2026-07-11 at 05:18 UTC +/// Built on 2026-07-11 at 05:50 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_ast.g.dart b/apps/app_seeds/lib/i18n/strings_ast.g.dart index 67e9a3d..1ad3284 100644 --- a/apps/app_seeds/lib/i18n/strings_ast.g.dart +++ b/apps/app_seeds/lib/i18n/strings_ast.g.dart @@ -114,6 +114,8 @@ class _Translations$seedSaving$ast extends Translations$seedSaving$en { @override String get diffEasy => 'Fácil'; @override String get diffMedium => 'Media'; @override String get diffHard => 'Difícil'; + @override String get advisory => 'Orientativu — afaílu al to clima y variedá.'; + @override String get sourcePrefix => 'Fonte'; } // Path: calendar @@ -1284,6 +1286,8 @@ extension on TranslationsAst { 'seedSaving.diffEasy' => 'Fácil', 'seedSaving.diffMedium' => 'Media', 'seedSaving.diffHard' => 'Difícil', + 'seedSaving.advisory' => 'Orientativu — afaílu al to clima y variedá.', + 'seedSaving.sourcePrefix' => 'Fonte', 'calendar.title' => 'Esti mes', 'calendar.filterChip' => 'Esti mes', 'calendar.nothing' => ({required Object month}) => 'Nada anotao pa ${month}.', diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index eb45b38..12d5c9b 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -163,6 +163,12 @@ class Translations$seedSaving$en { /// en: 'Hard' String get diffHard => 'Hard'; + + /// en: 'General guidance — adapt it to your climate and variety.' + String get advisory => 'General guidance — adapt it to your climate and variety.'; + + /// en: 'Source' + String get sourcePrefix => 'Source'; } // Path: calendar @@ -2251,6 +2257,8 @@ extension on Translations { 'seedSaving.diffEasy' => 'Easy', 'seedSaving.diffMedium' => 'Medium', 'seedSaving.diffHard' => 'Hard', + 'seedSaving.advisory' => 'General guidance — adapt it to your climate and variety.', + 'seedSaving.sourcePrefix' => 'Source', 'calendar.title' => 'This month', 'calendar.filterChip' => 'This month', 'calendar.nothing' => ({required Object month}) => 'Nothing noted for ${month}.', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index f79d52d..a1d5934 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -114,6 +114,8 @@ class _Translations$seedSaving$es extends Translations$seedSaving$en { @override String get diffEasy => 'Fácil'; @override String get diffMedium => 'Media'; @override String get diffHard => 'Difícil'; + @override String get advisory => 'Orientativo — adáptalo a tu clima y variedad.'; + @override String get sourcePrefix => 'Fuente'; } // Path: calendar @@ -1286,6 +1288,8 @@ extension on TranslationsEs { 'seedSaving.diffEasy' => 'Fácil', 'seedSaving.diffMedium' => 'Media', 'seedSaving.diffHard' => 'Difícil', + 'seedSaving.advisory' => 'Orientativo — adáptalo a tu clima y variedad.', + 'seedSaving.sourcePrefix' => 'Fuente', 'calendar.title' => 'Este mes', 'calendar.filterChip' => 'Este mes', 'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 27068d1..4335de7 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -114,6 +114,8 @@ class _Translations$seedSaving$pt extends Translations$seedSaving$en { @override String get diffEasy => 'Fácil'; @override String get diffMedium => 'Média'; @override String get diffHard => 'Difícil'; + @override String get advisory => 'Orientativo — adapte-o ao seu clima e variedade.'; + @override String get sourcePrefix => 'Fonte'; } // Path: calendar @@ -1283,6 +1285,8 @@ extension on TranslationsPt { 'seedSaving.diffEasy' => 'Fácil', 'seedSaving.diffMedium' => 'Média', 'seedSaving.diffHard' => 'Difícil', + 'seedSaving.advisory' => 'Orientativo — adapte-o ao seu clima e variedade.', + 'seedSaving.sourcePrefix' => 'Fonte', 'calendar.title' => 'Este mês', 'calendar.filterChip' => 'Este mês', 'calendar.nothing' => ({required Object month}) => 'Nada anotado para ${month}.', diff --git a/apps/app_seeds/lib/ui/variety_detail_screen.dart b/apps/app_seeds/lib/ui/variety_detail_screen.dart index 8472b77..46ecc99 100644 --- a/apps/app_seeds/lib/ui/variety_detail_screen.dart +++ b/apps/app_seeds/lib/ui/variety_detail_screen.dart @@ -1097,6 +1097,21 @@ class _SeedSavingView extends StatelessWidget { const SizedBox(height: 8), Text(note, style: const TextStyle(color: seedMuted, height: 1.35)), ], + // Advisory + a light source credit — the figures are general, not local + // gospel, and drawn from the seed-saving literature. + const SizedBox(height: 10), + Text(t.seedSaving.advisory, + style: const TextStyle( + color: seedMuted, fontSize: 12, fontStyle: FontStyle.italic)), + if (SeedSavingCatalog.sources case final sources when sources.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + '${t.seedSaving.sourcePrefix}: ' + '${sources.map((s) => s.title).join(' · ')}', + style: const TextStyle(color: seedMuted, fontSize: 11), + ), + ), ], ); } diff --git a/apps/app_seeds/test/data/seed_saving_asset_test.dart b/apps/app_seeds/test/data/seed_saving_asset_test.dart index 4f0bda9..fb8e049 100644 --- a/apps/app_seeds/test/data/seed_saving_asset_test.dart +++ b/apps/app_seeds/test/data/seed_saving_asset_test.dart @@ -48,4 +48,9 @@ void main() { expect(g?.vector, PollinationVector.wind); expect(g?.recommendedPlants, greaterThanOrEqualTo(50)); }); + + test('carries source attributions', () { + expect(data.sources, isNotEmpty); + expect(data.sources.map((s) => s.title), contains('Seed to Seed')); + }); } diff --git a/apps/app_seeds/test/ui/seed_saving_section_test.dart b/apps/app_seeds/test/ui/seed_saving_section_test.dart index e6559f9..015a0a9 100644 --- a/apps/app_seeds/test/ui/seed_saving_section_test.dart +++ b/apps/app_seeds/test/ui/seed_saving_section_test.dart @@ -13,6 +13,7 @@ void main() { db = newTestDatabase(); SeedSavingCatalog.data = parseSeedSaving(''' { + "sources": [ { "title": "Seed to Seed" } ], "families": { "Solanaceae": { "pollination": "self", "isolationMinM": 3, "isolationMaxM": 6, "processing": "wet", "difficulty": "easy" } }, @@ -39,6 +40,9 @@ void main() { // The pollination line is a Text.rich (label + value), so match the run. expect(find.textContaining('Self-pollinating'), findsOneWidget); expect(find.text('Easy'), findsOneWidget); // the difficulty pill + // Advisory note + a light source credit. + expect(find.textContaining('adapt it to your climate'), findsOneWidget); + expect(find.textContaining('Seed to Seed'), findsOneWidget); await disposeTree(tester); });