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
|
|
@ -48,6 +48,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
|||
late final Translations$settings$en settings = Translations$settings$en.internal(_root);
|
||||
late final Translations$backup$en backup = Translations$backup$en.internal(_root);
|
||||
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$quickAdd$en quickAdd = Translations$quickAdd$en.internal(_root);
|
||||
late final Translations$detail$en detail = Translations$detail$en.internal(_root);
|
||||
|
|
@ -239,12 +240,24 @@ class Translations$backup$en {
|
|||
/// en: 'Merge a saved copy into this inventory'
|
||||
String get importJsonSubtitle => 'Merge a saved copy into this inventory';
|
||||
|
||||
/// en: 'Import from CSV'
|
||||
String get importCsv => 'Import from CSV';
|
||||
|
||||
/// en: 'Add a list from a spreadsheet'
|
||||
String get importCsvSubtitle => 'Add a list from a spreadsheet';
|
||||
|
||||
/// en: 'Import a saved copy?'
|
||||
String get importConfirmTitle => 'Import a saved copy?';
|
||||
|
||||
/// 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: '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.';
|
||||
|
||||
/// en: 'Import'
|
||||
String get importAction => 'Import';
|
||||
|
||||
|
|
@ -257,6 +270,9 @@ 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: 'This file could not be read as a Tanemaki copy'
|
||||
String get importFailed => 'This file could not be read as a Tanemaki copy';
|
||||
|
||||
|
|
@ -306,6 +322,29 @@ class Translations$about$en {
|
|||
String get openSourceLicensesSubtitle => 'Third-party libraries and their licenses';
|
||||
}
|
||||
|
||||
// Path: intro
|
||||
class Translations$intro$en {
|
||||
Translations$intro$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Skip'
|
||||
String get skip => 'Skip';
|
||||
|
||||
/// en: 'Next'
|
||||
String get next => 'Next';
|
||||
|
||||
/// en: 'Get started'
|
||||
String get start => 'Get started';
|
||||
|
||||
/// en: 'How Tanemaki works'
|
||||
String get menuEntry => 'How Tanemaki works';
|
||||
|
||||
late final Translations$intro$slides$en slides = Translations$intro$slides$en.internal(_root);
|
||||
}
|
||||
|
||||
// Path: inventory
|
||||
class Translations$inventory$en {
|
||||
Translations$inventory$en.internal(this._root);
|
||||
|
|
@ -631,6 +670,20 @@ class Translations$unit$en {
|
|||
late final Translations$unit$count$en count = Translations$unit$count$en.internal(_root);
|
||||
}
|
||||
|
||||
// Path: intro.slides
|
||||
class Translations$intro$slides$en {
|
||||
Translations$intro$slides$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
late final Translations$intro$slides$welcome$en welcome = Translations$intro$slides$welcome$en.internal(_root);
|
||||
late final Translations$intro$slides$inventory$en inventory = Translations$intro$slides$inventory$en.internal(_root);
|
||||
late final Translations$intro$slides$privacy$en privacy = Translations$intro$slides$privacy$en.internal(_root);
|
||||
late final Translations$intro$slides$share$en share = Translations$intro$slides$share$en.internal(_root);
|
||||
late final Translations$intro$slides$plantare$en plantare = Translations$intro$slides$plantare$en.internal(_root);
|
||||
}
|
||||
|
||||
// Path: unit.handful
|
||||
class Translations$unit$handful$en {
|
||||
Translations$unit$handful$en.internal(this._root);
|
||||
|
|
@ -991,6 +1044,81 @@ class Translations$unit$count$en {
|
|||
String get plural => 'seeds';
|
||||
}
|
||||
|
||||
// Path: intro.slides.welcome
|
||||
class Translations$intro$slides$welcome$en {
|
||||
Translations$intro$slides$welcome$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'The seed that brought you here'
|
||||
String get title => 'The seed that brought you here';
|
||||
|
||||
/// en: 'Every traditional seed is a letter written by thousands of generations, passed from hand to hand. We are what we are thanks to that sharing.'
|
||||
String get body => 'Every traditional seed is a letter written by thousands of generations, passed from hand to hand. We are what we are thanks to that sharing.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.inventory
|
||||
class Translations$intro$slides$inventory$en {
|
||||
Translations$intro$slides$inventory$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Your seed bank, in your pocket'
|
||||
String get title => 'Your seed bank, in your pocket';
|
||||
|
||||
/// en: 'Note what you have, from which year, how much and where it came from — with the name you use. A photo and a name are enough to start.'
|
||||
String get body => 'Note what you have, from which year, how much and where it came from — with the name you use. A photo and a name are enough to start.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.privacy
|
||||
class Translations$intro$slides$privacy$en {
|
||||
Translations$intro$slides$privacy$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Yours, and only yours'
|
||||
String get title => 'Yours, and only yours';
|
||||
|
||||
/// en: 'No account, no internet, no trackers. Your data lives encrypted on your device, and only what you choose is ever shared.'
|
||||
String get body => 'No account, no internet, no trackers. Your data lives encrypted on your device, and only what you choose is ever shared.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.share
|
||||
class Translations$intro$slides$share$en {
|
||||
Translations$intro$slides$share$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'Share, the way it's always been done'
|
||||
String get title => 'Share, the way it\'s always been done';
|
||||
|
||||
/// en: 'Offer what you have spare — gift, swap or sell — and let someone nearby find it. Only an approximate distance is shown, never your address; you close the deal in person.'
|
||||
String get body => 'Offer what you have spare — gift, swap or sell — and let someone nearby find it. Only an approximate distance is shown, never your address; you close the deal in person.';
|
||||
}
|
||||
|
||||
// Path: intro.slides.plantare
|
||||
class Translations$intro$slides$plantare$en {
|
||||
Translations$intro$slides$plantare$en.internal(this._root);
|
||||
|
||||
final Translations _root; // ignore: unused_field
|
||||
|
||||
// Translations
|
||||
|
||||
/// en: 'To sow is to multiply'
|
||||
String get title => 'To sow is to multiply';
|
||||
|
||||
/// en: 'With a Plantare, whoever receives seed promises to return some later. And since returning it means growing it, every loan multiplies the commons.'
|
||||
String get body => 'With a Plantare, whoever receives seed promises to return some later. And since returning it means growing it, every loan multiplies the commons.';
|
||||
}
|
||||
|
||||
/// The flat map containing all translations for locale <en>.
|
||||
/// Only for edge cases! For simple maps, use the map function of this library.
|
||||
///
|
||||
|
|
@ -1038,12 +1166,17 @@ extension on Translations {
|
|||
'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.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.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.importFailed' => 'This file could not be read as a Tanemaki copy',
|
||||
'backup.failed' => 'Something went wrong',
|
||||
'about.title' => 'About',
|
||||
|
|
@ -1057,6 +1190,20 @@ extension on Translations {
|
|||
'about.website' => 'Website',
|
||||
'about.openSourceLicenses' => 'Open-source licenses',
|
||||
'about.openSourceLicensesSubtitle' => 'Third-party libraries and their licenses',
|
||||
'intro.skip' => 'Skip',
|
||||
'intro.next' => 'Next',
|
||||
'intro.start' => 'Get started',
|
||||
'intro.menuEntry' => 'How Tanemaki works',
|
||||
'intro.slides.welcome.title' => 'The seed that brought you here',
|
||||
'intro.slides.welcome.body' => 'Every traditional seed is a letter written by thousands of generations, passed from hand to hand. We are what we are thanks to that sharing.',
|
||||
'intro.slides.inventory.title' => 'Your seed bank, in your pocket',
|
||||
'intro.slides.inventory.body' => 'Note what you have, from which year, how much and where it came from — with the name you use. A photo and a name are enough to start.',
|
||||
'intro.slides.privacy.title' => 'Yours, and only yours',
|
||||
'intro.slides.privacy.body' => 'No account, no internet, no trackers. Your data lives encrypted on your device, and only what you choose is ever shared.',
|
||||
'intro.slides.share.title' => 'Share, the way it\'s always been done',
|
||||
'intro.slides.share.body' => 'Offer what you have spare — gift, swap or sell — and let someone nearby find it. Only an approximate distance is shown, never your address; you close the deal in person.',
|
||||
'intro.slides.plantare.title' => 'To sow is to multiply',
|
||||
'intro.slides.plantare.body' => 'With a Plantare, whoever receives seed promises to return some later. And since returning it means growing it, every loan multiplies the commons.',
|
||||
'inventory.title' => 'Inventory',
|
||||
'inventory.searchHint' => 'Search seeds',
|
||||
'inventory.empty' => 'No seeds yet. Tap + to add your first.',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue