feat(inventory): additive CSV import for bulk digitization
Import a spreadsheet list (the same 21-column schema the CSV export already produces) as new varieties/lots. Tolerant reader: columns matched by header name in any order, only variety_label required, unknown enums fall back to a safe default, rows sharing variety_id collapse into one variety with several lots. Species re-linked by scientific name. Additive by design (fresh ids + HLC), so it never overwrites existing rows; JSON stays the canonical id-reconciling import. Adds a hand-rolled RFC 4180 parser (no new dependency), an importCsv path in VarietyRepository/ExportImportService, an 'Import from CSV' tile in the backup section, and en/es strings. Note: i18n .g.dart also carries pre-existing About/intro strings from the working tree (regenerated by slang).
This commit is contained in:
parent
21072633b5
commit
89b61bc9e4
11 changed files with 1070 additions and 2 deletions
|
|
@ -47,6 +47,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
|||
@override late final _Translations$settings$es settings = _Translations$settings$es._(_root);
|
||||
@override late final _Translations$backup$es backup = _Translations$backup$es._(_root);
|
||||
@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$quickAdd$es quickAdd = _Translations$quickAdd$es._(_root);
|
||||
@override late final _Translations$detail$es detail = _Translations$detail$es._(_root);
|
||||
|
|
@ -159,12 +160,17 @@ class _Translations$backup$es extends Translations$backup$en {
|
|||
@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 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 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 get importFailed => 'Este fichero no se pudo leer como una copia de Tanemaki';
|
||||
@override String get failed => 'Algo ha salido mal';
|
||||
}
|
||||
|
|
@ -189,6 +195,20 @@ class _Translations$about$es extends Translations$about$en {
|
|||
@override String get openSourceLicensesSubtitle => 'Bibliotecas de terceros y sus licencias';
|
||||
}
|
||||
|
||||
// Path: intro
|
||||
class _Translations$intro$es extends Translations$intro$en {
|
||||
_Translations$intro$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get skip => 'Saltar';
|
||||
@override String get next => 'Siguiente';
|
||||
@override String get start => 'Empezar';
|
||||
@override String get menuEntry => 'Cómo funciona Tanemaki';
|
||||
@override late final _Translations$intro$slides$es slides = _Translations$intro$slides$es._(_root);
|
||||
}
|
||||
|
||||
// Path: inventory
|
||||
class _Translations$inventory$es extends Translations$inventory$en {
|
||||
_Translations$inventory$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -382,6 +402,20 @@ class _Translations$unit$es extends Translations$unit$en {
|
|||
@override late final _Translations$unit$count$es count = _Translations$unit$count$es._(_root);
|
||||
}
|
||||
|
||||
// Path: intro.slides
|
||||
class _Translations$intro$slides$es extends Translations$intro$slides$en {
|
||||
_Translations$intro$slides$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override late final _Translations$intro$slides$welcome$es welcome = _Translations$intro$slides$welcome$es._(_root);
|
||||
@override late final _Translations$intro$slides$inventory$es inventory = _Translations$intro$slides$inventory$es._(_root);
|
||||
@override late final _Translations$intro$slides$privacy$es privacy = _Translations$intro$slides$privacy$es._(_root);
|
||||
@override late final _Translations$intro$slides$share$es share = _Translations$intro$slides$share$es._(_root);
|
||||
@override late final _Translations$intro$slides$plantare$es plantare = _Translations$intro$slides$plantare$es._(_root);
|
||||
}
|
||||
|
||||
// Path: unit.handful
|
||||
class _Translations$unit$handful$es extends Translations$unit$handful$en {
|
||||
_Translations$unit$handful$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
|
@ -646,6 +680,61 @@ class _Translations$unit$count$es extends Translations$unit$count$en {
|
|||
@override String get plural => 'semillas';
|
||||
}
|
||||
|
||||
// Path: intro.slides.welcome
|
||||
class _Translations$intro$slides$welcome$es extends Translations$intro$slides$welcome$en {
|
||||
_Translations$intro$slides$welcome$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'La semilla que te trajo hasta aquí';
|
||||
@override String get body => 'Cada semilla tradicional es una carta escrita por miles de generaciones, que pasó de mano en mano. Somos lo que somos gracias a ese intercambio.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.inventory
|
||||
class _Translations$intro$slides$inventory$es extends Translations$intro$slides$inventory$en {
|
||||
_Translations$intro$slides$inventory$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Tu banco de semillas, en el bolsillo';
|
||||
@override String get body => 'Apunta qué tienes, de qué año, cuánto y de dónde vino, con el nombre que tú usas. Una foto y un nombre bastan para empezar.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.privacy
|
||||
class _Translations$intro$slides$privacy$es extends Translations$intro$slides$privacy$en {
|
||||
_Translations$intro$slides$privacy$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Tuyo y solo tuyo';
|
||||
@override String get body => 'Sin cuenta, sin internet, sin rastreadores. Tus datos viven cifrados en tu dispositivo y solo se comparte lo que tú decidas.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.share
|
||||
class _Translations$intro$slides$share$es extends Translations$intro$slides$share$en {
|
||||
_Translations$intro$slides$share$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Compartir, como siempre se hizo';
|
||||
@override String get body => 'Ofrece lo que te sobra —regalo, trueque o venta— y que alguien cerca lo encuentre. Solo se muestra la distancia aproximada, nunca tu dirección; el trato lo cierras en persona.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.plantare
|
||||
class _Translations$intro$slides$plantare$es extends Translations$intro$slides$plantare$en {
|
||||
_Translations$intro$slides$plantare$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||
|
||||
final TranslationsEs _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
@override String get title => 'Sembrar es multiplicar';
|
||||
@override String get body => 'Con el Plantare, quien recibe promete devolver semilla más adelante. Y como para devolverla hay que cultivarla, cada préstamo multiplica el común.';
|
||||
}
|
||||
|
||||
/// The flat map containing all translations for locale <es>.
|
||||
/// Only for edge cases! For simple maps, use the map function of this library.
|
||||
///
|
||||
|
|
@ -693,12 +782,17 @@ extension on TranslationsEs {
|
|||
'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.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.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.importFailed' => 'Este fichero no se pudo leer como una copia de Tanemaki',
|
||||
'backup.failed' => 'Algo ha salido mal',
|
||||
'about.title' => 'Acerca de',
|
||||
|
|
@ -712,6 +806,20 @@ extension on TranslationsEs {
|
|||
'about.website' => 'Sitio web',
|
||||
'about.openSourceLicenses' => 'Licencias de código abierto',
|
||||
'about.openSourceLicensesSubtitle' => 'Bibliotecas de terceros y sus licencias',
|
||||
'intro.skip' => 'Saltar',
|
||||
'intro.next' => 'Siguiente',
|
||||
'intro.start' => 'Empezar',
|
||||
'intro.menuEntry' => 'Cómo funciona Tanemaki',
|
||||
'intro.slides.welcome.title' => 'La semilla que te trajo hasta aquí',
|
||||
'intro.slides.welcome.body' => 'Cada semilla tradicional es una carta escrita por miles de generaciones, que pasó de mano en mano. Somos lo que somos gracias a ese intercambio.',
|
||||
'intro.slides.inventory.title' => 'Tu banco de semillas, en el bolsillo',
|
||||
'intro.slides.inventory.body' => 'Apunta qué tienes, de qué año, cuánto y de dónde vino, con el nombre que tú usas. Una foto y un nombre bastan para empezar.',
|
||||
'intro.slides.privacy.title' => 'Tuyo y solo tuyo',
|
||||
'intro.slides.privacy.body' => 'Sin cuenta, sin internet, sin rastreadores. Tus datos viven cifrados en tu dispositivo y solo se comparte lo que tú decidas.',
|
||||
'intro.slides.share.title' => 'Compartir, como siempre se hizo',
|
||||
'intro.slides.share.body' => 'Ofrece lo que te sobra —regalo, trueque o venta— y que alguien cerca lo encuentre. Solo se muestra la distancia aproximada, nunca tu dirección; el trato lo cierras en persona.',
|
||||
'intro.slides.plantare.title' => 'Sembrar es multiplicar',
|
||||
'intro.slides.plantare.body' => 'Con el Plantare, quien recibe promete devolver semilla más adelante. Y como para devolverla hay que cultivarla, cada préstamo multiplica el común.',
|
||||
'inventory.title' => 'Inventario',
|
||||
'inventory.searchHint' => 'Buscar semillas',
|
||||
'inventory.empty' => 'Aún no hay semillas. Toca + para añadir la primera.',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue