feat(seed-saving): per-crop 'how to save this seed' guidance

A bundled, curated knowledge layer — the differentiator of a serious
seed app. For a variety's crop it shows how the plant reproduces and what
it takes to keep the variety true: life cycle, pollination (self/cross +
insect/wind), isolation distance, how many plants to save from, dry/wet
processing, a difficulty pill, and a short tip — all in human words.

- assets/catalog/seed_saving.json: curated public-domain data (Seed to
  Seed / Seed Savers), keyed by botanical FAMILY (defaults) with
  per-species/genus OVERRIDES that win — so e.g. Vicia faba is correctly
  insect-cross-pollinated despite the selfing Fabaceae default, and maize
  is wind-pollinated with a large population. Notes are locale-keyed
  (es/en), extensible to any language — a starter set, not a ceiling.
- domain/seed_saving.dart (pure): enums + SeedSavingGuide (merge, note
  fallback) + SeedSavingData.guideFor (species → genus → family).
- data/seed_saving_catalog.dart: loads the asset once into an in-memory
  singleton (no DB table, no migration); injector loads it at startup.
- VarietyDetail gains  (from the linked species — the reliable
  key, vs the editable category); detail screen shows a _SeedSavingView
  when a guide resolves.
- i18n seedSaving block (labels + enum values + params) in en/es/pt/ast.
- Tests: domain lookup/merge, the real asset's key corrections, and the
  detail section shows/hides. 314 suite tests green; analyze clean.
This commit is contained in:
vjrj 2026-07-11 07:17:12 +02:00
parent bfff95fe8d
commit 7c2e3f207f
19 changed files with 954 additions and 2 deletions

View file

@ -40,6 +40,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
Translations $copyWith({TranslationMetadata<AppLocale, Translations>? meta}) => Translations(meta: meta ?? this.$meta);
// Translations
late final Translations$seedSaving$en seedSaving = Translations$seedSaving$en.internal(_root);
late final Translations$calendar$en calendar = Translations$calendar$en.internal(_root);
late final Translations$app$en app = Translations$app$en.internal(_root);
late final Translations$bootstrap$en bootstrap = Translations$bootstrap$en.internal(_root);
@ -83,6 +84,87 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$sale$en sale = Translations$sale$en.internal(_root);
}
// Path: seedSaving
class Translations$seedSaving$en {
Translations$seedSaving$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Saving its seed'
String get title => 'Saving its seed';
/// en: 'What it takes to keep the variety true'
String get subtitle => 'What it takes to keep the variety true';
/// en: 'Cycle'
String get lifeCycle => 'Cycle';
/// en: 'Annual'
String get cycleAnnual => 'Annual';
/// en: 'Biennial — seeds in year 2'
String get cycleBiennial => 'Biennial — seeds in year 2';
/// en: 'Perennial'
String get cyclePerennial => 'Perennial';
/// en: 'Pollination'
String get pollination => 'Pollination';
/// en: 'Self-pollinating'
String get pollSelf => 'Self-pollinating';
/// en: 'Crosses with others'
String get pollCross => 'Crosses with others';
/// en: 'Self-pollinates, sometimes crosses'
String get pollMixed => 'Self-pollinates, sometimes crosses';
/// en: 'by insects'
String get byInsect => 'by insects';
/// en: 'on the wind'
String get byWind => 'on the wind';
/// en: 'Keep apart'
String get isolation => 'Keep apart';
/// en: '{min}{max} m from other varieties'
String isolationRange({required Object min, required Object max}) => '${min}${max} m from other varieties';
/// en: '{min} m from other varieties'
String isolationSingle({required Object min}) => '${min} m from other varieties';
/// en: 'Save from several plants'
String get plants => 'Save from several plants';
/// en: 'From at least {n} plants'
String plantsValue({required Object n}) => 'From at least ${n} plants';
/// en: 'Cleaning the seed'
String get processing => 'Cleaning the seed';
/// en: 'Dry seed (thresh)'
String get procDry => 'Dry seed (thresh)';
/// en: 'Wet seed (ferment and rinse)'
String get procWet => 'Wet seed (ferment and rinse)';
/// en: 'Difficulty'
String get difficulty => 'Difficulty';
/// en: 'Easy'
String get diffEasy => 'Easy';
/// en: 'Medium'
String get diffMedium => 'Medium';
/// en: 'Hard'
String get diffHard => 'Hard';
}
// Path: calendar
class Translations$calendar$en {
Translations$calendar$en.internal(this._root);
@ -2145,6 +2227,30 @@ class Translations$intro$slides$plantare$en {
extension on Translations {
dynamic _flatMapFunction(String path) {
return switch (path) {
'seedSaving.title' => 'Saving its seed',
'seedSaving.subtitle' => 'What it takes to keep the variety true',
'seedSaving.lifeCycle' => 'Cycle',
'seedSaving.cycleAnnual' => 'Annual',
'seedSaving.cycleBiennial' => 'Biennial — seeds in year 2',
'seedSaving.cyclePerennial' => 'Perennial',
'seedSaving.pollination' => 'Pollination',
'seedSaving.pollSelf' => 'Self-pollinating',
'seedSaving.pollCross' => 'Crosses with others',
'seedSaving.pollMixed' => 'Self-pollinates, sometimes crosses',
'seedSaving.byInsect' => 'by insects',
'seedSaving.byWind' => 'on the wind',
'seedSaving.isolation' => 'Keep apart',
'seedSaving.isolationRange' => ({required Object min, required Object max}) => '${min}${max} m from other varieties',
'seedSaving.isolationSingle' => ({required Object min}) => '${min} m from other varieties',
'seedSaving.plants' => 'Save from several plants',
'seedSaving.plantsValue' => ({required Object n}) => 'From at least ${n} plants',
'seedSaving.processing' => 'Cleaning the seed',
'seedSaving.procDry' => 'Dry seed (thresh)',
'seedSaving.procWet' => 'Wet seed (ferment and rinse)',
'seedSaving.difficulty' => 'Difficulty',
'seedSaving.diffEasy' => 'Easy',
'seedSaving.diffMedium' => 'Medium',
'seedSaving.diffHard' => 'Hard',
'calendar.title' => 'This month',
'calendar.filterChip' => 'This month',
'calendar.nothing' => ({required Object month}) => 'Nothing noted for ${month}.',