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:
vjrj 2026-07-09 21:23:46 +02:00
parent 12a2ee2d64
commit 6809dc6143
89 changed files with 17141 additions and 228 deletions

View file

@ -44,24 +44,24 @@
"aboutOpen": "About Tanemaki"
},
"backup": {
"title": "Backup",
"exportCsv": "Export as CSV",
"exportCsvSubtitle": "For spreadsheets — no photos",
"exportJson": "Export as JSON",
"exportJsonSubtitle": "Complete copy, can be imported back",
"importJson": "Import from JSON",
"importJsonSubtitle": "Merge a saved copy into this inventory",
"importCsv": "Import from CSV",
"importCsvSubtitle": "Add a list from a spreadsheet",
"importConfirmTitle": "Import a saved copy?",
"title": "Backup & restore",
"exportJson": "Save a backup",
"exportJsonSubtitle": "A complete copy to keep safe, restore later or move to another device",
"importJson": "Restore a backup",
"importJsonSubtitle": "Bring a saved copy back — nothing gets duplicated",
"exportCsv": "Export to a spreadsheet",
"exportCsvSubtitle": "A simple list for Excel or LibreOffice — no photos",
"importCsv": "Import a list",
"importCsvSubtitle": "Add entries from a spreadsheet",
"importConfirmTitle": "Restore a backup?",
"importConfirmBody": "Entries merge with your inventory; when the same entry exists on both sides, the newest version wins. Nothing gets duplicated.",
"importCsvConfirmTitle": "Import a CSV list?",
"importCsvConfirmTitle": "Import a list?",
"importCsvConfirmBody": "Every row is added as a new entry. This does not merge or replace, so importing the same file twice adds it twice.",
"importAction": "Import",
"exportSaved": "Copy saved",
"cancelled": "Cancelled",
"importDone": "Imported: {added} new, {updated} updated",
"importCsvDone": "Imported {count} from CSV",
"importCsvDone": "Added {count} entries",
"importFailed": "This file could not be read as a Tanemaki copy",
"failed": "Something went wrong"
},
@ -112,7 +112,19 @@
"empty": "No seeds yet. Tap + to add your first.",
"noMatches": "No seeds match your filters.",
"clearFilters": "Clear filters",
"uncategorized": "Uncategorized"
"uncategorized": "Uncategorized",
"needsReproductionFilter": "To regrow"
},
"draft": {
"capture": "Capture photos",
"captured": "{n} captured to catalogue",
"triageTitle": "To catalogue",
"triageCount": "{n} to catalogue",
"untitled": "Unnamed",
"nameField": "Name this seed",
"nameHint": "What is it?",
"suggestFromPhoto": "Suggest name from photo",
"discard": "Discard"
},
"quickAdd": {
"title": "Add a seed",
@ -151,13 +163,21 @@
"none": "No germination tests yet.",
"result": "{percent}%"
},
"viability": {
"expiringSoon": "Use or reproduce this season",
"expiringSoonYears": "Use or reproduce this season · keeps ~{years} yr",
"expired": "Past typical viability — reproduce",
"expiredYears": "Past typical viability (~{years} yr) — reproduce"
},
"editVariety": {
"title": "Edit seed",
"name": "Name",
"category": "Category",
"notes": "Notes",
"species": "Species (from catalog)",
"speciesHint": "Search a species…"
"speciesHint": "Search a species…",
"organic": "Organic",
"organicHint": "Grown organically (eco)"
},
"addLot": {
"title": "Add lot",
@ -191,6 +211,65 @@
"bareRoot": "Bare-root",
"rootBall": "Root-ball"
},
"provenance": {
"section": "Where it's from",
"seedsFrom": "Seeds from",
"seedsFromHint": "Who grew or gave them",
"place": "Place",
"placeHint": "Where they come from (with region)",
"addSeedsFrom": "Seeds from",
"addPlace": "Place"
},
"abundance": {
"add": "How much I have",
"title": "How much I have",
"none": "Not set",
"plentyToShare": "Plenty to share",
"enoughToShare": "Enough to share a little",
"enoughForMe": "Enough for me",
"runningLow": "Running low"
},
"cropCalendar": {
"add": "Crop calendar",
"title": "Crop calendar",
"sow": "Sow",
"transplant": "Transplant",
"flowering": "Flowering",
"fruiting": "Fruiting",
"seedHarvest": "Seed harvest",
"unset": "—"
},
"needsReproduction": {
"label": "To regrow this season",
"hint": "Grow it out before the seed runs out",
"badge": "To regrow"
},
"preservation": {
"add": "How it's kept",
"title": "How it's kept",
"none": "Unspecified",
"jarWithDesiccant": "Jar with drying agent",
"glassJar": "Glass jar",
"paperEnvelope": "Paper envelope",
"paperBag": "Paper bag",
"plasticBag": "Plastic bag"
},
"conditionCheck": {
"advanced": "Storage & seed-bank details",
"title": "Storage checks",
"add": "Add check",
"containers": "Jars / containers",
"desiccant": "Drying agent",
"none": "No storage checks yet.",
"summary": "{count} jar(s) · {state}"
},
"desiccant": {
"none": "None",
"add": "Add some",
"replace": "Replace it",
"dry": "Blue — dry",
"fresh": "Just renewed"
},
"unit": {
"aFew": "a few",
"some": "some",

View file

@ -45,23 +45,23 @@
},
"backup": {
"title": "Copia de seguridad",
"exportCsv": "Exportar a CSV",
"exportCsvSubtitle": "Para hojas de cálculo — sin fotos",
"exportJson": "Exportar a JSON",
"exportJsonSubtitle": "Copia completa, se puede volver a importar",
"importJson": "Importar desde JSON",
"importJsonSubtitle": "Fusiona una copia guardada con este inventario",
"importCsv": "Importar desde CSV",
"importCsvSubtitle": "Añade una lista desde una hoja de cálculo",
"importConfirmTitle": "¿Importar una copia guardada?",
"exportJson": "Guardar una copia de seguridad",
"exportJsonSubtitle": "Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo",
"importJson": "Restaurar una copia",
"importJsonSubtitle": "Recupera una copia guardada — no se duplica nada",
"exportCsv": "Exportar a una hoja de cálculo",
"exportCsvSubtitle": "Una lista sencilla para Excel o LibreOffice — sin fotos",
"importCsv": "Importar una lista",
"importCsvSubtitle": "Añade entradas desde una hoja de cálculo",
"importConfirmTitle": "¿Restaurar una copia?",
"importConfirmBody": "Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.",
"importCsvConfirmTitle": "¿Importar una lista CSV?",
"importCsvConfirmTitle": "¿Importar una lista?",
"importCsvConfirmBody": "Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.",
"importAction": "Importar",
"exportSaved": "Copia guardada",
"cancelled": "Cancelado",
"importDone": "Importado: {added} nuevas, {updated} actualizadas",
"importCsvDone": "Importadas {count} desde CSV",
"importCsvDone": "Añadidas {count} entradas",
"importFailed": "Este fichero no se pudo leer como una copia de Tanemaki",
"failed": "Algo ha salido mal"
},
@ -112,7 +112,19 @@
"empty": "Aún no hay semillas. Toca + para añadir la primera.",
"noMatches": "Ninguna semilla coincide con los filtros.",
"clearFilters": "Quitar filtros",
"uncategorized": "Sin categoría"
"uncategorized": "Sin categoría",
"needsReproductionFilter": "Por reproducir"
},
"draft": {
"capture": "Capturar fotos",
"captured": "{n} capturadas por catalogar",
"triageTitle": "Por catalogar",
"triageCount": "{n} por catalogar",
"untitled": "Sin nombre",
"nameField": "Nombra esta semilla",
"nameHint": "¿Qué es?",
"suggestFromPhoto": "Sugerir nombre de la foto",
"discard": "Descartar"
},
"quickAdd": {
"title": "Añadir una semilla",
@ -151,13 +163,21 @@
"none": "Aún no hay pruebas de germinación.",
"result": "{percent}%"
},
"viability": {
"expiringSoon": "Úsala o multiplícala esta temporada",
"expiringSoonYears": "Úsala o multiplícala esta temporada · dura ~{years} años",
"expired": "Supera su viabilidad típica — a multiplicar",
"expiredYears": "Supera su viabilidad típica (~{years} años) — a multiplicar"
},
"editVariety": {
"title": "Editar semilla",
"name": "Nombre",
"category": "Categoría",
"notes": "Notas",
"species": "Especie (del catálogo)",
"speciesHint": "Buscar una especie…"
"speciesHint": "Buscar una especie…",
"organic": "Ecológica",
"organicHint": "Cultivada de forma ecológica (eco)"
},
"addLot": {
"title": "Añadir lote",
@ -191,6 +211,65 @@
"bareRoot": "Raíz desnuda",
"rootBall": "Cepellón"
},
"provenance": {
"section": "De dónde viene",
"seedsFrom": "Semillas de",
"seedsFromHint": "Quién las cultivó o las dio",
"place": "Lugar",
"placeHint": "De dónde vienen (con provincia)",
"addSeedsFrom": "Semillas de",
"addPlace": "Lugar"
},
"abundance": {
"add": "Cuánta tengo",
"title": "Cuánta tengo",
"none": "Sin indicar",
"plentyToShare": "De sobra para compartir",
"enoughToShare": "Bastante, para compartir con moderación",
"enoughForMe": "Suficiente para mí",
"runningLow": "Queda poca"
},
"cropCalendar": {
"add": "Calendario de cultivo",
"title": "Calendario de cultivo",
"sow": "Siembra",
"transplant": "Trasplante",
"flowering": "Floración",
"fruiting": "Fructificación",
"seedHarvest": "Cosecha de semilla",
"unset": "—"
},
"needsReproduction": {
"label": "Reproducir esta temporada",
"hint": "Cultívala antes de que se acabe la semilla",
"badge": "Por reproducir"
},
"preservation": {
"add": "Cómo se conserva",
"title": "Cómo se conserva",
"none": "Sin especificar",
"jarWithDesiccant": "Bote con desecante",
"glassJar": "Bote de cristal",
"paperEnvelope": "Sobre de papel",
"paperBag": "Bolsa de papel",
"plasticBag": "Bolsa de plástico"
},
"conditionCheck": {
"advanced": "Conservación y detalles del banco",
"title": "Revisiones de conservación",
"add": "Añadir revisión",
"containers": "Botes / recipientes",
"desiccant": "Desecante",
"none": "Aún no hay revisiones.",
"summary": "{count} bote(s) · {state}"
},
"desiccant": {
"none": "No tiene",
"add": "Se pone",
"replace": "Se cambia",
"dry": "Azul — seco",
"fresh": "Recién puesto"
},
"unit": {
"aFew": "unas pocas",
"some": "algunas",

View file

@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 408 (204 per locale)
/// Strings: 530 (265 per locale)
///
/// Built on 2026-07-09 at 12:44 UTC
/// Built on 2026-07-09 at 17:18 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View file

@ -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',

View file

@ -49,14 +49,23 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$about$es about = _Translations$about$es._(_root);
@override late final _Translations$intro$es intro = _Translations$intro$es._(_root);
@override late final _Translations$inventory$es inventory = _Translations$inventory$es._(_root);
@override late final _Translations$draft$es draft = _Translations$draft$es._(_root);
@override late final _Translations$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
@override late final _Translations$germination$es germination = _Translations$germination$es._(_root);
@override late final _Translations$viability$es viability = _Translations$viability$es._(_root);
@override late final _Translations$editVariety$es editVariety = _Translations$editVariety$es._(_root);
@override late final _Translations$addLot$es addLot = _Translations$addLot$es._(_root);
@override late final _Translations$harvest$es harvest = _Translations$harvest$es._(_root);
@override late final _Translations$lotType$es lotType = _Translations$lotType$es._(_root);
@override late final _Translations$presentation$es presentation = _Translations$presentation$es._(_root);
@override late final _Translations$provenance$es provenance = _Translations$provenance$es._(_root);
@override late final _Translations$abundance$es abundance = _Translations$abundance$es._(_root);
@override late final _Translations$cropCalendar$es cropCalendar = _Translations$cropCalendar$es._(_root);
@override late final _Translations$needsReproduction$es needsReproduction = _Translations$needsReproduction$es._(_root);
@override late final _Translations$preservation$es preservation = _Translations$preservation$es._(_root);
@override late final _Translations$conditionCheck$es conditionCheck = _Translations$conditionCheck$es._(_root);
@override late final _Translations$desiccant$es desiccant = _Translations$desiccant$es._(_root);
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
}
@ -154,23 +163,23 @@ class _Translations$backup$es extends Translations$backup$en {
// Translations
@override String get title => 'Copia de seguridad';
@override String get exportCsv => 'Exportar a CSV';
@override String get exportCsvSubtitle => 'Para hojas de cálculo — sin fotos';
@override String get exportJson => 'Exportar a JSON';
@override String get exportJsonSubtitle => 'Copia completa, se puede volver a importar';
@override String get importJson => 'Importar desde JSON';
@override String get importJsonSubtitle => 'Fusiona una copia guardada con este inventario';
@override String get importCsv => 'Importar desde CSV';
@override String get importCsvSubtitle => 'Añade una lista desde una hoja de cálculo';
@override String get importConfirmTitle => '¿Importar una copia guardada?';
@override String get exportJson => 'Guardar una copia de seguridad';
@override String get exportJsonSubtitle => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo';
@override String get importJson => 'Restaurar una copia';
@override String get importJsonSubtitle => 'Recupera una copia guardada — no se duplica nada';
@override String get exportCsv => 'Exportar a una hoja de cálculo';
@override String get exportCsvSubtitle => 'Una lista sencilla para Excel o LibreOffice — sin fotos';
@override String get importCsv => 'Importar una lista';
@override String get importCsvSubtitle => 'Añade entradas desde una hoja de cálculo';
@override String get importConfirmTitle => '¿Restaurar una copia?';
@override String get importConfirmBody => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.';
@override String get importCsvConfirmTitle => '¿Importar una lista CSV?';
@override String get importCsvConfirmTitle => '¿Importar una lista?';
@override String get importCsvConfirmBody => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.';
@override String get importAction => 'Importar';
@override String get exportSaved => 'Copia guardada';
@override String get cancelled => 'Cancelado';
@override String importDone({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas';
@override String importCsvDone({required Object count}) => 'Importadas ${count} desde CSV';
@override String importCsvDone({required Object count}) => 'Añadidas ${count} entradas';
@override String get importFailed => 'Este fichero no se pudo leer como una copia de Tanemaki';
@override String get failed => 'Algo ha salido mal';
}
@ -222,6 +231,25 @@ class _Translations$inventory$es extends Translations$inventory$en {
@override String get noMatches => 'Ninguna semilla coincide con los filtros.';
@override String get clearFilters => 'Quitar filtros';
@override String get uncategorized => 'Sin categoría';
@override String get needsReproductionFilter => 'Por reproducir';
}
// Path: draft
class _Translations$draft$es extends Translations$draft$en {
_Translations$draft$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get capture => 'Capturar fotos';
@override String captured({required Object n}) => '${n} capturadas por catalogar';
@override String get triageTitle => 'Por catalogar';
@override String triageCount({required Object n}) => '${n} por catalogar';
@override String get untitled => 'Sin nombre';
@override String get nameField => 'Nombra esta semilla';
@override String get nameHint => '¿Qué es?';
@override String get suggestFromPhoto => 'Sugerir nombre de la foto';
@override String get discard => 'Descartar';
}
// Path: quickAdd
@ -282,6 +310,19 @@ class _Translations$germination$es extends Translations$germination$en {
@override String result({required Object percent}) => '${percent}%';
}
// Path: viability
class _Translations$viability$es extends Translations$viability$en {
_Translations$viability$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get expiringSoon => 'Úsala o multiplícala esta temporada';
@override String expiringSoonYears({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años';
@override String get expired => 'Supera su viabilidad típica — a multiplicar';
@override String expiredYears({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar';
}
// Path: editVariety
class _Translations$editVariety$es extends Translations$editVariety$en {
_Translations$editVariety$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -295,6 +336,8 @@ class _Translations$editVariety$es extends Translations$editVariety$en {
@override String get notes => 'Notas';
@override String get species => 'Especie (del catálogo)';
@override String get speciesHint => 'Buscar una especie…';
@override String get organic => 'Ecológica';
@override String get organicHint => 'Cultivada de forma ecológica (eco)';
}
// Path: addLot
@ -367,6 +410,114 @@ class _Translations$presentation$es extends Translations$presentation$en {
@override String get rootBall => 'Cepellón';
}
// Path: provenance
class _Translations$provenance$es extends Translations$provenance$en {
_Translations$provenance$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get section => 'De dónde viene';
@override String get seedsFrom => 'Semillas de';
@override String get seedsFromHint => 'Quién las cultivó o las dio';
@override String get place => 'Lugar';
@override String get placeHint => 'De dónde vienen (con provincia)';
@override String get addSeedsFrom => 'Semillas de';
@override String get addPlace => 'Lugar';
}
// Path: abundance
class _Translations$abundance$es extends Translations$abundance$en {
_Translations$abundance$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get add => 'Cuánta tengo';
@override String get title => 'Cuánta tengo';
@override String get none => 'Sin indicar';
@override String get plentyToShare => 'De sobra para compartir';
@override String get enoughToShare => 'Bastante, para compartir con moderación';
@override String get enoughForMe => 'Suficiente para mí';
@override String get runningLow => 'Queda poca';
}
// Path: cropCalendar
class _Translations$cropCalendar$es extends Translations$cropCalendar$en {
_Translations$cropCalendar$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get add => 'Calendario de cultivo';
@override String get title => 'Calendario de cultivo';
@override String get sow => 'Siembra';
@override String get transplant => 'Trasplante';
@override String get flowering => 'Floración';
@override String get fruiting => 'Fructificación';
@override String get seedHarvest => 'Cosecha de semilla';
@override String get unset => '';
}
// Path: needsReproduction
class _Translations$needsReproduction$es extends Translations$needsReproduction$en {
_Translations$needsReproduction$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get label => 'Reproducir esta temporada';
@override String get hint => 'Cultívala antes de que se acabe la semilla';
@override String get badge => 'Por reproducir';
}
// Path: preservation
class _Translations$preservation$es extends Translations$preservation$en {
_Translations$preservation$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get add => 'Cómo se conserva';
@override String get title => 'Cómo se conserva';
@override String get none => 'Sin especificar';
@override String get jarWithDesiccant => 'Bote con desecante';
@override String get glassJar => 'Bote de cristal';
@override String get paperEnvelope => 'Sobre de papel';
@override String get paperBag => 'Bolsa de papel';
@override String get plasticBag => 'Bolsa de plástico';
}
// Path: conditionCheck
class _Translations$conditionCheck$es extends Translations$conditionCheck$en {
_Translations$conditionCheck$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get advanced => 'Conservación y detalles del banco';
@override String get title => 'Revisiones de conservación';
@override String get add => 'Añadir revisión';
@override String get containers => 'Botes / recipientes';
@override String get desiccant => 'Desecante';
@override String get none => 'Aún no hay revisiones.';
@override String summary({required Object count, required Object state}) => '${count} bote(s) · ${state}';
}
// Path: desiccant
class _Translations$desiccant$es extends Translations$desiccant$en {
_Translations$desiccant$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get none => 'No tiene';
@override String get add => 'Se pone';
@override String get replace => 'Se cambia';
@override String get dry => 'Azul — seco';
@override String get fresh => 'Recién puesto';
}
// Path: unit
class _Translations$unit$es extends Translations$unit$en {
_Translations$unit$es._(TranslationsEs root) : this._root = root, super.internal(root);
@ -778,23 +929,23 @@ extension on TranslationsEs {
'settings.aboutText' => 'Inventario local y cifrado para semillas tradicionales. AGPL-3.0.',
'settings.aboutOpen' => 'Acerca de Tanemaki',
'backup.title' => 'Copia de seguridad',
'backup.exportCsv' => 'Exportar a CSV',
'backup.exportCsvSubtitle' => 'Para hojas de cálculo — sin fotos',
'backup.exportJson' => 'Exportar a JSON',
'backup.exportJsonSubtitle' => 'Copia completa, se puede volver a importar',
'backup.importJson' => 'Importar desde JSON',
'backup.importJsonSubtitle' => 'Fusiona una copia guardada con este inventario',
'backup.importCsv' => 'Importar desde CSV',
'backup.importCsvSubtitle' => 'Añade una lista desde una hoja de cálculo',
'backup.importConfirmTitle' => '¿Importar una copia guardada?',
'backup.exportJson' => 'Guardar una copia de seguridad',
'backup.exportJsonSubtitle' => 'Una copia completa para guardar a salvo, restaurar luego o pasar a otro dispositivo',
'backup.importJson' => 'Restaurar una copia',
'backup.importJsonSubtitle' => 'Recupera una copia guardada — no se duplica nada',
'backup.exportCsv' => 'Exportar a una hoja de cálculo',
'backup.exportCsvSubtitle' => 'Una lista sencilla para Excel o LibreOffice — sin fotos',
'backup.importCsv' => 'Importar una lista',
'backup.importCsvSubtitle' => 'Añade entradas desde una hoja de cálculo',
'backup.importConfirmTitle' => '¿Restaurar una copia?',
'backup.importConfirmBody' => 'Las entradas se fusionan con tu inventario; si una entrada existe en ambos lados, gana la versión más reciente. No se duplica nada.',
'backup.importCsvConfirmTitle' => '¿Importar una lista CSV?',
'backup.importCsvConfirmTitle' => '¿Importar una lista?',
'backup.importCsvConfirmBody' => 'Cada fila se añade como una entrada nueva. No fusiona ni reemplaza, así que importar el mismo fichero dos veces lo añade dos veces.',
'backup.importAction' => 'Importar',
'backup.exportSaved' => 'Copia guardada',
'backup.cancelled' => 'Cancelado',
'backup.importDone' => ({required Object added, required Object updated}) => 'Importado: ${added} nuevas, ${updated} actualizadas',
'backup.importCsvDone' => ({required Object count}) => 'Importadas ${count} desde CSV',
'backup.importCsvDone' => ({required Object count}) => 'Añadidas ${count} entradas',
'backup.importFailed' => 'Este fichero no se pudo leer como una copia de Tanemaki',
'backup.failed' => 'Algo ha salido mal',
'about.title' => 'Acerca de',
@ -828,6 +979,16 @@ extension on TranslationsEs {
'inventory.noMatches' => 'Ninguna semilla coincide con los filtros.',
'inventory.clearFilters' => 'Quitar filtros',
'inventory.uncategorized' => 'Sin categoría',
'inventory.needsReproductionFilter' => 'Por reproducir',
'draft.capture' => 'Capturar fotos',
'draft.captured' => ({required Object n}) => '${n} capturadas por catalogar',
'draft.triageTitle' => 'Por catalogar',
'draft.triageCount' => ({required Object n}) => '${n} por catalogar',
'draft.untitled' => 'Sin nombre',
'draft.nameField' => 'Nombra esta semilla',
'draft.nameHint' => '¿Qué es?',
'draft.suggestFromPhoto' => 'Sugerir nombre de la foto',
'draft.discard' => 'Descartar',
'quickAdd.title' => 'Añadir una semilla',
'quickAdd.labelField' => 'Nombre',
'quickAdd.labelRequired' => 'Ponle un nombre',
@ -859,12 +1020,18 @@ extension on TranslationsEs {
'germination.germinated' => 'Germinadas',
'germination.none' => 'Aún no hay pruebas de germinación.',
'germination.result' => ({required Object percent}) => '${percent}%',
'viability.expiringSoon' => 'Úsala o multiplícala esta temporada',
'viability.expiringSoonYears' => ({required Object years}) => 'Úsala o multiplícala esta temporada · dura ~${years} años',
'viability.expired' => 'Supera su viabilidad típica — a multiplicar',
'viability.expiredYears' => ({required Object years}) => 'Supera su viabilidad típica (~${years} años) — a multiplicar',
'editVariety.title' => 'Editar semilla',
'editVariety.name' => 'Nombre',
'editVariety.category' => 'Categoría',
'editVariety.notes' => 'Notas',
'editVariety.species' => 'Especie (del catálogo)',
'editVariety.speciesHint' => 'Buscar una especie…',
'editVariety.organic' => 'Ecológica',
'editVariety.organicHint' => 'Cultivada de forma ecológica (eco)',
'addLot.title' => 'Añadir lote',
'addLot.year' => 'Fecha de cosecha',
'addLot.quantity' => '¿Cuánta?',
@ -897,6 +1064,51 @@ extension on TranslationsEs {
'presentation.plug' => 'Alvéolo',
'presentation.bareRoot' => 'Raíz desnuda',
'presentation.rootBall' => 'Cepellón',
'provenance.section' => 'De dónde viene',
'provenance.seedsFrom' => 'Semillas de',
'provenance.seedsFromHint' => 'Quién las cultivó o las dio',
'provenance.place' => 'Lugar',
'provenance.placeHint' => 'De dónde vienen (con provincia)',
'provenance.addSeedsFrom' => 'Semillas de',
'provenance.addPlace' => 'Lugar',
'abundance.add' => 'Cuánta tengo',
'abundance.title' => 'Cuánta tengo',
'abundance.none' => 'Sin indicar',
'abundance.plentyToShare' => 'De sobra para compartir',
'abundance.enoughToShare' => 'Bastante, para compartir con moderación',
'abundance.enoughForMe' => 'Suficiente para mí',
'abundance.runningLow' => 'Queda poca',
'cropCalendar.add' => 'Calendario de cultivo',
'cropCalendar.title' => 'Calendario de cultivo',
'cropCalendar.sow' => 'Siembra',
'cropCalendar.transplant' => 'Trasplante',
'cropCalendar.flowering' => 'Floración',
'cropCalendar.fruiting' => 'Fructificación',
'cropCalendar.seedHarvest' => 'Cosecha de semilla',
'cropCalendar.unset' => '',
'needsReproduction.label' => 'Reproducir esta temporada',
'needsReproduction.hint' => 'Cultívala antes de que se acabe la semilla',
'needsReproduction.badge' => 'Por reproducir',
'preservation.add' => 'Cómo se conserva',
'preservation.title' => 'Cómo se conserva',
'preservation.none' => 'Sin especificar',
'preservation.jarWithDesiccant' => 'Bote con desecante',
'preservation.glassJar' => 'Bote de cristal',
'preservation.paperEnvelope' => 'Sobre de papel',
'preservation.paperBag' => 'Bolsa de papel',
'preservation.plasticBag' => 'Bolsa de plástico',
'conditionCheck.advanced' => 'Conservación y detalles del banco',
'conditionCheck.title' => 'Revisiones de conservación',
'conditionCheck.add' => 'Añadir revisión',
'conditionCheck.containers' => 'Botes / recipientes',
'conditionCheck.desiccant' => 'Desecante',
'conditionCheck.none' => 'Aún no hay revisiones.',
'conditionCheck.summary' => ({required Object count, required Object state}) => '${count} bote(s) · ${state}',
'desiccant.none' => 'No tiene',
'desiccant.add' => 'Se pone',
'desiccant.replace' => 'Se cambia',
'desiccant.dry' => 'Azul — seco',
'desiccant.fresh' => 'Recién puesto',
'unit.aFew' => 'unas pocas',
'unit.some' => 'algunas',
'unit.plenty' => 'muchas',