feat(inventory): photo-first drafts + on-device OCR (digitization R2+R4)
Lower the bulk-digitization cliff with two more routes on top of the already-landed CSV import and "save and add another": - Photo-first drafts (capture now, catalogue later): burst-capture photos (camera or multi-gallery) into unnamed draft varieties, shown in a "to catalogue" tray, hidden from the main list until named. Adds Variety.isDraft (schema), addDraftVariety/watchDrafts/nameDraft, the triage sheet and the inventory banner. - On-device OCR label suggestion (Tesseract, offline, no Google): a "Suggest name from photo" button in the naming dialog behind a LabelTextExtractor interface (Tesseract on Android/iOS, no-op elsewhere). Reads the largest print via hOCR bounding boxes, drops boilerplate/low-confidence noise, preprocesses (grayscale, contrast, upscale) and sweeps rotations (0-315 deg) so tilted packets still read. Bundles tessdata_fast eng+spa; validated on-device against real packets. The photo is written to a temp file deleted immediately in a finally block (the plugin needs a path) - a bounded, documented exception to no-plaintext-at-rest. This commit also carries the co-developed schema evolution v5 to v8 that shares these files (organic flag, species viability years, crop calendar, lot provenance/abundance/preservation format, condition checks) plus their exports/migrations and i18n. Tests: CSV/draft/OCR unit + widget + migration green in isolation. Note: the full widget suite currently hangs (>10 min) - under investigation.
This commit is contained in:
parent
12a2ee2d64
commit
6809dc6143
89 changed files with 17141 additions and 228 deletions
|
|
@ -50,14 +50,23 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$about$en about = Translations$about$en.internal(_root);
|
||||
late final Translations$intro$en intro = Translations$intro$en.internal(_root);
|
||||
late final Translations$inventory$en inventory = Translations$inventory$en.internal(_root);
|
||||
late final Translations$draft$en draft = Translations$draft$en.internal(_root);
|
||||
late final Translations$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root);
|
||||
late final Translations$detail$en detail = Translations$detail$en.internal(_root);
|
||||
late final Translations$germination$en germination = Translations$germination$en.internal(_root);
|
||||
late final Translations$viability$en viability = Translations$viability$en.internal(_root);
|
||||
late final Translations$editVariety$en editVariety = Translations$editVariety$en.internal(_root);
|
||||
late final Translations$addLot$en addLot = Translations$addLot$en.internal(_root);
|
||||
late final Translations$harvest$en harvest = Translations$harvest$en.internal(_root);
|
||||
late final Translations$lotType$en lotType = Translations$lotType$en.internal(_root);
|
||||
late final Translations$presentation$en presentation = Translations$presentation$en.internal(_root);
|
||||
late final Translations$provenance$en provenance = Translations$provenance$en.internal(_root);
|
||||
late final Translations$abundance$en abundance = Translations$abundance$en.internal(_root);
|
||||
late final Translations$cropCalendar$en cropCalendar = Translations$cropCalendar$en.internal(_root);
|
||||
late final Translations$needsReproduction$en needsReproduction = Translations$needsReproduction$en.internal(_root);
|
||||
late final Translations$preservation$en preservation = Translations$preservation$en.internal(_root);
|
||||
late final Translations$conditionCheck$en conditionCheck = Translations$conditionCheck$en.internal(_root);
|
||||
late final Translations$desiccant$en desiccant = Translations$desiccant$en.internal(_root);
|
||||
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
||||
}
|
||||
|
||||
|
|
@ -219,41 +228,41 @@ class Translations$backup$en {
|
|||
|
||||
// Translations
|
||||
|
||||
/// en: 'Backup'
|
||||
String get title => 'Backup';
|
||||
/// en: 'Backup & restore'
|
||||
String get title => 'Backup & restore';
|
||||
|
||||
/// en: 'Export as CSV'
|
||||
String get exportCsv => 'Export as CSV';
|
||||
/// en: 'Save a backup'
|
||||
String get exportJson => 'Save a backup';
|
||||
|
||||
/// en: 'For spreadsheets — no photos'
|
||||
String get exportCsvSubtitle => 'For spreadsheets — no photos';
|
||||
/// en: 'A complete copy to keep safe, restore later or move to another device'
|
||||
String get exportJsonSubtitle => 'A complete copy to keep safe, restore later or move to another device';
|
||||
|
||||
/// en: 'Export as JSON'
|
||||
String get exportJson => 'Export as JSON';
|
||||
/// en: 'Restore a backup'
|
||||
String get importJson => 'Restore a backup';
|
||||
|
||||
/// en: 'Complete copy, can be imported back'
|
||||
String get exportJsonSubtitle => 'Complete copy, can be imported back';
|
||||
/// en: 'Bring a saved copy back — nothing gets duplicated'
|
||||
String get importJsonSubtitle => 'Bring a saved copy back — nothing gets duplicated';
|
||||
|
||||
/// en: 'Import from JSON'
|
||||
String get importJson => 'Import from JSON';
|
||||
/// en: 'Export to a spreadsheet'
|
||||
String get exportCsv => 'Export to a spreadsheet';
|
||||
|
||||
/// en: 'Merge a saved copy into this inventory'
|
||||
String get importJsonSubtitle => 'Merge a saved copy into this inventory';
|
||||
/// en: 'A simple list for Excel or LibreOffice — no photos'
|
||||
String get exportCsvSubtitle => 'A simple list for Excel or LibreOffice — no photos';
|
||||
|
||||
/// en: 'Import from CSV'
|
||||
String get importCsv => 'Import from CSV';
|
||||
/// en: 'Import a list'
|
||||
String get importCsv => 'Import a list';
|
||||
|
||||
/// en: 'Add a list from a spreadsheet'
|
||||
String get importCsvSubtitle => 'Add a list from a spreadsheet';
|
||||
/// en: 'Add entries from a spreadsheet'
|
||||
String get importCsvSubtitle => 'Add entries from a spreadsheet';
|
||||
|
||||
/// en: 'Import a saved copy?'
|
||||
String get importConfirmTitle => 'Import a saved copy?';
|
||||
/// en: 'Restore a backup?'
|
||||
String get importConfirmTitle => 'Restore a backup?';
|
||||
|
||||
/// en: 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.'
|
||||
String get importConfirmBody => 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.';
|
||||
|
||||
/// en: 'Import a CSV list?'
|
||||
String get importCsvConfirmTitle => 'Import a CSV list?';
|
||||
/// en: 'Import a list?'
|
||||
String get importCsvConfirmTitle => 'Import a list?';
|
||||
|
||||
/// en: 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.'
|
||||
String get importCsvConfirmBody => 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.';
|
||||
|
|
@ -270,8 +279,8 @@ class Translations$backup$en {
|
|||
/// en: 'Imported: {added} new, {updated} updated'
|
||||
String importDone({required Object added, required Object updated}) => 'Imported: ${added} new, ${updated} updated';
|
||||
|
||||
/// en: 'Imported {count} from CSV'
|
||||
String importCsvDone({required Object count}) => 'Imported ${count} from CSV';
|
||||
/// en: 'Added {count} entries'
|
||||
String importCsvDone({required Object count}) => 'Added ${count} entries';
|
||||
|
||||
/// en: 'This file could not be read as a Tanemaki copy'
|
||||
String get importFailed => 'This file could not be read as a Tanemaki copy';
|
||||
|
|
@ -370,6 +379,45 @@ class Translations$inventory$en {
|
|||
|
||||
/// en: 'Uncategorized'
|
||||
String get uncategorized => 'Uncategorized';
|
||||
|
||||
/// en: 'To regrow'
|
||||
String get needsReproductionFilter => 'To regrow';
|
||||
}
|
||||
|
||||
// Path: draft
|
||||
class Translations$draft$en {
|
||||
Translations$draft$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Capture photos'
|
||||
String get capture => 'Capture photos';
|
||||
|
||||
/// en: '{n} captured to catalogue'
|
||||
String captured({required Object n}) => '${n} captured to catalogue';
|
||||
|
||||
/// en: 'To catalogue'
|
||||
String get triageTitle => 'To catalogue';
|
||||
|
||||
/// en: '{n} to catalogue'
|
||||
String triageCount({required Object n}) => '${n} to catalogue';
|
||||
|
||||
/// en: 'Unnamed'
|
||||
String get untitled => 'Unnamed';
|
||||
|
||||
/// en: 'Name this seed'
|
||||
String get nameField => 'Name this seed';
|
||||
|
||||
/// en: 'What is it?'
|
||||
String get nameHint => 'What is it?';
|
||||
|
||||
/// en: 'Suggest name from photo'
|
||||
String get suggestFromPhoto => 'Suggest name from photo';
|
||||
|
||||
/// en: 'Discard'
|
||||
String get discard => 'Discard';
|
||||
}
|
||||
|
||||
// Path: quickAdd
|
||||
|
|
@ -492,6 +540,27 @@ class Translations$germination$en {
|
|||
String result({required Object percent}) => '${percent}%';
|
||||
}
|
||||
|
||||
// Path: viability
|
||||
class Translations$viability$en {
|
||||
Translations$viability$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Use or reproduce this season'
|
||||
String get expiringSoon => 'Use or reproduce this season';
|
||||
|
||||
/// en: 'Use or reproduce this season · keeps ~{years} yr'
|
||||
String expiringSoonYears({required Object years}) => 'Use or reproduce this season · keeps ~${years} yr';
|
||||
|
||||
/// en: 'Past typical viability — reproduce'
|
||||
String get expired => 'Past typical viability — reproduce';
|
||||
|
||||
/// en: 'Past typical viability (~{years} yr) — reproduce'
|
||||
String expiredYears({required Object years}) => 'Past typical viability (~${years} yr) — reproduce';
|
||||
}
|
||||
|
||||
// Path: editVariety
|
||||
class Translations$editVariety$en {
|
||||
Translations$editVariety$en.internal(this._root);
|
||||
|
|
@ -517,6 +586,12 @@ class Translations$editVariety$en {
|
|||
|
||||
/// en: 'Search a species…'
|
||||
String get speciesHint => 'Search a species…';
|
||||
|
||||
/// en: 'Organic'
|
||||
String get organic => 'Organic';
|
||||
|
||||
/// en: 'Grown organically (eco)'
|
||||
String get organicHint => 'Grown organically (eco)';
|
||||
}
|
||||
|
||||
// Path: addLot
|
||||
|
|
@ -630,6 +705,204 @@ class Translations$presentation$en {
|
|||
String get rootBall => 'Root-ball';
|
||||
}
|
||||
|
||||
// Path: provenance
|
||||
class Translations$provenance$en {
|
||||
Translations$provenance$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Where it's from'
|
||||
String get section => 'Where it\'s from';
|
||||
|
||||
/// en: 'Seeds from'
|
||||
String get seedsFrom => 'Seeds from';
|
||||
|
||||
/// en: 'Who grew or gave them'
|
||||
String get seedsFromHint => 'Who grew or gave them';
|
||||
|
||||
/// en: 'Place'
|
||||
String get place => 'Place';
|
||||
|
||||
/// en: 'Where they come from (with region)'
|
||||
String get placeHint => 'Where they come from (with region)';
|
||||
|
||||
/// en: 'Seeds from'
|
||||
String get addSeedsFrom => 'Seeds from';
|
||||
|
||||
/// en: 'Place'
|
||||
String get addPlace => 'Place';
|
||||
}
|
||||
|
||||
// Path: abundance
|
||||
class Translations$abundance$en {
|
||||
Translations$abundance$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'How much I have'
|
||||
String get add => 'How much I have';
|
||||
|
||||
/// en: 'How much I have'
|
||||
String get title => 'How much I have';
|
||||
|
||||
/// en: 'Not set'
|
||||
String get none => 'Not set';
|
||||
|
||||
/// en: 'Plenty to share'
|
||||
String get plentyToShare => 'Plenty to share';
|
||||
|
||||
/// en: 'Enough to share a little'
|
||||
String get enoughToShare => 'Enough to share a little';
|
||||
|
||||
/// en: 'Enough for me'
|
||||
String get enoughForMe => 'Enough for me';
|
||||
|
||||
/// en: 'Running low'
|
||||
String get runningLow => 'Running low';
|
||||
}
|
||||
|
||||
// Path: cropCalendar
|
||||
class Translations$cropCalendar$en {
|
||||
Translations$cropCalendar$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Crop calendar'
|
||||
String get add => 'Crop calendar';
|
||||
|
||||
/// en: 'Crop calendar'
|
||||
String get title => 'Crop calendar';
|
||||
|
||||
/// en: 'Sow'
|
||||
String get sow => 'Sow';
|
||||
|
||||
/// en: 'Transplant'
|
||||
String get transplant => 'Transplant';
|
||||
|
||||
/// en: 'Flowering'
|
||||
String get flowering => 'Flowering';
|
||||
|
||||
/// en: 'Fruiting'
|
||||
String get fruiting => 'Fruiting';
|
||||
|
||||
/// en: 'Seed harvest'
|
||||
String get seedHarvest => 'Seed harvest';
|
||||
|
||||
/// en: '—'
|
||||
String get unset => '—';
|
||||
}
|
||||
|
||||
// Path: needsReproduction
|
||||
class Translations$needsReproduction$en {
|
||||
Translations$needsReproduction$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'To regrow this season'
|
||||
String get label => 'To regrow this season';
|
||||
|
||||
/// en: 'Grow it out before the seed runs out'
|
||||
String get hint => 'Grow it out before the seed runs out';
|
||||
|
||||
/// en: 'To regrow'
|
||||
String get badge => 'To regrow';
|
||||
}
|
||||
|
||||
// Path: preservation
|
||||
class Translations$preservation$en {
|
||||
Translations$preservation$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'How it's kept'
|
||||
String get add => 'How it\'s kept';
|
||||
|
||||
/// en: 'How it's kept'
|
||||
String get title => 'How it\'s kept';
|
||||
|
||||
/// en: 'Unspecified'
|
||||
String get none => 'Unspecified';
|
||||
|
||||
/// en: 'Jar with drying agent'
|
||||
String get jarWithDesiccant => 'Jar with drying agent';
|
||||
|
||||
/// en: 'Glass jar'
|
||||
String get glassJar => 'Glass jar';
|
||||
|
||||
/// en: 'Paper envelope'
|
||||
String get paperEnvelope => 'Paper envelope';
|
||||
|
||||
/// en: 'Paper bag'
|
||||
String get paperBag => 'Paper bag';
|
||||
|
||||
/// en: 'Plastic bag'
|
||||
String get plasticBag => 'Plastic bag';
|
||||
}
|
||||
|
||||
// Path: conditionCheck
|
||||
class Translations$conditionCheck$en {
|
||||
Translations$conditionCheck$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Storage & seed-bank details'
|
||||
String get advanced => 'Storage & seed-bank details';
|
||||
|
||||
/// en: 'Storage checks'
|
||||
String get title => 'Storage checks';
|
||||
|
||||
/// en: 'Add check'
|
||||
String get add => 'Add check';
|
||||
|
||||
/// en: 'Jars / containers'
|
||||
String get containers => 'Jars / containers';
|
||||
|
||||
/// en: 'Drying agent'
|
||||
String get desiccant => 'Drying agent';
|
||||
|
||||
/// en: 'No storage checks yet.'
|
||||
String get none => 'No storage checks yet.';
|
||||
|
||||
/// en: '{count} jar(s) · {state}'
|
||||
String summary({required Object count, required Object state}) => '${count} jar(s) · ${state}';
|
||||
}
|
||||
|
||||
// Path: desiccant
|
||||
class Translations$desiccant$en {
|
||||
Translations$desiccant$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'None'
|
||||
String get none => 'None';
|
||||
|
||||
/// en: 'Add some'
|
||||
String get add => 'Add some';
|
||||
|
||||
/// en: 'Replace it'
|
||||
String get replace => 'Replace it';
|
||||
|
||||
/// en: 'Blue — dry'
|
||||
String get dry => 'Blue — dry';
|
||||
|
||||
/// en: 'Just renewed'
|
||||
String get fresh => 'Just renewed';
|
||||
}
|
||||
|
||||
// Path: unit
|
||||
class Translations$unit$en {
|
||||
Translations$unit$en.internal(this._root);
|
||||
|
|
@ -1165,24 +1438,24 @@ extension on Translations {
|
|||
'settings.about' => 'About',
|
||||
'settings.aboutText' => 'Local-first, encrypted inventory for traditional seeds. AGPL-3.0.',
|
||||
'settings.aboutOpen' => 'About Tanemaki',
|
||||
'backup.title' => 'Backup',
|
||||
'backup.exportCsv' => 'Export as CSV',
|
||||
'backup.exportCsvSubtitle' => 'For spreadsheets — no photos',
|
||||
'backup.exportJson' => 'Export as JSON',
|
||||
'backup.exportJsonSubtitle' => 'Complete copy, can be imported back',
|
||||
'backup.importJson' => 'Import from JSON',
|
||||
'backup.importJsonSubtitle' => 'Merge a saved copy into this inventory',
|
||||
'backup.importCsv' => 'Import from CSV',
|
||||
'backup.importCsvSubtitle' => 'Add a list from a spreadsheet',
|
||||
'backup.importConfirmTitle' => 'Import a saved copy?',
|
||||
'backup.title' => 'Backup & restore',
|
||||
'backup.exportJson' => 'Save a backup',
|
||||
'backup.exportJsonSubtitle' => 'A complete copy to keep safe, restore later or move to another device',
|
||||
'backup.importJson' => 'Restore a backup',
|
||||
'backup.importJsonSubtitle' => 'Bring a saved copy back — nothing gets duplicated',
|
||||
'backup.exportCsv' => 'Export to a spreadsheet',
|
||||
'backup.exportCsvSubtitle' => 'A simple list for Excel or LibreOffice — no photos',
|
||||
'backup.importCsv' => 'Import a list',
|
||||
'backup.importCsvSubtitle' => 'Add entries from a spreadsheet',
|
||||
'backup.importConfirmTitle' => 'Restore a backup?',
|
||||
'backup.importConfirmBody' => 'Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.',
|
||||
'backup.importCsvConfirmTitle' => 'Import a CSV list?',
|
||||
'backup.importCsvConfirmTitle' => 'Import a list?',
|
||||
'backup.importCsvConfirmBody' => 'Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.',
|
||||
'backup.importAction' => 'Import',
|
||||
'backup.exportSaved' => 'Copy saved',
|
||||
'backup.cancelled' => 'Cancelled',
|
||||
'backup.importDone' => ({required Object added, required Object updated}) => 'Imported: ${added} new, ${updated} updated',
|
||||
'backup.importCsvDone' => ({required Object count}) => 'Imported ${count} from CSV',
|
||||
'backup.importCsvDone' => ({required Object count}) => 'Added ${count} entries',
|
||||
'backup.importFailed' => 'This file could not be read as a Tanemaki copy',
|
||||
'backup.failed' => 'Something went wrong',
|
||||
'about.title' => 'About',
|
||||
|
|
@ -1216,6 +1489,16 @@ extension on Translations {
|
|||
'inventory.noMatches' => 'No seeds match your filters.',
|
||||
'inventory.clearFilters' => 'Clear filters',
|
||||
'inventory.uncategorized' => 'Uncategorized',
|
||||
'inventory.needsReproductionFilter' => 'To regrow',
|
||||
'draft.capture' => 'Capture photos',
|
||||
'draft.captured' => ({required Object n}) => '${n} captured to catalogue',
|
||||
'draft.triageTitle' => 'To catalogue',
|
||||
'draft.triageCount' => ({required Object n}) => '${n} to catalogue',
|
||||
'draft.untitled' => 'Unnamed',
|
||||
'draft.nameField' => 'Name this seed',
|
||||
'draft.nameHint' => 'What is it?',
|
||||
'draft.suggestFromPhoto' => 'Suggest name from photo',
|
||||
'draft.discard' => 'Discard',
|
||||
'quickAdd.title' => 'Add a seed',
|
||||
'quickAdd.labelField' => 'Name',
|
||||
'quickAdd.labelRequired' => 'Give it a name',
|
||||
|
|
@ -1247,12 +1530,18 @@ extension on Translations {
|
|||
'germination.germinated' => 'Germinated',
|
||||
'germination.none' => 'No germination tests yet.',
|
||||
'germination.result' => ({required Object percent}) => '${percent}%',
|
||||
'viability.expiringSoon' => 'Use or reproduce this season',
|
||||
'viability.expiringSoonYears' => ({required Object years}) => 'Use or reproduce this season · keeps ~${years} yr',
|
||||
'viability.expired' => 'Past typical viability — reproduce',
|
||||
'viability.expiredYears' => ({required Object years}) => 'Past typical viability (~${years} yr) — reproduce',
|
||||
'editVariety.title' => 'Edit seed',
|
||||
'editVariety.name' => 'Name',
|
||||
'editVariety.category' => 'Category',
|
||||
'editVariety.notes' => 'Notes',
|
||||
'editVariety.species' => 'Species (from catalog)',
|
||||
'editVariety.speciesHint' => 'Search a species…',
|
||||
'editVariety.organic' => 'Organic',
|
||||
'editVariety.organicHint' => 'Grown organically (eco)',
|
||||
'addLot.title' => 'Add lot',
|
||||
'addLot.year' => 'Harvest date',
|
||||
'addLot.quantity' => 'How much?',
|
||||
|
|
@ -1285,6 +1574,51 @@ extension on Translations {
|
|||
'presentation.plug' => 'Plug',
|
||||
'presentation.bareRoot' => 'Bare-root',
|
||||
'presentation.rootBall' => 'Root-ball',
|
||||
'provenance.section' => 'Where it\'s from',
|
||||
'provenance.seedsFrom' => 'Seeds from',
|
||||
'provenance.seedsFromHint' => 'Who grew or gave them',
|
||||
'provenance.place' => 'Place',
|
||||
'provenance.placeHint' => 'Where they come from (with region)',
|
||||
'provenance.addSeedsFrom' => 'Seeds from',
|
||||
'provenance.addPlace' => 'Place',
|
||||
'abundance.add' => 'How much I have',
|
||||
'abundance.title' => 'How much I have',
|
||||
'abundance.none' => 'Not set',
|
||||
'abundance.plentyToShare' => 'Plenty to share',
|
||||
'abundance.enoughToShare' => 'Enough to share a little',
|
||||
'abundance.enoughForMe' => 'Enough for me',
|
||||
'abundance.runningLow' => 'Running low',
|
||||
'cropCalendar.add' => 'Crop calendar',
|
||||
'cropCalendar.title' => 'Crop calendar',
|
||||
'cropCalendar.sow' => 'Sow',
|
||||
'cropCalendar.transplant' => 'Transplant',
|
||||
'cropCalendar.flowering' => 'Flowering',
|
||||
'cropCalendar.fruiting' => 'Fruiting',
|
||||
'cropCalendar.seedHarvest' => 'Seed harvest',
|
||||
'cropCalendar.unset' => '—',
|
||||
'needsReproduction.label' => 'To regrow this season',
|
||||
'needsReproduction.hint' => 'Grow it out before the seed runs out',
|
||||
'needsReproduction.badge' => 'To regrow',
|
||||
'preservation.add' => 'How it\'s kept',
|
||||
'preservation.title' => 'How it\'s kept',
|
||||
'preservation.none' => 'Unspecified',
|
||||
'preservation.jarWithDesiccant' => 'Jar with drying agent',
|
||||
'preservation.glassJar' => 'Glass jar',
|
||||
'preservation.paperEnvelope' => 'Paper envelope',
|
||||
'preservation.paperBag' => 'Paper bag',
|
||||
'preservation.plasticBag' => 'Plastic bag',
|
||||
'conditionCheck.advanced' => 'Storage & seed-bank details',
|
||||
'conditionCheck.title' => 'Storage checks',
|
||||
'conditionCheck.add' => 'Add check',
|
||||
'conditionCheck.containers' => 'Jars / containers',
|
||||
'conditionCheck.desiccant' => 'Drying agent',
|
||||
'conditionCheck.none' => 'No storage checks yet.',
|
||||
'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} jar(s) · ${state}',
|
||||
'desiccant.none' => 'None',
|
||||
'desiccant.add' => 'Add some',
|
||||
'desiccant.replace' => 'Replace it',
|
||||
'desiccant.dry' => 'Blue — dry',
|
||||
'desiccant.fresh' => 'Just renewed',
|
||||
'unit.aFew' => 'a few',
|
||||
'unit.some' => 'some',
|
||||
'unit.plenty' => 'plenty',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue