mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
feat: import transactions from a CSV bank statement
Fourth of the four ideas picked from the Maybe/Sure feature audit, and the last one queued up. A 3-step wizard (account -> column mapping -> preview) at /transactions/import, parsed entirely client-side (new lib/csv.ts — delimiter auto-detect, 5 date formats, thousands/decimal-separator-aware amounts) and submitted through a new all-or-nothing POST /transactions/bulk endpoint, mirroring backup restore's validate-everything-before-touching- anything approach. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,37 @@ const ru = {
|
||||
"netWorth.form.saveError": "Не удалось сохранить актив. Проверьте данные и попробуйте снова.",
|
||||
|
||||
"transactions.addButton": "Добавить",
|
||||
"transactions.importButton": "Импорт CSV",
|
||||
"transactions.import.title": "Импорт транзакций из CSV",
|
||||
"transactions.import.accountLabel": "Счёт для импорта",
|
||||
"transactions.import.accountHint": "Все транзакции из файла будут добавлены на этот счёт.",
|
||||
"transactions.import.uploadTitle": "Файл",
|
||||
"transactions.import.uploadHint": "Выгрузка транзакций из банка или другого приложения в формате CSV (с заголовками колонок в первой строке).",
|
||||
"transactions.import.chooseFile": "Выбрать файл",
|
||||
"transactions.import.pickAccountFirst": "Сначала выберите счёт.",
|
||||
"transactions.import.errorEmptyFile": "В файле нет ни одной строки с данными (кроме заголовка).",
|
||||
"transactions.import.mapTitle": "Сопоставьте колонки",
|
||||
"transactions.import.dateColumnLabel": "Колонка с датой",
|
||||
"transactions.import.dateFormatLabel": "Формат даты",
|
||||
"transactions.import.selectColumn": "Выберите колонку…",
|
||||
"transactions.import.amountColumnLabel": "Колонка с суммой",
|
||||
"transactions.import.amountHint": "Отрицательные суммы — расход, положительные — доход.",
|
||||
"transactions.import.descriptionColumnLabel": "Колонка с описанием",
|
||||
"transactions.import.notMapped": "— не сопоставлено —",
|
||||
"transactions.import.categoryHint": "Если текст в колонке точно совпадёт с названием существующей категории — она подставится автоматически.",
|
||||
"transactions.import.startOver": "Начать заново",
|
||||
"transactions.import.previewButton": "Показать превью",
|
||||
"transactions.import.previewTitle": "Превью импорта",
|
||||
"transactions.import.summary": "Готово к импорту: {{valid}}. Пропущено из-за ошибок: {{skipped}}.",
|
||||
"transactions.import.andMore": "…и ещё {{count}}",
|
||||
"transactions.import.skippedRow": "Строка {{row}}: {{reason}}",
|
||||
"transactions.import.importButton": "Импортировать {{count}}",
|
||||
"transactions.import.importSuccess": "Готово — добавлено транзакций: {{count}}.",
|
||||
"transactions.import.goToTransactions": "К транзакциям",
|
||||
"transactions.import.importError": "Не удалось импортировать. Проверьте данные и попробуйте снова.",
|
||||
"transactions.import.errorBadDate": "не удалось распознать дату «{{value}}»",
|
||||
"transactions.import.errorBadAmount": "не удалось распознать сумму «{{value}}»",
|
||||
"transactions.import.errorNoDescription": "нет описания",
|
||||
"transactions.allTypes": "Все типы",
|
||||
"transactions.expense": "Расходы",
|
||||
"transactions.income": "Доходы",
|
||||
@@ -501,6 +532,37 @@ const en: Record<keyof typeof ru, string> = {
|
||||
"netWorth.form.saveError": "Failed to save the asset. Check the data and try again.",
|
||||
|
||||
"transactions.addButton": "Add",
|
||||
"transactions.importButton": "Import CSV",
|
||||
"transactions.import.title": "Import transactions from CSV",
|
||||
"transactions.import.accountLabel": "Account to import into",
|
||||
"transactions.import.accountHint": "Every transaction from the file will be added to this account.",
|
||||
"transactions.import.uploadTitle": "File",
|
||||
"transactions.import.uploadHint": "A transaction export from your bank or another app, in CSV format (with column headers in the first row).",
|
||||
"transactions.import.chooseFile": "Choose file",
|
||||
"transactions.import.pickAccountFirst": "Pick an account first.",
|
||||
"transactions.import.errorEmptyFile": "The file has no data rows (besides the header).",
|
||||
"transactions.import.mapTitle": "Map the columns",
|
||||
"transactions.import.dateColumnLabel": "Date column",
|
||||
"transactions.import.dateFormatLabel": "Date format",
|
||||
"transactions.import.selectColumn": "Select a column…",
|
||||
"transactions.import.amountColumnLabel": "Amount column",
|
||||
"transactions.import.amountHint": "Negative amounts are expenses, positive are income.",
|
||||
"transactions.import.descriptionColumnLabel": "Description column",
|
||||
"transactions.import.notMapped": "— not mapped —",
|
||||
"transactions.import.categoryHint": "If the column's text exactly matches an existing category name, it's filled in automatically.",
|
||||
"transactions.import.startOver": "Start over",
|
||||
"transactions.import.previewButton": "Show preview",
|
||||
"transactions.import.previewTitle": "Import preview",
|
||||
"transactions.import.summary": "Ready to import: {{valid}}. Skipped due to errors: {{skipped}}.",
|
||||
"transactions.import.andMore": "…and {{count}} more",
|
||||
"transactions.import.skippedRow": "Row {{row}}: {{reason}}",
|
||||
"transactions.import.importButton": "Import {{count}}",
|
||||
"transactions.import.importSuccess": "Done — {{count}} transactions added.",
|
||||
"transactions.import.goToTransactions": "Go to transactions",
|
||||
"transactions.import.importError": "Import failed. Check the data and try again.",
|
||||
"transactions.import.errorBadDate": "couldn't parse the date \"{{value}}\"",
|
||||
"transactions.import.errorBadAmount": "couldn't parse the amount \"{{value}}\"",
|
||||
"transactions.import.errorNoDescription": "no description",
|
||||
"transactions.allTypes": "All types",
|
||||
"transactions.expense": "Expenses",
|
||||
"transactions.income": "Income",
|
||||
|
||||
Reference in New Issue
Block a user