Compare commits

...

100 Commits

Author SHA1 Message Date
MaysWind 422f18443a add transaction timezone offset to axis / category / series in insights explorer 2026-03-22 23:25:06 +08:00
MaysWind 0fbf185223 show year-over-year and period-over-period in trends chart 2026-03-22 01:38:35 +08:00
MaysWind 91cdffa9a6 fix incorrect ordinal translations 2026-03-21 00:49:21 +08:00
MaysWind 89199eed8b support importing WeChat statements with the latest format that includes thousand separators (#534) 2026-03-20 23:41:17 +08:00
MaysWind 1a65bb9db6 display the currency name instead of the currency code when using the source account currency or destination account currency as the axis, category or series in insights explorer 2026-03-18 00:11:55 +08:00
MaysWind 9772d9ca62 support custom quick save button styles on the mobile transaction edit page 2026-03-17 00:16:37 +08:00
MaysWind 5ee93a5db1 add attributes to disable spell check and automatic capitalization to all username input fields 2026-03-16 23:38:49 +08:00
MaysWind 85c4f686da add new contributor 2026-03-16 23:32:41 +08:00
Alex 1f066b0d1e fix:params for username field on login page mobile (#526) 2026-03-16 23:28:18 +08:00
MaysWind 38ddb7aaa3 add new contributor 2026-03-16 09:50:51 +08:00
Ivan Noleto a22931f96b Improve and standardize Brazilian Portuguese translation (#530)
* Improve and standardize Brazilian Portuguese translation

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* revert timezone translations

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:49:12 +08:00
MaysWind dcee067aea insights explorer supports sub condition 2026-03-16 02:07:36 +08:00
MaysWind 302d118ae0 remove unused code 2026-03-13 00:04:09 +08:00
MaysWind 09eea96cdc use const for variables that will not be modified 2026-03-13 00:03:58 +08:00
MaysWind 205dea9e58 move the agent skill files to the skills directory 2026-03-12 23:30:37 +08:00
MaysWind 089eabb806 clear legacy runtime cache when clearing the application code cache 2026-03-12 23:23:19 +08:00
MaysWind dd63500202 reorder the button display order 2026-03-11 01:08:11 +08:00
MaysWind 13488efdaf support clearing application code cache 2026-03-11 00:52:43 +08:00
MaysWind edcf33f49c add boxplot chart in reconciliation statement dialog 2026-03-08 23:33:46 +08:00
MaysWind d601e01029 fix incorrect html tag in axis charts tooltip 2026-03-08 23:19:32 +08:00
MaysWind 4d7c3650b5 support filtering by geographic longitude and latitude in insights explorer queries 2026-03-08 20:55:35 +08:00
MaysWind a0fd468309 fix the amounts in non-default currencies are not converted to the default currency in the statistics data shown in the insights explorer data table 2026-03-08 18:04:48 +08:00
MaysWind 0b7471879d add Coefficient of Variation to statistics data on data table tab and value metric on charts of insights explorer 2026-03-08 17:24:08 +08:00
MaysWind 282b74c95e support receiving images from the Web Share Target API Level 2 and directly opening AI image recognition on mobile version 2026-03-08 15:33:58 +08:00
MaysWind 5ce1dc973c add more icons 2026-03-08 02:15:42 +08:00
MaysWind 7ac1e0b69f add transaction hour of day to axis / category / series in insights explorer 2026-03-07 23:04:15 +08:00
MaysWind 127bed1026 fix incorrect display when use transaction year-quarter as axis / category / series in insights explorer 2026-03-07 22:52:32 +08:00
MaysWind d517a1862b add 90th percentile amount, range, interquartile range, variance, and standard deviation to the value metrics in insights explorer 2026-03-07 22:18:33 +08:00
MaysWind 8e5202b375 code refactor 2026-03-07 22:18:18 +08:00
MaysWind 301fb58917 hide some statistics when the number of transactions is not enough 2026-03-07 21:31:18 +08:00
MaysWind aedebb1461 fix test case failures when the original Chinese calendar data uses CRLF line endings 2026-03-07 21:01:23 +08:00
MaysWind 1336377598 add more statistic data on Data Table tab of Insights Explorer page 2026-03-07 18:47:57 +08:00
Dmitry Shemin 3b58dcbc4d translation/ru: fix translation to russian language (#521) 2026-03-07 16:12:04 +08:00
MaysWind 23a5f0a96f display total transactions, total amount, average amount, median amount, minimum amount, and maximum amount in the Data Table tab of Insights Explorer page 2026-03-07 01:04:47 +08:00
MaysWind b81d2ec63c fix the context menu disappears after the second long press on the add icon on the mobile home page 2026-03-06 00:43:05 +08:00
MaysWind cabe365907 disable native browser drag behavior on mobile version 2026-03-05 23:17:27 +08:00
MaysWind 54f61ecb18 add agent skill 2026-03-05 00:34:03 +08:00
MaysWind 404cd62d7b Support restricting API token access based on IP address 2026-03-04 23:49:14 +08:00
MaysWind f0f3143605 fix the user settings is reset after using the command line tool to change the user password (#516) 2026-03-04 23:18:14 +08:00
MaysWind b729fdedca update the command description 2026-03-04 22:58:16 +08:00
MaysWind 973cec2c6a automatically apply known transaction types when setting the transaction type column mapping 2026-03-04 00:36:09 +08:00
MaysWind 6e61aba050 remove deprecated Reserve Bank of Australia exchange rate data source 2026-03-03 22:49:19 +08:00
MaysWind 40a8deba12 optimize the performance of the retrieve all transactions api 2026-03-03 01:12:38 +08:00
MaysWind 0ba762ba6e support batch replacement of transaction time zones in the import tool 2026-03-02 23:54:48 +08:00
MaysWind 732c256db2 support "Add Another" in transaction add page / dialog (#471) 2026-03-02 00:54:17 +08:00
MaysWind d2ce801277 disable the sort button when fewer than two items are present 2026-03-01 23:05:27 +08:00
MaysWind 4845fdedfd automatically reload data after changed display order 2026-03-01 22:25:44 +08:00
MaysWind f5a7e2e2d6 sort transaction tags by name (#487) 2026-03-01 22:03:38 +08:00
MaysWind a84f48ae8a support syncing the settings autoUpdateExchangeRatesData, showAddTransactionButtonInDesktopNavbar, mapCacheExpiration, and exchangeRatesDataCacheExpiration 2026-03-01 21:08:34 +08:00
MaysWind c4c9503e31 update the styling used when reloading browser cache data 2026-03-01 19:31:34 +08:00
MaysWind 8c1f499ed8 upgrade golang to 1.25.7, node.js to 24.14.0 and alpine base image to 3.23.3 2026-03-01 16:50:41 +08:00
MaysWind c6eb3cfb74 support automatically applying known column mapping and transaction type mapping rules when importing custom files with column mapping handle method 2026-03-01 16:32:22 +08:00
MaysWind d7a0d253c4 support utf-32 file encoding 2026-03-01 16:04:29 +08:00
MaysWind 9d275a3051 merge UTF-8 and UTF-16 encodings with or without BOM, with BOM automatically detected and handled 2026-03-01 15:54:41 +08:00
MaysWind 8192a48bc5 support setting exchange rate cache expiration time 2026-02-28 21:36:00 +08:00
MaysWind 247181830c support caching map data when map_data_fetch_proxy is set true 2026-02-28 21:35:28 +08:00
MaysWind d5dfdc8c05 modify cache capacity calculation 2026-02-28 13:51:27 +08:00
MaysWind d95fcd8b00 add cache management page 2026-02-27 00:50:52 +08:00
MaysWind 40a366e68d modify background color of time zone tag to improve the dark theme experience 2026-02-26 00:42:02 +08:00
MaysWind 593ae10783 fix incorrect data when exporting 100% stacked charts data 2026-02-25 01:30:10 +08:00
MaysWind 75d9e11bab support exporting statistics & analysis result to mermaid 2026-02-25 01:16:42 +08:00
MaysWind 6d37d42e50 support exporting statistics & analysis result, reconciliation statement and import check result to SSV (semicolon separated values) file 2026-02-24 22:59:29 +08:00
MaysWind f9e9c9285f fix the updated transaction template is not reflected in the interface immediately after modification 2026-02-23 23:50:12 +08:00
MaysWind 314bf876f2 code refactor 2026-02-23 23:30:49 +08:00
MaysWind 61c52cc888 fix incorrect display name of sort type in the insight explorer 2026-02-23 23:25:44 +08:00
MaysWind b42f226aba update transaction edit dialog height 2026-02-23 21:17:35 +08:00
MaysWind 767b841866 add more detailed comments for amount fields 2026-02-23 01:16:27 +08:00
MaysWind fd08666f49 import transactions from custom xlsx/xls file 2026-02-23 00:50:01 +08:00
MaysWind eb662681a1 fix incorrect column count when importing mscfb excel file 2026-02-23 00:40:47 +08:00
MaysWind ef15eccc33 use consistent quotation marks in the help text 2026-02-19 10:45:12 +08:00
MaysWind e0286ff133 fix incorrect height for some toolbar buttons 2026-02-18 22:56:25 +08:00
MaysWind 2baffe3f11 reduce the size of the bottom save button 2026-02-17 23:04:16 +08:00
MaysWind 196657ee86 reduce tabbar height 2026-02-17 21:55:01 +08:00
MaysWind b4c4aafc99 support loading environment variables from .env file 2026-02-17 17:22:23 +08:00
MaysWind b907a79223 fix incorrect time for some time zones on the scheduled transaction edit page (#499) 2026-02-17 13:58:16 +08:00
MaysWind 0d213de580 code refactor 2026-02-17 13:55:35 +08:00
MaysWind 2e97d699e7 ezBookkeeping API Tools supports formatting response to table 2026-02-16 01:14:21 +08:00
MaysWind 22e4738b7a upgrade third party dependencies 2026-02-15 14:54:39 +08:00
MaysWind 4b68641043 move the user agent constants of special token into the core package 2026-02-15 01:17:43 +08:00
MaysWind 3a66a3d655 move the variables set during the building process into the core package 2026-02-15 01:17:43 +08:00
MaysWind 76d1d3aef3 fix the the Anthropic API key was not masked with asterisks in startup logs 2026-02-15 01:17:42 +08:00
Albert Brugués fe2aa5d28b update ES locale (#495) 2026-02-13 22:55:20 +08:00
MaysWind f474bbf09a update README.md 2026-02-09 00:35:52 +08:00
MaysWind c4d02db879 upgrade third party dependencies 2026-02-08 00:24:52 +08:00
MaysWind 75b36ec547 upgrade third party dependencies 2026-02-08 00:12:51 +08:00
MaysWind 43b7aea76e add new contributor 2026-02-05 09:42:47 +08:00
Dmitry Shemin 13a4a47d40 translation/ru: fix translation to russian language (#483) 2026-02-05 09:36:13 +08:00
MaysWind fd9f380922 update api command description 2026-02-03 23:51:19 +08:00
MaysWind a5fdb9d6b7 add server version command 2026-02-02 23:11:20 +08:00
MaysWind 983c65e4f8 add latest exchange rates command 2026-02-02 23:04:28 +08:00
MaysWind fa568056d3 update API tools script 2026-02-02 22:51:23 +08:00
MaysWind ea8b2812d4 add ezBookkeeping API tools 2026-02-02 09:18:54 +08:00
MaysWind b6a2aea8fd validate whether the transaction tag group exists when creating a transaction tag or move transaction tag to another group 2026-02-02 01:03:38 +08:00
MaysWind fa047bf303 llm provider supports Anthropic and Anthropic compatibility api 2026-02-01 16:17:22 +08:00
MaysWind 4177ac3d46 add missing stream field 2026-02-01 15:22:15 +08:00
MaysWind 7647f4f5b9 update README.md 2026-02-01 00:59:13 +08:00
MaysWind bab03dbde1 fix the the Google AI token was not masked with asterisks in startup logs 2026-01-31 22:43:01 +08:00
MaysWind 85db6e96af llm provider supports LM Studio 2026-01-31 22:38:08 +08:00
MaysWind 548461ade0 update git ignore file 2026-01-31 01:37:33 +08:00
MaysWind ecbf182173 bump version to 1.4.0 2026-01-31 00:48:54 +08:00
169 changed files with 13356 additions and 4249 deletions
@@ -3,13 +3,13 @@ name: Build backend file for windows
inputs: inputs:
go-version: go-version:
required: false required: false
default: "1.25.5" default: "1.25.7"
mingw-version: mingw-version:
required: false required: false
default: "15.2.0" default: "15.2.0"
mingw-revison: mingw-revison:
required: false required: false
default: "v13-rev0" default: "v13-rev1"
release-build: release-build:
required: false required: false
type: string type: string
+14
View File
@@ -147,3 +147,17 @@ dist/
# Roo Code # Roo Code
.roo/ .roo/
# Binary and build files
ezbookkeeping
!**/ezbookkeeping/
package/
# Environment variable files
.env
**/.env
# Other directories
data/
storage/
log/
+3 -3
View File
@@ -1,5 +1,5 @@
# Build backend binary file # Build backend binary file
FROM golang:1.25.5-alpine3.23 AS be-builder FROM golang:1.25.7-alpine3.23 AS be-builder
ARG RELEASE_BUILD ARG RELEASE_BUILD
ARG BUILD_PIPELINE ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME ARG BUILD_UNIXTIME
@@ -19,7 +19,7 @@ RUN apk add git gcc g++ libc-dev
RUN ./build.sh backend RUN ./build.sh backend
# Build frontend files # Build frontend files
FROM --platform=$BUILDPLATFORM node:24.12.0-alpine3.23 AS fe-builder FROM --platform=$BUILDPLATFORM node:24.14.0-alpine3.23 AS fe-builder
ARG RELEASE_BUILD ARG RELEASE_BUILD
ARG BUILD_PIPELINE ARG BUILD_PIPELINE
ARG BUILD_UNIXTIME ARG BUILD_UNIXTIME
@@ -35,7 +35,7 @@ RUN apk add git
RUN ./build.sh frontend RUN ./build.sh frontend
# Package docker image # Package docker image
FROM alpine:3.23.2 FROM alpine:3.23.3
LABEL maintainer="MaysWind <i@mayswind.net>" LABEL maintainer="MaysWind <i@mayswind.net>"
RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping RUN addgroup -S -g 1000 ezbookkeeping && adduser -S -G ezbookkeeping -u 1000 ezbookkeeping
RUN apk --no-cache add tzdata RUN apk --no-cache add tzdata
+24 -22
View File
@@ -11,7 +11,7 @@
[![Trending](https://trendshift.io/api/badge/repositories/12917)](https://trendshift.io/repositories/12917) [![Trending](https://trendshift.io/api/badge/repositories/12917)](https://trendshift.io/repositories/12917)
## Introduction ## Introduction
ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It's easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient and highly scalable, it can run smoothly on devices as small as a Raspberry Pi, or scale up to NAS, MicroServers, and even large cluster environments. ezBookkeeping is a lightweight, self-hosted personal finance app with a user-friendly interface and powerful bookkeeping features. It helps you record daily transactions, import data from various sources, and quickly search and filter your bills. You can analyze historical data using built-in charts or perform custom queries with your own chart dimensions to better understand spending patterns and financial trends. ezBookkeeping is easy to deploy, and you can start it with just one single Docker command. Designed to be resource-efficient, it runs smoothly on devices such as Raspberry Pi, NAS, and MicroServers.
ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app. ezBookkeeping offers tailored interfaces for both mobile and desktop devices. With support for PWA (Progressive Web Apps), you can even [add it to your mobile home screen](https://raw.githubusercontent.com/wiki/mayswind/ezbookkeeping/img/mobile/add_to_home_screen.gif) and use it like a native app.
@@ -21,9 +21,9 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
- **Open Source & Self-Hosted** - **Open Source & Self-Hosted**
- Built for privacy and control - Built for privacy and control
- **Lightweight & Fast** - **Lightweight & Fast**
- Optimized for performance, runs smoothly even on low-resource environments - Minimal resource usage, runs smoothly even on low-resource devices
- **Easy Installation** - **Easy Installation**
- Docker-ready - Docker support
- Supports SQLite, MySQL, PostgreSQL - Supports SQLite, MySQL, PostgreSQL
- Cross-platform (Windows, macOS, Linux) - Cross-platform (Windows, macOS, Linux)
- Works on x86, amd64, ARM architectures - Works on x86, amd64, ARM architectures
@@ -33,24 +33,28 @@ Live Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-demo.
- Dark mode - Dark mode
- **AI-Powered Features** - **AI-Powered Features**
- Receipt image recognition - Receipt image recognition
- Supports MCP (Model Context Protocol) for AI integration - MCP (Model Context Protocol) support for AI integration
- API command-line script tools for AI integration
- **Powerful Bookkeeping** - **Powerful Bookkeeping**
- Two-level accounts and categories - Two-level accounts and categories
- Attach images to transactions - Image attachments for transactions
- Location tracking with maps - Location tracking with maps
- Recurring transactions - Scheduled transactions
- Advanced filtering, search, visualization, and analysis - Advanced filtering, search, visualization and analysis
- **Localization & Globalization** - **Localization & Internationalization**
- Multi-language and multi-currency support - Multi-language and multi-currency support
- Automatic exchange rates - Multiple exchange rate sources with automatic updates
- Multi-timezone awareness - Multi-timezone support
- Custom formats for dates, numbers, and currencies - Custom formats for dates, numbers and currencies
- **Security** - **Security**
- Two-factor authentication (2FA) - Two-factor authentication (2FA)
- OIDC external authentication
- Login rate limiting - Login rate limiting
- Application lock (PIN code / WebAuthn) - Application lock (PIN code / WebAuthn)
- **Data Import/Export** - **Data Import & Export**
- Supports CSV, OFX, QFX, QIF, IIF, Camt.053, MT940, GnuCash, Firefly III, Beancount, and more - Supports CSV, OFX, QFX, QIF, IIF, Camt.052, Camt.053, MT940, GnuCash, Firefly III, Beancount and more
For a full list of features, visit the [Full Feature List](https://ezbookkeeping.mayswind.net/comparison/).
## Screenshots ## Screenshots
### Desktop Version ### Desktop Version
@@ -112,16 +116,16 @@ You can also build a Docker image. Make sure you have [Docker](https://www.docke
## Contributing ## Contributing
We welcome contributions of all kinds. We welcome contributions of all kinds.
Found a bug? [Submit an issue](https://github.com/mayswind/ezbookkeeping/issues) If you find a bug, please [submit an issue](https://github.com/mayswind/ezbookkeeping/issues) on GitHub.
Want to contribute code? Feel free to fork and send a pull request. If you would like to contribute code, you can fork the repository and open a pull request.
Contributions of all kinds — bug reports, feature suggestions, documentation improvements, or code — are highly appreciated. Improvements to documentation, feature suggestions, and other forms of feedback are also appreciated.
Check out our [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors) to see the amazing people who've already helped. You can view existing contributors on the [Contributor Graph](https://github.com/mayswind/ezbookkeeping/graphs/contributors).
## Translating ## Translating
Help make ezBookkeeping accessible to users around the world. If you want to contribute a translation, please refer to our [translation guide](https://ezbookkeeping.mayswind.net/translating). Help make ezBookkeeping accessible to users around the world. We welcome help to improve existing translations or add new ones. If you would like to contribute a translation, please refer to the [translation guide](https://ezbookkeeping.mayswind.net/translating).
Currently available translations: Currently available translations:
@@ -136,8 +140,8 @@ Currently available translations:
| kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) | | kn | ಕನ್ನಡ | [@Darshanbm05](https://github.com/Darshanbm05) |
| ko | 한국어 | [@overworks](https://github.com/overworks) | | ko | 한국어 | [@overworks](https://github.com/overworks) |
| nl | Nederlands | [@automagics](https://github.com/automagics) | | nl | Nederlands | [@automagics](https://github.com/automagics) |
| pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus) | | pt-BR | Português (Brasil) | [@thecodergus](https://github.com/thecodergus), [@balaios](https://github.com/balaios) |
| ru | Русский | [@artegoser](https://github.com/artegoser) | | ru | Русский | [@artegoser](https://github.com/artegoser), [@dshemin](https://github.com/dshemin) |
| sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) | | sl | Slovenščina | [@thehijacker](https://github.com/thehijacker) |
| ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) | | ta | தமிழ் | [@hhharsha36](https://github.com/hhharsha36) |
| th | ไทย | [@natthavat28](https://github.com/natthavat28) | | th | ไทย | [@natthavat28](https://github.com/natthavat28) |
@@ -147,8 +151,6 @@ Currently available translations:
| zh-Hans | 中文 (简体) | / | | zh-Hans | 中文 (简体) | / |
| zh-Hant | 中文 (繁體) | / | | zh-Hant | 中文 (繁體) | / |
Don't see your language? Help us add it.
## Documentation ## Documentation
1. [English](https://ezbookkeeping.mayswind.net) 1. [English](https://ezbookkeeping.mayswind.net)
1. [中文 (简体)](https://ezbookkeeping.mayswind.net/zh_Hans) 1. [中文 (简体)](https://ezbookkeeping.mayswind.net/zh_Hans)
+16
View File
@@ -195,9 +195,25 @@ func getConfigWithoutSensitiveData(config *settings.Config) *settings.Config {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****" clonedConfig.ReceiptImageRecognitionLLMConfig.OpenAICompatibleAPIKey = "****"
} }
if clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicCompatibleAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicCompatibleAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.AnthropicAPIKey = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" { if clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****" clonedConfig.ReceiptImageRecognitionLLMConfig.OpenRouterAPIKey = "****"
} }
if clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.LMStudioToken = "****"
}
if clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey != "" {
clonedConfig.ReceiptImageRecognitionLLMConfig.GoogleAIAPIKey = "****"
}
} }
if clonedConfig.OAuth2ClientSecret != "" { if clonedConfig.OAuth2ClientSecret != "" {
+2 -1
View File
@@ -316,6 +316,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route := apiRoute.Group("/v1") apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config))) apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization(config)))
apiV1Route.Use(bindMiddleware(middlewares.APITokenIpLimit(config)))
{ {
// Tokens // Tokens
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler)) apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
@@ -396,7 +397,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
if config.EnableDataImport { if config.EnableDataImport {
apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler)) apiV1Route.POST("/transactions/parse_custom_file.json", bindApi(api.Transactions.TransactionParseImportCustomFileDataHandler))
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler)) apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler)) apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler)) apiV1Route.GET("/transactions/import/process.json", bindApi(api.Transactions.TransactionImportProcessHandler))
+37 -2
View File
@@ -169,7 +169,7 @@ transaction_from_ai_image_recognition = false
max_ai_recognition_picture_size = 10485760 max_ai_recognition_picture_size = 10485760
[llm_image_recognition] [llm_image_recognition]
# Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "openrouter", "ollama", "google_ai" # Large Language Model (LLM) provider for receipt image recognition, supports the following types: "openai", "openai_compatible", "anthropic", "anthropic_compatible", "openrouter", "ollama", "lm_studio", "google_ai"
llm_provider = llm_provider =
# For "openai" llm provider only, OpenAI API secret key, please visit https://platform.openai.com/api-keys for more information # For "openai" llm provider only, OpenAI API secret key, please visit https://platform.openai.com/api-keys for more information
@@ -187,6 +187,30 @@ openai_compatible_api_key =
# For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images # For "openai_compatible" llm provider only, receipt image recognition model for creating transactions from images
openai_compatible_model_id = openai_compatible_model_id =
# For "anthropic" llm provider only, Anthropic API key, please visit https://platform.claude.com/settings/keys for more information
anthropic_api_key =
# For "anthropic" llm provider only, receipt image recognition model for creating transactions from images
anthropic_model_id =
# For "anthropic" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
anthropic_max_tokens = 1024
# For "anthropic_compatible" llm provider only, Anthropic compatible API base url, e.g. "https://api.anthropic.com/v1/"
anthropic_compatible_base_url =
# For "anthropic_compatible" llm provider only, Anthropic compatible API version, e.g. "2023-06-01". If the LLM service does not require API versioning, leave it blank
anthropic_compatible_api_version =
# For "anthropic_compatible" llm provider only, Anthropic compatible API secret key
anthropic_compatible_api_key =
# For "anthropic_compatible" llm provider only, receipt image recognition model for creating transactions from images
anthropic_compatible_model_id =
# For "anthropic_compatible" llm provider only, maximum allowed number of generated tokens for creating transactions from images, default is 1024
anthropic_compatible_max_tokens = 1024
# For "openrouter" llm provider only, OpenRouter API key, please visit https://openrouter.ai/settings/keys for more information # For "openrouter" llm provider only, OpenRouter API key, please visit https://openrouter.ai/settings/keys for more information
openrouter_api_key = openrouter_api_key =
@@ -199,6 +223,15 @@ ollama_server_url =
# For "ollama" llm provider only, receipt image recognition model for creating transactions from images # For "ollama" llm provider only, receipt image recognition model for creating transactions from images
ollama_model_id = ollama_model_id =
# For "lm_studio" llm provider only, LM Studio server url, e.g. "http://127.0.0.1:1234/"
lm_studio_server_url =
# For "lm_studio" llm provider only, LM Studio API token, if "require authentication" is not enabled in LM Studio, leave it blank
lm_studio_token =
# For "lm_studio" llm provider only, receipt image recognition model for creating transactions from images
lm_studio_model_id =
# For "google_ai" llm provider only, Google AI Studio API key, please visit https://aistudio.google.com/apikey for more information # For "google_ai" llm provider only, Google AI Studio API key, please visit https://aistudio.google.com/apikey for more information
google_ai_api_key = google_ai_api_key =
@@ -263,6 +296,9 @@ password_reset_token_expired_time = 3600
# Set to true to enable API token generation # Set to true to enable API token generation
enable_api_token = false enable_api_token = false
# Allowed remote IPs for using the API token, a comma-separated list of allowed remote IPs (asterisk * for any addresses, e.g. 192.168.1.* means any IPs in the 192.168.1.x subnet), leave blank to allow all remote IPs
api_token_allowed_remote_ips =
# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable # Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable
max_failures_per_ip_per_minute = 5 max_failures_per_ip_per_minute = 5
@@ -493,7 +529,6 @@ custom_map_tile_server_default_zoom_level = 14
[exchange_rates] [exchange_rates]
# Exchange rates data source, supports the following types: # Exchange rates data source, supports the following types:
# "reserve_bank_of_australia": https://www.rba.gov.au/statistics/frequency/exchange-rates.html
# "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/ # "bank_of_canada": https://www.bankofcanada.ca/rates/exchange/daily-exchange-rates/
# "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/ # "czech_national_bank": https://www.cnb.cz/en/financial-markets/foreign-exchange-market/central-bank-exchange-rate-fixing/central-bank-exchange-rate-fixing/
# "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates # "danmarks_national_bank": https://www.nationalbanken.dk/en/what-we-do/stable-prices-monetary-policy-and-the-danish-economy/exchange-rates
+6 -3
View File
@@ -9,7 +9,8 @@
"lvdou-bing", "lvdou-bing",
"dshemin", "dshemin",
"lucdsouza", "lucdsouza",
"OuIChien" "OuIChien",
"RasterCrow"
], ],
"translators": { "translators": {
"de": [ "de": [
@@ -41,10 +42,12 @@
"automagics" "automagics"
], ],
"pt-BR": [ "pt-BR": [
"thecodergus" "thecodergus",
"balaios"
], ],
"ru": [ "ru": [
"artegoser" "artegoser",
"dshemin"
], ],
"sl": [ "sl": [
"thehijacker" "thehijacker"
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"github.com/mayswind/ezbookkeeping/cmd" "github.com/mayswind/ezbookkeeping/cmd"
"github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
@@ -26,9 +26,9 @@ var (
) )
func main() { func main() {
settings.Version = Version core.Version = Version
settings.CommitHash = CommitHash core.CommitHash = CommitHash
settings.BuildTime = BuildUnixTime core.BuildTime = BuildUnixTime
cmd := &cli.Command{ cmd := &cli.Command{
Name: "ezBookkeeping", Name: "ezBookkeeping",
+20 -19
View File
@@ -9,26 +9,26 @@ require (
github.com/gin-contrib/cache v1.4.1 github.com/gin-contrib/cache v1.4.1
github.com/gin-contrib/gzip v1.2.5 github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron/v2 v2.18.2 github.com/go-co-op/gocron/v2 v2.19.1
github.com/go-playground/validator/v10 v10.28.0 github.com/go-playground/validator/v10 v10.30.1
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/invopop/jsonschema v0.13.0 github.com/invopop/jsonschema v0.13.0
github.com/lib/pq v1.10.9 github.com/lib/pq v1.11.1
github.com/mattn/go-sqlite3 v1.14.32 github.com/mattn/go-sqlite3 v1.14.33
github.com/minio/minio-go/v7 v7.0.97 github.com/minio/minio-go/v7 v7.0.98
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.4
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.1 github.com/urfave/cli/v3 v3.6.2
github.com/wk8/go-ordered-map/v2 v2.1.8 github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xuri/excelize/v2 v2.10.0 github.com/xuri/excelize/v2 v2.10.0
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.47.0
golang.org/x/net v0.48.0 golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0 golang.org/x/oauth2 v0.34.0
golang.org/x/text v0.32.0 golang.org/x/text v0.33.0
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.1
gopkg.in/mail.v2 v2.3.1 gopkg.in/mail.v2 v2.3.1
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.3.11 xorm.io/xorm v1.3.11
@@ -52,7 +52,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect github.com/extrame/goyymmdd v0.0.0-20210114090516-7cc815f00d1a // indirect
github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -65,14 +65,14 @@ require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/memcachier/mc/v3 v3.0.3 // indirect github.com/memcachier/mc/v3 v3.0.3 // indirect
github.com/minio/crc64nvme v1.1.0 // indirect github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -91,18 +91,19 @@ require (
github.com/syndtr/goleveldb v1.0.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/tealeg/xlsx v1.0.5 // indirect github.com/tealeg/xlsx v1.0.5 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // indirect github.com/tiendc/go-deepcopy v1.7.1 // indirect
github.com/tinylib/msgp v1.3.0 // indirect github.com/tinylib/msgp v1.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/xuri/efp v0.0.1 // indirect github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect golang.org/x/arch v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/tools v0.39.0 // indirect golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+42 -39
View File
@@ -43,8 +43,8 @@ github.com/extrame/ole2 v0.0.0-20160812065207-d69429661ad7/go.mod h1:GPpMrAfHdb8
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg= github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b h1:jqW/h4gcXYEB6kVf6iuxjU9ONWA0ugUB94TP9UNmgdg=
github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI= github.com/extrame/xls v0.0.2-0.20200426124601-4a6cf263071b/go.mod h1:iACcgahst7BboCpIMSpnFs4SKyU9ZjsvZBfNbUxZOJI=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4= github.com/gin-contrib/cache v1.4.1 h1:HcLwLfw7p+FasNp5VAnFbbBj9SzB4bDtswvon7wYSg4=
github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM= github.com/gin-contrib/cache v1.4.1/go.mod h1:tykDV+FgItJHYEO0eCasuRYsZKPPyb4BYhAjuTlG6RM=
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
@@ -53,8 +53,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-co-op/gocron/v2 v2.18.2 h1:+5VU41FUXPWSPKLXZQ/77SGzUiPCcakU0v7ENc2H20Q= github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
github.com/go-co-op/gocron/v2 v2.18.2/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
@@ -63,16 +63,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@@ -90,8 +90,8 @@ github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7X
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -104,22 +104,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4= github.com/memcachier/mc/v3 v3.0.3 h1:qii+lDiPKi36O4Xg+HVKwHu6Oq+Gt17b+uEiA0Drwv4=
github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug= github.com/memcachier/mc/v3 v3.0.3/go.mod h1:GzjocBahcXPxt2cmqzknrgqCOmMxiSzhVKPOe90Tpug=
github.com/minio/crc64nvme v1.1.0 h1:e/tAguZ+4cw32D+IO/8GSf5UVr9y+3eJcxZI2WOO/7Q= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.0/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ= github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk= github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -155,16 +155,18 @@ github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
@@ -173,14 +175,14 @@ github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM= github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
@@ -190,32 +192,33 @@ github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstf
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU= github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
@@ -223,8 +226,8 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+2022 -1518
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "ezbookkeeping", "name": "ezbookkeeping",
"version": "1.3.2", "version": "1.4.0",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@@ -21,16 +21,16 @@
"dependencies": { "dependencies": {
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@vuepic/vue-datepicker": "^12.1.0", "@vuepic/vue-datepicker": "^12.1.0",
"axios": "^1.13.2", "axios": "^1.13.4",
"cbor-js": "^0.1.0", "cbor-js": "^0.1.0",
"chardet": "^2.1.1", "chardet": "^2.1.1",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dom7": "^4.0.6", "dom7": "^4.0.6",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"framework7": "^9.0.2", "framework7": "^9.0.3",
"framework7-icons": "^5.0.5", "framework7-icons": "^5.0.5",
"framework7-vue": "^9.0.2", "framework7-vue": "^9.0.3",
"jalaali-js": "^1.2.8", "jalaali-js": "^1.2.8",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"line-awesome": "^1.3.0", "line-awesome": "^1.3.0",
@@ -39,19 +39,19 @@
"pinia": "^3.0.4", "pinia": "^3.0.4",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"skeleton-elements": "^4.0.1", "skeleton-elements": "^4.0.1",
"swiper": "^12.0.3", "swiper": "^12.1.0",
"ua-parser-js": "^1.0.39", "ua-parser-js": "^1.0.39",
"vue": "^3.5.25", "vue": "^3.5.27",
"vue-echarts": "^8.0.1", "vue-echarts": "^8.0.1",
"vue-i18n": "^11.2.2", "vue-i18n": "^11.2.8",
"vue-router": "^4.6.4", "vue-router": "^5.0.2",
"vue3-perfect-scrollbar": "^2.0.0", "vue3-perfect-scrollbar": "^2.0.0",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"vuetify": "^3.11.3" "vuetify": "^3.11.8"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^30.2.0", "@jest/globals": "^30.2.0",
"@tsconfig/node24": "^24.0.3", "@tsconfig/node24": "^24.0.4",
"@types/cbor-js": "^0.1.1", "@types/cbor-js": "^0.1.1",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/git-rev-sync": "^2.0.2", "@types/git-rev-sync": "^2.0.2",
@@ -59,24 +59,24 @@
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4",
"@vue/eslint-config-typescript": "^14.6.0", "@vue/eslint-config-typescript": "^14.6.0",
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-vue": "^10.6.2", "eslint-plugin-vue": "^10.7.0",
"git-rev-sync": "^3.0.2", "git-rev-sync": "^3.0.2",
"jest": "^30.2.0", "jest": "^30.2.0",
"postcss-preset-env": "^10.5.0", "postcss-preset-env": "^11.1.3",
"sass": "^1.96.0", "sass": "^1.97.3",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.7", "vite": "^7.3.1",
"vite-plugin-checker": "^0.12.0", "vite-plugin-checker": "^0.12.0",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-vuetify": "^2.1.2", "vite-plugin-vuetify": "^2.1.3",
"vue-tsc": "^3.1.8" "vue-tsc": "^3.2.4"
}, },
"browserslist": [ "browserslist": [
"last 5 Chrome versions", "last 5 Chrome versions",
+2 -3
View File
@@ -3,7 +3,6 @@ package api
import ( import (
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
) )
// HealthsApi represents health api // HealthsApi represents health api
@@ -18,8 +17,8 @@ var (
func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) { func (a *HealthsApi) HealthStatusHandler(c *core.WebContext) (any, *errs.Error) {
result := make(map[string]string) result := make(map[string]string)
result["version"] = settings.Version result["version"] = core.Version
result["commit"] = settings.CommitHash result["commit"] = core.CommitHash
result["status"] = "ok" result["status"] = "ok"
return result, nil return result, nil
+1 -1
View File
@@ -103,7 +103,7 @@ func (a *ModelContextProtocolAPI) InitializeHandler(c *core.WebContext, jsonRPCR
ServerInfo: &mcp.MCPImplementation{ ServerInfo: &mcp.MCPImplementation{
Name: mcpServerName, Name: mcpServerName,
Title: core.ApplicationName, Title: core.ApplicationName,
Version: settings.Version, Version: core.Version,
}, },
} }
+4 -5
View File
@@ -3,7 +3,6 @@ package api
import ( import (
"github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
) )
// SystemsApi represents system api // SystemsApi represents system api
@@ -18,11 +17,11 @@ var (
func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) { func (a *SystemsApi) VersionHandler(c *core.WebContext) (any, *errs.Error) {
result := make(map[string]string) result := make(map[string]string)
result["version"] = settings.Version result["version"] = core.Version
result["commitHash"] = settings.CommitHash result["commitHash"] = core.CommitHash
if settings.BuildTime != "" { if core.BuildTime != "" {
result["buildTime"] = settings.BuildTime result["buildTime"] = core.BuildTime
} }
return result, nil return result, nil
+4 -4
View File
@@ -69,10 +69,10 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
tokenResp.IsCurrent = true tokenResp.IsCurrent = true
} }
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != services.TokenUserAgentCreatedViaCli { if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != core.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForAPI tokenResp.UserAgent = core.TokenUserAgentForAPI
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli { } else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != core.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForMCP tokenResp.UserAgent = core.TokenUserAgentForMCP
} }
tokenResps[i] = tokenResp tokenResps[i] = tokenResp
+46 -2
View File
@@ -12,13 +12,15 @@ import (
// TransactionTagsApi represents transaction tag api // TransactionTagsApi represents transaction tag api
type TransactionTagsApi struct { type TransactionTagsApi struct {
tags *services.TransactionTagService tags *services.TransactionTagService
tagGroups *services.TransactionTagGroupService
} }
// Initialize a transaction tag api singleton instance // Initialize a transaction tag api singleton instance
var ( var (
TransactionTags = &TransactionTagsApi{ TransactionTags = &TransactionTagsApi{
tags: services.TransactionTags, tags: services.TransactionTags,
tagGroups: services.TransactionTagGroups,
} }
) )
@@ -78,6 +80,20 @@ func (a *TransactionTagsApi) TagCreateHandler(c *core.WebContext) (any, *errs.Er
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
if tagCreateReq.GroupId > 0 {
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagCreateHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
}
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId) maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateReq.GroupId)
if err != nil { if err != nil {
@@ -120,6 +136,20 @@ func (a *TransactionTagsApi) TagCreateBatchHandler(c *core.WebContext) (any, *er
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
if tagCreateBatchReq.GroupId > 0 {
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagCreateBatchReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagCreateBatchHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagCreateBatchReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagCreateBatchHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagCreateBatchReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
}
maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId) maxOrderId, err := a.tags.GetMaxDisplayOrder(c, uid, tagCreateBatchReq.GroupId)
if err != nil { if err != nil {
@@ -167,6 +197,20 @@ func (a *TransactionTagsApi) TagModifyHandler(c *core.WebContext) (any, *errs.Er
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
if tagModifyReq.GroupId != tag.TagGroupId && tagModifyReq.GroupId > 0 {
tagGroup, err := a.tagGroups.GetTagGroupByTagGroupId(c, uid, tagModifyReq.GroupId)
if err != nil {
log.Errorf(c, "[transaction_tags.TagModifyHandler] failed to get tag group \"id:%d\" for user \"uid:%d\", because %s", tagModifyReq.GroupId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if tagGroup == nil {
log.Warnf(c, "[transaction_tags.TagModifyHandler] the tag group \"id:%d\" does not exist for user \"uid:%d\"", tagModifyReq.GroupId, uid)
return nil, errs.ErrTransactionTagGroupNotFound
}
}
newTag := &models.TransactionTag{ newTag := &models.TransactionTag{
TagId: tag.TagId, TagId: tag.TagId,
Uid: uid, Uid: uid,
+149 -47
View File
@@ -192,7 +192,15 @@ func (a *TransactionsApi) TransactionListHandler(c *core.WebContext) (any, *errs
transactions = transactions[:transactionListReq.Count] transactions = transactions[:transactionListReq.Count]
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions = a.filterTransactions(c, uid, transactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -275,7 +283,15 @@ func (a *TransactionsApi) TransactionMonthListHandler(c *core.WebContext) (any,
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag) accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err := a.getTransactionEssentialDataByTransactionIds(c, user, transactions, transactionListReq.WithPictures, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactions = a.filterTransactions(c, uid, transactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionListReq.WithPictures, transactionListReq.TrimAccount, transactionListReq.TrimCategory, transactionListReq.TrimTag)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionMonthListHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -362,7 +378,25 @@ func (a *TransactionsApi) TransactionListAllHandler(c *core.WebContext) (any, *e
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, clientTimezone, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag) var accountMap map[int64]*models.Account
var categoryMap map[int64]*models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var allTransactionTagIds map[int64][]int64
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
if minTransactionTime == 0 && maxTransactionTime == math.MaxInt64 && len(allCategoryIds) < 1 && len(allAccountIds) < 1 && len(tagFilters) < 1 && transactionAllListReq.AmountFilter == "" && transactionAllListReq.Keyword == "" {
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionAllEssentialData(c, user, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
} else {
accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, err = a.getTransactionEssentialDataByTransactionIds(c, user, allTransactions, transactionAllListReq.WithPictures, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
}
if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allTransactions = a.filterTransactions(c, uid, allTransactions, accountMap)
transactionResult, err := a.getTransactionResponseListResult(c, user, allTransactions, accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, clientTimezone, transactionAllListReq.WithPictures, transactionAllListReq.TrimAccount, transactionAllListReq.TrimCategory, transactionAllListReq.TrimTag)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionListAllHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -441,7 +475,24 @@ func (a *TransactionsApi) TransactionReconciliationStatementHandler(c *core.WebC
transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance transactionAccountBalanceMap[transactionWithBalance.RelatedId] = transactionWithBalance
} }
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, clientTimezone, false, true, true, true) allAccountIds := make([]int64, 0, len(transactions)*2)
for i := 0; i < len(transactions); i++ {
allAccountIds = append(allAccountIds, transactions[i].AccountId)
if transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN || transactions[i].Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
allAccountIds = append(allAccountIds, transactions[i].RelatedAccountId)
}
}
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(allAccountIds))
if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to get essential data for assembling transaction result for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
transactionResult, err := a.getTransactionResponseListResult(c, user, transactions, allAccounts, nil, nil, nil, nil, clientTimezone, false, true, true, true)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionReconciliationStatementHandler] failed to assemble transaction result for user \"uid:%d\", because %s", uid, err.Error())
@@ -1401,13 +1452,13 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return true, nil return true, nil
} }
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user // TransactionParseImportCustomFileDataHandler returns the parsed file data by request parameters for current user
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) { func (a *TransactionsApi) TransactionParseImportCustomFileDataHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid() uid := c.GetCurrentUid()
form, err := c.MultipartForm() form, err := c.MultipartForm()
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrParameterInvalid return nil, errs.ErrParameterInvalid
} }
@@ -1419,18 +1470,18 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
fileType := fileTypes[0] fileType := fileTypes[0]
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) { if !converters.IsCustomFileFormatFileType(fileType) {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported) return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
} }
fileEncodings := form.Value["fileEncoding"] fileEncodings := form.Value["fileEncoding"]
fileEncoding := ""
if len(fileEncodings) < 1 || fileEncodings[0] == "" { if len(fileEncodings) > 0 {
return nil, errs.ErrImportFileEncodingIsEmpty fileEncoding = fileEncodings[0]
} }
fileEncoding := fileEncodings[0] dataParser, err := converters.CreateNewCustomFileFormatTransactionDataParser(fileType, fileEncoding)
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
if err != nil { if err != nil {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported) return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
@@ -1439,24 +1490,24 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
importFiles := form.File["file"] importFiles := form.File["file"]
if len(importFiles) < 1 { if len(importFiles) < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] there is no import file in request for user \"uid:%d\"", uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
return nil, errs.ErrNoFilesUpload return nil, errs.ErrNoFilesUpload
} }
if importFiles[0].Size < 1 { if importFiles[0].Size < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
return nil, errs.ErrUploadedFileEmpty return nil, errs.ErrUploadedFileEmpty
} }
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) { if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid) log.Warnf(c, "[transactions.TransactionParseImportCustomFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
return nil, errs.ErrExceedMaxUploadFileSize return nil, errs.ErrExceedMaxUploadFileSize
} }
importFile, err := importFiles[0].Open() importFile, err := importFiles[0].Open()
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed return nil, errs.ErrOperationFailed
} }
@@ -1464,14 +1515,14 @@ func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebCo
fileData, err := io.ReadAll(importFile) fileData, err := io.ReadAll(importFile)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
allLines, err := dataParser.ParseDsvFileLines(c, fileData) allLines, err := dataParser.ParseDataLines(c, fileData)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.TransactionParseImportCustomFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed) return nil, errs.Or(err, errs.ErrOperationFailed)
} }
@@ -1514,15 +1565,14 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
var dataImporter converter.TransactionDataImporter var dataImporter converter.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) { if converters.IsCustomFileFormatFileType(fileType) {
fileEncodings := form.Value["fileEncoding"] fileEncodings := form.Value["fileEncoding"]
fileEncoding := ""
if len(fileEncodings) < 1 || fileEncodings[0] == "" { if len(fileEncodings) > 0 {
return nil, errs.ErrImportFileEncodingIsEmpty fileEncoding = fileEncodings[0]
} }
fileEncoding := fileEncodings[0]
columnMappings := form.Value["columnMapping"] columnMappings := form.Value["columnMapping"]
if len(columnMappings) < 1 || columnMappings[0] == "" { if len(columnMappings) < 1 || columnMappings[0] == "" {
@@ -1606,7 +1656,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
transactionTagSeparator = transactionTagSeparators[0] transactionTagSeparator = transactionTagSeparators[0]
} }
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator) dataImporter, err = converters.CreateNewCustomTransactionDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else { } else {
dataImporter, err = converters.GetTransactionDataImporter(fileType) dataImporter, err = converters.GetTransactionDataImporter(fileType)
} }
@@ -1928,7 +1978,61 @@ func (a *TransactionsApi) getTransactionTagInfoResponses(tagIds []int64, allTran
return allTags return allTags
} }
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) { func (a *TransactionsApi) getTransactionAllEssentialData(c *core.WebContext, user *models.User, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
uid := user.Uid
allAccounts, err := a.accounts.GetAllAccountsByUid(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
accountMap = a.accounts.GetAccountMapByList(allAccounts)
allTagIndexes, err := a.transactionTags.GetAllTagIdsOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
allTransactionTagIds = a.transactionTags.GetGroupedTransactionTagIds(allTagIndexes)
if !trimCategory {
allCategories, err := a.transactionCategories.GetAllCategoriesByUid(c, uid, 0, -1)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
categoryMap = a.transactionCategories.GetCategoryMapByList(allCategories)
}
if !trimTag {
allTags, err := a.transactionTags.GetAllTagsByUid(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
tagMap = a.transactionTags.GetTagMapByList(allTags)
}
if withPictures && a.CurrentConfig().EnableTransactionPictures {
pictureInfoMap, err = a.transactionPictures.GetAllPictureInfosOfAllTransactions(c, uid)
if err != nil {
log.Errorf(c, "[transactions.getTransactionAllEssentialData] failed to get all transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, nil, nil, nil, nil, err
}
}
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
}
func (a *TransactionsApi) getTransactionEssentialDataByTransactionIds(c *core.WebContext, user *models.User, transactions []*models.Transaction, withPictures bool, trimCategory bool, trimTag bool) (accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, err error) {
uid := user.Uid uid := user.Uid
transactionIds := make([]int64, len(transactions)) transactionIds := make([]int64, len(transactions))
accountIds := make([]int64, 0, len(transactions)*2) accountIds := make([]int64, 0, len(transactions)*2)
@@ -1951,32 +2055,26 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
categoryIds = append(categoryIds, transactions[i].CategoryId) categoryIds = append(categoryIds, transactions[i].CategoryId)
} }
allAccounts, err := a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds)) accountMap, err = a.accounts.GetAccountsByAccountIds(c, uid, utils.ToUniqueInt64Slice(accountIds))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get accounts for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
transactions = a.filterTransactions(c, uid, transactions, allAccounts) allTransactionTagIds, err = a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
allTransactionTagIds, err := a.transactionTags.GetAllTagIdsOfTransactions(c, uid, transactionIds)
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tag ids for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
var categoryMap map[int64]*models.TransactionCategory
var tagMap map[int64]*models.TransactionTag
var pictureInfoMap map[int64][]*models.TransactionPictureInfo
if !trimCategory { if !trimCategory {
categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds)) categoryMap, err = a.transactionCategories.GetCategoriesByCategoryIds(c, uid, utils.ToUniqueInt64Slice(categoryIds))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions categories for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
@@ -1984,8 +2082,8 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds))) tagMap, err = a.transactionTags.GetTagsByTagIds(c, uid, utils.ToUniqueInt64Slice(a.transactionTags.GetTransactionTagIds(allTransactionTagIds)))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions tags for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
@@ -1993,11 +2091,15 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions))) pictureInfoMap, err = a.transactionPictures.GetPictureInfosByTransactionIds(c, uid, utils.ToUniqueInt64Slice(a.transactions.GetTransactionIds(transactions)))
if err != nil { if err != nil {
log.Errorf(c, "[transactions.getTransactionResponseListResult] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error()) log.Errorf(c, "[transactions.getTransactionEssentialDataByTransactionIds] failed to get transactions pictures for user \"uid:%d\", because %s", uid, err.Error())
return nil, err return nil, nil, nil, nil, nil, err
} }
} }
return accountMap, categoryMap, tagMap, allTransactionTagIds, pictureInfoMap, nil
}
func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, user *models.User, transactions []*models.Transaction, allAccounts map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTransactionTagIds map[int64][]int64, pictureInfoMap map[int64][]*models.TransactionPictureInfo, clientTimezone *time.Location, withPictures bool, trimAccount bool, trimCategory bool, trimTag bool) (models.TransactionInfoResponseSlice, error) {
result := make(models.TransactionInfoResponseSlice, len(transactions)) result := make(models.TransactionInfoResponseSlice, len(transactions))
for i := 0; i < len(transactions); i++ { for i := 0; i < len(transactions); i++ {
@@ -2021,17 +2123,17 @@ func (a *TransactionsApi) getTransactionResponseListResult(c *core.WebContext, u
} }
} }
if !trimCategory { if !trimCategory && categoryMap != nil {
if category := categoryMap[transaction.CategoryId]; category != nil { if category := categoryMap[transaction.CategoryId]; category != nil {
result[i].Category = category.ToTransactionCategoryInfoResponse() result[i].Category = category.ToTransactionCategoryInfoResponse()
} }
} }
if !trimTag { if !trimTag && tagMap != nil && transactionTagIds != nil {
result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap) result[i].Tags = a.getTransactionTagInfoResponses(transactionTagIds, tagMap)
} }
if withPictures && a.CurrentConfig().EnableTransactionPictures { if withPictures && a.CurrentConfig().EnableTransactionPictures && pictureInfoMap != nil {
pictureInfos, exists := pictureInfoMap[transaction.TransactionId] pictureInfos, exists := pictureInfoMap[transaction.TransactionId]
if exists { if exists {
+1 -1
View File
@@ -67,7 +67,7 @@ func InitializeOAuth2Provider(config *settings.Config) error {
Container.current = oauth2Provider Container.current = oauth2Provider
Container.usePKCE = config.OAuth2UsePKCE Container.usePKCE = config.OAuth2UsePKCE
Container.oauth2HttpClient = httpclient.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, settings.GetUserAgent(), config.EnableDebugLog) Container.oauth2HttpClient = httpclient.NewHttpClient(config.OAuth2RequestTimeout, config.OAuth2Proxy, config.OAuth2SkipTLSVerify, core.GetOutgoingUserAgent(), config.EnableDebugLog)
Container.externalUserAuthType = externalUserAuthType Container.externalUserAuthType = externalUserAuthType
return nil return nil
+1 -1
View File
@@ -150,7 +150,7 @@ func (l *UserDataCli) ModifyUserPassword(c *core.CliContext, username string, pa
Password: password, Password: password,
} }
_, _, err = l.users.UpdateUser(c, userNew, false) err = l.users.UpdateUserPassword(c, userNew)
if err != nil { if err != nil {
log.CliErrorf(c, "[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error()) log.CliErrorf(c, "[user_data.ModifyUserPassword] failed to update user \"%s\" password, because %s", user.Username, err.Error())
@@ -0,0 +1,8 @@
package custom
import "github.com/mayswind/ezbookkeeping/pkg/core"
// CustomTransactionDataParser represents the parser for custom transaction data files
type CustomTransactionDataParser interface {
ParseDataLines(ctx core.Context, data []byte) ([][]string, error)
}
@@ -1,4 +1,4 @@
package dsv package custom
import ( import (
"bytes" "bytes"
@@ -14,6 +14,7 @@ import (
"golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese" "golang.org/x/text/encoding/traditionalchinese"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/encoding/unicode/utf32"
"golang.org/x/text/transform" "golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
@@ -33,58 +34,57 @@ var supportedFileTypeSeparators = map[string]rune{
} }
var supportedFileEncodings = map[string]encoding.Encoding{ var supportedFileEncodings = map[string]encoding.Encoding{
"utf-8": unicode.UTF8, // UTF-8 "utf-8": unicode.UTF8BOM, // UTF-8
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM "utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), // UTF-16 Little Endian
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.UseBOM), // UTF-16 Little Endian "utf-16be": unicode.UTF16(unicode.BigEndian, unicode.UseBOM), // UTF-16 Big Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.UseBOM), // UTF-16 Big Endian "utf-32le": utf32.UTF32(utf32.LittleEndian, utf32.UseBOM), // UTF-32 Little Endian
"utf-16le-bom": unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM), // UTF-16 Little Endian with BOM "utf-32be": utf32.UTF32(utf32.BigEndian, utf32.UseBOM), // UTF-32 Big Endian
"utf-16be-bom": unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM), // UTF-16 Big Endian with BOM "cp437": charmap.CodePage437, // OEM United States (CP-437)
"cp437": charmap.CodePage437, // OEM United States (CP-437) "cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863) "cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037) "cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047) "cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140) "iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1) "cp850": charmap.CodePage850, // Western European (CP-850)
"cp850": charmap.CodePage850, // Western European (CP-850) "cp858": charmap.CodePage858, // Western European with Euro (CP-858)
"cp858": charmap.CodePage858, // Western European with Euro (CP-858) "windows-1252": charmap.Windows1252, // Western European (Windows-1252)
"windows-1252": charmap.Windows1252, // Western European (Windows-1252) "iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15) "iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4) "iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10) "cp865": charmap.CodePage865, // North European (CP-865)
"cp865": charmap.CodePage865, // North European (CP-865) "iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2) "cp852": charmap.CodePage852, // Central European (CP-852)
"cp852": charmap.CodePage852, // Central European (CP-852) "windows-1250": charmap.Windows1250, // Central European (Windows-1250)
"windows-1250": charmap.Windows1250, // Central European (Windows-1250) "iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14) "iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3) "cp860": charmap.CodePage860, // Portuguese (CP-860)
"cp860": charmap.CodePage860, // Portuguese (CP-860) "iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7) "windows-1253": charmap.Windows1253, // Greek (Windows-1253)
"windows-1253": charmap.Windows1253, // Greek (Windows-1253) "iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9) "windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254) "iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13) "windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257) "iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16) "iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5) "cp855": charmap.CodePage855, // Cyrillic (CP-855)
"cp855": charmap.CodePage855, // Cyrillic (CP-855) "cp866": charmap.CodePage866, // Cyrillic (CP-866)
"cp866": charmap.CodePage866, // Cyrillic (CP-866) "windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251) "koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R) "koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U) "iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6) "windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256) "iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8) "cp862": charmap.CodePage862, // Hebrew (CP-862)
"cp862": charmap.CodePage862, // Hebrew (CP-862) "windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255) "windows-874": charmap.Windows874, // Thai (Windows-874)
"windows-874": charmap.Windows874, // Thai (Windows-874) "windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258) "gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030)
"gb18030": simplifiedchinese.GB18030, // Chinese (Simplified, GB18030) "gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK)
"gbk": simplifiedchinese.GBK, // Chinese (Simplified, GBK) "big5": traditionalchinese.Big5, // Chinese (Traditional, Big5)
"big5": traditionalchinese.Big5, // Chinese (Traditional, Big5) "euc-kr": korean.EUCKR, // Korean (EUC-KR)
"euc-kr": korean.EUCKR, // Korean (EUC-KR) "euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP) "iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP) "shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
} }
var customTransactionTypeNameMapping = map[models.TransactionType]string{ var customTransactionTypeNameMapping = map[models.TransactionType]string{
@@ -94,10 +94,6 @@ var customTransactionTypeNameMapping = map[models.TransactionType]string{
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)), models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
} }
type CustomTransactionDataDsvFileParser interface {
ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error)
}
// customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data // customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data
type customTransactionDataDsvFileImporter struct { type customTransactionDataDsvFileImporter struct {
fileEncoding encoding.Encoding fileEncoding encoding.Encoding
@@ -114,8 +110,8 @@ type customTransactionDataDsvFileImporter struct {
transactionTagSeparator string transactionTagSeparator string
} }
// ParseDsvFileLines returns the parsed file lines for specified the dsv file data // ParseDataLines returns the parsed file lines for specified the dsv file data
func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error) { func (c *customTransactionDataDsvFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder()) reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder())
csvReader := csv.NewReader(reader) csvReader := csv.NewReader(reader)
csvReader.Comma = c.separator csvReader.Comma = c.separator
@@ -131,7 +127,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex
} }
if err != nil { if err != nil {
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error()) log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDataLines] cannot parse dsv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile return nil, errs.ErrInvalidCSVFile
} }
@@ -151,7 +147,7 @@ func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Contex
// ParseImportedData returns the imported data by parsing the custom transaction dsv data // ParseImportedData returns the imported data by parsing the custom transaction dsv data
func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) { func (c *customTransactionDataDsvFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
allLines, err := c.ParseDsvFileLines(ctx, data) allLines, err := c.ParseDataLines(ctx, data)
if err != nil { if err != nil {
return nil, nil, nil, nil, nil, nil, err return nil, nil, nil, nil, nil, nil, err
@@ -170,14 +166,18 @@ func IsDelimiterSeparatedValuesFileType(fileType string) bool {
return exists return exists
} }
// CreateNewCustomTransactionDataDsvFileParser returns a new custom dsv parser for transaction data // CreateNewCustomTransactionDataDsvFileParser returns a new custom transaction data parser
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataDsvFileParser, error) { func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataParser, error) {
separator, exists := supportedFileTypeSeparators[fileType] separator, exists := supportedFileTypeSeparators[fileType]
if !exists { if !exists {
return nil, errs.ErrImportFileTypeNotSupported return nil, errs.ErrImportFileTypeNotSupported
} }
if fileEncoding == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
enc, exists := supportedFileEncodings[fileEncoding] enc, exists := supportedFileEncodings[fileEncoding]
if !exists { if !exists {
@@ -198,6 +198,10 @@ func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding
return nil, errs.ErrImportFileTypeNotSupported return nil, errs.ErrImportFileTypeNotSupported
} }
if fileEncoding == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
enc, exists := supportedFileEncodings[fileEncoding] enc, exists := supportedFileEncodings[fileEncoding]
if !exists { if !exists {
@@ -1,4 +1,4 @@
package dsv package custom
import ( import (
"testing" "testing"
@@ -25,13 +25,13 @@ func TestIsDelimiterSeparatedValuesFileType(t *testing.T) {
assert.False(t, IsDelimiterSeparatedValuesFileType("ssv")) assert.False(t, IsDelimiterSeparatedValuesFileType("ssv"))
} }
func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) { func TestCustomTransactionDataDsvFileParser_ParseDataLines(t *testing.T) {
importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8") importer, err := CreateNewCustomTransactionDataDsvFileParser("custom_csv", "utf-8")
assert.Nil(t, err) assert.Nil(t, err)
context := core.NewNullContext() context := core.NewNullContext()
allLines, err := importer.ParseDsvFileLines(context, []byte( allLines, err := importer.ParseDataLines(context, []byte(
"2024-09-01 00:00:00,B,123.45\n"+ "2024-09-01 00:00:00,B,123.45\n"+
"2024-09-01 01:23:45,I,0.12\n")) "2024-09-01 01:23:45,I,0.12\n"))
assert.Nil(t, err) assert.Nil(t, err)
@@ -51,7 +51,7 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8") importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_tsv", "utf-8")
assert.Nil(t, err) assert.Nil(t, err)
allLines, err = importer.ParseDsvFileLines(context, []byte( allLines, err = importer.ParseDataLines(context, []byte(
"2024-09-01 12:34:56\tE\t1.00\n"+ "2024-09-01 12:34:56\tE\t1.00\n"+
"2024-09-01 23:59:59\tT\t0.05")) "2024-09-01 23:59:59\tT\t0.05"))
assert.Nil(t, err) assert.Nil(t, err)
@@ -71,7 +71,7 @@ func TestCustomTransactionDataDsvFileParser_ParseDsvFileLines(t *testing.T) {
importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_ssv", "utf-8") importer, err = CreateNewCustomTransactionDataDsvFileParser("custom_ssv", "utf-8")
assert.Nil(t, err) assert.Nil(t, err)
allLines, err = importer.ParseDsvFileLines(context, []byte( allLines, err = importer.ParseDataLines(context, []byte(
"2024-09-01 12:34:56;E;1.00\n"+ "2024-09-01 12:34:56;E;1.00\n"+
"2024-09-01 23:59:59;T;0.05")) "2024-09-01 23:59:59;T;0.05"))
assert.Nil(t, err) assert.Nil(t, err)
@@ -0,0 +1,137 @@
package custom
import (
"strings"
"time"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/excel"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const customOOXMLExcelFileType = "custom_xlsx"
const customMSCFBExcelFileType = "custom_xls"
// customTransactionDataExcelFileImporter defines the structure of custom excel importer for transaction data
type customTransactionDataExcelFileImporter struct {
fileType string
columnIndexMapping map[datatable.TransactionDataTableColumn]int
transactionTypeNameMapping map[string]models.TransactionType
hasHeaderLine bool
timeFormat string
timezoneFormat string
amountDecimalSeparator string
amountDigitGroupingSymbol string
geoLocationSeparator string
geoLocationOrder converter.TransactionGeoLocationOrder
transactionTagSeparator string
}
// ParseDataLines returns the parsed file lines for specified the excel file data
func (c *customTransactionDataExcelFileImporter) ParseDataLines(ctx core.Context, data []byte) ([][]string, error) {
var excelDataTable datatable.BasicDataTable
var err error
if c.fileType == customOOXMLExcelFileType {
excelDataTable, err = excel.CreateNewExcelOOXMLFileBasicDataTable(data, false)
} else if c.fileType == customMSCFBExcelFileType {
excelDataTable, err = excel.CreateNewExcelMSCFBFileBasicDataTable(data, false)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
if err != nil {
return nil, err
}
iterator := excelDataTable.DataRowIterator()
allLines := make([][]string, 0)
for iterator.HasNext() {
row := iterator.Next()
items := make([]string, row.ColumnCount())
for i := 0; i < row.ColumnCount(); i++ {
items[i] = strings.Trim(row.GetData(i), " ")
}
allLines = append(allLines, items)
}
return allLines, nil
}
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
func (c *customTransactionDataExcelFileImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezone *time.Location, additionalOptions converter.TransactionDataImporterOptions, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
allLines, err := c.ParseDataLines(ctx, data)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
dataTable := csvconverter.CreateNewCustomCsvBasicDataTable(allLines, c.hasHeaderLine)
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat, c.amountDecimalSeparator, c.amountDigitGroupingSymbol)
dataTableImporter := converter.CreateNewImporterWithTypeNameMapping(customTransactionTypeNameMapping, c.geoLocationSeparator, c.geoLocationOrder, c.transactionTagSeparator)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezone, additionalOptions, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
// IsCustomExcelFileType returns whether the file type is the custom excel file type
func IsCustomExcelFileType(fileType string) bool {
return fileType == customOOXMLExcelFileType || fileType == customMSCFBExcelFileType
}
// CreateNewCustomTransactionDataExcelFileParser returns a new custom transaction data parser
func CreateNewCustomTransactionDataExcelFileParser(fileType string) (CustomTransactionDataParser, error) {
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
return nil, errs.ErrImportFileTypeNotSupported
}
return &customTransactionDataExcelFileImporter{
fileType: fileType,
}, nil
}
// CreateNewCustomTransactionDataExcelFileImporter returns a new custom excel importer for transaction data
func CreateNewCustomTransactionDataExcelFileImporter(fileType string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) {
if fileType != customOOXMLExcelFileType && fileType != customMSCFBExcelFileType {
return nil, errs.ErrImportFileTypeNotSupported
}
if geoLocationOrder == "" {
geoLocationOrder = string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE)
} else if geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LONGITUDE_LATITUDE) &&
geoLocationOrder != string(converter.TRANSACTION_GEO_LOCATION_ORDER_LATITUDE_LONGITUDE) {
return nil, errs.ErrImportFileTypeNotSupported
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists := columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_AMOUNT]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
return &customTransactionDataExcelFileImporter{
fileType: fileType,
columnIndexMapping: columnIndexMapping,
transactionTypeNameMapping: transactionTypeNameMapping,
hasHeaderLine: hasHeaderLine,
timeFormat: timeFormat,
timezoneFormat: timezoneFormat,
amountDecimalSeparator: amountDecimalSeparator,
amountDigitGroupingSymbol: amountDigitGroupingSymbol,
geoLocationSeparator: geoLocationSeparator,
geoLocationOrder: converter.TransactionGeoLocationOrder(geoLocationOrder),
transactionTagSeparator: transactionTagSeparator,
}, nil
}
@@ -0,0 +1,254 @@
package custom
import (
"os"
"testing"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/stretchr/testify/assert"
)
func TestIsCustomExcelFileType(t *testing.T) {
assert.True(t, IsCustomExcelFileType("custom_xlsx"))
assert.True(t, IsCustomExcelFileType("custom_xls"))
assert.False(t, IsCustomExcelFileType("xlsx"))
assert.False(t, IsCustomExcelFileType("xls"))
assert.False(t, IsCustomExcelFileType("excel"))
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_EmptyData(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 0, len(allLines))
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_SingleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 3, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "A2", allLines[1][0])
assert.Equal(t, "B2", allLines[1][1])
assert.Equal(t, "C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "A3", allLines[2][0])
assert.Equal(t, "B3", allLines[2][1])
assert.Equal(t, "C3", allLines[2][2])
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 9, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "1-A2", allLines[1][0])
assert.Equal(t, "1-B2", allLines[1][1])
assert.Equal(t, "1-C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "1-A3", allLines[2][0])
assert.Equal(t, "1-B3", allLines[2][1])
assert.Equal(t, "1-C3", allLines[2][2])
assert.Equal(t, 3, len(allLines[3]))
assert.Equal(t, "A1", allLines[3][0])
assert.Equal(t, "B1", allLines[3][1])
assert.Equal(t, "C1", allLines[3][2])
assert.Equal(t, 2, len(allLines[4]))
assert.Equal(t, "3-A2", allLines[4][0])
assert.Equal(t, "3-B2", allLines[4][1])
assert.Equal(t, 3, len(allLines[5]))
assert.Equal(t, "A1", allLines[5][0])
assert.Equal(t, "B1", allLines[5][1])
assert.Equal(t, "C1", allLines[5][2])
assert.Equal(t, 3, len(allLines[6]))
assert.Equal(t, "A1", allLines[6][0])
assert.Equal(t, "B1", allLines[6][1])
assert.Equal(t, "C1", allLines[6][2])
assert.Equal(t, 3, len(allLines[7]))
assert.Equal(t, "5-A2", allLines[7][0])
assert.Equal(t, "5-B2", allLines[7][1])
assert.Equal(t, "5-C2", allLines[7][2])
assert.Equal(t, 3, len(allLines[8]))
assert.Equal(t, "5-A3", allLines[8][0])
assert.Equal(t, "5-B3", allLines[8][1])
assert.Equal(t, "5-C3", allLines[8][2])
}
func TestCustomTransactionDataParser_ParseOOXMLExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xlsx")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx")
assert.Nil(t, err)
_, err = importer.ParseDataLines(context, testdata)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_EmptyData(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 0, len(allLines))
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_SingleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 3, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "A2", allLines[1][0])
assert.Equal(t, "B2", allLines[1][1])
assert.Equal(t, "C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "A3", allLines[2][0])
assert.Equal(t, "B3", allLines[2][1])
assert.Equal(t, "C3", allLines[2][2])
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheet(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xls")
assert.Nil(t, err)
allLines, err := importer.ParseDataLines(context, testdata)
assert.Nil(t, err)
assert.Equal(t, 9, len(allLines))
assert.Equal(t, 3, len(allLines[0]))
assert.Equal(t, "A1", allLines[0][0])
assert.Equal(t, "B1", allLines[0][1])
assert.Equal(t, "C1", allLines[0][2])
assert.Equal(t, 3, len(allLines[1]))
assert.Equal(t, "1-A2", allLines[1][0])
assert.Equal(t, "1-B2", allLines[1][1])
assert.Equal(t, "1-C2", allLines[1][2])
assert.Equal(t, 3, len(allLines[2]))
assert.Equal(t, "1-A3", allLines[2][0])
assert.Equal(t, "1-B3", allLines[2][1])
assert.Equal(t, "1-C3", allLines[2][2])
assert.Equal(t, 3, len(allLines[3]))
assert.Equal(t, "A1", allLines[3][0])
assert.Equal(t, "B1", allLines[3][1])
assert.Equal(t, "C1", allLines[3][2])
assert.Equal(t, 3, len(allLines[4]))
assert.Equal(t, "3-A2", allLines[4][0])
assert.Equal(t, "3-B2", allLines[4][1])
assert.Equal(t, "", allLines[4][2])
assert.Equal(t, 3, len(allLines[5]))
assert.Equal(t, "A1", allLines[5][0])
assert.Equal(t, "B1", allLines[5][1])
assert.Equal(t, "C1", allLines[5][2])
assert.Equal(t, 3, len(allLines[6]))
assert.Equal(t, "A1", allLines[6][0])
assert.Equal(t, "B1", allLines[6][1])
assert.Equal(t, "C1", allLines[6][2])
assert.Equal(t, 3, len(allLines[7]))
assert.Equal(t, "5-A2", allLines[7][0])
assert.Equal(t, "5-B2", allLines[7][1])
assert.Equal(t, "5-C2", allLines[7][2])
assert.Equal(t, 3, len(allLines[8]))
assert.Equal(t, "5-A3", allLines[8][0])
assert.Equal(t, "5-B3", allLines[8][1])
assert.Equal(t, "5-C3", allLines[8][2])
}
func TestCustomTransactionDataParser_ParseMSCFBExcelDataLines_MultipleSheetWithDifferentColumnCount(t *testing.T) {
importer, err := CreateNewCustomTransactionDataExcelFileParser("custom_xls")
assert.Nil(t, err)
context := core.NewNullContext()
testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xls")
assert.Nil(t, err)
_, err = importer.ParseDataLines(context, testdata)
assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message)
}
@@ -1,4 +1,4 @@
package dsv package custom
import ( import (
"strings" "strings"
@@ -86,7 +86,7 @@ func (t *ExcelMSCFBFileBasicDataTable) DataRowIterator() datatable.BasicDataTabl
// ColumnCount returns the total count of column in this data row // ColumnCount returns the total count of column in this data row
func (r *ExcelMSCFBFileBasicDataTableRow) ColumnCount() int { func (r *ExcelMSCFBFileBasicDataTableRow) ColumnCount() int {
row := r.sheet.Row(r.rowIndex) row := r.sheet.Row(r.rowIndex)
return row.LastCol() + 1 return row.LastCol()
} }
// GetData returns the data in the specified column index // GetData returns the data in the specified column index
@@ -195,7 +195,10 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
} }
if i == 0 { if i == 0 {
for j := 0; j <= row.LastCol(); j++ { // row.LastCol() returns "colMac" in the "Row" struct, that is an unsigned integer that specifies the one-based index of the last column.
// But row.FirstCol() returns "colMic" in the "Row" struct, that is an unsigned integer that specifies the zero-based index of the first column.
// Reference: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/4aab09eb-49ed-4d01-a3b1-1d726247d3c2
for j := 0; j < row.LastCol(); j++ {
headerItem := row.Col(j) headerItem := row.Col(j)
if headerItem == "" { if headerItem == "" {
@@ -205,7 +208,7 @@ func CreateNewExcelMSCFBFileBasicDataTable(data []byte, hasTitleLine bool) (data
firstRowItems = append(firstRowItems, headerItem) firstRowItems = append(firstRowItems, headerItem)
} }
} else { } else {
for j := 0; j <= min(row.LastCol(), len(firstRowItems)-1); j++ { for j := 0; j < min(row.LastCol(), len(firstRowItems)); j++ {
headerItem := row.Col(j) headerItem := row.Col(j)
if headerItem != firstRowItems[j] { if headerItem != firstRowItems[j] {
@@ -300,10 +300,10 @@ func TestExcelMSCFBFileBasicDataRowColumnCount(t *testing.T) {
iterator := datatable.DataRowIterator() iterator := datatable.DataRowIterator()
row1 := iterator.Next() row1 := iterator.Next()
assert.EqualValues(t, 4, row1.ColumnCount()) assert.EqualValues(t, 3, row1.ColumnCount())
row2 := iterator.Next() row2 := iterator.Next()
assert.EqualValues(t, 4, row2.ColumnCount()) assert.EqualValues(t, 3, row2.ColumnCount())
} }
func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) { func TestExcelMSCFBFileBasicDataRowGetData(t *testing.T) {
+22 -10
View File
@@ -5,9 +5,9 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/converters/beancount" "github.com/mayswind/ezbookkeeping/pkg/converters/beancount"
"github.com/mayswind/ezbookkeeping/pkg/converters/camt" "github.com/mayswind/ezbookkeeping/pkg/converters/camt"
"github.com/mayswind/ezbookkeeping/pkg/converters/converter" "github.com/mayswind/ezbookkeeping/pkg/converters/converter"
"github.com/mayswind/ezbookkeeping/pkg/converters/custom"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable" "github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/default" "github.com/mayswind/ezbookkeeping/pkg/converters/default"
"github.com/mayswind/ezbookkeeping/pkg/converters/dsv"
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee" "github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII" "github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash" "github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
@@ -85,17 +85,29 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
} }
} }
// IsCustomDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type // IsCustomFileFormatFileType returns whether the file type is the custom file format
func IsCustomDelimiterSeparatedValuesFileType(fileType string) bool { func IsCustomFileFormatFileType(fileType string) bool {
return dsv.IsDelimiterSeparatedValuesFileType(fileType) return custom.IsDelimiterSeparatedValuesFileType(fileType) || custom.IsCustomExcelFileType(fileType)
} }
// CreateNewDelimiterSeparatedValuesDataParser returns a new delimiter-separated values data parser according to the file type and encoding // CreateNewCustomFileFormatTransactionDataParser returns a new custom transaction data parser according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataParser(fileType string, fileEncoding string) (dsv.CustomTransactionDataDsvFileParser, error) { func CreateNewCustomFileFormatTransactionDataParser(fileType string, fileEncoding string) (custom.CustomTransactionDataParser, error) {
return dsv.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding) if custom.IsDelimiterSeparatedValuesFileType(fileType) {
return custom.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding)
} else if custom.IsCustomExcelFileType(fileType) {
return custom.CreateNewCustomTransactionDataExcelFileParser(fileType)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
} }
// CreateNewDelimiterSeparatedValuesDataImporter returns a new delimiter-separated values data importer according to the file type and encoding // CreateNewCustomTransactionDataImporter returns a new custom transaction data importer according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) { func CreateNewCustomTransactionDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, amountDecimalSeparator string, amountDigitGroupingSymbol string, geoLocationSeparator string, geoLocationOrder string, transactionTagSeparator string) (converter.TransactionDataImporter, error) {
return dsv.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator) if custom.IsDelimiterSeparatedValuesFileType(fileType) {
return custom.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else if custom.IsCustomExcelFileType(fileType) {
return custom.CreateNewCustomTransactionDataExcelFileImporter(fileType, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, amountDecimalSeparator, amountDigitGroupingSymbol, geoLocationSeparator, geoLocationOrder, transactionTagSeparator)
} else {
return nil, errs.ErrImportFileTypeNotSupported
}
} }
@@ -170,6 +170,29 @@ func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidType(t *testing.T
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message) assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
} }
func TestWeChatPayCsvFileImporterParseImportedData_ParseAmountWithThousandSeparator(t *testing.T) {
importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext()
user := &models.User{
Uid: 1234567890,
DefaultCurrency: "CNY",
}
data1 := "微信支付账单明细,,,,\n" +
"微信昵称:[xxx],,,,\n" +
"起始时间:[2024-01-01 00:00:00] 终止时间:[2024-09-01 23:59:59],,,,\n" +
",,,,\n" +
"----------------------微信支付账单明细列表--------------------,,,,\n" +
"交易时间,交易类型,收/支,金额(元),支付方式,当前状态\n" +
"2024-09-01 01:23:45,二维码收款,收入,\"¥1,234.56\",/,已收钱\n"
allNewTransactions, _, _, _, _, _, err := importer.ParseImportedData(context, user, []byte(data1), time.UTC, converter.DefaultImporterOptions, nil, nil, nil, nil, nil)
assert.Nil(t, err)
assert.Equal(t, 1, len(allNewTransactions))
assert.Equal(t, int64(123456), allNewTransactions[0].Amount)
}
func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) { func TestWeChatPayCsvFileImporterParseImportedData_ParseInvalidAmount(t *testing.T) {
importer := WeChatPayTransactionDataCsvFileImporter importer := WeChatPayTransactionDataCsvFileImporter
context := core.NewNullContext() context := core.NewNullContext()
@@ -71,7 +71,7 @@ func (p *weChatPayTransactionDataRowParser) Parse(ctx core.Context, user *models
} }
if p.hasOriginalColumn(wechatPayTransactionAmountColumnName) { if p.hasOriginalColumn(wechatPayTransactionAmountColumnName) {
amount, success := utils.ParseFirstConsecutiveNumber(dataRow.GetData(wechatPayTransactionAmountColumnName)) amount, success := utils.ParseFirstConsecutiveNumber(strings.ReplaceAll(dataRow.GetData(wechatPayTransactionAmountColumnName), ",", ""))
if !success { if !success {
log.Errorf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] cannot parse amount \"%s\" of transaction in row \"%s\"", dataRow.GetData(wechatPayTransactionAmountColumnName), rowId) log.Errorf(ctx, "[wechat_pay_transaction_data_row_parser.Parse] cannot parse amount \"%s\" of transaction in row \"%s\"", dataRow.GetData(wechatPayTransactionAmountColumnName), rowId)
+17
View File
@@ -1,4 +1,21 @@
package core package core
import "fmt"
// ApplicationName represents the application name // ApplicationName represents the application name
const ApplicationName = "ezBookkeeping" const ApplicationName = "ezBookkeeping"
// Version, CommitHash and BuildTime are set at build
var (
Version string
CommitHash string
BuildTime string
)
func GetOutgoingUserAgent() string {
if Version == "" {
return ApplicationName
}
return fmt.Sprintf("%s/%s", ApplicationName, Version)
}
+9
View File
@@ -6,6 +6,15 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
const TokenUserAgentCreatedViaCli = ApplicationName + " Cli"
// TokenUserAgentForAPI is the user agent for API token
const TokenUserAgentForAPI = ApplicationName + " API"
// TokenUserAgentForMCP is the user agent for MCP token
const TokenUserAgentForMCP = ApplicationName + " MCP"
// TokenType represents token type // TokenType represents token type
type TokenType byte type TokenType byte
@@ -108,6 +108,6 @@ func (e *CommonHttpExchangeRatesDataProvider) GetLatestExchangeRates(c core.Cont
func newCommonHttpExchangeRatesDataProvider(config *settings.Config, dataSource HttpExchangeRatesDataSource) *CommonHttpExchangeRatesDataProvider { func newCommonHttpExchangeRatesDataProvider(config *settings.Config, dataSource HttpExchangeRatesDataSource) *CommonHttpExchangeRatesDataProvider {
return &CommonHttpExchangeRatesDataProvider{ return &CommonHttpExchangeRatesDataProvider{
dataSource: dataSource, dataSource: dataSource,
httpClient: httpclient.NewHttpClient(config.ExchangeRatesRequestTimeout, config.ExchangeRatesProxy, config.ExchangeRatesSkipTLSVerify, settings.GetUserAgent(), config.EnableDebugLog), httpClient: httpclient.NewHttpClient(config.ExchangeRatesRequestTimeout, config.ExchangeRatesProxy, config.ExchangeRatesSkipTLSVerify, core.GetOutgoingUserAgent(), config.EnableDebugLog),
} }
} }
@@ -14,21 +14,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
func TestExchangeRatesApiLatestExchangeRateHandler_ReserveBankOfAustraliaDataSource(t *testing.T) {
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.ReserveBankOfAustraliaDataSource)
if exchangeRateResponse == nil {
return
}
assert.Equal(t, "AUD", exchangeRateResponse.BaseCurrency)
supportedCurrencyCodes := []string{"CAD", "CHF", "CNY", "EUR", "GBP", "HKD", "IDR", "INR", "JPY", "KRW",
"MYR", "NZD", "PGK", "PHP", "SGD", "THB", "TWD", "USD", "VND"}
checkExchangeRatesHaveSpecifiedCurrencies(t, exchangeRateResponse.BaseCurrency, supportedCurrencyCodes, exchangeRateResponse.ExchangeRates)
}
func TestExchangeRatesApiLatestExchangeRateHandler_BankOfCanadaDataSource(t *testing.T) { func TestExchangeRatesApiLatestExchangeRateHandler_BankOfCanadaDataSource(t *testing.T) {
exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.BankOfCanadaDataSource) exchangeRateResponse := executeLatestExchangeRateHandler(t, settings.BankOfCanadaDataSource)
@@ -19,10 +19,7 @@ var (
// InitializeExchangeRatesDataSource initializes the current exchange rates data source according to the config // InitializeExchangeRatesDataSource initializes the current exchange rates data source according to the config
func InitializeExchangeRatesDataSource(config *settings.Config) error { func InitializeExchangeRatesDataSource(config *settings.Config) error {
if config.ExchangeRatesDataSource == settings.ReserveBankOfAustraliaDataSource { if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource {
Container.current = newCommonHttpExchangeRatesDataProvider(config, &ReserveBankOfAustraliaDataSource{})
return nil
} else if config.ExchangeRatesDataSource == settings.BankOfCanadaDataSource {
Container.current = newCommonHttpExchangeRatesDataProvider(config, &BankOfCanadaDataSource{}) Container.current = newCommonHttpExchangeRatesDataProvider(config, &BankOfCanadaDataSource{})
return nil return nil
} else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource { } else if config.ExchangeRatesDataSource == settings.CzechNationalBankDataSource {
@@ -1,161 +0,0 @@
package exchangerates
import (
"bytes"
"encoding/xml"
"net/http"
"time"
"golang.org/x/net/html/charset"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/models"
"github.com/mayswind/ezbookkeeping/pkg/utils"
"github.com/mayswind/ezbookkeeping/pkg/validators"
)
const reserveBankOfAustraliaExchangeRateUrl = "https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml"
const reserveBankOfAustraliaExchangeRateReferenceUrl = "https://www.rba.gov.au/statistics/frequency/exchange-rates.html"
const reserveBankOfAustraliaDataSource = "Reserve Bank of Australia"
const reserveBankOfAustraliaBaseCurrency = "AUD"
const reserveBankOfAustraliaDataUpdateDateFormat = "2006-01-02T15:04:05Z07:00"
// ReserveBankOfAustraliaDataSource defines the structure of exchange rates data source of the reserve bank of Australia
type ReserveBankOfAustraliaDataSource struct {
HttpExchangeRatesDataSource
}
// ReserveBankOfAustraliaData represents the whole data from the reserve bank of Australia
type ReserveBankOfAustraliaData struct {
XMLName xml.Name `xml:"RDF"`
Channel *ReserveBankOfAustraliaRssChannel `xml:"channel"`
Items []*ReserveBankOfAustraliaRssItem `xml:"item"`
}
// ReserveBankOfAustraliaRssChannel represents the rss channel from the reserve bank of Australia
type ReserveBankOfAustraliaRssChannel struct {
Date string `xml:"date"`
}
// ReserveBankOfAustraliaRssItem represents the rss item from the reserve bank of Australia
type ReserveBankOfAustraliaRssItem struct {
Statistics *ReserveBankOfAustraliaItemStatistics `xml:"statistics"`
}
// ReserveBankOfAustraliaItemStatistics represents the item statistics from the reserve bank of Australia
type ReserveBankOfAustraliaItemStatistics struct {
ExchangeRate *ReserveBankOfAustraliaExchangeRate `xml:"exchangeRate"`
}
// ReserveBankOfAustraliaExchangeRate represents the exchange rate from the reserve bank of Australia
type ReserveBankOfAustraliaExchangeRate struct {
BaseCurrency string `xml:"baseCurrency"`
TargetCurrency string `xml:"targetCurrency"`
Observation *ReserveBankOfAustraliaExchangeRateObservation `xml:"observation"`
}
// ReserveBankOfAustraliaExchangeRateObservation represents the exchange rate data from the reserve bank of Australia
type ReserveBankOfAustraliaExchangeRateObservation struct {
Value string `xml:"value"`
Unit string `xml:"unit"`
}
// ToLatestExchangeRateResponse returns a view-object according to original data from the reserve bank of Australia
func (e *ReserveBankOfAustraliaData) ToLatestExchangeRateResponse(c core.Context) *models.LatestExchangeRateResponse {
if e.Channel == nil {
log.Errorf(c, "[reserve_bank_of_australia_datasource.ToLatestExchangeRateResponse] rss channel does not exist")
return nil
}
if len(e.Items) < 1 {
log.Errorf(c, "[reserve_bank_of_australia_datasource.ToLatestExchangeRateResponse] rss items is empty")
return nil
}
exchangeRates := make(models.LatestExchangeRateSlice, 0, len(e.Items))
for i := 0; i < len(e.Items); i++ {
item := e.Items[i]
if item.Statistics == nil || item.Statistics.ExchangeRate == nil || item.Statistics.ExchangeRate.Observation == nil {
continue
}
if item.Statistics.ExchangeRate.BaseCurrency != reserveBankOfAustraliaBaseCurrency || item.Statistics.ExchangeRate.Observation.Unit != reserveBankOfAustraliaBaseCurrency {
continue
}
if _, exists := validators.AllCurrencyNames[item.Statistics.ExchangeRate.TargetCurrency]; !exists {
continue
}
if _, err := utils.StringToFloat64(item.Statistics.ExchangeRate.Observation.Value); err != nil {
continue
}
exchangeRates = append(exchangeRates, item.Statistics.ExchangeRate.ToLatestExchangeRate())
}
updateDateTime := e.Channel.Date
updateTime, err := time.Parse(reserveBankOfAustraliaDataUpdateDateFormat, updateDateTime)
if err != nil {
log.Errorf(c, "[reserve_bank_of_australia_datasource.ToLatestExchangeRateResponse] failed to parse update date, datetime is %s", updateDateTime)
return nil
}
latestExchangeRateResp := &models.LatestExchangeRateResponse{
DataSource: reserveBankOfAustraliaDataSource,
ReferenceUrl: reserveBankOfAustraliaExchangeRateReferenceUrl,
UpdateTime: updateTime.Unix(),
BaseCurrency: reserveBankOfAustraliaBaseCurrency,
ExchangeRates: exchangeRates,
}
return latestExchangeRateResp
}
// ToLatestExchangeRate returns a data pair according to original data from the reserve bank of Australia
func (e *ReserveBankOfAustraliaExchangeRate) ToLatestExchangeRate() *models.LatestExchangeRate {
return &models.LatestExchangeRate{
Currency: e.TargetCurrency,
Rate: e.Observation.Value,
}
}
// BuildRequests returns the reserve bank of Australia exchange rates http requests
func (e *ReserveBankOfAustraliaDataSource) BuildRequests() ([]*http.Request, error) {
req, err := http.NewRequest("GET", reserveBankOfAustraliaExchangeRateUrl, nil)
if err != nil {
return nil, err
}
return []*http.Request{req}, nil
}
// Parse returns the common response entity according to the the reserve bank of Australia data source raw response
func (e *ReserveBankOfAustraliaDataSource) Parse(c core.Context, content []byte) (*models.LatestExchangeRateResponse, error) {
xmlDecoder := xml.NewDecoder(bytes.NewReader(content))
xmlDecoder.CharsetReader = charset.NewReaderLabel
reserveBankOfAustraliaData := &ReserveBankOfAustraliaData{}
err := xmlDecoder.Decode(reserveBankOfAustraliaData)
if err != nil {
log.Errorf(c, "[reserve_bank_of_australia_datasource.Parse] failed to parse xml data, content is %s, because %s", string(content), err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
latestExchangeRateResponse := reserveBankOfAustraliaData.ToLatestExchangeRateResponse(c)
if latestExchangeRateResponse == nil {
log.Errorf(c, "[reserve_bank_of_australia_datasource.Parse] failed to parse latest exchange rate data, content is %s", string(content))
return nil, errs.ErrFailedToRequestRemoteApi
}
return latestExchangeRateResponse, nil
}
@@ -1,268 +0,0 @@
package exchangerates
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
const reserveBankOfAustraliaMinimumRequiredContent = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n" +
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n" +
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n" +
" </channel>\n" +
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n" +
" <cb:statistics rdf:parseType=\"Resource\">\n" +
" <cb:exchangeRate rdf:parseType=\"Resource\">\n" +
" <cb:observation rdf:parseType=\"Resource\">\n" +
" <cb:value>0.7543</cb:value>\n" +
" <cb:unit>AUD</cb:unit>\n" +
" </cb:observation>\n" +
" <cb:baseCurrency>AUD</cb:baseCurrency>\n" +
" <cb:targetCurrency>USD</cb:targetCurrency>\n" +
" </cb:exchangeRate>\n" +
" </cb:statistics>\n" +
" </item>\n" +
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#CNY\">\n" +
" <cb:statistics rdf:parseType=\"Resource\">\n" +
" <cb:exchangeRate rdf:parseType=\"Resource\">\n" +
" <cb:observation rdf:parseType=\"Resource\">\n" +
" <cb:value>4.9577</cb:value>\n" +
" <cb:unit>AUD</cb:unit>\n" +
" </cb:observation>\n" +
" <cb:baseCurrency>AUD</cb:baseCurrency>\n" +
" <cb:targetCurrency>CNY</cb:targetCurrency>\n" +
" </cb:exchangeRate>\n" +
" </cb:statistics>\n" +
" </item>\n" +
"</rdf:RDF>"
func TestReserveBankOfAustraliaDataSource_StandardDataExtractBaseCurrency(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(reserveBankOfAustraliaMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, "AUD", actualLatestExchangeRateResponse.BaseCurrency)
}
func TestReserveBankOfAustraliaDataSource_StandardDataExtractUpdateTime(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(reserveBankOfAustraliaMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Equal(t, int64(1617255900), actualLatestExchangeRateResponse.UpdateTime)
}
func TestReserveBankOfAustraliaDataSource_StandardDataExtractExchangeRates(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte(reserveBankOfAustraliaMinimumRequiredContent))
assert.Equal(t, nil, err)
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "USD",
Rate: "0.7543",
})
assert.Contains(t, actualLatestExchangeRateResponse.ExchangeRates, &models.LatestExchangeRate{
Currency: "CNY",
Rate: "4.9577",
})
}
func TestReserveBankOfAustraliaDataSource_BlankContent(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
_, err := dataSource.Parse(context, []byte(""))
assert.NotEqual(t, nil, err)
}
func TestReserveBankOfAustraliaDataSource_OnlyXMLHeader(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"))
assert.NotEqual(t, nil, err)
}
func TestReserveBankOfAustraliaDataSource_EmptyRDFContent(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
"</rdf:RDF>"))
assert.NotEqual(t, nil, err)
}
func TestReserveBankOfAustraliaDataSource_EmptyChannelContent(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" </channel>"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value>0.7543</cb:value>\n"+
" <cb:unit>AUD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>AUD</cb:baseCurrency>\n"+
" <cb:targetCurrency>USD</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.NotEqual(t, nil, err)
}
func TestReserveBankOfAustraliaDataSource_NoItem(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
_, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
"</rdf:RDF>"))
assert.NotEqual(t, nil, err)
}
func TestReserveBankOfAustraliaDataSource_BaseCurrencyNotEqualPreset(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value>0.7543</cb:value>\n"+
" <cb:unit>AUD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>USD</cb:baseCurrency>\n"+
" <cb:targetCurrency>USD</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestReserveBankOfAustraliaDataSource_UnitCurrencyNotEqualPreset(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value>0.7543</cb:value>\n"+
" <cb:unit>USD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>AUD</cb:baseCurrency>\n"+
" <cb:targetCurrency>USD</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestReserveBankOfAustraliaDataSource_InvalidCurrency(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value>1</cb:value>\n"+
" <cb:unit>AUD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>AUD</cb:baseCurrency>\n"+
" <cb:targetCurrency>XXX</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestReserveBankOfAustraliaDataSource_EmptyRate(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value></cb:value>\n"+
" <cb:unit>AUD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>AUD</cb:baseCurrency>\n"+
" <cb:targetCurrency>USD</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
func TestReserveBankOfAustraliaDataSource_InvalidRate(t *testing.T) {
dataSource := &ReserveBankOfAustraliaDataSource{}
context := core.NewNullContext()
actualLatestExchangeRateResponse, err := dataSource.Parse(context, []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
"<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rba=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html\" xmlns:cb=\"http://www.cbwiki.net/wiki/index.php/Specification_1.2/\" xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://purl.org/rss/1.0/\" xsi:schemaLocation=\"http://www.w3.org/1999/02/22-rdf-syntax-ns# rdf.xsd\">\n"+
" <channel rdf:about=\"https://www.rba.gov.au/rss/rss-cb-exchange-rates.xml\">\n"+
" <dc:date>2021-04-01T16:45:00+11:00</dc:date>\n"+
" </channel>\n"+
" <item rdf:about=\"https://www.rba.gov.au/statistics/frequency/exchange-rates.html#USD\">\n"+
" <cb:statistics rdf:parseType=\"Resource\">\n"+
" <cb:exchangeRate rdf:parseType=\"Resource\">\n"+
" <cb:observation rdf:parseType=\"Resource\">\n"+
" <cb:value>null</cb:value>\n"+
" <cb:unit>AUD</cb:unit>\n"+
" </cb:observation>\n"+
" <cb:baseCurrency>AUD</cb:baseCurrency>\n"+
" <cb:targetCurrency>USD</cb:targetCurrency>\n"+
" </cb:exchangeRate>\n"+
" </cb:statistics>\n"+
" </item>\n"+
"</rdf:RDF>"))
assert.Equal(t, nil, err)
assert.Len(t, actualLatestExchangeRateResponse.ExchangeRates, 0)
}
@@ -5,7 +5,9 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/llm/data" "github.com/mayswind/ezbookkeeping/pkg/llm/data"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider" "github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/anthropic"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/googleai" "github.com/mayswind/ezbookkeeping/pkg/llm/provider/googleai"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/lmstudio"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/ollama" "github.com/mayswind/ezbookkeeping/pkg/llm/provider/ollama"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/openai" "github.com/mayswind/ezbookkeeping/pkg/llm/provider/openai"
"github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/settings"
@@ -41,10 +43,16 @@ func initializeLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableR
return openai.NewOpenAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil return openai.NewOpenAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OpenAICompatibleLLMProvider { } else if llmConfig.LLMProvider == settings.OpenAICompatibleLLMProvider {
return openai.NewOpenAICompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil return openai.NewOpenAICompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.AnthropicLLMProvider {
return anthropic.NewAnthropicLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.AnthropicCompatibleLLMProvider {
return anthropic.NewAnthropicCompatibleLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OpenRouterLLMProvider { } else if llmConfig.LLMProvider == settings.OpenRouterLLMProvider {
return openai.NewOpenRouterLargeLanguageModelProvider(llmConfig, enableResponseLog), nil return openai.NewOpenRouterLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.OllamaLLMProvider { } else if llmConfig.LLMProvider == settings.OllamaLLMProvider {
return ollama.NewOllamaLargeLanguageModelProvider(llmConfig, enableResponseLog), nil return ollama.NewOllamaLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.LMStudioLLMProvider {
return lmstudio.NewLMStudioLargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == settings.GoogleAILLMProvider { } else if llmConfig.LLMProvider == settings.GoogleAILLMProvider {
return googleai.NewGoogleAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil return googleai.NewGoogleAILargeLanguageModelProvider(llmConfig, enableResponseLog), nil
} else if llmConfig.LLMProvider == "" { } else if llmConfig.LLMProvider == "" {
@@ -0,0 +1,196 @@
package anthropic
import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// AnthropicMessagesAPIProvider defines the structure of Anthropic messages API provider
type AnthropicMessagesAPIProvider interface {
// BuildMessagesHttpRequest returns the messages http request
BuildMessagesHttpRequest(c core.Context, uid int64) (*http.Request, error)
// GetModelID returns the model id
GetModelID() string
// GetMaxTokens returns the max tokens to generate
GetMaxTokens() uint32
}
// CommonAnthropicMessagesAPILargeLanguageModelAdapter defines the structure of Anthropic common compatible large language model adapter based on messages api
type CommonAnthropicMessagesAPILargeLanguageModelAdapter struct {
common.HttpLargeLanguageModelAdapter
apiProvider AnthropicMessagesAPIProvider
}
// AnthropicMessageRole defines the role of Anthropic message
type AnthropicMessageRole string
// Anthropic Message Roles
const (
AnthropicMessageRoleUser AnthropicMessageRole = "user"
)
type AnthropicThinkingType string
// Anthropic Thinking Types
const (
AnthropicThinkingTypeDisabled AnthropicThinkingType = "disabled"
)
// AnthropicMessagesRequest defines the structure of Anthropic messages request
type AnthropicMessagesRequest struct {
Model string `json:"model"`
MaxTokens uint32 `json:"max_tokens"`
Stream bool `json:"stream"`
System string `json:"system,omitempty"`
Messages []any `json:"messages"`
Thinking *AnthropicMessagesRequestThinkingConfigParam `json:"thinking,omitempty"`
}
// AnthropicMessagesRequestMessage defines the structure of Anthropic messages request message
type AnthropicMessagesRequestMessage[T string | []*AnthropicMessagesRequestImageBlockParam] struct {
Role AnthropicMessageRole `json:"role"`
Content T `json:"content"`
}
// AnthropicMessagesRequestImageBlockParam defines the structure of Anthropic messages request image content block param
type AnthropicMessagesRequestImageBlockParam struct {
Source *AnthropicMessagesRequestBase64ImageSource `json:"source"`
Type string `json:"type"`
}
// AnthropicMessagesRequestBase64ImageSource defines the structure of Anthropic messages request base64 image source
type AnthropicMessagesRequestBase64ImageSource struct {
Data string `json:"data"`
MediaType string `json:"media_type"`
Type string `json:"type"`
}
// AnthropicMessagesRequestThinkingConfigParam defines the structure of Anthropic messages request thinking config param
type AnthropicMessagesRequestThinkingConfigParam struct {
Type AnthropicThinkingType `json:"type"`
}
// AnthropicMessagesResponse defines the structure of Anthropic messages response
type AnthropicMessagesResponse struct {
Content []*AnthropicMessagesResponseContentBlock `json:"content"`
}
// AnthropicMessagesResponseContentBlock defines the structure of Anthropic messages response content block
type AnthropicMessagesResponseContentBlock struct {
Text *string `json:"text"`
}
// BuildTextualRequest returns the http request by Anthropic common compatible adapter
func (p *CommonAnthropicMessagesAPILargeLanguageModelAdapter) BuildTextualRequest(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) (*http.Request, error) {
requestBody, err := p.buildJsonRequestBody(c, uid, request, responseType)
if err != nil {
return nil, err
}
httpRequest, err := p.apiProvider.BuildMessagesHttpRequest(c, uid)
if err != nil {
return nil, err
}
httpRequest.Body = io.NopCloser(bytes.NewReader(requestBody))
httpRequest.Header.Set("Content-Type", "application/json")
return httpRequest, nil
}
// ParseTextualResponse returns the textual response by Anthropic common compatible adapter
func (p *CommonAnthropicMessagesAPILargeLanguageModelAdapter) ParseTextualResponse(c core.Context, uid int64, body []byte, responseType data.LargeLanguageModelResponseFormat) (*data.LargeLanguageModelTextualResponse, error) {
messagesResponse := &AnthropicMessagesResponse{}
err := json.Unmarshal(body, &messagesResponse)
if err != nil {
log.Errorf(c, "[anthropic_common_compatible_large_language_model_adapter.ParseTextualResponse] failed to parse messages response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
if messagesResponse == nil || messagesResponse.Content == nil || len(messagesResponse.Content) < 1 || messagesResponse.Content[0].Text == nil {
log.Errorf(c, "[anthropic_common_compatible_large_language_model_adapter.ParseTextualResponse] messages response is invalid for user \"uid:%d\"", uid)
return nil, errs.ErrFailedToRequestRemoteApi
}
textualResponse := &data.LargeLanguageModelTextualResponse{
Content: *messagesResponse.Content[0].Text,
}
return textualResponse, nil
}
func (p *CommonAnthropicMessagesAPILargeLanguageModelAdapter) buildJsonRequestBody(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) ([]byte, error) {
if p.apiProvider.GetModelID() == "" {
return nil, errs.ErrInvalidLLMModelId
}
messagesRequest := &AnthropicMessagesRequest{
Model: p.apiProvider.GetModelID(),
MaxTokens: p.apiProvider.GetMaxTokens(),
Stream: request.Stream,
Messages: make([]any, 0, 1),
Thinking: &AnthropicMessagesRequestThinkingConfigParam{
Type: AnthropicThinkingTypeDisabled,
},
}
if request.SystemPrompt != "" {
messagesRequest.System = request.SystemPrompt
}
if len(request.UserPrompt) > 0 {
if request.UserPromptType == data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL {
imageBase64Data := base64.StdEncoding.EncodeToString(request.UserPrompt)
messagesRequest.Messages = append(messagesRequest.Messages, &AnthropicMessagesRequestMessage[[]*AnthropicMessagesRequestImageBlockParam]{
Role: AnthropicMessageRoleUser,
Content: []*AnthropicMessagesRequestImageBlockParam{
{
Type: "image",
Source: &AnthropicMessagesRequestBase64ImageSource{
Data: imageBase64Data,
MediaType: request.UserPromptContentType,
Type: "base64",
},
},
},
})
} else {
messagesRequest.Messages = append(messagesRequest.Messages, &AnthropicMessagesRequestMessage[string]{
Role: AnthropicMessageRoleUser,
Content: string(request.UserPrompt),
})
}
}
requestBodyBytes, err := json.Marshal(messagesRequest)
if err != nil {
log.Errorf(c, "[anthropic_common_compatible_large_language_model_adapter.buildJsonRequestBody] failed to marshal request body for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
log.Debugf(c, "[anthropic_common_compatible_large_language_model_adapter.buildJsonRequestBody] request body is %s", requestBodyBytes)
return requestBodyBytes, nil
}
func newCommonAnthropicMessagesAPILargeLanguageModelAdapter(llmConfig *settings.LLMConfig, enableResponseLog bool, apiProvider AnthropicMessagesAPIProvider) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, enableResponseLog, &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: apiProvider,
})
}
@@ -0,0 +1,152 @@
package anthropic
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
)
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_buildJsonRequestBody_TextualUserPrompt(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{
AnthropicModelID: "test",
AnthropicMaxTokens: 128,
},
}
request := &data.LargeLanguageModelRequest{
SystemPrompt: "You are a helpful assistant.",
UserPrompt: []byte("Hello, how are you?"),
}
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
var body map[string]interface{}
err = json.Unmarshal(bodyBytes, &body)
assert.Nil(t, err)
assert.Equal(t, "{\"model\":\"test\",\"max_tokens\":128,\"stream\":false,\"system\":\"You are a helpful assistant.\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello, how are you?\"}],\"thinking\":{\"type\":\"disabled\"}}", string(bodyBytes))
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_buildJsonRequestBody_ImageUserPrompt(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{
AnthropicModelID: "test",
AnthropicMaxTokens: 128,
},
}
request := &data.LargeLanguageModelRequest{
SystemPrompt: "What's in this image?",
UserPrompt: []byte("fakedata"),
UserPromptType: data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL,
UserPromptContentType: "image/png",
}
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
var body map[string]interface{}
err = json.Unmarshal(bodyBytes, &body)
assert.Nil(t, err)
assert.Equal(t, "{\"model\":\"test\",\"max_tokens\":128,\"stream\":false,\"system\":\"What's in this image?\",\"messages\":[{\"role\":\"user\",\"content\":[{\"source\":{\"data\":\"ZmFrZWRhdGE=\",\"media_type\":\"image/png\",\"type\":\"base64\"},\"type\":\"image\"}]}],\"thinking\":{\"type\":\"disabled\"}}", string(bodyBytes))
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_ParseTextualResponse_ValidJsonResponse(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{},
}
response := `{
"id": "test-123",
"role": "assistant",
"type": "message",
"model": "test",
"usage": {
"input_tokens": 13,
"output_tokens": 7
},
"content": [
{
"type": "text",
"text": "This is a test response"
}
]
}`
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
assert.Equal(t, "This is a test response", result.Content)
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_ParseTextualResponse_EmptyContentText(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{},
}
response := `{
"id": "test-123",
"role": "assistant",
"content": [
{
"type": "text",
"text": ""
}
]
}`
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
assert.Equal(t, "", result.Content)
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_ParseTextualResponse_EmptyContent(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{},
}
response := `{
"id": "test-123",
"role": "assistant",
"content": []
}`
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_ParseTextualResponse_NoContentText(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{},
}
response := `{
"id": "msg_123",
"role": "assistant",
"content": [
{
"type": "text"
}
]
}`
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
func TestCommonAnthropicMessagesAPILargeLanguageModelAdapter_ParseTextualResponse_InvalidJson(t *testing.T) {
adapter := &CommonAnthropicMessagesAPILargeLanguageModelAdapter{
apiProvider: &AnthropicOfficialMessagesAPIProvider{},
}
response := "error"
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
@@ -0,0 +1,72 @@
package anthropic
import (
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const anthropicCompatibleMessagesPath = "messages"
// AnthropicCompatibleMessagesAPIProvider defines the structure of Anthropic compatible messages API provider
type AnthropicCompatibleMessagesAPIProvider struct {
AnthropicMessagesAPIProvider
AnthropicCompatibleBaseURL string
AnthropicCompatibleAPIVersion string
AnthropicCompatibleAPIKey string
AnthropicCompatibleModelID string
AnthropicCompatibleMaxTokens uint32
}
// BuildMessagesHttpRequest returns the messages http request by Anthropic compatible messages API provider
func (p *AnthropicCompatibleMessagesAPIProvider) BuildMessagesHttpRequest(c core.Context, uid int64) (*http.Request, error) {
req, err := http.NewRequest("POST", p.getFinalMessagesRequestUrl(), nil)
if err != nil {
return nil, err
}
if p.AnthropicCompatibleAPIVersion != "" {
req.Header.Set("anthropic-version", p.AnthropicCompatibleAPIVersion)
}
if p.AnthropicCompatibleAPIKey != "" {
req.Header.Set("X-Api-Key", p.AnthropicCompatibleAPIKey)
}
return req, nil
}
// GetModelID returns the model id of Anthropic compatible messages API provider
func (p *AnthropicCompatibleMessagesAPIProvider) GetModelID() string {
return p.AnthropicCompatibleModelID
}
// GetMaxTokens returns the max tokens to generate of Anthropic compatible messages API provider
func (p *AnthropicCompatibleMessagesAPIProvider) GetMaxTokens() uint32 {
return p.AnthropicCompatibleMaxTokens
}
func (p *AnthropicCompatibleMessagesAPIProvider) getFinalMessagesRequestUrl() string {
url := p.AnthropicCompatibleBaseURL
if url[len(url)-1] != '/' {
url += "/"
}
url += anthropicCompatibleMessagesPath
return url
}
// NewAnthropicCompatibleLargeLanguageModelProvider creates a new Anthropic compatible large language model provider instance
func NewAnthropicCompatibleLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return newCommonAnthropicMessagesAPILargeLanguageModelAdapter(llmConfig, enableResponseLog, &AnthropicCompatibleMessagesAPIProvider{
AnthropicCompatibleBaseURL: llmConfig.AnthropicCompatibleBaseURL,
AnthropicCompatibleAPIVersion: llmConfig.AnthropicCompatibleAPIVersion,
AnthropicCompatibleAPIKey: llmConfig.AnthropicCompatibleAPIKey,
AnthropicCompatibleModelID: llmConfig.AnthropicCompatibleModelID,
AnthropicCompatibleMaxTokens: llmConfig.AnthropicCompatibleMaxTokens,
})
}
@@ -0,0 +1,27 @@
package anthropic
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAnthropicCompatibleMessagesAPIProvider_GetFinalRequestUrl(t *testing.T) {
apiProvider := &AnthropicCompatibleMessagesAPIProvider{
AnthropicCompatibleBaseURL: "https://api.example.com/v1/",
}
url := apiProvider.getFinalMessagesRequestUrl()
assert.Equal(t, "https://api.example.com/v1/messages", url)
apiProvider = &AnthropicCompatibleMessagesAPIProvider{
AnthropicCompatibleBaseURL: "https://api.example.com/v1",
}
url = apiProvider.getFinalMessagesRequestUrl()
assert.Equal(t, "https://api.example.com/v1/messages", url)
apiProvider = &AnthropicCompatibleMessagesAPIProvider{
AnthropicCompatibleBaseURL: "https://example.com/api",
}
url = apiProvider.getFinalMessagesRequestUrl()
assert.Equal(t, "https://example.com/api/messages", url)
}
@@ -0,0 +1,53 @@
package anthropic
import (
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
// AnthropicOfficialMessagesAPIProvider defines the structure of Anthropic official messages API provider
type AnthropicOfficialMessagesAPIProvider struct {
AnthropicMessagesAPIProvider
AnthropicAPIKey string
AnthropicModelID string
AnthropicMaxTokens uint32
}
const anthropicMessagesUrl = "https://api.anthropic.com/v1/messages"
const anthropicAPIVersion = "2023-06-01"
// BuildMessagesHttpRequest returns the messages http request by Anthropic official messages API provider
func (p *AnthropicOfficialMessagesAPIProvider) BuildMessagesHttpRequest(c core.Context, uid int64) (*http.Request, error) {
req, err := http.NewRequest("POST", anthropicMessagesUrl, nil)
if err != nil {
return nil, err
}
req.Header.Set("anthropic-version", anthropicAPIVersion)
req.Header.Set("X-Api-Key", p.AnthropicAPIKey)
return req, nil
}
// GetModelID returns the model id of Anthropic official messages API provider
func (p *AnthropicOfficialMessagesAPIProvider) GetModelID() string {
return p.AnthropicModelID
}
// GetMaxTokens returns the max tokens to generate of Anthropic official messages API provider
func (p *AnthropicOfficialMessagesAPIProvider) GetMaxTokens() uint32 {
return p.AnthropicMaxTokens
}
// NewAnthropicLargeLanguageModelProvider creates a new Anthropic large language model provider instance
func NewAnthropicLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return newCommonAnthropicMessagesAPILargeLanguageModelAdapter(llmConfig, enableResponseLog, &AnthropicOfficialMessagesAPIProvider{
AnthropicAPIKey: llmConfig.AnthropicAPIKey,
AnthropicModelID: llmConfig.AnthropicModelID,
AnthropicMaxTokens: llmConfig.AnthropicMaxTokens,
})
}
@@ -83,6 +83,6 @@ func (p *CommonHttpLargeLanguageModelProvider) getTextualResponse(c core.Context
func NewCommonHttpLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool, adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider { func NewCommonHttpLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool, adapter HttpLargeLanguageModelAdapter) *CommonHttpLargeLanguageModelProvider {
return &CommonHttpLargeLanguageModelProvider{ return &CommonHttpLargeLanguageModelProvider{
adapter: adapter, adapter: adapter,
httpClient: httpclient.NewHttpClient(llmConfig.LargeLanguageModelAPIRequestTimeout, llmConfig.LargeLanguageModelAPIProxy, llmConfig.LargeLanguageModelAPISkipTLSVerify, settings.GetUserAgent(), enableResponseLog), httpClient: httpclient.NewHttpClient(llmConfig.LargeLanguageModelAPIRequestTimeout, llmConfig.LargeLanguageModelAPIProxy, llmConfig.LargeLanguageModelAPISkipTLSVerify, core.GetOutgoingUserAgent(), enableResponseLog),
} }
} }
@@ -0,0 +1,157 @@
package lmstudio
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider"
"github.com/mayswind/ezbookkeeping/pkg/llm/provider/common"
"github.com/mayswind/ezbookkeeping/pkg/log"
"github.com/mayswind/ezbookkeeping/pkg/settings"
)
const lmStudioChatPath = "api/v1/chat"
// LMStudioLargeLanguageModelAdapter defines the structure of LM Studio large language model adapter
type LMStudioLargeLanguageModelAdapter struct {
common.HttpLargeLanguageModelAdapter
LMStudioServerURL string
LMStudioToken string
LMStudioModelID string
}
// LMStudioChatRequest defines the structure of LM Studio chat request
type LMStudioChatRequest struct {
Model string `json:"model"`
Stream bool `json:"stream"`
SystemPrompt string `json:"system_prompt,omitempty"`
Input []*LMStudioChatRequestInput `json:"input"`
}
// LMStudioChatRequestInput defines the structure of LM Studio chat request message
type LMStudioChatRequestInput struct {
Type string `json:"type"`
Content string `json:"content,omitempty"`
DataUrl string `json:"data_url,omitempty"`
}
// LMStudioChatResponse defines the structure of LM Studio chat response
type LMStudioChatResponse struct {
Output []*LMStudioChatResponseOutput `json:"output"`
}
// LMStudioChatResponseOutput defines the structure of LM Studio chat response message
type LMStudioChatResponseOutput struct {
Content *string `json:"content"`
}
// BuildTextualRequest returns the http request by LM Studio large language model adapter
func (p *LMStudioLargeLanguageModelAdapter) BuildTextualRequest(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) (*http.Request, error) {
requestBody, err := p.buildJsonRequestBody(c, uid, request, responseType)
if err != nil {
return nil, err
}
httpRequest, err := http.NewRequest("POST", p.getLMStudioRequestUrl(), bytes.NewReader(requestBody))
if err != nil {
return nil, err
}
if p.LMStudioToken != "" {
httpRequest.Header.Set("Authorization", "Bearer "+p.LMStudioToken)
}
httpRequest.Header.Set("Content-Type", "application/json")
return httpRequest, nil
}
// ParseTextualResponse returns the textual response by LM Studio large language model adapter
func (p *LMStudioLargeLanguageModelAdapter) ParseTextualResponse(c core.Context, uid int64, body []byte, responseType data.LargeLanguageModelResponseFormat) (*data.LargeLanguageModelTextualResponse, error) {
chatResponse := &LMStudioChatResponse{}
err := json.Unmarshal(body, &chatResponse)
if err != nil {
log.Errorf(c, "[lm_studio_large_language_model_adapter.ParseTextualResponse] failed to parse chat response for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrFailedToRequestRemoteApi
}
if chatResponse == nil || len(chatResponse.Output) < 1 || chatResponse.Output[0].Content == nil {
log.Errorf(c, "[lm_studio_large_language_model_adapter.ParseTextualResponse] chat response is invalid for user \"uid:%d\"", uid)
return nil, errs.ErrFailedToRequestRemoteApi
}
textualResponse := &data.LargeLanguageModelTextualResponse{
Content: *chatResponse.Output[0].Content,
}
return textualResponse, nil
}
func (p *LMStudioLargeLanguageModelAdapter) buildJsonRequestBody(c core.Context, uid int64, request *data.LargeLanguageModelRequest, responseType data.LargeLanguageModelResponseFormat) ([]byte, error) {
if p.LMStudioModelID == "" {
return nil, errs.ErrInvalidLLMModelId
}
chatRequest := &LMStudioChatRequest{
Model: p.LMStudioModelID,
Stream: request.Stream,
Input: make([]*LMStudioChatRequestInput, 0, 1),
}
if request.SystemPrompt != "" {
chatRequest.SystemPrompt = request.SystemPrompt
}
if len(request.UserPrompt) > 0 {
if request.UserPromptType == data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL {
imageBase64Data := "data:" + request.UserPromptContentType + ";base64," + base64.StdEncoding.EncodeToString(request.UserPrompt)
chatRequest.Input = append(chatRequest.Input, &LMStudioChatRequestInput{
Type: "image",
DataUrl: imageBase64Data,
})
} else {
chatRequest.Input = append(chatRequest.Input, &LMStudioChatRequestInput{
Type: "text",
Content: string(request.UserPrompt),
})
}
}
requestBodyBytes, err := json.Marshal(chatRequest)
if err != nil {
log.Errorf(c, "[lm_studio_large_language_model_adapter.buildJsonRequestBody] failed to marshal request body for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
log.Debugf(c, "[lm_studio_large_language_model_adapter.buildJsonRequestBody] request body is %s", requestBodyBytes)
return requestBodyBytes, nil
}
func (p *LMStudioLargeLanguageModelAdapter) getLMStudioRequestUrl() string {
url := p.LMStudioServerURL
if url[len(url)-1] != '/' {
url += "/"
}
url += lmStudioChatPath
return url
}
// NewLMStudioLargeLanguageModelProvider creates a new LM Studio large language model provider instance
func NewLMStudioLargeLanguageModelProvider(llmConfig *settings.LLMConfig, enableResponseLog bool) provider.LargeLanguageModelProvider {
return common.NewCommonHttpLargeLanguageModelProvider(llmConfig, enableResponseLog, &LMStudioLargeLanguageModelAdapter{
LMStudioServerURL: llmConfig.LMStudioServerURL,
LMStudioToken: llmConfig.LMStudioToken,
LMStudioModelID: llmConfig.LMStudioModelID,
})
}
@@ -0,0 +1,146 @@
package lmstudio
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/llm/data"
)
func TestLMStudioLargeLanguageModelAdapter_buildJsonRequestBody_TextualUserPrompt(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{
LMStudioModelID: "test",
}
request := &data.LargeLanguageModelRequest{
SystemPrompt: "You are a helpful assistant.",
UserPrompt: []byte("Hello, how are you?"),
}
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
var body map[string]interface{}
err = json.Unmarshal(bodyBytes, &body)
assert.Nil(t, err)
assert.Equal(t, "{\"model\":\"test\",\"stream\":false,\"system_prompt\":\"You are a helpful assistant.\",\"input\":[{\"type\":\"text\",\"content\":\"Hello, how are you?\"}]}", string(bodyBytes))
}
func TestLMStudioLargeLanguageModelAdapter_buildJsonRequestBody_ImageUserPrompt(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{
LMStudioModelID: "test",
}
request := &data.LargeLanguageModelRequest{
SystemPrompt: "What's in this image?",
UserPrompt: []byte("fakedata"),
UserPromptType: data.LARGE_LANGUAGE_MODEL_REQUEST_PROMPT_TYPE_IMAGE_URL,
UserPromptContentType: "image/png",
}
bodyBytes, err := adapter.buildJsonRequestBody(core.NewNullContext(), 0, request, data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
var body map[string]interface{}
err = json.Unmarshal(bodyBytes, &body)
assert.Nil(t, err)
assert.Equal(t, "{\"model\":\"test\",\"stream\":false,\"system_prompt\":\"What's in this image?\",\"input\":[{\"type\":\"image\",\"data_url\":\"data:image/png;base64,ZmFrZWRhdGE=\"}]}", string(bodyBytes))
}
func TestLMStudioLargeLanguageModelAdapter_ParseTextualResponse_ValidJsonResponse(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{}
response := `{
"model_instance_id": "test",
"output": [
{
"type": "message",
"content": "This is a test response"
}
]
}`
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
assert.Equal(t, "This is a test response", result.Content)
}
func TestLMStudioLargeLanguageModelAdapter_ParseTextualResponse_EmptyOutputContent(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{}
response := `{
"model_instance_id": "test",
"output": [
{
"type": "message",
"content": ""
}
]
}`
result, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.Nil(t, err)
assert.Equal(t, "", result.Content)
}
func TestLMStudioLargeLanguageModelAdapter_ParseTextualResponse_EmptyOutput(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{}
response := `{
"model_instance_id": "test",
"output": []
}`
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
func TestLMStudioLargeLanguageModelAdapter_ParseTextualResponse_NoContentFieldInOutput(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{}
response := `{
"model_instance_id": "test",
"output": [
{
"type": "message"
}
]
}`
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
func TestLMStudioLargeLanguageModelAdapter_ParseTextualResponse_InvalidJson(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{}
response := "error"
_, err := adapter.ParseTextualResponse(core.NewNullContext(), 0, []byte(response), data.LARGE_LANGUAGE_MODEL_RESPONSE_FORMAT_JSON)
assert.EqualError(t, err, "failed to request third party api")
}
func TestLMStudioLargeLanguageModelAdapter_GetOllamaRequestUrl(t *testing.T) {
adapter := &LMStudioLargeLanguageModelAdapter{
LMStudioServerURL: "http://localhost:1234/",
}
url := adapter.getLMStudioRequestUrl()
assert.Equal(t, "http://localhost:1234/api/v1/chat", url)
adapter = &LMStudioLargeLanguageModelAdapter{
LMStudioServerURL: "http://localhost:1234",
}
url = adapter.getLMStudioRequestUrl()
assert.Equal(t, "http://localhost:1234/api/v1/chat", url)
adapter = &LMStudioLargeLanguageModelAdapter{
LMStudioServerURL: "http://example.com/lmstudio/",
}
url = adapter.getLMStudioRequestUrl()
assert.Equal(t, "http://example.com/lmstudio/api/v1/chat", url)
}
+9 -9
View File
@@ -10,24 +10,24 @@ var ptBR = &LocaleTextItems{
}, },
DefaultTypes: &DefaultTypes{ DefaultTypes: &DefaultTypes{
DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA, DecimalSeparator: core.DECIMAL_SEPARATOR_COMMA,
DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_SPACE, DigitGroupingSymbol: core.DIGIT_GROUPING_SYMBOL_DOT,
}, },
DataConverterTextItems: &DataConverterTextItems{ DataConverterTextItems: &DataConverterTextItems{
Alipay: "Alipay", Alipay: "Alipay",
WeChatWallet: "Wallet", WeChatWallet: "Wallet",
}, },
VerifyEmailTextItems: &VerifyEmailTextItems{ VerifyEmailTextItems: &VerifyEmailTextItems{
Title: "Verificar Email", Title: "Verifique seu e-mail",
SalutationFormat: "Olá %s,", SalutationFormat: "Olá %s,",
DescriptionAboveBtn: "Por favor, clique no link abaixo para confirmar o seu endereço de e-mail.", DescriptionAboveBtn: "Clique no link abaixo para confirmar seu endereço de e-mail.",
VerifyEmail: "Verificar Email", VerifyEmail: "Verificar e-mail",
DescriptionBelowBtnFormat: "Se você não se registrou para uma conta %s, basta ignorar este e-mail. Se não conseguir clicar no link acima, copie a URL acima e cole no seu navegador. O link para verificação de e-mail expirará após %v minutos.", DescriptionBelowBtnFormat: "Se você não criou uma conta no %s, ignore este e-mail. Se não conseguir clicar no link acima, copie a URL e cole no navegador. O link de verificação de e-mail expira em %v minutos.",
}, },
ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{ ForgetPasswordMailTextItems: &ForgetPasswordMailTextItems{
Title: "Redefinir Sua Senha", Title: "Redefina sua senha",
SalutationFormat: "Olá %s,", SalutationFormat: "Olá %s,",
DescriptionAboveBtn: "Recebemos recentemente uma solicitação para redefinir a sua senha. Você pode clicar no link abaixo para redefinir sua senha.", DescriptionAboveBtn: "Recebemos recentemente uma solicitação para redefinir sua senha. Clique no link abaixo para redefini-la.",
ResetPassword: "Redefinir Senha", ResetPassword: "Redefinir senha",
DescriptionBelowBtnFormat: "Se você não solicitou a redefinição de senha, basta ignorar este e-mail. Se não conseguir clicar no link acima, copie a URL acima e cole no seu navegador. O link de redefinição de senha expirará após %v minutos.", DescriptionBelowBtnFormat: "Se você não solicitou a redefinição da senha, ignore este e-mail. Se não conseguir clicar no link acima, copie a URL e cole no navegador. O link de redefinição de senha expira em %v minutos.",
}, },
} }
+39
View File
@@ -0,0 +1,39 @@
package middlewares
import (
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/settings"
"github.com/mayswind/ezbookkeeping/pkg/utils"
)
// APITokenIpLimit limits API token access based on IP address
func APITokenIpLimit(config *settings.Config) core.MiddlewareHandlerFunc {
return func(c *core.WebContext) {
claims := c.GetTokenClaims()
if claims == nil {
c.Next()
return
}
if claims.Type != core.USER_TOKEN_TYPE_API {
c.Next()
return
}
if len(config.APITokenAllowedRemoteIPs) < 1 {
c.Next()
return
}
for i := 0; i < len(config.APITokenAllowedRemoteIPs); i++ {
if config.APITokenAllowedRemoteIPs[i].Match(c.ClientIP()) {
c.Next()
return
}
}
utils.PrintJsonErrorResult(c, errs.ErrIPForbidden)
}
}
+9 -1
View File
@@ -15,7 +15,10 @@ const (
var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationCloudSettingType{ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationCloudSettingType{
// Basic Settings // Basic Settings
"showAccountBalance": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "showAccountBalance": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
"autoUpdateExchangeRatesData": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
// Navigation Bar
"showAddTransactionButtonInDesktopNavbar": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
// Overview Page // Overview Page
"showAmountInHomePage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "showAmountInHomePage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
"timezoneUsedForStatisticsInHomePage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "timezoneUsedForStatisticsInHomePage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
@@ -26,6 +29,8 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo
"showTotalAmountInTransactionListPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "showTotalAmountInTransactionListPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
"showTagInTransactionListPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "showTagInTransactionListPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
// Transaction Edit Page // Transaction Edit Page
"quickSaveButtonStyleInMobileTransactionListPage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
"quickAddButtonActionInMobileTransactionEditPage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
"autoSaveTransactionDraft": USER_APPLICATION_CLOUD_SETTING_TYPE_STRING, "autoSaveTransactionDraft": USER_APPLICATION_CLOUD_SETTING_TYPE_STRING,
"autoGetCurrentGeoLocation": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "autoGetCurrentGeoLocation": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
"alwaysShowTransactionPicturesInMobileTransactionEditPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "alwaysShowTransactionPicturesInMobileTransactionEditPage": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
@@ -41,6 +46,9 @@ var ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES = map[string]UserApplicationClo
"hideCategoriesWithoutAccounts": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN, "hideCategoriesWithoutAccounts": USER_APPLICATION_CLOUD_SETTING_TYPE_BOOLEAN,
// Exchange Rates Data Page // Exchange Rates Data Page
"currencySortByInExchangeRatesPage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "currencySortByInExchangeRatesPage": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
// Browser Cache Management
"mapCacheExpiration": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
"exchangeRatesDataCacheExpiration": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
// Statistics Settings // Statistics Settings
"statistics.defaultChartDataType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "statistics.defaultChartDataType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
"statistics.defaultTimezoneType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER, "statistics.defaultTimezoneType": USER_APPLICATION_CLOUD_SETTING_TYPE_NUMBER,
+2 -11
View File
@@ -20,15 +20,6 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/utils"
) )
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
const TokenUserAgentCreatedViaCli = core.ApplicationName + " Cli"
// TokenUserAgentForAPI is the user agent for API token
const TokenUserAgentForAPI = core.ApplicationName + " API"
// TokenUserAgentForMCP is the user agent for MCP token
const TokenUserAgentForMCP = core.ApplicationName + " MCP"
const tokenMaxExpiredAtUnixTime = int64(253402300799) // 9999-12-31 23:59:59 UTC const tokenMaxExpiredAtUnixTime = int64(253402300799) // 9999-12-31 23:59:59 UTC
// TokenService represents user token service // TokenService represents user token service
@@ -140,7 +131,7 @@ func (s *TokenService) CreateAPITokenViaCli(c *core.CliContext, user *models.Use
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now()) tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
} }
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration) token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, core.TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration)
return token, tokenRecord, err return token, tokenRecord, err
} }
@@ -168,7 +159,7 @@ func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.Use
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now()) tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
} }
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration) token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, core.TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration)
return token, tokenRecord, err return token, tokenRecord, err
} }
+17
View File
@@ -132,6 +132,23 @@ func (s *TransactionPictureService) GetPictureInfosByTransactionIds(c core.Conte
return pictureInfoMap, err return pictureInfoMap, err
} }
// GetAllPictureInfosOfAllTransactions returns all transaction picture info models
func (s *TransactionPictureService) GetAllPictureInfosOfAllTransactions(c core.Context, uid int64) (map[int64][]*models.TransactionPictureInfo, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var pictureInfos []*models.TransactionPictureInfo
err := s.UserDataDB(uid).NewSession(c).Where("uid=? AND deleted=?", uid, false).OrderBy("picture_id asc").Find(&pictureInfos)
if err != nil {
return nil, err
}
pictureInfoMap := s.GetPictureInfoListMapByList(pictureInfos)
return pictureInfoMap, err
}
// GetPictureByPictureId returns the transaction picture data according to transaction picture id // GetPictureByPictureId returns the transaction picture data according to transaction picture id
func (s *TransactionPictureService) GetPictureByPictureId(c core.Context, uid int64, pictureId int64, fileExtension string) ([]byte, error) { func (s *TransactionPictureService) GetPictureByPictureId(c core.Context, uid int64, pictureId int64, fileExtension string) ([]byte, error) {
if uid <= 0 { if uid <= 0 {
+26
View File
@@ -379,6 +379,32 @@ func (s *UserService) UpdateUser(c core.Context, user *models.User, modifyUserLa
return keyProfileUpdated, emailSetToUnverified, nil return keyProfileUpdated, emailSetToUnverified, nil
} }
// UpdateUserPassword updates the password of specified user
func (s *UserService) UpdateUserPassword(c core.Context, user *models.User) error {
if user.Uid <= 0 {
return errs.ErrUserIdInvalid
}
if user.Password == "" {
return errs.ErrPasswordIsEmpty
}
user.Password = utils.EncodePassword(user.Password, user.Salt)
user.UpdatedUnixTime = time.Now().Unix()
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
updatedRows, err := sess.ID(user.Uid).Cols("password", "updated_unix_time").Where("deleted=?", false).Update(user)
if err != nil {
return err
} else if updatedRows < 1 {
return errs.ErrUserNotFound
}
return nil
})
}
// UpdateUserAvatar updates the custom avatar type of specified user // UpdateUserAvatar updates the custom avatar type of specified user
func (s *UserService) UpdateUserAvatar(c core.Context, uid int64, avatarFile multipart.File, fileExtension string, oldFileExtension string) error { func (s *UserService) UpdateUserAvatar(c core.Context, uid int64, avatarFile multipart.File, fileExtension string, oldFileExtension string) error {
if uid <= 0 { if uid <= 0 {
+84 -31
View File
@@ -69,11 +69,14 @@ const (
) )
const ( const (
OpenAILLMProvider string = "openai" OpenAILLMProvider string = "openai"
OpenAICompatibleLLMProvider string = "openai_compatible" OpenAICompatibleLLMProvider string = "openai_compatible"
OpenRouterLLMProvider string = "openrouter" AnthropicLLMProvider string = "anthropic"
OllamaLLMProvider string = "ollama" AnthropicCompatibleLLMProvider string = "anthropic_compatible"
GoogleAILLMProvider string = "google_ai" OpenRouterLLMProvider string = "openrouter"
OllamaLLMProvider string = "ollama"
LMStudioLLMProvider string = "lm_studio"
GoogleAILLMProvider string = "google_ai"
) )
// Uuid generator types // Uuid generator types
@@ -125,7 +128,6 @@ const (
// Exchange rates data source types // Exchange rates data source types
const ( const (
ReserveBankOfAustraliaDataSource string = "reserve_bank_of_australia"
BankOfCanadaDataSource string = "bank_of_canada" BankOfCanadaDataSource string = "bank_of_canada"
CzechNationalBankDataSource string = "czech_national_bank" CzechNationalBankDataSource string = "czech_national_bank"
DanmarksNationalbankDataSource string = "danmarks_national_bank" DanmarksNationalbankDataSource string = "danmarks_national_bank"
@@ -161,8 +163,9 @@ const (
defaultWebDAVRequestTimeout uint32 = 10000 // 10 seconds defaultWebDAVRequestTimeout uint32 = 10000 // 10 seconds
defaultAIRecognitionPictureMaxSize uint32 = 10485760 // 10MB defaultAIRecognitionPictureMaxSize uint32 = 10485760 // 10MB
defaultLargeLanguageModelAPIRequestTimeout uint32 = 60000 // 60 seconds defaultAnthropicLargeLanguageModelAPIMaximumTokens uint32 = 1024
defaultLargeLanguageModelAPIRequestTimeout uint32 = 60000 // 60 seconds
defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes defaultInMemoryDuplicateCheckerCleanupInterval uint32 = 60 // 1 minutes
defaultDuplicateSubmissionsInterval uint32 = 300 // 5 minutes defaultDuplicateSubmissionsInterval uint32 = 300 // 5 minutes
@@ -244,10 +247,21 @@ type LLMConfig struct {
OpenAICompatibleBaseURL string OpenAICompatibleBaseURL string
OpenAICompatibleAPIKey string OpenAICompatibleAPIKey string
OpenAICompatibleModelID string OpenAICompatibleModelID string
AnthropicAPIKey string
AnthropicModelID string
AnthropicMaxTokens uint32
AnthropicCompatibleBaseURL string
AnthropicCompatibleAPIVersion string
AnthropicCompatibleAPIKey string
AnthropicCompatibleModelID string
AnthropicCompatibleMaxTokens uint32
OpenRouterAPIKey string OpenRouterAPIKey string
OpenRouterModelID string OpenRouterModelID string
OllamaServerURL string OllamaServerURL string
OllamaModelID string OllamaModelID string
LMStudioServerURL string
LMStudioToken string
LMStudioModelID string
GoogleAIAPIKey string GoogleAIAPIKey string
GoogleAIModelID string GoogleAIModelID string
LargeLanguageModelAPIRequestTimeout uint32 LargeLanguageModelAPIRequestTimeout uint32
@@ -356,6 +370,7 @@ type Config struct {
PasswordResetTokenExpiredTime uint32 PasswordResetTokenExpiredTime uint32
PasswordResetTokenExpiredTimeDuration time.Duration PasswordResetTokenExpiredTimeDuration time.Duration
EnableAPIToken bool EnableAPIToken bool
APITokenAllowedRemoteIPs []*core.IPPattern
MaxFailuresPerIpPerMinute uint32 MaxFailuresPerIpPerMinute uint32
MaxFailuresPerUserPerMinute uint32 MaxFailuresPerUserPerMinute uint32
@@ -653,29 +668,13 @@ func loadServerConfiguration(config *Config, configFile *ini.File, sectionName s
} }
func loadMCPServerConfiguration(config *Config, configFile *ini.File, sectionName string) error { func loadMCPServerConfiguration(config *Config, configFile *ini.File, sectionName string) error {
var err error
config.EnableMCPServer = getConfigItemBoolValue(configFile, sectionName, "enable_mcp", false) config.EnableMCPServer = getConfigItemBoolValue(configFile, sectionName, "enable_mcp", false)
mcpAllowedRemoteIps := getConfigItemStringValue(configFile, sectionName, "mcp_allowed_remote_ips", "") config.MCPAllowedRemoteIPs, err = getIPPatterns(configFile, sectionName, "mcp_allowed_remote_ips", "")
if mcpAllowedRemoteIps != "" { if err != nil {
remoteIPs := strings.Split(mcpAllowedRemoteIps, ",") return err
config.MCPAllowedRemoteIPs = make([]*core.IPPattern, 0, len(remoteIPs))
for i := 0; i < len(remoteIPs); i++ {
ip := strings.TrimSpace(remoteIPs[i])
pattern, err := core.ParseIPPattern(ip)
if err != nil {
return err
}
if pattern == nil {
continue
}
config.MCPAllowedRemoteIPs = append(config.MCPAllowedRemoteIPs, pattern)
}
} else {
config.MCPAllowedRemoteIPs = nil
} }
return nil return nil
@@ -860,10 +859,16 @@ func loadLLMConfiguration(configFile *ini.File, sectionName string) (*LLMConfig,
llmConfig.LLMProvider = OpenAILLMProvider llmConfig.LLMProvider = OpenAILLMProvider
} else if llmProvider == OpenAICompatibleLLMProvider { } else if llmProvider == OpenAICompatibleLLMProvider {
llmConfig.LLMProvider = OpenAICompatibleLLMProvider llmConfig.LLMProvider = OpenAICompatibleLLMProvider
} else if llmProvider == AnthropicLLMProvider {
llmConfig.LLMProvider = AnthropicLLMProvider
} else if llmProvider == AnthropicCompatibleLLMProvider {
llmConfig.LLMProvider = AnthropicCompatibleLLMProvider
} else if llmProvider == OpenRouterLLMProvider { } else if llmProvider == OpenRouterLLMProvider {
llmConfig.LLMProvider = OpenRouterLLMProvider llmConfig.LLMProvider = OpenRouterLLMProvider
} else if llmProvider == OllamaLLMProvider { } else if llmProvider == OllamaLLMProvider {
llmConfig.LLMProvider = OllamaLLMProvider llmConfig.LLMProvider = OllamaLLMProvider
} else if llmProvider == LMStudioLLMProvider {
llmConfig.LLMProvider = LMStudioLLMProvider
} else if llmProvider == GoogleAILLMProvider { } else if llmProvider == GoogleAILLMProvider {
llmConfig.LLMProvider = GoogleAILLMProvider llmConfig.LLMProvider = GoogleAILLMProvider
} else { } else {
@@ -877,12 +882,26 @@ func loadLLMConfiguration(configFile *ini.File, sectionName string) (*LLMConfig,
llmConfig.OpenAICompatibleAPIKey = getConfigItemStringValue(configFile, sectionName, "openai_compatible_api_key") llmConfig.OpenAICompatibleAPIKey = getConfigItemStringValue(configFile, sectionName, "openai_compatible_api_key")
llmConfig.OpenAICompatibleModelID = getConfigItemStringValue(configFile, sectionName, "openai_compatible_model_id") llmConfig.OpenAICompatibleModelID = getConfigItemStringValue(configFile, sectionName, "openai_compatible_model_id")
llmConfig.AnthropicAPIKey = getConfigItemStringValue(configFile, sectionName, "anthropic_api_key")
llmConfig.AnthropicModelID = getConfigItemStringValue(configFile, sectionName, "anthropic_model_id")
llmConfig.AnthropicMaxTokens = getConfigItemUint32Value(configFile, sectionName, "anthropic_max_tokens", defaultAnthropicLargeLanguageModelAPIMaximumTokens)
llmConfig.AnthropicCompatibleBaseURL = getConfigItemStringValue(configFile, sectionName, "anthropic_compatible_base_url")
llmConfig.AnthropicCompatibleAPIVersion = getConfigItemStringValue(configFile, sectionName, "anthropic_compatible_api_version")
llmConfig.AnthropicCompatibleAPIKey = getConfigItemStringValue(configFile, sectionName, "anthropic_compatible_api_key")
llmConfig.AnthropicCompatibleModelID = getConfigItemStringValue(configFile, sectionName, "anthropic_compatible_model_id")
llmConfig.AnthropicCompatibleMaxTokens = getConfigItemUint32Value(configFile, sectionName, "anthropic_compatible_max_tokens", defaultAnthropicLargeLanguageModelAPIMaximumTokens)
llmConfig.OpenRouterAPIKey = getConfigItemStringValue(configFile, sectionName, "openrouter_api_key") llmConfig.OpenRouterAPIKey = getConfigItemStringValue(configFile, sectionName, "openrouter_api_key")
llmConfig.OpenRouterModelID = getConfigItemStringValue(configFile, sectionName, "openrouter_model_id") llmConfig.OpenRouterModelID = getConfigItemStringValue(configFile, sectionName, "openrouter_model_id")
llmConfig.OllamaServerURL = getConfigItemStringValue(configFile, sectionName, "ollama_server_url") llmConfig.OllamaServerURL = getConfigItemStringValue(configFile, sectionName, "ollama_server_url")
llmConfig.OllamaModelID = getConfigItemStringValue(configFile, sectionName, "ollama_model_id") llmConfig.OllamaModelID = getConfigItemStringValue(configFile, sectionName, "ollama_model_id")
llmConfig.LMStudioServerURL = getConfigItemStringValue(configFile, sectionName, "lm_studio_server_url")
llmConfig.LMStudioToken = getConfigItemStringValue(configFile, sectionName, "lm_studio_token")
llmConfig.LMStudioModelID = getConfigItemStringValue(configFile, sectionName, "lm_studio_model_id")
llmConfig.GoogleAIAPIKey = getConfigItemStringValue(configFile, sectionName, "google_ai_api_key") llmConfig.GoogleAIAPIKey = getConfigItemStringValue(configFile, sectionName, "google_ai_api_key")
llmConfig.GoogleAIModelID = getConfigItemStringValue(configFile, sectionName, "google_ai_model_id") llmConfig.GoogleAIModelID = getConfigItemStringValue(configFile, sectionName, "google_ai_model_id")
@@ -942,6 +961,8 @@ func loadCronConfiguration(config *Config, configFile *ini.File, sectionName str
} }
func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error { func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName string) error {
var err error
config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key") config.SecretKeyNoSet = !getConfigItemIsSet(configFile, sectionName, "secret_key")
config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey) config.SecretKey = getConfigItemStringValue(configFile, sectionName, "secret_key", defaultSecretKey)
@@ -984,6 +1005,11 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
config.EnableAPIToken = getConfigItemBoolValue(configFile, sectionName, "enable_api_token", false) config.EnableAPIToken = getConfigItemBoolValue(configFile, sectionName, "enable_api_token", false)
config.APITokenAllowedRemoteIPs, err = getIPPatterns(configFile, sectionName, "api_token_allowed_remote_ips", "")
if err != nil {
return err
}
config.MaxFailuresPerIpPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_ip_per_minute", defaultMaxFailuresPerIpPerMinute) config.MaxFailuresPerIpPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_ip_per_minute", defaultMaxFailuresPerIpPerMinute)
config.MaxFailuresPerUserPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_user_per_minute", defaultMaxFailuresPerUserPerMinute) config.MaxFailuresPerUserPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_user_per_minute", defaultMaxFailuresPerUserPerMinute)
@@ -1163,8 +1189,7 @@ func loadMapConfiguration(config *Config, configFile *ini.File, sectionName stri
func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error { func loadExchangeRatesConfiguration(config *Config, configFile *ini.File, sectionName string) error {
dataSource := getConfigItemStringValue(configFile, sectionName, "data_source") dataSource := getConfigItemStringValue(configFile, sectionName, "data_source")
if dataSource == ReserveBankOfAustraliaDataSource || if dataSource == BankOfCanadaDataSource ||
dataSource == BankOfCanadaDataSource ||
dataSource == CzechNationalBankDataSource || dataSource == CzechNationalBankDataSource ||
dataSource == DanmarksNationalbankDataSource || dataSource == DanmarksNationalbankDataSource ||
dataSource == EuroCentralBankDataSource || dataSource == EuroCentralBankDataSource ||
@@ -1227,6 +1252,34 @@ func getFinalPath(workingPath, p string) (string, error) {
return p, err return p, err
} }
func getIPPatterns(configFile *ini.File, sectionName string, itemName string, defaultValue string) ([]*core.IPPattern, error) {
configValue := getConfigItemStringValue(configFile, sectionName, itemName, defaultValue)
if configValue == "" {
return nil, nil
}
remoteIPs := strings.Split(configValue, ",")
ipPatterns := make([]*core.IPPattern, 0, len(remoteIPs))
for i := 0; i < len(remoteIPs); i++ {
ip := strings.TrimSpace(remoteIPs[i])
pattern, err := core.ParseIPPattern(ip)
if err != nil {
return nil, err
}
if pattern == nil {
continue
}
ipPatterns = append(ipPatterns, pattern)
}
return ipPatterns, nil
}
func getMultiLanguageContentConfig(configFile *ini.File, sectionName string, enableKey string, contentKey string) MultiLanguageContentConfig { func getMultiLanguageContentConfig(configFile *ini.File, sectionName string, enableKey string, contentKey string) MultiLanguageContentConfig {
config := MultiLanguageContentConfig{ config := MultiLanguageContentConfig{
Enabled: getConfigItemBoolValue(configFile, sectionName, enableKey, false), Enabled: getConfigItemBoolValue(configFile, sectionName, enableKey, false),
+1 -18
View File
@@ -1,11 +1,5 @@
package settings package settings
import (
"fmt"
"github.com/mayswind/ezbookkeeping/pkg/core"
)
// ConfigContainer contains the current setting config // ConfigContainer contains the current setting config
type ConfigContainer struct { type ConfigContainer struct {
current *Config current *Config
@@ -13,10 +7,7 @@ type ConfigContainer struct {
// Initialize a config container singleton instance // Initialize a config container singleton instance
var ( var (
Version string Container = &ConfigContainer{}
CommitHash string
BuildTime string
Container = &ConfigContainer{}
) )
// SetCurrentConfig sets the current config by a given config // SetCurrentConfig sets the current config by a given config
@@ -28,11 +19,3 @@ func SetCurrentConfig(config *Config) {
func (c *ConfigContainer) GetCurrentConfig() *Config { func (c *ConfigContainer) GetCurrentConfig() *Config {
return c.current return c.current
} }
func GetUserAgent() string {
if Version == "" {
return core.ApplicationName
}
return fmt.Sprintf("%s/%s", core.ApplicationName, Version)
}
+1 -1
View File
@@ -26,7 +26,7 @@ func NewWebDAVObjectStorage(config *settings.Config, pathPrefix string) (*WebDAV
webDavConfig := config.WebDAVConfig webDavConfig := config.WebDAVConfig
storage := &WebDAVObjectStorage{ storage := &WebDAVObjectStorage{
httpClient: httpclient.NewHttpClient(webDavConfig.RequestTimeout, webDavConfig.Proxy, webDavConfig.SkipTLSVerify, settings.GetUserAgent(), false), httpClient: httpclient.NewHttpClient(webDavConfig.RequestTimeout, webDavConfig.Proxy, webDavConfig.SkipTLSVerify, core.GetOutgoingUserAgent(), false),
webDavConfig: webDavConfig, webDavConfig: webDavConfig,
rootPath: webDavConfig.RootPath, rootPath: webDavConfig.RootPath,
} }
+65
View File
@@ -0,0 +1,65 @@
---
name: ezbookkeeping
description: Use ezBookkeeping API Tools script to record new transactions, query transactions, retrieve account information, retrieve categories, retrieve tags, and retrieve exchange rate data in the self hosted personal finance application ezBookkeeping.
---
# ezBookkeeping API Tools
## Usage
### List all supported commands
Linux / macOS
```bash
sh scripts/ebktools.sh list
```
Windows
```powershell
scripts\ebktools.ps1 list
```
### Show help for a specific command
Linux / macOS
```bash
sh scripts/ebktools.sh help <command>
```
Windows
```powershell
scripts\ebktools.ps1 help <command>
```
### Call API
Linux / macOS
```bash
sh scripts/ebktools.sh [global-options] <command> [command-options]
```
Windows
```powershell
scripts\ebktools.ps1 [global-options] <command> [command-options]
```
## Troubleshooting
If the script reports that the environment variable `EBKTOOL_SERVER_BASEURL` or `EBKTOOL_TOKEN` is not set, user can define them as system environment variables, or create a `.env` file in the user home directory that contains these two variables and place it there.
The meanings of these environment variables are as follows:
| Variable | Required | Description |
| --- | --- | --- |
| `EBKTOOL_SERVER_BASEURL` | Required | ezBookkeeping server base URL (e.g., `http://localhost:8080`) |
| `EBKTOOL_TOKEN` | Required | ezBookkeeping API token |
## Reference
ezBookkeeping: [https://ezbookkeeping.mayswind.net](https://ezbookkeeping.mayswind.net)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -34,6 +34,7 @@ import { ThemeType } from '@/core/theme.ts';
import { isProduction } from '@/lib/version.ts'; import { isProduction } from '@/lib/version.ts';
import { initMapProvider } from '@/lib/map/index.ts'; import { initMapProvider } from '@/lib/map/index.ts';
import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts'; import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
import { updateMapCacheExpiration } from '@/lib/cache.ts';
import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; import { getSystemTheme, setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n(); const { tt, getCurrentLanguageInfo, setLanguage, initLocale } = useI18n();
@@ -120,12 +121,11 @@ if (isUserLogined() && initialRoutePath !== '/verify_email' && initialRoutePath
rootStore.setNotificationContent(response.notificationContent); rootStore.setNotificationContent(response.notificationContent);
} }
} }
});
// auto refresh exchange rates data updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration);
if (settingsStore.appSettings.autoUpdateExchangeRatesData) { exchangeRatesStore.removeExpiredExchangeRates(true);
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false }); exchangeRatesStore.autoUpdateExchangeRatesData();
} });
} }
} }
+14 -6
View File
@@ -22,10 +22,13 @@ import { useExchangeRatesStore } from '@/stores/exchangeRates.ts';
import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts'; import { APPLICATION_LOGO_PATH } from '@/consts/asset.ts';
import { ThemeType } from '@/core/theme.ts'; import { ThemeType } from '@/core/theme.ts';
import { isFunction } from '@/lib/common.ts';
import { isProduction } from '@/lib/version.ts'; import { isProduction } from '@/lib/version.ts';
import { getTheme, isEnableSwipeBack, isEnableAnimate } from '@/lib/settings.ts'; import { getTheme, isEnableSwipeBack, isEnableAnimate } from '@/lib/settings.ts';
import { initMapProvider } from '@/lib/map/index.ts'; import { initMapProvider } from '@/lib/map/index.ts';
import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts'; import { isUserLogined, isUserUnlocked } from '@/lib/userstate.ts';
import { updateMapCacheExpiration } from '@/lib/cache.ts';
import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts'; import { setExpenseAndIncomeAmountColor } from '@/lib/ui/common.ts';
import { isiOSHomeScreenMode, isModalShowing, setAppFontSize } from '@/lib/ui/mobile.ts'; import { isiOSHomeScreenMode, isModalShowing, setAppFontSize } from '@/lib/ui/mobile.ts';
@@ -160,7 +163,7 @@ onMounted(() => {
f7.on('sheetOpen', (sheet: Sheet.Sheet) => onBackdropChanged(sheet)); f7.on('sheetOpen', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
f7.on('sheetClose', (sheet: Sheet.Sheet) => onBackdropChanged(sheet)); f7.on('sheetClose', (sheet: Sheet.Sheet) => onBackdropChanged(sheet));
f7.on('pageBeforeOut', () => { f7.on('pageBeforeOut', () => {
if (isModalShowing()) { if (isModalShowing()) {
f7.actions.close('.actions-modal.modal-in', false); f7.actions.close('.actions-modal.modal-in', false);
f7.dialog.close('.dialog.modal-in', false); f7.dialog.close('.dialog.modal-in', false);
@@ -180,6 +183,12 @@ onMounted(() => {
const languageInfo = getCurrentLanguageInfo(); const languageInfo = getCurrentLanguageInfo();
initMapProvider(languageInfo?.alternativeLanguageTag); initMapProvider(languageInfo?.alternativeLanguageTag);
}); });
document.addEventListener('dragstart', (e) => {
if (!e.target || !('closest' in e.target) || !isFunction(e.target.closest) || !e.target.closest('.dragenabled')) {
e.preventDefault();
}
}, true);
}); });
watch(currentNotificationContent, (newValue) => { watch(currentNotificationContent, (newValue) => {
@@ -227,12 +236,11 @@ if (isUserLogined()) {
rootStore.setNotificationContent(response.notificationContent); rootStore.setNotificationContent(response.notificationContent);
} }
} }
});
// auto refresh exchange rates data updateMapCacheExpiration(settingsStore.appSettings.mapCacheExpiration);
if (settingsStore.appSettings.autoUpdateExchangeRatesData) { exchangeRatesStore.removeExpiredExchangeRates(true);
exchangeRatesStore.getLatestExchangeRates({ silent: true, force: false }); exchangeRatesStore.autoUpdateExchangeRatesData();
} });
} }
} }
</script> </script>
@@ -44,6 +44,8 @@ export interface AccountBalanceTrendsChartItem {
maximumBalance: number; maximumBalance: number;
medianBalance: number; medianBalance: number;
averageBalance: number; averageBalance: number;
q1Balance: number;
q3Balance: number;
} }
export interface CommonAccountBalanceTrendsChartProps { export interface CommonAccountBalanceTrendsChartProps {
@@ -162,6 +164,8 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
let lastMaximumBalance = lastClosingBalance; let lastMaximumBalance = lastClosingBalance;
let lastMedianBalance = lastClosingBalance; let lastMedianBalance = lastClosingBalance;
let lastAverageBalance = lastClosingBalance; let lastAverageBalance = lastClosingBalance;
let lastQ1Balance = lastClosingBalance;
let lastQ3Balance = lastClosingBalance;
for (const dateRange of allDateRanges.value) { for (const dateRange of allDateRanges.value) {
const minDateTime = parseDateTimeFromUnixTime(dateRange.minUnixTime); const minDateTime = parseDateTimeFromUnixTime(dateRange.minUnixTime);
@@ -205,6 +209,8 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
const maximumBalance = Math.max(...dataItems.map(item => item.accountClosingBalance)); const maximumBalance = Math.max(...dataItems.map(item => item.accountClosingBalance));
const medianBalance = allDataItemsSortedByClosingBalance[Math.floor(allDataItemsSortedByClosingBalance.length / 2)]!.accountClosingBalance; const medianBalance = allDataItemsSortedByClosingBalance[Math.floor(allDataItemsSortedByClosingBalance.length / 2)]!.accountClosingBalance;
const averageBalance = Math.trunc(sumAmounts(dataItems.map(item => item.accountClosingBalance)) / dataItems.length); const averageBalance = Math.trunc(sumAmounts(dataItems.map(item => item.accountClosingBalance)) / dataItems.length);
const q1Balance = allDataItemsSortedByClosingBalance[Math.floor(allDataItemsSortedByClosingBalance.length / 4)]!.accountClosingBalance;
const q3Balance = allDataItemsSortedByClosingBalance[Math.floor(allDataItemsSortedByClosingBalance.length * 3 / 4)]!.accountClosingBalance;
if (props.account.isAsset) { if (props.account.isAsset) {
lastOpeningBalance = openingBalance; lastOpeningBalance = openingBalance;
@@ -213,6 +219,8 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
lastMaximumBalance = maximumBalance; lastMaximumBalance = maximumBalance;
lastMedianBalance = medianBalance; lastMedianBalance = medianBalance;
lastAverageBalance = averageBalance; lastAverageBalance = averageBalance;
lastQ1Balance = q1Balance;
lastQ3Balance = q3Balance;
} else if (props.account.isLiability) { } else if (props.account.isLiability) {
lastOpeningBalance = -openingBalance; lastOpeningBalance = -openingBalance;
lastClosingBalance = -closingBalance; lastClosingBalance = -closingBalance;
@@ -220,6 +228,8 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
lastMaximumBalance = -maximumBalance; lastMaximumBalance = -maximumBalance;
lastMedianBalance = -medianBalance; lastMedianBalance = -medianBalance;
lastAverageBalance = -averageBalance; lastAverageBalance = -averageBalance;
lastQ1Balance = -q1Balance;
lastQ3Balance = -q3Balance;
} else { } else {
lastOpeningBalance = openingBalance; lastOpeningBalance = openingBalance;
lastClosingBalance = closingBalance; lastClosingBalance = closingBalance;
@@ -227,6 +237,8 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
lastMaximumBalance = maximumBalance; lastMaximumBalance = maximumBalance;
lastMedianBalance = medianBalance; lastMedianBalance = medianBalance;
lastAverageBalance = averageBalance; lastAverageBalance = averageBalance;
lastQ1Balance = q1Balance;
lastQ3Balance = q3Balance;
} }
} }
@@ -237,7 +249,9 @@ export function useAccountBalanceTrendsChartBase(props: CommonAccountBalanceTren
minimumBalance: lastMinimumBalance, minimumBalance: lastMinimumBalance,
maximumBalance: lastMaximumBalance, maximumBalance: lastMaximumBalance,
medianBalance: lastMedianBalance, medianBalance: lastMedianBalance,
averageBalance: lastAverageBalance averageBalance: lastAverageBalance,
q1Balance: lastQ1Balance,
q3Balance: lastQ3Balance
}); });
lastOpeningBalance = lastClosingBalance; lastOpeningBalance = lastClosingBalance;
+1 -1
View File
@@ -81,7 +81,7 @@ export function usePieChartBase(props: CommonPieChartProps) {
accumulatedPaintPercent += finalItem.paintPercent; accumulatedPaintPercent += finalItem.paintPercent;
finalItem.displayPercent = formatPercentToLocalizedNumerals(finalItem.percent, 2, '<0.01'); finalItem.displayPercent = formatPercentToLocalizedNumerals(finalItem.percent, 2, '<0.01');
finalItem.displayValue = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency) : formatNumberToLocalizedNumerals(value); finalItem.displayValue = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency) : formatNumberToLocalizedNumerals(value, 2);
validItems.push(finalItem); validItems.push(finalItem);
} }
@@ -78,6 +78,9 @@ const allSeries = computed<AccountBalanceTrendsChartDataItem[]>(() => {
series.areaStyle = {}; series.areaStyle = {};
} else if (props.type === AccountBalanceTrendChartType.Column.type) { } else if (props.type === AccountBalanceTrendChartType.Column.type) {
series.type = 'bar'; series.type = 'bar';
} else if (props.type === AccountBalanceTrendChartType.Boxplot.type) {
series.type = 'boxplot';
series.itemStyle.borderColor = series.itemStyle.color;
} else if (props.type === AccountBalanceTrendChartType.Candlestick.type) { } else if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor, isDarkMode.value); const expenseIncomeAmountColor = getExpenseAndIncomeAmountColor(userStore.currentUserExpenseAmountColor, userStore.currentUserIncomeAmountColor, isDarkMode.value);
series.type = 'candlestick'; series.type = 'candlestick';
@@ -88,7 +91,15 @@ const allSeries = computed<AccountBalanceTrendsChartDataItem[]>(() => {
} }
for (const item of allDataItems.value) { for (const item of allDataItems.value) {
if (props.type === AccountBalanceTrendChartType.Candlestick.type) { if (props.type === AccountBalanceTrendChartType.Boxplot.type) {
series.data.push([
item.minimumBalance,
item.q1Balance,
item.medianBalance,
item.q3Balance,
item.maximumBalance
]);
} else if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
series.data.push([ series.data.push([
item.openingBalance, item.openingBalance,
item.closingBalance, item.closingBalance,
@@ -114,20 +125,26 @@ const yAxisWidth = computed<number>(() => {
for (const series of allSeries.value) { for (const series of allSeries.value) {
for (const data of series.data) { for (const data of series.data) {
let value: number; let currentMinValue: number;
let currentMaxValue: number;
if (isArray(data)) { if (isArray(data) && props.type === AccountBalanceTrendChartType.Boxplot.type) {
value = data[1] as number; // for candlestick, use closing balance currentMinValue = data[0] as number;
currentMaxValue = data[4] as number;
} else if (isArray(data) && props.type === AccountBalanceTrendChartType.Candlestick.type) {
currentMinValue = data[2] as number;
currentMaxValue = data[3] as number;
} else { } else {
value = data as number; // for line or bar chart currentMinValue = data as number;
currentMaxValue = data as number;
} }
if (value > maxValue) { if (currentMaxValue > maxValue) {
maxValue = value; maxValue = currentMaxValue;
} }
if (value < minValue) { if (currentMinValue < minValue) {
minValue = value; minValue = currentMinValue;
} }
} }
} }
@@ -172,7 +189,54 @@ const chartOptions = computed<object>(() => {
color: isDarkMode.value ? '#eee' : '#333' color: isDarkMode.value ? '#eee' : '#333'
}, },
formatter: (params: CallbackDataParams[]) => { formatter: (params: CallbackDataParams[]) => {
if (props.type === AccountBalanceTrendChartType.Candlestick.type) { if (props.type === AccountBalanceTrendChartType.Boxplot.type) {
const dataIndex = params[0]!.dataIndex;
const dataItem = allDataItems.value[dataIndex] as AccountBalanceTrendsChartItem;
const displayItems: NameValue[] = [
{
name: tt('Minimum Balance'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.minimumBalance, props.account.currency)
},
{
name: tt('Q1 Balance (First Quartile)'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.q1Balance, props.account.currency)
},
{
name: tt('Median Balance'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.medianBalance, props.account.currency)
},
{
name: tt('Q3 Balance (Third Quartile)'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.q3Balance, props.account.currency)
},
{
name: tt('Maximum Balance'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.maximumBalance, props.account.currency)
},
{
name: tt('Opening Balance'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.openingBalance, props.account.currency)
},
{
name: tt('Closing Balance'),
value: formatAmountToLocalizedNumeralsWithCurrency(dataItem.closingBalance, props.account.currency)
}
];
let tooltip = `${params[0]!.name} ${props.legendName}<br/>`;
for (const [displayItem, index] of itemAndIndex(displayItems)) {
if (index === 5) {
tooltip += '<div style="border-bottom: ' + (isDarkMode.value ? '#eee' : '#333') + ' dashed 1px"></div>';
}
tooltip += `<div><span class="chart-pointer" style="background-color: #${DEFAULT_CHART_COLORS[index]}"></span>`
+ `<span>${displayItem.name}</span><span class="ms-5" style="float: inline-end">${displayItem.value}</span>`
+ `</div>`;
}
return tooltip;
} else if (props.type === AccountBalanceTrendChartType.Candlestick.type) {
const dataIndex = params[0]!.dataIndex; const dataIndex = params[0]!.dataIndex;
const dataItem = allDataItems.value[dataIndex] as AccountBalanceTrendsChartItem; const dataItem = allDataItems.value[dataIndex] as AccountBalanceTrendsChartItem;
const displayItems: NameValue[] = [ const displayItems: NameValue[] = [
@@ -205,8 +269,12 @@ const chartOptions = computed<object>(() => {
let tooltip = `${params[0]!.name} ${props.legendName}<br/>`; let tooltip = `${params[0]!.name} ${props.legendName}<br/>`;
for (const [displayItem, index] of itemAndIndex(displayItems)) { for (const [displayItem, index] of itemAndIndex(displayItems)) {
if (index === 4) {
tooltip += '<div style="border-bottom: ' + (isDarkMode.value ? '#eee' : '#333') + ' dashed 1px"></div>';
}
tooltip += `<div><span class="chart-pointer" style="background-color: #${DEFAULT_CHART_COLORS[index]}"></span>` tooltip += `<div><span class="chart-pointer" style="background-color: #${DEFAULT_CHART_COLORS[index]}"></span>`
+ `<span>${displayItem.name}</span><span class="ms-5" style="float: inline-end">${displayItem.value}</span><br/>` + `<span>${displayItem.name}</span><span class="ms-5" style="float: inline-end">${displayItem.value}</span>`
+ `</div>`; + `</div>`;
} }
@@ -217,7 +285,7 @@ const chartOptions = computed<object>(() => {
return `${params[0]!.name}<br/>` return `${params[0]!.name}<br/>`
+ '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>' + '<div><span class="chart-pointer" style="background-color: #' + DEFAULT_CHART_COLORS[0] + '"></span>'
+ `<span>${props.legendName}</span><span class="ms-5" style="float: inline-end">${value}</span><br/>` + `<span>${props.legendName}</span><span class="ms-5" style="float: inline-end">${value}</span>`
+ '</div>'; + '</div>';
} }
} }
+98 -11
View File
@@ -42,6 +42,7 @@ interface AxisChartDataItem {
} }
interface AxisChartTooltipItem extends SortableTransactionStatisticDataItem { interface AxisChartTooltipItem extends SortableTransactionStatisticDataItem {
readonly id: string;
readonly name: string; readonly name: string;
readonly color: unknown; readonly color: unknown;
readonly displayOrders: number[]; readonly displayOrders: number[];
@@ -71,6 +72,9 @@ const props = defineProps<{
amountValue?: boolean; amountValue?: boolean;
defaultCurrency?: string; defaultCurrency?: string;
enableClickItem?: boolean; enableClickItem?: boolean;
tooltipExtraColumnNames?: string[];
tooltipExtraColumnTotalValues?: (categoryIndex: number, totalValue: number, visibleSeriesIds: string[]) => string[];
tooltipExtraColumnValues?: (seriesId: string, categoryIndex: number, currentValue: number) => string[];
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -85,6 +89,7 @@ const {
formatAmountToWesternArabicNumeralsWithoutDigitGrouping, formatAmountToWesternArabicNumeralsWithoutDigitGrouping,
formatAmountToLocalizedNumeralsWithCurrency, formatAmountToLocalizedNumeralsWithCurrency,
formatNumberToLocalizedNumerals, formatNumberToLocalizedNumerals,
formatNumberToWesternArabicNumerals,
formatPercentToLocalizedNumerals formatPercentToLocalizedNumerals
} = useI18n(); } = useI18n();
@@ -289,6 +294,8 @@ const chartOptions = computed<object>(() => {
let totalAmount = 0; let totalAmount = 0;
let actualDisplayItemCount = 0; let actualDisplayItemCount = 0;
const displayItems: AxisChartTooltipItem[] = []; const displayItems: AxisChartTooltipItem[] = [];
const categoryIndex = params.length > 0 && params[0] ? (params[0].dataIndex ?? 0) : 0;
const visibleSeriesIds: string[] = [];
for (const param of params) { for (const param of params) {
const id = param.seriesId as string; const id = param.seriesId as string;
@@ -298,37 +305,111 @@ const chartOptions = computed<object>(() => {
const amount = param.data as number; const amount = param.data as number;
displayItems.push({ displayItems.push({
id: id,
name: name, name: name,
color: color, color: color,
displayOrders: displayOrders, displayOrders: displayOrders,
totalAmount: amount totalAmount: amount
}); });
visibleSeriesIds.push(id);
totalAmount += amount; totalAmount += amount;
} }
sortStatisticsItems(displayItems, props.sortingType); sortStatisticsItems(displayItems, props.sortingType);
for (const item of displayItems) { const extraColumnValuesMap: Record<number, string[]> = {};
const extraColumnTotalValues: string[] = [];
const hasExtraColumnIndexes: Record<number, boolean> = {};
if (props.tooltipExtraColumnNames) {
if (props.tooltipExtraColumnValues) {
for (const [item, index] of itemAndIndex(displayItems)) {
const values = props.tooltipExtraColumnValues(item.id, categoryIndex, item.totalAmount);
extraColumnValuesMap[index] = values;
for (const [value, columnIndex] of itemAndIndex(values)) {
if (value && value !== '-') {
hasExtraColumnIndexes[columnIndex] = true;
}
}
}
}
if (props.tooltipExtraColumnTotalValues) {
const values = props.tooltipExtraColumnTotalValues(categoryIndex, totalAmount, visibleSeriesIds);
extraColumnTotalValues.push(...values);
for (const [value, columnIndex] of itemAndIndex(values)) {
if (value && value !== '-') {
hasExtraColumnIndexes[columnIndex] = true;
}
}
}
}
for (const [item, index] of itemAndIndex(displayItems)) {
if (displayItems.length === 1 || item.totalAmount !== 0) { if (displayItems.length === 1 || item.totalAmount !== 0) {
const value = getDisplayValue(item.totalAmount); const value = getDisplayValue(item.totalAmount);
tooltip += '<div><span class="chart-pointer" style="background-color: ' + item.color + '"></span>'; tooltip += '<tr><td><span class="chart-pointer" style="background-color: ' + item.color + '"></span>';
tooltip += `<span>${item.name}</span><span class="ms-5" style="float: inline-end">${value}</span><br/>`; tooltip += `<span>${item.name}</span></td><td><span class="ms-5" style="float: inline-end">${value}</span></td>`;
tooltip += '</div>';
if (props.tooltipExtraColumnNames) {
const values = extraColumnValuesMap[index] ?? [];
for (let i = 0; i < props.tooltipExtraColumnNames.length; i++) {
if (!hasExtraColumnIndexes[i]) {
continue;
}
const value = values[i] ?? '-';
tooltip += `<td><span class="ms-5" style="float: inline-end">${value}</span></td>`;
}
}
tooltip += '</tr>';
actualDisplayItemCount++; actualDisplayItemCount++;
} }
} }
if (props.showTotalAmountInTooltip && !props.oneHundredPercentStacked) { if (props.showTotalAmountInTooltip && !props.oneHundredPercentStacked) {
const displayTotalAmount = getDisplayValue(totalAmount); const displayTotalAmount = getDisplayValue(totalAmount);
tooltip = (actualDisplayItemCount > 0 ? '<div style="border-bottom: ' + (isDarkMode.value ? '#eee' : '#333') + ' dashed 1px">' : '<div></div>') let totalColumnCount = 2;
+ '<span class="chart-pointer" style="background-color: ' + (isDarkMode.value ? '#eee' : '#333') + '"></span>'
+ `<span>${props.totalNameInTooltip}</span><span class="ms-5" style="float: inline-end">${displayTotalAmount}</span><br/>` let totalTooltip = `<tr><td><span class="chart-pointer" style="background-color: ${isDarkMode.value ? '#eee' : '#333'}"></span>`
+ '</div>' + tooltip; + `<span>${props.totalNameInTooltip}</span></td><td><span class="ms-5" style="float: inline-end">${displayTotalAmount}</span></td>`;
if (props.tooltipExtraColumnNames) {
for (let i = 0; i < props.tooltipExtraColumnNames.length; i++) {
if (!hasExtraColumnIndexes[i]) {
continue;
}
const value = extraColumnTotalValues[i] ?? '-';
totalTooltip += `<td><span class="ms-5" style="float: inline-end">${value}</span></td>`;
totalColumnCount++;
}
}
totalTooltip += '</tr>';
totalTooltip += `<tr><td colspan="${totalColumnCount}" ${actualDisplayItemCount > 0 ? 'style="border-bottom: ' + (isDarkMode.value ? '#eee' : '#333') + ' dashed 1px"' : ''}></td></tr>`;
tooltip = totalTooltip + tooltip;
} }
if (params.length && params[0] && params[0].name) { if (params.length && params[0] && params[0].name) {
tooltip = `${params[0].name}<br/>` + tooltip; let tooltipHeader = `<td>${params[0].name}</td><td></td>`;
if (props.tooltipExtraColumnNames) {
for (const [columnName, columnIndex] of itemAndIndex(props.tooltipExtraColumnNames)) {
if (!hasExtraColumnIndexes[columnIndex]) {
continue;
}
tooltipHeader += `<td><span class="ms-5" style="float: inline-end">${columnName}</span></td>`;
}
}
tooltip = `<table class="chart-tooltip-table"><tbody><tr>${tooltipHeader}</tr>${tooltip}</tbody></table>`
} }
return tooltip; return tooltip;
@@ -404,7 +485,7 @@ function getDisplayValue(value: number): string {
return formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency); return formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency);
} }
return formatNumberToLocalizedNumerals(value); return formatNumberToLocalizedNumerals(value, 2);
} }
function clickItem(e: ECElementEvent): void { function clickItem(e: ECElementEvent): void {
@@ -439,7 +520,13 @@ function exportData(): { headers: string[], data: string[][] } {
for (const [categoryName, index] of itemAndIndex(props.allCategoryNames)) { for (const [categoryName, index] of itemAndIndex(props.allCategoryNames)) {
const row: string[] = []; const row: string[] = [];
row.push(categoryName); row.push(categoryName);
row.push(...allSeries.value.map(item => formatAmountToWesternArabicNumeralsWithoutDigitGrouping(item.data[index] ?? 0))); row.push(...allSeries.value.map(item => {
if (props.oneHundredPercentStacked) {
return formatNumberToWesternArabicNumerals(item.data[index] ?? 0);
} else {
return formatAmountToWesternArabicNumeralsWithoutDigitGrouping(item.data[index] ?? 0);
}
}));
data.push(row); data.push(row);
} }
+1 -1
View File
@@ -84,7 +84,7 @@ const radarData = computed<RadarChartData>(() => {
const finalPercent = (isNumber(percent) && percent >= 0) ? percent : (value > 0 ? value / totalValidValue * 100 : 0); const finalPercent = (isNumber(percent) && percent >= 0) ? percent : (value > 0 ? value / totalValidValue * 100 : 0);
const displayPercent = formatPercentToLocalizedNumerals(finalPercent, 2, '<0.01'); const displayPercent = formatPercentToLocalizedNumerals(finalPercent, 2, '<0.01');
const displayValue = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency) : formatNumberToLocalizedNumerals(value); const displayValue = props.amountValue ? formatAmountToLocalizedNumeralsWithCurrency(value, props.defaultCurrency) : formatNumberToLocalizedNumerals(value, 2);
indicators.push({ indicators.push({
name: name, name: name,
+203 -17
View File
@@ -9,6 +9,9 @@
:translate-name="translateName" :translate-name="translateName"
:amount-value="true" :default-currency="defaultCurrency" :amount-value="true" :default-currency="defaultCurrency"
:enable-click-item="enableClickItem" :enable-click-item="enableClickItem"
:tooltip-extra-column-names="allTooltipExtraColumnNames"
:tooltip-extra-column-total-values="showYearOverYear || showPeriodOverPeriod ? getTooltipExtraColumnTotalValues : undefined"
:tooltip-extra-column-values="showYearOverYear || showPeriodOverPeriod ? getTooltipExtraColumnValues : undefined"
@click="clickItem" @click="clickItem"
v-if="chartDisplayType" v-if="chartDisplayType"
/> />
@@ -29,18 +32,31 @@ import {
import { useUserStore } from '@/stores/user.ts'; import { useUserStore } from '@/stores/user.ts';
import {
itemAndIndex
} from '@/core/base.ts';
import { import {
type Year1BasedMonth, type Year1BasedMonth,
type YearMonthDay, type YearMonthDay,
type YearUnixTime,
type YearQuarterUnixTime,
type YearMonthUnixTime,
type YearMonthDayUnixTime,
DateRangeScene DateRangeScene
} from '@/core/datetime.ts'; } from '@/core/datetime.ts';
import {
type FiscalYearUnixTime
} from '@/core/fiscalyear.ts';
import { import {
ChartDataAggregationType, ChartDataAggregationType,
TrendChartType, TrendChartType,
ChartDateAggregationType ChartDateAggregationType
} from '@/core/statistics.ts'; } from '@/core/statistics.ts';
import { isArray, isNumber } from '@/lib/common.ts'; import {
isArray,
isNumber
} from '@/lib/common.ts';
import { import {
parseDateTimeFromUnixTime, parseDateTimeFromUnixTime,
getYearMonthFirstUnixTime, getYearMonthFirstUnixTime,
@@ -56,6 +72,8 @@ interface DesktopTrendsChartProps<T extends TrendsChartDateType> extends CommonT
type?: number; type?: number;
showValue?: boolean; showValue?: boolean;
showTotalAmountInTooltip?: boolean; showTotalAmountInTooltip?: boolean;
showYearOverYear?: boolean;
showPeriodOverPeriod?: boolean;
} }
const props = defineProps<DesktopTrendsChartProps<TrendsChartDateType>>(); const props = defineProps<DesktopTrendsChartProps<TrendsChartDateType>>();
@@ -70,7 +88,8 @@ const {
formatDateTimeToGregorianLikeShortYear, formatDateTimeToGregorianLikeShortYear,
formatDateTimeToGregorianLikeShortYearMonth, formatDateTimeToGregorianLikeShortYearMonth,
formatYearQuarterToGregorianLikeYearQuarter, formatYearQuarterToGregorianLikeYearQuarter,
formatDateTimeToGregorianLikeFiscalYear formatDateTimeToGregorianLikeFiscalYear,
formatPercentToLocalizedNumerals
} = useI18n(); } = useI18n();
const { allDateRanges } = useTrendsChartBase(props); const { allDateRanges } = useTrendsChartBase(props);
@@ -91,6 +110,20 @@ const chartDisplayType = computed<AxisChartDisplayType | undefined>(() => {
} }
}); });
const allTooltipExtraColumnNames = computed<string[]>(() => {
const extraColumnNames: string[] = [];
if (props.showYearOverYear) {
extraColumnNames.push(tt('Year-over-Year'));
}
if (props.showPeriodOverPeriod) {
extraColumnNames.push(tt('Period-over-Period'));
}
return extraColumnNames;
});
const allDisplayDateRanges = computed<string[]>(() => { const allDisplayDateRanges = computed<string[]>(() => {
const allDisplayDateRanges: string[] = []; const allDisplayDateRanges: string[] = [];
@@ -188,22 +221,9 @@ const allSeriesData = computed<Record<string, unknown>[]>(() => {
} }
for (const dateRange of allDateRanges.value) { for (const dateRange of allDateRanges.value) {
let dateRangeKey = ''; const dateRangeKey = getDateRangeKey(dateRange) ?? '';
if (props.dateAggregationType === ChartDateAggregationType.Year.type) {
dateRangeKey = dateRange.year.toString();
} else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type && 'year' in dateRange) {
dateRangeKey = dateRange.year.toString();
} else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) {
dateRangeKey = `${dateRange.year}-${dateRange.quarter}`;
} else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month0base' in dateRange) {
dateRangeKey = `${dateRange.year}-${dateRange.month0base + 1}`;
} else if (props.dateAggregationType === ChartDateAggregationType.Day.type && 'day' in dateRange && props.chartMode === 'daily') {
dateRangeKey = `${dateRange.year}-${dateRange.month}-${dateRange.day}`;
}
let amount = 0;
const dataItems = dateRangeAmountMap[dateRangeKey]; const dataItems = dateRangeAmountMap[dateRangeKey];
let amount = 0;
if (isArray(dataItems)) { if (isArray(dataItems)) {
for (const dataItem of dataItems) { for (const dataItem of dataItems) {
@@ -229,6 +249,172 @@ const allSeriesData = computed<Record<string, unknown>[]>(() => {
return result; return result;
}); });
const seriesIdValuesMap = computed<Record<string, number[]>>(() => {
const result: Record<string, number[]> = {};
for (const item of allSeriesData.value) {
const id = getSeriesId(item);
const values = item['values'] as number[];
if (id && values) {
result[id] = values;
}
}
return result;
});
const yoyIndexMap = computed<Record<number, number>>(() => {
const result: Record<number, number> = {};
const dateKeyToIndex: Record<string, number> = {};
for (const [dateRange, index] of itemAndIndex(allDateRanges.value)) {
const key = getDateRangeKey(dateRange);
if (key) {
dateKeyToIndex[key] = index;
}
}
for (const [dateRange, index] of itemAndIndex(allDateRanges.value)) {
const yoyKey = getDateRangeKey(dateRange, -1);
if (yoyKey && isNumber(dateKeyToIndex[yoyKey])) {
result[index] = dateKeyToIndex[yoyKey];
}
}
return result;
});
function getSeriesId(item: Record<string, unknown>): string {
if (props.idField && item[props.idField]) {
return item[props.idField] as string;
}
const name = item[props.nameField] as string;
return props.translateName ? tt(name) : name;
}
function getDateRangeKey(dateRange: YearUnixTime | FiscalYearUnixTime | YearQuarterUnixTime | YearMonthUnixTime | YearMonthDayUnixTime, yearOffset?: number): string | undefined {
if (props.dateAggregationType === ChartDateAggregationType.Year.type) {
return (dateRange.year + (yearOffset ?? 0)).toString();
} else if (props.dateAggregationType === ChartDateAggregationType.FiscalYear.type && 'year' in dateRange) {
return (dateRange.year + (yearOffset ?? 0)).toString();
} else if (props.dateAggregationType === ChartDateAggregationType.Quarter.type && 'quarter' in dateRange) {
return `${dateRange.year + (yearOffset ?? 0)}-${dateRange.quarter}`;
} else if (props.dateAggregationType === ChartDateAggregationType.Month.type && 'month0base' in dateRange) {
return `${dateRange.year + (yearOffset ?? 0)}-${dateRange.month0base + 1}`;
} else if (props.dateAggregationType === ChartDateAggregationType.Day.type && 'day' in dateRange && props.chartMode === 'daily') {
return `${dateRange.year + (yearOffset ?? 0)}-${dateRange.month}-${dateRange.day}`;
} else {
return undefined;
}
}
function formatDisplayChangeRate(current: number, reference: number): string {
if (reference === 0 && current === 0) {
return formatPercentToLocalizedNumerals(0, 2, '<0.01');
}
if (reference === 0) {
return '-';
}
const rate = (current - reference) / reference * 100;
return formatPercentToLocalizedNumerals(rate, 2, '<0.01');
}
function getTooltipExtraColumnTotalValues(categoryIndex: number, totalValue: number, visibleSeriesIds: string[]): string[] {
const extraColumnValues: string[] = [];
if (!props.showYearOverYear && !props.showPeriodOverPeriod) {
return extraColumnValues;
}
if (props.showYearOverYear) {
const yoyReferenceIndex = yoyIndexMap.value[categoryIndex];
let displayChangeRate = '-';
if (isNumber(yoyReferenceIndex)) {
let referenceTotalValue = 0;
for (const seriesId of visibleSeriesIds) {
const values = seriesIdValuesMap.value[seriesId];
if (values) {
referenceTotalValue += values[yoyReferenceIndex] ?? 0;
}
}
displayChangeRate = formatDisplayChangeRate(totalValue, referenceTotalValue);
}
extraColumnValues.push(displayChangeRate);
}
if (props.showPeriodOverPeriod) {
const popReferenceIndex = categoryIndex - 1;
let displayChangeRate = '-';
if (popReferenceIndex >= 0) {
let referenceTotalValue = 0;
for (const seriesId of visibleSeriesIds) {
const values = seriesIdValuesMap.value[seriesId];
if (values) {
referenceTotalValue += values[popReferenceIndex] ?? 0;
}
}
displayChangeRate = formatDisplayChangeRate(totalValue, referenceTotalValue);
}
extraColumnValues.push(displayChangeRate);
}
return extraColumnValues;
}
function getTooltipExtraColumnValues(seriesId: string, categoryIndex: number, currentValue: number): string[] {
const extraColumnValues: string[] = [];
if (!props.showYearOverYear && !props.showPeriodOverPeriod) {
return extraColumnValues;
}
const values = seriesIdValuesMap.value[seriesId];
if (!values) {
return extraColumnValues;
}
if (props.showYearOverYear) {
const yoyReferenceIndex = yoyIndexMap.value[categoryIndex];
let displayChangeRate = '-';
if (isNumber(yoyReferenceIndex) && yoyReferenceIndex >= 0 && yoyReferenceIndex < values.length) {
displayChangeRate = formatDisplayChangeRate(currentValue, values[yoyReferenceIndex] ?? 0);
}
extraColumnValues.push(displayChangeRate);
}
if (props.showPeriodOverPeriod) {
const popReferenceIndex = categoryIndex - 1;
let displayChangeRate = '-';
if (popReferenceIndex >= 0 && popReferenceIndex < values.length) {
displayChangeRate = formatDisplayChangeRate(currentValue, values[popReferenceIndex] ?? 0);
}
extraColumnValues.push(displayChangeRate);
}
return extraColumnValues;
}
function clickItem(itemId: string, categoryIndex: number): void { function clickItem(itemId: string, categoryIndex: number): void {
const dateRange = allDateRanges.value[categoryIndex]; const dateRange = allDateRanges.value[categoryIndex];
@@ -70,12 +70,12 @@ const cancelRecognizingUuid = ref<string | undefined>(undefined);
const imageFile = ref<File | null>(null); const imageFile = ref<File | null>(null);
const imageSrc = ref<string | undefined>(undefined); const imageSrc = ref<string | undefined>(undefined);
function loadImage(file: File): void { function loadImage(image: Blob): void {
loading.value = true; loading.value = true;
imageFile.value = null; imageFile.value = null;
imageSrc.value = undefined; imageSrc.value = undefined;
compressJpgImage(file, 1280, 1280, 0.8).then(blob => { compressJpgImage(image, 1280, 1280, 0.8).then(blob => {
imageFile.value = KnownFileType.JPG.createFileFromBlob(blob, "image"); imageFile.value = KnownFileType.JPG.createFileFromBlob(blob, "image");
imageSrc.value = URL.createObjectURL(blob); imageSrc.value = URL.createObjectURL(blob);
loading.value = false; loading.value = false;
@@ -184,6 +184,10 @@ function onSheetOpen(): void {
function onSheetClosed(): void { function onSheetClosed(): void {
close(); close();
} }
defineExpose({
loadImage
});
</script> </script>
<style> <style>
@@ -103,6 +103,8 @@ const allVirtualListItems = computed<MobileAccountBalanceTrendsChartItem[]>(() =
averageBalance: dataItem.averageBalance, averageBalance: dataItem.averageBalance,
minimumBalance: dataItem.minimumBalance, minimumBalance: dataItem.minimumBalance,
maximumBalance: dataItem.maximumBalance, maximumBalance: dataItem.maximumBalance,
q1Balance: dataItem.q1Balance,
q3Balance: dataItem.q3Balance,
color: `#${DEFAULT_CHART_COLORS[0] as string}`, color: `#${DEFAULT_CHART_COLORS[0] as string}`,
percent: 0.0 percent: 0.0
}; };
+1 -1
View File
@@ -78,7 +78,7 @@ export const SPECIFIED_API_NOT_FOUND_ERRORS: Record<string, SpecifiedApiError> =
'/api/v1/users/2fa/recovery/regenerate.json': { '/api/v1/users/2fa/recovery/regenerate.json': {
message: 'Two-factor authentication is disabled' message: 'Two-factor authentication is disabled'
}, },
'/api/v1/transactions/parse_dsv_file.json': { '/api/v1/transactions/parse_custom_file.json': {
message: 'Transaction importing is disabled' message: 'Transaction importing is disabled'
}, },
'/api/v1/transactions/parse_import.json': { '/api/v1/transactions/parse_import.json': {
+11
View File
@@ -0,0 +1,11 @@
export const SW_PRECACHE_CACHE_NAME_PREFIX: string = 'workbox-precache-v2-';
export const SW_RUNTIME_CACHE_NAME_PREFIX: string = 'workbox-runtime-';
export const SW_ASSETS_CACHE_NAME: string = 'ezbookkeeping-assets-cache';
export const SW_CODE_CACHE_NAME: string = 'ezbookkeeping-code-cache';
export const SW_MAP_CACHE_NAME: string = 'ezbookkeeping-map-cache';
export const SW_SHARE_CACHE_NAME: string = 'ezbookkeeping-share-cache';
export const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG: string = 'UPDATE_MAP_CACHE_CONFIG';
export const SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE: string = 'UPDATE_MAP_CACHE_CONFIG_RESPONSE';
export const MAP_CACHE_MAX_ENTRIES: number = 1000;
+26 -6
View File
@@ -13,11 +13,10 @@ export const UTF_8 = 'utf-8';
export const SUPPORTED_FILE_ENCODINGS: string[] = [ export const SUPPORTED_FILE_ENCODINGS: string[] = [
UTF_8, // UTF-8 UTF_8, // UTF-8
'utf-8-bom', // UTF-8 with BOM
'utf-16le', // UTF-16 Little Endian 'utf-16le', // UTF-16 Little Endian
'utf-16be', // UTF-16 Big Endian 'utf-16be', // UTF-16 Big Endian
'utf-16le-bom', // UTF-16 Little Endian with BOM 'utf-32le', // UTF-32 Little Endian
'utf-16be-bom', // UTF-16 Big Endian with BOM 'utf-32be', // UTF-32 Big Endian
'cp437', // OEM United States (CP-437) 'cp437', // OEM United States (CP-437)
'cp863', // OEM Canadian French (CP-863) 'cp863', // OEM Canadian French (CP-863)
'cp037', // IBM EBCDIC US/Canada (CP-037) 'cp037', // IBM EBCDIC US/Canada (CP-037)
@@ -70,8 +69,8 @@ export const CHARDET_ENCODING_NAME_MAPPING: Record<string, string> = {
'UTF-8': UTF_8, 'UTF-8': UTF_8,
'UTF-16LE': 'utf-16le', 'UTF-16LE': 'utf-16le',
'UTF-16BE': 'utf-16be', 'UTF-16BE': 'utf-16be',
// 'UTF-32 LE': '', // not supported 'UTF-32LE': 'utf-32le',
// 'UTF-32 BE': '', // not supported 'UTF-32BE': 'utf-32be',
'ISO-2022-JP': 'iso-2022-jp', 'ISO-2022-JP': 'iso-2022-jp',
// 'ISO-2022-KR': '', // not supported // 'ISO-2022-KR': '', // not supported
// 'ISO-2022-CN': '', // not supported // 'ISO-2022-CN': '', // not supported
@@ -180,7 +179,28 @@ export const SUPPORTED_IMPORT_FILE_CATEGORY_AND_TYPES: ImportFileCategoryAndType
supportMultiLanguages: true, supportMultiLanguages: true,
anchor: 'how-to-import-delimiter-separated-values-dsv-file-or-data' anchor: 'how-to-import-delimiter-separated-values-dsv-file-or-data'
} }
} },
{
type: 'excel',
name: 'Excel Workbook File',
extensions: '.xlsx,.xls',
subTypes: [
{
type: 'custom_xlsx',
name: 'Excel Workbook File (.xlsx)',
extensions: '.xlsx',
},
{
type: 'custom_xls',
name: 'Excel 97-2003 Workbook File (.xls)',
extensions: '.xls',
}
],
document: {
supportMultiLanguages: true,
anchor: 'how-to-import-delimiter-separated-values-dsv-file-or-data'
}
},
] ]
}, },
{ {
+60
View File
@@ -158,6 +158,9 @@ export const ALL_ACCOUNT_ICONS: Record<string, IconInfo> = {
}, },
'8302': { '8302': {
icon: 'lab la-weixin' icon: 'lab la-weixin'
},
'8303': {
icon: 'lab la-line'
} }
}; };
@@ -257,6 +260,12 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'200': { '200': {
icon: 'las la-home' icon: 'las la-home'
}, },
'201': {
icon: 'las la-store'
},
'202': {
icon: 'las la-store-alt'
},
'210': { '210': {
icon: 'las la-toilet-paper' icon: 'las la-toilet-paper'
}, },
@@ -305,6 +314,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'252': { '252': {
icon: 'las la-toolbox' icon: 'las la-toolbox'
}, },
'253': {
icon: 'las la-paint-roller'
},
'260': { '260': {
icon: 'las la-broom' icon: 'las la-broom'
}, },
@@ -400,6 +412,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'442': { '442': {
icon: 'las la-satellite' icon: 'las la-satellite'
}, },
'443': {
icon: 'las la-ethernet'
},
'450': { '450': {
icon: 'las la-tv' icon: 'las la-tv'
}, },
@@ -418,6 +433,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'480': { '480': {
icon: 'las la-shipping-fast' icon: 'las la-shipping-fast'
}, },
'490': {
icon: 'las la-language'
},
// 500 - 599 : Expense - Entertainment // 500 - 599 : Expense - Entertainment
'500': { '500': {
icon: 'las la-heart' icon: 'las la-heart'
@@ -449,6 +467,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'518': { '518': {
icon: 'las la-hiking' icon: 'las la-hiking'
}, },
'519': {
icon: 'las la-compass'
},
'520': { '520': {
icon: 'las la-futbol' icon: 'las la-futbol'
}, },
@@ -533,6 +554,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'582': { '582': {
icon: 'las la-crow' icon: 'las la-crow'
}, },
'583': {
icon: 'las la-cat'
},
'589': { '589': {
icon: 'las la-bone' icon: 'las la-bone'
}, },
@@ -644,6 +668,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'880': { '880': {
icon: 'las la-glasses' icon: 'las la-glasses'
}, },
'881': {
icon: 'las la-wheelchair'
},
'890': { '890': {
icon: 'las la-thermometer' icon: 'las la-thermometer'
}, },
@@ -694,6 +721,33 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'1010': { '1010': {
icon: 'las la-minus-circle' icon: 'las la-minus-circle'
}, },
'1020': {
icon: 'lab la-dev'
},
'1021': {
icon: 'las la-laptop-code'
},
'1022': {
icon: 'las la-server'
},
'1023': {
icon: 'las la-hdd'
},
'1024': {
icon: 'las la-memory'
},
'1025': {
icon: 'las la-microchip'
},
'1026': {
icon: 'las la-robot'
},
'1027': {
icon: 'las la-link'
},
'1100': {
icon: 'las la-seedling'
},
// 2000 - 2099 : Income - Occupational Earnings // 2000 - 2099 : Income - Occupational Earnings
'2000': { '2000': {
icon: 'las la-suitcase' icon: 'las la-suitcase'
@@ -769,6 +823,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'6001': { '6001': {
icon: 'lab la-ebay' icon: 'lab la-ebay'
}, },
'6010': {
icon: 'lab la-line'
},
'6100': { '6100': {
icon: 'lab la-app-store' icon: 'lab la-app-store'
}, },
@@ -784,6 +841,9 @@ export const ALL_CATEGORY_ICONS: Record<string, IconInfo> = {
'6400': { '6400': {
icon: 'lab la-uber' icon: 'lab la-uber'
}, },
'6410': {
icon: 'lab la-airbnb'
},
'6500': { '6500': {
icon: 'lab la-fedex' icon: 'lab la-fedex'
}, },
+200
View File
@@ -0,0 +1,200 @@
import { entries } from '@/core/base.ts';
import { TransactionType } from '@/core/transaction.ts';
import { ImportTransactionColumnType } from '@/core/import_transaction.ts';
export const KNOWN_COLUMN_NAME_MAPPING: Record<string, ImportTransactionColumnType> = ((mappings: Record<string, ImportTransactionColumnType>[]) => {
const result: Record<string, ImportTransactionColumnType> = {};
for (const mapping of mappings) {
for (const [key, value] of entries(mapping)) {
const normalizedKey = key.toLowerCase().replaceAll(' ', '').replaceAll('_', '').replaceAll('-', '');
if (result[normalizedKey]) {
continue;
}
result[normalizedKey] = value;
}
}
return result;
})([
// Columns of ezbookkeeping Data Export File
{
['Time']: ImportTransactionColumnType.TransactionTime,
['Timezone']: ImportTransactionColumnType.TransactionTimezone,
['Type']: ImportTransactionColumnType.TransactionType,
['Category']: ImportTransactionColumnType.Category,
['Sub Category']: ImportTransactionColumnType.SubCategory,
['Account']: ImportTransactionColumnType.AccountName,
['Account Currency']: ImportTransactionColumnType.AccountCurrency,
['Amount']: ImportTransactionColumnType.Amount,
['Account2']: ImportTransactionColumnType.RelatedAccountName,
['Account2 Currency']: ImportTransactionColumnType.RelatedAccountCurrency,
['Account2 Amount']: ImportTransactionColumnType.RelatedAmount,
['Geographic Location']: ImportTransactionColumnType.GeographicLocation,
['Tags']: ImportTransactionColumnType.Tags,
['Description']: ImportTransactionColumnType.Description
},
// Other common columns of transaction time
{
// en
['Date']: ImportTransactionColumnType.TransactionTime,
['Datetime']: ImportTransactionColumnType.TransactionTime,
['Timestamp']: ImportTransactionColumnType.TransactionTime,
// zh-Hans
['日期']: ImportTransactionColumnType.TransactionTime,
['时间']: ImportTransactionColumnType.TransactionTime,
['交易日期']: ImportTransactionColumnType.TransactionTime,
['交易时间']: ImportTransactionColumnType.TransactionTime,
},
// Other common columns of transaction timezone
{
},
// Other common columns of transaction type
{
// en
['Transaction Type']: ImportTransactionColumnType.TransactionType,
// zh-Hans
['交易类型']: ImportTransactionColumnType.TransactionType,
['类型']: ImportTransactionColumnType.TransactionType,
['收/支']: ImportTransactionColumnType.TransactionType,
},
// Other common columns of category
{
// en
['Category Name']: ImportTransactionColumnType.Category,
// zh-Hans
['交易分类']: ImportTransactionColumnType.Category,
['类别']: ImportTransactionColumnType.Category,
['分类']: ImportTransactionColumnType.Category,
},
// Other common columns of sub category
{
// zh-Hans
['子类别']: ImportTransactionColumnType.SubCategory,
['子分类']: ImportTransactionColumnType.SubCategory,
['二级分类']: ImportTransactionColumnType.SubCategory,
},
// Other common columns of account name
{
// en
['Account Name']: ImportTransactionColumnType.AccountName,
['Source Name']: ImportTransactionColumnType.AccountName,
// zh-Hans
['账户']: ImportTransactionColumnType.AccountName,
['账户1']: ImportTransactionColumnType.AccountName,
},
// Other common columns of account currency
{
// en
['Currency']: ImportTransactionColumnType.AccountCurrency,
['Currency Code']: ImportTransactionColumnType.AccountCurrency,
// zh-Hans
['账户币种']: ImportTransactionColumnType.AccountCurrency,
['币种']: ImportTransactionColumnType.AccountCurrency,
},
// Other common columns of amount
{
// zh-Hans
['金额']: ImportTransactionColumnType.Amount,
},
// Other common columns of related account name
{
// en
['Destination Name']: ImportTransactionColumnType.RelatedAccountName,
// zh-Hans
['账户2']: ImportTransactionColumnType.RelatedAccountName,
},
// Other common columns of related account currency
{
// en
['Foreign Currency']: ImportTransactionColumnType.RelatedAccountCurrency,
['Foreign Currency Code']: ImportTransactionColumnType.RelatedAccountCurrency,
},
// Other common columns of related amount
{
// en
['Foreign Amount']: ImportTransactionColumnType.RelatedAmount,
},
// Other common columns of geographic location
{
},
// Other common columns of tags
{
// zh-Hans
['标签']: ImportTransactionColumnType.Tags,
},
// Other common columns of description
{
// en
['Comment']: ImportTransactionColumnType.Description,
['Note']: ImportTransactionColumnType.Description,
['Memo']: ImportTransactionColumnType.Description,
// zh-Hans
['备注']: ImportTransactionColumnType.Description,
}
]);
export const KNOWN_TRANSACTION_TYPE_NAME_MAPPING: Record<string, TransactionType> = ((mappings: Record<string, TransactionType>[]) => {
const result: Record<string, TransactionType> = {};
for (const mapping of mappings) {
for (const [key, value] of entries(mapping)) {
const normalizedKey = key.toLowerCase().replaceAll(' ', '').replaceAll('_', '').replaceAll('-', '');
if (result[normalizedKey]) {
continue;
}
result[normalizedKey] = value;
}
}
return result;
})([
// Transaction types of ezbookkeeping Data Export File
{
['Balance Modification']: TransactionType.ModifyBalance,
['Income']: TransactionType.Income,
['Expense']: TransactionType.Expense,
['Transfer']: TransactionType.Transfer,
},
// Other common balance modification type
{
// en
['Opening balance']: TransactionType.ModifyBalance,
// zh-Hans
['余额变更']: TransactionType.ModifyBalance,
['负债变更']: TransactionType.ModifyBalance,
},
// Other common income type
{
// en
['Deposit']: TransactionType.Income,
// zh-Hans
['收入']: TransactionType.Income,
},
// Other common expense type
{
// en
['Withdrawal']: TransactionType.Expense,
// zh-Hans
['支出']: TransactionType.Expense,
},
// Other common transfer type
{
// zh-Hans
['转账']: TransactionType.Transfer,
['还款']: TransactionType.Transfer,
['借入']: TransactionType.Transfer,
['借出']: TransactionType.Transfer,
['收债']: TransactionType.Transfer,
['还债']: TransactionType.Transfer,
['代付']: TransactionType.Transfer,
['报销']: TransactionType.Transfer,
['退款']: TransactionType.Transfer,
}
]);
+11 -2
View File
@@ -65,12 +65,17 @@ export function* values<K extends string | number | symbol, V>(obj: Record<K, V>
} }
} }
export interface NameValue { export interface GenericNameValue<T> {
readonly name: string;
readonly value: T;
}
export interface NameValue extends GenericNameValue<string> {
readonly name: string; readonly name: string;
readonly value: string; readonly value: string;
} }
export interface NameNumeralValue { export interface NameNumeralValue extends GenericNameValue<number> {
readonly name: string; readonly name: string;
readonly value: number; readonly value: number;
} }
@@ -85,6 +90,10 @@ export interface TypeAndName {
readonly name: string; readonly name: string;
} }
export interface TypeAndNameWithAlternativeName extends TypeAndName {
readonly alternativeName?: string;
}
export interface TypeAndDisplayName { export interface TypeAndDisplayName {
readonly type: number; readonly type: number;
readonly displayName: string; readonly displayName: string;
+14
View File
@@ -0,0 +1,14 @@
export interface BrowserCacheStatistics {
readonly totalCacheSize: number;
readonly codeCacheSize: number;
readonly assetsCacheSize: number;
readonly mapCacheSize: number;
readonly othersCacheSize: number;
}
export interface SWMapCacheConfig {
enabled: boolean;
patterns: string[];
maxEntries: number;
maxAgeMilliseconds: number;
}
+3
View File
@@ -570,6 +570,9 @@ export class KnownDateTimeFormat {
public static readonly YYYYMMDD = new KnownDateTimeFormat('YYYYMMDD', DateFormatOrder.YMD, /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/); public static readonly YYYYMMDD = new KnownDateTimeFormat('YYYYMMDD', DateFormatOrder.YMD, /^\d{4}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$/);
public static readonly MMDDYYDash = new KnownDateTimeFormat('MM-DD-YY', DateFormatOrder.MDY, /^(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])-\d{2}$/);
public static readonly MMDDYYSlash = new KnownDateTimeFormat('MM/DD/YY', DateFormatOrder.MDY, /^(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])\/\d{2}$/);
public readonly format: string; public readonly format: string;
public readonly type: DateFormatOrder; public readonly type: DateFormatOrder;
private readonly regex: RegExp; private readonly regex: RegExp;
+47 -11
View File
@@ -4,17 +4,27 @@ import { DateRange } from '@/core/datetime.ts';
export enum TransactionExplorerConditionRelation { export enum TransactionExplorerConditionRelation {
First = 'first', First = 'first',
And = 'and', And = 'and',
Or = 'or' Or = 'or',
AndSub = 'and(',
OrSub = 'or(',
SubEnd = ')'
} }
export type TransactionExplorerSubConditionStartRelation = '(';
export const TransactionExplorerSubConditionStartRelationPlaceholder: TransactionExplorerSubConditionStartRelation = '(';
export const TransactionExplorerConditionRelationPriority: Record<TransactionExplorerConditionRelation, number> = { export const TransactionExplorerConditionRelationPriority: Record<TransactionExplorerConditionRelation, number> = {
[TransactionExplorerConditionRelation.First]: 0, [TransactionExplorerConditionRelation.First]: 0,
[TransactionExplorerConditionRelation.Or]: 1, [TransactionExplorerConditionRelation.Or]: 1,
[TransactionExplorerConditionRelation.And]: 2 [TransactionExplorerConditionRelation.And]: 2,
[TransactionExplorerConditionRelation.AndSub]: 0,
[TransactionExplorerConditionRelation.OrSub]: 0,
[TransactionExplorerConditionRelation.SubEnd]: 0
}; };
export enum TransactionExplorerConditionFieldType { export enum TransactionExplorerConditionFieldType {
Undefined = 'undefined',
TransactionType = 'transactionType', TransactionType = 'transactionType',
TransactionCategory = 'transactionCategory', TransactionCategory = 'transactionCategory',
SourceAccount = 'sourceAccount', SourceAccount = 'sourceAccount',
@@ -81,7 +91,11 @@ export enum TransactionExplorerConditionOperatorType {
StartsWith = 'startsWith', StartsWith = 'startsWith',
NotStartsWith = 'notStartsWith', NotStartsWith = 'notStartsWith',
EndsWith = 'endsWith', EndsWith = 'endsWith',
NotEndsWith = 'notEndsWith' NotEndsWith = 'notEndsWith',
LatitudeBetween = 'latitudeBetween',
LatitudeNotBetween = 'latitudeNotBetween',
LongitudeBetween = 'longitudeBetween',
LongitudeNotBetween = 'longitudeNotBetween'
} }
export class TransactionExplorerConditionOperator implements NameValue { export class TransactionExplorerConditionOperator implements NameValue {
@@ -107,6 +121,10 @@ export class TransactionExplorerConditionOperator implements NameValue {
public static readonly NotStartsWith = new TransactionExplorerConditionOperator('Not starts with', TransactionExplorerConditionOperatorType.NotStartsWith); public static readonly NotStartsWith = new TransactionExplorerConditionOperator('Not starts with', TransactionExplorerConditionOperatorType.NotStartsWith);
public static readonly EndsWith = new TransactionExplorerConditionOperator('Ends with', TransactionExplorerConditionOperatorType.EndsWith); public static readonly EndsWith = new TransactionExplorerConditionOperator('Ends with', TransactionExplorerConditionOperatorType.EndsWith);
public static readonly NotEndsWith = new TransactionExplorerConditionOperator('Not ends with', TransactionExplorerConditionOperatorType.NotEndsWith); public static readonly NotEndsWith = new TransactionExplorerConditionOperator('Not ends with', TransactionExplorerConditionOperatorType.NotEndsWith);
public static readonly LatitudeBetween = new TransactionExplorerConditionOperator('Latitude between', TransactionExplorerConditionOperatorType.LatitudeBetween);
public static readonly LatitudeNotBetween = new TransactionExplorerConditionOperator('Latitude not between', TransactionExplorerConditionOperatorType.LatitudeNotBetween);
public static readonly LongitudeBetween = new TransactionExplorerConditionOperator('Longitude between', TransactionExplorerConditionOperatorType.LongitudeBetween);
public static readonly LongitudeNotBetween = new TransactionExplorerConditionOperator('Longitude not between', TransactionExplorerConditionOperatorType.LongitudeNotBetween);
public readonly name: string; public readonly name: string;
public readonly value: TransactionExplorerConditionOperatorType; public readonly value: TransactionExplorerConditionOperatorType;
@@ -191,6 +209,8 @@ export enum TransactionExplorerDataDimensionType {
DateTimeByDayOfMonth = 'dateTimeByDayOfMonth', DateTimeByDayOfMonth = 'dateTimeByDayOfMonth',
DateTimeByMonthOfYear = 'dateTimeByMonthOfYear', DateTimeByMonthOfYear = 'dateTimeByMonthOfYear',
DateTimeByQuarterOfYear = 'dateTimeByQuarterOfYear', DateTimeByQuarterOfYear = 'dateTimeByQuarterOfYear',
DateTimeByHourOfDay = 'dateTimeByHourOfDay',
TimezoneOffset = 'timezoneOffset',
TransactionType = 'transactionType', TransactionType = 'transactionType',
SourceAccount = 'sourceAccount', SourceAccount = 'sourceAccount',
SourceAccountCategory = 'sourceAccountCategory', SourceAccountCategory = 'sourceAccountCategory',
@@ -220,6 +240,8 @@ export class TransactionExplorerDataDimension implements NameValue {
public static readonly DateTimeByDayOfMonth = new TransactionExplorerDataDimension('Transaction Day of Month', TransactionExplorerDataDimensionType.DateTimeByDayOfMonth); public static readonly DateTimeByDayOfMonth = new TransactionExplorerDataDimension('Transaction Day of Month', TransactionExplorerDataDimensionType.DateTimeByDayOfMonth);
public static readonly DateTimeByMonthOfYear = new TransactionExplorerDataDimension('Transaction Month of Year', TransactionExplorerDataDimensionType.DateTimeByMonthOfYear); public static readonly DateTimeByMonthOfYear = new TransactionExplorerDataDimension('Transaction Month of Year', TransactionExplorerDataDimensionType.DateTimeByMonthOfYear);
public static readonly DateTimeByQuarterOfYear = new TransactionExplorerDataDimension('Transaction Quarter of Year', TransactionExplorerDataDimensionType.DateTimeByQuarterOfYear); public static readonly DateTimeByQuarterOfYear = new TransactionExplorerDataDimension('Transaction Quarter of Year', TransactionExplorerDataDimensionType.DateTimeByQuarterOfYear);
public static readonly DateTimeByHourOfDay = new TransactionExplorerDataDimension('Transaction Hour of Day', TransactionExplorerDataDimensionType.DateTimeByHourOfDay);
public static readonly TimezoneOffset = new TransactionExplorerDataDimension('Transaction Timezone', TransactionExplorerDataDimensionType.TimezoneOffset);
public static readonly TransactionType = new TransactionExplorerDataDimension('Transaction Type', TransactionExplorerDataDimensionType.TransactionType); public static readonly TransactionType = new TransactionExplorerDataDimension('Transaction Type', TransactionExplorerDataDimensionType.TransactionType);
public static readonly SourceAccount = new TransactionExplorerDataDimension('Source Account', TransactionExplorerDataDimensionType.SourceAccount); public static readonly SourceAccount = new TransactionExplorerDataDimension('Source Account', TransactionExplorerDataDimensionType.SourceAccount);
public static readonly SourceAccountCategory = new TransactionExplorerDataDimension('Source Account Category', TransactionExplorerDataDimensionType.SourceAccountCategory); public static readonly SourceAccountCategory = new TransactionExplorerDataDimension('Source Account Category', TransactionExplorerDataDimensionType.SourceAccountCategory);
@@ -260,31 +282,45 @@ export enum TransactionExplorerValueMetricType {
SourceAmountSum = 'sourceAmountSum', SourceAmountSum = 'sourceAmountSum',
SourceAmountAverage = 'sourceAmountAverage', SourceAmountAverage = 'sourceAmountAverage',
SourceAmountMedian = 'sourceAmountMedian', SourceAmountMedian = 'sourceAmountMedian',
SourceAmount90thPercentile = 'source90thPercentileAmount',
SourceAmountMinimum = 'sourceAmountMinimum', SourceAmountMinimum = 'sourceAmountMinimum',
SourceAmountMaximum = 'sourceAmountMaximum' SourceAmountMaximum = 'sourceAmountMaximum',
SourceAmountRange = 'sourceAmountRange',
SourceAmountInterquartileRange = 'sourceAmountInterquartileRange',
SourceAmountVariance = 'sourceAmountVariance',
SourceAmountStandardDeviation = 'sourceAmountStandardDeviation',
SourceAmountCoefficientOfVariation = 'sourceAmountCoefficientOfVariation'
} }
export class TransactionExplorerValueMetric implements NameValue { export class TransactionExplorerValueMetric implements NameValue {
private static readonly allInstances: TransactionExplorerValueMetric[] = []; private static readonly allInstances: TransactionExplorerValueMetric[] = [];
private static readonly allInstancesByValue: Record<string, TransactionExplorerValueMetric> = {}; private static readonly allInstancesByValue: Record<string, TransactionExplorerValueMetric> = {};
public static readonly TransactionCount = new TransactionExplorerValueMetric('Transaction Count', TransactionExplorerValueMetricType.TransactionCount, false); public static readonly TransactionCount = new TransactionExplorerValueMetric('Transaction Count', TransactionExplorerValueMetricType.TransactionCount, false, true);
public static readonly SourceAmountSum = new TransactionExplorerValueMetric('Total Amount', TransactionExplorerValueMetricType.SourceAmountSum, true); public static readonly SourceAmountSum = new TransactionExplorerValueMetric('Total Amount', TransactionExplorerValueMetricType.SourceAmountSum, true, true);
public static readonly SourceAmountAverage = new TransactionExplorerValueMetric('Average Amount', TransactionExplorerValueMetricType.SourceAmountAverage, true); public static readonly SourceAmountAverage = new TransactionExplorerValueMetric('Average Amount', TransactionExplorerValueMetricType.SourceAmountAverage, true, true);
public static readonly SourceAmountMedian = new TransactionExplorerValueMetric('Median Amount', TransactionExplorerValueMetricType.SourceAmountMedian, true); public static readonly SourceAmountMedian = new TransactionExplorerValueMetric('Median Amount', TransactionExplorerValueMetricType.SourceAmountMedian, true, true);
public static readonly SourceAmountMinimum = new TransactionExplorerValueMetric('Minimum Amount', TransactionExplorerValueMetricType.SourceAmountMinimum, true); public static readonly SourceAmount90thPercentile = new TransactionExplorerValueMetric('90th Percentile Amount', TransactionExplorerValueMetricType.SourceAmount90thPercentile, true, true);
public static readonly SourceAmountMaximum = new TransactionExplorerValueMetric('Maximum Amount', TransactionExplorerValueMetricType.SourceAmountMaximum, true); public static readonly SourceAmountMinimum = new TransactionExplorerValueMetric('Minimum Amount', TransactionExplorerValueMetricType.SourceAmountMinimum, true, true);
public static readonly SourceAmountMaximum = new TransactionExplorerValueMetric('Maximum Amount', TransactionExplorerValueMetricType.SourceAmountMaximum, true, true);
public static readonly SourceAmountRange = new TransactionExplorerValueMetric('Range (Max - Min)', TransactionExplorerValueMetricType.SourceAmountRange, true, true);
public static readonly SourceAmountInterquartileRange = new TransactionExplorerValueMetric('Interquartile Range (Q3 - Q1)', TransactionExplorerValueMetricType.SourceAmountInterquartileRange, true, true);
public static readonly SourceAmountVariance = new TransactionExplorerValueMetric('Variance', TransactionExplorerValueMetricType.SourceAmountVariance, false, false);
public static readonly SourceAmountStandardDeviation = new TransactionExplorerValueMetric('Standard Deviation', TransactionExplorerValueMetricType.SourceAmountStandardDeviation, false, false);
public static readonly SourceAmountCoefficientOfVariation = new TransactionExplorerValueMetric('Coefficient of Variation', TransactionExplorerValueMetricType.SourceAmountCoefficientOfVariation, false, false);
public static readonly Default = TransactionExplorerValueMetric.SourceAmountSum; public static readonly Default = TransactionExplorerValueMetric.SourceAmountSum;
public readonly name: string; public readonly name: string;
public readonly value: TransactionExplorerValueMetricType; public readonly value: TransactionExplorerValueMetricType;
public readonly isAmount: boolean; public readonly isAmount: boolean;
public readonly supportSum: boolean;
private constructor(name: string, value: TransactionExplorerValueMetricType, isAmount: boolean) { private constructor(name: string, value: TransactionExplorerValueMetricType, isAmount: boolean, supportSum: boolean) {
this.name = name; this.name = name;
this.value = value; this.value = value;
this.isAmount = isAmount; this.isAmount = isAmount;
this.supportSum = supportSum;
TransactionExplorerValueMetric.allInstances.push(this); TransactionExplorerValueMetric.allInstances.push(this);
TransactionExplorerValueMetric.allInstancesByValue[value] = this; TransactionExplorerValueMetric.allInstancesByValue[value] = this;
+2
View File
@@ -4,8 +4,10 @@ export class KnownFileType {
public static readonly JSON = new KnownFileType('json', 'application/json'); public static readonly JSON = new KnownFileType('json', 'application/json');
public static readonly CSV = new KnownFileType('csv', 'text/csv'); public static readonly CSV = new KnownFileType('csv', 'text/csv');
public static readonly TSV = new KnownFileType('tsv', 'text/tab-separated-values'); public static readonly TSV = new KnownFileType('tsv', 'text/tab-separated-values');
public static readonly SSV = new KnownFileType('txt', 'text/plain');
public static readonly TXT = new KnownFileType('txt', 'text/plain'); public static readonly TXT = new KnownFileType('txt', 'text/plain');
public static readonly MARKDOWN = new KnownFileType('md', 'text/markdown'); public static readonly MARKDOWN = new KnownFileType('md', 'text/markdown');
public static readonly MERMAID = new KnownFileType('mermaid', 'text/vnd.mermaid');
public static readonly JS = new KnownFileType('js', 'application/javascript'); public static readonly JS = new KnownFileType('js', 'application/javascript');
public static readonly JPG = new KnownFileType('jpg', 'image/jpeg'); public static readonly JPG = new KnownFileType('jpg', 'image/jpeg');
+22
View File
@@ -1,6 +1,10 @@
import { type WeekDayValue, WeekDay } from './datetime.ts'; import { type WeekDayValue, WeekDay } from './datetime.ts';
import { TimezoneTypeForStatistics } from './timezone.ts'; import { TimezoneTypeForStatistics } from './timezone.ts';
import { CurrencySortingType } from './currency.ts'; import { CurrencySortingType } from './currency.ts';
import {
TransactionQuickSaveButtonStyle,
TransactionQuickAddButtonActionType
} from './transaction.ts';
import { import {
CategoricalChartType, CategoricalChartType,
TrendChartType, TrendChartType,
@@ -43,6 +47,8 @@ export interface ApplicationSettings extends BaseApplicationSetting {
overviewAccountFilterInHomePage: Record<string, boolean>; overviewAccountFilterInHomePage: Record<string, boolean>;
overviewTransactionCategoryFilterInHomePage: Record<string, boolean>; overviewTransactionCategoryFilterInHomePage: Record<string, boolean>;
// Transaction List Page // Transaction List Page
quickSaveButtonStyleInMobileTransactionListPage: number;
quickAddButtonActionInMobileTransactionEditPage: number;
itemsCountInTransactionListPage: number; itemsCountInTransactionListPage: number;
showTotalAmountInTransactionListPage: boolean; showTotalAmountInTransactionListPage: boolean;
showTagInTransactionListPage: boolean; showTagInTransactionListPage: boolean;
@@ -62,6 +68,9 @@ export interface ApplicationSettings extends BaseApplicationSetting {
hideCategoriesWithoutAccounts: boolean; hideCategoriesWithoutAccounts: boolean;
// Exchange Rates Data Page // Exchange Rates Data Page
currencySortByInExchangeRatesPage: number; currencySortByInExchangeRatesPage: number;
// Browser Cache Management
mapCacheExpiration: number,
exchangeRatesDataCacheExpiration: number,
// Statistics Settings // Statistics Settings
statistics: { statistics: {
defaultChartDataType: number; defaultChartDataType: number;
@@ -107,6 +116,9 @@ export interface WebAuthnConfig {
export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserApplicationCloudSettingType> = { export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserApplicationCloudSettingType> = {
// Basic Settings // Basic Settings
'showAccountBalance': UserApplicationCloudSettingType.Boolean, 'showAccountBalance': UserApplicationCloudSettingType.Boolean,
'autoUpdateExchangeRatesData': UserApplicationCloudSettingType.Boolean,
// Navigation Bar
'showAddTransactionButtonInDesktopNavbar': UserApplicationCloudSettingType.Boolean,
// Overview Page // Overview Page
'showAmountInHomePage': UserApplicationCloudSettingType.Boolean, 'showAmountInHomePage': UserApplicationCloudSettingType.Boolean,
'timezoneUsedForStatisticsInHomePage': UserApplicationCloudSettingType.Number, 'timezoneUsedForStatisticsInHomePage': UserApplicationCloudSettingType.Number,
@@ -117,6 +129,8 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserAp
'showTotalAmountInTransactionListPage': UserApplicationCloudSettingType.Boolean, 'showTotalAmountInTransactionListPage': UserApplicationCloudSettingType.Boolean,
'showTagInTransactionListPage': UserApplicationCloudSettingType.Boolean, 'showTagInTransactionListPage': UserApplicationCloudSettingType.Boolean,
// Transaction Edit Page // Transaction Edit Page
'quickSaveButtonStyleInMobileTransactionListPage': UserApplicationCloudSettingType.Number,
'quickAddButtonActionInMobileTransactionEditPage': UserApplicationCloudSettingType.Number,
'autoSaveTransactionDraft': UserApplicationCloudSettingType.String, 'autoSaveTransactionDraft': UserApplicationCloudSettingType.String,
'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean, 'autoGetCurrentGeoLocation': UserApplicationCloudSettingType.Boolean,
'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean, 'alwaysShowTransactionPicturesInMobileTransactionEditPage': UserApplicationCloudSettingType.Boolean,
@@ -132,6 +146,9 @@ export const ALL_ALLOWED_CLOUD_SYNC_APP_SETTING_KEY_TYPES: Record<string, UserAp
'hideCategoriesWithoutAccounts': UserApplicationCloudSettingType.Boolean, 'hideCategoriesWithoutAccounts': UserApplicationCloudSettingType.Boolean,
// Exchange Rates Data Page // Exchange Rates Data Page
'currencySortByInExchangeRatesPage': UserApplicationCloudSettingType.Number, 'currencySortByInExchangeRatesPage': UserApplicationCloudSettingType.Number,
// Browser Cache Management
'mapCacheExpiration': UserApplicationCloudSettingType.Number,
'exchangeRatesDataCacheExpiration': UserApplicationCloudSettingType.Number,
// Statistics Settings // Statistics Settings
'statistics.defaultChartDataType': UserApplicationCloudSettingType.Number, 'statistics.defaultChartDataType': UserApplicationCloudSettingType.Number,
'statistics.defaultTimezoneType': UserApplicationCloudSettingType.Number, 'statistics.defaultTimezoneType': UserApplicationCloudSettingType.Number,
@@ -172,6 +189,8 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
showTotalAmountInTransactionListPage: true, showTotalAmountInTransactionListPage: true,
showTagInTransactionListPage: true, showTagInTransactionListPage: true,
// Transaction Edit Page // Transaction Edit Page
quickSaveButtonStyleInMobileTransactionListPage: TransactionQuickSaveButtonStyle.Default.type,
quickAddButtonActionInMobileTransactionEditPage: TransactionQuickAddButtonActionType.Default.type,
autoSaveTransactionDraft: 'disabled', autoSaveTransactionDraft: 'disabled',
autoGetCurrentGeoLocation: false, autoGetCurrentGeoLocation: false,
alwaysShowTransactionPicturesInMobileTransactionEditPage: false, alwaysShowTransactionPicturesInMobileTransactionEditPage: false,
@@ -187,6 +206,9 @@ export const DEFAULT_APPLICATION_SETTINGS: ApplicationSettings = {
hideCategoriesWithoutAccounts: false, hideCategoriesWithoutAccounts: false,
// Exchange Rates Data Page // Exchange Rates Data Page
currencySortByInExchangeRatesPage: CurrencySortingType.Default.type, currencySortByInExchangeRatesPage: CurrencySortingType.Default.type,
// Browser Cache Management
mapCacheExpiration: -1,
exchangeRatesDataCacheExpiration: 0,
// Statistics Settings // Statistics Settings
statistics: { statistics: {
defaultChartDataType: ChartDataType.Default.type, defaultChartDataType: ChartDataType.Default.type,
+16 -9
View File
@@ -1,4 +1,4 @@
import type { TypeAndName } from './base.ts'; import type { TypeAndName, TypeAndNameWithAlternativeName } from './base.ts';
import { DateRange } from '@/core/datetime.ts'; import { DateRange } from '@/core/datetime.ts';
export enum StatisticsAnalysisType { export enum StatisticsAnalysisType {
@@ -88,7 +88,8 @@ export class AccountBalanceTrendChartType implements TypeAndName {
public static readonly Area = new AccountBalanceTrendChartType(0, 'Area Chart'); public static readonly Area = new AccountBalanceTrendChartType(0, 'Area Chart');
public static readonly Column = new AccountBalanceTrendChartType(1, 'Column Chart'); public static readonly Column = new AccountBalanceTrendChartType(1, 'Column Chart');
public static readonly Candlestick = new AccountBalanceTrendChartType(2, 'Candlestick Chart'); public static readonly Boxplot = new AccountBalanceTrendChartType(2, 'Boxplot Chart');
public static readonly Candlestick = new AccountBalanceTrendChartType(3, 'Candlestick Chart');
public static readonly Default = TrendChartType.Column; public static readonly Default = TrendChartType.Column;
@@ -193,24 +194,24 @@ export class ChartDataType implements TypeAndName {
} }
} }
export class ChartSortingType implements TypeAndName { export class ChartSortingType implements TypeAndNameWithAlternativeName {
private static readonly allInstances: ChartSortingType[] = []; private static readonly allInstances: ChartSortingType[] = [];
private static readonly allInstancesByType: Record<number, ChartSortingType> = {}; private static readonly allInstancesByType: Record<number, ChartSortingType> = {};
public static readonly Amount = new ChartSortingType(0, 'Amount', 'Sort by Amount'); public static readonly Amount = new ChartSortingType(0, 'Amount', 'Value');
public static readonly DisplayOrder = new ChartSortingType(1, 'Display Order', 'Sort by Display Order'); public static readonly DisplayOrder = new ChartSortingType(1, 'Display Order');
public static readonly Name = new ChartSortingType(2, 'Name', 'Sort by Name'); public static readonly Name = new ChartSortingType(2, 'Name');
public static readonly Default = ChartSortingType.Amount; public static readonly Default = ChartSortingType.Amount;
public readonly type: number; public readonly type: number;
public readonly name: string; public readonly name: string;
public readonly fullName: string; public readonly alternativeName?: string;
private constructor(type: number, name: string, fullName: string) { private constructor(type: number, name: string, alternativeName?: string) {
this.type = type; this.type = type;
this.name = name; this.name = name;
this.fullName = fullName; this.alternativeName = alternativeName;
ChartSortingType.allInstances.push(this); ChartSortingType.allInstances.push(this);
ChartSortingType.allInstancesByType[type] = this; ChartSortingType.allInstancesByType[type] = this;
@@ -285,6 +286,12 @@ export class ChartDateAggregationType {
} }
} }
export enum ExportMermaidChartType {
PieChart = 'pieChart',
XYChartBar = 'xyChartBar',
XYChartLine = 'xyChartLine'
}
export const DEFAULT_CATEGORICAL_CHART_DATA_RANGE: DateRange = DateRange.ThisMonth; export const DEFAULT_CATEGORICAL_CHART_DATA_RANGE: DateRange = DateRange.ThisMonth;
export const DEFAULT_TREND_CHART_DATA_RANGE: DateRange = DateRange.ThisYear; export const DEFAULT_TREND_CHART_DATA_RANGE: DateRange = DateRange.ThisYear;
export const DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE: DateRange = DateRange.ThisYear; export const DEFAULT_ASSET_TRENDS_CHART_DATA_RANGE: DateRange = DateRange.ThisYear;
+63
View File
@@ -66,3 +66,66 @@ export class TransactionTagFilterType implements TypeAndName {
return TransactionTagFilterType.allInstancesByType[type]; return TransactionTagFilterType.allInstancesByType[type];
} }
} }
export class TransactionQuickSaveButtonStyle implements TypeAndName {
private static readonly allInstances: TransactionQuickSaveButtonStyle[] = [];
private static readonly allInstancesByType: Record<number, TransactionQuickSaveButtonStyle> = {};
public static readonly Disabled = new TransactionQuickSaveButtonStyle(0, 'Disabled');
public static readonly BottomFixed = new TransactionQuickSaveButtonStyle(1, 'Bottom Fixed');
public static readonly BottomLeftFloating = new TransactionQuickSaveButtonStyle(2, 'Bottom Left Floating');
public static readonly BottomCenterFloating = new TransactionQuickSaveButtonStyle(3, 'Bottom Center Floating');
public static readonly BottomRightFloating = new TransactionQuickSaveButtonStyle(4, 'Bottom Right Floating');
public static readonly Default = TransactionQuickSaveButtonStyle.BottomRightFloating;
public readonly type: number;
public readonly name: string;
private constructor(type: number, name: string) {
this.type = type;
this.name = name;
TransactionQuickSaveButtonStyle.allInstances.push(this);
TransactionQuickSaveButtonStyle.allInstancesByType[type] = this;
}
public static values(): TransactionQuickSaveButtonStyle[] {
return TransactionQuickSaveButtonStyle.allInstances;
}
public static valueOf(type: number): TransactionQuickSaveButtonStyle | undefined {
return TransactionQuickSaveButtonStyle.allInstancesByType[type];
}
}
export class TransactionQuickAddButtonActionType implements TypeAndName {
private static readonly allInstances: TransactionQuickAddButtonActionType[] = [];
private static readonly allInstancesByType: Record<number, TransactionQuickAddButtonActionType> = {};
public static readonly SaveAndGoBack = new TransactionQuickAddButtonActionType(0, 'Save');
public static readonly OpenMenu = new TransactionQuickAddButtonActionType(1, 'Open Menu');
public static readonly SaveAndAddNewTransaction = new TransactionQuickAddButtonActionType(2, 'Save & New');
public static readonly SaveAndKeepCurrentData = new TransactionQuickAddButtonActionType(3, 'Save & Duplicate');
public static readonly Default = TransactionQuickAddButtonActionType.SaveAndGoBack;
public readonly type: number;
public readonly name: string;
private constructor(type: number, name: string) {
this.type = type;
this.name = name;
TransactionQuickAddButtonActionType.allInstances.push(this);
TransactionQuickAddButtonActionType.allInstancesByType[type] = this;
}
public static values(): TransactionQuickAddButtonActionType[] {
return TransactionQuickAddButtonActionType.allInstances;
}
public static valueOf(type: number): TransactionQuickAddButtonActionType | undefined {
return TransactionQuickAddButtonActionType.allInstancesByType[type];
}
}
+2 -1
View File
@@ -52,7 +52,7 @@ import 'vuetify/styles';
import * as echarts from 'echarts/core'; import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers'; import { CanvasRenderer } from 'echarts/renderers';
import { LineChart, BarChart, PieChart, ScatterChart, CandlestickChart, RadarChart, SankeyChart } from 'echarts/charts'; import { LineChart, BarChart, PieChart, ScatterChart, BoxplotChart, CandlestickChart, RadarChart, SankeyChart } from 'echarts/charts';
import { import {
GridComponent, GridComponent,
TooltipComponent, TooltipComponent,
@@ -503,6 +503,7 @@ echarts.use([
BarChart, BarChart,
PieChart, PieChart,
ScatterChart, ScatterChart,
BoxplotChart,
CandlestickChart, CandlestickChart,
RadarChart, RadarChart,
SankeyChart, SankeyChart,
+1
View File
@@ -0,0 +1 @@
export const extendMdiSemicolon: string = 'M15.6,2.4H8V10H15.6V6.6Z M5.6,12.4H15.56V18.368L12.368,21.752 H8.4L11.576,18.368H8V12.4Z';
+304
View File
@@ -0,0 +1,304 @@
import type {
BrowserCacheStatistics,
SWMapCacheConfig
} from '@/core/cache.ts';
import {
SW_PRECACHE_CACHE_NAME_PREFIX,
SW_RUNTIME_CACHE_NAME_PREFIX,
SW_ASSETS_CACHE_NAME,
SW_CODE_CACHE_NAME,
SW_MAP_CACHE_NAME,
SW_SHARE_CACHE_NAME,
SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG,
SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE,
MAP_CACHE_MAX_ENTRIES
} from '@/consts/cache.ts';
import { isFunction, isObject, isNumber } from './common.ts';
import services from './services.ts';
import logger from './logger.ts';
let controllerchangeListenerAdded: boolean = false;
function findFirstCacheName(prefix: string): Promise<string> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.reject(new Error('caches API is not supported'));
}
return window.caches.keys().then(cacheNames => {
for (const cacheName of cacheNames) {
if (cacheName.startsWith(prefix)) {
return cacheName;
}
}
throw new Error(`cache with prefix "${prefix}" not found`);
});
}
function doUpdateMapCacheExpiration(expireSeconds: number): Promise<void> {
const config: SWMapCacheConfig = {
enabled: expireSeconds >= 0,
patterns: services.getMapProxyTileImageAndAnnotationImageUrlPatterns(),
maxEntries: MAP_CACHE_MAX_ENTRIES,
maxAgeMilliseconds: expireSeconds * 1000
};
return new Promise((resolve, reject) => {
if (!navigator.serviceWorker || !navigator.serviceWorker.controller) {
reject(new Error('Service worker is not supported or not active'));
return;
}
const controller = navigator.serviceWorker.controller;
navigator.serviceWorker.ready.then(() => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
if (event.data && event.data.type === SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG_RESPONSE) {
logger.info('Map cache config updated successfully in service worker: ' + JSON.stringify(event.data.payload));
resolve();
} else {
logger.error('cannot update map cache config, invalid response from service worker', event);
reject(new Error('Invalid response from service worker'));
}
};
controller.postMessage({
type: SW_MESSAGE_TYPE_UPDATE_MAP_CACHE_CONFIG,
payload: config
}, [messageChannel.port2]);
}).catch(error => {
logger.error('failed to update map cache config', error);
reject(error);
});
});
}
async function getCacheTotalSize(cacheName: string): Promise<number> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.reject(new Error('caches API is not supported'));
}
const cache = await window.caches.open(cacheName);
const requests = await cache.keys();
let totalSize = 0;
for (const request of requests) {
try {
const response = await cache.match(request);
if (response) {
const blob = await response.clone().blob();
totalSize += blob.size;
}
} catch (ex) {
logger.warn(`failed to get size for request ${request.url} in cache ${cacheName}`, ex);
}
}
return totalSize;
}
export function getShareCacheImageBlob(): Promise<Blob | undefined> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.resolve(undefined);
}
return new Promise((resolve) => {
window.caches.open(SW_SHARE_CACHE_NAME).then(cache => {
cache.match(SW_SHARE_CACHE_NAME).then(response => {
if (!response) {
resolve(undefined);
return;
}
response.blob().then(blob => {
cache.delete(SW_SHARE_CACHE_NAME).then(() => {
resolve(blob);
}).catch(error => {
logger.warn('failed to delete share cache image blob', error);
resolve(blob);
});
}).catch(error => {
logger.error('failed to read share cache image blob', error);
resolve(undefined);
});
}).catch(error => {
logger.error('failed to match share cache image blob', error);
resolve(undefined);
});
}).catch(error => {
logger.error('failed to open share cache', error);
resolve(undefined);
});
});
}
export function loadBrowserCacheStatistics(): Promise<BrowserCacheStatistics> {
return new Promise((resolve, reject) => {
const caches = window.caches;
if (!caches) {
logger.error('caches API is not supported in this browser');
reject(new Error('caches API is not supported in this browser'));
return;
}
return Promise.all([
navigator && navigator.storage && isFunction(navigator.storage.estimate) ? navigator.storage.estimate() : Promise.resolve(undefined),
findFirstCacheName(SW_PRECACHE_CACHE_NAME_PREFIX).then(cacheName => getCacheTotalSize(cacheName)).catch(() => 0),
findFirstCacheName(SW_RUNTIME_CACHE_NAME_PREFIX).then(cacheName => getCacheTotalSize(cacheName)).catch(() => 0),
getCacheTotalSize(SW_CODE_CACHE_NAME),
getCacheTotalSize(SW_ASSETS_CACHE_NAME),
getCacheTotalSize(SW_MAP_CACHE_NAME)
]).then(([storageEstimate, precacheCacheSize, runtimeCacheSize, codeCacheSize, assetsCacheSize, mapCacheSize]) => {
let totalCacheSize: number = 0;
if (storageEstimate) {
const cachesUsage = 'usageDetails' in storageEstimate
&& isObject(storageEstimate.usageDetails)
&& 'caches' in storageEstimate.usageDetails
? storageEstimate.usageDetails.caches : undefined;
if (isNumber(cachesUsage)) {
totalCacheSize = cachesUsage;
} else if (isNumber(storageEstimate.usage)) {
totalCacheSize = storageEstimate.usage;
}
}
if (totalCacheSize < 1) {
totalCacheSize = precacheCacheSize + runtimeCacheSize + codeCacheSize + assetsCacheSize + mapCacheSize;
}
let othersCacheSize: number = totalCacheSize - precacheCacheSize - runtimeCacheSize - codeCacheSize - assetsCacheSize - mapCacheSize;
if (othersCacheSize < 0) {
othersCacheSize = 0;
}
resolve({
totalCacheSize: totalCacheSize,
codeCacheSize: codeCacheSize + runtimeCacheSize,
assetsCacheSize: assetsCacheSize + precacheCacheSize,
mapCacheSize: mapCacheSize,
othersCacheSize: othersCacheSize
});
}).catch(error => {
logger.error("failed to clear cache", error);
reject(error);
});
});
}
export function updateMapCacheExpiration(expireSeconds: number): void {
if ('serviceWorker' in navigator) {
if (!controllerchangeListenerAdded) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
doUpdateMapCacheExpiration(expireSeconds);
});
controllerchangeListenerAdded = true;
}
if (navigator.serviceWorker.controller) {
doUpdateMapCacheExpiration(expireSeconds);
}
}
}
export function clearCaches(cacheNames: string[], cacheNamePrefixes?: string[]): Promise<void> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.reject();
}
return new Promise((resolve) => {
const promises = [];
for (const cacheName of cacheNames) {
promises.push(window.caches.delete(cacheName).then(success => {
if (success) {
logger.info(`cache "${cacheName}" cleared successfully`);
return Promise.resolve(cacheName);
} else {
logger.warn(`failed to clear cache "${cacheName}"`);
return Promise.reject(cacheName);
}
}));
}
if (cacheNamePrefixes) {
for (const prefix of cacheNamePrefixes) {
promises.push(findFirstCacheName(prefix).then(cacheName => {
return window.caches.delete(cacheName).then(success => {
if (success) {
logger.info(`cache "${cacheName}" cleared successfully`);
return Promise.resolve(cacheName);
} else {
logger.warn(`failed to clear cache "${cacheName}"`);
return Promise.reject(cacheName);
}
});
}).catch(error => {
logger.warn(`cache with prefix "${prefix}" not found`, error);
return Promise.resolve();
}));
}
}
Promise.all(promises).then(() => {
resolve();
}).catch(() => {
resolve();
});
});
}
export function clearApplicationCodeCache(): Promise<void> {
return clearCaches([SW_CODE_CACHE_NAME], [SW_RUNTIME_CACHE_NAME_PREFIX]);
}
export function clearMapDataCache(): Promise<void> {
return clearCaches([SW_MAP_CACHE_NAME]);
}
export function clearAllBrowserCaches(): Promise<void> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.reject();
}
return new Promise((resolve, reject) => {
window.caches.keys().then(cacheNames => {
const promises = [];
for (const cacheName of cacheNames) {
promises.push(window.caches.delete(cacheName).then(success => {
if (success) {
logger.info(`cache "${cacheName}" cleared successfully`);
return Promise.resolve(cacheName);
} else {
logger.warn(`failed to clear cache "${cacheName}"`);
return Promise.reject(cacheName);
}
}));
}
Promise.all(promises).then(() => {
logger.info("all caches cleared successfully");
resolve();
}).catch(() => {
resolve();
});
}).catch(error => {
logger.error("failed to clear cache", error);
reject(error);
});
});
}
@@ -48,7 +48,7 @@ const localeData: ChineseCalendarLocaleData = {
const ordinalSuffix = ['st', 'nd', 'rd']; const ordinalSuffix = ['st', 'nd', 'rd'];
describe('getChineseYearMonthAllDayInfos', () => { describe('getChineseYearMonthAllDayInfos', () => {
const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').split('\n'); const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').replace(/\r/g, '').split('\n');
const allMonthChineseDays: Record<string, string[]> = {}; const allMonthChineseDays: Record<string, string[]> = {};
const allMonthSolarTermNames: Record<string, string[]> = {}; const allMonthSolarTermNames: Record<string, string[]> = {};
let currentMonthChineseDays: string[] = []; let currentMonthChineseDays: string[] = [];
@@ -121,7 +121,7 @@ describe('getChineseYearMonthAllDayInfos', () => {
}); });
describe('getChineseYearMonthDayInfo', () => { describe('getChineseYearMonthDayInfo', () => {
const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').split('\n'); const lines: string[] = fs.readFileSync(path.join(__dirname, 'chinese_calendar_all_data.txt'), 'utf8').replace(/\r/g, '').split('\n');
for (const line of lines) { for (const line of lines) {
if (!line.trim() || line.startsWith('#')) { if (!line.trim() || line.startsWith('#')) {
+9 -3
View File
@@ -1,5 +1,11 @@
import { keys, keysIfValueEquals, values } from '@/core/base.ts'; import {
import type { NameValue, TypeAndName, TypeAndDisplayName} from '@/core/base.ts'; type GenericNameValue,
type TypeAndName,
type TypeAndDisplayName,
keys,
keysIfValueEquals,
values
} from '@/core/base.ts';
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export function isFunction(val: unknown): val is Function { export function isFunction(val: unknown): val is Function {
@@ -285,7 +291,7 @@ export function getItemByKeyValue<T>(src: Record<string, T>[] | Record<string, R
return null; return null;
} }
export function findNameByValue(items: NameValue[], value: string): string | null { export function findNameByValue<T>(items: GenericNameValue<T>[], value: T): string | null {
for (const item of items) { for (const item of items) {
if (item.value === value) { if (item.value === value) {
return item.name; return item.name;
+14
View File
@@ -27,6 +27,20 @@ export function initMapProvider(language?: string): void {
} }
} }
export function isMapProviderUseExternalSDK(): boolean {
const mapProviderType = getMapProvider();
if (mapProviderType === 'googlemap') {
return true;
} else if (mapProviderType === 'baidumap') {
return true;
} else if (mapProviderType === 'amap') {
return true;
} else {
return false;
}
}
export function getMapWebsite(): string { export function getMapWebsite(): string {
return mapProvider?.getWebsite() || ''; return mapProvider?.getWebsite() || '';
} }
+8 -2
View File
@@ -617,8 +617,8 @@ export default {
deleteTransaction: (req: TransactionDeleteRequest): ApiResponsePromise<boolean> => { deleteTransaction: (req: TransactionDeleteRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/transactions/delete.json', req); return axios.post<ApiResponse<boolean>>('v1/transactions/delete.json', req);
}, },
parseImportDsvFile: ({ fileType, fileEncoding, importFile }: { fileType: string, fileEncoding?: string, importFile: File }): ApiResponsePromise<string[][]> => { parseImportCustomFile: ({ fileType, fileEncoding, importFile }: { fileType: string, fileEncoding?: string, importFile: File }): ApiResponsePromise<string[][]> => {
return axios.postForm<ApiResponse<string[][]>>('v1/transactions/parse_dsv_file.json', { return axios.postForm<ApiResponse<string[][]>>('v1/transactions/parse_custom_file.json', {
fileType: fileType, fileType: fileType,
fileEncoding: fileEncoding, fileEncoding: fileEncoding,
file: importFile file: importFile
@@ -831,6 +831,12 @@ export default {
generateQrCodeUrl: (qrCodeName: string): string => { generateQrCodeUrl: (qrCodeName: string): string => {
return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`; return `${getBasePath()}${BASE_QRCODE_PATH}/${qrCodeName}.png`;
}, },
getMapProxyTileImageAndAnnotationImageUrlPatterns(): string[] {
return [
`.*${BASE_PROXY_URL_PATH}/map/tile/[^/]+/[^/]+/[^/]+\\.png\\?provider=[^&]+.*$`,
`.*${BASE_PROXY_URL_PATH}/map/annotation/[^/]+/[^/]+/[^/]+\\.png\\?provider=[^&]+.*$`
];
},
generateMapProxyTileImageUrl: (mapProvider: string, language: string): string => { generateMapProxyTileImageUrl: (mapProvider: string, language: string): string => {
const token = getCurrentToken(); const token = getCurrentToken();
let url = `${getBasePath()}${BASE_PROXY_URL_PATH}/map/tile/{z}/{x}/{y}.png?provider=${mapProvider}&token=${token}`; let url = `${getBasePath()}${BASE_PROXY_URL_PATH}/map/tile/{z}/{x}/{y}.png?provider=${mapProvider}&token=${token}`;
+2 -37
View File
@@ -188,7 +188,7 @@ export function startDownloadFile(fileName: string, fileData: Blob): void {
dataLink.click(); dataLink.click();
} }
export function compressJpgImage(file: File, maxWidth: number, maxHeight: number, quality: number): Promise<Blob> { export function compressJpgImage(blob: Blob, maxWidth: number, maxHeight: number, quality: number): Promise<Blob> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
@@ -242,41 +242,6 @@ export function compressJpgImage(file: File, maxWidth: number, maxHeight: number
reject(error); reject(error);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(blob);
});
}
export function clearBrowserCaches(): Promise<void> {
if (!window.caches) {
logger.error('caches API is not supported in this browser');
return Promise.reject();
}
return new Promise((resolve, reject) => {
window.caches.keys().then(cacheNames => {
const promises = [];
for (const cacheName of cacheNames) {
promises.push(window.caches.delete(cacheName).then(success => {
if (success) {
logger.info(`cache "${cacheName}" cleared successfully`);
return Promise.resolve(cacheName);
} else {
logger.warn(`failed to clear cache "${cacheName}"`);
return Promise.reject(cacheName);
}
}));
}
Promise.all(promises).then(() => {
logger.info("all caches cleared successfully");
resolve();
}).catch(() => {
resolve();
});
}).catch(error => {
logger.warn("failed to clear cache", error);
reject(error);
});
}); });
} }
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM United States (CP-437)", "cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)", "cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)", "cp037": "IBM EBCDIC US/Canada (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optional] Category name", "fieldCategoryNameDescription": "[optional] Category name",
"fieldSourceAccountNameDescription": "[optional] Source account name", "fieldSourceAccountNameDescription": "[optional] Source account name",
"fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)", "fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)",
"fieldSourceAmountDescription": "[required] Source amount", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'",
"fieldCommentDescription": "[optional] Description" "fieldCommentDescription": "[optional] Description"
@@ -1496,7 +1495,10 @@
"Remove": "Entfernen", "Remove": "Entfernen",
"Delete": "Löschen", "Delete": "Löschen",
"Duplicate": "Duplizieren", "Duplicate": "Duplizieren",
"Open Menu": "Open Menu",
"Sort": "Sortieren", "Sort": "Sortieren",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "Datum", "Date": "Datum",
"Time": "Zeit", "Time": "Zeit",
"Color": "Farbe", "Color": "Farbe",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "Heute", "Today": "Heute",
"Yesterday": "Gestern", "Yesterday": "Gestern",
"Recent 7 days": "Letzte 7 Tage", "Recent 7 days": "Letzte 7 Tage",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "Tortendiagramm", "Pie Chart": "Tortendiagramm",
"Bar Chart": "Balkendiagramm", "Bar Chart": "Balkendiagramm",
"Radar Chart": "Radar Chart", "Radar Chart": "Radar Chart",
"Area Chart": "Flächendiagramm", "Area Chart": "Flächendiagramm",
"Column Chart": "Säulendiagramm", "Column Chart": "Säulendiagramm",
"Bubble Chart": "Bubble Chart", "Bubble Chart": "Bubble Chart",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Candlestick Chart", "Candlestick Chart": "Candlestick Chart",
"Sankey Chart": "Sankey Chart", "Sankey Chart": "Sankey Chart",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "Kontoliste", "Account List": "Kontoliste",
"This Week": "Diese Woche", "This Week": "Diese Woche",
"This Month": "Dieser Monat", "This Month": "Dieser Monat",
@@ -1900,6 +1919,8 @@
"Swap Account": "Konto tauschen", "Swap Account": "Konto tauschen",
"Swap Amount": "Betrag tauschen", "Swap Amount": "Betrag tauschen",
"Swap Account and Amount": "Konto und Betrag tauschen", "Swap Account and Amount": "Konto und Betrag tauschen",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "Duplicate (With Time)", "Duplicate (With Time)": "Duplicate (With Time)",
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)", "Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)", "Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Other Finance App File Format", "Other Finance App File Format": "Other Finance App File Format",
"ezbookkeeping Data Export File": "ezBookkeeping-Datenexportdatei", "ezbookkeeping Data Export File": "ezBookkeeping-Datenexportdatei",
"Excel Workbook File": "Excel Workbook File", "Excel Workbook File": "Excel Workbook File",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX)-Datei", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX)-Datei",
"Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX)-Datei", "Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX)-Datei",
"Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF)-Datei", "Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF)-Datei",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "Ausgewählte Konten im Batch ersetzen", "Batch Replace Selected Accounts": "Ausgewählte Konten im Batch ersetzen",
"Batch Replace Selected Destination Accounts": "Ausgewählte Zielkonten im Batch ersetzen", "Batch Replace Selected Destination Accounts": "Ausgewählte Zielkonten im Batch ersetzen",
"Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags", "Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Batch Add Transaction Tags", "Batch Add Transaction Tags": "Batch Add Transaction Tags",
"Replace Invalid Expense Categories": "Ungültige Ausgabenkategorien ersetzen", "Replace Invalid Expense Categories": "Ungültige Ausgabenkategorien ersetzen",
"Replace Invalid Income Categories": "Ungültige Einnahmenkategorien ersetzen", "Replace Invalid Income Categories": "Ungültige Einnahmenkategorien ersetzen",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "Ungültiges Tag", "Invalid Tag": "Ungültiges Tag",
"Target Tag": "Ziel-Tag", "Target Tag": "Ziel-Tag",
"Remove Tag": "Remove Tag", "Remove Tag": "Remove Tag",
"Target Timezone": "Target Timezone",
"(Empty)": "(Leer)", "(Empty)": "(Leer)",
"Source Value": "Source Value", "Source Value": "Source Value",
"Target Value": "Target Value", "Target Value": "Target Value",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Maximum Balance", "Maximum Balance": "Maximum Balance",
"Median Balance": "Median Balance", "Median Balance": "Median Balance",
"Average Balance": "Average Balance", "Average Balance": "Average Balance",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "Ausgaben nach Konto", "Expense By Account": "Ausgaben nach Konto",
"Expense By Primary Category": "Ausgaben nach Primärkategorie", "Expense By Primary Category": "Ausgaben nach Primärkategorie",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "Höchstbetrag", "Maximum Amount": "Höchstbetrag",
"Display Order": "Anzeigereihenfolge", "Display Order": "Anzeigereihenfolge",
"Name": "Name", "Name": "Name",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "Nach Betrag sortieren",
"Sort by Display Order": "Nach Anzeigereihenfolge sortieren",
"Sort by Name": "Nach Name sortieren",
"Time Granularity": "Time Granularity", "Time Granularity": "Time Granularity",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "Nach Monat aggregieren", "Aggregate by Month": "Nach Monat aggregieren",
"Aggregate by Quarter": "Nach Quartal aggregieren", "Aggregate by Quarter": "Nach Quartal aggregieren",
"Aggregate by Year": "Nach Jahr aggregieren", "Aggregate by Year": "Nach Jahr aggregieren",
"Aggregate by Fiscal Year": "Aggregate by Fiscal Year", "Aggregate by Fiscal Year": "Aggregate by Fiscal Year",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "Konten filtern", "Filter Accounts": "Konten filtern",
"Filter Transaction Categories": "Transaktionskategorien filtern", "Filter Transaction Categories": "Transaktionskategorien filtern",
"Filter Transaction Tags": "Transaktions-Tags filtern", "Filter Transaction Tags": "Transaktions-Tags filtern",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "Monatlichen Gesamtbetrag anzeigen", "Show Monthly Total Amount": "Monatlichen Gesamtbetrag anzeigen",
"Show Transaction Tags": "Transaktions-Tag anzeigen", "Show Transaction Tags": "Transaktions-Tag anzeigen",
"Transaction Edit Page": "Transaktionsbearbeitungsseite", "Transaction Edit Page": "Transaktionsbearbeitungsseite",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "Entwurf automatisch speichern", "Automatically Save Draft": "Entwurf automatisch speichern",
"Always Show Confirmation": "Bestätigung jedes mal anzeigen", "Always Show Confirmation": "Bestätigung jedes mal anzeigen",
"Automatically Add Geolocation": "Geolocation automatisch hinzufügen", "Automatically Add Geolocation": "Geolocation automatisch hinzufügen",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File", "Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File",
"Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File", "Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown File", "Markdown File": "Markdown File",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "Benutzerdaten löschen", "Clear User Data": "Benutzerdaten löschen",
"Clear All Transactions": "Clear All Transactions", "Clear All Transactions": "Clear All Transactions",
"Clear All Data": "Clear All Data", "Clear All Data": "Clear All Data",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings", "Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings",
"Unable to update user synchronized application settings": "Unable to update user synchronized application settings", "Unable to update user synchronized application settings": "Unable to update user synchronized application settings",
"Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings", "Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "Sind Sie sicher, dass Sie sich erneut anmelden möchten?", "Are you sure you want to re-login?": "Sind Sie sicher, dass Sie sich erneut anmelden möchten?",
"Exchange Rates Data": "Wechselkursdaten", "Exchange Rates Data": "Wechselkursdaten",
"User Custom": "User Custom", "User Custom": "User Custom",
+66 -11
View File
@@ -250,7 +250,7 @@
"long": "December" "long": "December"
}, },
"monthDayOrdinal": { "monthDayOrdinal": {
"1": "1th", "1": "1st",
"2": "2nd", "2": "2nd",
"3": "3rd", "3": "3rd",
"4": "4th", "4": "4th",
@@ -270,7 +270,7 @@
"18": "18th", "18": "18th",
"19": "19th", "19": "19th",
"20": "20th", "20": "20th",
"21": "21th", "21": "21st",
"22": "22nd", "22": "22nd",
"23": "23rd", "23": "23rd",
"24": "24th", "24": "24th",
@@ -280,7 +280,7 @@
"28": "28th", "28": "28th",
"29": "29th", "29": "29th",
"30": "30th", "30": "30th",
"31": "31th" "31": "31st"
}, },
"quarter": { "quarter": {
"q1": "Q1", "q1": "Q1",
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM United States (CP-437)", "cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)", "cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)", "cp037": "IBM EBCDIC US/Canada (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optional] Category name", "fieldCategoryNameDescription": "[optional] Category name",
"fieldSourceAccountNameDescription": "[optional] Source account name", "fieldSourceAccountNameDescription": "[optional] Source account name",
"fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)", "fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)",
"fieldSourceAmountDescription": "[required] Source amount", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'",
"fieldCommentDescription": "[optional] Description" "fieldCommentDescription": "[optional] Description"
@@ -1496,7 +1495,10 @@
"Remove": "Remove", "Remove": "Remove",
"Delete": "Delete", "Delete": "Delete",
"Duplicate": "Duplicate", "Duplicate": "Duplicate",
"Open Menu": "Open Menu",
"Sort": "Sort", "Sort": "Sort",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "Date", "Date": "Date",
"Time": "Time", "Time": "Time",
"Color": "Color", "Color": "Color",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "Today", "Today": "Today",
"Yesterday": "Yesterday", "Yesterday": "Yesterday",
"Recent 7 days": "Recent 7 days", "Recent 7 days": "Recent 7 days",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "Pie Chart", "Pie Chart": "Pie Chart",
"Bar Chart": "Bar Chart", "Bar Chart": "Bar Chart",
"Radar Chart": "Radar Chart", "Radar Chart": "Radar Chart",
"Area Chart": "Area Chart", "Area Chart": "Area Chart",
"Column Chart": "Column Chart", "Column Chart": "Column Chart",
"Bubble Chart": "Bubble Chart", "Bubble Chart": "Bubble Chart",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Candlestick Chart", "Candlestick Chart": "Candlestick Chart",
"Sankey Chart": "Sankey Chart", "Sankey Chart": "Sankey Chart",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "Account List", "Account List": "Account List",
"This Week": "This Week", "This Week": "This Week",
"This Month": "This Month", "This Month": "This Month",
@@ -1900,6 +1919,8 @@
"Swap Account": "Swap Account", "Swap Account": "Swap Account",
"Swap Amount": "Swap Amount", "Swap Amount": "Swap Amount",
"Swap Account and Amount": "Swap Account and Amount", "Swap Account and Amount": "Swap Account and Amount",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "Duplicate (With Time)", "Duplicate (With Time)": "Duplicate (With Time)",
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)", "Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)", "Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Other Finance App File Format", "Other Finance App File Format": "Other Finance App File Format",
"ezbookkeeping Data Export File": "ezbookkeeping Data Export File", "ezbookkeeping Data Export File": "ezbookkeeping Data Export File",
"Excel Workbook File": "Excel Workbook File", "Excel Workbook File": "Excel Workbook File",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) File", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) File",
"Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) File", "Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) File",
"Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) File", "Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) File",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "Batch Replace Selected Accounts", "Batch Replace Selected Accounts": "Batch Replace Selected Accounts",
"Batch Replace Selected Destination Accounts": "Batch Replace Selected Destination Accounts", "Batch Replace Selected Destination Accounts": "Batch Replace Selected Destination Accounts",
"Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags", "Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Batch Add Transaction Tags", "Batch Add Transaction Tags": "Batch Add Transaction Tags",
"Replace Invalid Expense Categories": "Replace Invalid Expense Categories", "Replace Invalid Expense Categories": "Replace Invalid Expense Categories",
"Replace Invalid Income Categories": "Replace Invalid Income Categories", "Replace Invalid Income Categories": "Replace Invalid Income Categories",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "Invalid Tag", "Invalid Tag": "Invalid Tag",
"Target Tag": "Target Tag", "Target Tag": "Target Tag",
"Remove Tag": "Remove Tag", "Remove Tag": "Remove Tag",
"Target Timezone": "Target Timezone",
"(Empty)": "(Empty)", "(Empty)": "(Empty)",
"Source Value": "Source Value", "Source Value": "Source Value",
"Target Value": "Target Value", "Target Value": "Target Value",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Maximum Balance", "Maximum Balance": "Maximum Balance",
"Median Balance": "Median Balance", "Median Balance": "Median Balance",
"Average Balance": "Average Balance", "Average Balance": "Average Balance",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "Expense By Account", "Expense By Account": "Expense By Account",
"Expense By Primary Category": "Expense By Primary Category", "Expense By Primary Category": "Expense By Primary Category",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "Maximum Amount", "Maximum Amount": "Maximum Amount",
"Display Order": "Display Order", "Display Order": "Display Order",
"Name": "Name", "Name": "Name",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "Sort by Amount",
"Sort by Display Order": "Sort by Display Order",
"Sort by Name": "Sort by Name",
"Time Granularity": "Time Granularity", "Time Granularity": "Time Granularity",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "Aggregate by Month", "Aggregate by Month": "Aggregate by Month",
"Aggregate by Quarter": "Aggregate by Quarter", "Aggregate by Quarter": "Aggregate by Quarter",
"Aggregate by Year": "Aggregate by Year", "Aggregate by Year": "Aggregate by Year",
"Aggregate by Fiscal Year": "Aggregate by Fiscal Year", "Aggregate by Fiscal Year": "Aggregate by Fiscal Year",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "Filter Accounts", "Filter Accounts": "Filter Accounts",
"Filter Transaction Categories": "Filter Transaction Categories", "Filter Transaction Categories": "Filter Transaction Categories",
"Filter Transaction Tags": "Filter Transaction Tags", "Filter Transaction Tags": "Filter Transaction Tags",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "Show Monthly Total Amount", "Show Monthly Total Amount": "Show Monthly Total Amount",
"Show Transaction Tags": "Show Transaction Tags", "Show Transaction Tags": "Show Transaction Tags",
"Transaction Edit Page": "Transaction Edit Page", "Transaction Edit Page": "Transaction Edit Page",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "Automatically Save Draft", "Automatically Save Draft": "Automatically Save Draft",
"Always Show Confirmation": "Always Show Confirmation", "Always Show Confirmation": "Always Show Confirmation",
"Automatically Add Geolocation": "Automatically Add Geolocation", "Automatically Add Geolocation": "Automatically Add Geolocation",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File", "Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File",
"Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File", "Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown File", "Markdown File": "Markdown File",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "Clear User Data", "Clear User Data": "Clear User Data",
"Clear All Transactions": "Clear All Transactions", "Clear All Transactions": "Clear All Transactions",
"Clear All Data": "Clear All Data", "Clear All Data": "Clear All Data",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings", "Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings",
"Unable to update user synchronized application settings": "Unable to update user synchronized application settings", "Unable to update user synchronized application settings": "Unable to update user synchronized application settings",
"Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings", "Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "Are you sure you want to re-login?", "Are you sure you want to re-login?": "Are you sure you want to re-login?",
"Exchange Rates Data": "Exchange Rates Data", "Exchange Rates Data": "Exchange Rates Data",
"User Custom": "User Custom", "User Custom": "User Custom",
+138 -83
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 con BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian con BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian con BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM United States (CP-437)", "cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)", "cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)", "cp037": "IBM EBCDIC US/Canada (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optional] Category name", "fieldCategoryNameDescription": "[optional] Category name",
"fieldSourceAccountNameDescription": "[optional] Source account name", "fieldSourceAccountNameDescription": "[optional] Source account name",
"fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)", "fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)",
"fieldSourceAmountDescription": "[required] Source amount", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'",
"fieldCommentDescription": "[optional] Description" "fieldCommentDescription": "[optional] Description"
@@ -1496,7 +1495,10 @@
"Remove": "Eliminar", "Remove": "Eliminar",
"Delete": "Borrar", "Delete": "Borrar",
"Duplicate": "Duplicar", "Duplicate": "Duplicar",
"Open Menu": "Open Menu",
"Sort": "Ordenar", "Sort": "Ordenar",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "Fecha", "Date": "Fecha",
"Time": "Hora", "Time": "Hora",
"Color": "Color", "Color": "Color",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "Hoy", "Today": "Hoy",
"Yesterday": "Ayer", "Yesterday": "Ayer",
"Recent 7 days": "Últimos 7 días", "Recent 7 days": "Últimos 7 días",
@@ -1553,24 +1557,29 @@
"Between": "Entre", "Between": "Entre",
"Not between": "No entre", "Not between": "No entre",
"In": "En", "In": "En",
"Has any": "Has any", "Has any": "Tiene cualquiera",
"Has all": "Has all", "Has all": "Tiene todas",
"Not has any": "Not has any", "Not has any": "No tiene cualquiera",
"Not has all": "Not has all", "Not has all": "No tiene todas",
"Is empty": "Is empty", "Is empty": "Está vacía",
"Is not empty": "Is not empty", "Is not empty": "No está vacía",
"Contains": "Contains", "Contains": "Contiene",
"Not contains": "Not contains", "Not contains": "No contiene",
"Starts with": "Starts with", "Starts with": "Empieza por",
"Not starts with": "Not starts with", "Not starts with": "No empieza por",
"Ends with": "Ends with", "Ends with": "Termina en",
"Not ends with": "Not ends with", "Not ends with": "No termina en",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "Gráfico Circular", "Pie Chart": "Gráfico Circular",
"Bar Chart": "Gráfico de Barras", "Bar Chart": "Gráfico de Barras",
"Radar Chart": "Gráfico de Radar", "Radar Chart": "Gráfico de Radar",
"Area Chart": "Gráfico de Área", "Area Chart": "Gráfico de Área",
"Column Chart": "Gráfico de Columnas", "Column Chart": "Gráfico de Columnas",
"Bubble Chart": "Gráfico de Burbujas", "Bubble Chart": "Gráfico de Burbujas",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Gráfico de Velas", "Candlestick Chart": "Gráfico de Velas",
"Sankey Chart": "Diagrama de Sankey", "Sankey Chart": "Diagrama de Sankey",
"Column Chart (Stacked)": "Gráfico de columnas (apilado)", "Column Chart (Stacked)": "Gráfico de columnas (apilado)",
@@ -1720,50 +1729,51 @@
"Transaction Calendar": "Calendario de Transacciones", "Transaction Calendar": "Calendario de Transacciones",
"Transaction Details": "Detalles", "Transaction Details": "Detalles",
"Statistics & Analysis": "Estadísticas y Análisis", "Statistics & Analysis": "Estadísticas y Análisis",
"Insights Explorer": "Insights Explorer", "Insights Explorer": "Explorador de Datos",
"Insights Explorers": "Insights Explorers", "Insights Explorers": "Exploradores de Datos",
"Query": "Query", "Query": "Consulta",
"Data Table": "Data Table", "Data Table": "Tabla de Datos",
"Chart": "Chart", "Chart": "Gráfico",
"No available explorer": "No available explorer", "No available explorer": "Exploración no disponible",
"New Explorer": "New Explorer", "New Explorer": "Nueva Exploración",
"Untitled Explorer": "Untitled Explorer", "Untitled Explorer": "Exploración Sin Título",
"Save Explorer": "Save Explorer", "Save Explorer": "Guardar Exploración",
"Save As New Explorer": "Save As New Explorer", "Save As New Explorer": "Guardar Como Nueva Exploración",
"Restore to Last Saved": "Restore to Last Saved", "Restore to Last Saved": "Restaurar al Último Guardado",
"Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "Are you sure you want to restore to last saved state? All unsaved changes will be lost.", "Are you sure you want to restore to last saved state? All unsaved changes will be lost.": "¿Seguro que quieres restaurar al último estado guardado? Se perderán todos los cambios no guardados.",
"Set Explorer Name": "Set Explorer Name", "Set Explorer Name": "Asignar Nombre a la Exploración",
"Rename Explorer": "Rename Explorer", "Rename Explorer": "Renombrar Exploración",
"Hide Explorer": "Hide Explorer", "Hide Explorer": "Ocultar Exploración",
"Unhide Explorer": "Unhide Explorer", "Unhide Explorer": "Mostrar Exploración",
"Delete Explorer": "Delete Explorer", "Delete Explorer": "Eliminar Exploración",
"Change Explorer Display Order": "Change Explorer Display Order", "Change Explorer Display Order": "Cambiar Orden de Visualización del Explorador",
"Explorer Name": "Explorer Name", "Explorer Name": "Nombre de la Exploración",
"Add Query": "Add Query", "Add Query": "Añadir Consulta",
"Remove Query": "Remove Query", "Remove Query": "Eliminar Consulta",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modificar el Nombre de la Consulta",
"Add Condition": "Add Condition", "Add Condition": "Añadir Condición",
"Remove Condition": "Remove Condition", "Add Sub Condition": "Add Sub Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "Remove Condition": "Eliminar Condición",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "No conditions defined. All transactions will match.": "Sin condiciones definidas. Todas las transacciones coincidirán.",
"Explorer list is up to date": "Explorer list is up to date", "Unable to retrieve explorer list": "No se puede recuperar la lista de exploraciones",
"Explorer list has been updated": "Explorer list has been updated", "Explorer list is up to date": "La lista de exploraciones está actualizada",
"Unable to retrieve explorer": "Unable to retrieve explorer", "Explorer list has been updated": "La lista de exploraciones se ha actualizado",
"Unable to add explorer": "Unable to add explorer", "Unable to retrieve explorer": "No se puede recuperar la exploración",
"Unable to save explorer": "Unable to save explorer", "Unable to add explorer": "No se puede añadir la exploración",
"Unable to move explorer": "Unable to move explorer", "Unable to save explorer": "No se puede guardar la exploración",
"Unable to hide this explorer": "Unable to hide this explorer", "Unable to move explorer": "No se puede mover la exploración",
"Unable to unhide this explorer": "Unable to unhide this explorer", "Unable to hide this explorer": "No se puede ocultar esta exploración",
"Are you sure you want to delete this explorer?": "Are you sure you want to delete this explorer?", "Unable to unhide this explorer": "No se puede mostrar esta exploración",
"Unable to delete this explorer": "Unable to delete this explorer", "Are you sure you want to delete this explorer?": "¿Seguro que quieres eliminar esta exploración?",
"Show Hidden Explorers": "Show Hidden Explorers", "Unable to delete this explorer": "No se puede eliminar esta exploración",
"Hide Hidden Explorers": "Hide Hidden Explorers", "Show Hidden Explorers": "Mostrar Exploraciones Ocultas",
"Hide Hidden Explorers": "Ocultar Exploraciones Ocultas",
"Editor": "Editor", "Editor": "Editor",
"Expression": "Expression", "Expression": "Expresn",
"Failed to generate expression": "Failed to generate expression", "Failed to generate expression": "Failed to generate expression",
"Data Source": "Data Source", "Data Source": "Fuente de Datos",
"All Queries": "All Queries", "All Queries": "Todas las Consultas",
"Axis / Category": "Axis / Category", "Axis / Category": "Ejes / Categoría",
"Series": "Series", "Series": "Series",
"Transaction Date": "Fecha de la transacción", "Transaction Date": "Fecha de la transacción",
"Transaction Year-Month": "Año-mes de la transacción", "Transaction Year-Month": "Año-mes de la transacción",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Día del mes de la transacción", "Transaction Day of Month": "Día del mes de la transacción",
"Transaction Month of Year": "Mes del año de la transacción", "Transaction Month of Year": "Mes del año de la transacción",
"Transaction Quarter of Year": "Trimestre de la transacción", "Transaction Quarter of Year": "Trimestre de la transacción",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Categoría de la cuenta de origen", "Source Account Category": "Categoría de la cuenta de origen",
"Source Account Currency": "Moneda de la cuenta de origen", "Source Account Currency": "Moneda de la cuenta de origen",
"Destination Account Category": "Categoría de la cuenta de destino", "Destination Account Category": "Categoría de la cuenta de destino",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Recuento de transacciones", "Transaction Count": "Recuento de transacciones",
"Average Amount": "Importe Medio", "Average Amount": "Importe Medio",
"Median Amount": "Importe Mediano", "Median Amount": "Importe Mediano",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "Lista de Cuentas", "Account List": "Lista de Cuentas",
"This Week": "Esta Semana", "This Week": "Esta Semana",
"This Month": "Este Mes", "This Month": "Este Mes",
@@ -1865,10 +1884,10 @@
"Are you sure you want to delete this account?": "¿Seguro que deseas eliminar esta cuenta?", "Are you sure you want to delete this account?": "¿Seguro que deseas eliminar esta cuenta?",
"Are you sure you want to delete this sub-account?": "¿Seguro que deseas eliminar esta subcuenta?", "Are you sure you want to delete this sub-account?": "¿Seguro que deseas eliminar esta subcuenta?",
"Unable to delete this account": "No se puede eliminar esta cuenta", "Unable to delete this account": "No se puede eliminar esta cuenta",
"Unable to delete this sub-account": "Unable to delete this sub-account", "Unable to delete this sub-account": "No se puede eliminar esta subcuenta",
"Move All Transactions": "Mover todas las transacciones", "Move All Transactions": "Mover todas las transacciones",
"Are you sure you want to move all transactions?": "¿Seguro que quieres mover todas las transacciones?", "Are you sure you want to move all transactions?": "¿Seguro que quieres mover todas las transacciones?",
"Unable to move transactions": "Unable to move transactions", "Unable to move transactions": "No se pueden mover las transacciones",
"All transactions in this account have been moved.": "Se han movido todas las transacciones de esta cuenta.", "All transactions in this account have been moved.": "Se han movido todas las transacciones de esta cuenta.",
"Reconciliation Statement": "Estado de Conciliación", "Reconciliation Statement": "Estado de Conciliación",
"Account Balance Trends": "Tendencias del Saldo de la Cuenta", "Account Balance Trends": "Tendencias del Saldo de la Cuenta",
@@ -1900,6 +1919,8 @@
"Swap Account": "Intercambiar Cuenta", "Swap Account": "Intercambiar Cuenta",
"Swap Amount": "Intercambiar Importe", "Swap Amount": "Intercambiar Importe",
"Swap Account and Amount": "Intercambiar Cuenta e Importe", "Swap Account and Amount": "Intercambiar Cuenta e Importe",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "Duplicar (Con Tiempo)", "Duplicate (With Time)": "Duplicar (Con Tiempo)",
"Duplicate (With Geographic Location)": "Duplicar (con Ubicación Geográfica)", "Duplicate (With Geographic Location)": "Duplicar (con Ubicación Geográfica)",
"Duplicate (With Time and Geographic Location)": "Duplicado (con Hora y Ubicación Geográfica)", "Duplicate (With Time and Geographic Location)": "Duplicado (con Hora y Ubicación Geográfica)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Otro Formato de Archivo de Aplicación Financiera", "Other Finance App File Format": "Otro Formato de Archivo de Aplicación Financiera",
"ezbookkeeping Data Export File": "Datos exportados de ezBookkeeping", "ezbookkeeping Data Export File": "Datos exportados de ezBookkeeping",
"Excel Workbook File": "Archivo Excel", "Excel Workbook File": "Archivo Excel",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Archivo OFX (Open Financial Exchange)", "Open Financial Exchange (OFX) File": "Archivo OFX (Open Financial Exchange)",
"Quicken Financial Exchange (QFX) File": "Archivo QFX (Quicken Financial Exchange)", "Quicken Financial Exchange (QFX) File": "Archivo QFX (Quicken Financial Exchange)",
"Quicken Interchange Format (QIF) File": "Archivo QIF (Quicken Interchange Format)", "Quicken Interchange Format (QIF) File": "Archivo QIF (Quicken Interchange Format)",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "Reemplazo por lotes de cuentas seleccionadas", "Batch Replace Selected Accounts": "Reemplazo por lotes de cuentas seleccionadas",
"Batch Replace Selected Destination Accounts": "Reemplazar por lotes cuentas de destino seleccionadas", "Batch Replace Selected Destination Accounts": "Reemplazar por lotes cuentas de destino seleccionadas",
"Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags", "Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Batch Add Transaction Tags", "Batch Add Transaction Tags": "Batch Add Transaction Tags",
"Replace Invalid Expense Categories": "Reemplazar categorías de gastos no válidas", "Replace Invalid Expense Categories": "Reemplazar categorías de gastos no válidas",
"Replace Invalid Income Categories": "Reemplazar categorías de ingresos no válidas", "Replace Invalid Income Categories": "Reemplazar categorías de ingresos no válidas",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "Etiqueta no Válida", "Invalid Tag": "Etiqueta no Válida",
"Target Tag": "Etiqueta de Destino", "Target Tag": "Etiqueta de Destino",
"Remove Tag": "Remove Tag", "Remove Tag": "Remove Tag",
"Target Timezone": "Target Timezone",
"(Empty)": "(Vacío)", "(Empty)": "(Vacío)",
"Source Value": "Source Value", "Source Value": "Source Value",
"Target Value": "Target Value", "Target Value": "Target Value",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Saldo Máximo", "Maximum Balance": "Saldo Máximo",
"Median Balance": "Saldo Mediano", "Median Balance": "Saldo Mediano",
"Average Balance": "Saldo Medio", "Average Balance": "Saldo Medio",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Salidas por Cuenta", "Outflows By Account": "Salidas por Cuenta",
"Expense By Account": "Gastos por Cuenta", "Expense By Account": "Gastos por Cuenta",
"Expense By Primary Category": "Gastos por Categoría Principal", "Expense By Primary Category": "Gastos por Categoría Principal",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "Importe Máximo", "Maximum Amount": "Importe Máximo",
"Display Order": "Orden de Visualización", "Display Order": "Orden de Visualización",
"Name": "Nombre", "Name": "Nombre",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "Ordenar por Importe",
"Sort by Display Order": "Ordenar por Orden de Visualización",
"Sort by Name": "Ordenar por Nombre",
"Time Granularity": "Granularidad Temporal", "Time Granularity": "Granularidad Temporal",
"Aggregate by Day": "Agregado por Día", "Aggregate by Day": "Agregado por Día",
"Aggregate by Month": "Agregado por Mes", "Aggregate by Month": "Agregado por Mes",
"Aggregate by Quarter": "Agregado por Trimestre", "Aggregate by Quarter": "Agregado por Trimestre",
"Aggregate by Year": "Agregado por Año", "Aggregate by Year": "Agregado por Año",
"Aggregate by Fiscal Year": "Agregado por Año Fiscal", "Aggregate by Fiscal Year": "Agregado por Año Fiscal",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "Filtrar cuentas", "Filter Accounts": "Filtrar cuentas",
"Filter Transaction Categories": "Filtrar categorías de transacciones", "Filter Transaction Categories": "Filtrar categorías de transacciones",
"Filter Transaction Tags": "Filtrar etiquetas de transacciones", "Filter Transaction Tags": "Filtrar etiquetas de transacciones",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "Mostrar Importe Total Mensual", "Show Monthly Total Amount": "Mostrar Importe Total Mensual",
"Show Transaction Tags": "Mostrar Etiqueta de Transacción", "Show Transaction Tags": "Mostrar Etiqueta de Transacción",
"Transaction Edit Page": "Página de Edición de Transacciones", "Transaction Edit Page": "Página de Edición de Transacciones",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "Guardar Borrador Automáticamente", "Automatically Save Draft": "Guardar Borrador Automáticamente",
"Always Show Confirmation": "Mostrar Confirmación Cada Vez", "Always Show Confirmation": "Mostrar Confirmación Cada Vez",
"Automatically Add Geolocation": "Agregar Geolocalización Automáticamente", "Automatically Add Geolocation": "Agregar Geolocalización Automáticamente",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Exportar a archivo CSV (Valores Separados por Comas)", "Export to CSV (Comma-separated values) File": "Exportar a archivo CSV (Valores Separados por Comas)",
"Export to TSV (Tab-separated values) File": "Exportar a archivo TSV (Valores Separados por Tabulaciones)", "Export to TSV (Tab-separated values) File": "Exportar a archivo TSV (Valores Separados por Tabulaciones)",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Archivo Markdown", "Markdown File": "Archivo Markdown",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "Borrar Datos de Usuario", "Clear User Data": "Borrar Datos de Usuario",
"Clear All Transactions": "Eliminar Todas las Transacciones", "Clear All Transactions": "Eliminar Todas las Transacciones",
"Clear All Data": "Eliminar Todos los Datos", "Clear All Data": "Eliminar Todos los Datos",
@@ -2353,7 +2389,7 @@
"No secondary expense categories are available": "No hay categorías secundarias de gasto disponibles", "No secondary expense categories are available": "No hay categorías secundarias de gasto disponibles",
"No secondary income categories are available": "No hay categorías secundarias de ingreso disponibles", "No secondary income categories are available": "No hay categorías secundarias de ingreso disponibles",
"No secondary transfer categories are available": "No hay categorías secundarias de transferencia disponibles", "No secondary transfer categories are available": "No hay categorías secundarias de transferencia disponibles",
"Find category": "Find category", "Find category": "Buscar categoría",
"Add Default Categories": "Añadir Categorías Predefinidas", "Add Default Categories": "Añadir Categorías Predefinidas",
"Default Categories": "Categorías Predefinidas", "Default Categories": "Categorías Predefinidas",
"Unable to retrieve category list": "No se puede recuperar la lista de categorías", "Unable to retrieve category list": "No se puede recuperar la lista de categorías",
@@ -2385,29 +2421,29 @@
"Show Hidden Transaction Categories": "Mostrar categorías de transacciones ocultas", "Show Hidden Transaction Categories": "Mostrar categorías de transacciones ocultas",
"Hide Hidden Transaction Categories": "Ocultar categorías de transacciones ocultas", "Hide Hidden Transaction Categories": "Ocultar categorías de transacciones ocultas",
"Transaction Tags": "Etiquetas de Transacciones", "Transaction Tags": "Etiquetas de Transacciones",
"Total tags": "Total tags", "Total tags": "Etiquetas totales",
"Default Group": "Default Group", "Default Group": "Grupo por Defecto",
"Tag Title": "Título de la Etiqueta", "Tag Title": "Título de la Etiqueta",
"No available tag": "No hay etiquetas disponibles", "No available tag": "No hay etiquetas disponibles",
"Find tag": "Find tag", "Find tag": "Buscar etiqueta",
"Unable to retrieve tag list": "No se puede recuperar la lista de etiquetas", "Unable to retrieve tag list": "No se puede recuperar la lista de etiquetas",
"Tag list is up to date": "La lista de etiquetas está actualizada.", "Tag list is up to date": "La lista de etiquetas está actualizada.",
"Tag list has been updated": "La lista de etiquetas ha sido actualizada.", "Tag list has been updated": "La lista de etiquetas ha sido actualizada.",
"Add Tag Group": "Add Tag Group", "Add Tag Group": "Añadir Grupo de Etiquetas",
"Rename Tag Group": "Rename Tag Group", "Rename Tag Group": "Renombrar Grupo de Etiquetas",
"Delete Tag Group": "Delete Tag Group", "Delete Tag Group": "Borrar Grupo de Etiquetas",
"Change Group Display Order": "Change Group Display Order", "Change Group Display Order": "Cambiar el Orden de Visualización del Grupo",
"Tag Group Name": "Tag Group Name", "Tag Group Name": "Nombre del Grupo de Etiquetas",
"New Tag Group Name": "New Tag Group Name", "New Tag Group Name": "Nombre del Nuevo Grupo de Etiquetas",
"No available tag group": "No available tag group", "No available tag group": "Grupo de etiquetas no disponible",
"Unable to retrieve tag group list": "Unable to retrieve tag group list", "Unable to retrieve tag group list": "No se puede recuperar la lista del grupo de etiquetas",
"Tag group list has been updated": "Tag group list has been updated", "Tag group list has been updated": "El grupo de etiquetas ha sido actualizado",
"Unable to add tag group": "Unable to add tag group", "Unable to add tag group": "No se puede añadir el grupo de etiquetas",
"Unable to save tag group": "Unable to save tag group", "Unable to save tag group": "No se puede guardar el grupo de etiquetas",
"Unable to move tag group": "Unable to move tag group", "Unable to move tag group": "No se puede mover el grupo de etiquetas",
"Unable to rename this tag group": "Unable to rename this tag group", "Unable to rename this tag group": "No se puede renombrar el grupo de etiquetas",
"Are you sure you want to delete this tag group?": "Are you sure you want to delete this tag group?", "Are you sure you want to delete this tag group?": "¿Seguro que quieres eliminar este grupo de etiquetas?",
"Unable to delete this tag group": "Unable to delete this tag group", "Unable to delete this tag group": "No se puede eliminar este grupo de etiquetas",
"Add new tag": "Agregar nueva etiqueta", "Add new tag": "Agregar nueva etiqueta",
"Unable to add tag": "No se puede agregar la etiqueta", "Unable to add tag": "No se puede agregar la etiqueta",
"Unable to save tag": "No se puede guardar la etiqueta", "Unable to save tag": "No se puede guardar la etiqueta",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings", "Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings",
"Unable to update user synchronized application settings": "Unable to update user synchronized application settings", "Unable to update user synchronized application settings": "Unable to update user synchronized application settings",
"Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings", "Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "¿Seguro que deseas volver a iniciar sesión?", "Are you sure you want to re-login?": "¿Seguro que deseas volver a iniciar sesión?",
"Exchange Rates Data": "Tipos de Cambio", "Exchange Rates Data": "Tipos de Cambio",
"User Custom": "User Custom", "User Custom": "User Custom",
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 avec BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM États-Unis (CP-437)", "cp437": "OEM États-Unis (CP-437)",
"cp863": "OEM Canadien Français (CP-863)", "cp863": "OEM Canadien Français (CP-863)",
"cp037": "IBM EBCDIC États-Unis/Canada (CP-037)", "cp037": "IBM EBCDIC États-Unis/Canada (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optionnel] Nom de catégorie", "fieldCategoryNameDescription": "[optionnel] Nom de catégorie",
"fieldSourceAccountNameDescription": "[optionnel] Nom du compte source", "fieldSourceAccountNameDescription": "[optionnel] Nom du compte source",
"fieldDestinationAccountNameDescription": "[optionnel] Nom du compte de destination (uniquement pour le type de virement)", "fieldDestinationAccountNameDescription": "[optionnel] Nom du compte de destination (uniquement pour le type de virement)",
"fieldSourceAmountDescription": "[requis] Montant source", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optionnel] Montant de destination (uniquement pour le type de virement)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optionnel] Géolocalisation, format : 'longitude latitude', par ex. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optionnel] Géolocalisation, format : 'longitude latitude', par ex. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optionnel] Noms d'étiquettes séparés par des virgules, par ex. 'étiquette1;étiquette2;étiquette3'", "fieldTagNamesDescription": "[optionnel] Noms d'étiquettes séparés par des virgules, par ex. 'étiquette1;étiquette2;étiquette3'",
"fieldCommentDescription": "[optionnel] Description" "fieldCommentDescription": "[optionnel] Description"
@@ -1496,7 +1495,10 @@
"Remove": "Supprimer", "Remove": "Supprimer",
"Delete": "Supprimer", "Delete": "Supprimer",
"Duplicate": "Dupliquer", "Duplicate": "Dupliquer",
"Open Menu": "Open Menu",
"Sort": "Trier", "Sort": "Trier",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "Date", "Date": "Date",
"Time": "Heure", "Time": "Heure",
"Color": "Couleur", "Color": "Couleur",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "Aujourd'hui", "Today": "Aujourd'hui",
"Yesterday": "Hier", "Yesterday": "Hier",
"Recent 7 days": "7 derniers jours", "Recent 7 days": "7 derniers jours",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "Graphique en secteurs", "Pie Chart": "Graphique en secteurs",
"Bar Chart": "Graphique en barres", "Bar Chart": "Graphique en barres",
"Radar Chart": "Radar Chart", "Radar Chart": "Radar Chart",
"Area Chart": "Graphique en aires", "Area Chart": "Graphique en aires",
"Column Chart": "Graphique en colonnes", "Column Chart": "Graphique en colonnes",
"Bubble Chart": "Bubble Chart", "Bubble Chart": "Bubble Chart",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Graphique en chandelier", "Candlestick Chart": "Graphique en chandelier",
"Sankey Chart": "Sankey Chart", "Sankey Chart": "Sankey Chart",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "Liste des comptes", "Account List": "Liste des comptes",
"This Week": "Cette semaine", "This Week": "Cette semaine",
"This Month": "Ce mois", "This Month": "Ce mois",
@@ -1900,6 +1919,8 @@
"Swap Account": "Échanger le compte", "Swap Account": "Échanger le compte",
"Swap Amount": "Échanger le montant", "Swap Amount": "Échanger le montant",
"Swap Account and Amount": "Échanger le compte et le montant", "Swap Account and Amount": "Échanger le compte et le montant",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "Dupliquer (avec heure)", "Duplicate (With Time)": "Dupliquer (avec heure)",
"Duplicate (With Geographic Location)": "Dupliquer (avec localisation géographique)", "Duplicate (With Geographic Location)": "Dupliquer (avec localisation géographique)",
"Duplicate (With Time and Geographic Location)": "Dupliquer (avec heure et localisation géographique)", "Duplicate (With Time and Geographic Location)": "Dupliquer (avec heure et localisation géographique)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Format de fichier d'autre app financière", "Other Finance App File Format": "Format de fichier d'autre app financière",
"ezbookkeeping Data Export File": "Fichier d'exportation de données ezbookkeeping", "ezbookkeeping Data Export File": "Fichier d'exportation de données ezbookkeeping",
"Excel Workbook File": "Fichier de classeur Excel", "Excel Workbook File": "Fichier de classeur Excel",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Fichier Open Financial Exchange (OFX)", "Open Financial Exchange (OFX) File": "Fichier Open Financial Exchange (OFX)",
"Quicken Financial Exchange (QFX) File": "Fichier Quicken Financial Exchange (QFX)", "Quicken Financial Exchange (QFX) File": "Fichier Quicken Financial Exchange (QFX)",
"Quicken Interchange Format (QIF) File": "Fichier Quicken Interchange Format (QIF)", "Quicken Interchange Format (QIF) File": "Fichier Quicken Interchange Format (QIF)",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "Remplacer en lot les comptes sélectionnés", "Batch Replace Selected Accounts": "Remplacer en lot les comptes sélectionnés",
"Batch Replace Selected Destination Accounts": "Remplacer en lot les comptes de destination sélectionnés", "Batch Replace Selected Destination Accounts": "Remplacer en lot les comptes de destination sélectionnés",
"Batch Replace Selected Transaction Tags": "Remplacer en lot les étiquettes de transaction sélectionnées", "Batch Replace Selected Transaction Tags": "Remplacer en lot les étiquettes de transaction sélectionnées",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Ajouter en lot des étiquettes de transaction", "Batch Add Transaction Tags": "Ajouter en lot des étiquettes de transaction",
"Replace Invalid Expense Categories": "Remplacer les catégories de dépenses invalides", "Replace Invalid Expense Categories": "Remplacer les catégories de dépenses invalides",
"Replace Invalid Income Categories": "Remplacer les catégories de revenus invalides", "Replace Invalid Income Categories": "Remplacer les catégories de revenus invalides",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "Étiquette invalide", "Invalid Tag": "Étiquette invalide",
"Target Tag": "Étiquette cible", "Target Tag": "Étiquette cible",
"Remove Tag": "Supprimer l'étiquette", "Remove Tag": "Supprimer l'étiquette",
"Target Timezone": "Target Timezone",
"(Empty)": "(Vide)", "(Empty)": "(Vide)",
"Source Value": "Valeur source", "Source Value": "Valeur source",
"Target Value": "Valeur cible", "Target Value": "Valeur cible",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Solde maximum", "Maximum Balance": "Solde maximum",
"Median Balance": "Solde médian", "Median Balance": "Solde médian",
"Average Balance": "Solde moyen", "Average Balance": "Solde moyen",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "Dépenses par compte", "Expense By Account": "Dépenses par compte",
"Expense By Primary Category": "Dépenses par catégorie principale", "Expense By Primary Category": "Dépenses par catégorie principale",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "Montant maximum", "Maximum Amount": "Montant maximum",
"Display Order": "Ordre d'affichage", "Display Order": "Ordre d'affichage",
"Name": "Nom", "Name": "Nom",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "Trier par montant",
"Sort by Display Order": "Trier par ordre d'affichage",
"Sort by Name": "Trier par nom",
"Time Granularity": "Granularité temporelle", "Time Granularity": "Granularité temporelle",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "Agréger par mois", "Aggregate by Month": "Agréger par mois",
"Aggregate by Quarter": "Agréger par trimestre", "Aggregate by Quarter": "Agréger par trimestre",
"Aggregate by Year": "Agréger par année", "Aggregate by Year": "Agréger par année",
"Aggregate by Fiscal Year": "Agréger par année fiscale", "Aggregate by Fiscal Year": "Agréger par année fiscale",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "Filtrer les comptes", "Filter Accounts": "Filtrer les comptes",
"Filter Transaction Categories": "Filtrer les catégories de transaction", "Filter Transaction Categories": "Filtrer les catégories de transaction",
"Filter Transaction Tags": "Filtrer les étiquettes de transaction", "Filter Transaction Tags": "Filtrer les étiquettes de transaction",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "Afficher le montant total mensuel", "Show Monthly Total Amount": "Afficher le montant total mensuel",
"Show Transaction Tags": "Afficher l'étiquette de transaction", "Show Transaction Tags": "Afficher l'étiquette de transaction",
"Transaction Edit Page": "Page de modification de transaction", "Transaction Edit Page": "Page de modification de transaction",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "Enregistrer automatiquement le brouillon", "Automatically Save Draft": "Enregistrer automatiquement le brouillon",
"Always Show Confirmation": "Afficher la confirmation à chaque fois", "Always Show Confirmation": "Afficher la confirmation à chaque fois",
"Automatically Add Geolocation": "Ajouter automatiquement la géolocalisation", "Automatically Add Geolocation": "Ajouter automatiquement la géolocalisation",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Exporter vers un fichier CSV (valeurs séparées par virgules)", "Export to CSV (Comma-separated values) File": "Exporter vers un fichier CSV (valeurs séparées par virgules)",
"Export to TSV (Tab-separated values) File": "Exporter vers un fichier TSV (valeurs séparées par tabulations)", "Export to TSV (Tab-separated values) File": "Exporter vers un fichier TSV (valeurs séparées par tabulations)",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Fichier Markdown", "Markdown File": "Fichier Markdown",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "Effacer les données utilisateur", "Clear User Data": "Effacer les données utilisateur",
"Clear All Transactions": "Effacer toutes les transactions", "Clear All Transactions": "Effacer toutes les transactions",
"Clear All Data": "Effacer toutes les données", "Clear All Data": "Effacer toutes les données",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Impossible de récupérer les paramètres d'application synchronisés de l'utilisateur", "Unable to retrieve user synchronized application settings": "Impossible de récupérer les paramètres d'application synchronisés de l'utilisateur",
"Unable to update user synchronized application settings": "Impossible de mettre à jour les paramètres d'application synchronisés de l'utilisateur", "Unable to update user synchronized application settings": "Impossible de mettre à jour les paramètres d'application synchronisés de l'utilisateur",
"Unable to disable user synchronized application settings": "Impossible de désactiver les paramètres d'application synchronisés de l'utilisateur", "Unable to disable user synchronized application settings": "Impossible de désactiver les paramètres d'application synchronisés de l'utilisateur",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "Êtes-vous sûr de vouloir vous reconnecter ?", "Are you sure you want to re-login?": "Êtes-vous sûr de vouloir vous reconnecter ?",
"Exchange Rates Data": "Données de taux de change", "Exchange Rates Data": "Données de taux de change",
"User Custom": "Personnalisé utilisateur", "User Custom": "Personnalisé utilisateur",
+47 -5
View File
@@ -1,7 +1,13 @@
import { useI18n as useVueI18n } from 'vue-i18n'; import { useI18n as useVueI18n } from 'vue-i18n';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import type { NameValue, TypeAndName, TypeAndDisplayName, LocalizedSwitchOption } from '@/core/base.ts'; import {
type NameValue,
type TypeAndName,
type TypeAndNameWithAlternativeName,
type TypeAndDisplayName,
type LocalizedSwitchOption
} from '@/core/base.ts';
import { import {
type LanguageInfo, type LanguageInfo,
@@ -122,7 +128,9 @@ import {
} from '@/core/category.ts'; } from '@/core/category.ts';
import { import {
TransactionEditScopeType TransactionEditScopeType,
TransactionQuickSaveButtonStyle,
TransactionQuickAddButtonActionType
} from '@/core/transaction.ts'; } from '@/core/transaction.ts';
import { import {
@@ -550,13 +558,19 @@ export function useI18n() {
return ret; return ret;
} }
function getLocalizedDisplayNameAndType(typeAndNames: TypeAndName[]): TypeAndDisplayName[] { function getLocalizedDisplayNameAndType(typeAndNames: TypeAndName[] | TypeAndNameWithAlternativeName[], useAlternativeName?: boolean): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = []; const ret: TypeAndDisplayName[] = [];
for (const typeAndName of typeAndNames) { for (const typeAndName of typeAndNames) {
let name: string = typeAndName.name;
if (useAlternativeName && 'alternativeName' in typeAndName && typeAndName.alternativeName) {
name = typeAndName.alternativeName;
}
ret.push({ ret.push({
type: typeAndName.type, type: typeAndName.type,
displayName: t(typeAndName.name) displayName: t(name)
}); });
} }
@@ -2088,6 +2102,31 @@ export function useI18n() {
return formatPercent(value, precision, lowPrecisionValue, numberFormatOptions); return formatPercent(value, precision, lowPrecisionValue, numberFormatOptions);
} }
function getFormattedVolume(value: number, precision?: number, unit?: 'KiB' | 'MiB'): string {
const numberFormatOptions = getNumberFormatOptions({});
let displayUnit = unit || 'B';
if (unit === 'KiB') {
value = value / 1024.0;
} else if (unit === 'MiB') {
value = value / 1024.0 / 1024.0;
} else {
displayUnit = 'B';
if (value >= 1024.0) {
value = value / 1024.0;
displayUnit = 'KiB';
}
if (value >= 1024.0) {
value = value / 1024.0;
displayUnit = 'MiB';
}
}
return formatNumber(value, numberFormatOptions, precision) + ' ' + displayUnit;
}
function getFormattedExchangeRateAmount(value: number, numeralSystem?: NumeralSystem): string { function getFormattedExchangeRateAmount(value: number, numeralSystem?: NumeralSystem): string {
const numberFormatOptions = getNumberFormatOptions({ numeralSystem }); const numberFormatOptions = getNumberFormatOptions({ numeralSystem });
return formatExchangeRateAmount(value, numberFormatOptions); return formatExchangeRateAmount(value, numberFormatOptions);
@@ -2392,10 +2431,12 @@ export function useI18n() {
getAllTrendChartTypes: () => getLocalizedDisplayNameAndType(TrendChartType.values()), getAllTrendChartTypes: () => getLocalizedDisplayNameAndType(TrendChartType.values()),
getAllAccountBalanceTrendChartTypes: () => getLocalizedDisplayNameAndType(AccountBalanceTrendChartType.values()), getAllAccountBalanceTrendChartTypes: () => getLocalizedDisplayNameAndType(AccountBalanceTrendChartType.values()),
getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType, withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType, withDesktopOnlyChart)), getAllStatisticsChartDataTypes: (analysisType: StatisticsAnalysisType, withDesktopOnlyChart?: boolean) => getLocalizedDisplayNameAndType(ChartDataType.values(analysisType, withDesktopOnlyChart)),
getAllStatisticsSortingTypes: () => getLocalizedDisplayNameAndType(ChartSortingType.values()), getAllStatisticsSortingTypes: (useAlternativeName?: boolean) => getLocalizedDisplayNameAndType(ChartSortingType.values(), useAlternativeName),
getAllStatisticsDateAggregationTypes: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, true), getAllStatisticsDateAggregationTypes: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, true),
getAllStatisticsDateAggregationTypesWithShortName: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, false), getAllStatisticsDateAggregationTypesWithShortName: (analysisType: StatisticsAnalysisType) => getLocalizedChartDateAggregationTypeAndDisplayName(analysisType, false),
getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()), getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()),
getAllTransactionQuickSaveButtonStyles: () => getLocalizedDisplayNameAndType(TransactionQuickSaveButtonStyle.values()),
getAllTransactionQuickAddButtonActionTypes: () => getLocalizedDisplayNameAndType(TransactionQuickAddButtonActionType.values()),
getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()), getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()),
getAllImportTransactionColumnTypes: () => getLocalizedDisplayNameAndType(ImportTransactionColumnType.values()), getAllImportTransactionColumnTypes: () => getLocalizedDisplayNameAndType(ImportTransactionColumnType.values()),
getAllTransactionDefaultCategories, getAllTransactionDefaultCategories,
@@ -2493,6 +2534,7 @@ export function useI18n() {
formatNumberToWesternArabicNumerals: (value: number, precision?: number) => getFormattedNumber(value, NumeralSystem.WesternArabicNumerals, precision), formatNumberToWesternArabicNumerals: (value: number, precision?: number) => getFormattedNumber(value, NumeralSystem.WesternArabicNumerals, precision),
formatPercentToLocalizedNumerals: (value: number, precision: number, lowPrecisionValue: string) => getFormattedPercentValue(value, precision, lowPrecisionValue), formatPercentToLocalizedNumerals: (value: number, precision: number, lowPrecisionValue: string) => getFormattedPercentValue(value, precision, lowPrecisionValue),
formatPercentToWesternArabicNumerals: (value: number, precision: number, lowPrecisionValue: string) => getFormattedPercentValue(value, precision, lowPrecisionValue, NumeralSystem.WesternArabicNumerals), formatPercentToWesternArabicNumerals: (value: number, precision: number, lowPrecisionValue: string) => getFormattedPercentValue(value, precision, lowPrecisionValue, NumeralSystem.WesternArabicNumerals),
formatVolumeToLocalizedNumerals: getFormattedVolume,
formatExchangeRateAmountToWesternArabicNumerals: (value: number) => getFormattedExchangeRateAmount(value, NumeralSystem.WesternArabicNumerals), formatExchangeRateAmountToWesternArabicNumerals: (value: number) => getFormattedExchangeRateAmount(value, NumeralSystem.WesternArabicNumerals),
appendDigitGroupingSymbolAndDecimalSeparator: (value: string) => appendDigitGroupingSymbolAndDecimalSeparator(value, getNumberFormatOptions({})), appendDigitGroupingSymbolAndDecimalSeparator: (value: string) => appendDigitGroupingSymbolAndDecimalSeparator(value, getNumberFormatOptions({})),
getAdaptiveAmountRate, getAdaptiveAmountRate,
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM United States (CP-437)", "cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)", "cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)", "cp037": "IBM EBCDIC US/Canada (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optional] Category name", "fieldCategoryNameDescription": "[optional] Category name",
"fieldSourceAccountNameDescription": "[optional] Source account name", "fieldSourceAccountNameDescription": "[optional] Source account name",
"fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)", "fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)",
"fieldSourceAmountDescription": "[required] Source amount", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'",
"fieldCommentDescription": "[optional] Description" "fieldCommentDescription": "[optional] Description"
@@ -1496,7 +1495,10 @@
"Remove": "Rimuovi", "Remove": "Rimuovi",
"Delete": "Elimina", "Delete": "Elimina",
"Duplicate": "Duplica", "Duplicate": "Duplica",
"Open Menu": "Open Menu",
"Sort": "Ordina", "Sort": "Ordina",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "Data", "Date": "Data",
"Time": "Ora", "Time": "Ora",
"Color": "Colore", "Color": "Colore",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "Oggi", "Today": "Oggi",
"Yesterday": "Ieri", "Yesterday": "Ieri",
"Recent 7 days": "Ultimi 7 giorni", "Recent 7 days": "Ultimi 7 giorni",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "Grafico a torta", "Pie Chart": "Grafico a torta",
"Bar Chart": "Grafico a barre", "Bar Chart": "Grafico a barre",
"Radar Chart": "Radar Chart", "Radar Chart": "Radar Chart",
"Area Chart": "Grafico ad area", "Area Chart": "Grafico ad area",
"Column Chart": "Grafico a colonne", "Column Chart": "Grafico a colonne",
"Bubble Chart": "Bubble Chart", "Bubble Chart": "Bubble Chart",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Candlestick Chart", "Candlestick Chart": "Candlestick Chart",
"Sankey Chart": "Sankey Chart", "Sankey Chart": "Sankey Chart",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "Elenco account", "Account List": "Elenco account",
"This Week": "Questa settimana", "This Week": "Questa settimana",
"This Month": "Questo mese", "This Month": "Questo mese",
@@ -1900,6 +1919,8 @@
"Swap Account": "Scambia account", "Swap Account": "Scambia account",
"Swap Amount": "Scambia importo", "Swap Amount": "Scambia importo",
"Swap Account and Amount": "Scambia account e importo", "Swap Account and Amount": "Scambia account e importo",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "Duplica (con ora)", "Duplicate (With Time)": "Duplica (con ora)",
"Duplicate (With Geographic Location)": "Duplica (con posizione geografica)", "Duplicate (With Geographic Location)": "Duplica (con posizione geografica)",
"Duplicate (With Time and Geographic Location)": "Duplica (con ora e posizione geografica)", "Duplicate (With Time and Geographic Location)": "Duplica (con ora e posizione geografica)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Other Finance App File Format", "Other Finance App File Format": "Other Finance App File Format",
"ezbookkeeping Data Export File": "File esportazione dati ezBookkeeping", "ezbookkeeping Data Export File": "File esportazione dati ezBookkeeping",
"Excel Workbook File": "Excel Workbook File", "Excel Workbook File": "Excel Workbook File",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "File Open Financial Exchange (OFX)", "Open Financial Exchange (OFX) File": "File Open Financial Exchange (OFX)",
"Quicken Financial Exchange (QFX) File": "File Quicken Financial Exchange (QFX)", "Quicken Financial Exchange (QFX) File": "File Quicken Financial Exchange (QFX)",
"Quicken Interchange Format (QIF) File": "File Quicken Interchange Format (QIF)", "Quicken Interchange Format (QIF) File": "File Quicken Interchange Format (QIF)",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "Sostituisci in blocco conti selezionati", "Batch Replace Selected Accounts": "Sostituisci in blocco conti selezionati",
"Batch Replace Selected Destination Accounts": "Sostituisci in blocco conti di destinazione selezionati", "Batch Replace Selected Destination Accounts": "Sostituisci in blocco conti di destinazione selezionati",
"Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags", "Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Batch Add Transaction Tags", "Batch Add Transaction Tags": "Batch Add Transaction Tags",
"Replace Invalid Expense Categories": "Sostituisci categorie di spesa non valide", "Replace Invalid Expense Categories": "Sostituisci categorie di spesa non valide",
"Replace Invalid Income Categories": "Sostituisci categorie di entrata non valide", "Replace Invalid Income Categories": "Sostituisci categorie di entrata non valide",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "Tag non valido", "Invalid Tag": "Tag non valido",
"Target Tag": "Tag di destinazione", "Target Tag": "Tag di destinazione",
"Remove Tag": "Remove Tag", "Remove Tag": "Remove Tag",
"Target Timezone": "Target Timezone",
"(Empty)": "(Vuoto)", "(Empty)": "(Vuoto)",
"Source Value": "Source Value", "Source Value": "Source Value",
"Target Value": "Target Value", "Target Value": "Target Value",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Maximum Balance", "Maximum Balance": "Maximum Balance",
"Median Balance": "Median Balance", "Median Balance": "Median Balance",
"Average Balance": "Average Balance", "Average Balance": "Average Balance",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "Spesa per conto", "Expense By Account": "Spesa per conto",
"Expense By Primary Category": "Spesa per categoria principale", "Expense By Primary Category": "Spesa per categoria principale",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "Importo massimo", "Maximum Amount": "Importo massimo",
"Display Order": "Ordine di visualizzazione", "Display Order": "Ordine di visualizzazione",
"Name": "Nome", "Name": "Nome",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "Ordina per importo",
"Sort by Display Order": "Ordina per ordine di visualizzazione",
"Sort by Name": "Ordina per nome",
"Time Granularity": "Time Granularity", "Time Granularity": "Time Granularity",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "Aggrega per mese", "Aggregate by Month": "Aggrega per mese",
"Aggregate by Quarter": "Aggrega per trimestre", "Aggregate by Quarter": "Aggrega per trimestre",
"Aggregate by Year": "Aggrega per anno", "Aggregate by Year": "Aggrega per anno",
"Aggregate by Fiscal Year": "Aggregate by Fiscal Year", "Aggregate by Fiscal Year": "Aggregate by Fiscal Year",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "Filtra conti", "Filter Accounts": "Filtra conti",
"Filter Transaction Categories": "Filtra categorie transazione", "Filter Transaction Categories": "Filtra categorie transazione",
"Filter Transaction Tags": "Filtra tag transazione", "Filter Transaction Tags": "Filtra tag transazione",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "Mostra importo totale mensile", "Show Monthly Total Amount": "Mostra importo totale mensile",
"Show Transaction Tags": "Mostra tag transazione", "Show Transaction Tags": "Mostra tag transazione",
"Transaction Edit Page": "Pagina modifica transazione", "Transaction Edit Page": "Pagina modifica transazione",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "Salva automaticamente bozza", "Automatically Save Draft": "Salva automaticamente bozza",
"Always Show Confirmation": "Mostra conferma ogni volta", "Always Show Confirmation": "Mostra conferma ogni volta",
"Automatically Add Geolocation": "Aggiungi automaticamente geolocalizzazione", "Automatically Add Geolocation": "Aggiungi automaticamente geolocalizzazione",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File", "Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File",
"Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File", "Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown File", "Markdown File": "Markdown File",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "Cancella dati utente", "Clear User Data": "Cancella dati utente",
"Clear All Transactions": "Clear All Transactions", "Clear All Transactions": "Clear All Transactions",
"Clear All Data": "Clear All Data", "Clear All Data": "Clear All Data",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings", "Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings",
"Unable to update user synchronized application settings": "Unable to update user synchronized application settings", "Unable to update user synchronized application settings": "Unable to update user synchronized application settings",
"Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings", "Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?", "Are you sure you want to re-login?": "Sei sicuro di voler accedere di nuovo?",
"Exchange Rates Data": "Dati tassi di cambio", "Exchange Rates Data": "Dati tassi di cambio",
"User Custom": "User Custom", "User Custom": "User Custom",
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian", "utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian", "utf-16be": "UTF-16 Big Endian",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 Little Endian",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 Big Endian",
"cp437": "OEM 米国 (CP-437)", "cp437": "OEM 米国 (CP-437)",
"cp863": "OEM カナダ系フランス語 (CP-863)", "cp863": "OEM カナダ系フランス語 (CP-863)",
"cp037": "IBM EBCDIC 米国/カナダ (CP-037)", "cp037": "IBM EBCDIC 米国/カナダ (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[optional] Category name", "fieldCategoryNameDescription": "[optional] Category name",
"fieldSourceAccountNameDescription": "[optional] Source account name", "fieldSourceAccountNameDescription": "[optional] Source account name",
"fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)", "fieldDestinationAccountNameDescription": "[optional] Destination account name (for transfer type only)",
"fieldSourceAmountDescription": "[required] Source amount", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[optional] Geolocation, format: 'longitude latitude', e.g. '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[optional] Comma separated tag names, e.g. 'tag1;tag2;tag3'",
"fieldCommentDescription": "[optional] Description" "fieldCommentDescription": "[optional] Description"
@@ -1496,7 +1495,10 @@
"Remove": "削除", "Remove": "削除",
"Delete": "削除", "Delete": "削除",
"Duplicate": "複製", "Duplicate": "複製",
"Open Menu": "Open Menu",
"Sort": "並べ替え", "Sort": "並べ替え",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "日付", "Date": "日付",
"Time": "時間", "Time": "時間",
"Color": "色", "Color": "色",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "今日", "Today": "今日",
"Yesterday": "昨日", "Yesterday": "昨日",
"Recent 7 days": "直近7日間", "Recent 7 days": "直近7日間",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "円グラフ", "Pie Chart": "円グラフ",
"Bar Chart": "棒グラフ", "Bar Chart": "棒グラフ",
"Radar Chart": "Radar Chart", "Radar Chart": "Radar Chart",
"Area Chart": "エリアチャート", "Area Chart": "エリアチャート",
"Column Chart": "列チャート", "Column Chart": "列チャート",
"Bubble Chart": "Bubble Chart", "Bubble Chart": "Bubble Chart",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "Candlestick Chart", "Candlestick Chart": "Candlestick Chart",
"Sankey Chart": "Sankey Chart", "Sankey Chart": "Sankey Chart",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "口座リスト", "Account List": "口座リスト",
"This Week": "今週", "This Week": "今週",
"This Month": "今月", "This Month": "今月",
@@ -1900,6 +1919,8 @@
"Swap Account": "口座のスワップ", "Swap Account": "口座のスワップ",
"Swap Amount": "金額のスワップ", "Swap Amount": "金額のスワップ",
"Swap Account and Amount": "口座と金額をスワップ", "Swap Account and Amount": "口座と金額をスワップ",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "複製(時間含む)", "Duplicate (With Time)": "複製(時間含む)",
"Duplicate (With Geographic Location)": "複製(地理座標を含む)", "Duplicate (With Geographic Location)": "複製(地理座標を含む)",
"Duplicate (With Time and Geographic Location)": "複製(時間と地理座標を含む)", "Duplicate (With Time and Geographic Location)": "複製(時間と地理座標を含む)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "Other Finance App File Format", "Other Finance App File Format": "Other Finance App File Format",
"ezbookkeeping Data Export File": "ezbookkeepingデータエクスポートファイル", "ezbookkeeping Data Export File": "ezbookkeepingデータエクスポートファイル",
"Excel Workbook File": "Excel Workbook File", "Excel Workbook File": "Excel Workbook File",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) ファイル", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) ファイル",
"Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) ファイル", "Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) ファイル",
"Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) ファイル", "Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) ファイル",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "バッチ選択した口座を置き換えます", "Batch Replace Selected Accounts": "バッチ選択した口座を置き換えます",
"Batch Replace Selected Destination Accounts": "バッチは選択した宛先口座を置き換えます", "Batch Replace Selected Destination Accounts": "バッチは選択した宛先口座を置き換えます",
"Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags", "Batch Replace Selected Transaction Tags": "Batch Replace Selected Transaction Tags",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "Batch Add Transaction Tags", "Batch Add Transaction Tags": "Batch Add Transaction Tags",
"Replace Invalid Expense Categories": "無効な支出カテゴリを置き換えます", "Replace Invalid Expense Categories": "無効な支出カテゴリを置き換えます",
"Replace Invalid Income Categories": "無効な収入カテゴリを置き換えます", "Replace Invalid Income Categories": "無効な収入カテゴリを置き換えます",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "無効なタグ", "Invalid Tag": "無効なタグ",
"Target Tag": "対象タグ", "Target Tag": "対象タグ",
"Remove Tag": "Remove Tag", "Remove Tag": "Remove Tag",
"Target Timezone": "Target Timezone",
"(Empty)": "(空)", "(Empty)": "(空)",
"Source Value": "Source Value", "Source Value": "Source Value",
"Target Value": "Target Value", "Target Value": "Target Value",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "Maximum Balance", "Maximum Balance": "Maximum Balance",
"Median Balance": "Median Balance", "Median Balance": "Median Balance",
"Average Balance": "Average Balance", "Average Balance": "Average Balance",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "口座別の支出", "Expense By Account": "口座別の支出",
"Expense By Primary Category": "一次カテゴリ別の支出", "Expense By Primary Category": "一次カテゴリ別の支出",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "最大金額", "Maximum Amount": "最大金額",
"Display Order": "表示順", "Display Order": "表示順",
"Name": "名前", "Name": "名前",
"Value": "Value",
"Proportion (%)": "Proportion (%)", "Proportion (%)": "Proportion (%)",
"Sort by Amount": "金額で並べ替え",
"Sort by Display Order": "表示で並べ替え",
"Sort by Name": "名前で並べ替え",
"Time Granularity": "Time Granularity", "Time Granularity": "Time Granularity",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "月ごとに集計", "Aggregate by Month": "月ごとに集計",
"Aggregate by Quarter": "四半期ごとに集計", "Aggregate by Quarter": "四半期ごとに集計",
"Aggregate by Year": "年ごとに集計", "Aggregate by Year": "年ごとに集計",
"Aggregate by Fiscal Year": "Aggregate by Fiscal Year", "Aggregate by Fiscal Year": "Aggregate by Fiscal Year",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "口座で絞り込み", "Filter Accounts": "口座で絞り込み",
"Filter Transaction Categories": "取引カテゴリで絞り込み", "Filter Transaction Categories": "取引カテゴリで絞り込み",
"Filter Transaction Tags": "取引タグで絞り込み", "Filter Transaction Tags": "取引タグで絞り込み",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "毎月の合計金額を表示", "Show Monthly Total Amount": "毎月の合計金額を表示",
"Show Transaction Tags": "取引タグを表示", "Show Transaction Tags": "取引タグを表示",
"Transaction Edit Page": "取引編集ページ", "Transaction Edit Page": "取引編集ページ",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "下書きの自動保存", "Automatically Save Draft": "下書きの自動保存",
"Always Show Confirmation": "確認を毎回表示", "Always Show Confirmation": "確認を毎回表示",
"Automatically Add Geolocation": "座標を自動的に追加", "Automatically Add Geolocation": "座標を自動的に追加",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File", "Export to CSV (Comma-separated values) File": "Export to CSV (Comma-separated values) File",
"Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File", "Export to TSV (Tab-separated values) File": "Export to TSV (Tab-separated values) File",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown File", "Markdown File": "Markdown File",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "ユーザーデータをクリア", "Clear User Data": "ユーザーデータをクリア",
"Clear All Transactions": "Clear All Transactions", "Clear All Transactions": "Clear All Transactions",
"Clear All Data": "Clear All Data", "Clear All Data": "Clear All Data",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings", "Unable to retrieve user synchronized application settings": "Unable to retrieve user synchronized application settings",
"Unable to update user synchronized application settings": "Unable to update user synchronized application settings", "Unable to update user synchronized application settings": "Unable to update user synchronized application settings",
"Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings", "Unable to disable user synchronized application settings": "Unable to disable user synchronized application settings",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "再ログインしますか?", "Are you sure you want to re-login?": "再ログインしますか?",
"Exchange Rates Data": "為替レートデータ", "Exchange Rates Data": "為替レートデータ",
"User Custom": "User Custom", "User Custom": "User Custom",
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "BOM ಸಹಿತ UTF-8",
"utf-16le": "UTF-16 ಲಿಟಲ್ ಎಂಡಿಯನ್", "utf-16le": "UTF-16 ಲಿಟಲ್ ಎಂಡಿಯನ್",
"utf-16be": "UTF-16 ಬಿಗ್ ಎಂಡಿಯನ್", "utf-16be": "UTF-16 ಬಿಗ್ ಎಂಡಿಯನ್",
"utf-16le-bom": "BOM ಸಹಿತ UTF-16 ಲಿಟಲ್ ಎಂಡಿಯನ್", "utf-32le": "UTF-32 ಲಿಟಲ್ ಎಂಡಿಯನ್",
"utf-16be-bom": "BOM ಸಹಿತ UTF-16 ಬಿಗ್ ಎಂಡಿಯನ್", "utf-32be": "UTF-32 ಬಿಗ್ ಎಂಡಿಯನ್",
"cp437": "OEM ಯುನೈಟೆಡ್ ಸ್ಟೇಟ್ಸ್ (CP-437)", "cp437": "OEM ಯುನೈಟೆಡ್ ಸ್ಟೇಟ್ಸ್ (CP-437)",
"cp863": "OEM ಕ್ಯಾನಡಿಯನ್ ಫ್ರೆಂಚ್ (CP-863)", "cp863": "OEM ಕ್ಯಾನಡಿಯನ್ ಫ್ರೆಂಚ್ (CP-863)",
"cp037": "IBM EBCDIC ಯುಎಸ್/ಕ್ಯಾನಡಾ (CP-037)", "cp037": "IBM EBCDIC ಯುಎಸ್/ಕ್ಯಾನಡಾ (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[ಐಚ್ಛಿಕ] ವರ್ಗದ ಹೆಸರು", "fieldCategoryNameDescription": "[ಐಚ್ಛಿಕ] ವರ್ಗದ ಹೆಸರು",
"fieldSourceAccountNameDescription": "[ಐಚ್ಛಿಕ] ಮೂಲ ಖಾತೆಯ ಹೆಸರು", "fieldSourceAccountNameDescription": "[ಐಚ್ಛಿಕ] ಮೂಲ ಖಾತೆಯ ಹೆಸರು",
"fieldDestinationAccountNameDescription": "[ಐಚ್ಛಿಕ] ಗಮ್ಯ ಖಾತೆಯ ಹೆಸರು (ಹಸ್ತಾಂತರ ಪ್ರಕಾರಕ್ಕೆ ಮಾತ್ರ)", "fieldDestinationAccountNameDescription": "[ಐಚ್ಛಿಕ] ಗಮ್ಯ ಖಾತೆಯ ಹೆಸರು (ಹಸ್ತಾಂತರ ಪ್ರಕಾರಕ್ಕೆ ಮಾತ್ರ)",
"fieldSourceAmountDescription": "[ಅಗತ್ಯ] ಮೂಲ ಮೊತ್ತ", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[ಐಚ್ಛಿಕ] ಗಮ್ಯ ಮೊತ್ತ (ಹಸ್ತಾಂತರ ಪ್ರಕಾರಕ್ಕೆ ಮಾತ್ರ)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[ಐಚ್ಛಿಕ] ಭೌಗೋಳಿಕ ಸ್ಥಳ, ಸ್ವರೂಪ: 'ದೀಕ್ಷಾಂಶ ಅಕ್ಷಾಂಶ', ಉದಾ: '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[ಐಚ್ಛಿಕ] ಭೌಗೋಳಿಕ ಸ್ಥಳ, ಸ್ವರೂಪ: 'ದೀಕ್ಷಾಂಶ ಅಕ್ಷಾಂಶ', ಉದಾ: '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[ಐಚ್ಛಿಕ] ಟ್ಯಾಗ್‌ಗಳ ಹೆಸರುಗಳು, ಸೆಮಿಕೋಲನ್ ಬೇರ್ಪಡಿಸಿದವು, ಉದಾ: 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[ಐಚ್ಛಿಕ] ಟ್ಯಾಗ್‌ಗಳ ಹೆಸರುಗಳು, ಸೆಮಿಕೋಲನ್ ಬೇರ್ಪಡಿಸಿದವು, ಉದಾ: 'tag1;tag2;tag3'",
"fieldCommentDescription": "[ಐಚ್ಛಿಕ] ವಿವರಣೆ" "fieldCommentDescription": "[ಐಚ್ಛಿಕ] ವಿವರಣೆ"
@@ -1496,7 +1495,10 @@
"Remove": "ತೆಗೆದುಹಾಕು", "Remove": "ತೆಗೆದುಹಾಕು",
"Delete": "ಅಳಿಸು", "Delete": "ಅಳಿಸು",
"Duplicate": "ನಕಲು ಮಾಡು", "Duplicate": "ನಕಲು ಮಾಡು",
"Open Menu": "Open Menu",
"Sort": "ವಿಂಗಡಿಸು", "Sort": "ವಿಂಗಡಿಸು",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "ದಿನಾಂಕ", "Date": "ದಿನಾಂಕ",
"Time": "ಸಮಯ", "Time": "ಸಮಯ",
"Color": "ಬಣ್ಣ", "Color": "ಬಣ್ಣ",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "ಇಂದು", "Today": "ಇಂದು",
"Yesterday": "ನಿನ್ನೆ", "Yesterday": "ನಿನ್ನೆ",
"Recent 7 days": "ಇತ್ತೀಚಿನ 7 ದಿನಗಳು", "Recent 7 days": "ಇತ್ತೀಚಿನ 7 ದಿನಗಳು",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "ಪಾಯಿ ಚಾರ್ಟ್", "Pie Chart": "ಪಾಯಿ ಚಾರ್ಟ್",
"Bar Chart": "ಬಾರ್ ಚಾರ್ಟ್", "Bar Chart": "ಬಾರ್ ಚಾರ್ಟ್",
"Radar Chart": "ರಡಾರ್ ಚಾರ್ಟ್", "Radar Chart": "ರಡಾರ್ ಚಾರ್ಟ್",
"Area Chart": "ಏರಿಯಾ ಚಾರ್ಟ್", "Area Chart": "ಏರಿಯಾ ಚಾರ್ಟ್",
"Column Chart": "ಕಾಲಮ್ ಚಾರ್ಟ್", "Column Chart": "ಕಾಲಮ್ ಚಾರ್ಟ್",
"Bubble Chart": "ಬಬಲ್ ಚಾರ್ಟ್", "Bubble Chart": "ಬಬಲ್ ಚಾರ್ಟ್",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "ಕ್ಯಾಂಡಲ್‌ಸ್ಟಿಕ್ ಚಾರ್ಟ್", "Candlestick Chart": "ಕ್ಯಾಂಡಲ್‌ಸ್ಟಿಕ್ ಚಾರ್ಟ್",
"Sankey Chart": "ಸ್ಯಾಂಕಿ ಚಾರ್ಟ್", "Sankey Chart": "ಸ್ಯಾಂಕಿ ಚಾರ್ಟ್",
"Column Chart (Stacked)": "Column Chart (Stacked)", "Column Chart (Stacked)": "Column Chart (Stacked)",
@@ -1743,6 +1752,7 @@
"Remove Query": "Remove Query", "Remove Query": "Remove Query",
"Modify Query Name": "Modify Query Name", "Modify Query Name": "Modify Query Name",
"Add Condition": "Add Condition", "Add Condition": "Add Condition",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "Remove Condition", "Remove Condition": "Remove Condition",
"No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.", "No conditions defined. All transactions will match.": "No conditions defined. All transactions will match.",
"Unable to retrieve explorer list": "Unable to retrieve explorer list", "Unable to retrieve explorer list": "Unable to retrieve explorer list",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "Transaction Day of Month", "Transaction Day of Month": "Transaction Day of Month",
"Transaction Month of Year": "Transaction Month of Year", "Transaction Month of Year": "Transaction Month of Year",
"Transaction Quarter of Year": "Transaction Quarter of Year", "Transaction Quarter of Year": "Transaction Quarter of Year",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "Source Account Category", "Source Account Category": "Source Account Category",
"Source Account Currency": "Source Account Currency", "Source Account Currency": "Source Account Currency",
"Destination Account Category": "Destination Account Category", "Destination Account Category": "Destination Account Category",
@@ -1782,6 +1793,14 @@
"Transaction Count": "Transaction Count", "Transaction Count": "Transaction Count",
"Average Amount": "Average Amount", "Average Amount": "Average Amount",
"Median Amount": "Median Amount", "Median Amount": "Median Amount",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ", "Account List": "ಖಾತೆಗಳ ಪಟ್ಟಿ",
"This Week": "ಈ ವಾರ", "This Week": "ಈ ವಾರ",
"This Month": "ಈ ತಿಂಗಳು", "This Month": "ಈ ತಿಂಗಳು",
@@ -1900,6 +1919,8 @@
"Swap Account": "ಖಾತೆ ಬದಲಾಯಿಸಿ", "Swap Account": "ಖಾತೆ ಬದಲಾಯಿಸಿ",
"Swap Amount": "ಮೊತ್ತ ಬದಲಾಯಿಸಿ", "Swap Amount": "ಮೊತ್ತ ಬದಲಾಯಿಸಿ",
"Swap Account and Amount": "ಖಾತೆ ಮತ್ತು ಮೊತ್ತ ಬದಲಾಯಿಸಿ", "Swap Account and Amount": "ಖಾತೆ ಮತ್ತು ಮೊತ್ತ ಬದಲಾಯಿಸಿ",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "ನಕಲು (ಸಮಯ ಸಹಿತ)", "Duplicate (With Time)": "ನಕಲು (ಸಮಯ ಸಹಿತ)",
"Duplicate (With Geographic Location)": "ನಕಲು (ಭೌಗೋಳಿಕ ಸ್ಥಳ ಸಹಿತ)", "Duplicate (With Geographic Location)": "ನಕಲು (ಭೌಗೋಳಿಕ ಸ್ಥಳ ಸಹಿತ)",
"Duplicate (With Time and Geographic Location)": "ನಕಲು (ಸಮಯ ಮತ್ತು ಭೌಗೋಳಿಕ ಸ್ಥಳ ಸಹಿತ)", "Duplicate (With Time and Geographic Location)": "ನಕಲು (ಸಮಯ ಮತ್ತು ಭೌಗೋಳಿಕ ಸ್ಥಳ ಸಹಿತ)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "ಇತರೆ ಹಣಕಾಸು ಅಪ್ ಫೈಲ್ ರೂಪ", "Other Finance App File Format": "ಇತರೆ ಹಣಕಾಸು ಅಪ್ ಫೈಲ್ ರೂಪ",
"ezbookkeeping Data Export File": "ezBookkeeping ಡೇಟಾ ರಫ್ತು ಫೈಲ್", "ezbookkeeping Data Export File": "ezBookkeeping ಡೇಟಾ ರಫ್ತು ಫೈಲ್",
"Excel Workbook File": "Excel ವರ್ಕ್‌ಬುಕ್ ಫೈಲ್", "Excel Workbook File": "Excel ವರ್ಕ್‌ಬುಕ್ ಫೈಲ್",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) ಫೈಲ್", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) ಫೈಲ್",
"Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) ಫೈಲ್", "Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) ಫೈಲ್",
"Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) ಫೈಲ್", "Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) ಫೈಲ್",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "ಆಯ್ಕೆ ಮಾಡಿದ ಖಾತೆಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ", "Batch Replace Selected Accounts": "ಆಯ್ಕೆ ಮಾಡಿದ ಖಾತೆಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ",
"Batch Replace Selected Destination Accounts": "ಆಯ್ಕೆ ಮಾಡಿದ ಗುರಿ ಖಾತೆಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ", "Batch Replace Selected Destination Accounts": "ಆಯ್ಕೆ ಮಾಡಿದ ಗುರಿ ಖಾತೆಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ",
"Batch Replace Selected Transaction Tags": "ಆಯ್ಕೆ ಮಾಡಿದ ವಹಿವಾಟು ಟ್ಯಾಗ್‌ಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ", "Batch Replace Selected Transaction Tags": "ಆಯ್ಕೆ ಮಾಡಿದ ವಹಿವಾಟು ಟ್ಯಾಗ್‌ಗಳನ್ನು ಬ್ಯಾಚ್ ಬದಲಾಯಿಸಿ",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್‌ಗಳನ್ನು ಬ್ಯಾಚ್ ಸೇರಿಸಿ", "Batch Add Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್‌ಗಳನ್ನು ಬ್ಯಾಚ್ ಸೇರಿಸಿ",
"Replace Invalid Expense Categories": "ಅಮಾನ್ಯ ಖರ್ಚು ವರ್ಗಗಳನ್ನು ಬದಲಾಯಿಸಿ", "Replace Invalid Expense Categories": "ಅಮಾನ್ಯ ಖರ್ಚು ವರ್ಗಗಳನ್ನು ಬದಲಾಯಿಸಿ",
"Replace Invalid Income Categories": "ಅಮಾನ್ಯ ಆದಾಯ ವರ್ಗಗಳನ್ನು ಬದಲಾಯಿಸಿ", "Replace Invalid Income Categories": "ಅಮಾನ್ಯ ಆದಾಯ ವರ್ಗಗಳನ್ನು ಬದಲಾಯಿಸಿ",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "ಅಮಾನ್ಯ ಟ್ಯಾಗ್", "Invalid Tag": "ಅಮಾನ್ಯ ಟ್ಯಾಗ್",
"Target Tag": "ಗುರಿ ಟ್ಯಾಗ್", "Target Tag": "ಗುರಿ ಟ್ಯಾಗ್",
"Remove Tag": "ಟ್ಯಾಗ್ ತೆಗೆದುಹಾಕಿ", "Remove Tag": "ಟ್ಯಾಗ್ ತೆಗೆದುಹಾಕಿ",
"Target Timezone": "Target Timezone",
"(Empty)": "(ಖಾಲಿ)", "(Empty)": "(ಖಾಲಿ)",
"Source Value": "ಮೂಲ ಮೌಲ್ಯ", "Source Value": "ಮೂಲ ಮೌಲ್ಯ",
"Target Value": "ಗುರಿ ಮೌಲ್ಯ", "Target Value": "ಗುರಿ ಮೌಲ್ಯ",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "ಗರಿಷ್ಠ ಶೇಷ", "Maximum Balance": "ಗರಿಷ್ಠ ಶೇಷ",
"Median Balance": "ಮಧ್ಯ ಶೇಷ", "Median Balance": "ಮಧ್ಯ ಶೇಷ",
"Average Balance": "ಸರಾಸರಿ ಶೇಷ", "Average Balance": "ಸರಾಸರಿ ಶೇಷ",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "ಖಾತೆವಾರು ಹೊರಹರಿವು", "Outflows By Account": "ಖಾತೆವಾರು ಹೊರಹರಿವು",
"Expense By Account": "ಖಾತೆವಾರು ಖರ್ಚು", "Expense By Account": "ಖಾತೆವಾರು ಖರ್ಚು",
"Expense By Primary Category": "ಪ್ರಾಥಮಿಕ ವರ್ಗವಾರು ಖರ್ಚು", "Expense By Primary Category": "ಪ್ರಾಥಮಿಕ ವರ್ಗವಾರು ಖರ್ಚು",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "ಗರಿಷ್ಠ ಮೊತ್ತ", "Maximum Amount": "ಗರಿಷ್ಠ ಮೊತ್ತ",
"Display Order": "ಪ್ರದರ್ಶಿಸುವ ಕ್ರಮ", "Display Order": "ಪ್ರದರ್ಶಿಸುವ ಕ್ರಮ",
"Name": "ಹೆಸರು", "Name": "ಹೆಸರು",
"Value": "Value",
"Proportion (%)": "ಪ್ರಮಾಣ (%)", "Proportion (%)": "ಪ್ರಮಾಣ (%)",
"Sort by Amount": "ಮೊತ್ತದ ಆಧಾರದ ಮೇಲೆ ವಿಂಗಡಿಸಿ",
"Sort by Display Order": "ಪ್ರದರ್ಶನ ಕ್ರಮದ ಆಧಾರದ ಮೇಲೆ ವಿಂಗಡಿಸಿ",
"Sort by Name": "ಹೆಸರಿನ ಆಧಾರದ ಮೇಲೆ ವಿಂಗಡಿಸಿ",
"Time Granularity": "ಸಮಯದ ವಿವರ ಮಟ್ಟ", "Time Granularity": "ಸಮಯದ ವಿವರ ಮಟ್ಟ",
"Aggregate by Day": "ದಿನವಾರು ಒಕ್ಕೂಟ", "Aggregate by Day": "ದಿನವಾರು ಒಕ್ಕೂಟ",
"Aggregate by Month": "ತಿಂಗಳುವಾರು ಒಕ್ಕೂಟ", "Aggregate by Month": "ತಿಂಗಳುವಾರು ಒಕ್ಕೂಟ",
"Aggregate by Quarter": "ತ್ರೈಮಾಸಿಕ ಒಕ್ಕೂಟ", "Aggregate by Quarter": "ತ್ರೈಮಾಸಿಕ ಒಕ್ಕೂಟ",
"Aggregate by Year": "ವರ್ಷವಾರು ಒಕ್ಕೂಟ", "Aggregate by Year": "ವರ್ಷವಾರು ಒಕ್ಕೂಟ",
"Aggregate by Fiscal Year": "ಹಣಕಾಸು ವರ್ಷವಾರು ಒಕ್ಕೂಟ", "Aggregate by Fiscal Year": "ಹಣಕಾಸು ವರ್ಷವಾರು ಒಕ್ಕೂಟ",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "ಖಾತೆಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ", "Filter Accounts": "ಖಾತೆಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ",
"Filter Transaction Categories": "ವಹಿವಾಟು ವರ್ಗಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ", "Filter Transaction Categories": "ವಹಿವಾಟು ವರ್ಗಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ",
"Filter Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್ಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ", "Filter Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್ಗಳನ್ನು ಫಿಲ್ಟರ್ ಮಾಡಿ",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "ಮಾಸಿಕ ಒಟ್ಟು ಮೊತ್ತ ತೋರಿಸಿ", "Show Monthly Total Amount": "ಮಾಸಿಕ ಒಟ್ಟು ಮೊತ್ತ ತೋರಿಸಿ",
"Show Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್ ತೋರಿಸಿ", "Show Transaction Tags": "ವಹಿವಾಟು ಟ್ಯಾಗ್ ತೋರಿಸಿ",
"Transaction Edit Page": "ವಹಿವಾಟು ಸಂಪಾದನೆ ಪುಟ", "Transaction Edit Page": "ವಹಿವಾಟು ಸಂಪಾದನೆ ಪುಟ",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "ಕರಡು ಸ್ವಯಂ ಉಳಿಸಿ", "Automatically Save Draft": "ಕರಡು ಸ್ವಯಂ ಉಳಿಸಿ",
"Always Show Confirmation": "ಪ್ರತಿ ಬಾರಿ ದೃಢೀಕರಣ ತೋರಿಸಿ", "Always Show Confirmation": "ಪ್ರತಿ ಬಾರಿ ದೃಢೀಕರಣ ತೋರಿಸಿ",
"Automatically Add Geolocation": "ಭೌಗೋಳಿಕ ಸ್ಥಾನವನ್ನು ಸ್ವಯಂ ಸೇರಿಸಿ", "Automatically Add Geolocation": "ಭೌಗೋಳಿಕ ಸ್ಥಾನವನ್ನು ಸ್ವಯಂ ಸೇರಿಸಿ",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File", "SSV (Semicolon-separated values) File": "SSV (Semicolon-separated values) File",
"Export to CSV (Comma-separated values) File": "CSV (ಕಾಮಾ-ಪ್ರತ್ಯೇಕಿತ ಮೌಲ್ಯಗಳು) ಫೈಲ್‌ಗೆ ರಫ್ತು ಮಾಡಿ", "Export to CSV (Comma-separated values) File": "CSV (ಕಾಮಾ-ಪ್ರತ್ಯೇಕಿತ ಮೌಲ್ಯಗಳು) ಫೈಲ್‌ಗೆ ರಫ್ತು ಮಾಡಿ",
"Export to TSV (Tab-separated values) File": "TSV (ಟ್ಯಾಬ್-ಪ್ರತ್ಯೇಕಿತ ಮೌಲ್ಯಗಳು) ಫೈಲ್‌ಗೆ ರಫ್ತು ಮಾಡಿ", "Export to TSV (Tab-separated values) File": "TSV (ಟ್ಯಾಬ್-ಪ್ರತ್ಯೇಕಿತ ಮೌಲ್ಯಗಳು) ಫೈಲ್‌ಗೆ ರಫ್ತು ಮಾಡಿ",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown ಫೈಲ್", "Markdown File": "Markdown ಫೈಲ್",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "ಬಳಕೆದಾರ ಡೇಟಾ ತೆರವುಗೊಳಿಸಿ", "Clear User Data": "ಬಳಕೆದಾರ ಡೇಟಾ ತೆರವುಗೊಳಿಸಿ",
"Clear All Transactions": "ಎಲ್ಲಾ ವಹಿವಾಟುಗಳನ್ನು ತೆರವುಗೊಳಿಸಿ", "Clear All Transactions": "ಎಲ್ಲಾ ವಹಿವಾಟುಗಳನ್ನು ತೆರವುಗೊಳಿಸಿ",
"Clear All Data": "ಎಲ್ಲಾ ಡೇಟಾವನ್ನು ತೆರವುಗೊಳಿಸಿ", "Clear All Data": "ಎಲ್ಲಾ ಡೇಟಾವನ್ನು ತೆರವುಗೊಳಿಸಿ",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ಪಡೆಯಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ", "Unable to retrieve user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ಪಡೆಯಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
"Unable to update user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ", "Unable to update user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
"Unable to disable user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ", "Unable to disable user synchronized application settings": "ಬಳಕೆದಾರರ ಸಿಂಕ್ ಮಾಡಿದ ಅಪ್ಲಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಸ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ಸಾಧ್ಯವಾಗಿಲ್ಲ",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "ಮತ್ತೆ ಲಾಗಿನ್ ಮಾಡಲು ನೀವು ಖಚಿತವೇ?", "Are you sure you want to re-login?": "ಮತ್ತೆ ಲಾಗಿನ್ ಮಾಡಲು ನೀವು ಖಚಿತವೇ?",
"Exchange Rates Data": "ವಿನಿಮಯ ದರ ಡೇಟಾ", "Exchange Rates Data": "ವಿನಿಮಯ ದರ ಡೇಟಾ",
"User Custom": "ಬಳಕೆದಾರ ಕಸ್ಟಮ್", "User Custom": "ಬಳಕೆದಾರ ಕಸ್ಟಮ್",
+63 -8
View File
@@ -1353,11 +1353,10 @@
}, },
"encoding": { "encoding": {
"utf-8": "UTF-8", "utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 리틀 엔디안", "utf-16le": "UTF-16 리틀 엔디안",
"utf-16be": "UTF-16 빅 엔디안", "utf-16be": "UTF-16 빅 엔디안",
"utf-16le-bom": "UTF-16 Little Endian with BOM", "utf-32le": "UTF-32 리틀 엔디안",
"utf-16be-bom": "UTF-16 Big Endian with BOM", "utf-32be": "UTF-32 빅 엔디안",
"cp437": "OEM 미국 (CP-437)", "cp437": "OEM 미국 (CP-437)",
"cp863": "OEM 캐나다 프랑스어 (CP-863)", "cp863": "OEM 캐나다 프랑스어 (CP-863)",
"cp037": "IBM EBCDIC 미국/캐나다 (CP-037)", "cp037": "IBM EBCDIC 미국/캐나다 (CP-037)",
@@ -1428,8 +1427,8 @@
"fieldCategoryNameDescription": "[선택] 카테고리 이름", "fieldCategoryNameDescription": "[선택] 카테고리 이름",
"fieldSourceAccountNameDescription": "[선택] 출처 계좌 이름", "fieldSourceAccountNameDescription": "[선택] 출처 계좌 이름",
"fieldDestinationAccountNameDescription": "[선택] 대상 계좌 이름 (전송 유형 전용)", "fieldDestinationAccountNameDescription": "[선택] 대상 계좌 이름 (전송 유형 전용)",
"fieldSourceAmountDescription": "[필수] 출처 금액", "fieldSourceAmountDescription": "[required] Source amount (including two decimal places expressed without a decimal separator, for example '12345' represents 123.45)",
"fieldDestinationAmountDescription": "[선택] 대상 금액 (전송 유형 전용)", "fieldDestinationAmountDescription": "[optional] Destination amount (for transfer type only, format is the same as the source amount)",
"fieldGeoLocationDescription": "[선택] 지리적 위치, 형식: '경도 위도', 예: '116.3912972 39.9057136'", "fieldGeoLocationDescription": "[선택] 지리적 위치, 형식: '경도 위도', 예: '116.3912972 39.9057136'",
"fieldTagNamesDescription": "[선택] 쉼표로 구분된 태그 이름, 예: 'tag1;tag2;tag3'", "fieldTagNamesDescription": "[선택] 쉼표로 구분된 태그 이름, 예: 'tag1;tag2;tag3'",
"fieldCommentDescription": "[선택] 설명" "fieldCommentDescription": "[선택] 설명"
@@ -1496,7 +1495,10 @@
"Remove": "제거", "Remove": "제거",
"Delete": "삭제", "Delete": "삭제",
"Duplicate": "복제", "Duplicate": "복제",
"Open Menu": "Open Menu",
"Sort": "정렬", "Sort": "정렬",
"Sort by Name (A to Z)": "Sort by Name (A to Z)",
"Sort by Name (Z to A)": "Sort by Name (Z to A)",
"Date": "날짜", "Date": "날짜",
"Time": "시간", "Time": "시간",
"Color": "색상", "Color": "색상",
@@ -1515,6 +1517,8 @@
"WHERE": "WHERE", "WHERE": "WHERE",
"AND": "AND", "AND": "AND",
"OR": "OR", "OR": "OR",
"AND SUB": "AND SUB",
"OR SUB": "OR SUB",
"Today": "오늘", "Today": "오늘",
"Yesterday": "어제", "Yesterday": "어제",
"Recent 7 days": "최근 7일", "Recent 7 days": "최근 7일",
@@ -1565,12 +1569,17 @@
"Not starts with": "Not starts with", "Not starts with": "Not starts with",
"Ends with": "Ends with", "Ends with": "Ends with",
"Not ends with": "Not ends with", "Not ends with": "Not ends with",
"Latitude between": "Latitude between",
"Latitude not between": "Latitude not between",
"Longitude between": "Longitude between",
"Longitude not between": "Longitude not between",
"Pie Chart": "원형 차트", "Pie Chart": "원형 차트",
"Bar Chart": "막대 차트", "Bar Chart": "막대 차트",
"Radar Chart": "레이더 차트", "Radar Chart": "레이더 차트",
"Area Chart": "영역 차트", "Area Chart": "영역 차트",
"Column Chart": "세로 막대 차트", "Column Chart": "세로 막대 차트",
"Bubble Chart": "버블 차트", "Bubble Chart": "버블 차트",
"Boxplot Chart": "Boxplot Chart",
"Candlestick Chart": "캠들스틱 차트", "Candlestick Chart": "캠들스틱 차트",
"Sankey Chart": "샌키 차트", "Sankey Chart": "샌키 차트",
"Column Chart (Stacked)": "누적 세로 막대 차트", "Column Chart (Stacked)": "누적 세로 막대 차트",
@@ -1743,6 +1752,7 @@
"Remove Query": "쿼리 제거", "Remove Query": "쿼리 제거",
"Modify Query Name": "쿼리 이름 수정", "Modify Query Name": "쿼리 이름 수정",
"Add Condition": "조건 추가", "Add Condition": "조건 추가",
"Add Sub Condition": "Add Sub Condition",
"Remove Condition": "조건 제거", "Remove Condition": "조건 제거",
"No conditions defined. All transactions will match.": "조건이 정의되지 않았습니다. 모든 거래가 일치합니다.", "No conditions defined. All transactions will match.": "조건이 정의되지 않았습니다. 모든 거래가 일치합니다.",
"Unable to retrieve explorer list": "탐색기 목록을 가져올 수 없습니다", "Unable to retrieve explorer list": "탐색기 목록을 가져올 수 없습니다",
@@ -1774,6 +1784,7 @@
"Transaction Day of Month": "거래 일자", "Transaction Day of Month": "거래 일자",
"Transaction Month of Year": "거래 월", "Transaction Month of Year": "거래 월",
"Transaction Quarter of Year": "거래 분기", "Transaction Quarter of Year": "거래 분기",
"Transaction Hour of Day": "Transaction Hour of Day",
"Source Account Category": "출처 계좌 분류", "Source Account Category": "출처 계좌 분류",
"Source Account Currency": "출처 계좌 통화", "Source Account Currency": "출처 계좌 통화",
"Destination Account Category": "목적지 계좌 분류", "Destination Account Category": "목적지 계좌 분류",
@@ -1782,6 +1793,14 @@
"Transaction Count": "거래 수", "Transaction Count": "거래 수",
"Average Amount": "평균 금액", "Average Amount": "평균 금액",
"Median Amount": "중간 금액", "Median Amount": "중간 금액",
"90th Percentile Amount": "90th Percentile Amount",
"Top 5 Amount Share": "Top 5 Amount Share",
"Transactions for 80% of Amount": "Transactions for 80% of Amount",
"Range (Max - Min)": "Range (Max - Min)",
"Interquartile Range (Q3 - Q1)": "Interquartile Range (Q3 - Q1)",
"Variance": "Variance",
"Standard Deviation": "Standard Deviation",
"Coefficient of Variation": "Coefficient of Variation",
"Account List": "계좌 목록", "Account List": "계좌 목록",
"This Week": "이번 주", "This Week": "이번 주",
"This Month": "이번 달", "This Month": "이번 달",
@@ -1900,6 +1919,8 @@
"Swap Account": "계좌 교체", "Swap Account": "계좌 교체",
"Swap Amount": "금액 교체", "Swap Amount": "금액 교체",
"Swap Account and Amount": "계좌 및 금액 교체", "Swap Account and Amount": "계좌 및 금액 교체",
"Save & New": "Save & New",
"Save & Duplicate": "Save & Duplicate",
"Duplicate (With Time)": "복제 (시간 포함)", "Duplicate (With Time)": "복제 (시간 포함)",
"Duplicate (With Geographic Location)": "복제 (위치 포함)", "Duplicate (With Geographic Location)": "복제 (위치 포함)",
"Duplicate (With Time and Geographic Location)": "복제 (시간 및 위치 포함)", "Duplicate (With Time and Geographic Location)": "복제 (시간 및 위치 포함)",
@@ -1974,6 +1995,8 @@
"Other Finance App File Format": "기타 금융 앱 파일 형식", "Other Finance App File Format": "기타 금융 앱 파일 형식",
"ezbookkeeping Data Export File": "ezbookkeeping 데이터 내보내기 파일", "ezbookkeeping Data Export File": "ezbookkeeping 데이터 내보내기 파일",
"Excel Workbook File": "Excel 통합 문서 파일", "Excel Workbook File": "Excel 통합 문서 파일",
"Excel Workbook File (.xlsx)": "Excel Workbook File (.xlsx)",
"Excel 97-2003 Workbook File (.xls)": "Excel 97-2003 Workbook File (.xls)",
"Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) 파일", "Open Financial Exchange (OFX) File": "Open Financial Exchange (OFX) 파일",
"Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) 파일", "Quicken Financial Exchange (QFX) File": "Quicken Financial Exchange (QFX) 파일",
"Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) 파일", "Quicken Interchange Format (QIF) File": "Quicken Interchange Format (QIF) 파일",
@@ -2047,6 +2070,7 @@
"Batch Replace Selected Accounts": "선택한 계좌 일괄 교체", "Batch Replace Selected Accounts": "선택한 계좌 일괄 교체",
"Batch Replace Selected Destination Accounts": "선택한 목적지 계좌 일괄 교체", "Batch Replace Selected Destination Accounts": "선택한 목적지 계좌 일괄 교체",
"Batch Replace Selected Transaction Tags": "선택한 거래 태그 일괄 교체", "Batch Replace Selected Transaction Tags": "선택한 거래 태그 일괄 교체",
"Batch Replace Selected Transaction Timezones": "Batch Replace Selected Transaction Timezones",
"Batch Add Transaction Tags": "거래 태그 일괄 추가", "Batch Add Transaction Tags": "거래 태그 일괄 추가",
"Replace Invalid Expense Categories": "유효하지 않은 비용 카테고리 교체", "Replace Invalid Expense Categories": "유효하지 않은 비용 카테고리 교체",
"Replace Invalid Income Categories": "유효하지 않은 수입 카테고리 교체", "Replace Invalid Income Categories": "유효하지 않은 수입 카테고리 교체",
@@ -2079,6 +2103,7 @@
"Invalid Tag": "유효하지 않은 태그", "Invalid Tag": "유효하지 않은 태그",
"Target Tag": "대상 태그", "Target Tag": "대상 태그",
"Remove Tag": "태그 제거", "Remove Tag": "태그 제거",
"Target Timezone": "Target Timezone",
"(Empty)": "(비어 있음)", "(Empty)": "(비어 있음)",
"Source Value": "원본 값", "Source Value": "원본 값",
"Target Value": "대상 값", "Target Value": "대상 값",
@@ -2144,6 +2169,8 @@
"Maximum Balance": "최대 잔액", "Maximum Balance": "최대 잔액",
"Median Balance": "중앙값 잔액", "Median Balance": "중앙값 잔액",
"Average Balance": "평균 잔액", "Average Balance": "평균 잔액",
"Q1 Balance (First Quartile)": "Q1 Balance (First Quartile)",
"Q3 Balance (Third Quartile)": "Q3 Balance (Third Quartile)",
"Outflows By Account": "Outflows By Account", "Outflows By Account": "Outflows By Account",
"Expense By Account": "계좌별 비용", "Expense By Account": "계좌별 비용",
"Expense By Primary Category": "주요 범주별 비용", "Expense By Primary Category": "주요 범주별 비용",
@@ -2175,16 +2202,16 @@
"Maximum Amount": "최대 금액", "Maximum Amount": "최대 금액",
"Display Order": "표시 순서", "Display Order": "표시 순서",
"Name": "이름", "Name": "이름",
"Value": "Value",
"Proportion (%)": "비율 (%)", "Proportion (%)": "비율 (%)",
"Sort by Amount": "금액별 정렬",
"Sort by Display Order": "표시 순서별 정렬",
"Sort by Name": "이름별 정렬",
"Time Granularity": "시간 세분화", "Time Granularity": "시간 세분화",
"Aggregate by Day": "Aggregate by Day", "Aggregate by Day": "Aggregate by Day",
"Aggregate by Month": "월별 집계", "Aggregate by Month": "월별 집계",
"Aggregate by Quarter": "분기별 집계", "Aggregate by Quarter": "분기별 집계",
"Aggregate by Year": "연도별 집계", "Aggregate by Year": "연도별 집계",
"Aggregate by Fiscal Year": "회계 연도별 집계", "Aggregate by Fiscal Year": "회계 연도별 집계",
"Year-over-Year": "Year-over-Year",
"Period-over-Period": "Period-over-Period",
"Filter Accounts": "계좌 필터", "Filter Accounts": "계좌 필터",
"Filter Transaction Categories": "거래 범주 필터", "Filter Transaction Categories": "거래 범주 필터",
"Filter Transaction Tags": "거래 태그 필터", "Filter Transaction Tags": "거래 태그 필터",
@@ -2230,6 +2257,12 @@
"Show Monthly Total Amount": "월별 총 금액 표시", "Show Monthly Total Amount": "월별 총 금액 표시",
"Show Transaction Tags": "거래 태그 표시", "Show Transaction Tags": "거래 태그 표시",
"Transaction Edit Page": "거래 편집 페이지", "Transaction Edit Page": "거래 편집 페이지",
"Quick Save Button Style": "Quick Save Button Style",
"Bottom Left Floating": "Bottom Left Floating",
"Bottom Center Floating": "Bottom Center Floating",
"Bottom Right Floating": "Bottom Right Floating",
"Bottom Fixed": "Bottom Fixed",
"Quick Add Button Action": "Quick Add Button Action",
"Automatically Save Draft": "초안 자동 저장", "Automatically Save Draft": "초안 자동 저장",
"Always Show Confirmation": "매번 확인 표시", "Always Show Confirmation": "매번 확인 표시",
"Automatically Add Geolocation": "지리적 위치 자동 추가", "Automatically Add Geolocation": "지리적 위치 자동 추가",
@@ -2298,7 +2331,10 @@
"SSV (Semicolon-separated values) File": "SSV (세미콜론으로 구분된 값) 파일", "SSV (Semicolon-separated values) File": "SSV (세미콜론으로 구분된 값) 파일",
"Export to CSV (Comma-separated values) File": "CSV (쉼표로 구분된 값) 파일로 내보내기", "Export to CSV (Comma-separated values) File": "CSV (쉼표로 구분된 값) 파일로 내보내기",
"Export to TSV (Tab-separated values) File": "TSV (탭으로 구분된 값) 파일로 내보내기", "Export to TSV (Tab-separated values) File": "TSV (탭으로 구분된 값) 파일로 내보내기",
"Export to SSV (Semicolon-separated values) File": "Export to SSV (Semicolon-separated values) File",
"Markdown File": "Markdown 파일", "Markdown File": "Markdown 파일",
"Mermaid (Pie Chart)": "Mermaid (Pie Chart)",
"Mermaid (XY Chart)": "Mermaid (XY Chart)",
"Clear User Data": "사용자 데이터 지우기", "Clear User Data": "사용자 데이터 지우기",
"Clear All Transactions": "모든 거래 지우기", "Clear All Transactions": "모든 거래 지우기",
"Clear All Data": "모든 데이터 지우기", "Clear All Data": "모든 데이터 지우기",
@@ -2502,6 +2538,25 @@
"Unable to retrieve user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 검색할 수 없습니다.", "Unable to retrieve user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 검색할 수 없습니다.",
"Unable to update user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 업데이트할 수 없습니다.", "Unable to update user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 업데이트할 수 없습니다.",
"Unable to disable user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 비활성화할 수 없습니다.", "Unable to disable user synchronized application settings": "사용자 동기화된 애플리케이션 설정을 비활성화할 수 없습니다.",
"Browser Cache Management": "Browser Cache Management",
"File Cache": "File Cache",
"Used storage": "Used storage",
"Application Code": "Application Code",
"Resource Files": "Resource Files",
"Map Data": "Map Data",
"Others": "Others",
"Cache Expiration Time": "Cache Expiration Time",
"Cache Expiration for Map Data": "Cache Expiration for Map Data",
"Cache Expiration for Exchange Rates Data": "Cache Expiration for Exchange Rates Data",
"Disable Cache": "Disable Cache",
"Clear All File Cache": "Clear All File Cache",
"Are you sure you want to clear all file cache?": "Are you sure you want to clear all file cache?",
"Clear Map Data Cache": "Clear Map Data Cache",
"Are you sure you want to clear map data cache?": "Are you sure you want to clear map data cache?",
"Clear Application Code Cache": "Clear Application Code Cache",
"Are you sure you want to clear application code cache?": "Are you sure you want to clear application code cache?",
"Clear Exchange Rates Data Cache": "Clear Exchange Rates Data Cache",
"Are you sure you want to clear exchange rates data cache?": "Are you sure you want to clear exchange rates data cache?",
"Are you sure you want to re-login?": "다시 로그인하시겠습니까?", "Are you sure you want to re-login?": "다시 로그인하시겠습니까?",
"Exchange Rates Data": "환율 데이터", "Exchange Rates Data": "환율 데이터",
"User Custom": "사용자 정의", "User Custom": "사용자 정의",

Some files were not shown because too many files have changed in this diff Show More