From 36212452122046dcfedabff47541ff5c0ad8d13b Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sun, 22 Jun 2025 22:49:06 +0800 Subject: [PATCH] save / load column mapping file for delimiter-separated values (dsv) file --- src/core/file.ts | 1 + src/core/import_transaction.ts | 327 ++++++++++++++++++ src/core/transaction.ts | 33 -- src/lib/ui/common.ts | 39 +++ src/locales/de.json | 6 +- src/locales/en.json | 6 +- src/locales/es.json | 6 +- src/locales/helpers.ts | 7 +- src/locales/it.json | 6 +- src/locales/ja.json | 6 +- src/locales/pt_BR.json | 6 +- src/locales/ru.json | 6 +- src/locales/uk.json | 6 +- src/locales/vi.json | 6 +- src/locales/zh_Hans.json | 6 +- src/locales/zh_Hant.json | 6 +- .../transactions/import/ImportDialog.vue | 327 ++++++------------ 17 files changed, 529 insertions(+), 271 deletions(-) create mode 100644 src/core/import_transaction.ts diff --git a/src/core/file.ts b/src/core/file.ts index ac1f8240..48bcf18b 100644 --- a/src/core/file.ts +++ b/src/core/file.ts @@ -1,6 +1,7 @@ export class KnownFileType { private static readonly allInstancesByExtension: Record = {}; + public static readonly JSON = new KnownFileType('json', 'application/json'); public static readonly CSV = new KnownFileType('csv', 'text/csv'); public static readonly TSV = new KnownFileType('tsv', 'text/tab-separated-values'); public static readonly MARKDOWN = new KnownFileType('md', 'text/markdown'); diff --git a/src/core/import_transaction.ts b/src/core/import_transaction.ts new file mode 100644 index 00000000..bd534879 --- /dev/null +++ b/src/core/import_transaction.ts @@ -0,0 +1,327 @@ +import type { TypeAndName, TypeAndDisplayName } from './base.ts'; +import { KnownAmountFormat } from './numeral.ts'; +import { KnownDateTimeFormat } from './datetime.ts'; +import { KnownDateTimezoneFormat } from './timezone.ts'; +import { TransactionType } from './transaction.ts'; + +export class ImportTransactionColumnType implements TypeAndName { + private static readonly allInstances: ImportTransactionColumnType[] = []; + + public static readonly TransactionTime = new ImportTransactionColumnType(1, 'Transaction Time'); + public static readonly TransactionTimezone = new ImportTransactionColumnType(2, 'Transaction Timezone'); + public static readonly TransactionType = new ImportTransactionColumnType(3, 'Transaction Type'); + public static readonly Category = new ImportTransactionColumnType(4, 'Category'); + public static readonly SubCategory = new ImportTransactionColumnType(5, 'Secondary Category'); + public static readonly AccountName = new ImportTransactionColumnType(6, 'Account Name'); + public static readonly AccountCurrency = new ImportTransactionColumnType(7, 'Currency'); + public static readonly Amount = new ImportTransactionColumnType(8, 'Amount'); + public static readonly RelatedAccountName = new ImportTransactionColumnType(9, 'Transfer In Account Name'); + public static readonly RelatedAccountCurrency = new ImportTransactionColumnType(10, 'Transfer In Currency'); + public static readonly RelatedAmount = new ImportTransactionColumnType(11, 'Transfer In Amount'); + public static readonly GeographicLocation = new ImportTransactionColumnType(12, 'Geographic Location'); + public static readonly Tags = new ImportTransactionColumnType(13, 'Tags'); + public static readonly Description = new ImportTransactionColumnType(14, 'Description'); + + public readonly type: number; + public readonly name: string; + + private constructor(type: number, name: string) { + this.type = type; + this.name = name; + + ImportTransactionColumnType.allInstances.push(this); + } + + public static values(): ImportTransactionColumnType[] { + return ImportTransactionColumnType.allInstances; + } +} + +export class ImportTransactionDataMapping { + private static readonly JSON_ROOT_FIELD = 'ezBookkeepingImportTransactionDataMapping'; + private static readonly DEFAULT_INCLUDE_HEADER = true; + private static readonly DEFAULT_TIME_FORMAT = ''; + private static readonly DEFAULT_TIMEZONE_FORMAT = ''; + private static readonly DEFAULT_AMOUNT_FORMAT = ''; + private static readonly DEFAULT_GEO_LOCATION_SEPARATOR = ' '; + private static readonly DEFAULT_GEO_LOCATION_ORDER = 'lonlat'; + private static readonly DEFAULT_TAG_SEPARATOR = ';'; + + public includeHeader: boolean; + public dataColumnMapping: Record; + public transactionTypeMapping: Record; + public timeFormat: string; + public timezoneFormat: string; + public amountFormat: string; + public geoLocationSeparator: string; + public geoLocationOrder: string; + public tagSeparator: string; + + private constructor(includeHeader: boolean, + dataColumnMapping: Record, + transactionTypeMapping: Record, + timeFormat: string, + timezoneFormat: string, + amountFormat: string, + geoLocationSeparator: string, + geoLocationOrder: string, + tagSeparator: string) { + this.includeHeader = includeHeader; + this.dataColumnMapping = dataColumnMapping; + this.transactionTypeMapping = transactionTypeMapping; + this.timeFormat = timeFormat; + this.timezoneFormat = timezoneFormat; + this.amountFormat = amountFormat; + this.geoLocationSeparator = geoLocationSeparator; + this.geoLocationOrder = geoLocationOrder; + this.tagSeparator = tagSeparator; + } + + public isColumnMappingSet(column: ImportTransactionColumnType | TypeAndDisplayName): boolean { + return this.dataColumnMapping.hasOwnProperty(column.type) && typeof(this.dataColumnMapping[column.type]) === 'number'; + } + + public toggleIncludeHeader(): void { + this.includeHeader = !this.includeHeader; + } + + public toggleDataMappingColumn(columnIndex: number, columnType: number): void { + if (this.dataColumnMapping[columnType] === columnIndex) { + delete this.dataColumnMapping[columnType]; + } else { + this.dataColumnMapping[columnType] = columnIndex; + } + + for (const otherColumnType in this.dataColumnMapping) { + if (otherColumnType !== columnType.toString() && this.dataColumnMapping[otherColumnType] === columnIndex) { + delete this.dataColumnMapping[otherColumnType]; + } + } + } + + public setGeoLocationFormat(separator: string, order: string): void { + this.geoLocationSeparator = separator; + this.geoLocationOrder = order; + } + + public formatGeoLocation(latitude: string, longitude: string): string { + if (this.geoLocationOrder === 'latlon') { + return `${latitude}${this.geoLocationSeparator}${longitude}`; + } else { + return `${longitude}${this.geoLocationSeparator}${latitude}`; + } + } + + public parseFileAllTransactionTypes(fileData: string[][] | undefined): string[] { + if (!fileData || !fileData.length || !this.isColumnMappingSet(ImportTransactionColumnType.TransactionType)) { + return []; + } + + const allTypeMap: Record = {}; + const allTypes: string[] = []; + const typeColumnIndex = this.dataColumnMapping[ImportTransactionColumnType.TransactionType.type]; + + const startIndex = this.includeHeader ? 1 : 0; + + for (let i = startIndex; i < fileData.length; i++) { + if (fileData[i].length <= typeColumnIndex) { + continue; + } + + const type = fileData[i][typeColumnIndex]; + + if (type && !allTypeMap[type]) { + allTypes.push(type); + allTypeMap[type] = true; + } + } + + return allTypes; + } + + public parseFileValidMappedTransactionTypes(fileData: string[][] | undefined): Record { + if (!fileData || !fileData.length || !this.isColumnMappingSet(ImportTransactionColumnType.TransactionType)) { + return {}; + } + + const result: Record = {}; + + if (!this.transactionTypeMapping) { + return result; + } + + for (const name in this.transactionTypeMapping) { + if (!Object.prototype.hasOwnProperty.call(this.transactionTypeMapping, name)) { + continue; + } + + const type = this.transactionTypeMapping[name]; + + if (TransactionType.ModifyBalance <= type && type <= TransactionType.Transfer) { + result[name] = type; + } + } + + return result; + } + + public parseFileAutoDetectedTimeFormat(fileData: string[][] | undefined): string | undefined { + if (!fileData || !fileData.length || !this.isColumnMappingSet(ImportTransactionColumnType.TransactionTime)) { + return undefined; + } + + const allDateTimes: string[] = []; + const dateTimeColumnIndex = this.dataColumnMapping[ImportTransactionColumnType.TransactionTime.type]; + + const startIndex = this.includeHeader ? 1 : 0; + + for (let i = startIndex; i < fileData.length; i++) { + if (fileData[i].length <= dateTimeColumnIndex) { + continue; + } + + const dateTime = fileData[i][dateTimeColumnIndex]; + + if (dateTime) { + allDateTimes.push(dateTime); + } + } + + const detectedFormats = KnownDateTimeFormat.detectMulti(allDateTimes); + + if (!detectedFormats || !detectedFormats.length || detectedFormats.length > 1) { + return undefined; + } + + return detectedFormats[0].format; + } + + public parseFileAutoDetectedTimezoneFormat(fileData: string[][] | undefined): string | undefined { + if (!fileData || !fileData.length || !this.isColumnMappingSet(ImportTransactionColumnType.TransactionTimezone)) { + return undefined; + } + + const allTimezones: string[] = []; + const timezoneColumnIndex = this.dataColumnMapping[ImportTransactionColumnType.TransactionTimezone.type]; + + const startIndex = this.includeHeader ? 1 : 0; + + for (let i = startIndex; i < fileData.length; i++) { + if (fileData[i].length <= timezoneColumnIndex) { + continue; + } + + const timezone = fileData[i][timezoneColumnIndex]; + + if (timezone) { + allTimezones.push(timezone); + } + } + + const detectedFormats = KnownDateTimezoneFormat.detectMulti(allTimezones); + + if (!detectedFormats || !detectedFormats.length || detectedFormats.length > 1) { + return undefined; + } + + return detectedFormats[0].value; + } + + public parseFileAutoDetectedAmountFormat(fileData: string[][] | undefined): string | undefined { + if (!fileData || !fileData.length || !this.isColumnMappingSet(ImportTransactionColumnType.TransactionTimezone)) { + return undefined; + } + + const allAmounts: string[] = []; + const amountColumnIndex = this.dataColumnMapping[ImportTransactionColumnType.Amount.type]; + + const startIndex = this.includeHeader ? 1 : 0; + + for (let i = startIndex; i < fileData.length; i++) { + if (fileData[i].length <= amountColumnIndex) { + continue; + } + + const amount = fileData[i][amountColumnIndex]; + + if (amount) { + allAmounts.push(amount); + } + } + + const detectedFormats = KnownAmountFormat.detectMulti(allAmounts); + + if (!detectedFormats || !detectedFormats.length) { + return undefined; + } + + return detectedFormats[0].type; + } + + public reset(): void { + this.includeHeader = ImportTransactionDataMapping.DEFAULT_INCLUDE_HEADER; + this.dataColumnMapping = {}; + this.transactionTypeMapping = {}; + this.timeFormat = ImportTransactionDataMapping.DEFAULT_TIME_FORMAT; + this.timezoneFormat = ImportTransactionDataMapping.DEFAULT_TIMEZONE_FORMAT; + this.amountFormat = ImportTransactionDataMapping.DEFAULT_AMOUNT_FORMAT; + this.geoLocationSeparator = ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_SEPARATOR; + this.geoLocationOrder = ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_ORDER; + this.tagSeparator = ImportTransactionDataMapping.DEFAULT_TAG_SEPARATOR; + } + + public toJson(): string { + return JSON.stringify({ + [ImportTransactionDataMapping.JSON_ROOT_FIELD]: { + includeHeader: this.includeHeader, + dataColumnMapping: this.dataColumnMapping, + transactionTypeMapping: this.transactionTypeMapping, + timeFormat: this.timeFormat, + timezoneFormat: this.timezoneFormat, + amountFormat: this.amountFormat, + geoLocationSeparator: this.geoLocationSeparator, + geoLocationOrder: this.geoLocationOrder, + tagSeparator: this.tagSeparator + } + }); + } + + public static createEmpty(): ImportTransactionDataMapping { + return new ImportTransactionDataMapping( + ImportTransactionDataMapping.DEFAULT_INCLUDE_HEADER, + {}, + {}, + ImportTransactionDataMapping.DEFAULT_TIME_FORMAT, + ImportTransactionDataMapping.DEFAULT_TIMEZONE_FORMAT, + ImportTransactionDataMapping.DEFAULT_AMOUNT_FORMAT, + ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_SEPARATOR, + ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_ORDER, + ImportTransactionDataMapping.DEFAULT_TAG_SEPARATOR + ); + } + + public static parseFromJson(json: string): ImportTransactionDataMapping | null { + try { + const parsed = JSON.parse(json); + const root = parsed[ImportTransactionDataMapping.JSON_ROOT_FIELD]; + + if (!root) { + return null; + } + + return new ImportTransactionDataMapping( + root.includeHeader ?? ImportTransactionDataMapping.DEFAULT_INCLUDE_HEADER, + root.dataColumnMapping ?? {}, + root.transactionTypeMapping ?? {}, + root.timeFormat ?? ImportTransactionDataMapping.DEFAULT_TIME_FORMAT, + root.timezoneFormat ?? ImportTransactionDataMapping.DEFAULT_TIMEZONE_FORMAT, + root.amountFormat ?? ImportTransactionDataMapping.DEFAULT_AMOUNT_FORMAT, + root.geoLocationSeparator ?? ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_SEPARATOR, + root.geoLocationOrder ?? ImportTransactionDataMapping.DEFAULT_GEO_LOCATION_ORDER, + root.tagSeparator ?? ImportTransactionDataMapping.DEFAULT_TAG_SEPARATOR + ); + } catch { + return null; + } + } +} diff --git a/src/core/transaction.ts b/src/core/transaction.ts index 851a4ce0..ae16c3e7 100644 --- a/src/core/transaction.ts +++ b/src/core/transaction.ts @@ -57,36 +57,3 @@ export class TransactionTagFilterType implements TypeAndName { return TransactionTagFilterType.allInstances; } } - -export class ImportTransactionColumnType implements TypeAndName { - private static readonly allInstances: ImportTransactionColumnType[] = []; - - public static readonly TransactionTime = new ImportTransactionColumnType(1, 'Transaction Time'); - public static readonly TransactionTimezone = new ImportTransactionColumnType(2, 'Transaction Timezone'); - public static readonly TransactionType = new ImportTransactionColumnType(3, 'Transaction Type'); - public static readonly Category = new ImportTransactionColumnType(4, 'Category'); - public static readonly SubCategory = new ImportTransactionColumnType(5, 'Secondary Category'); - public static readonly AccountName = new ImportTransactionColumnType(6, 'Account Name'); - public static readonly AccountCurrency = new ImportTransactionColumnType(7, 'Currency'); - public static readonly Amount = new ImportTransactionColumnType(8, 'Amount'); - public static readonly RelatedAccountName = new ImportTransactionColumnType(9, 'Transfer In Account Name'); - public static readonly RelatedAccountCurrency = new ImportTransactionColumnType(10, 'Transfer In Currency'); - public static readonly RelatedAmount = new ImportTransactionColumnType(11, 'Transfer In Amount'); - public static readonly GeographicLocation = new ImportTransactionColumnType(12, 'Geographic Location'); - public static readonly Tags = new ImportTransactionColumnType(13, 'Tags'); - public static readonly Description = new ImportTransactionColumnType(14, 'Description'); - - public readonly type: number; - public readonly name: string; - - private constructor(type: number, name: string) { - this.type = type; - this.name = name; - - ImportTransactionColumnType.allInstances.push(this); - } - - public static values(): ImportTransactionColumnType[] { - return ImportTransactionColumnType.allInstances; - } -} diff --git a/src/lib/ui/common.ts b/src/lib/ui/common.ts index 1df48d33..43f8ac81 100644 --- a/src/lib/ui/common.ts +++ b/src/lib/ui/common.ts @@ -4,6 +4,8 @@ import { ThemeType } from '@/core/theme.ts'; import { type AmountColor, PresetAmountColor } from '@/core/color.ts'; +import logger from '../logger.ts'; + export function getSystemTheme(): ThemeType { if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return ThemeType.Dark; @@ -84,6 +86,43 @@ export function copyTextToClipboard(text: string, container?: Element | null): v }); } +export function openTextFileContent({ allowedExtensions }: { allowedExtensions: string }): Promise { + return new Promise((resolve, reject) => { + const fileInput = document.createElement('input'); + + fileInput.style.display = 'none'; + fileInput.type = 'file'; + fileInput.accept = allowedExtensions; + + fileInput.onchange = (event) => { + const el = event.target as HTMLInputElement; + + if (el.files && el.files.length > 0) { + const file = el.files[0]; + const reader = new FileReader(); + + reader.onload = (e) => { + const content = e.target?.result; + + if (typeof content === 'string') { + resolve(content); + } else { + reject(new Error('failed to load file, because file reader result is not string')); + } + }; + + reader.onerror = (error) => { + logger.error('failed to load file', error); + }; + + reader.readAsText(file); + } + }; + + fileInput.click(); + }); +} + export function startDownloadFile(fileName: string, fileData: Blob): void { const dataObjectUrl = URL.createObjectURL(fileData); const dataLink = document.createElement('a'); diff --git a/src/locales/de.json b/src/locales/de.json index 5a5d85d0..512e9c37 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Ungültige Transaktionen können nicht importiert werden", "Unable to parse import file": "Importdatei kann nicht geparst werden", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Ausgewählte Ausgabenkategorien im Batch ersetzen", "Batch Replace Selected Income Categories": "Ausgewählte Einnahmenkategorien im Batch ersetzen", "Batch Replace Selected Transfer Categories": "Ausgewählte Überweisungskategorien im Batch ersetzen", diff --git a/src/locales/en.json b/src/locales/en.json index a4994f1d..600986c4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Cannot import invalid transactions", "Unable to parse import file": "Unable to parse import file", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Batch Replace Selected Expense Categories", "Batch Replace Selected Income Categories": "Batch Replace Selected Income Categories", "Batch Replace Selected Transfer Categories": "Batch Replace Selected Transfer Categories", diff --git a/src/locales/es.json b/src/locales/es.json index 30b572b1..df8af863 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "No se pueden importar transacciones no válidas", "Unable to parse import file": "No se puede analizar el archivo de importación", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Reemplazar por lotes categorías de gastos seleccionadas", "Batch Replace Selected Income Categories": "Reemplazo por lotes de categorías de ingresos seleccionadas", "Batch Replace Selected Transfer Categories": "Reemplazar por lotes las categorías de transferencia seleccionadas", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index 3acec86d..7c3ac893 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -79,10 +79,13 @@ import { import { TransactionEditScopeType, - TransactionTagFilterType, - ImportTransactionColumnType + TransactionTagFilterType } from '@/core/transaction.ts'; +import { + ImportTransactionColumnType +} from '@/core/import_transaction.ts'; + import { ScheduledTemplateFrequencyType } from '@/core/template.ts'; diff --git a/src/locales/it.json b/src/locales/it.json index 94a4e236..ab398442 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Impossibile importare transazioni non valide", "Unable to parse import file": "Impossibile analizzare il file di importazione", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Sostituisci in blocco categorie di spesa selezionate", "Batch Replace Selected Income Categories": "Sostituisci in blocco categorie di entrata selezionate", "Batch Replace Selected Transfer Categories": "Sostituisci in blocco categorie di trasferimento selezionate", diff --git a/src/locales/ja.json b/src/locales/ja.json index 38c32116..c9f95dc9 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_エクスポートデータ", "exportFilename": "ezBookkeeping_{nickname}_エクスポートデータ", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "無効な取引をインポートできません", "Unable to parse import file": "インポートファイルを解析できません", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "バッチは選択した支出カテゴリを置き換えます", "Batch Replace Selected Income Categories": "バッチは選択した収入カテゴリを置き換えます", "Batch Replace Selected Transfer Categories": "バッチは選択した振替カテゴリを置き換えます", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 6664ba49..401c4842 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_dados_estatísticos", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_dados_estatísticos" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_dados_estatísticos", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Não é possível importar transações inválidas", "Unable to parse import file": "Não foi possível analisar o arquivo de importação", "Unable to import transactions": "Não foi possível importar transações", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Substituir em Lote as Categorias de Despesas Selecionadas", "Batch Replace Selected Income Categories": "Substituir em Lote as Categorias de Renda Selecionadas", "Batch Replace Selected Transfer Categories": "Substituir em Lote as Categorias de Transferência Selecionadas", diff --git a/src/locales/ru.json b/src/locales/ru.json index 71191e28..39f96c93 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Невозможно импортировать недействительные транзакции", "Unable to parse import file": "Не удалось обработать файл импорта", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Пакетная замена выбранных категорий расходов", "Batch Replace Selected Income Categories": "Пакетная замена выбранных категорий доходов", "Batch Replace Selected Transfer Categories": "Пакетная замена выбранных категорий переводов", diff --git a/src/locales/uk.json b/src/locales/uk.json index 8e50a5c0..dcac5744 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Неможливо імпортувати недійсні транзакції", "Unable to parse import file": "Не вдалося обробити файл імпорту", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Пакетна заміна вибраних категорій витрат", "Batch Replace Selected Income Categories": "Пакетна заміна вибраних категорій доходів", "Batch Replace Selected Transfer Categories": "Пакетна заміна вибраних категорій переказів", diff --git a/src/locales/vi.json b/src/locales/vi.json index 0e8aac08..cb841baa 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_export_data", "exportFilename": "ezBookkeeping_{nickname}_export_data", "defaultExportStatisticsFileName": "ezBookkeeping_statistics_data", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_statistics_data", + "defaultImportDataMappingFileName": "ezBookkeeping_import_data_mapping" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "Không thể nhập giao dịch không hợp lệ", "Unable to parse import file": "Không thể phân tích tệp nhập", "Unable to import transactions": "Unable to import transactions", + "Load Data Mapping File": "Load Data Mapping File", + "Save Data Mapping File": "Save Data Mapping File", + "Data mapping file is invalid": "Data mapping file is invalid", "Batch Replace Selected Expense Categories": "Thay thế hàng loạt các danh mục chi phí đã chọn", "Batch Replace Selected Income Categories": "Thay thế hàng loạt các danh mục thu nhập đã chọn", "Batch Replace Selected Transfer Categories": "Thay thế hàng loạt các danh mục chuyển khoản đã chọn", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 0de3106a..54777ae0 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_导出数据", "exportFilename": "ezBookkeeping_{nickname}_导出数据", "defaultExportStatisticsFileName": "ezBookkeeping_统计数据", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_统计数据" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_统计数据", + "defaultImportDataMappingFileName": "ezBookkeeping_导入数据映射文件" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "不能导入无效的交易", "Unable to parse import file": "无法解析导入的文件", "Unable to import transactions": "无法导入交易", + "Load Data Mapping File": "加载数据映射文件", + "Save Data Mapping File": "保存数据映射文件", + "Data mapping file is invalid": "数据映射文件无效", "Batch Replace Selected Expense Categories": "批量替换选中的支出分类", "Batch Replace Selected Income Categories": "批量替换选中的收入分类", "Batch Replace Selected Transfer Categories": "批量替换选中的转账分类", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 969b8830..23299eda 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -121,7 +121,8 @@ "defaultExportFilename": "ezBookkeeping_匯出資料", "exportFilename": "ezBookkeeping_{nickname}_匯出資料", "defaultExportStatisticsFileName": "ezBookkeeping_統計資料", - "exportStatisticsFileName": "ezBookkeeping_{nickname}_統計資料" + "exportStatisticsFileName": "ezBookkeeping_{nickname}_統計資料", + "defaultImportDataMappingFileName": "ezBookkeeping_匯入資料對應檔案" }, "datetime": { "AM": { @@ -1728,6 +1729,9 @@ "Cannot import invalid transactions": "無法匯入無效的交易", "Unable to parse import file": "無法解析匯入的檔案", "Unable to import transactions": "無法匯入交易", + "Load Data Mapping File": "載入資料對應檔案", + "Save Data Mapping File": "儲存資料對應檔案", + "Data mapping file is invalid": "資料對應檔案無效", "Batch Replace Selected Expense Categories": "批次替換選中的支出分類", "Batch Replace Selected Income Categories": "批次替換選中的收入分類", "Batch Replace Selected Transfer Categories": "批次替換選中的轉帳分類", diff --git a/src/views/desktop/transactions/import/ImportDialog.vue b/src/views/desktop/transactions/import/ImportDialog.vue index be338e32..c5550392 100644 --- a/src/views/desktop/transactions/import/ImportDialog.vue +++ b/src/views/desktop/transactions/import/ImportDialog.vue @@ -7,6 +7,21 @@

{{ tt('Import Transactions') }}

+ + + + + + + + + @@ -294,9 +309,9 @@ + @click="parsedFileDataColumnMapping.toggleDataMappingColumn(parseInt(column.key), columnType.type)"> {{ columnType.displayName }} @@ -310,12 +325,12 @@