diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0859eca33..02a5a640e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,9 @@ jobs: - name: Scan for security vulnerabilities in Ruby dependencies run: bin/brakeman --no-pager + - name: Validate preview deploy workflow hardening + run: ruby bin/preview_deploy_security_check.rb + scan_js: runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/label-not-gittensor.yml b/.github/workflows/label-not-gittensor.yml deleted file mode 100644 index 7b846e8f7..000000000 --- a/.github/workflows/label-not-gittensor.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Label non-Gittensor PRs - -on: - pull_request_target: - types: - - opened - - reopened - -permissions: - pull-requests: write - -jobs: - label-pr: - runs-on: ubuntu-latest - steps: - - name: Add not-gittensor label for matched authors - uses: actions/github-script@v7 - env: - GITTENSOR_USERS: ${{ vars.GITTENSOR_USERS || '[]' }} - GITTENSOR_EXCEPTIONS: ${{ vars.GITTENSOR_EXCEPTIONS || '[]' }} - TARGET_LABEL: not-gittensor - with: - script: | - const parseList = (raw, name) => { - try { - const parsed = JSON.parse(raw || '[]'); - if (!Array.isArray(parsed)) { - core.setFailed(`${name} must be a JSON array.`); - return []; - } - return parsed.map((value) => String(value).toLowerCase()); - } catch (error) { - core.setFailed(`Failed to parse ${name}: ${error.message}`); - return []; - } - }; - - const author = context.payload.pull_request.user.login.toLowerCase(); - const users = new Set(parseList(process.env.GITTENSOR_USERS, 'GITTENSOR_USERS')); - const exceptions = new Set(parseList(process.env.GITTENSOR_EXCEPTIONS, 'GITTENSOR_EXCEPTIONS')); - - if (users.has(author) || exceptions.has(author)) { - core.info(`No label needed for @${author}.`); - return; - } - - const existingLabels = context.payload.pull_request.labels.map((label) => label.name); - if (existingLabels.includes(process.env.TARGET_LABEL)) { - core.info(`Label ${process.env.TARGET_LABEL} already present.`); - return; - } - - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: [process.env.TARGET_LABEL], - }); - - core.info(`Added ${process.env.TARGET_LABEL} to PR #${context.payload.pull_request.number}.`); diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 3074503de..aba03b987 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -16,6 +16,11 @@ jobs: name: Deploy to Cloudflare Containers runs-on: ubuntu-latest timeout-minutes: 15 + concurrency: + group: preview-deploy-${{ github.event.pull_request.number }} + cancel-in-progress: true + environment: + name: preview permissions: actions: read contents: read @@ -32,10 +37,10 @@ jobs: steps: - name: Wait for PR CI to pass - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | - const headSha = context.payload.pull_request.head.sha; + const headSha = process.env.HEAD_SHA; const timeoutMs = 10 * 60 * 1000; const pollMs = 15 * 1000; const startedAt = Date.now(); @@ -73,68 +78,62 @@ jobs: core.setFailed(`Timed out waiting for Pull Request workflow for ${headSha}. Last state: ${lastState}`); - - name: Checkout code - uses: actions/checkout@v5 + - name: Checkout PR code + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + path: pr + persist-credentials: false + + - name: Checkout trusted preview tooling + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: trusted + persist-credentials: false + sparse-checkout: | + workers/preview - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: "24" - - name: Install Wrangler dependencies - working-directory: workers/preview - run: npm install - - - name: Configure preview files for this PR - working-directory: workers/preview - run: | - sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" wrangler.toml - sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" src/index.ts - cat wrangler.toml - - - name: Delete existing preview container app before redeploy - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - working-directory: workers/preview + - name: Prepare trusted preview deploy workspace run: | set -euo pipefail - CONTAINER_NAME="sure-preview-${PR_NUMBER}-railscontainer" - echo "Looking for stale preview container app: $CONTAINER_NAME" - CONTAINER_ID=$(npx wrangler containers list --json | jq -r --arg NAME "$CONTAINER_NAME" ' - map(select((.name // .application_name // .app_name // "") == $NAME)) - | first - | (.id // .container_id // .application_id // empty) - ') + preview_dir="$RUNNER_TEMP/sure-preview-worker" + rm -rf "$preview_dir" + mkdir -p "$preview_dir" - if [ -n "$CONTAINER_ID" ]; then - echo "Deleting stale preview container app $CONTAINER_NAME ($CONTAINER_ID)" - npx wrangler containers delete "$CONTAINER_ID" - else - echo "No stale preview container app found; continuing" - fi + cp trusted/workers/preview/package.json "$preview_dir/package.json" + cp trusted/workers/preview/package-lock.json "$preview_dir/package-lock.json" + cp trusted/workers/preview/tsconfig.json "$preview_dir/tsconfig.json" + cp trusted/workers/preview/wrangler.toml "$preview_dir/wrangler.toml" + cp -R pr/workers/preview/src "$preview_dir/src" - - name: Delete existing preview Worker before redeploy - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - working-directory: workers/preview - run: | - WORKER_NAME="sure-preview-${PR_NUMBER}" - echo "Ensuring fresh preview deployment for $WORKER_NAME" - npx wrangler delete --name "$WORKER_NAME" --force || echo "Existing preview not found; continuing" + sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/wrangler.toml" + sed -i "s/\${PR_NUMBER}/${PR_NUMBER}/g" "$preview_dir/src/index.ts" + sed -i \ + "s#image = \"../../Dockerfile.preview\"#image = \"${GITHUB_WORKSPACE}/pr/Dockerfile.preview\"#" \ + "$preview_dir/wrangler.toml" + + cat "$preview_dir/wrangler.toml" + cd "$preview_dir" + npm ci --ignore-scripts --no-audit --no-fund - name: Create GitHub Deployment id: deployment - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | + const prNumber = process.env.PR_NUMBER; + const headSha = process.env.HEAD_SHA; const deployment = await github.rest.repos.createDeployment({ owner: context.repo.owner, repo: context.repo.repo, - ref: context.payload.pull_request.head.sha, - environment: `preview-pr-${process.env.PR_NUMBER}`, + ref: headSha, + environment: `preview-pr-${prNumber}`, auto_merge: false, required_contexts: [], description: 'PR Preview Deployment' @@ -144,13 +143,15 @@ jobs: - name: Deploy to Cloudflare Containers id: deploy - working-directory: workers/preview env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_WORKERS_SUBDOMAIN: ${{ secrets.CLOUDFLARE_WORKERS_SUBDOMAIN }} run: | - npx wrangler deploy --var "PR_NUMBER:${PR_NUMBER}" + set -euo pipefail + + cd "$RUNNER_TEMP/sure-preview-worker" + ./node_modules/.bin/wrangler deploy --config wrangler.toml --var "PR_NUMBER:${PR_NUMBER}" # Get the deployment URL PREVIEW_URL="https://sure-preview-${PR_NUMBER}.${CLOUDFLARE_WORKERS_SUBDOMAIN}.workers.dev" @@ -165,22 +166,26 @@ jobs: - name: Update Deployment Status if: always() && steps.deployment.outputs.result - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + DEPLOYMENT_ID: ${{ steps.deployment.outputs.result }} + PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} with: script: | const state = '${{ job.status }}' === 'success' ? 'success' : 'failure'; + const previewUrl = process.env.PREVIEW_URL || undefined; await github.rest.repos.createDeploymentStatus({ owner: context.repo.owner, repo: context.repo.repo, - deployment_id: ${{ steps.deployment.outputs.result }}, + deployment_id: Number(process.env.DEPLOYMENT_ID), state: state, - environment_url: state === 'success' ? '${{ steps.deploy.outputs.preview_url }}' : undefined, + environment_url: state === 'success' ? previewUrl : undefined, description: state === 'success' ? 'Preview deployed successfully' : 'Preview deployment failed' }); - name: Comment on PR if: success() - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }} with: @@ -229,9 +234,8 @@ jobs: } - name: Store cleanup metadata if: success() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: preview-cleanup-pr-${{ env.PR_NUMBER }} - path: | - workers/preview/wrangler.toml + path: ${{ runner.temp }}/sure-preview-worker/wrangler.toml retention-days: 2 diff --git a/app/components/UI/account/chart.html.erb b/app/components/UI/account/chart.html.erb index 91fb0b69c..c14627575 100644 --- a/app/components/UI/account/chart.html.erb +++ b/app/components/UI/account/chart.html.erb @@ -4,7 +4,7 @@
<%= t(".setup_needed") %>
-<%= t(".setup_description") %>
+<%= t(".setup_description") %>
<%= render DS::Link.new( text: t(".setup_action"), icon: "plus", diff --git a/app/views/binance_items/setup_accounts.html.erb b/app/views/binance_items/setup_accounts.html.erb index 4b7ab0af5..323f2fb70 100644 --- a/app/views/binance_items/setup_accounts.html.erb +++ b/app/views/binance_items/setup_accounts.html.erb @@ -33,6 +33,18 @@<%= t(".historical_import") %>
+<%= t("settings.providers.binance_panel.sync_start_date_help") %>
+<%= t(".no_accounts") %>
@@ -69,7 +81,7 @@ <%= binance_account.currency %><%= number_with_delimiter(binance_account.current_balance || 0, delimiter: ",") %>
diff --git a/app/views/categories/_badge.html.erb b/app/views/categories/_badge.html.erb index 5570f2c70..5ab8d6c1d 100644 --- a/app/views/categories/_badge.html.erb +++ b/app/views/categories/_badge.html.erb @@ -1,7 +1,7 @@ <%# locals: (category:) %> <% category ||= Category.uncategorized %> -<%= t(".sources_label") %>
+<%= t(".sources_hint") %>
+<%= aspsp[:name] %>
<% if aspsp[:beta] %> - - <%= t(".beta_label", default: "Beta") %> - + <%= render DS::Pill.new(label: t(".beta_label", default: "Beta"), tone: :warning, marker: false) %> <% end %>+ <%= link_to import.account_statement.filename, account_statement_path(import.account_statement), class: "text-primary hover:text-primary-hover" %> +
++
<%= t("imports.document_types.#{import.document_type}", default: import.document_type&.humanize || t("imports.pdf_import.unknown_document_type", default: "Unknown")) %>
+
<%= t("imports.pdf_import.transactions_extracted_count", count: import.rows_count, default: "%{count} transactions") %>
<%= t("imports.pdf_import.select_account_hint", default: "Choose which account to import these transactions into.") %>
<% end %> <% else %> -+
<%= t("imports.pdf_import.no_accounts", default: "No accounts available. Please create an account first.") %>
<%= render DS::Link.new(text: t("imports.pdf_import.create_account", default: "Create Account"), href: new_account_path(return_to: import_path(import)), variant: "primary", full_width: true, frame: :modal) %> @@ -106,16 +115,25 @@+ <%= link_to import.account_statement.filename, account_statement_path(import.account_statement), class: "text-primary hover:text-primary-hover" %> +
++
<%= t("imports.document_types.#{import.document_type}", default: import.document_type&.humanize || t("imports.pdf_import.unknown_document_type", default: "Unknown")) %>
+
<%= import.ai_summary %>
+ <%= format_money(Money.new(investment_metrics[:period_return_trend].value, Current.family.currency)) %> + (<%= investment_metrics[:period_return_trend].percent_formatted %>) +
+ <% else %> +<%= t("reports.investment_performance.no_data") %>
+ <% end %> +Bằng cách xóa tài khoản này, bạn sẽ xóa lịch sử giá trị của nó, ảnh hưởng đến nhiều khía cạnh tài khoản tổng thể. Hành động này sẽ ảnh hưởng trực tiếp đến tính toán tài sản ròng và biểu đồ tài khoản.
Sau khi xóa, bạn không thể khôi phục thông tin tài khoản và cần thêm lại như tài khoản mới.
" + confirm_title: Xóa tài khoản? + delete_account: Xóa tài khoản + edit: Chỉnh sửa + import: Nhập giao dịch + import_trades: Nhập giao dịch chứng khoán + import_transactions: Nhập giao dịch + manage: Quản lý tài khoản + sharing: Chia sẻ + statements: Sao kê + update: + success: "Đã cập nhật tài khoản %{type}" + sidebar: + missing_data: Thiếu dữ liệu lịch sử + missing_data_description: "%{product} sử dụng nhà cung cấp bên thứ ba để lấy tỷ giá hối đoái lịch sử, giá chứng khoán và nhiều hơn nữa. Dữ liệu này cần thiết để tính số dư tài khoản lịch sử chính xác." + configure_providers: Cấu hình nhà cung cấp của bạn tại đây. + tabs: + all: Tất cả + assets: Tài sản + debts: Nợ + new_asset: Tài sản mới + new_debt: Nợ mới + new_account: Tài khoản mới + new_account_group: "Nhóm %{account_group} mới" + types: + depository: Tiền mặt + investment: Đầu tư + crypto: Tiền mã hóa + property: Bất động sản + vehicle: Phương tiện + other_asset: Tài sản khác + credit_card: Thẻ tín dụng + loan: Khoản vay + other_liability: Nợ khác + types_plural: + depository: Tiền mặt + investment: Đầu tư + crypto: Tiền mã hóa + property: Bất động sản + vehicle: Phương tiện + other_asset: Tài sản khác + credit_card: Thẻ tín dụng + loan: Khoản vay + other_liability: Nợ khác + tax_treatments: + taxable: Chịu thuế + tax_deferred: Hoãn thuế + tax_exempt: Miễn thuế + tax_advantaged: Ưu đãi thuế + tax_treatment_descriptions: + taxable: Lợi nhuận bị đánh thuế khi thực hiện + tax_deferred: Đóng góp được khấu trừ, đánh thuế khi rút + tax_exempt: Đóng góp sau thuế, lợi nhuận không bị đánh thuế + tax_advantaged: Lợi ích thuế đặc biệt có điều kiện + subtype_regions: + us: Hoa Kỳ + uk: Vương quốc Anh + ca: Canada + au: Úc + eu: Châu Âu + in: Ấn Độ + generic: Chung + confirm_unlink: + title: Hủy liên kết tài khoản với nhà cung cấp? + description_html: "Bạn đang hủy liên kết %{account_name} khỏi %{provider_name}. Tài khoản này sẽ được chuyển thành tài khoản thủ công." + warning_title: Điều này có nghĩa là gì + warning_no_sync: Tài khoản sẽ không còn tự động đồng bộ với nhà cung cấp + warning_manual_updates: Bạn cần thêm giao dịch và cập nhật số dư thủ công + warning_transactions_kept: Tất cả giao dịch và số dư hiện có sẽ được giữ nguyên + warning_can_delete: Sau khi hủy liên kết, bạn có thể xóa tài khoản nếu cần + confirm_button: Xác nhận và hủy liên kết + unlink: + success: "Tài khoản đã được hủy liên kết thành công. Bây giờ là tài khoản thủ công." + not_linked: "Tài khoản chưa được liên kết với nhà cung cấp" + error: "Hủy liên kết tài khoản thất bại: %{error}" + generic_error: "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại." + select_provider: + title: Chọn nhà cung cấp để liên kết + description: "Chọn nhà cung cấp bạn muốn dùng để liên kết %{account_name}" + already_linked: "Tài khoản đã được liên kết với nhà cung cấp" + no_providers: "Hiện không có nhà cung cấp nào được cấu hình" + + email_confirmations: + new: + invalid_token: Liên kết xác nhận không hợp lệ hoặc đã hết hạn. + success_login: Email của bạn đã được xác nhận. Vui lòng đăng nhập bằng địa chỉ email mới. diff --git a/config/locales/views/accounts/zh-CN.yml b/config/locales/views/accounts/zh-CN.yml index 3e6facff8..d07d85a33 100644 --- a/config/locales/views/accounts/zh-CN.yml +++ b/config/locales/views/accounts/zh-CN.yml @@ -3,116 +3,189 @@ zh-CN: account: entries: destroy: - success: 删除成功 + success: 记录已成功删除。 accounts: + not_authorized: 您没有权限管理此账户 account: - link_lunchflow: 关联 Lunch Flow 账户 - link_provider: 关联服务提供商 - troubleshoot: 故障排查 - unlink_provider: 解除提供商关联 + complete_setup: 完成设置 + edit: 编辑 + link_lunchflow: 连接 Lunch Flow + link_provider: 连接提供商 + unlink_provider: 取消与提供商的连接 + change_simplefin_account: 更改 SimpleFIN 账户 + troubleshoot: 排查问题 + enable: 启用账户 + disable: 停用账户 + set_default: 设为默认 + remove_default: 取消默认 + default_label: 默认 + delete: 删除账户 + sharing: 共享 chart: data_not_available: 所选时间段内无可用数据 - confirm_unlink: - confirm_button: 确认并解除关联 - description_html: 您即将解除 %{account_name} 与 %{provider_name} - 的关联。这将转换为手动账户。 - title: 确认解除账户关联? - warning_can_delete: 解除关联后,您可以在需要时删除账户 - warning_manual_updates: 您需要手动添加交易和更新余额 - warning_no_sync: 账户将不再与您的银行自动同步 - warning_title: 这意味着 - warning_transactions_kept: 所有现有交易和余额将被保留 create: - success: "%{type} 账户已创建" + success: 已创建 %{type} 账户 + set_default: + depository_only: 只有现金和信用卡账户才能设为默认。 destroy: - cannot_delete_linked: 无法删除已关联账户,请先解除关联。 - success: "%{type} 账户已计划删除" + success: "%{type} 账户已安排删除" + cannot_delete_linked: 无法删除已连接的账户,请先取消连接。 + failed: 资源删除失败,请稍后再试。 empty: - empty_message: 可通过关联服务、导入数据或手动输入的方式添加账户。 + empty_message: 您可以通过连接、导入或手动录入的方式添加账户。 new_account: 新建账户 no_accounts: 暂无账户 form: - balance: 当前余额 + balance: 该日期余额: + opening_balance_date_label: 初始余额日期 name_label: 账户名称 - name_placeholder: 例如:储蓄账户 + name_placeholder: 示例账户名称 + additional_details: 其他详情 + institution_name_label: 金融机构名称 + institution_name_placeholder: 例如:Chase Bank + institution_domain_label: 金融机构域名 + institution_domain_placeholder: 例如:chase.com + notes_label: 备注 + notes_placeholder: 存储其他信息,例如账号、排序码、IBAN、路由号码等。 index: - accounts: 账户管理 + accounts: 账户 manual_accounts: other_accounts: 其他账户 new_account: 新建账户 - sync: 同步所有账户 + sync: 全部同步 + sync_all: + syncing: 正在同步账户…… new: import_accounts: 导入账户 + container: + select: 选择 + navigate: 浏览 + close: 关闭 method_selector: - connected_entry: 关联账户 - connected_entry_eu: 关联欧盟账户 - link_with_provider: 关联 %{provider} - lunchflow_entry: 关联 Lunch Flow 账户 + connected_entry: 连接账户 + connected_entry_eu: 连接欧盟账户 + link_with_provider: 连接 %{provider} + lunchflow_entry: 连接 Lunch Flow 账户 manual_entry: 手动输入账户余额 - title: 请选择添加方式 - title: 您希望添加什么类型的账户? - select_provider: - already_linked: 账户已关联到服务提供商 - description: 请选择用于关联 %{account_name} 的服务提供商 - no_providers: 当前未配置任何服务提供商 - title: 选择关联的服务提供商 + title: 您希望如何添加? + title: 您想添加什么? show: + limited_fx_history_warning: 汇率历史仅从 %{date} 起可用。该日期之前的交易将使用近似货币换算——当外汇提供商仅提供有限历史窗口时会出现这种情况。 + tabs: + activity: 活动 + holdings: 持仓 + overview: 概览 + statements: 对账单 activity: amount: 金额 balance: 余额 + confirmed: 已确认 date: 日期 entries: 条记录 entry: 条记录 + filter: 筛选 new: 新建 - new_balance: 新增余额 - new_transaction: 新建交易 - no_entries: 未找到相关记录 + new_activity: 新活动 + new_balance: 新余额 + new_trade: 新交易 + new_transaction: 新交易记录 + new_transfer: 新转账 + no_entries: 未找到记录 + pending: 待处理 search: placeholder: 按名称搜索记录 - title: 账户活动 + search_placeholder: 按名称搜索记录 + status: 状态 + title: 活动 chart: balance: 余额 - owed: 欠款金额 + owed: 应付金额 + header: + complete_setup: 完成设置 menu: confirm_accept: 删除“%{name}” - confirm_body_html: "删除此账户将清除其历史价值数据,影响您整体账户的多个方面。此操作将直接影响您的资产净值计算和账户图表。
删除后无法恢复账户信息,您需要将其作为新账户重新添加。
" - confirm_title: 确认删除账户? + confirm_body_html: "删除此账户将清除其价值历史,影响您整体账户的多个方面。此操作会直接影响您的净资产计算和账户图表。
删除后,您将无法恢复该账户信息,因为需要将其作为新账户重新添加。
" + confirm_title: 删除账户? + delete_account: 删除账户 edit: 编辑 - import: 导入交易记录 + import: 导入交易 + import_trades: 导入交易 + import_transactions: 导入交易记录 manage: 管理账户 + sharing: 共享 + statements: 对账单 + update: + success: 已更新 %{type} 账户 sidebar: - configure_providers: 在此配置您的服务提供商。 - missing_data: 历史数据缺失 - missing_data_description: "%{product} 使用第三方服务提供商获取历史汇率、证券价格等数据。这些数据是计算准确历史账户余额所必需的。" - new_account: 新建账户 - new_account_group: 新建 %{account_group} - new_asset: 新增资产 - new_debt: 新增负债 + missing_data: 缺少历史数据 + missing_data_description: "%{product} 使用第三方提供商获取历史汇率、证券价格等数据。这些数据是计算准确历史账户余额所必需的。" + configure_providers: 请在此配置您的提供商。 tabs: all: 全部 assets: 资产 debts: 负债 - sync_all: - syncing: 账户同步中... + new_asset: 新增资产 + new_debt: 新增负债 + new_account: 新建账户 + new_account_group: 新建%{account_group} types: - credit_card: 信用卡账户 - crypto: 加密货币账户 - depository: 现金账户 - investment: 投资账户 - loan: 贷款账户 - other_asset: 其他资产账户 - other_liability: 其他负债账户 - property: 房产账户 - vehicle: 车辆账户 + depository: 现金 + investment: 投资 + crypto: 加密资产 + property: 房产 + vehicle: 车辆 + other_asset: 其他资产 + credit_card: 信用卡 + loan: 贷款 + other_liability: 其他负债 + types_plural: + depository: 现金 + investment: 投资 + crypto: 加密资产 + property: 房产 + vehicle: 车辆 + other_asset: 其他资产 + credit_card: 信用卡 + loan: 贷款 + other_liability: 其他负债 + tax_treatments: + taxable: 应税 + tax_deferred: 税延 + tax_exempt: 免税 + tax_advantaged: 税收优惠 + tax_treatment_descriptions: + taxable: 实现收益时征税 + tax_deferred: 供款可抵扣,提取时征税 + tax_exempt: 供款后税,收益不征税 + tax_advantaged: 有条件的特殊税收优惠 + subtype_regions: + us: 美国 + uk: 英国 + ca: 加拿大 + au: 澳大利亚 + eu: 欧洲 + in: 印度 + generic: 通用 + confirm_unlink: + title: 取消与提供商的连接? + description_html: 您即将把 %{account_name} 与 %{provider_name} 取消连接。这将把它转换为手动账户。 + warning_title: 这意味着什么 + warning_no_sync: 该账户将不再自动与您的提供商同步 + warning_manual_updates: 您需要手动添加交易并更新余额 + warning_transactions_kept: 所有现有交易和余额都将保留 + warning_can_delete: 取消连接后,如有需要您可以删除该账户 + confirm_button: 确认并取消连接 unlink: - error: 解除账户关联失败:%{error} - generic_error: 发生意外错误,请重试。 - not_linked: 账户未关联到任何服务提供商 - success: 账户已成功解除关联,现已成为手动账户。 - update: - success: "%{type} 账户已更新" + success: 账户已成功取消连接,现已成为手动账户。 + not_linked: 该账户未连接到任何提供商 + error: 取消连接账户失败:%{error} + generic_error: 发生意外错误,请稍后再试。 + select_provider: + title: 选择要连接的提供商 + description: 请选择您要用来连接 %{account_name} 的提供商 + already_linked: 该账户已连接到提供商 + no_providers: 当前没有可用的提供商 email_confirmations: new: invalid_token: 确认链接无效或已过期。 - success_login: 您的邮箱已确认成功。请使用新邮箱地址登录。 + success_login: 您的邮箱已确认,请使用新邮箱地址登录。 diff --git a/config/locales/views/admin/invitations/vi.yml b/config/locales/views/admin/invitations/vi.yml new file mode 100644 index 000000000..f7c81bb5e --- /dev/null +++ b/config/locales/views/admin/invitations/vi.yml @@ -0,0 +1,8 @@ +--- +vi: + admin: + invitations: + destroy: + success: "Lời mời đã được xóa." + destroy_all: + success: "Tất cả lời mời của gia đình này đã được xóa." diff --git a/config/locales/views/admin/invitations/zh-CN.yml b/config/locales/views/admin/invitations/zh-CN.yml new file mode 100644 index 000000000..d9f00d236 --- /dev/null +++ b/config/locales/views/admin/invitations/zh-CN.yml @@ -0,0 +1,8 @@ +--- +zh-CN: + admin: + invitations: + destroy: + success: 邀请已删除。 + destroy_all: + success: 此家庭的所有邀请都已删除。 diff --git a/config/locales/views/admin/sso_providers/vi.yml b/config/locales/views/admin/sso_providers/vi.yml new file mode 100644 index 000000000..906455c11 --- /dev/null +++ b/config/locales/views/admin/sso_providers/vi.yml @@ -0,0 +1,138 @@ +--- +vi: + admin: + unauthorized: "Bạn không được phép truy cập khu vực này." + sso_providers: + index: + page_title: "Nhà cung cấp SSO" + title: "Nhà cung cấp SSO" + description: "Quản lý các nhà cung cấp xác thực đăng nhập một lần cho phiên bản của bạn." + restart_required: "Các thay đổi yêu cầu khởi động lại máy chủ để có hiệu lực." + configured_providers: "Nhà cung cấp đã cấu hình" + add_provider: "Thêm nhà cung cấp" + no_providers_title: "Không có nhà cung cấp SSO" + no_providers_message: "Chưa có nhà cung cấp SSO nào được cấu hình." + note: "Các thay đổi đối với nhà cung cấp SSO yêu cầu khởi động lại máy chủ để có hiệu lực. Ngoài ra, hãy bật cờ tính năng AUTH_PROVIDERS_SOURCE=db để tải nhà cung cấp từ cơ sở dữ liệu một cách động." + enabled: "Đã bật" + disabled: "Đã tắt" + edit: "Chỉnh sửa" + enable: "Bật" + disable: "Tắt" + delete: "Xóa" + configuration_mode: "Chế độ cấu hình" + db_backed_providers: "Nhà cung cấp dựa trên cơ sở dữ liệu" + db_backed_providers_description: "Tải nhà cung cấp từ cơ sở dữ liệu thay vì cấu hình YAML" + db_backed_providers_help_html: "ĐặtAUTH_PROVIDERS_SOURCE=db để bật nhà cung cấp dựa trên cơ sở dữ liệu. Điều này cho phép thay đổi mà không cần khởi động lại máy chủ."
+ table:
+ name: "Tên"
+ strategy: "Chiến lược"
+ status: "Trạng thái"
+ issuer: "Nhà phát hành"
+ actions: "Hành động"
+ enabled: "Đã bật"
+ disabled: "Đã tắt"
+ legacy_providers_title: "Nhà cung cấp được cấu hình qua môi trường"
+ legacy_providers_notice: "Các nhà cung cấp này được cấu hình qua biến môi trường hoặc YAML và không thể quản lý thông qua giao diện này. Để quản lý ở đây, hãy di chuyển chúng sang nhà cung cấp dựa trên cơ sở dữ liệu bằng cách bật AUTH_PROVIDERS_SOURCE=db và tạo lại chúng trong giao diện."
+ env_configured: "Môi trường/YAML"
+ new:
+ title: "Thêm nhà cung cấp SSO"
+ description: "Cấu hình nhà cung cấp xác thực đăng nhập một lần mới"
+ edit:
+ title: "Chỉnh sửa nhà cung cấp SSO"
+ description: "Cập nhật cấu hình cho %{label}"
+ create:
+ success: "Nhà cung cấp SSO đã được tạo thành công."
+ update:
+ success: "Nhà cung cấp SSO đã được cập nhật thành công."
+ destroy:
+ success: "Nhà cung cấp SSO đã được xóa thành công."
+ confirm: "Bạn có chắc chắn muốn xóa nhà cung cấp này không? Hành động này không thể hoàn tác."
+ toggle:
+ success_enabled: "Nhà cung cấp SSO đã được bật thành công."
+ success_disabled: "Nhà cung cấp SSO đã được tắt thành công."
+ confirm_enable: "Bạn có chắc chắn muốn bật nhà cung cấp này không?"
+ confirm_disable: "Bạn có chắc chắn muốn tắt nhà cung cấp này không?"
+ form:
+ basic_information: "Thông tin cơ bản"
+ oauth_configuration: "Cấu hình OAuth/OIDC"
+ strategy_label: "Chiến lược"
+ strategy_help: "Chiến lược xác thực cần sử dụng"
+ strategy_openid_connect: "OpenID Connect"
+ strategy_saml: "SAML 2.0"
+ strategy_google_oauth2: "Google OAuth2"
+ strategy_github: "GitHub"
+ name_label: "Tên"
+ name_placeholder: "ví dụ: keycloak, authentik"
+ name_help: "Mã định danh duy nhất (chỉ chữ thường, số, dấu gạch dưới)"
+ label_label: "Nhãn nút"
+ label_placeholder: "ví dụ: Đăng nhập với Keycloak"
+ label_help: "Văn bản nút hiển thị cho người dùng"
+ icon_label: "Biểu tượng (tùy chọn)"
+ icon_placeholder: "ví dụ: key, shield"
+ icon_help: "Tên biểu tượng Lucide cho nút đăng nhập"
+ enabled_label: "Bật nhà cung cấp này"
+ enabled_help: "Người dùng có thể đăng nhập với nhà cung cấp này khi được bật"
+ issuer_label: "URL nhà phát hành"
+ issuer_placeholder: "https://your-idp.example.com/realms/your-realm"
+ issuer_help: "URL nhà phát hành OIDC (xác thực .well-known/openid-configuration)"
+ client_id_label: "Client ID"
+ client_id_placeholder: "your-client-id"
+ client_id_help: "OAuth client ID từ nhà cung cấp danh tính của bạn"
+ client_secret_label: "Client Secret"
+ client_secret_placeholder_new: "your-client-secret"
+ client_secret_placeholder_existing: "••••••••"
+ client_secret_help: "OAuth client secret (được mã hóa trong cơ sở dữ liệu)"
+ client_secret_help_existing: "Để trống để giữ nguyên secret hiện tại"
+ redirect_uri_label: "URL Callback"
+ redirect_uri_placeholder: "https://yourdomain.com/auth/openid_connect/callback"
+ redirect_uri_help: "Cấu hình URL này trong nhà cung cấp danh tính của bạn"
+ saml_sp_callback_url_label: "URL Callback SP (ACS URL)"
+ saml_sp_callback_url_help: "Cấu hình URL này là URL Assertion Consumer Service trong IdP của bạn"
+ copy_button: "Sao chép"
+ cancel: "Hủy"
+ submit: "Lưu nhà cung cấp"
+ create_provider: "Tạo nhà cung cấp"
+ update_provider: "Cập nhật nhà cung cấp"
+ errors_title:
+ one: "Một lỗi đã ngăn không thể lưu nhà cung cấp này:"
+ other: "%{count} lỗi đã ngăn không thể lưu nhà cung cấp này:"
+ provisioning_title: "Cấp phát người dùng"
+ default_role_label: "Vai trò mặc định cho người dùng mới"
+ default_role_help: "Vai trò được gán cho người dùng được tạo qua cấp phát tài khoản SSO tức thời (JIT). Mặc định là Thành viên."
+ role_guest: "Khách"
+ role_member: "Thành viên"
+ role_admin: "Quản trị viên"
+ role_super_admin: "Quản trị viên cấp cao"
+ role_mapping_title: "Ánh xạ nhóm sang vai trò (tùy chọn)"
+ role_mapping_help: "Ánh xạ các nhóm/claims của IdP sang vai trò ứng dụng. Người dùng được gán vai trò khớp cao nhất. Để trống để sử dụng vai trò mặc định ở trên."
+ super_admin_groups: "Nhóm quản trị viên cấp cao"
+ admin_groups: "Nhóm quản trị viên"
+ guest_groups: "Nhóm khách"
+ member_groups: "Nhóm thành viên"
+ groups_help: "Danh sách tên nhóm IdP phân cách bằng dấu phẩy. Dùng * để khớp tất cả các nhóm."
+ advanced_title: "Cài đặt OIDC nâng cao"
+ scopes_label: "Phạm vi tùy chỉnh"
+ scopes_help: "Danh sách phạm vi OIDC phân cách bằng dấu cách. Để trống cho mặc định (openid email profile). Thêm 'groups' để lấy claims nhóm."
+ prompt_label: "Lệnh nhắc xác thực"
+ prompt_default: "Mặc định (IdP quyết định)"
+ prompt_login: "Buộc đăng nhập (xác thực lại)"
+ prompt_consent: "Buộc đồng ý (ủy quyền lại)"
+ prompt_select_account: "Chọn tài khoản (chọn tài khoản)"
+ prompt_none: "Không nhắc (xác thực im lặng)"
+ prompt_help: "Kiểm soát cách IdP nhắc người dùng trong quá trình xác thực."
+ test_connection: "Kiểm tra kết nối"
+ saml_configuration: "Cấu hình SAML"
+ idp_metadata_url: "URL siêu dữ liệu IdP"
+ idp_metadata_url_help: "URL đến siêu dữ liệu SAML của IdP. Nếu được cung cấp, các cài đặt SAML khác sẽ được cấu hình tự động."
+ manual_saml_config: "Cấu hình thủ công (nếu không sử dụng URL siêu dữ liệu)"
+ manual_saml_help: "Chỉ sử dụng các cài đặt này nếu IdP của bạn không cung cấp URL siêu dữ liệu."
+ idp_sso_url: "URL SSO của IdP"
+ idp_slo_url: "URL SLO của IdP (tùy chọn)"
+ idp_certificate: "Chứng chỉ IdP"
+ idp_certificate_help: "Chứng chỉ X.509 ở định dạng PEM. Bắt buộc nếu không sử dụng URL siêu dữ liệu."
+ idp_cert_fingerprint: "Dấu vân tay chứng chỉ (thay thế)"
+ name_id_format: "Định dạng NameID"
+ name_id_email: "Địa chỉ email (mặc định)"
+ name_id_persistent: "Liên tục"
+ name_id_transient: "Tạm thời"
+ name_id_unspecified: "Không xác định"
diff --git a/config/locales/views/admin/sso_providers/zh-CN.yml b/config/locales/views/admin/sso_providers/zh-CN.yml
new file mode 100644
index 000000000..4e4faf7d7
--- /dev/null
+++ b/config/locales/views/admin/sso_providers/zh-CN.yml
@@ -0,0 +1,138 @@
+---
+zh-CN:
+ admin:
+ unauthorized: 您无权访问此区域。
+ sso_providers:
+ index:
+ page_title: SSO 提供商
+ title: SSO 提供商
+ description: 管理您实例的单点登录认证提供商。
+ restart_required: 更改需要重启服务器后才会生效。
+ configured_providers: 已配置的提供商
+ add_provider: 添加提供商
+ no_providers_title: 尚无 SSO 提供商
+ no_providers_message: 目前还没有配置任何 SSO 提供商。
+ note: 对 SSO 提供商的更改需要重启服务器后才会生效。或者启用 AUTH_PROVIDERS_SOURCE=db 功能标志以从数据库动态加载提供商。
+ enabled: 已启用
+ disabled: 已禁用
+ edit: 编辑
+ enable: 启用
+ disable: 禁用
+ delete: 删除
+ configuration_mode: 配置模式
+ db_backed_providers: 数据库提供商
+ db_backed_providers_description: 从数据库加载提供商,而不是 YAML 配置
+ db_backed_providers_help_html: 设置 AUTH_PROVIDERS_SOURCE=db 以启用数据库提供商。这允许无需重启服务器即可修改。
+ table:
+ name: 名称
+ strategy: 策略
+ status: 状态
+ issuer: 签发方
+ actions: 操作
+ enabled: 已启用
+ disabled: 已禁用
+ legacy_providers_title: 环境配置的提供商
+ legacy_providers_notice: 这些提供商通过环境变量或 YAML 配置,无法在此界面中管理。若要在此管理,请启用 AUTH_PROVIDERS_SOURCE=db 并将它们重新创建为数据库提供商。
+ env_configured: 环境/YAML
+ new:
+ title: 添加 SSO 提供商
+ description: 配置一个新的单点登录认证提供商
+ edit:
+ title: 编辑 SSO 提供商
+ description: 更新 %{label} 的配置
+ create:
+ success: SSO 提供商创建成功。
+ update:
+ success: SSO 提供商更新成功。
+ destroy:
+ success: SSO 提供商删除成功。
+ confirm: 确定要删除此提供商吗?此操作无法撤销。
+ toggle:
+ success_enabled: SSO 提供商已成功启用。
+ success_disabled: SSO 提供商已成功禁用。
+ confirm_enable: 确定要启用此提供商吗?
+ confirm_disable: 确定要禁用此提供商吗?
+ form:
+ basic_information: 基本信息
+ oauth_configuration: OAuth/OIDC 配置
+ strategy_label: 策略
+ strategy_help: 要使用的认证策略
+ strategy_openid_connect: OpenID Connect
+ strategy_saml: SAML 2.0
+ strategy_google_oauth2: Google OAuth2
+ strategy_github: GitHub
+ name_label: 名称
+ name_placeholder: 例如:keycloak、authentik
+ name_help: 唯一标识符(仅限小写字母、数字和下划线)
+ label_label: 按钮标签
+ label_placeholder: 例如:使用 Keycloak 登录
+ label_help: 显示给用户的按钮文字
+ icon_label: 图标(可选)
+ icon_placeholder: 例如:key、shield
+ icon_help: 登录按钮的 Lucide 图标名称
+ enabled_label: 启用此提供商
+ enabled_help: 启用后,用户可以使用此提供商登录
+ issuer_label: Issuer URL
+ issuer_placeholder: https://your-idp.example.com/realms/your-realm
+ issuer_help: OIDC issuer URL(会验证 .well-known/openid-configuration)
+ client_id_label: Client ID
+ client_id_placeholder: your-client-id
+ client_id_help: 来自身份提供商的 OAuth client ID
+ client_secret_label: Client Secret
+ client_secret_placeholder_new: your-client-secret
+ client_secret_placeholder_existing: ••••••••
+ client_secret_help: OAuth client secret(在数据库中加密存储)
+ client_secret_help_existing: 留空以保留现有密钥
+ redirect_uri_label: 回调 URL
+ redirect_uri_placeholder: https://yourdomain.com/auth/openid_connect/callback
+ redirect_uri_help: 在身份提供商中配置此 URL
+ saml_sp_callback_url_label: SP 回调 URL(ACS URL)
+ saml_sp_callback_url_help: 在 IdP 中将此 URL 配置为 Assertion Consumer Service URL
+ copy_button: 复制
+ cancel: 取消
+ submit: 保存提供商
+ create_provider: 创建提供商
+ update_provider: 更新提供商
+ errors_title:
+ one: 一个错误阻止了此提供商被保存:
+ other: "%{count} 个错误阻止了此提供商被保存:"
+ provisioning_title: 用户配置
+ default_role_label: 新用户的默认角色
+ default_role_help: 通过即时(JIT)SSO 账户配置创建的用户将被分配此角色。默认为成员。
+ role_guest: 访客
+ role_member: 成员
+ role_admin: 管理员
+ role_super_admin: 超级管理员
+ role_mapping_title: 组到角色映射(可选)
+ role_mapping_help: 将 IdP 组/声明映射到应用角色。用户将分配到最高匹配角色。留空则使用上面的默认角色。
+ super_admin_groups: 超级管理员组
+ admin_groups: 管理员组
+ guest_groups: 访客组
+ member_groups: 成员组
+ groups_help: 以逗号分隔的 IdP 组名列表。使用 * 匹配所有组。
+ advanced_title: 高级 OIDC 设置
+ scopes_label: 自定义范围
+ scopes_help: 以空格分隔的 OIDC scopes 列表。留空则使用默认值(openid email profile)。添加 'groups' 可获取组声明。
+ prompt_label: 认证提示
+ prompt_default: 默认(由 IdP 决定)
+ prompt_login: 强制登录(重新认证)
+ prompt_consent: 强制同意(重新授权)
+ prompt_select_account: 选择账户(选择账号)
+ prompt_none: 无提示(静默认证)
+ prompt_help: 控制 IdP 在认证时如何提示用户。
+ test_connection: 测试连接
+ saml_configuration: SAML 配置
+ idp_metadata_url: IdP 元数据 URL
+ idp_metadata_url_help: 您的 IdP 的 SAML 元数据 URL。如果提供,其余 SAML 设置将自动配置。
+ manual_saml_config: 手动配置(如果不使用元数据 URL)
+ manual_saml_help: 仅当您的 IdP 不提供元数据 URL 时才使用这些设置。
+ idp_sso_url: IdP SSO URL
+ idp_slo_url: IdP SLO URL(可选)
+ idp_certificate: IdP 证书
+ idp_certificate_help: PEM 格式的 X.509 证书。如果不使用元数据 URL,则必填。
+ idp_cert_fingerprint: 证书指纹(替代)
+ name_id_format: NameID 格式
+ name_id_email: 邮箱地址(默认)
+ name_id_persistent: 持久
+ name_id_transient: 临时
+ name_id_unspecified: 未指定
diff --git a/config/locales/views/admin/users/vi.yml b/config/locales/views/admin/users/vi.yml
new file mode 100644
index 000000000..0e1c2a2c0
--- /dev/null
+++ b/config/locales/views/admin/users/vi.yml
@@ -0,0 +1,53 @@
+---
+vi:
+ admin:
+ users:
+ index:
+ title: "Quản lý người dùng"
+ description: "Quản lý vai trò người dùng cho phiên bản của bạn. Quản trị viên cấp cao có thể truy cập cài đặt nhà cung cấp SSO và quản lý người dùng."
+ section_title: "Gia đình / Nhóm"
+ you: "(Bạn)"
+ trial_ends_at: "Kết thúc dùng thử"
+ not_available: "n/a"
+ no_users: "Không tìm thấy người dùng nào."
+ unnamed_family: "Gia đình/Nhóm chưa đặt tên"
+ no_subscription: "Không có gói đăng ký"
+ family_summary: "%{members} thành viên · %{accounts} tài khoản · %{transactions} giao dịch"
+ filters:
+ role: "Vai trò"
+ role_all: "Tất cả vai trò"
+ trial_status: "Trạng thái dùng thử"
+ trial_all: "Tất cả"
+ trial_expiring_soon: "Hết hạn trong 7 ngày"
+ trial_trialing: "Đang dùng thử"
+ submit: "Lọc"
+ summary:
+ trials_expiring_7_days: "Các bản dùng thử hết hạn trong 7 ngày tới"
+ table:
+ user: "Người dùng"
+ trial_ends_at: "Kết thúc dùng thử"
+ family_accounts: "Tài khoản gia đình"
+ family_transactions: "Giao dịch gia đình"
+ last_login: "Đăng nhập lần cuối"
+ session_count: "Số phiên"
+ never: "Chưa bao giờ"
+ role: "Vai trò"
+ role_descriptions_title: "Mô tả vai trò"
+ roles:
+ guest: "Khách"
+ member: "Thành viên"
+ admin: "Quản trị viên"
+ super_admin: "Quản trị viên cấp cao"
+ role_descriptions:
+ guest: "Trải nghiệm ưu tiên trợ lý với quyền hạn bị hạn chế có chủ ý cho quy trình giới thiệu."
+ member: "Quyền truy cập người dùng cơ bản. Có thể quản lý tài khoản, giao dịch và cài đặt của riêng họ."
+ admin: "Quản trị viên gia đình. Có thể truy cập các cài đặt nâng cao như khóa API, nhập dữ liệu và lệnh AI."
+ super_admin: "Quản trị viên phiên bản. Có thể quản lý nhà cung cấp SSO, vai trò người dùng và mạo danh người dùng để hỗ trợ."
+ invitations:
+ pending_label: "Đã mời (đang chờ)"
+ expires: "Hết hạn %{date}"
+ delete: "Xóa"
+ delete_all: "Xóa tất cả"
+ update:
+ success: "Vai trò người dùng đã được cập nhật thành công."
+ failure: "Không thể cập nhật vai trò người dùng."
diff --git a/config/locales/views/admin/users/zh-CN.yml b/config/locales/views/admin/users/zh-CN.yml
new file mode 100644
index 000000000..5338a611d
--- /dev/null
+++ b/config/locales/views/admin/users/zh-CN.yml
@@ -0,0 +1,53 @@
+---
+zh-CN:
+ admin:
+ users:
+ index:
+ title: 用户管理
+ description: 管理您实例中的用户角色。超级管理员可以访问 SSO 提供商设置和用户管理。
+ section_title: 家庭 / 组
+ you: (您)
+ trial_ends_at: 试用结束于
+ not_available: 不适用
+ no_users: 未找到用户。
+ unnamed_family: 未命名家庭/组
+ no_subscription: 无订阅
+ family_summary: "%{members} 名成员 · %{accounts} 个账户 · %{transactions} 笔交易"
+ filters:
+ role: 角色
+ role_all: 所有角色
+ trial_status: 试用状态
+ trial_all: 全部
+ trial_expiring_soon: 7 天内到期
+ trial_trialing: 试用中
+ submit: 筛选
+ summary:
+ trials_expiring_7_days: 未来 7 天内到期的试用
+ table:
+ user: 用户
+ trial_ends_at: 试用结束
+ family_accounts: 家庭账户
+ family_transactions: 家庭交易
+ last_login: 上次登录
+ session_count: 会话数
+ never: 从未
+ role: 角色
+ role_descriptions_title: 角色说明
+ roles:
+ guest: 访客
+ member: 成员
+ admin: 管理员
+ super_admin: 超级管理员
+ role_descriptions:
+ guest: 以助手优先的体验为主,并为入门流程保留有意限制的权限。
+ member: 基本用户权限。可管理自己的账户、交易和设置。
+ admin: 家庭管理员。可访问 API 密钥、导入和 AI 提示词等高级设置。
+ super_admin: 实例管理员。可管理 SSO 提供商、用户角色,并可代为登录用户进行支持。
+ invitations:
+ pending_label: 已邀请(待处理)
+ expires: 于 %{date} 过期
+ delete: 删除
+ delete_all: 全部删除
+ update:
+ success: 用户角色更新成功。
+ failure: 更新用户角色失败。
diff --git a/config/locales/views/application/vi.yml b/config/locales/views/application/vi.yml
new file mode 100644
index 000000000..c7f0c3c6e
--- /dev/null
+++ b/config/locales/views/application/vi.yml
@@ -0,0 +1,10 @@
+---
+vi:
+ number:
+ currency:
+ format:
+ delimiter: ","
+ format: "%u%n"
+ precision: 2
+ separator: "."
+ unit: "$"
diff --git a/config/locales/views/binance_items/vi.yml b/config/locales/views/binance_items/vi.yml
new file mode 100644
index 000000000..2066870ef
--- /dev/null
+++ b/config/locales/views/binance_items/vi.yml
@@ -0,0 +1,75 @@
+---
+vi:
+ binance_items:
+ create:
+ default_name: Binance
+ success: Đã kết nối thành công với Binance! Tài khoản của bạn đang được đồng bộ.
+ update:
+ success: Đã cập nhật cấu hình Binance thành công.
+ destroy:
+ success: Đã lên lịch xóa kết nối Binance.
+ setup_accounts:
+ title: Nhập tài khoản Binance
+ subtitle: Chọn danh mục đầu tư muốn theo dõi
+ instructions: Chọn các danh mục đầu tư Binance bạn muốn nhập. Chỉ hiển thị các danh mục có số dư.
+ no_accounts: Tất cả tài khoản đã được nhập.
+ accounts_count:
+ one: "%{count} tài khoản có sẵn"
+ other: "%{count} tài khoản có sẵn"
+ select_all: Chọn tất cả
+ import_selected: Nhập đã chọn
+ cancel: Hủy
+ creating: Đang nhập...
+ complete_account_setup:
+ success:
+ one: "Đã nhập %{count} tài khoản"
+ other: "Đã nhập %{count} tài khoản"
+ none_selected: Chưa chọn tài khoản nào
+ no_accounts: Không có tài khoản nào để nhập
+ binance_item:
+ provider_name: Binance
+ syncing: Đang đồng bộ...
+ reconnect: Thông tin xác thực cần cập nhật
+ deletion_in_progress: Đang xóa...
+ sync_status:
+ no_accounts: Không tìm thấy tài khoản
+ all_synced:
+ one: "%{count} tài khoản đã đồng bộ"
+ other: "%{count} tài khoản đã đồng bộ"
+ partial_sync: "%{linked_count} đã đồng bộ, %{unlinked_count} cần thiết lập"
+ status: "Đồng bộ lần cuối %{timestamp} trước"
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước - %{summary}"
+ status_never: Chưa bao giờ đồng bộ
+ update_credentials: Cập nhật thông tin xác thực
+ delete: Xóa
+ no_accounts_title: Không tìm thấy tài khoản
+ no_accounts_message: Danh mục đầu tư Binance của bạn sẽ xuất hiện ở đây sau khi đồng bộ.
+ setup_needed: Tài khoản sẵn sàng để nhập
+ setup_description: Chọn danh mục đầu tư Binance bạn muốn theo dõi.
+ setup_action: Nhập tài khoản
+ import_accounts_menu: Nhập tài khoản
+ stale_rate_warning: "Số dư là xấp xỉ — tỷ giá hối đoái chính xác cho ngày %{date} không có sẵn. Sẽ cập nhật lần đồng bộ tiếp theo."
+ select_existing_account:
+ title: Liên kết tài khoản Binance
+ no_accounts_found: Không tìm thấy tài khoản Binance nào.
+ wait_for_sync: Chờ Binance hoàn tất đồng bộ
+ check_provider_health: Kiểm tra xem thông tin API Binance của bạn có hợp lệ không
+ currently_linked_to: "Hiện đang liên kết với: %{account_name}"
+ link: Liên kết
+ cancel: Hủy
+ link_existing_account:
+ success: Đã liên kết thành công với tài khoản Binance
+ errors:
+ only_manual: Chỉ tài khoản thủ công mới có thể liên kết với Binance
+ invalid_binance_account: Tài khoản Binance không hợp lệ
+ binance_item:
+ syncer:
+ checking_credentials: Đang kiểm tra thông tin xác thực...
+ credentials_invalid: Thông tin API không hợp lệ. Vui lòng kiểm tra khóa API và bí mật của bạn.
+ importing_accounts: Đang nhập tài khoản từ Binance...
+ checking_configuration: Đang kiểm tra cấu hình tài khoản...
+ accounts_need_setup:
+ one: "%{count} tài khoản cần thiết lập"
+ other: "%{count} tài khoản cần thiết lập"
+ processing_accounts: Đang xử lý dữ liệu tài khoản...
+ calculating_balances: Đang tính toán số dư...
diff --git a/config/locales/views/binance_items/zh-CN.yml b/config/locales/views/binance_items/zh-CN.yml
new file mode 100644
index 000000000..9e62db4bc
--- /dev/null
+++ b/config/locales/views/binance_items/zh-CN.yml
@@ -0,0 +1,75 @@
+---
+zh-CN:
+ binance_items:
+ create:
+ default_name: Binance
+ success: 已成功连接到 Binance!您的账户正在同步。
+ update:
+ success: Binance 配置更新成功。
+ destroy:
+ success: Binance 连接已安排删除。
+ setup_accounts:
+ title: 导入 Binance 账户
+ subtitle: 选择要跟踪的投资组合
+ instructions: 选择您想导入的 Binance 投资组合。只有有余额的投资组合才会显示。
+ no_accounts: 所有账户都已导入。
+ accounts_count:
+ one: 有 %{count} 个账户可用
+ other: 有 %{count} 个账户可用
+ select_all: 全选
+ import_selected: 导入所选
+ cancel: 取消
+ creating: 正在导入...
+ complete_account_setup:
+ success:
+ one: 已导入 %{count} 个账户
+ other: 已导入 %{count} 个账户
+ none_selected: 未选择账户
+ no_accounts: 没有可导入的账户
+ binance_item:
+ provider_name: Binance
+ syncing: 正在同步...
+ reconnect: 凭据需要更新
+ deletion_in_progress: 正在删除...
+ sync_status:
+ no_accounts: 未找到账户
+ all_synced:
+ one: 已同步 %{count} 个账户
+ other: 已同步 %{count} 个账户
+ partial_sync: 已同步 %{linked_count} 个,%{unlinked_count} 个需要设置
+ status: "%{timestamp} 前上次同步"
+ status_with_summary: "%{timestamp} 前上次同步 - %{summary}"
+ status_never: 从未同步
+ update_credentials: 更新凭据
+ delete: 删除
+ no_accounts_title: 未找到账户
+ no_accounts_message: 您的 Binance 投资组合在同步后会显示在这里。
+ setup_needed: 账户已准备好导入
+ setup_description: 选择您想跟踪的 Binance 投资组合。
+ setup_action: 导入账户
+ import_accounts_menu: 导入账户
+ stale_rate_warning: "余额为近似值 — %{date} 的准确汇率不可用。将在下次同步时更新。"
+ select_existing_account:
+ title: 关联 Binance 账户
+ no_accounts_found: 未找到 Binance 账户。
+ wait_for_sync: 等待 Binance 完成同步
+ check_provider_health: 检查您的 Binance API 凭据是否有效
+ currently_linked_to: "当前关联到:%{account_name}"
+ link: 关联
+ cancel: 取消
+ link_existing_account:
+ success: 已成功关联到 Binance 账户
+ errors:
+ only_manual: 只有手动账户才能关联到 Binance
+ invalid_binance_account: 无效的 Binance 账户
+ binance_item:
+ syncer:
+ checking_credentials: 正在检查凭据...
+ credentials_invalid: API 凭据无效。请检查您的 API key 和 secret。
+ importing_accounts: 正在从 Binance 导入账户...
+ checking_configuration: 正在检查账户配置...
+ accounts_need_setup:
+ one: "%{count} 个账户需要设置"
+ other: "%{count} 个账户需要设置"
+ processing_accounts: 正在处理账户数据...
+ calculating_balances: 正在计算余额...
diff --git a/config/locales/views/brex_items/vi.yml b/config/locales/views/brex_items/vi.yml
new file mode 100644
index 000000000..5e44d02ec
--- /dev/null
+++ b/config/locales/views/brex_items/vi.yml
@@ -0,0 +1,277 @@
+---
+vi:
+ brex_items:
+ default_connection_name: Kết nối Brex
+ account_metadata:
+ provider: Brex
+ separator: " • "
+ kinds:
+ cash: Tiền mặt
+ card: Thẻ
+ statuses:
+ ACTIVE: Hoạt động
+ active: Hoạt động
+ CLOSED: Đã đóng
+ closed: Đã đóng
+ frozen: Bị đóng băng
+ FROZEN: Bị đóng băng
+ create:
+ success: Kết nối Brex đã được tạo thành công
+ default_card_name: Thẻ Brex
+ default_cash_name: "Tiền mặt Brex %{id}"
+ destroy:
+ success: Kết nối Brex đã được xóa
+ index:
+ title: Kết nối Brex
+ institution_summary:
+ none: Chưa kết nối tổ chức nào
+ one: "%{name}"
+ count:
+ one: "%{count} tổ chức"
+ other: "%{count} tổ chức"
+ sync_status:
+ no_accounts: Không tìm thấy tài khoản
+ all_synced:
+ one: "%{count} tài khoản đã đồng bộ"
+ other: "%{count} tài khoản đã đồng bộ"
+ partial_setup: "%{synced} đã đồng bộ, %{pending} cần thiết lập"
+ api_error:
+ common_issues: "Các vấn đề thường gặp:"
+ expired_credentials: Tạo mã thông báo API mới từ Brex.
+ expired_credentials_label: "Thông tin xác thực hết hạn:"
+ heading: Không thể kết nối với Brex
+ invalid_token: Kiểm tra mã thông báo API của bạn trong Cài đặt nhà cung cấp.
+ invalid_token_label: "Mã thông báo API không hợp lệ:"
+ network: Kiểm tra kết nối internet của bạn.
+ network_label: "Sự cố mạng:"
+ permissions: Đảm bảo mã thông báo có quyền đọc tài khoản và giao dịch cần thiết.
+ permissions_label: "Quyền không đủ:"
+ service: API Brex có thể tạm thời không khả dụng.
+ service_label: "Dịch vụ gián đoạn:"
+ settings_link: Kiểm tra Cài đặt nhà cung cấp
+ title: Lỗi kết nối Brex
+ errors:
+ unexpected_error: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau.
+ entries:
+ default_name: Giao dịch Brex
+ loading:
+ loading_message: Đang tải tài khoản Brex...
+ loading_title: Đang tải
+ link_accounts:
+ all_already_linked:
+ one: "Tài khoản đã chọn (%{names}) đã được liên kết"
+ other: "Tất cả %{count} tài khoản đã chọn đã được liên kết: %{names}"
+ api_error: "Lỗi API: %{message}"
+ invalid_account_names:
+ one: "Không thể liên kết tài khoản không có tên"
+ other: "Không thể liên kết %{count} tài khoản không có tên"
+ invalid_account_type: Loại tài khoản Brex không được hỗ trợ
+ link_failed: Liên kết tài khoản thất bại
+ no_accounts_selected: Vui lòng chọn ít nhất một tài khoản
+ no_api_token: Không tìm thấy mã thông báo API Brex. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ partial_invalid: "Đã liên kết thành công %{created_count} tài khoản, %{already_linked_count} tài khoản đã được liên kết, %{invalid_count} tài khoản có tên không hợp lệ"
+ partial_success: "Đã liên kết thành công %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết: %{already_linked_names}"
+ select_connection: Chọn kết nối Brex trước khi liên kết tài khoản.
+ success:
+ one: "Đã liên kết thành công %{count} tài khoản"
+ other: "Đã liên kết thành công %{count} tài khoản"
+ brex_item:
+ accounts_need_setup: Tài khoản cần thiết lập
+ delete: Xóa kết nối
+ deletion_in_progress: đang xóa...
+ error: Lỗi
+ no_accounts_description: Kết nối này chưa có tài khoản nào được liên kết.
+ no_accounts_title: Không có tài khoản
+ setup_action: Thiết lập tài khoản mới
+ setup_description: "%{linked} trong số %{total} tài khoản đã liên kết. Chọn loại tài khoản cho các tài khoản Brex mới được nhập."
+ setup_needed: Tài khoản mới sẵn sàng để thiết lập
+ status: "Đồng bộ %{timestamp} trước"
+ status_never: Chưa bao giờ đồng bộ
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước - %{summary}"
+ syncing: Đang đồng bộ...
+ total: Tổng cộng
+ unlinked: Chưa liên kết
+ provider_panel:
+ accounts_link: Tài khoản
+ add_connection: Thêm kết nối Brex
+ base_url_label: URL cơ sở (tùy chọn)
+ base_url_placeholder: https://api.brex.com
+ configured_html: "Đã cấu hình và sẵn sàng sử dụng. Truy cập tab %{accounts_link} để quản lý và thiết lập tài khoản."
+ connection_name_label: Tên kết nối
+ connection_name_placeholder: Tài khoản kinh doanh
+ default_connection_name: Kết nối Brex
+ disconnect_label: "Ngắt kết nối %{name}"
+ disconnect_confirm: "Ngắt kết nối %{name}?"
+ encryption_warning:
+ title: Mã hóa cơ sở dữ liệu chưa được cấu hình
+ message: Cấu hình khóa mã hóa Active Record trước khi thêm mã thông báo Brex trong môi trường sản xuất. Không có khóa mã hóa, Sure lưu trữ thông tin xác thực nhà cung cấp Brex và ảnh chụp dưới dạng văn bản thuần túy như các bản ghi nhà cung cấp khác.
+ instructions:
+ copy_token_html: "Sao chép mã thông báo và thêm làm kết nối có tên bên dưới. Sure chỉ lưu mã thông báo để đồng bộ gia đình này."
+ create_token: "Tạo mã thông báo API với các phạm vi chỉ đọc sau: accounts.cash.readonly, accounts.card.readonly, transactions.cash.readonly, transactions.card.readonly"
+ open_tokens: Đi đến cài đặt nhà phát triển/mã thông báo API Brex cho công ty bạn muốn kết nối
+ sign_in_html: "Truy cập %{link} và đăng nhập vào tài khoản bạn muốn kết nối"
+ keep_token_placeholder: Để trống để giữ mã thông báo hiện tại
+ not_configured: Chưa được cấu hình
+ sandbox_note_html: "Sử dụng kết nối có tên riêng cho mỗi công ty Brex/mã thông báo API bạn muốn đồng bộ. Để trống URL cơ sở cho môi trường sản xuất. Môi trường staging chỉ dành cho kiểm thử được Brex phê duyệt và không hoạt động với mã thông báo khách hàng."
+ setup_accounts: Thiết lập tài khoản
+ setup_title: "Hướng dẫn thiết lập:"
+ sync: Đồng bộ
+ token_label: Mã thông báo
+ token_placeholder: Dán mã thông báo vào đây
+ update_connection: Cập nhật kết nối
+ provider_connection:
+ default_description: Kết nối với tài khoản Brex của bạn
+ default_name: Brex
+ description: "Kết nối bằng %{name}"
+ name: "Brex - %{name}"
+ select_accounts:
+ accounts_selected: tài khoản đã chọn
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_brex: Không thể nhập - vui lòng cấu hình tên tài khoản trong Brex
+ description: Chọn tài khoản bạn muốn liên kết với tài khoản %{product_name} của bạn.
+ link_accounts: Liên kết tài khoản đã chọn
+ no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình mã thông báo API.
+ no_api_token: Không tìm thấy mã thông báo API Brex. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ no_credentials_configured: Vui lòng cấu hình mã thông báo API Brex trước trong Cài đặt nhà cung cấp.
+ no_name_placeholder: "(Không có tên)"
+ select_connection: Chọn kết nối Brex trong Cài đặt nhà cung cấp.
+ title: Chọn tài khoản Brex
+ unexpected_error: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau.
+ select_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ all_accounts_already_linked: Tất cả tài khoản Brex đã được liên kết
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_brex: Không thể nhập - vui lòng cấu hình tên tài khoản trong Brex
+ description: Chọn tài khoản Brex để liên kết với tài khoản này. Giao dịch sẽ được đồng bộ và loại bỏ trùng lặp tự động.
+ link_account: Liên kết tài khoản
+ no_account_specified: Chưa chỉ định tài khoản
+ no_accounts_found: Không tìm thấy tài khoản Brex. Vui lòng kiểm tra cấu hình mã thông báo API.
+ no_api_token: Không tìm thấy mã thông báo API Brex. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ no_credentials_configured: Vui lòng cấu hình mã thông báo API Brex trước trong Cài đặt nhà cung cấp.
+ no_name_placeholder: "(Không có tên)"
+ select_connection: Chọn kết nối Brex trong Cài đặt nhà cung cấp.
+ title: "Liên kết %{account_name} với Brex"
+ unexpected_error: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau.
+ setup_required:
+ description: Trước khi liên kết tài khoản Brex, bạn cần cấu hình mã thông báo API Brex.
+ heading: Mã thông báo API chưa được cấu hình
+ settings_link: Đi đến Cài đặt nhà cung cấp
+ setup_steps: "Các bước thiết lập:"
+ steps:
+ enter_token: Nhập mã thông báo API Brex của bạn
+ find_section_html: "Tìm phần Brex"
+ open_settings_html: "Đi đến Cài đặt > Nhà cung cấp"
+ return_to_link: Quay lại đây để liên kết tài khoản
+ title: Cần thiết lập Brex
+ subtype_select:
+ placeholder:
+ subtype: Chọn loại phụ
+ type: Chọn loại
+ link_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ api_error: "Lỗi API: %{message}"
+ invalid_account_name: Không thể liên kết tài khoản không có tên
+ missing_parameters: Thiếu tham số bắt buộc
+ no_account_specified: Chưa chỉ định tài khoản
+ no_api_token: Không tìm thấy mã thông báo API Brex. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ provider_account_already_linked: Tài khoản Brex này đã được liên kết với tài khoản khác
+ provider_account_not_found: Không tìm thấy tài khoản Brex
+ select_connection: Chọn kết nối Brex trước khi liên kết tài khoản.
+ success: "Đã liên kết thành công %{account_name} với Brex"
+ setup_accounts:
+ account_type_label: "Loại tài khoản:"
+ all_accounts_linked: "Tất cả tài khoản Brex của bạn đã được thiết lập."
+ api_error: "Lỗi API: %{message}"
+ fetch_failed: "Tải tài khoản thất bại"
+ no_accounts_to_setup: "Không có tài khoản nào để thiết lập"
+ no_api_token: Không tìm thấy mã thông báo API Brex. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ account_types:
+ skip: Bỏ qua tài khoản này
+ depository: Tài khoản thanh toán hoặc tiết kiệm
+ credit_card: Thẻ tín dụng
+ investment: Tài khoản đầu tư
+ loan: Vay hoặc thế chấp
+ other_asset: Tài sản khác
+ subtype_labels:
+ depository: "Loại phụ tài khoản:"
+ credit_card: ""
+ investment: "Loại đầu tư:"
+ loan: "Loại vay:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Thẻ tín dụng sẽ được tự động thiết lập dưới dạng tài khoản thẻ tín dụng."
+ other_asset: "Không cần tùy chọn bổ sung cho Tài sản khác."
+ subtypes:
+ depository:
+ checking: Tài khoản thanh toán
+ savings: Tài khoản tiết kiệm
+ hsa: Tài khoản tiết kiệm y tế
+ cd: Chứng chỉ tiền gửi
+ money_market: Thị trường tiền tệ
+ investment:
+ brokerage: Brokerage
+ pension: Lương hưu
+ retirement: Hưu trí
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: Thrift Savings Plan
+ "529_plan": "529 Plan"
+ hsa: Tài khoản tiết kiệm y tế
+ mutual_fund: Quỹ tương hỗ
+ ira: Traditional IRA
+ roth_ira: Roth IRA
+ angel: Angel
+ loan:
+ mortgage: Thế chấp
+ student: Vay sinh viên
+ auto: Vay mua xe
+ other: Vay khác
+ balance: Số dư
+ cancel: Hủy
+ choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Brex:"
+ create_accounts: Tạo tài khoản
+ creating_accounts: Đang tạo tài khoản...
+ historical_data_range: "Phạm vi dữ liệu lịch sử:"
+ subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập
+ sync_start_date_help: Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ. Tối đa 3 năm lịch sử có sẵn.
+ sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:"
+ title: Thiết lập tài khoản Brex của bạn
+ complete_account_setup:
+ all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo."
+ creation_failed: "Tạo tài khoản thất bại: %{error}"
+ creation_failed_count: "Tạo %{count} tài khoản thất bại."
+ no_accounts: "Không có tài khoản nào để thiết lập."
+ partial_skipped: "Đã tạo thành công %{created_count} tài khoản; %{skipped_count} tài khoản đã bị bỏ qua."
+ partial_success: "Đã tạo thành công %{created_count} tài khoản, nhưng %{failed_count} tài khoản thất bại."
+ success: "Đã tạo thành công %{count} tài khoản."
+ unexpected_error: Đã xảy ra lỗi không mong muốn.
+ sync:
+ success: Đã bắt đầu đồng bộ
+ syncer:
+ account_processing_failed:
+ one: "%{count} tài khoản Brex gặp lỗi khi xử lý."
+ other: "%{count} tài khoản Brex gặp lỗi khi xử lý."
+ account_sync_failed:
+ one: "%{count} đồng bộ tài khoản Brex không thể được lên lịch."
+ other: "%{count} đồng bộ tài khoản Brex không thể được lên lịch."
+ accounts_need_setup:
+ one: "%{count} tài khoản cần thiết lập..."
+ other: "%{count} tài khoản cần thiết lập..."
+ accounts_failed:
+ one: "%{count} tài khoản Brex nhập thất bại."
+ other: "%{count} tài khoản Brex nhập thất bại."
+ calculating_balances: Đang tính toán số dư...
+ checking_account_configuration: Đang kiểm tra cấu hình tài khoản...
+ credentials_invalid: Mã thông báo API Brex hoặc quyền tài khoản không hợp lệ
+ failed: Đồng bộ thất bại. Vui lòng thử lại hoặc liên hệ hỗ trợ.
+ import_failed: Nhập Brex thất bại.
+ importing_accounts: Đang nhập tài khoản từ Brex...
+ processing_transactions: Đang xử lý giao dịch...
+ transactions_failed:
+ one: "%{count} tài khoản Brex gặp lỗi nhập giao dịch."
+ other: "%{count} tài khoản Brex gặp lỗi nhập giao dịch."
+ update:
+ success: Kết nối Brex đã được cập nhật
diff --git a/config/locales/views/brex_items/zh-CN.yml b/config/locales/views/brex_items/zh-CN.yml
new file mode 100644
index 000000000..fa8ec0a6c
--- /dev/null
+++ b/config/locales/views/brex_items/zh-CN.yml
@@ -0,0 +1,241 @@
+---
+zh-CN:
+ brex_items:
+ default_connection_name: Brex 连接
+ account_metadata:
+ provider: Brex
+ separator: " • "
+ kinds:
+ cash: 现金
+ card: 卡片
+ statuses:
+ ACTIVE: 活跃
+ active: 活跃
+ CLOSED: 已关闭
+ closed: 已关闭
+ frozen: 冻结
+ FROZEN: 冻结
+ create:
+ success: Brex 连接创建成功
+ default_card_name: Brex 卡
+ default_cash_name: "Brex 现金 %{id}"
+ destroy:
+ success: Brex 连接已移除
+ index:
+ title: Brex 连接
+ institution_summary:
+ none: 未连接任何机构
+ one: "%{name}"
+ count:
+ one: "%{count} 个机构"
+ other: "%{count} 个机构"
+ sync_status:
+ no_accounts: 未找到账户
+ all_synced:
+ one: 已同步 %{count} 个账户
+ other: 已同步 %{count} 个账户
+ partial_setup: 已同步 %{synced} 个,%{pending} 个需要设置
+ api_error:
+ common_issues: "常见问题:"
+ expired_credentials: 请从 Brex 生成新的 API token。
+ expired_credentials_label: "凭据已过期:"
+ heading: 无法连接到 Brex
+ invalid_token: 请在提供商设置中检查您的 API token。
+ invalid_token_label: "API token 无效:"
+ network: 请检查您的互联网连接。
+ network_label: "网络问题:"
+ permissions: 请确保您的 token 具有所需的只读账户和交易权限范围。
+ permissions_label: "权限不足:"
+ service: Brex API 可能暂时不可用。
+ service_label: "服务不可用:"
+ settings_link: 检查提供商设置
+ title: Brex 连接错误
+ errors:
+ unexpected_error: 发生意外错误。请稍后再试。
+ entries:
+ default_name: Brex 交易
+ loading:
+ loading_message: 正在加载 Brex 账户...
+ loading_title: 正在加载
+ link_accounts:
+ all_already_linked:
+ one: 选定账户(%{names})已关联
+ other: 所有选定的 %{count} 个账户都已关联:%{names}
+ api_error: "API 错误:%{message}"
+ invalid_account_names:
+ one: 无法关联空名称账户
+ other: 无法关联 %{count} 个空名称账户
+ invalid_account_type: 不支持的 Brex 账户类型
+ link_failed: 账户关联失败
+ no_accounts_selected: 请至少选择一个账户
+ no_api_token: 未找到 Brex API token。请在提供商设置中配置。
+ partial_invalid: "成功关联 %{created_count} 个账户,%{already_linked_count} 个账户已关联,%{invalid_count} 个账户名称无效"
+ partial_success: "成功关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联:%{already_linked_names}"
+ select_connection: 在关联账户之前请选择一个 Brex 连接。
+ success:
+ one: "成功关联 %{count} 个账户"
+ other: "成功关联 %{count} 个账户"
+ brex_item:
+ accounts_need_setup: 账户需要设置
+ delete: 删除连接
+ deletion_in_progress: 正在删除...
+ error: 错误
+ no_accounts_description: 此连接尚未关联任何账户。
+ no_accounts_title: 无账户
+ setup_action: 设置新账户
+ setup_description: 已关联 %{linked} / %{total} 个账户。请选择新导入的 Brex 账户类型。
+ setup_needed: 新账户已准备好设置
+ status: "%{timestamp} 前已同步"
+ status_never: 从未同步
+ status_with_summary: "%{timestamp} 前上次同步 - %{summary}"
+ syncing: 正在同步...
+ total: 总计
+ unlinked: 未关联
+ provider_panel:
+ accounts_link: 账户
+ add_connection: 添加 Brex 连接
+ base_url_label: Base URL(可选)
+ base_url_placeholder: https://api.brex.com
+ configured_html: "已配置并可使用。请访问 %{accounts_link} 标签页管理并设置账户。"
+ connection_name_label: 连接名称
+ connection_name_placeholder: 企业支票账户
+ default_connection_name: Brex 连接
+ disconnect_label: "断开 %{name}"
+ disconnect_confirm: "断开 %{name}?"
+ encryption_warning:
+ title: 数据库加密未配置
+ message: 在生产环境中添加 Brex token 之前,请先配置 Active Record 加密密钥。没有加密密钥时,Sure 会像其他提供商记录一样以明文存储 Brex 提供商凭据和快照。
+ instructions:
+ copy_token_html: "复制该 token,并将其作为命名连接添加到下方。Sure 只会为本家庭同步而保存该 token。"
+ create_token: "创建具有以下只读范围的 API token:accounts.cash.readonly、accounts.card.readonly、transactions.cash.readonly、transactions.card.readonly"
+ open_tokens: 前往您要连接公司的 Brex 开发者/API token 设置
+ sign_in_html: "访问 %{link} 并登录您想要连接的账户"
+ keep_token_placeholder: 留空以保留当前 token
+ not_configured: 未配置
+ sandbox_note_html: "为每个想同步的 Brex 公司/API token 使用一个单独的命名连接。生产环境请留空 Base URL。Staging 仅限 Brex 批准的测试,无法与客户 token 配合使用。"
+ setup_accounts: 设置账户
+ setup_title: "设置说明:"
+ sync: 同步
+ token_label: Token
+ token_placeholder: 在此粘贴 token
+ update_connection: 更新连接
+ provider_connection:
+ default_description: 连接到您的 Brex 账户
+ default_name: Brex
+ description: "使用 %{name} 连接"
+ name: "Brex - %{name}"
+ select_accounts:
+ accounts_selected: 已选择账户
+ api_error: "API 错误:%{message}"
+ cancel: 取消
+ configure_name_in_brex: 无法导入 - 请先在 Brex 中配置账户名称
+ description: 选择您想链接到 %{product_name} 账户的账户。
+ link_accounts: 链接所选账户
+ no_accounts_found: 未找到账户。请检查您的 API token 配置。
+ no_api_token: 未找到 Brex API token。请在提供商设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Brex API token。
+ no_name_placeholder: (无名称)
+ select_connection: 请在提供商设置中选择一个 Brex 连接。
+ title: 选择 Brex 账户
+ unexpected_error: 发生意外错误。请稍后再试。
+ select_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ all_accounts_already_linked: 所有 Brex 账户都已关联
+ api_error: "API 错误:%{message}"
+ cancel: 取消
+ configure_name_in_brex: 无法导入 - 请先在 Brex 中配置账户名称
+ description: 选择一个 Brex 账户与此账户关联。交易将自动同步并去重。
+ link_account: 关联账户
+ no_account_specified: 未指定账户
+ no_accounts_found: 未找到 Brex 账户。请检查您的 API token 配置。
+ no_api_token: 未找到 Brex API token。请在提供商设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Brex API token。
+ no_name_placeholder: (无名称)
+ select_connection: 请在提供商设置中选择一个 Brex 连接。
+ title: 将 %{account_name} 与 Brex 关联
+ unexpected_error: 发生意外错误。请稍后再试。
+ setup_required:
+ description: 在链接 Brex 账户之前,您需要先配置 Brex API token。
+ heading: 未配置 API Token
+ settings_link: 前往提供商设置
+ setup_steps: "设置步骤:"
+ steps:
+ enter_token: 输入您的 Brex API token
+ find_section_html: 找到 Brex 区块
+ open_settings_html: 前往 设置 > 提供商
+ return_to_link: 返回这里链接您的账户
+ title: 需要设置 Brex
+ subtype_select:
+ placeholder:
+ subtype: 选择子类型
+ type: 选择类型
+ link_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ api_error: "API 错误:%{message}"
+ invalid_account_name: 无法关联空名称账户
+ missing_parameters: 缺少必需参数
+ no_account_specified: 未指定账户
+ no_api_token: 未找到 Brex API token。请在提供商设置中配置。
+ provider_account_already_linked: 此 Brex 账户已关联到另一个账户
+ provider_account_not_found: 未找到 Brex 账户
+ select_connection: 在关联账户之前请选择一个 Brex 连接。
+ success: "已成功将 %{account_name} 与 Brex 关联"
+ setup_accounts:
+ account_type_label: "账户类型:"
+ all_accounts_linked: "您的所有 Brex 账户都已完成设置。"
+ api_error: "API 错误:%{message}"
+ fetch_failed: "获取账户失败"
+ no_accounts_to_setup: "没有需要设置的账户"
+ no_api_token: Brex API token 未找到。请在提供商设置中配置。
+ account_types:
+ skip: 跳过此账户
+ depository: 支票账户或储蓄账户
+ credit_card: 信用卡
+ investment: 投资账户
+ loan: 贷款或抵押贷款
+ other_asset: 其他资产
+ subtype_labels:
+ depository: "账户子类型:"
+ credit_card: ""
+ investment: "投资类型:"
+ loan: "贷款类型:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "信用卡将自动设为信用卡账户。"
+ other_asset: "其他资产无需额外选项。"
+ subtypes:
+ depository:
+ checking: 支票账户
+ savings: 储蓄账户
+ hsa: 健康储蓄账户
+ cd: 定期存单
+ money_market: 货币市场
+ investment:
+ brokerage: 券商账户
+ pension: 养老金
+ retirement: 退休账户
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: 节俭储蓄计划
+ "529_plan": "529 计划"
+ hsa: 健康储蓄账户
+ mutual_fund: 共同基金
+ ira: 传统 IRA
+ roth_ira: Roth IRA
+ angel: 天使投资
+ loan:
+ mortgage: 抵押贷款
+ student: 学生贷款
+ auto: 汽车贷款
+ other: 其他贷款
+ balance: 余额
+ cancel: 取消
+ choose_account_type: "为每个 Brex 账户选择正确的账户类型:"
+ create_accounts: 创建账户
+ creating_accounts: 正在创建账户...
+ historical_data_range: "历史数据范围:"
+ subtitle: 为您导入的账户选择正确的账户类型
+ sync_start_date_help: 选择您希望向前同步交易历史的时间范围。最多可用 3 年历史记录。
+ sync_start_date_label: "从以下日期开始同步交易:"
+ title: 设置您的 Brex 账户
diff --git a/config/locales/views/budgets/vi.yml b/config/locales/views/budgets/vi.yml
new file mode 100644
index 000000000..f2f8e59cb
--- /dev/null
+++ b/config/locales/views/budgets/vi.yml
@@ -0,0 +1,97 @@
+---
+vi:
+ budgets:
+ budget_donut:
+ spent: "Đã chi"
+ new_budget: "Ngân sách mới"
+ of_budget: "trong %{amount}"
+ unused: "Chưa dùng"
+ budget_header:
+ today: "Hôm nay"
+ budget_nav:
+ categories: Danh mục
+ setup: Thiết lập
+ over_allocation_warning:
+ over_allocated_message: "Bạn đã phân bổ vượt ngân sách. Vui lòng điều chỉnh lại."
+ fix_allocations: "Sửa phân bổ"
+ actuals_summary:
+ income: "Thu nhập"
+ expenses: "Chi tiêu"
+ budgeted_summary:
+ expected_income: "Thu nhập dự kiến"
+ budgeted: "Đã lập ngân sách"
+ earned: "%{amount} đã kiếm được"
+ over: "%{amount} vượt"
+ left: "%{amount} còn lại"
+ spent: "%{amount} đã chi"
+ edit:
+ setup_title: "Thiết lập ngân sách"
+ setup_description: "Nhập thu nhập hàng tháng và chi tiêu kế hoạch bên dưới để thiết lập ngân sách."
+ budgeted_spending: "Chi tiêu theo ngân sách"
+ expected_income: "Thu nhập dự kiến"
+ autosuggest_title: "Tự động đề xuất ngân sách thu nhập & chi tiêu"
+ autosuggest_description: "Dựa trên lịch sử giao dịch. AI có thể mắc lỗi, hãy xác minh trước khi tiếp tục."
+ continue: "Tiếp tục"
+ name:
+ custom_range: "%{start} - %{end_date}"
+ month_year: "%{month}"
+ show:
+ categories:
+ amount: Số tiền
+ edit: Chỉnh sửa
+ title: Danh mục
+ on_track_categories:
+ short_title: Đúng kế hoạch
+ title: Đúng kế hoạch
+ over_budget_categories:
+ short_title: Vượt ngân sách
+ title: Vượt ngân sách
+ filter:
+ all: Tất cả
+ on_track: Đúng kế hoạch
+ over_budget: Vượt ngân sách
+ tabs:
+ actual: Thực tế
+ budgeted: Ngân sách
+ copy_previous_prompt:
+ title: "Thiết lập ngân sách"
+ description: "Bạn có thể sao chép ngân sách từ %{source_name} hoặc bắt đầu mới."
+ copy_button: "Sao chép từ %{source_name}"
+ fresh_button: "Bắt đầu mới"
+ copy_previous:
+ success: "Đã sao chép ngân sách từ %{source_name}"
+ no_source: "Không tìm thấy ngân sách trước để sao chép"
+ already_initialized: "Ngân sách này đã được thiết lập"
+ budget_categories:
+ allocation_progress:
+ budget_exceeded_html: 'Vượt ngân sách %{amount}'
+ left_to_allocate: còn lại để phân bổ
+ over_set: "> 100% đã đặt"
+ percent_set: "%{percent} đã đặt"
+ budget_category_form:
+ monthly_average: "%{amount}/tháng tb"
+ shared_placeholder: Chia sẻ
+ shared_title: Để trống để dùng chung ngân sách của danh mục cha
+ confirm_button:
+ confirm: "Xác nhận"
+ no_categories:
+ oops: "Ối!"
+ no_categories_message: "Bạn chưa tạo hoặc gán danh mục chi tiêu nào cho giao dịch."
+ use_defaults: "Dùng mặc định (khuyến nghị)"
+ new_category: "Danh mục mới"
+ index:
+ title: "Chỉnh sửa ngân sách danh mục"
+ description: "Điều chỉnh ngân sách danh mục để đặt giới hạn chi tiêu. Số tiền chưa phân bổ sẽ tự động gán là chưa phân loại."
+ show:
+ category: "Danh mục"
+ overview: "Tổng quan"
+ spending: "Chi tiêu %{date}"
+ status: "Trạng thái"
+ overspent: "chi quá"
+ left: "còn lại"
+ budgeted: "Ngân sách"
+ monthly_average_spending: "Chi tiêu trung bình hàng tháng"
+ monthly_median_spending: "Chi tiêu trung vị hàng tháng"
+ recent_transactions: "Giao dịch gần đây"
+ view_all_transactions: "Xem tất cả giao dịch trong danh mục"
+ no_transactions: "Không tìm thấy giao dịch trong kỳ ngân sách này."
diff --git a/config/locales/views/budgets/zh-CN.yml b/config/locales/views/budgets/zh-CN.yml
index e111d4d91..e32bfb743 100644
--- a/config/locales/views/budgets/zh-CN.yml
+++ b/config/locales/views/budgets/zh-CN.yml
@@ -1,6 +1,40 @@
---
zh-CN:
budgets:
+ budget_donut:
+ spent: 已花费
+ new_budget: 新预算
+ of_budget: 占 %{amount}
+ unused: 未使用
+ budget_header:
+ today: 今天
+ budget_nav:
+ categories: 分类
+ setup: 设置
+ over_allocation_warning:
+ over_allocated_message: 您的预算分配超出了上限。请修正您的分配。
+ fix_allocations: 修正分配
+ actuals_summary:
+ income: 收入
+ expenses: 支出
+ budgeted_summary:
+ expected_income: 预期收入
+ budgeted: 预算
+ earned: 已赚 %{amount}
+ over: 超出 %{amount}
+ left: 剩余 %{amount}
+ spent: 已花 %{amount}
+ edit:
+ setup_title: 设置您的预算
+ setup_description: 在下方输入您的月收入和计划支出以设置预算。
+ budgeted_spending: 预算支出
+ expected_income: 预期收入
+ autosuggest_title: 自动建议收入与支出预算
+ autosuggest_description: 这将基于交易历史。AI 可能出错,请在继续前核对。
+ continue: 继续
+ name:
+ custom_range: "%{start} - %{end_date}"
+ month_year: "%{month}"
show:
categories:
amount: 金额
@@ -10,12 +44,54 @@ zh-CN:
short_title: 正常
title: 正常
over_budget_categories:
- short_title: 超预算
- title: 超预算
+ short_title: 超支
+ title: 超支
filter:
all: 全部
on_track: 正常
- over_budget: 超预算
+ over_budget: 超支
tabs:
actual: 实际
budgeted: 预算
+ copy_previous_prompt:
+ title: 设置您的预算
+ description: 您可以复制 %{source_name} 的预算,或从头开始。
+ copy_button: 复制 %{source_name}
+ fresh_button: 从头开始
+ copy_previous:
+ success: 已从 %{source_name} 复制预算
+ no_source: 未找到可复制的上一个预算
+ already_initialized: 此预算已设置过
+ budget_categories:
+ allocation_progress:
+ budget_exceeded_html: '预算超出 %{amount}'
+ left_to_allocate: 待分配
+ over_set: 超过 100%
+ percent_set: 已设置 %{percent}
+ budget_category_form:
+ monthly_average: 月均 %{amount}
+ shared_placeholder: 共享
+ shared_title: 留空以共享父级预算
+ confirm_button:
+ confirm: 确认
+ no_categories:
+ oops: 哎呀!
+ no_categories_message: 您还没有为交易创建或分配任何支出分类。
+ use_defaults: 使用默认值(推荐)
+ new_category: 新分类
+ index:
+ title: 编辑您的分类预算
+ description: 调整分类预算以设置支出限额。未分配资金将自动归入未分类。
+ show:
+ category: 分类
+ overview: 概览
+ spending: "%{date} 支出"
+ status: 状态
+ overspent: 超支
+ left: 剩余
+ budgeted: 预算
+ monthly_average_spending: 月均支出
+ monthly_median_spending: 月度中位支出
+ recent_transactions: 最近交易
+ view_all_transactions: 查看该分类的全部交易
+ no_transactions: 此预算周期内未找到交易。
diff --git a/config/locales/views/categories/en.yml b/config/locales/views/categories/en.yml
index 0422c07cf..18ae3170e 100644
--- a/config/locales/views/categories/en.yml
+++ b/config/locales/views/categories/en.yml
@@ -30,11 +30,28 @@ en:
categories_incomes: Income categories
delete_all: Delete all
empty: No categories found
+ merge: Merge categories
new: New category
+ merge:
+ title: Merge categories
+ description: Select a target category and the categories to merge into it. Matching transactions and budget lines will move to the target.
+ target_label: Merge into (target)
+ select_target: Select target category...
+ sources_label: Categories to merge
+ sources_hint: Selected categories will be deleted after their transactions and budget lines move to the target. Do not select the target as a source.
+ submit: Merge selected
menu:
loading: Loading...
new:
new_category: New category
+ perform_merge:
+ success:
+ one: Successfully merged %{count} category
+ other: Successfully merged %{count} categories
+ no_categories_selected: No categories selected to merge
+ target_not_found: Target category not found
+ invalid_categories: Invalid categories selected
+ target_selected_as_source: Choose different categories for the target and sources.
update:
success: Category updated successfully
virtual:
diff --git a/config/locales/views/categories/vi.yml b/config/locales/views/categories/vi.yml
new file mode 100644
index 000000000..596f848d8
--- /dev/null
+++ b/config/locales/views/categories/vi.yml
@@ -0,0 +1,52 @@
+---
+vi:
+ categories:
+ bootstrap:
+ success: Đã tạo danh mục mặc định thành công
+ category:
+ delete: Xóa danh mục
+ edit: Chỉnh sửa danh mục
+ create:
+ success: Đã tạo danh mục thành công
+ destroy:
+ success: Đã xóa danh mục thành công
+ edit:
+ edit: Chỉnh sửa danh mục
+ form:
+ placeholder: Tên danh mục
+ name_label: Tên
+ unassigned: "(chưa gán)"
+ parent_category_label: "Danh mục cha (tùy chọn)"
+ color: Màu sắc
+ icon: Biểu tượng
+ auto_adjust: tự động điều chỉnh.
+ poor_contrast: "Độ tương phản kém, chọn màu tối hơn hoặc"
+ destroy_all:
+ success: Đã xóa tất cả danh mục
+ index:
+ bootstrap: Dùng mặc định (khuyến nghị)
+ categories: Danh mục
+ categories_expenses: Danh mục chi tiêu
+ categories_incomes: Danh mục thu nhập
+ delete_all: Xóa tất cả
+ empty: Không tìm thấy danh mục
+ new: Danh mục mới
+ menu:
+ loading: Đang tải...
+ new:
+ new_category: Danh mục mới
+ update:
+ success: Đã cập nhật danh mục thành công
+ virtual:
+ transfer: Chuyển khoản
+ payment: Thanh toán
+ trade: Giao dịch chứng khoán
+ category:
+ dropdowns:
+ show:
+ bootstrap: Tạo danh mục mặc định
+ empty: Không tìm thấy danh mục
+ match_transfer: "Khớp chuyển khoản/thanh toán"
+ one_time: "Một lần %{type}"
+ income: "thu nhập"
+ expense: "chi tiêu"
diff --git a/config/locales/views/categories/zh-CN.yml b/config/locales/views/categories/zh-CN.yml
index 973b4277a..785587249 100644
--- a/config/locales/views/categories/zh-CN.yml
+++ b/config/locales/views/categories/zh-CN.yml
@@ -2,7 +2,7 @@
zh-CN:
categories:
bootstrap:
- success: 默认分类已创建成功
+ success: 默认分类创建成功
category:
delete: 删除分类
edit: 编辑分类
@@ -14,21 +14,39 @@ zh-CN:
edit: 编辑分类
form:
placeholder: 分类名称
+ name_label: 名称
+ unassigned: (未分配)
+ parent_category_label: 父分类(可选)
+ color: 颜色
+ icon: 图标
+ auto_adjust: 自动调整。
+ poor_contrast: 对比度较差,请选择更深的颜色或
+ destroy_all:
+ success: 所有分类已删除
index:
- bootstrap: 使用默认分类(推荐)
- categories: 分类管理
+ bootstrap: 使用默认值(推荐)
+ categories: 分类
categories_expenses: 支出分类
categories_incomes: 收入分类
- empty: 暂无分类
+ delete_all: 全部删除
+ empty: 未找到分类
new: 新建分类
menu:
- loading: 加载中...
+ loading: 正在加载...
new:
new_category: 新建分类
update:
success: 分类更新成功
+ virtual:
+ transfer: 转账
+ payment: 付款
+ trade: 交易
category:
dropdowns:
show:
bootstrap: 生成默认分类
- empty: 暂无分类
+ empty: 未找到分类
+ match_transfer: 匹配转账/付款
+ one_time: 一次性%{type}
+ income: 收入
+ expense: 支出
diff --git a/config/locales/views/category/deletions/vi.yml b/config/locales/views/category/deletions/vi.yml
new file mode 100644
index 000000000..3f22eae69
--- /dev/null
+++ b/config/locales/views/category/deletions/vi.yml
@@ -0,0 +1,13 @@
+---
+vi:
+ category:
+ deletions:
+ create:
+ success: Danh mục giao dịch đã được xóa thành công
+ new:
+ category: Danh mục
+ delete_and_leave_uncategorized: Xóa "%{category_name}" và để chưa phân loại
+ delete_and_recategorize: Xóa "%{category_name}" và gán danh mục mới
+ delete_category: Xóa danh mục?
+ explanation: Bằng cách xóa danh mục này, mọi giao dịch có danh mục "%{category_name}" sẽ trở thành chưa phân loại. Thay vì để chúng không được phân loại, bạn cũng có thể gán một danh mục mới bên dưới.
+ replacement_category_prompt: Chọn danh mục
diff --git a/config/locales/views/category/dropdowns/vi.yml b/config/locales/views/category/dropdowns/vi.yml
new file mode 100644
index 000000000..04cf03947
--- /dev/null
+++ b/config/locales/views/category/dropdowns/vi.yml
@@ -0,0 +1,11 @@
+---
+vi:
+ category:
+ dropdowns:
+ row:
+ delete: Xóa danh mục
+ edit: Chỉnh sửa danh mục
+ show:
+ clear: Xóa danh mục
+ no_categories: Không tìm thấy danh mục nào
+ search_placeholder: Tìm kiếm
diff --git a/config/locales/views/chats/vi.yml b/config/locales/views/chats/vi.yml
new file mode 100644
index 000000000..654b0fbbb
--- /dev/null
+++ b/config/locales/views/chats/vi.yml
@@ -0,0 +1,44 @@
+---
+vi:
+ chats:
+ demo_banner_title: "Chế độ Demo đang hoạt động"
+ demo_banner_message: "Bạn đang sử dụng LLM thông qua tín dụng do Cloudflare Workers AI cung cấp. Kết quả có thể thay đổi vì codebase được thử nghiệm trên `gpt-4.1` nhưng token của bạn không được sử dụng để đào tạo mô hình! 🤖"
+ thinking: "Đang xử lý ..."
+ ai_greeting:
+ greeting: "Xin chào %{name}! Tôi là một AI/mô hình ngôn ngữ lớn có thể hỗ trợ bạn về tài chính. Tôi có quyền truy cập internet và dữ liệu tài khoản của bạn."
+ there: "bạn"
+ commands_hint_html: "Bạn có thể sử dụng / để truy cập các lệnh"
+ questions_intro: "Dưới đây là một số câu hỏi bạn có thể đặt:"
+ evaluate_portfolio: "Đánh giá danh mục đầu tư"
+ spending_insights: "Xem thông tin chi tiêu"
+ unusual_patterns: "Tìm các giao dịch bất thường"
+ chat:
+ edit_chat_title: "Chỉnh sửa tiêu đề cuộc trò chuyện"
+ delete_chat: "Xóa cuộc trò chuyện"
+ chat_nav:
+ all_chats: "Tất cả cuộc trò chuyện"
+ start_new_chat: "Bắt đầu cuộc trò chuyện mới"
+ edit_chat_title: "Chỉnh sửa tiêu đề cuộc trò chuyện"
+ delete_chat: "Xóa cuộc trò chuyện"
+ error:
+ retry: "Thử lại"
+ destroy:
+ notice: "Cuộc trò chuyện đã được xóa thành công"
+ index:
+ chats: "Cuộc trò chuyện"
+ new_chat: "Cuộc trò chuyện mới"
+ update:
+ success: "Cuộc trò chuyện đã được cập nhật"
+ ai_consent:
+ title: "Bật tính năng Trò chuyện AI"
+ available_description: "Trò chuyện AI có thể trả lời các câu hỏi tài chính và cung cấp thông tin dựa trên dữ liệu của bạn. Để sử dụng tính năng này, bạn cần bật nó một cách rõ ràng."
+ unavailable_description_html: "Để sử dụng trợ lý AI, bạn cần đặt biến môi trường OPENAI_ACCESS_TOKEN hoặc cấu hình nó trong phần Cài đặt Self-Hosting của phiên bản của bạn."
+ enable_button: "Bật Trò chuyện AI"
+ disable_note: "Tắt bất kỳ lúc nào. Tất cả dữ liệu gửi đến các nhà cung cấp LLM của chúng tôi đều được ẩn danh hóa."
+ assistant_messages:
+ assistant_message:
+ assistant_reasoning: "Lý luận của trợ lý"
+ tool_calls:
+ tool_calls: "Lệnh gọi công cụ"
+ function: "Hàm:"
+ arguments: "Đối số:"
diff --git a/config/locales/views/chats/zh-CN.yml b/config/locales/views/chats/zh-CN.yml
index d2f541a70..12eaf0318 100644
--- a/config/locales/views/chats/zh-CN.yml
+++ b/config/locales/views/chats/zh-CN.yml
@@ -1,6 +1,44 @@
---
zh-CN:
chats:
- demo_banner_message: "您正在使用由 Cloudflare Workers AI 提供额度的开源权重 Qwen3 大语言模型。由于代码库主要在 `gpt-4.1` 上测试,结果可能有所不同,但您的令牌不会被用于其他地方进行训练!🤖"
- demo_banner_title: 演示模式已激活
- thinking: "处理中 ..."
+ demo_banner_title: 演示模式已启用
+ demo_banner_message: 您正在通过 Cloudflare Workers AI 提供的额度使用 LLM。结果可能会有所不同,因为代码库是在 `gpt-4.1` 上测试的,而您的 token 不会被用于其他训练!🤖
+ thinking: 思考中……
+ ai_greeting:
+ greeting: "%{name},你好!我是一个 AI/大语言模型,可以帮您处理财务问题。我可以访问网络和您的账户数据。"
+ there: 朋友
+ commands_hint_html: 您可以使用 / 来访问命令
+ questions_intro: 这里有几个您可以提的问题:
+ evaluate_portfolio: 评估投资组合
+ spending_insights: 查看消费洞察
+ unusual_patterns: 查找异常模式
+ chat:
+ edit_chat_title: 编辑聊天标题
+ delete_chat: 删除聊天
+ chat_nav:
+ all_chats: 所有聊天
+ start_new_chat: 开始新聊天
+ edit_chat_title: 编辑聊天标题
+ delete_chat: 删除聊天
+ error:
+ retry: 重试
+ destroy:
+ notice: 聊天已成功删除
+ index:
+ chats: 聊天
+ new_chat: 新建聊天
+ update:
+ success: 聊天已更新
+ ai_consent:
+ title: 启用 AI 聊天
+ available_description: AI 聊天可以基于您的数据回答财务问题并提供洞察。要使用此功能,您需要显式启用它。
+ unavailable_description_html: 要使用 AI 助手,您需要设置 OPENAI_ACCESS_TOKEN 环境变量,或在您的实例的自托管设置中进行配置。
+ enable_button: 启用 AI 聊天
+ disable_note: 可随时关闭。发送给我们的 LLM 提供商的所有数据都会被匿名化。
+ assistant_messages:
+ assistant_message:
+ assistant_reasoning: 助手推理
+ tool_calls:
+ tool_calls: 工具调用
+ function: 函数:
+ arguments: 参数:
diff --git a/config/locales/views/coinbase_items/vi.yml b/config/locales/views/coinbase_items/vi.yml
new file mode 100644
index 000000000..342631fd0
--- /dev/null
+++ b/config/locales/views/coinbase_items/vi.yml
@@ -0,0 +1,78 @@
+---
+vi:
+ coinbase_items:
+ create:
+ default_name: Coinbase
+ success: Đã kết nối thành công với Coinbase! Các tài khoản của bạn đang được đồng bộ.
+ update:
+ success: Đã cập nhật cấu hình Coinbase thành công.
+ destroy:
+ success: Đã lên lịch xóa kết nối Coinbase.
+ setup_accounts:
+ title: Nhập ví Coinbase
+ subtitle: Chọn ví muốn theo dõi
+ instructions: Chọn các ví bạn muốn nhập. Các ví chưa chọn vẫn có sẵn nếu bạn muốn thêm sau.
+ no_accounts: Tất cả ví đã được nhập.
+ accounts_count:
+ one: "%{count} ví có sẵn"
+ other: "%{count} ví có sẵn"
+ select_all: Chọn tất cả
+ import_selected: Nhập đã chọn
+ cancel: Hủy
+ creating: Đang nhập...
+ complete_account_setup:
+ success:
+ one: "Đã nhập %{count} ví"
+ other: "Đã nhập %{count} ví"
+ none_selected: Chưa chọn ví nào
+ no_accounts: Không có ví nào để nhập
+ coinbase_item:
+ provider_name: Coinbase
+ syncing: Đang đồng bộ...
+ reconnect: Thông tin xác thực cần cập nhật
+ deletion_in_progress: Đang xóa...
+ sync_status:
+ no_accounts: Không tìm thấy tài khoản
+ all_synced:
+ one: "%{count} tài khoản đã đồng bộ"
+ other: "%{count} tài khoản đã đồng bộ"
+ partial_sync: "%{linked_count} đã đồng bộ, %{unlinked_count} cần thiết lập"
+ status: "Đồng bộ lần cuối %{timestamp} trước"
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước - %{summary}"
+ status_never: Chưa bao giờ đồng bộ
+ update_credentials: Cập nhật thông tin xác thực
+ delete: Xóa
+ no_accounts_title: Không tìm thấy tài khoản
+ no_accounts_message: Các ví Coinbase của bạn sẽ xuất hiện ở đây sau khi đồng bộ.
+ setup_needed: Ví sẵn sàng để nhập
+ setup_description: Chọn ví Coinbase bạn muốn theo dõi.
+ setup_action: Nhập ví
+ import_wallets_menu: Nhập ví
+ more_wallets_available:
+ one: "%{count} ví khác có sẵn để nhập"
+ other: "%{count} ví khác có sẵn để nhập"
+ select_existing_account:
+ title: Liên kết tài khoản Coinbase
+ no_accounts_found: Không tìm thấy tài khoản Coinbase nào.
+ wait_for_sync: Chờ Coinbase hoàn tất đồng bộ
+ check_provider_health: Kiểm tra xem thông tin API Coinbase của bạn có hợp lệ không
+ balance: Số dư
+ currently_linked_to: "Hiện đang liên kết với: %{account_name}"
+ link: Liên kết
+ cancel: Hủy
+ link_existing_account:
+ success: Đã liên kết thành công với tài khoản Coinbase
+ errors:
+ only_manual: Chỉ tài khoản thủ công mới có thể liên kết với Coinbase
+ invalid_coinbase_account: Tài khoản Coinbase không hợp lệ
+ coinbase_item:
+ syncer:
+ checking_credentials: Đang kiểm tra thông tin xác thực...
+ credentials_invalid: Thông tin API không hợp lệ. Vui lòng kiểm tra khóa API và bí mật của bạn.
+ importing_accounts: Đang nhập tài khoản từ Coinbase...
+ checking_configuration: Đang kiểm tra cấu hình tài khoản...
+ accounts_need_setup:
+ one: "%{count} tài khoản cần thiết lập"
+ other: "%{count} tài khoản cần thiết lập"
+ processing_accounts: Đang xử lý dữ liệu tài khoản...
+ calculating_balances: Đang tính toán số dư...
diff --git a/config/locales/views/coinbase_items/zh-CN.yml b/config/locales/views/coinbase_items/zh-CN.yml
new file mode 100644
index 000000000..38f31ba11
--- /dev/null
+++ b/config/locales/views/coinbase_items/zh-CN.yml
@@ -0,0 +1,78 @@
+---
+zh-CN:
+ coinbase_items:
+ create:
+ default_name: Coinbase
+ success: 已成功连接到 Coinbase!您的账户正在同步。
+ update:
+ success: Coinbase 配置更新成功。
+ destroy:
+ success: Coinbase 连接已安排删除。
+ setup_accounts:
+ title: 导入 Coinbase 钱包
+ subtitle: 选择要跟踪的钱包
+ instructions: 选择您想导入的钱包。未选择的钱包以后仍可继续添加。
+ no_accounts: 所有钱包都已导入。
+ accounts_count:
+ one: 有 %{count} 个钱包可用
+ other: 有 %{count} 个钱包可用
+ select_all: 全选
+ import_selected: 导入所选
+ cancel: 取消
+ creating: 正在导入...
+ complete_account_setup:
+ success:
+ one: 已导入 %{count} 个钱包
+ other: 已导入 %{count} 个钱包
+ none_selected: 未选择钱包
+ no_accounts: 没有可导入的钱包
+ coinbase_item:
+ provider_name: Coinbase
+ syncing: 正在同步...
+ reconnect: 凭据需要更新
+ deletion_in_progress: 正在删除...
+ sync_status:
+ no_accounts: 未找到账户
+ all_synced:
+ one: 已同步 %{count} 个账户
+ other: 已同步 %{count} 个账户
+ partial_sync: 已同步 %{linked_count} 个,%{unlinked_count} 个需要设置
+ status: "%{timestamp} 前上次同步"
+ status_with_summary: "%{timestamp} 前上次同步 - %{summary}"
+ status_never: 从未同步
+ update_credentials: 更新凭据
+ delete: 删除
+ no_accounts_title: 未找到账户
+ no_accounts_message: 您的 Coinbase 钱包在同步后会显示在这里。
+ setup_needed: 钱包已准备好导入
+ setup_description: 选择您想跟踪的 Coinbase 钱包。
+ setup_action: 导入钱包
+ import_wallets_menu: 导入钱包
+ more_wallets_available:
+ one: 还有 %{count} 个钱包可导入
+ other: 还有 %{count} 个钱包可导入
+ select_existing_account:
+ title: 关联 Coinbase 账户
+ no_accounts_found: 未找到 Coinbase 账户。
+ wait_for_sync: 等待 Coinbase 完成同步
+ check_provider_health: 检查您的 Coinbase API 凭据是否有效
+ balance: 余额
+ currently_linked_to: "当前关联到:%{account_name}"
+ link: 关联
+ cancel: 取消
+ link_existing_account:
+ success: 已成功关联到 Coinbase 账户
+ errors:
+ only_manual: 只有手动账户才能关联到 Coinbase
+ invalid_coinbase_account: 无效的 Coinbase 账户
+ coinbase_item:
+ syncer:
+ checking_credentials: 正在检查凭据...
+ credentials_invalid: API 凭据无效。请检查您的 API key 和 secret。
+ importing_accounts: 正在从 Coinbase 导入账户...
+ checking_configuration: 正在检查账户配置...
+ accounts_need_setup:
+ one: "%{count} 个账户需要设置"
+ other: "%{count} 个账户需要设置"
+ processing_accounts: 正在处理账户数据...
+ calculating_balances: 正在计算余额...
diff --git a/config/locales/views/coinstats_items/vi.yml b/config/locales/views/coinstats_items/vi.yml
new file mode 100644
index 000000000..5ec0b638e
--- /dev/null
+++ b/config/locales/views/coinstats_items/vi.yml
@@ -0,0 +1,75 @@
+---
+vi:
+ coinstats_items:
+ create:
+ success: Kết nối nhà cung cấp CoinStats đã được cấu hình thành công.
+ default_name: Kết nối CoinStats
+ errors:
+ validation_failed: "Xác thực thất bại: %{message}."
+ update:
+ success: Kết nối nhà cung cấp CoinStats đã được cập nhật thành công.
+ errors:
+ validation_failed: "Xác thực thất bại: %{message}."
+ destroy:
+ success: Kết nối nhà cung cấp CoinStats đã được lên lịch xóa.
+ link_wallet:
+ success: "%{count} ví tiền điện tử đã được liên kết thành công."
+ missing_params: "Thiếu tham số bắt buộc: địa chỉ và blockchain."
+ failed: Liên kết ví tiền điện tử thất bại.
+ error: "Liên kết ví tiền điện tử thất bại: %{message}."
+ link_exchange:
+ success: "%{name} sàn giao dịch đã được liên kết."
+ missing_params: Sàn giao dịch và thông tin xác thực là bắt buộc.
+ invalid_exchange: Sàn giao dịch đã chọn không còn được hỗ trợ.
+ failed: Liên kết sàn giao dịch thất bại.
+ error: "Liên kết sàn giao dịch thất bại: %{message}."
+ new:
+ title: Liên kết tiền điện tử với CoinStats
+ blockchain_fetch_error: Tải blockchain thất bại. Vui lòng thử lại sau.
+ link_wallet_title: Liên kết địa chỉ ví
+ link_wallet_description: Theo dõi ví tự quản hoặc địa chỉ on-chain đơn lẻ qua CoinStats.
+ address_label: Địa chỉ
+ address_placeholder: Bắt buộc
+ blockchain_label: Blockchain
+ blockchain_placeholder: Bắt buộc
+ blockchain_select_blank: Chọn một Blockchain
+ link_wallet_submit: Liên kết ví tiền điện tử
+ link_exchange_title: Liên kết API sàn giao dịch
+ link_exchange_description: Sử dụng khóa API chỉ đọc của sàn giao dịch để CoinStats có thể đồng bộ số dư và giao dịch từ Bitvavo, Binance và các sàn được hỗ trợ khác.
+ link_exchange_note: Nếu sàn giao dịch của bạn yêu cầu kích hoạt khóa API hoặc xác nhận qua email, hãy hoàn tất bước đó trước khi liên kết ở đây.
+ exchange_select_blank: Chọn một sàn giao dịch
+ exchange_label: Sàn giao dịch
+ link_exchange_submit: Liên kết sàn giao dịch
+ not_configured_title: Kết nối nhà cung cấp CoinStats chưa được cấu hình
+ not_configured_message: Để liên kết ví tiền điện tử hoặc sàn giao dịch, bạn phải cấu hình kết nối nhà cung cấp CoinStats trước.
+ not_configured_step1_html: Đi đến Cài đặt → Nhà cung cấp
+ not_configured_step2_html: Tìm nhà cung cấp CoinStats
+ not_configured_step3_html: Làm theo hướng dẫn thiết lập được cung cấp để hoàn tất cấu hình nhà cung cấp
+ go_to_settings: Đi đến Cài đặt nhà cung cấp
+ setup_instructions: "Hướng dẫn thiết lập:"
+ step1_html: Truy cập CoinStats Public API Dashboard để lấy khóa API.
+ step2: Nhập khóa API của bạn bên dưới và nhấn Cấu hình.
+ step3_html: Sau khi kết nối thành công, truy cập tab Tài khoản để thiết lập tài khoản tiền điện tử.
+ api_key_label: Khóa API
+ api_key_placeholder: Bắt buộc
+ configure: Cấu hình
+ update_configuration: Cấu hình lại
+ default_name: Kết nối CoinStats
+ coinstats_item:
+ deletion_in_progress: Dữ liệu ví tiền điện tử đang được xóa…
+ provider_name: CoinStats
+ syncing: Đang đồng bộ…
+ sync_status:
+ no_accounts: Không tìm thấy ví tiền điện tử
+ all_synced:
+ one: "%{count} ví tiền điện tử đã đồng bộ"
+ other: "%{count} ví tiền điện tử đã đồng bộ"
+ partial_sync: "%{linked_count} ví tiền điện tử đã đồng bộ, %{unlinked_count} cần thiết lập"
+ reconnect: Kết nối lại
+ status: Đồng bộ lần cuối %{timestamp} trước
+ status_never: Chưa bao giờ đồng bộ
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước • %{summary}"
+ update_api_key: Cập nhật khóa API
+ delete: Xóa
+ no_wallets_title: Không có ví tiền điện tử nào được kết nối
+ no_wallets_message: Hiện không có ví tiền điện tử nào được kết nối với CoinStats.
diff --git a/config/locales/views/coinstats_items/zh-CN.yml b/config/locales/views/coinstats_items/zh-CN.yml
new file mode 100644
index 000000000..015505c4a
--- /dev/null
+++ b/config/locales/views/coinstats_items/zh-CN.yml
@@ -0,0 +1,75 @@
+---
+zh-CN:
+ coinstats_items:
+ create:
+ success: CoinStats 提供商连接配置成功。
+ default_name: CoinStats 连接
+ errors:
+ validation_failed: "验证失败:%{message}。"
+ update:
+ success: CoinStats 提供商连接已更新。
+ errors:
+ validation_failed: "验证失败:%{message}。"
+ destroy:
+ success: CoinStats 提供商连接已安排删除。
+ link_wallet:
+ success: "%{count} 个加密钱包链接成功。"
+ missing_params: "缺少必需参数:address 和 blockchain。"
+ failed: 加密钱包链接失败。
+ error: "加密钱包链接失败:%{message}。"
+ link_exchange:
+ success: "%{name} 交易所已关联。"
+ missing_params: 需要交易所和凭据。
+ invalid_exchange: 所选交易所已不再受支持。
+ failed: 关联交易所失败。
+ error: "关联交易所失败:%{message}。"
+ new:
+ title: 使用 CoinStats 连接加密资产
+ blockchain_fetch_error: 获取区块链列表失败,请稍后再试。
+ link_wallet_title: 关联钱包地址
+ link_wallet_description: 通过 CoinStats 跟踪自托管钱包或单个链上地址。
+ address_label: 地址
+ address_placeholder: 必填
+ blockchain_label: 区块链
+ blockchain_placeholder: 必填
+ blockchain_select_blank: 选择区块链
+ link_wallet_submit: 关联加密钱包
+ link_exchange_title: 关联交易所 API
+ link_exchange_description: 使用只读交易所 API key,以便 CoinStats 从 Bitvavo、Binance 和其他受支持的交易所同步余额和交易。
+ link_exchange_note: 如果交易所要求 API key 激活或邮件确认,请先完成这一步再在此关联。
+ exchange_select_blank: 选择交易所
+ exchange_label: 交易所
+ link_exchange_submit: 关联交易所
+ not_configured_title: CoinStats 提供商连接未配置
+ not_configured_message: 要关联加密钱包或交易所,您必须先配置 CoinStats 提供商连接。
+ not_configured_step1_html: 前往 设置 → 提供商
+ not_configured_step2_html: 找到 CoinStats 提供商
+ not_configured_step3_html: 按照提供的 设置说明 完成提供商配置
+ go_to_settings: 前往提供商设置
+ setup_instructions: "设置说明:"
+ step1_html: 访问 CoinStats 公共 API 仪表盘 获取 API key。
+ step2: 在下方输入您的 API key,然后点击配置。
+ step3_html: 连接成功后,前往 账户 标签页设置您的加密账户。
+ api_key_label: API Key
+ api_key_placeholder: 必填
+ configure: 配置
+ update_configuration: 重新配置
+ default_name: CoinStats 连接
+ coinstats_item:
+ deletion_in_progress: 正在删除加密钱包数据…
+ provider_name: CoinStats
+ syncing: 正在同步…
+ sync_status:
+ no_accounts: 未找到加密钱包
+ all_synced:
+ one: 已同步 %{count} 个加密钱包
+ other: 已同步 %{count} 个加密钱包
+ partial_sync: 已同步 %{linked_count} 个加密钱包,%{unlinked_count} 个需要设置
+ reconnect: 重新连接
+ status: "%{timestamp} 前上次同步"
+ status_never: 从未同步
+ status_with_summary: "%{timestamp} 前上次同步 • %{summary}"
+ update_api_key: 更新 API Key
+ delete: 删除
+ no_wallets_title: 未连接任何加密钱包
+ no_wallets_message: 当前没有任何加密钱包连接到 CoinStats。
diff --git a/config/locales/views/components/vi.yml b/config/locales/views/components/vi.yml
new file mode 100644
index 000000000..fc8f5ea88
--- /dev/null
+++ b/config/locales/views/components/vi.yml
@@ -0,0 +1,162 @@
+---
+vi:
+ UI:
+ account:
+ activity_feed:
+ toggle_selection_checkboxes: Bật/tắt chọn
+ balance_reconciliation:
+ labels:
+ adjustments: Điều chỉnh
+ buys: Mua vào
+ change_in_brokerage_cash: Thay đổi tiền mặt môi giới
+ change_in_holdings_market: Thay đổi danh mục (giá thị trường)
+ change_in_holdings_trades: Thay đổi danh mục (mua/bán)
+ charges: Phí phát sinh
+ end_balance: Số dư cuối
+ end_principal: Gốc cuối
+ end_value: Giá trị cuối
+ final_balance: Số dư cuối cùng
+ final_principal: Gốc cuối cùng
+ final_value: Giá trị cuối cùng
+ market_changes: Biến động thị trường
+ net_cash_flow: Dòng tiền thuần
+ net_principal_change: Thay đổi gốc thuần
+ net_value_change: Thay đổi giá trị thuần
+ payments: Thanh toán
+ sells: Bán ra
+ start_balance: Số dư đầu
+ start_principal: Gốc đầu
+ start_value: Giá trị đầu
+ tooltips:
+ adjustments: Điều chỉnh thủ công hoặc điều chỉnh khác
+ adjustments_asset: Điều chỉnh giá trị thủ công hoặc thẩm định
+ buys: Mua tiền mã hóa trong ngày
+ change_in_brokerage_cash: Thay đổi tiền mặt thuần từ nạp/rút và giao dịch
+ change_in_holdings_market: Thay đổi giá trị danh mục do biến động giá thị trường
+ change_in_holdings_trades: Tác động đến danh mục từ mua bán chứng khoán
+ charges: Phí phát sinh mới trong ngày
+ end_balance: Số dư tính toán sau tất cả giao dịch
+ end_balance_investment: Số dư tính toán sau tất cả hoạt động
+ end_principal: Gốc tính toán sau tất cả giao dịch
+ end_value: Giá trị tính toán sau tất cả thay đổi
+ final_balance: Số dư tài khoản cuối cùng trong ngày
+ final_balance_credit: Số dư nợ cuối cùng trong ngày
+ final_balance_crypto: Giá trị danh mục tiền mã hóa cuối cùng trong ngày
+ final_balance_investment: Giá trị danh mục đầu tư cuối cùng trong ngày
+ final_principal: Số dư gốc cuối cùng trong ngày
+ final_value: Giá trị tài sản cuối cùng trong ngày
+ market_changes: Thay đổi giá trị do biến động giá thị trường
+ net_cash_flow: Thay đổi số dư thuần từ tất cả giao dịch trong ngày
+ net_principal_change: Thanh toán gốc và khoản vay mới trong ngày
+ net_value_change: Tất cả thay đổi giá trị bao gồm cải thiện và khấu hao
+ payments: Thanh toán thẻ trong ngày
+ sells: Bán tiền mã hóa trong ngày
+ start_balance: Số dư tài khoản đầu ngày
+ start_balance_credit: Số dư nợ đầu ngày
+ start_balance_crypto: Giá trị danh mục tiền mã hóa đầu ngày
+ start_balance_investment: Tổng giá trị danh mục đầu tư đầu ngày
+ start_principal: Số dư gốc đầu ngày
+ start_value: Giá trị tài sản đầu ngày
+ chart:
+ no_data_available: "Không có dữ liệu"
+ title:
+ balance: Số dư
+ cash_value: Giá trị tiền mặt
+ debt_balance: Số dư nợ
+ estimated_property_value: Giá trị bất động sản ước tính
+ estimated_vehicle_value: Giá trị phương tiện ước tính
+ holdings_value: Giá trị danh mục
+ remaining_principal_balance: Số dư gốc còn lại
+ total_account_value: Tổng giá trị tài khoản
+ views:
+ cash: Tiền mặt
+ holdings: Danh mục
+ total_value: Tổng giá trị
+ vs_available_history: so với lịch sử hiện có
+ activity_date:
+ balance_tooltip: "Số dư cuối ngày, sau tất cả giao dịch và điều chỉnh"
+ no_balance_data: "Không có dữ liệu số dư cho ngày này"
+ ds:
+ alert:
+ variants:
+ info: Thông tin
+ success: Thành công
+ warning: Cảnh báo
+ error: Lỗi
+ destructive: Lỗi
+ pill:
+ aria_label: "%{label}"
+ default_label: Xem trước
+ dialog:
+ close: Đóng
+ popover:
+ avatar_default_label: Mở menu
+ tooltip:
+ trigger_label: Thêm thông tin
+ link:
+ opens_in_new_tab: (mở trong tab mới)
+ provider_sync_summary:
+ title: Tóm tắt đồng bộ
+ last_sync: "Đồng bộ lần cuối: %{time_ago} trước"
+ accounts:
+ title: Tài khoản
+ total: "Tổng: %{count}"
+ linked: "Đã liên kết: %{count}"
+ unlinked: "Chưa liên kết: %{count}"
+ institutions: "Tổ chức: %{count}"
+ transactions:
+ title: Giao dịch
+ seen: "Đã thấy: %{count}"
+ imported: "Đã nhập: %{count}"
+ updated: "Đã cập nhật: %{count}"
+ skipped: "Đã bỏ qua: %{count}"
+ fetching: "Đang tải từ sàn giao dịch..."
+ protected:
+ one: "%{count} mục được bảo vệ (không ghi đè)"
+ other: "%{count} mục được bảo vệ (không ghi đè)"
+ view_protected: Xem các mục được bảo vệ
+ skip_reasons:
+ excluded: Đã loại trừ
+ user_modified: Người dùng đã sửa
+ import_locked: Nhập CSV
+ protected: Được bảo vệ
+ holdings:
+ title: Danh mục nắm giữ
+ found: "Tìm thấy: %{count}"
+ processed: "Đã xử lý: %{count}"
+ trades:
+ title: Giao dịch chứng khoán
+ imported: "Đã nhập: %{count}"
+ skipped: "Đã bỏ qua: %{count}"
+ fetching: "Đang tải hoạt động từ sàn giao dịch..."
+ health:
+ title: Tình trạng
+ view_error_details: Xem chi tiết lỗi
+ rate_limited: "Bị giới hạn tốc độ %{time_ago}"
+ recently: gần đây
+ errors: "Lỗi: %{count}"
+ pending_reconciled:
+ one: "%{count} giao dịch chờ trùng lặp đã được đối chiếu"
+ other: "%{count} giao dịch chờ trùng lặp đã được đối chiếu"
+ view_reconciled: Xem giao dịch đã đối chiếu
+ duplicate_suggestions:
+ one: "%{count} giao dịch có thể trùng lặp cần xem xét"
+ other: "%{count} giao dịch có thể trùng lặp cần xem xét"
+ view_duplicate_suggestions: Xem các giao dịch trùng lặp được đề xuất
+ stale_pending:
+ one: "%{count} giao dịch chờ cũ (không tính vào ngân sách)"
+ other: "%{count} giao dịch chờ cũ (không tính vào ngân sách)"
+ view_stale_pending: Xem các tài khoản bị ảnh hưởng
+ stale_pending_count:
+ one: "%{count} giao dịch"
+ other: "%{count} giao dịch"
+ stale_unmatched:
+ one: "%{count} giao dịch chờ cần xem xét thủ công"
+ other: "%{count} giao dịch chờ cần xem xét thủ công"
+ view_stale_unmatched: Xem giao dịch cần xem xét
+ stale_unmatched_count:
+ one: "%{count} giao dịch"
+ other: "%{count} giao dịch"
+ data_warnings: "Cảnh báo dữ liệu: %{count}"
+ notices: "Thông báo: %{count}"
+ view_data_quality: Xem chi tiết chất lượng dữ liệu
diff --git a/config/locales/views/components/zh-CN.yml b/config/locales/views/components/zh-CN.yml
new file mode 100644
index 000000000..4f8f2189b
--- /dev/null
+++ b/config/locales/views/components/zh-CN.yml
@@ -0,0 +1,162 @@
+---
+zh-CN:
+ UI:
+ account:
+ activity_feed:
+ toggle_selection_checkboxes: 切换选择
+ balance_reconciliation:
+ labels:
+ adjustments: 调整
+ buys: 买入
+ change_in_brokerage_cash: 经纪账户现金变动
+ change_in_holdings_market: 持仓变动(市价活动)
+ change_in_holdings_trades: 持仓变动(买入/卖出)
+ charges: 费用
+ end_balance: 期末余额
+ end_principal: 期末本金
+ end_value: 期末价值
+ final_balance: 最终余额
+ final_principal: 最终本金
+ final_value: 最终价值
+ market_changes: 市场变动
+ net_cash_flow: 净现金流
+ net_principal_change: 净本金变动
+ net_value_change: 净价值变动
+ payments: 付款
+ sells: 卖出
+ start_balance: 期初余额
+ start_principal: 期初本金
+ start_value: 期初价值
+ tooltips:
+ adjustments: 手动对账或其他调整
+ adjustments_asset: 手动价值调整或估值
+ buys: 当日加密货币买入
+ change_in_brokerage_cash: 存款、取款和交易带来的现金净变化
+ change_in_holdings_market: 市场价格变动导致的持仓价值变化
+ change_in_holdings_trades: 买卖证券对持仓的影响
+ charges: 当日新增费用
+ end_balance: 所有交易后的计算余额
+ end_balance_investment: 所有活动后的计算余额
+ end_principal: 所有交易后的计算本金
+ end_value: 所有变动后的计算价值
+ final_balance: 当日最终账户余额
+ final_balance_credit: 当日最终应付余额
+ final_balance_crypto: 当日最终加密持仓价值
+ final_balance_investment: 当日最终投资组合价值
+ final_principal: 当日最终本金余额
+ final_value: 当日最终资产价值
+ market_changes: 市场价格变动导致的价值变化
+ net_cash_flow: 当日所有交易带来的余额净变化
+ net_principal_change: 当日的本金还款和新增借款
+ net_value_change: 包括增值和贬值在内的所有价值变化
+ payments: 当日记入卡上的付款
+ sells: 当日加密货币卖出
+ start_balance: 当日开始时的账户余额
+ start_balance_credit: 当日开始时的欠款余额
+ start_balance_crypto: 当日开始时的加密持仓价值
+ start_balance_investment: 当日开始时的总投资组合价值
+ start_principal: 当日开始时的本金余额
+ start_value: 当日开始时的资产价值
+ chart:
+ no_data_available: 暂无数据
+ title:
+ balance: 余额
+ cash_value: 现金价值
+ debt_balance: 债务余额
+ estimated_property_value: 预估房产价值
+ estimated_vehicle_value: 预估车辆价值
+ holdings_value: 持仓价值
+ remaining_principal_balance: 剩余本金余额
+ total_account_value: 账户总价值
+ views:
+ cash: 现金
+ holdings: 持仓
+ total_value: 总价值
+ vs_available_history: 与可用历史相比
+ activity_date:
+ balance_tooltip: 当日结束余额,已包含所有交易和调整
+ no_balance_data: 此日期没有余额数据
+ ds:
+ alert:
+ variants:
+ info: 信息
+ success: 成功
+ warning: 警告
+ error: 错误
+ destructive: 错误
+ pill:
+ aria_label: "%{label}"
+ default_label: 预览
+ dialog:
+ close: 关闭
+ popover:
+ avatar_default_label: 打开菜单
+ tooltip:
+ trigger_label: 更多信息
+ link:
+ opens_in_new_tab: (在新标签页打开)
+ provider_sync_summary:
+ title: 同步摘要
+ last_sync: "上次同步:%{time_ago} 前"
+ accounts:
+ title: 账户
+ total: "总计:%{count}"
+ linked: "已关联:%{count}"
+ unlinked: "未关联:%{count}"
+ institutions: "机构:%{count}"
+ transactions:
+ title: 交易
+ seen: "已看到:%{count}"
+ imported: "已导入:%{count}"
+ updated: "已更新:%{count}"
+ skipped: "已跳过:%{count}"
+ fetching: 正在从券商获取...
+ protected:
+ one: 已保护 %{count} 条记录(未覆盖)
+ other: 已保护 %{count} 条记录(未覆盖)
+ view_protected: 查看受保护记录
+ skip_reasons:
+ excluded: 已排除
+ user_modified: 用户已修改
+ import_locked: CSV 导入
+ protected: 受保护
+ holdings:
+ title: 持仓
+ found: "已找到:%{count}"
+ processed: "已处理:%{count}"
+ trades:
+ title: 交易
+ imported: "已导入:%{count}"
+ skipped: "已跳过:%{count}"
+ fetching: 正在从券商获取活动...
+ health:
+ title: 健康状况
+ view_error_details: 查看错误详情
+ rate_limited: "%{time_ago} 前触发限流"
+ recently: 最近
+ errors: "错误:%{count}"
+ pending_reconciled:
+ one: 已核对 %{count} 笔重复待处理交易
+ other: 已核对 %{count} 笔重复待处理交易
+ view_reconciled: 查看已核对交易
+ duplicate_suggestions:
+ one: 有 %{count} 条可能的重复记录需要查看
+ other: 有 %{count} 条可能的重复记录需要查看
+ view_duplicate_suggestions: 查看建议的重复项
+ stale_pending:
+ one: "%{count} 笔过期待处理交易(已从预算中排除)"
+ other: "%{count} 笔过期待处理交易(已从预算中排除)"
+ view_stale_pending: 查看受影响账户
+ stale_pending_count:
+ one: "%{count} 笔交易"
+ other: "%{count} 笔交易"
+ stale_unmatched:
+ one: "%{count} 笔待处理交易需要手动审核"
+ other: "%{count} 笔待处理交易需要手动审核"
+ view_stale_unmatched: 查看需要审核的交易
+ stale_unmatched_count:
+ one: "%{count} 笔交易"
+ other: "%{count} 笔交易"
+ data_warnings: "数据警告:%{count}"
+ notices: "通知:%{count}"
+ view_data_quality: 查看数据质量详情
diff --git a/config/locales/views/credit_cards/vi.yml b/config/locales/views/credit_cards/vi.yml
new file mode 100644
index 000000000..96fd56c1e
--- /dev/null
+++ b/config/locales/views/credit_cards/vi.yml
@@ -0,0 +1,26 @@
+---
+vi:
+ credit_cards:
+ edit:
+ edit: Chỉnh sửa %{account}
+ form:
+ annual_fee: Phí thường niên
+ annual_fee_placeholder: '99'
+ apr: Lãi suất APR
+ apr_placeholder: '15.99'
+ available_credit: Hạn mức tín dụng còn lại
+ available_credit_placeholder: '10000'
+ expiration_date: Ngày hết hạn
+ minimum_payment: Thanh toán tối thiểu
+ minimum_payment_placeholder: '100'
+ new:
+ title: Nhập thông tin thẻ tín dụng
+ overview:
+ amount_owed: Số tiền nợ
+ annual_fee: Phí thường niên
+ apr: Lãi suất APR
+ available_credit: Hạn mức tín dụng còn lại
+ edit_account_details: Chỉnh sửa thông tin tài khoản
+ expiration_date: Ngày hết hạn
+ minimum_payment: Thanh toán tối thiểu
+ unknown: Không xác định
diff --git a/config/locales/views/cryptos/vi.yml b/config/locales/views/cryptos/vi.yml
new file mode 100644
index 000000000..ddc345939
--- /dev/null
+++ b/config/locales/views/cryptos/vi.yml
@@ -0,0 +1,20 @@
+---
+vi:
+ cryptos:
+ edit:
+ edit: Chỉnh sửa %{account}
+ form:
+ subtype_label: Loại tài khoản
+ subtype_prompt: Chọn loại tài khoản
+ subtype_none: Không có
+ tax_treatment_label: Chế độ thuế
+ tax_treatment_hint: Hầu hết tiền mã hóa được nắm giữ trong các tài khoản chịu thuế. Chọn một tùy chọn khác nếu được nắm giữ trong tài khoản có lợi thế về thuế.
+ new:
+ title: Nhập số dư tài khoản
+ subtypes:
+ wallet:
+ short: Ví
+ long: Ví tiền mã hóa
+ exchange:
+ short: Sàn giao dịch
+ long: Sàn giao dịch tiền mã hóa
diff --git a/config/locales/views/depositories/vi.yml b/config/locales/views/depositories/vi.yml
new file mode 100644
index 000000000..ef89650a2
--- /dev/null
+++ b/config/locales/views/depositories/vi.yml
@@ -0,0 +1,26 @@
+---
+vi:
+ depositories:
+ edit:
+ edit: Chỉnh sửa %{account}
+ form:
+ none: Không có
+ subtype_prompt: Chọn loại tài khoản
+ new:
+ title: Nhập số dư tài khoản
+ subtypes:
+ cd:
+ long: Chứng chỉ tiền gửi
+ short: CD
+ checking:
+ long: Tài khoản thanh toán
+ short: Thanh toán
+ hsa:
+ long: Tài khoản tiết kiệm y tế
+ short: HSA
+ money_market:
+ long: Thị trường tiền tệ
+ short: MM
+ savings:
+ long: Tài khoản tiết kiệm
+ short: Tiết kiệm
diff --git a/config/locales/views/email_confirmation_mailer/vi.yml b/config/locales/views/email_confirmation_mailer/vi.yml
new file mode 100644
index 000000000..e7c042401
--- /dev/null
+++ b/config/locales/views/email_confirmation_mailer/vi.yml
@@ -0,0 +1,9 @@
+---
+vi:
+ email_confirmation_mailer:
+ confirmation_email:
+ body: Bạn vừa yêu cầu thay đổi địa chỉ email. Nhấp vào nút bên dưới để xác nhận thay đổi này.
+ cta: Xác nhận thay đổi email
+ expiry_notice: Liên kết này sẽ hết hạn sau %{hours} giờ.
+ greeting: Xin chào!
+ subject: '%{product_name}: Xác nhận thay đổi email của bạn'
diff --git a/config/locales/views/email_confirmation_mailer/zh-CN.yml b/config/locales/views/email_confirmation_mailer/zh-CN.yml
index 5b3bd42cf..952a04bcb 100644
--- a/config/locales/views/email_confirmation_mailer/zh-CN.yml
+++ b/config/locales/views/email_confirmation_mailer/zh-CN.yml
@@ -2,8 +2,8 @@
zh-CN:
email_confirmation_mailer:
confirmation_email:
- body: 您最近请求更改邮箱地址。请点击下方按钮确认此次更改。
+ body: 您最近请求更改邮箱地址。请点击下方按钮确认此更改。
cta: 确认邮箱更改
- expiry_notice: 此链接将在 %{hours} 小时后失效。
+ expiry_notice: 此链接将在 %{hours} 小时后过期。
greeting: 您好!
- subject: "%{product_name}:确认您的邮箱更改"
+ subject: '%{product_name}:确认您的邮箱更改'
diff --git a/config/locales/views/enable_banking_items/vi.yml b/config/locales/views/enable_banking_items/vi.yml
new file mode 100644
index 000000000..138ba872d
--- /dev/null
+++ b/config/locales/views/enable_banking_items/vi.yml
@@ -0,0 +1,116 @@
+---
+vi:
+ enable_banking_items:
+ errors:
+ api_error: "Đã xảy ra lỗi giao tiếp với ngân hàng."
+ network_unreachable: "Dịch vụ ngân hàng tạm thời không khả dụng. Vui lòng thử lại sau."
+ session_invalid: "Phiên đã hết hạn. Vui lòng kết nối lại ngân hàng của bạn."
+ unexpected: "Đã xảy ra lỗi không mong muốn trong quá trình đồng bộ."
+ authorize:
+ authorization_failed: "Không thể khởi tạo ủy quyền: %{message}"
+ bank_required: Vui lòng chọn ngân hàng.
+ decoupled_not_supported: Ngân hàng này sử dụng phương thức xác thực thiết bị riêng biệt chưa được hỗ trợ. Vui lòng thêm tài khoản này thủ công.
+ invalid_redirect: URL ủy quyền nhận được không hợp lệ. Vui lòng thử lại.
+ redirect_uri_not_allowed: Chuyển hướng không được phép. Vui lòng cấu hình `%{callback_url}` trong cài đặt ứng dụng Enable Banking của bạn.
+ unexpected_error: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại.
+ callback:
+ authorization_error: Ủy quyền thất bại
+ invalid_callback: Tham số callback không hợp lệ.
+ item_not_found: Không tìm thấy kết nối.
+ session_failed: Không thể hoàn tất ủy quyền
+ success: Đã kết nối thành công với ngân hàng của bạn. Các tài khoản đang được đồng bộ.
+ unexpected_error: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại.
+ complete_account_setup:
+ all_skipped: Tất cả tài khoản đã bị bỏ qua. Bạn có thể thiết lập sau trên trang tài khoản.
+ no_accounts: Không có tài khoản nào để thiết lập.
+ success: Đã tạo thành công %{count} tài khoản!
+ create:
+ success: Cấu hình Enable Banking thành công.
+ destroy:
+ success: Kết nối Enable Banking đã được đưa vào hàng đợi xóa.
+ link_accounts:
+ already_linked: Các tài khoản đã chọn đã được liên kết.
+ link_failed: Liên kết tài khoản thất bại
+ no_accounts_selected: Chưa chọn tài khoản nào.
+ no_session: Không có kết nối Enable Banking nào đang hoạt động. Vui lòng kết nối với ngân hàng trước.
+ success: Đã liên kết thành công %{count} tài khoản.
+ link_existing_account:
+ success: Tài khoản đã được liên kết thành công với Enable Banking
+ errors:
+ only_manual: Chỉ tài khoản thủ công mới có thể được liên kết
+ invalid_enable_banking_account: Tài khoản Enable Banking được chọn không hợp lệ
+ enable_banking_item:
+ deletion_in_progress: Đang xóa
+ provider_name: Enable Banking
+ syncing: Đang đồng bộ...
+ reconnect: Kết nối lại
+ last_synced: Đồng bộ lần cuối %{time} trước
+ never_synced: Chưa bao giờ đồng bộ
+ update: Cập nhật
+ delete: Xóa
+ setup_needed: Cần thiết lập
+ setup_needed_description:
+ one: 1 tài khoản được nhập từ Enable Banking cần thiết lập
+ other: "%{count} tài khoản được nhập từ Enable Banking cần thiết lập"
+ set_up_accounts: Thiết lập tài khoản
+ no_accounts_found: Không tìm thấy tài khoản
+ no_accounts_found_description: Không tìm thấy tài khoản từ Enable Banking. Hãy thử đồng bộ lại.
+ select_existing_account:
+ title: Liên kết tài khoản Enable Banking
+ all_linked: Tất cả tài khoản Enable Banking có vẻ đã được liên kết.
+ try_after_sync: Nếu bạn vừa kết nối hoặc đồng bộ, hãy thử lại sau khi hoàn tất.
+ unlink_to_move: Để liên kết tài khoản khác, trước tiên hãy hủy liên kết từ menu thao tác của tài khoản.
+ balance: Số dư
+ link: Liên kết
+ cancel: Hủy
+ setup_accounts:
+ title: Thiết lập tài khoản Enable Banking của bạn
+ header_subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập
+ choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Enable Banking:"
+ historical_data_range: "Phạm vi dữ liệu lịch sử:"
+ sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:"
+ sync_start_date_help: Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ. Tối đa 2 năm lịch sử có sẵn.
+ account_type_label: "Loại tài khoản:"
+ balance: Số dư
+ create_accounts: Tạo tài khoản
+ creating_accounts: Đang tạo tài khoản...
+ cancel: Hủy
+ new:
+ link_enable_banking_title: Liên kết Enable Banking
+ session_expired: Phiên hết hạn - cần ủy quyền lại
+ connected_bank: Ngân hàng đã kết nối
+ session_expires: "Phiên hết hạn"
+ unknown: Không rõ
+ connection: Kết nối
+ configured: Đã cấu hình
+ ready_to_connect: Sẵn sàng kết nối ngân hàng
+ sync: Đồng bộ
+ reconnect: Kết nối lại
+ connect_bank: Kết nối ngân hàng
+ remove_confirm: Bạn có chắc chắn muốn xóa kết nối này không?
+ remove: Xóa
+ add_connection: Thêm kết nối
+ not_configured: Kết nối Enable Banking chưa được cấu hình
+ not_configured_description: Trước khi liên kết tài khoản Enable Banking, bạn cần cấu hình kết nối Enable Banking.
+ setup_steps_title: "Các bước thiết lập:"
+ setup_step_1_html: "Đi đến Cài đặt → Nhà cung cấp"
+ setup_step_2_html: "Tìm phần Enable Banking"
+ setup_step_3: Nhập thông tin xác thực Enable Banking của bạn
+ setup_step_4: Quay lại đây để liên kết tài khoản
+ go_to_provider_settings: Đi đến Cài đặt nhà cung cấp
+ reauthorize:
+ invalid_redirect: URL ủy quyền nhận được không hợp lệ. Vui lòng thử lại.
+ reauthorization_failed: Ủy quyền lại thất bại
+ select_bank:
+ beta_label: Beta
+ cancel: Hủy
+ check_country: Vui lòng kiểm tra cài đặt mã quốc gia của bạn.
+ credentials_required: Vui lòng cấu hình thông tin xác thực Enable Banking trước.
+ description: Chọn ngân hàng bạn muốn kết nối với tài khoản của mình.
+ no_banks: Không có ngân hàng nào cho quốc gia/khu vực này.
+ no_search_results: Không có ngân hàng nào khớp với tìm kiếm của bạn.
+ search_label: Tìm kiếm ngân hàng của bạn
+ search_placeholder: Tìm kiếm ngân hàng của bạn...
+ title: Chọn ngân hàng của bạn
+ update:
+ success: Đã cập nhật cấu hình Enable Banking.
diff --git a/config/locales/views/enable_banking_items/zh-CN.yml b/config/locales/views/enable_banking_items/zh-CN.yml
index 3285fbee5..3bf277767 100644
--- a/config/locales/views/enable_banking_items/zh-CN.yml
+++ b/config/locales/views/enable_banking_items/zh-CN.yml
@@ -1,10 +1,16 @@
---
zh-CN:
enable_banking_items:
+ errors:
+ api_error: 与银行通信时发生错误。
+ network_unreachable: 银行服务暂时无法访问,请稍后再试。
+ session_invalid: 会话已过期。请重新连接您的银行。
+ unexpected: 同步过程中发生意外错误。
authorize:
- authorization_failed: 启动授权失败
+ authorization_failed: 启动授权失败:%{message}
bank_required: 请选择一家银行。
- invalid_redirect: 收到的授权 URL 无效。请重试或联系支持人员。
+ decoupled_not_supported: 该银行使用独立设备认证方式,目前尚不支持。请手动添加此账户。
+ invalid_redirect: 收到的授权 URL 无效。请重试。
redirect_uri_not_allowed: 不允许重定向。请在 Enable Banking 应用设置中配置 `%{callback_url}`。
unexpected_error: 发生意外错误。请重试。
callback:
@@ -12,38 +18,99 @@ zh-CN:
invalid_callback: 回调参数无效。
item_not_found: 未找到连接。
session_failed: 无法完成授权
- success: 已成功连接到您的银行。您的账户正在同步中。
+ success: 已成功连接到您的银行。您的账户正在同步。
unexpected_error: 发生意外错误。请重试。
complete_account_setup:
- all_skipped: 已跳过所有账户。您可以稍后在账户页面进行设置。
+ all_skipped: 所有账户都已跳过。您可以稍后在账户页面进行设置。
no_accounts: 没有可设置的账户。
- success: 成功创建了 %{count} 个账户!
+ success: 已成功创建 %{count} 个账户!
create:
success: Enable Banking 配置成功。
destroy:
- success: Enable Banking 连接已加入删除队列。
+ success: Enable Banking 连接已排队等待删除。
link_accounts:
- already_linked: 所选账户已关联。
- link_failed: 关联账户失败
- no_accounts_selected: 未选择任何账户。
- no_session: 无活跃的 Enable Banking 连接。请先连接一家银行。
- success: 成功关联了 %{count} 个账户。
+ already_linked: 所选账户已连接。
+ link_failed: 连接账户失败
+ no_accounts_selected: 未选择账户。
+ no_session: 当前没有活动的 Enable Banking 连接。请先连接银行。
+ success: 已成功连接 %{count} 个账户。
link_existing_account:
+ success: 账户已成功连接到 Enable Banking
errors:
+ only_manual: 只有手动账户才能连接
invalid_enable_banking_account: 所选 Enable Banking 账户无效
- only_manual: 只能关联手动账户
- success: 账户已成功关联到 Enable Banking
+ enable_banking_item:
+ deletion_in_progress: 正在删除
+ provider_name: Enable Banking
+ syncing: 正在同步...
+ reconnect: 重新连接
+ last_synced: 上次同步于 %{time} 前
+ never_synced: 从未同步
+ update: 更新
+ delete: 删除
+ setup_needed: 需要设置
+ setup_needed_description:
+ one: 从 Enable Banking 导入的 1 个账户需要设置
+ other: 从 Enable Banking 导入的 %{count} 个账户需要设置
+ set_up_accounts: 设置账户
+ no_accounts_found: 未找到账户
+ no_accounts_found_description: 未从 Enable Banking 找到账户。请重试同步。
+ select_existing_account:
+ title: 连接 Enable Banking 账户
+ all_linked: 看起来所有 Enable Banking 账户都已连接。
+ try_after_sync: 如果您刚刚连接或同步,请等待同步完成后再试。
+ unlink_to_move: 若要链接不同的账户,请先在账户的操作菜单中取消其连接。
+ balance: 余额
+ link: 连接
+ cancel: 取消
+ setup_accounts:
+ title: 设置您的 Enable Banking 账户
+ header_subtitle: 为您导入的账户选择正确的账户类型
+ choose_account_type: 为每个 Enable Banking 账户选择正确的账户类型:
+ historical_data_range: 历史数据范围:
+ sync_start_date_label: 从以下日期开始同步交易:
+ sync_start_date_help: 选择要同步交易历史的回溯范围。最多可获取 2 年历史。
+ account_type_label: 账户类型:
+ balance: 余额
+ create_accounts: 创建账户
+ creating_accounts: 正在创建账户...
+ cancel: 取消
new:
- link_enable_banking_title: 关联 Enable Banking
+ link_enable_banking_title: 连接 Enable Banking
+ session_expired: 会话已过期 - 需要重新授权
+ connected_bank: 已连接的银行
+ session_expires: 会话过期时间
+ unknown: 未知
+ connection: 连接
+ configured: 已配置
+ ready_to_connect: 准备连接银行
+ sync: 同步
+ reconnect: 重新连接
+ connect_bank: 连接银行
+ remove_confirm: 确定要移除此连接吗?
+ remove: 移除
+ add_connection: 添加连接
+ not_configured: 尚未配置 Enable Banking 连接
+ not_configured_description: 在连接 Enable Banking 账户之前,您需要先配置 Enable Banking 连接。
+ setup_steps_title: 设置步骤:
+ setup_step_1_html: 前往 设置 → 提供商
+ setup_step_2_html: 找到 Enable Banking 区块
+ setup_step_3: 输入您的 Enable Banking 凭据
+ setup_step_4: 返回此处连接您的账户
+ go_to_provider_settings: 前往提供商设置
reauthorize:
- invalid_redirect: 收到的授权 URL 无效。请重试或联系支持人员。
+ invalid_redirect: 收到的授权 URL 无效。请重试。
reauthorization_failed: 重新授权失败
select_bank:
+ beta_label: Beta
cancel: 取消
- check_country: 请检查您的国家代码设置。
+ check_country: 请检查您的国家/地区代码设置。
credentials_required: 请先配置您的 Enable Banking 凭据。
- description: 选择您想要连接到账户的银行。
- no_banks: 该国家/地区暂无可用银行。
+ description: 选择您要连接到账户的银行。
+ no_banks: 此国家/地区没有可用银行。
+ no_search_results: 没有匹配您搜索的银行。
+ search_label: 搜索您的银行
+ search_placeholder: 搜索您的银行...
title: 选择您的银行
update:
success: Enable Banking 配置已更新。
diff --git a/config/locales/views/entries/vi.yml b/config/locales/views/entries/vi.yml
new file mode 100644
index 000000000..6f8194302
--- /dev/null
+++ b/config/locales/views/entries/vi.yml
@@ -0,0 +1,23 @@
+---
+vi:
+ entries:
+ create:
+ success: Đã tạo mục nhập
+ destroy:
+ success: Đã xóa mục nhập
+ empty:
+ description: Thử thêm mục nhập, chỉnh bộ lọc hoặc tinh chỉnh tìm kiếm
+ title: Không tìm thấy mục nhập
+ loading:
+ loading: Đang tải mục nhập...
+ update:
+ success: Đã cập nhật mục nhập
+ unlock:
+ success: Đã mở khóa mục nhập. Có thể được cập nhật lần đồng bộ tiếp theo.
+ protection:
+ tooltip: Được bảo vệ khỏi đồng bộ
+ title: Được bảo vệ khỏi đồng bộ
+ description: Các chỉnh sửa của bạn với mục nhập này sẽ không bị ghi đè khi đồng bộ từ nhà cung cấp.
+ locked_fields_label: "Các trường bị khóa:"
+ unlock_button: Cho phép đồng bộ cập nhật
+ unlock_confirm: Cho phép đồng bộ cập nhật mục nhập này? Các thay đổi của bạn có thể bị ghi đè lần đồng bộ tiếp theo.
diff --git a/config/locales/views/family_exports/vi.yml b/config/locales/views/family_exports/vi.yml
new file mode 100644
index 000000000..e2102acf1
--- /dev/null
+++ b/config/locales/views/family_exports/vi.yml
@@ -0,0 +1,43 @@
+---
+vi:
+ family_exports:
+ access_denied: Truy cập bị từ chối
+ create:
+ success: Xuất dữ liệu đã bắt đầu. Bạn sẽ có thể tải xuống trong thời gian ngắn.
+ delete_confirmation: Bạn có chắc chắn muốn xóa bản xuất này không? Hành động này không thể hoàn tác.
+ delete_failed_confirmation: Bạn có chắc chắn muốn xóa bản xuất thất bại này không?
+ destroy:
+ success: Bản xuất đã được xóa thành công
+ export_not_ready: Bản xuất chưa sẵn sàng để tải xuống
+ exporting: Đang xuất dữ liệu...
+ new:
+ dialog_title: Xuất dữ liệu của bạn
+ dialog_subtitle: Tải xuống tất cả dữ liệu tài chính của bạn
+ whats_included: "Bao gồm:"
+ accounts_and_balances: Tất cả tài khoản và số dư
+ transaction_history: Lịch sử giao dịch
+ investment_trades: Giao dịch đầu tư
+ categories_tags_rules: Danh mục, nhãn và quy tắc
+ note_label: Lưu ý
+ note_description: Bản xuất này bao gồm tất cả dữ liệu của bạn, nhưng chỉ một phần dữ liệu có thể được nhập lại thông qua tính năng nhập CSV. Chúng tôi hỗ trợ nhập tài khoản, giao dịch (với danh mục và nhãn) và giao dịch mua bán. Dữ liệu tài khoản khác không thể nhập và chỉ dùng để lưu trữ hồ sơ của bạn.
+ cancel: Hủy
+ export_data: Xuất dữ liệu
+ index:
+ title: Xuất dữ liệu
+ new: Xuất mới
+ table:
+ title: Xuất dữ liệu
+ header:
+ date: Ngày
+ filename: Tên tệp
+ status: Trạng thái
+ actions: Hành động
+ row:
+ status:
+ in_progress: Đang xử lý
+ complete: Hoàn tất
+ failed: Thất bại
+ actions:
+ delete: Xóa
+ download: Tải xuống
+ empty: Chưa có bản xuất nào.
diff --git a/config/locales/views/holdings/vi.yml b/config/locales/views/holdings/vi.yml
new file mode 100644
index 000000000..216836479
--- /dev/null
+++ b/config/locales/views/holdings/vi.yml
@@ -0,0 +1,101 @@
+---
+vi:
+ holdings:
+ cash:
+ brokerage_cash: Tiền mặt môi giới
+ destroy:
+ success: Đã xóa danh mục nắm giữ
+ cannot_delete: Bạn không thể xóa danh mục nắm giữ này
+ update:
+ success: Đã lưu giá vốn.
+ error: Giá trị giá vốn không hợp lệ.
+ unlock_cost_basis:
+ success: Đã mở khóa giá vốn. Có thể được cập nhật lần đồng bộ tiếp theo.
+ remap_security:
+ success: Đã cập nhật chứng khoán thành công.
+ security_not_found: Không tìm thấy chứng khoán đã chọn.
+ reset_security:
+ success: Chứng khoán đã được đặt lại về giá trị từ nhà cung cấp.
+ sync_prices:
+ success: Đã đồng bộ dữ liệu thị trường thành công.
+ unavailable: Đồng bộ dữ liệu thị trường không khả dụng cho chứng khoán ngoại tuyến.
+ provider_error: Không thể tải giá mới nhất. Vui lòng thử lại sau vài phút.
+ errors:
+ security_collision: "Không thể ánh xạ lại: bạn đã có danh mục nắm giữ cho %{ticker} vào ngày %{date}."
+ cost_basis_sources:
+ manual: Người dùng đặt
+ calculated: Từ giao dịch
+ provider: Từ nhà cung cấp
+ cost_basis_cell:
+ unknown: "--"
+ set: Đặt
+ set_cost_basis_header: "Đặt giá vốn cho %{ticker} (%{qty} cổ phiếu)"
+ total_cost_basis_label: Tổng giá vốn
+ or_per_share_label: "Hoặc nhập mỗi cổ phiếu:"
+ per_share: mỗi cổ phiếu
+ cancel: Hủy
+ save: Lưu
+ overwrite_confirm_title: Ghi đè giá vốn?
+ overwrite_confirm_body: "Thao tác này sẽ thay thế giá vốn hiện tại là %{current}."
+ holding:
+ per_share: mỗi cổ phiếu
+ shares: "%{qty} cổ phiếu"
+ unknown: "--"
+ no_cost_basis: Không có giá vốn
+ index:
+ average_cost: Giá trung bình
+ holdings: Danh mục nắm giữ
+ name: Tên
+ new_holding: Hoạt động mới
+ no_holdings: Không có danh mục nắm giữ.
+ return: Tổng lợi nhuận
+ weight: Tỷ trọng
+ missing_price_tooltip:
+ description: Khoản đầu tư này có giá trị bị thiếu và chúng tôi không thể tính lợi nhuận hoặc giá trị.
+ missing_data: Thiếu dữ liệu
+ show:
+ avg_cost_label: Giá trung bình
+ current_market_price_label: Giá thị trường hiện tại
+ delete: Xóa
+ delete_subtitle: Thao tác này sẽ xóa danh mục nắm giữ và tất cả giao dịch liên quan trong tài khoản này. Không thể hoàn tác.
+ delete_title: Xóa danh mục nắm giữ
+ edit_security: Chỉnh sửa chứng khoán
+ history: Lịch sử
+ no_trade_history: Không có lịch sử giao dịch cho danh mục nắm giữ này.
+ overview: Tổng quan
+ portfolio_weight_label: Tỷ trọng danh mục
+ settings: Cài đặt
+ security_label: Chứng khoán
+ originally: "là %{ticker}"
+ search_security: Tìm kiếm chứng khoán
+ search_security_placeholder: Tìm theo mã ticker hoặc tên
+ cancel: Hủy
+ remap_security: Lưu
+ provider_disabled_warning: "Cập nhật giá tạm dừng — nhà cung cấp %{provider} đã bị tắt. Chuyển sang nhà cung cấp khác bên dưới hoặc bật lại trong Cài đặt."
+ truncated_history_warning: "Lịch sử giá chỉ khả dụng từ %{date} trở đi. Các ngày trước đó không có dữ liệu từ nhà cung cấp đã chọn."
+ switch_provider_label: Chuyển nhà cung cấp
+ switch_provider_description: "%{provider} đã bị tắt. Tìm kiếm chứng khoán này từ nhà cung cấp khác đã bật."
+ switch_provider_button: Chuyển
+ no_security_provider: Chưa cấu hình nhà cung cấp chứng khoán. Không thể tìm kiếm chứng khoán.
+ security_remapped_label: Chứng khoán đã được ánh xạ lại
+ provider_sent: "Nhà cung cấp gửi: %{ticker}"
+ reset_to_provider: Đặt lại về nhà cung cấp
+ reset_confirm_title: Đặt lại chứng khoán về nhà cung cấp?
+ reset_confirm_body: "Thao tác này sẽ thay đổi chứng khoán từ %{current} về %{original} và chuyển tất cả giao dịch liên quan."
+ ticker_label: Mã ticker
+ trade_history_entry: "%{qty} cổ phiếu %{security} ở mức %{price}"
+ total_return_label: Tổng lợi nhuận
+ unknown: Không xác định
+ cost_basis_locked_label: Giá vốn đã bị khóa
+ cost_basis_locked_description: Giá vốn bạn đặt thủ công sẽ không bị thay đổi khi đồng bộ.
+ unlock_cost_basis: Mở khóa
+ unlock_confirm_title: Mở khóa giá vốn?
+ unlock_confirm_body: Thao tác này sẽ cho phép giá vốn được cập nhật bởi đồng bộ nhà cung cấp hoặc tính toán giao dịch.
+ shares_label: Cổ phiếu
+ book_value_label: Giá trị sổ sách
+ market_value_label: Giá trị thị trường
+ market_data_label: Dữ liệu thị trường
+ market_data_sync_button: Làm mới
+ last_price_update: Cập nhật giá lần cuối
+ syncing: Đang đồng bộ...
+ never: Chưa bao giờ
diff --git a/config/locales/views/holdings/zh-CN.yml b/config/locales/views/holdings/zh-CN.yml
index 1dce20e3e..f0c6a38ef 100644
--- a/config/locales/views/holdings/zh-CN.yml
+++ b/config/locales/views/holdings/zh-CN.yml
@@ -2,34 +2,100 @@
zh-CN:
holdings:
cash:
- brokerage_cash: 经纪账户现金
+ brokerage_cash: 券商现金
destroy:
success: 持仓已删除
+ cannot_delete: 您不能删除此持仓
+ update:
+ success: 成本基础已保存。
+ error: 成本基础值无效。
+ unlock_cost_basis:
+ success: 成本基础已解锁。下次同步时可能会更新。
+ remap_security:
+ success: 证券更新成功。
+ security_not_found: 找不到所选证券。
+ reset_security:
+ success: 已重置为提供商值。
+ sync_prices:
+ success: 市场数据同步成功。
+ unavailable: 离线证券无法同步市场数据。
+ provider_error: 无法获取最新价格。请几分钟后再试。
+ errors:
+ security_collision: 无法重新映射:您已在 %{date} 拥有 %{ticker} 的持仓。
+ cost_basis_sources:
+ manual: 用户设置
+ calculated: 根据交易计算
+ provider: 来自提供商
+ cost_basis_cell:
+ unknown: --
+ set: 设置
+ set_cost_basis_header: 为 %{ticker}(%{qty} 股)设置成本基础
+ total_cost_basis_label: 总成本基础
+ or_per_share_label: 或输入每股:
+ per_share: 每股
+ cancel: 取消
+ save: 保存
+ overwrite_confirm_title: 覆盖成本基础?
+ overwrite_confirm_body: 这将替换当前成本基础 %{current}。
holding:
per_share: 每股
- shares: "%{qty}股"
+ shares: "%{qty} 股"
+ unknown: --
+ no_cost_basis: 没有成本基础
index:
average_cost: 平均成本
holdings: 持仓
name: 名称
- new_holding: 新建交易
- no_holdings: 暂无持仓记录
- return: 总收益
+ new_holding: 新活动
+ no_holdings: 没有可显示的持仓。
+ return: 总回报
weight: 权重
missing_price_tooltip:
- description: 此投资缺少价格数据,无法计算其收益或价值。
- missing_data: 数据缺失
+ description: 此投资缺少数值,我们无法计算其回报或价值。
+ missing_data: 缺少数据
show:
avg_cost_label: 平均成本
- current_market_price_label: 当前市价
+ current_market_price_label: 当前市场价
delete: 删除
- delete_subtitle: 此操作将删除该持仓及相关所有交易记录,且不可恢复。
+ delete_subtitle: 这将删除该持仓以及该账户下所有相关交易。此操作无法撤销。
delete_title: 删除持仓
- history: 历史记录
+ edit_security: 编辑证券
+ history: 历史
+ no_trade_history: 此持仓没有可用的交易历史。
overview: 概览
portfolio_weight_label: 投资组合权重
settings: 设置
- ticker_label: 股票代码
- total_return_label: 总收益
- trade_history_entry: "%{qty}股%{security} @ %{price}"
+ security_label: 证券
+ originally: 曾为 %{ticker}
+ search_security: 搜索证券
+ search_security_placeholder: 按代码或名称搜索
+ cancel: 取消
+ remap_security: 保存
+ provider_disabled_warning: 价格更新已暂停——%{provider} 提供商已禁用。请切换到下方其他提供商,或在设置中重新启用。
+ truncated_history_warning: 价格历史仅从 %{date} 起可用。更早日期在所选提供商中没有数据——这可能是因为该资产在您的交易日期之后才上市,或者该提供商当前方案只提供有限的历史窗口。
+ switch_provider_label: 切换提供商
+ switch_provider_description: "%{provider} 已禁用。请从其他已启用的提供商中搜索此证券。"
+ switch_provider_button: 切换
+ no_security_provider: 尚未配置证券提供商。无法搜索证券。
+ security_remapped_label: 证券已重新映射
+ provider_sent: 提供商返回:%{ticker}
+ reset_to_provider: 重置为提供商值
+ reset_confirm_title: 重置为提供商证券?
+ reset_confirm_body: 这会将证券从 %{current} 变回 %{original},并移动所有相关交易。
+ ticker_label: 代码
+ trade_history_entry: "%{qty} 股 %{security},价格 %{price}"
+ total_return_label: 总回报
unknown: 未知
+ cost_basis_locked_label: 成本基础已锁定
+ cost_basis_locked_description: 您手动设置的成本基础不会被同步更改。
+ unlock_cost_basis: 解锁
+ unlock_confirm_title: 解锁成本基础?
+ unlock_confirm_body: 这将允许成本基础通过提供商同步或交易计算进行更新。
+ shares_label: 股数
+ book_value_label: 账面价值
+ market_value_label: 市值
+ market_data_label: 市场数据
+ market_data_sync_button: 刷新
+ last_price_update: 最后价格更新
+ syncing: 正在同步...
+ never: 从未
diff --git a/config/locales/views/ibkr_items/vi.yml b/config/locales/views/ibkr_items/vi.yml
new file mode 100644
index 000000000..2bb59f9a5
--- /dev/null
+++ b/config/locales/views/ibkr_items/vi.yml
@@ -0,0 +1,92 @@
+---
+vi:
+ providers:
+ ibkr:
+ name: Interactive Brokers
+ connection_description: Kết nối báo cáo Flex Web Service của Interactive Brokers
+ institution_name: Interactive Brokers
+ ibkr_items:
+ defaults:
+ name: Interactive Brokers
+ ibkr_item:
+ deletion_in_progress: Đang xóa
+ flex_web_service: Flex Web Service
+ syncing: Đang đồng bộ
+ requires_update: Thông tin xác thực cần chú ý
+ error: Lỗi
+ synced: Đã đồng bộ %{time} trước. %{summary}.
+ never_synced: Chưa bao giờ đồng bộ.
+ setup_accounts: Thiết lập tài khoản
+ delete: Xóa
+ accounts_need_setup: Tài khoản cần thiết lập
+ accounts_need_setup_description: Một số tài khoản từ IBKR cần được liên kết với tài khoản Sure.
+ no_accounts_discovered: Chưa phát hiện tài khoản IBKR nào.
+ no_accounts_discovered_description: Chạy đồng bộ sau khi cấu hình truy vấn Flex để phát hiện tài khoản.
+ setup_accounts:
+ page_title: Thiết lập tài khoản Interactive Brokers
+ dialog_title: Thiết lập tài khoản Interactive Brokers của bạn
+ subtitle: Chọn tài khoản brokerage IBKR để liên kết.
+ info_box:
+ title: Nhập Flex Query IBKR
+ items:
+ item_1: Tài sản nắm giữ với giá hiện tại và số lượng
+ item_2: Cơ sở chi phí theo vị thế
+ item_3: Giao dịch, cổ tức, hoa hồng và tiền gửi hoặc rút tiền
+ warning: Hoạt động lịch sử bị giới hạn trong khoảng thời gian báo cáo của Flex Query
+ status:
+ fetching_accounts: Đang lấy tài khoản từ Interactive Brokers...
+ no_accounts_found_title: Không tìm thấy tài khoản.
+ no_accounts_found_description: Sure không thể tìm thấy tài khoản IBKR nào trong báo cáo Flex mới nhất.
+ available_accounts:
+ title: Tài khoản có sẵn
+ account_type_investment: Đầu tư
+ account_summary: "%{account_type} • Số dư: %{balance}"
+ account_id: "Mã tài khoản: %{account_id}"
+ link_existing:
+ description: Hoặc liên kết tài khoản IBKR đã phát hiện với tài khoản đầu tư thủ công hiện có.
+ manual_account_option: "%{name} (%{balance})"
+ select_prompt: Chọn một tài khoản...
+ linked_accounts:
+ title: Đã liên kết
+ linked_to_html: "Liên kết với: %{account}"
+ buttons:
+ refresh: Làm mới
+ cancel: Hủy
+ back_to_settings: Quay lại Cài đặt
+ create_selected_accounts: Tạo tài khoản đã chọn
+ link: Liên kết
+ done: Xong
+ sync_status:
+ no_accounts: Chưa phát hiện tài khoản IBKR nào
+ all_linked:
+ one: 1 tài khoản đã liên kết
+ other: "%{count} tài khoản đã liên kết"
+ partial: "%{linked} đã liên kết, %{unlinked} cần thiết lập"
+ select_existing_account:
+ title: Liên kết tài khoản Interactive Brokers
+ no_accounts_available: Chưa có tài khoản Interactive Brokers chưa được liên kết.
+ run_sync_hint: "Chạy đồng bộ từ Cài đặt > Nhà cung cấp sau khi cập nhật truy vấn Flex."
+ wait_for_sync: Chờ quá trình đồng bộ phát hiện tài khoản hoàn tất.
+ balance: Số dư
+ link: Liên kết
+ cancel: Hủy
+ create:
+ success: Đã cấu hình Interactive Brokers thành công.
+ update:
+ success: Đã cập nhật cấu hình Interactive Brokers thành công.
+ destroy:
+ success: Đã lên lịch xóa kết nối Interactive Brokers.
+ select_accounts:
+ not_configured: Interactive Brokers chưa được cấu hình.
+ link_existing_account:
+ not_found: Không tìm thấy tài khoản hoặc cấu hình Interactive Brokers.
+ only_manual_investment: Chỉ tài khoản đầu tư thủ công mới có thể liên kết với Interactive Brokers.
+ already_linked: Tài khoản Interactive Brokers này đã được liên kết.
+ success: Đã liên kết thành công với tài khoản Interactive Brokers.
+ failed: Liên kết tài khoản Interactive Brokers thất bại.
+ complete_account_setup:
+ success:
+ one: Đã tạo thành công %{count} tài khoản Interactive Brokers.
+ other: Đã tạo thành công %{count} tài khoản Interactive Brokers.
+ none_selected: Không có tài khoản nào được chọn.
+ none_created: Không có tài khoản nào được tạo.
diff --git a/config/locales/views/ibkr_items/zh-CN.yml b/config/locales/views/ibkr_items/zh-CN.yml
new file mode 100644
index 000000000..8c6e2cc46
--- /dev/null
+++ b/config/locales/views/ibkr_items/zh-CN.yml
@@ -0,0 +1,92 @@
+---
+zh-CN:
+ providers:
+ ibkr:
+ name: Interactive Brokers
+ connection_description: 连接 Interactive Brokers Flex Web Service 报告
+ institution_name: Interactive Brokers
+ ibkr_items:
+ defaults:
+ name: Interactive Brokers
+ ibkr_item:
+ deletion_in_progress: 正在删除
+ flex_web_service: Flex Web Service
+ syncing: 正在同步
+ requires_update: 凭据需要注意
+ error: 错误
+ synced: "%{time} 前已同步。%{summary}。"
+ never_synced: 从未同步。
+ setup_accounts: 设置账户
+ delete: 删除
+ accounts_need_setup: 账户需要设置
+ accounts_need_setup_description: IBKR 的部分账户需要关联到 Sure 账户。
+ no_accounts_discovered: 尚未发现 IBKR 账户。
+ no_accounts_discovered_description: 请在配置 Flex 查询后运行一次同步,以发现账户。
+ setup_accounts:
+ page_title: 设置 Interactive Brokers 账户
+ dialog_title: 设置您的 Interactive Brokers 账户
+ subtitle: 选择要关联的 IBKR 券商账户。
+ info_box:
+ title: IBKR Flex Query 导入
+ items:
+ item_1: 具有当前价格和数量的持仓
+ item_2: 每个持仓的成本基础
+ item_3: 交易、分红、佣金,以及现金存入或取出
+ warning: 历史活动仅限于 Flex Query 的报表窗口
+ status:
+ fetching_accounts: 正在从 Interactive Brokers 获取账户...
+ no_accounts_found_title: 未找到账户。
+ no_accounts_found_description: Sure 在最新的 Flex 报告中未找到任何 IBKR 账户。
+ available_accounts:
+ title: 可用账户
+ account_type_investment: 投资
+ account_summary: "%{account_type} • 余额:%{balance}"
+ account_id: "账户 ID:%{account_id}"
+ link_existing:
+ description: 或将发现的 IBKR 账户关联到现有的手动投资账户。
+ manual_account_option: "%{name}(%{balance})"
+ select_prompt: 选择一个账户...
+ linked_accounts:
+ title: 已关联
+ linked_to_html: "已关联到:%{account}"
+ buttons:
+ refresh: 刷新
+ cancel: 取消
+ back_to_settings: 返回设置
+ create_selected_accounts: 创建所选账户
+ link: 关联
+ done: 完成
+ sync_status:
+ no_accounts: 尚未发现 IBKR 账户
+ all_linked:
+ one: 已关联 1 个账户
+ other: 已关联 %{count} 个账户
+ partial: 已关联 %{linked} 个,%{unlinked} 个需要设置
+ select_existing_account:
+ title: 关联 Interactive Brokers 账户
+ no_accounts_available: 目前没有可关联的未链接 Interactive Brokers 账户。
+ run_sync_hint: "在更新 Flex 查询后,请前往 设置 > 提供商 运行一次同步。"
+ wait_for_sync: 等待账户发现同步完成。
+ balance: 余额
+ link: 关联
+ cancel: 取消
+ create:
+ success: Interactive Brokers 配置成功。
+ update:
+ success: Interactive Brokers 配置更新成功。
+ destroy:
+ success: Interactive Brokers 连接已安排删除。
+ select_accounts:
+ not_configured: Interactive Brokers 尚未配置。
+ link_existing_account:
+ not_found: 未找到账户或 Interactive Brokers 配置。
+ only_manual_investment: 只有手动投资账户才能关联到 Interactive Brokers。
+ already_linked: 此 Interactive Brokers 账户已关联。
+ success: 已成功关联到 Interactive Brokers 账户。
+ failed: 关联 Interactive Brokers 账户失败。
+ complete_account_setup:
+ success:
+ one: 已成功创建 %{count} 个 Interactive Brokers 账户。
+ other: 已成功创建 %{count} 个 Interactive Brokers 账户。
+ none_selected: 未选择任何账户。
+ none_created: 未创建任何账户。
diff --git a/config/locales/views/impersonation_sessions/vi.yml b/config/locales/views/impersonation_sessions/vi.yml
new file mode 100644
index 000000000..a47890da8
--- /dev/null
+++ b/config/locales/views/impersonation_sessions/vi.yml
@@ -0,0 +1,25 @@
+---
+vi:
+ impersonation_sessions:
+ approve:
+ success: Yêu cầu đã được chấp thuận
+ complete:
+ success: Phiên đã hoàn tất
+ create:
+ success: Yêu cầu đã được gửi đến người dùng. Đang chờ chấp thuận.
+ join:
+ success: Đã tham gia phiên
+ leave:
+ success: Đã rời phiên
+ reject:
+ success: Yêu cầu đã bị từ chối
+ super_admin_bar:
+ super_admin: Quản trị viên cấp cao
+ jobs: Công việc
+ impersonating: Đang mạo danh
+ leave: Rời
+ terminate: Kết thúc
+ join_a_session: Tham gia phiên
+ join: Tham gia
+ uuid_placeholder: UUID
+ request_impersonation: Yêu cầu mạo danh
diff --git a/config/locales/views/imports/en.yml b/config/locales/views/imports/en.yml
index d97b7cf21..7d83f0793 100644
--- a/config/locales/views/imports/en.yml
+++ b/config/locales/views/imports/en.yml
@@ -384,6 +384,7 @@ en:
pdf_too_large: PDF file is too large. Maximum size is %{max_size}MB.
pdf_processing: Your PDF is being processed. You will receive an email when analysis is complete.
invalid_pdf: The uploaded file is not a valid PDF.
+ duplicate_pdf_unavailable: This PDF is already stored as a statement you cannot access.
document_too_large: Document file is too large. Maximum size is %{max_size}MB.
invalid_document_file_type: Invalid document file type for the active vector store.
document_uploaded: Document uploaded successfully.
@@ -425,6 +426,7 @@ en:
complete_title: Document analyzed
complete_description: We've analyzed your PDF and here's what we found.
document_type_label: Document Type
+ source_statement: Source statement
summary_label: Summary
email_sent_notice: An email has been sent to you with next steps.
back_to_imports: Back to imports
diff --git a/config/locales/views/imports/vi.yml b/config/locales/views/imports/vi.yml
new file mode 100644
index 000000000..1943a9334
--- /dev/null
+++ b/config/locales/views/imports/vi.yml
@@ -0,0 +1,436 @@
+---
+vi:
+ import:
+ qif_category_selections:
+ update:
+ success: "Đã lưu danh mục và nhãn."
+ show:
+ title: "Cấu hình & chọn"
+ description: "Xem lại định dạng ngày được phát hiện, sau đó chọn danh mục và nhãn từ tệp QIF để đưa vào %{product_name}."
+ categories_heading: Danh mục
+ categories_found:
+ one: "Tìm thấy 1 danh mục"
+ other: "Tìm thấy %{count} danh mục"
+ category_name_col: Tên danh mục
+ transactions_col: Giao dịch
+ tags_heading: Nhãn
+ tags_found:
+ one: "Tìm thấy 1 nhãn"
+ other: "Tìm thấy %{count} nhãn"
+ tag_name_col: Tên nhãn
+ txn_count:
+ one: "1 giao dịch"
+ other: "%{count} giao dịch"
+ split_warning_title: Phát hiện giao dịch phân chia
+ split_warning_description: "Tệp QIF này chứa giao dịch phân chia. Phân chia chưa được hỗ trợ, mỗi giao dịch phân chia sẽ được nhập như một giao dịch đơn với toàn bộ số tiền và không có danh mục."
+ split_badge: phân chia
+ empty_state_primary: Không tìm thấy danh mục hoặc nhãn nào trong tệp QIF này.
+ empty_state_secondary: Tất cả giao dịch sẽ được nhập mà không có danh mục hoặc nhãn.
+ submit: Tiếp tục xem xét
+ cleans:
+ show:
+ not_configured: "Vui lòng cấu hình nhập dữ liệu trước khi tiếp tục."
+ description: Chỉnh sửa dữ liệu trong bảng bên dưới. Ô đỏ là không hợp lệ.
+ errors_notice: Dữ liệu của bạn có lỗi. Di chuột qua lỗi để xem chi tiết.
+ errors_notice_mobile: Dữ liệu của bạn có lỗi. Nhấn vào tooltip lỗi để xem chi tiết.
+ title: Làm sạch dữ liệu
+ data_cleaned: Dữ liệu của bạn đã được làm sạch
+ next_step: Bước tiếp theo
+ all_rows: Tất cả hàng
+ error_rows: Hàng lỗi
+ configurations:
+ update:
+ success: Đã cấu hình nhập dữ liệu thành công.
+ account_import:
+ leave_empty: Để trống
+ default: Mặc định
+ entity_type: Loại thực thể
+ name: Tên
+ balance: Số dư
+ currency: Tiền tệ
+ balance_date: Ngày số dư
+ date_format: Định dạng ngày
+ select_format: Chọn định dạng
+ apply_configuration: Áp dụng cấu hình
+ transaction_import:
+ date_label: Ngày
+ select_column: Chọn cột
+ select_format: Chọn định dạng
+ amount_label: Số tiền
+ default: Mặc định
+ currency_label: Tiền tệ
+ format_label: Định dạng
+ amount_type_strategy_label: Chiến lược loại số tiền
+ select_strategy: Chọn chiến lược
+ set: Đặt
+ as_amount_type_column: là cột loại số tiền
+ select_value: Chọn giá trị
+ as_identifier_value: là giá trị định danh
+ treat_as_html: "Xem \"%{value}\" là"
+ income_inflow: Thu nhập (dòng vào)
+ expense_outflow: Chi tiêu (dòng ra)
+ select_type: Chọn loại
+ leave_empty: Để trống
+ account_label: Tài khoản
+ name_label: Tên
+ category_label: Danh mục
+ tags_label: Nhãn
+ notes_label: Ghi chú
+ apply_configuration: Áp dụng cấu hình
+ incomes_are_positive: Thu nhập là số dương
+ incomes_are_negative: Thu nhập là số âm
+ amount_type_label: Loại số tiền
+ select_convention: Chọn quy ước
+ date_format_label: Định dạng ngày
+ rows_to_skip_label: Bỏ qua n hàng đầu
+ trade_import:
+ select_column: Chọn cột
+ date_label: Ngày
+ quantity_label: Số lượng
+ buys_are_positive: Mua là số lượng dương
+ buys_are_negative: Mua là số lượng âm
+ default: Mặc định
+ currency_label: Tiền tệ
+ format_label: Định dạng
+ select_format: Chọn định dạng
+ ticker_label: Mã ticker
+ leave_empty: Để trống
+ stock_exchange_code_label: Mã sàn giao dịch
+ price_label: Giá
+ account_label: Tài khoản
+ name_label: Tên
+ note_label: Ghi chú
+ no_security_provider_warning: Nhà cung cấp giá chứng khoán chưa được cấu hình. Nhập giao dịch sẽ hoạt động, nhưng Sure sẽ không bổ sung lịch sử giá. Vui lòng vào cài đặt để cấu hình.
+ date_format_label: Định dạng ngày
+ apply_configuration: Áp dụng cấu hình
+ category_import:
+ button_label: Tiếp tục
+ description: Tải lên tệp CSV đơn giản (như tệp chúng tôi tạo khi bạn xuất dữ liệu). Chúng tôi sẽ tự động ánh xạ các cột cho bạn.
+ instructions: Chọn tiếp tục để phân tích CSV và chuyển sang bước làm sạch.
+ mint_import:
+ date_format_label: Định dạng ngày
+ actual_import:
+ preconfigured_notice: Chúng tôi đã cấu hình sẵn nhập Actual Budget cho bạn. Vui lòng tiến hành bước tiếp theo.
+ leave_empty: Để trống
+ date_label: Ngày
+ date_format_label: Định dạng ngày
+ amount_label: Số tiền
+ signage_convention_label: Quy ước dấu
+ incomes_are_negative: Thu nhập là số âm
+ incomes_are_positive: Thu nhập là số dương
+ account_label: Tài khoản (tùy chọn)
+ name_label: Người trả (tùy chọn)
+ category_label: Danh mục (tùy chọn)
+ notes_label: Ghi chú (tùy chọn)
+ apply_configuration: Áp dụng cấu hình
+ rule_import:
+ description: Cấu hình nhập quy tắc. Các quy tắc sẽ được tạo hoặc cập nhật dựa trên dữ liệu CSV.
+ process_button: Xử lý quy tắc
+ process_help: Nhấn nút bên dưới để xử lý CSV và tạo các hàng quy tắc.
+ show:
+ description: Chọn các cột tương ứng với từng trường trong CSV.
+ title: Cấu hình nhập dữ liệu
+ confirms:
+ sure_import:
+ title: Xác nhận nhập dữ liệu
+ description: Xem lại dữ liệu sẽ được nhập từ tệp xuất.
+ summary: Tóm tắt nhập dữ liệu
+ empty_summary: Chúng tôi không tìm thấy bản ghi nào có thể nhập trong tệp này. Tệp có thể trống hoặc các dòng không khớp định dạng xuất mong đợi.
+ publish_button: Bắt đầu nhập
+ cancel: Hủy
+ mappings:
+ create_account: Tạo tài khoản
+ csv_mapping_label: "%{mapping} trong CSV"
+ sure_mapping_label: "%{mapping} trong %{product_name}"
+ no_accounts: Bạn chưa có tài khoản nào. Vui lòng tạo tài khoản để sử dụng cho các hàng (chưa gán) trong CSV hoặc quay lại bước Làm sạch và cung cấp tên tài khoản.
+ rows_label: Hàng
+ unassigned_account: Cần tạo tài khoản mới cho các hàng chưa gán?
+ next: Tiếp theo
+ show:
+ invalid_data: "Dữ liệu không hợp lệ, vui lòng chỉnh sửa cho đến khi tất cả lỗi được giải quyết"
+ account_mapping_description: Gán tất cả tài khoản từ tệp nhập vào các tài khoản hiện có của %{product_name}. Bạn cũng có thể thêm tài khoản mới hoặc để chưa phân loại.
+ account_mapping_title: Gán tài khoản
+ account_type_mapping_description: Gán tất cả loại tài khoản từ tệp nhập vào %{product_name}
+ account_type_mapping_title: Gán loại tài khoản
+ category_mapping_description: Gán tất cả danh mục từ tệp nhập vào các danh mục hiện có của %{product_name}. Bạn cũng có thể thêm danh mục mới hoặc để chưa phân loại.
+ category_mapping_title: Gán danh mục
+ tag_mapping_description: Gán tất cả nhãn từ tệp nhập vào các nhãn hiện có của %{product_name}. Bạn cũng có thể thêm nhãn mới hoặc để chưa phân loại.
+ tag_mapping_title: Gán nhãn
+ uploads:
+ update:
+ qif_uploaded: "Tệp QIF đã được tải lên thành công."
+ show:
+ csv_invalid: "Phải là CSV hợp lệ có tiêu đề và ít nhất một hàng dữ liệu"
+ drop_csv_title: Thả CSV để tải lên
+ drop_csv_subtitle: Tệp của bạn sẽ được tải lên tự động
+ upload_csv_tab: Tải CSV lên
+ copy_paste_tab: Sao chép & Dán
+ account_optional_label: Tài khoản (tùy chọn)
+ multi_account_import: Nhập nhiều tài khoản
+ upload_csv_button: Tải CSV lên
+ paste_csv_placeholder: Dán nội dung tệp CSV của bạn tại đây
+ download_sample_csv: Tải xuống CSV mẫu
+ to_see_format: để xem định dạng CSV yêu cầu
+ qif_title: Tải tệp QIF lên
+ qif_description: Chọn tài khoản mà tệp QIF này thuộc về, rồi tải lên tệp xuất .qif từ Quicken.
+ qif_account_label: Tài khoản
+ qif_account_placeholder: Chọn tài khoản…
+ qif_file_prompt: để thêm tệp QIF tại đây
+ qif_file_hint: Chỉ tệp .qif
+ qif_submit: Tải QIF lên
+ browse: Duyệt
+ csv_file_prompt: để thêm tệp CSV tại đây
+ description: Dán hoặc tải lên tệp CSV bên dưới. Vui lòng xem hướng dẫn trong bảng bên dưới trước khi bắt đầu.
+ instructions_1: Bên dưới là CSV mẫu với các cột có thể nhập.
+ instructions_2: CSV của bạn phải có hàng tiêu đề
+ instructions_3: Bạn có thể đặt tên cột tùy ý. Bạn sẽ ánh xạ chúng ở bước sau.
+ instructions_4: Các cột được đánh dấu dấu hoa thị (*) là dữ liệu bắt buộc.
+ instructions_5: Không có dấu phẩy, ký hiệu tiền tệ và dấu ngoặc đơn trong số.
+ title: Nhập dữ liệu
+ sure_import:
+ title: Nhập từ xuất
+ description: Tải lên tệp all.ndjson từ xuất dữ liệu để khôi phục tài khoản, giao dịch, danh mục và nhiều hơn nữa.
+ drop_title: Thả NDJSON để tải lên
+ drop_subtitle: Tệp của bạn sẽ được tải lên tự động
+ browse: Duyệt
+ browse_hint: để thêm tệp all.ndjson tại đây
+ upload_button: Tải NDJSON lên
+ hint_html: Tải lên tệp all.ndjson từ tệp ZIP xuất dữ liệu
+ ndjson_invalid: Phải là NDJSON hợp lệ với ít nhất một bản ghi
+ imports:
+ mapping_labels:
+ account_type: "Loại tài khoản"
+ account: "Tài khoản"
+ category: "Danh mục"
+ tag: "Nhãn"
+ dry_run_resources:
+ transactions: "Giao dịch"
+ balances: "Số dư"
+ accounts: "Tài khoản"
+ categories: "Danh mục"
+ tags: "Nhãn"
+ rules: "Quy tắc"
+ merchants: "Nhà cung cấp"
+ recurring_transactions: "Giao dịch định kỳ"
+ transfers: "Chuyển khoản"
+ rejected_transfers: "Chuyển khoản bị từ chối"
+ trades: "Giao dịch chứng khoán"
+ holdings: "Danh mục nắm giữ"
+ valuations: "Định giá"
+ budgets: "Ngân sách"
+ budget_categories: "Danh mục ngân sách"
+ column_labels:
+ date: "Ngày"
+ amount: "Số tiền"
+ name: "Tên"
+ currency: "Tiền tệ"
+ category: "Danh mục"
+ tags: "Nhãn"
+ account: "Tài khoản"
+ notes: "Ghi chú"
+ qty: "Số lượng"
+ ticker: "Mã ticker"
+ exchange: "Sàn giao dịch"
+ price: "Giá"
+ entity_type: "Loại"
+ category_parent: "Danh mục cha"
+ category_color: "Màu sắc"
+ category_icon: "Biểu tượng Lucide"
+ update:
+ account_saved: "Đã lưu tài khoản."
+ invalid_account: "Không tìm thấy tài khoản."
+ publish:
+ started: "Quá trình nhập đã bắt đầu trong nền."
+ max_rows_exceeded: "Tệp nhập vượt quá số hàng tối đa là %{max}."
+ revert:
+ started: "Đang hoàn tác nhập dữ liệu trong nền."
+ apply_template:
+ template_applied: "Đã áp dụng mẫu."
+ no_template_found: "Không tìm thấy mẫu, vui lòng cấu hình nhập thủ công."
+ destroy:
+ deleted: "Đã xóa nhập dữ liệu."
+ failure:
+ title: Nhập thất bại
+ description: Vui lòng kiểm tra định dạng tệp, các lỗi và đảm bảo tất cả trường bắt buộc được điền, rồi thử lại.
+ try_again: Thử lại
+ success:
+ title: Nhập thành công
+ description: Dữ liệu đã được thêm thành công vào ứng dụng và sẵn sàng sử dụng.
+ back_to_dashboard: Quay lại bảng điều khiển
+ verification:
+ title: Xác minh đọc lại
+ checked: Đã kiểm tra
+ mismatches: Không khớp
+ status:
+ not_verified: Chưa xác minh
+ matched: Khớp
+ mismatch: Không khớp
+ failed: Thất bại
+ reverted: Đã hoàn tác
+ importing:
+ title: Đang nhập dữ liệu
+ description: "Nhập dữ liệu đang được thực hiện. Kiểm tra menu nhập để biết cập nhật hoặc nhấn 'Kiểm tra trạng thái' để làm mới. Bạn có thể tiếp tục sử dụng ứng dụng."
+ check_status: Kiểm tra trạng thái
+ back_to_dashboard: Quay lại bảng điều khiển
+ revert_failure:
+ title: Hoàn tác nhập thất bại
+ description: Vui lòng thử lại
+ try_again: Thử lại
+ date_format:
+ heading: Định dạng ngày
+ description: "Định dạng ngày đã được tự động phát hiện từ tệp. Thay đổi nếu ngày hiển thị không đúng."
+ preview: "Ngày được phân tích đầu tiên"
+ error_title: "Không thể phát hiện định dạng ngày"
+ error_description: "Không có định dạng ngày được hỗ trợ nào có thể phân tích các ngày trong tệp này. Vui lòng kiểm tra tệp có chứa ngày hợp lệ."
+ type_labels:
+ transaction_import: "Nhập giao dịch"
+ trade_import: "Nhập giao dịch chứng khoán"
+ account_import: "Nhập tài khoản"
+ mint_import: "Nhập từ Mint"
+ actual_import: "Nhập từ Actual"
+ qif_import: "Nhập từ QIF"
+ category_import: "Nhập danh mục"
+ rule_import: "Nhập quy tắc"
+ pdf_import: "Nhập PDF"
+ document_import: "Nhập tài liệu"
+ sure_import: "Nhập từ Sure"
+ steps:
+ upload: Tải lên
+ configure: Cấu hình
+ clean: Làm sạch
+ map: Ánh xạ
+ confirm: Xác nhận
+ select: Chọn
+ progress: "Bước %{step} trong %{total}"
+ empty:
+ message: Không tìm thấy nhập dữ liệu.
+ index:
+ title: Nhập dữ liệu
+ new: Nhập mới
+ table:
+ title: Nhập dữ liệu
+ header:
+ date: Ngày
+ operation: Thao tác
+ status: Trạng thái
+ actions: Hành động
+ row:
+ type_labels:
+ transaction_import: "Giao dịch"
+ trade_import: "Giao dịch CK"
+ account_import: "Tài khoản"
+ mint_import: "Mint"
+ actual_import: "Actual"
+ qif_import: "QIF"
+ category_import: "Danh mục"
+ rule_import: "Quy tắc"
+ pdf_import: "PDF"
+ document_import: "Tài liệu"
+ sure_import: "Sure"
+ status:
+ in_progress: Đang xử lý
+ uploading: Đang xử lý hàng
+ reverting: Đang hoàn tác
+ revert_failed: Hoàn tác thất bại
+ complete: Hoàn thành
+ failed: Thất bại
+ actions:
+ revert: Hoàn tác
+ confirm_revert: Thao tác này sẽ xóa các giao dịch đã nhập, nhưng bạn vẫn có thể xem lại và nhập lại dữ liệu bất cứ lúc nào.
+ delete: Xóa
+ view: Xem
+ empty: Chưa có nhập dữ liệu.
+ new:
+ description: Nhập từ công cụ tài chính hoặc tải lên tệp dữ liệu thô.
+ tab_financial_tools: Công cụ tài chính & Tệp
+ tab_raw_data: Dữ liệu thô
+ coming_soon: Sắp ra mắt
+ import_ynab: Nhập từ YNAB
+ import_accounts: Nhập tài khoản
+ import_categories: Nhập danh mục
+ import_mint: Nhập từ Mint
+ import_actual: Nhập từ Actual Budget
+ import_portfolio: Nhập đầu tư
+ import_rules: Nhập quy tắc
+ import_transactions: Nhập giao dịch
+ import_qif: Nhập từ Quicken (QIF)
+ import_sure: Nhập từ Sure
+ import_sure_description: Tệp xuất .ndjson đầy đủ
+ import_file: Nhập tài liệu
+ import_file_description: Phân tích bằng AI cho PDF và tải lên tệp tìm kiếm được
+ requires_account: Nhập tài khoản trước để mở khóa tùy chọn này.
+ resume: Tiếp tục nhập %{type}
+ sources: Nguồn
+ title: Nhập mới
+ create:
+ file_too_large: Tệp quá lớn. Kích thước tối đa là %{max_size}MB.
+ invalid_file_type: Loại tệp không hợp lệ. Vui lòng tải lên tệp CSV.
+ csv_uploaded: Đã tải lên CSV thành công.
+ ndjson_uploaded: Đã tải lên tệp NDJSON thành công.
+ pdf_too_large: Tệp PDF quá lớn. Kích thước tối đa là %{max_size}MB.
+ pdf_processing: PDF của bạn đang được xử lý. Bạn sẽ nhận email khi phân tích hoàn tất.
+ invalid_pdf: Tệp đã tải lên không phải PDF hợp lệ.
+ document_too_large: Tệp tài liệu quá lớn. Kích thước tối đa là %{max_size}MB.
+ invalid_document_file_type: Loại tệp tài liệu không hợp lệ cho kho vector hiện tại.
+ document_uploaded: Đã tải lên tài liệu thành công.
+ document_upload_failed: Không thể tải tài liệu lên kho vector. Vui lòng thử lại.
+ invalid_ndjson_file_type: Loại hoặc định dạng tệp không hợp lệ. Vui lòng tải lên tệp xuất .ndjson hoặc .json hợp lệ.
+ document_provider_not_configured: Chưa cấu hình kho vector để tải tài liệu lên.
+ show:
+ finalize_upload: Vui lòng hoàn tất tải lên tệp.
+ finalize_mappings: Vui lòng hoàn tất ánh xạ trước khi tiếp tục.
+ ready:
+ description: Dưới đây là tóm tắt các mục mới sẽ được thêm vào tài khoản sau khi bạn xuất bản nhập này.
+ title: Xác nhận dữ liệu nhập
+ summary_item_label: Mục
+ summary_count_label: Số lượng
+ empty_summary: Chúng tôi không tìm thấy bản ghi nào có thể nhập trong tệp này.
+ publish_import: Xuất bản nhập
+ back_to_imports: Quay lại nhập dữ liệu
+ errors:
+ custom_column_requires_inflow: "Nhập cột tùy chỉnh yêu cầu phải chọn cột dòng vào"
+ document_types:
+ bank_statement: Sao kê ngân hàng
+ credit_card_statement: Sao kê thẻ tín dụng
+ investment_statement: Sao kê đầu tư
+ financial_document: Tài liệu tài chính
+ contract: Hợp đồng
+ other: Tài liệu khác
+ unknown: Tài liệu không xác định
+ pdf_import:
+ processing_title: Đang xử lý PDF
+ processing_description: Chúng tôi đang phân tích tài liệu bằng AI. Quá trình này có thể mất chút thời gian. Bạn sẽ nhận email khi phân tích hoàn tất.
+ check_status: Kiểm tra trạng thái
+ back_to_dashboard: Quay lại bảng điều khiển
+ failed_title: Xử lý thất bại
+ failed_description: Chúng tôi không thể xử lý tài liệu PDF. Vui lòng thử lại hoặc liên hệ hỗ trợ.
+ try_again: Thử lại
+ delete_import: Xóa nhập
+ complete_title: Tài liệu đã được phân tích
+ complete_description: Chúng tôi đã phân tích PDF và đây là kết quả.
+ document_type_label: Loại tài liệu
+ summary_label: Tóm tắt
+ email_sent_notice: Email đã được gửi đến bạn với các bước tiếp theo.
+ back_to_imports: Quay lại nhập dữ liệu
+ unknown_state_title: Trạng thái không xác định
+ unknown_state_description: Nhập này ở trạng thái không mong đợi. Vui lòng quay lại nhập dữ liệu.
+ processing_failed_with_message: "%{message}"
+ processing_failed_generic: "Xử lý thất bại: %{error}"
+ ready_for_review_title: Sẵn sàng xem xét
+ ready_for_review_description: "Chúng tôi đã trích xuất %{count} giao dịch từ sao kê. Xem xét và xuất bản để thêm vào tài khoản."
+ transactions_extracted: Giao dịch đã trích xuất
+ transactions_extracted_count:
+ one: "%{count} giao dịch"
+ other: "%{count} giao dịch"
+ select_account: Nhập vào tài khoản
+ select_account_placeholder: Chọn tài khoản...
+ select_account_hint: Chọn tài khoản để nhập các giao dịch này vào.
+ no_accounts: Chưa có tài khoản. Vui lòng tạo tài khoản trước.
+ create_account: Tạo tài khoản
+ save_account: Lưu
+ publish_transactions:
+ one: "Xuất bản %{count} giao dịch"
+ other: "Xuất bản %{count} giao dịch"
+ review_transactions: Xem xét giao dịch
+ select_account_to_continue: Vui lòng chọn tài khoản ở trên để tiếp tục.
+ unknown_document_type: Không xác định
diff --git a/config/locales/views/imports/zh-CN.yml b/config/locales/views/imports/zh-CN.yml
index 8ea66765b..21be8ebf1 100644
--- a/config/locales/views/imports/zh-CN.yml
+++ b/config/locales/views/imports/zh-CN.yml
@@ -1,57 +1,249 @@
---
zh-CN:
import:
+ qif_category_selections:
+ update:
+ success: 分类和标签已保存。
+ show:
+ title: 配置并选择
+ description: 请先检查检测到的日期格式,然后选择要从 QIF 文件导入到 %{product_name} 的分类和标签。
+ categories_heading: 分类
+ categories_found:
+ one: 找到 1 个分类
+ other: 找到 %{count} 个分类
+ category_name_col: 分类名称
+ transactions_col: 交易
+ tags_heading: 标签
+ tags_found:
+ one: 找到 1 个标签
+ other: 找到 %{count} 个标签
+ tag_name_col: 标签名称
+ txn_count:
+ one: 1 笔交易
+ other: "%{count} 笔交易"
+ split_warning_title: 检测到拆分交易
+ split_warning_description: 此 QIF 文件包含拆分交易。当前尚不支持拆分,因此每笔拆分交易将作为一笔完整金额、无分类的单笔交易导入。各个拆分明细不会被保留。
+ split_badge: 拆分
+ empty_state_primary: 此 QIF 文件中未找到任何分类或标签。
+ empty_state_secondary: 所有交易都将以无分类或无标签的形式导入。
+ submit: 继续审核
cleans:
show:
- description: 请在下方表格中编辑您的数据。红色单元格表示无效数据。
+ not_configured: 请先配置导入设置再继续。
+ description: 在下面的表格中编辑您的数据。红色单元格表示无效。
errors_notice: 您的数据中存在错误。将鼠标悬停在错误上可查看详情。
errors_notice_mobile: 您的数据中存在错误。点击错误提示可查看详情。
- title: 数据清洗
+ title: 清理您的数据
+ data_cleaned: 您的数据已清理完成
+ next_step: 下一步
+ all_rows: 所有行
+ error_rows: 错误行
configurations:
+ update:
+ success: 导入配置成功。
+ account_import:
+ leave_empty: 留空
+ default: 默认
+ entity_type: 实体类型
+ name: 名称
+ balance: 余额
+ currency: 货币
+ balance_date: 余额日期
+ date_format: 日期格式
+ select_format: 选择格式
+ apply_configuration: 应用配置
+ transaction_import:
+ date_label: 日期
+ select_column: 选择列
+ select_format: 选择格式
+ amount_label: 金额
+ default: 默认
+ currency_label: 货币
+ format_label: 格式
+ amount_type_strategy_label: 金额类型策略
+ select_strategy: 选择策略
+ set: 设为
+ as_amount_type_column: 作为金额类型列
+ select_value: 选择值
+ as_identifier_value: 作为标识值
+ treat_as_html: 将“%{value}”视为
+ income_inflow: 收入(流入)
+ expense_outflow: 支出(流出)
+ select_type: 选择类型
+ leave_empty: 留空
+ account_label: 账户
+ name_label: 名称
+ category_label: 分类
+ tags_label: 标签
+ notes_label: 备注
+ apply_configuration: 应用配置
+ incomes_are_positive: 收入为正数
+ incomes_are_negative: 收入为负数
+ amount_type_label: 金额类型
+ select_convention: 选择约定
+ date_format_label: 日期格式
+ rows_to_skip_label: 跳过前 n 行
+ trade_import:
+ select_column: 选择列
+ date_label: 日期
+ quantity_label: 数量
+ buys_are_positive: 买入为正数量
+ buys_are_negative: 买入为负数量
+ default: 默认
+ currency_label: 货币
+ format_label: 格式
+ select_format: 选择格式
+ ticker_label: 代码
+ leave_empty: 留空
+ stock_exchange_code_label: 证券交易所代码
+ price_label: 价格
+ account_label: 账户
+ name_label: 名称
+ note_label: 备注
+ no_security_provider_warning: 证券价格提供方尚未配置。您的交易导入仍可进行,但 Sure 不会回填价格历史。请前往设置进行配置。
+ date_format_label: 日期格式
+ apply_configuration: 应用配置
category_import:
button_label: 继续
- description: 上传简单的 CSV 文件(例如我们导出数据时生成的文件格式)。我们将自动为您映射列。
- instructions: 选择继续以解析您的 CSV 文件并进入清洗步骤。
+ description: 上传一个简单 CSV 文件(例如我们在导出数据时生成的那个)。系统会自动为您映射列。
+ instructions: 选择继续即可解析 CSV 并进入清理步骤。
mint_import:
date_format_label: 日期格式
+ actual_import:
+ preconfigured_notice: 我们已为您的 Actual Budget 导入预先配置好了。请继续下一步。
+ leave_empty: 留空
+ date_label: 日期
+ date_format_label: 日期格式
+ amount_label: 金额
+ signage_convention_label: 符号约定
+ incomes_are_negative: 收入为负数
+ incomes_are_positive: 收入为正数
+ account_label: 账户(可选)
+ name_label: 收款方(可选)
+ category_label: 分类(可选)
+ notes_label: 备注(可选)
+ apply_configuration: 应用配置
rule_import:
- description: 配置您的规则导入。规则将根据 CSV 数据创建或更新。
+ description: 配置您的规则导入。系统将根据 CSV 数据创建或更新规则。
process_button: 处理规则
process_help: 点击下方按钮处理您的 CSV 并生成规则行。
show:
- description: 请选择 CSV 文件中各列对应的字段。
- title: 配置导入设置
- trade_import:
- date_format_label: 日期格式
- transaction_import:
- date_format_label: 日期格式
+ description: 选择与 CSV 中每个字段对应的列。
+ title: 配置您的导入
confirms:
+ sure_import:
+ title: 确认您的导入
+ description: 检查将从导出文件中导入的数据。
+ summary: 导入摘要
+ empty_summary: 我们在此文件中找不到任何可导入的记录。它可能为空,或者这些行与预期的导出格式不匹配(每行应为一个 JSON 对象,包含 "type" 和 "data" 键,并使用此导入所支持的类型)。
+ publish_button: 开始导入
+ cancel: 取消
mappings:
- create_account: 新建账户
+ create_account: 创建账户
csv_mapping_label: CSV 中的 %{mapping}
- no_accounts: 您目前没有任何账户。请创建一个账户用于处理 CSV 中的(未分配)行,或返回上一步的“清洗”环节提供一个可用的账户名称。
- rows_label: 行数
sure_mapping_label: "%{product_name} 中的 %{mapping}"
- unassigned_account: 需要为未分配行创建新账户?
+ no_accounts: 您还没有任何账户。请创建一个账户,用于 CSV 中未分配的行,或者返回清理步骤并提供一个我们可以使用的账户名称。
+ rows_label: 行
+ unassigned_account: 是否需要为未分配的行创建新账户?
+ next: 下一步
show:
- account_mapping_description: 将导入文件中的所有账户分配到 %{product_name} 的现有账户。您也可以新建账户或保留为未分类。
- account_mapping_title: 分配账户
+ invalid_data: 您的数据无效,请编辑直到所有错误都解决
+ account_mapping_description: 将导入文件中的所有账户映射到 %{product_name} 中现有的账户。您也可以添加新账户,或将其保留为未分类。
+ account_mapping_title: 分配您的账户
account_type_mapping_description: 将导入文件中的所有账户类型分配到 %{product_name} 的账户类型
- account_type_mapping_title: 分配账户类型
- category_mapping_description: 将导入文件中的所有分类分配到 %{product_name} 的现有分类。您也可以新建分类或保留为未分类。
- category_mapping_title: 分配分类
- tag_mapping_description: 将导入文件中的所有标签分配到 %{product_name} 的现有标签。您也可以新建标签或保留为未分类。
- tag_mapping_title: 分配标签
+ account_type_mapping_title: 分配您的账户类型
+ category_mapping_description: 将导入文件中的所有分类分配到 %{product_name} 中现有的分类。您也可以添加新分类,或将其保留为未分类。
+ category_mapping_title: 分配您的分类
+ tag_mapping_description: 将导入文件中的所有标签分配到 %{product_name} 中现有的标签。您也可以添加新标签,或将其保留为未分类。
+ tag_mapping_title: 分配您的标签
uploads:
+ update:
+ qif_uploaded: QIF 文件上传成功。
show:
- description: 请在下方粘贴或上传您的 CSV 文件。开始前请仔细阅读下表中的说明。
- instructions_1: 下方是可供导入的 CSV 示例文件及列说明。
- instructions_2: 您的 CSV 文件必须包含表头行
- instructions_3: 您可以为各列自由命名,稍后步骤中将进行字段映射。
- instructions_4: 标有星号 (*) 的列为必填数据。
- instructions_5: 数字中请勿包含逗号、货币符号或括号。
- title: 导入数据
+ csv_invalid: 必须是有效的 CSV,且至少包含标题和一行数据
+ drop_csv_title: 拖放 CSV 以上传
+ drop_csv_subtitle: 您的文件将自动上传
+ upload_csv_tab: 上传 CSV
+ copy_paste_tab: 复制并粘贴
+ account_optional_label: 账户(可选)
+ multi_account_import: 多账户导入
+ upload_csv_button: 上传 CSV
+ paste_csv_placeholder: 将 CSV 文件内容粘贴到这里
+ download_sample_csv: 下载示例 CSV
+ to_see_format: 以查看所需的 CSV 格式
+ qif_title: 上传 QIF 文件
+ qif_description: 选择此 QIF 文件所属的账户,然后上传您从 Quicken 导出的 .qif 文件。
+ qif_account_label: 账户
+ qif_account_placeholder: 选择一个账户…
+ qif_file_prompt: 将您的 QIF 文件添加到这里
+ qif_file_hint: 仅限 .qif 文件
+ qif_submit: 上传 QIF
+ browse: 浏览
+ csv_file_prompt: 将您的 CSV 文件添加到这里
+ description: 请在下方粘贴或上传您的 CSV 文件。开始前请先阅读下方表格中的说明。
+ instructions_1: 下方示例 CSV 展示了可用于导入的列。
+ instructions_2: 您的 CSV 必须有表头行
+ instructions_3: 列名可以随意命名。后续步骤中您会进行映射。
+ instructions_4: 标有星号(*)的列为必填数据。
+ instructions_5: 数字中不要包含逗号、货币符号和括号。
+ title: 导入您的数据
+ sure_import:
+ title: 从导出文件导入
+ description: 上传您的数据导出 ZIP 中的 all.ndjson 文件,以恢复您的账户、交易、分类等数据。
+ drop_title: 拖放 NDJSON 以上传
+ drop_subtitle: 您的文件将自动上传
+ browse: 浏览
+ browse_hint: 将您的 all.ndjson 文件添加到这里
+ upload_button: 上传 NDJSON
+ hint_html: 上传您数据导出 ZIP 中的 all.ndjson 文件
+ ndjson_invalid: 必须是有效的 NDJSON,且至少包含一条记录
imports:
+ mapping_labels:
+ account_type: 账户类型
+ account: 账户
+ category: 分类
+ tag: 标签
+ dry_run_resources:
+ transactions: 交易
+ balances: 余额
+ accounts: 账户
+ categories: 分类
+ tags: 标签
+ rules: 规则
+ merchants: 商户
+ recurring_transactions: 循环交易
+ transfers: 转账
+ rejected_transfers: 被拒绝的转账
+ trades: 交易
+ holdings: 持仓
+ valuations: 估值
+ budgets: 预算
+ budget_categories: 预算分类
+ column_labels:
+ date: 日期
+ amount: 金额
+ name: 名称
+ currency: 货币
+ category: 分类
+ tags: 标签
+ account: 账户
+ notes: 备注
+ qty: 数量
+ ticker: 代码
+ exchange: 交易所
+ price: 价格
+ entity_type: 类型
+ category_parent: 父分类
+ category_color: 颜色
+ category_icon: Lucide 图标
+ update:
+ account_saved: 账户已保存。
+ invalid_account: 未找到账户。
+ publish:
+ started: 您的导入已在后台开始。
+ max_rows_exceeded: 您的导入超过了最大行数 %{max}。
+ revert:
+ started: 导入正在后台回滚。
index:
title: 导入记录
new: 新建导入
diff --git a/config/locales/views/indexa_capital_items/vi.yml b/config/locales/views/indexa_capital_items/vi.yml
new file mode 100644
index 000000000..2e0ab47bb
--- /dev/null
+++ b/config/locales/views/indexa_capital_items/vi.yml
@@ -0,0 +1,239 @@
+---
+vi:
+ indexa_capital_items:
+ sync_status:
+ no_accounts: "Không tìm thấy tài khoản"
+ synced:
+ one: "%{count} tài khoản đã đồng bộ"
+ other: "%{count} tài khoản đã đồng bộ"
+ synced_with_setup: "%{linked} đã đồng bộ, %{unlinked} cần thiết lập"
+ institution_summary:
+ none: "Chưa kết nối tổ chức nào"
+ count:
+ one: "%{count} tổ chức"
+ other: "%{count} tổ chức"
+ errors:
+ provider_not_configured: "Nhà cung cấp Indexa Capital chưa được cấu hình"
+
+ sync:
+ status:
+ importing: "Đang nhập tài khoản từ Indexa Capital..."
+ processing: "Đang xử lý tài sản nắm giữ và hoạt động..."
+ calculating: "Đang tính toán số dư..."
+ importing_data: "Đang nhập dữ liệu tài khoản..."
+ checking_setup: "Đang kiểm tra cấu hình tài khoản..."
+ needs_setup: "%{count} tài khoản cần thiết lập..."
+ success: "Đã bắt đầu đồng bộ"
+
+ panel:
+ setup_instructions: "Hướng dẫn thiết lập:"
+ step_1: "Truy cập bảng điều khiển Indexa Capital để tạo mã thông báo API chỉ đọc"
+ step_2: "Dán mã thông báo API của bạn bên dưới và nhấn Lưu"
+ step_3: "Sau khi kết nối thành công, đi đến tab Tài khoản để thiết lập tài khoản mới"
+ field_descriptions: "Mô tả các trường:"
+ optional: "(Tùy chọn)"
+ required: "(bắt buộc)"
+ optional_with_default: "(tùy chọn, mặc định là %{default_value})"
+ alternative_auth: "Hoặc sử dụng xác thực tên đăng nhập/mật khẩu thay thế..."
+ save_button: "Lưu cấu hình"
+ update_button: "Cập nhật cấu hình"
+ fields:
+ api_token:
+ label: "Mã thông báo API"
+ description: "Mã thông báo API chỉ đọc từ bảng điều khiển Indexa Capital"
+ placeholder_new: "Dán mã thông báo API vào đây"
+ placeholder_update: "Nhập mã thông báo API mới để cập nhật"
+ username:
+ label: "Tên đăng nhập"
+ description: "Tên đăng nhập/email Indexa Capital của bạn"
+ placeholder_new: "Dán tên đăng nhập vào đây"
+ placeholder_update: "Nhập tên đăng nhập mới để cập nhật"
+ document:
+ label: "Mã tài liệu"
+ description: "Mã tài liệu/ID Indexa Capital của bạn"
+ placeholder_new: "Dán mã tài liệu vào đây"
+ placeholder_update: "Nhập mã tài liệu mới để cập nhật"
+ password:
+ label: "Mật khẩu"
+ description: "Mật khẩu Indexa Capital của bạn"
+ placeholder_new: "Dán mật khẩu vào đây"
+ placeholder_update: "Nhập mật khẩu mới để cập nhật"
+
+ create:
+ success: "Kết nối Indexa Capital đã được tạo thành công"
+ update:
+ success: "Kết nối Indexa Capital đã được cập nhật"
+ destroy:
+ success: "Kết nối Indexa Capital đã được xóa"
+ index:
+ title: "Kết nối Indexa Capital"
+
+ loading:
+ loading_message: "Đang tải tài khoản Indexa Capital..."
+ loading_title: "Đang tải"
+
+ link_accounts:
+ all_already_linked:
+ one: "Tài khoản đã chọn (%{names}) đã được liên kết"
+ other: "Tất cả %{count} tài khoản đã chọn đã được liên kết: %{names}"
+ api_error: "Lỗi API: %{message}"
+ invalid_account_names:
+ one: "Không thể liên kết tài khoản không có tên"
+ other: "Không thể liên kết %{count} tài khoản không có tên"
+ link_failed: "Liên kết tài khoản thất bại"
+ no_accounts_selected: "Vui lòng chọn ít nhất một tài khoản"
+ no_api_key: "Không tìm thấy thông tin xác thực Indexa Capital. Vui lòng cấu hình trong Cài đặt nhà cung cấp."
+ partial_invalid: "Đã liên kết thành công %{created_count} tài khoản, %{already_linked_count} đã được liên kết, %{invalid_count} tài khoản có tên không hợp lệ"
+ partial_success: "Đã liên kết thành công %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết: %{already_linked_names}"
+ success:
+ one: "Đã liên kết thành công %{count} tài khoản"
+ other: "Đã liên kết thành công %{count} tài khoản"
+
+ indexa_capital_item:
+ accounts_need_setup: "Tài khoản cần thiết lập"
+ delete: "Xóa kết nối"
+ deletion_in_progress: "đang xóa..."
+ error: "Lỗi"
+ more_accounts_available:
+ one: "%{count} tài khoản khác có sẵn"
+ other: "%{count} tài khoản khác có sẵn"
+ no_accounts_description: "Kết nối này chưa có tài khoản nào được liên kết."
+ no_accounts_title: "Không có tài khoản"
+ provider_name: "Indexa Capital"
+ requires_update: "Kết nối cần cập nhật"
+ setup_action: "Thiết lập tài khoản mới"
+ setup_description: "%{linked} trong số %{total} tài khoản đã liên kết. Chọn loại tài khoản cho các tài khoản Indexa Capital mới được nhập."
+ setup_needed: "Tài khoản mới sẵn sàng để thiết lập"
+ status: "Đồng bộ %{timestamp} trước — %{summary}"
+ status_never: "Chưa bao giờ đồng bộ"
+ syncing: "Đang đồng bộ..."
+ total: "Tổng cộng"
+ unlinked: "Chưa liên kết"
+ update_credentials: "Cập nhật thông tin xác thực"
+
+ select_accounts:
+ accounts_selected: "tài khoản đã chọn"
+ api_error: "Lỗi API: %{message}"
+ cancel: "Hủy"
+ configure_name_in_provider: "Không thể nhập - vui lòng cấu hình tên tài khoản trong Indexa Capital"
+ description: "Chọn tài khoản bạn muốn liên kết với tài khoản %{product_name} của bạn."
+ link_accounts: "Liên kết tài khoản đã chọn"
+ no_accounts_found: "Không tìm thấy tài khoản. Vui lòng kiểm tra thông tin xác thực Indexa Capital."
+ no_api_key: "Thông tin xác thực Indexa Capital chưa được cấu hình. Vui lòng cấu hình trong Cài đặt."
+ no_credentials_configured: "Vui lòng cấu hình thông tin xác thực Indexa Capital trước trong Cài đặt nhà cung cấp."
+ no_name_placeholder: "(Không có tên)"
+ title: "Chọn tài khoản Indexa Capital"
+
+ select_existing_account:
+ account_already_linked: "Tài khoản này đã được liên kết với nhà cung cấp"
+ all_accounts_already_linked: "Tất cả tài khoản Indexa Capital đã được liên kết"
+ api_error: "Lỗi API: %{message}"
+ balance_label: "Số dư:"
+ cancel: "Hủy"
+ cancel_button: "Hủy"
+ configure_name_in_provider: "Không thể nhập - vui lòng cấu hình tên tài khoản trong Indexa Capital"
+ connect_hint: "Kết nối tài khoản Indexa Capital để bật đồng bộ tự động."
+ description: "Chọn tài khoản Indexa Capital để liên kết với tài khoản này. Giao dịch sẽ được đồng bộ và loại bỏ trùng lặp tự động."
+ header: "Liên kết với Indexa Capital"
+ link_account: "Liên kết tài khoản"
+ link_button: "Liên kết tài khoản này"
+ linking_to: "Đang liên kết với:"
+ no_account_specified: "Chưa chỉ định tài khoản"
+ no_accounts: "Không tìm thấy tài khoản Indexa Capital chưa liên kết."
+ no_accounts_found: "Không tìm thấy tài khoản Indexa Capital. Vui lòng kiểm tra thông tin xác thực."
+ no_api_key: "Thông tin xác thực Indexa Capital chưa được cấu hình. Vui lòng cấu hình trong Cài đặt."
+ no_credentials_configured: "Vui lòng cấu hình thông tin xác thực Indexa Capital trước trong Cài đặt nhà cung cấp."
+ no_name_placeholder: "(Không có tên)"
+ settings_link: "Đi đến Cài đặt nhà cung cấp"
+ subtitle: "Chọn tài khoản Indexa Capital"
+ title: "Liên kết %{account_name} với Indexa Capital"
+
+ link_existing_account:
+ account_already_linked: "Tài khoản này đã được liên kết với nhà cung cấp"
+ api_error: "Lỗi API: %{message}"
+ invalid_account_name: "Không thể liên kết tài khoản không có tên"
+ provider_account_already_linked: "Tài khoản Indexa Capital này đã được liên kết với tài khoản khác"
+ provider_account_not_found: "Không tìm thấy tài khoản Indexa Capital"
+ missing_parameters: "Thiếu tham số bắt buộc"
+ no_api_key: "Không tìm thấy thông tin xác thực Indexa Capital. Vui lòng cấu hình trong Cài đặt nhà cung cấp."
+ success: "Đã liên kết thành công %{account_name} với Indexa Capital"
+
+ setup_accounts:
+ account_type_label: "Loại tài khoản:"
+ accounts_count:
+ one: "%{count} tài khoản có sẵn"
+ other: "%{count} tài khoản có sẵn"
+ all_accounts_linked: "Tất cả tài khoản Indexa Capital của bạn đã được thiết lập."
+ api_error: "Lỗi API: %{message}"
+ creating: "Đang tạo tài khoản..."
+ fetch_failed: "Tải tài khoản thất bại"
+ import_selected: "Nhập tài khoản đã chọn"
+ instructions: "Chọn tài khoản bạn muốn nhập từ Indexa Capital. Bạn có thể chọn nhiều tài khoản."
+ no_accounts: "Không tìm thấy tài khoản chưa liên kết từ kết nối Indexa Capital này."
+ no_accounts_to_setup: "Không có tài khoản nào để thiết lập"
+ no_api_key: "Thông tin xác thực Indexa Capital chưa được cấu hình. Vui lòng kiểm tra cài đặt kết nối."
+ select_all: "Chọn tất cả"
+ account_types:
+ skip: "Bỏ qua tài khoản này"
+ depository: "Tài khoản thanh toán hoặc tiết kiệm"
+ credit_card: "Thẻ tín dụng"
+ investment: "Tài khoản đầu tư"
+ crypto: "Tài khoản tiền điện tử"
+ loan: "Vay hoặc thế chấp"
+ other_asset: "Tài sản khác"
+ subtype_labels:
+ depository: "Loại phụ tài khoản:"
+ credit_card: ""
+ investment: "Loại đầu tư:"
+ crypto: ""
+ loan: "Loại vay:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Thẻ tín dụng sẽ được tự động thiết lập dưới dạng tài khoản thẻ tín dụng."
+ other_asset: "Không cần tùy chọn bổ sung cho Tài sản khác."
+ crypto: "Tài khoản tiền điện tử sẽ được thiết lập để theo dõi tài sản nắm giữ và giao dịch."
+ subtypes:
+ depository:
+ checking: "Tài khoản thanh toán"
+ savings: "Tài khoản tiết kiệm"
+ hsa: "Tài khoản tiết kiệm y tế"
+ cd: "Chứng chỉ tiền gửi"
+ money_market: "Thị trường tiền tệ"
+ investment:
+ brokerage: "Brokerage"
+ pension: "Lương hưu"
+ retirement: "Hưu trí"
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: "Thrift Savings Plan"
+ "529_plan": "529 Plan"
+ hsa: "Tài khoản tiết kiệm y tế"
+ mutual_fund: "Quỹ tương hỗ"
+ ira: "Traditional IRA"
+ roth_ira: "Roth IRA"
+ angel: "Angel"
+ loan:
+ mortgage: "Thế chấp"
+ student: "Vay sinh viên"
+ auto: "Vay mua xe"
+ other: "Vay khác"
+ balance: "Số dư"
+ cancel: "Hủy"
+ choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Indexa Capital:"
+ create_accounts: "Tạo tài khoản"
+ creating_accounts: "Đang tạo tài khoản..."
+ historical_data_range: "Phạm vi dữ liệu lịch sử:"
+ subtitle: "Chọn đúng loại tài khoản cho các tài khoản đã nhập"
+ sync_start_date_help: "Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ."
+ sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:"
+ title: "Thiết lập tài khoản Indexa Capital của bạn"
+
+ complete_account_setup:
+ all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo."
+ creation_failed: "Tạo tài khoản thất bại: %{error}"
+ no_accounts: "Không có tài khoản nào để thiết lập."
+ success: "Đã tạo thành công %{count} tài khoản."
+
+ preload_accounts:
+ no_credentials_configured: "Vui lòng cấu hình thông tin xác thực Indexa Capital trước trong Cài đặt nhà cung cấp."
diff --git a/config/locales/views/indexa_capital_items/zh-CN.yml b/config/locales/views/indexa_capital_items/zh-CN.yml
new file mode 100644
index 000000000..f59c271dd
--- /dev/null
+++ b/config/locales/views/indexa_capital_items/zh-CN.yml
@@ -0,0 +1,227 @@
+---
+zh-CN:
+ indexa_capital_items:
+ sync_status:
+ no_accounts: 未找到账户
+ synced:
+ one: 已同步 %{count} 个账户
+ other: 已同步 %{count} 个账户
+ synced_with_setup: 已同步 %{linked} 个,%{unlinked} 个需要设置
+ institution_summary:
+ none: 未连接任何机构
+ count:
+ one: "%{count} 个机构"
+ other: "%{count} 个机构"
+ errors:
+ provider_not_configured: Indexa Capital 提供商未配置
+ sync:
+ status:
+ importing: 正在从 Indexa Capital 导入账户...
+ processing: 正在处理持仓和活动...
+ calculating: 正在计算余额...
+ importing_data: 正在导入账户数据...
+ checking_setup: 正在检查账户配置...
+ needs_setup: "%{count} 个账户需要设置..."
+ success: 已开始同步
+ panel:
+ setup_instructions: "设置说明:"
+ step_1: 前往您的 Indexa Capital 仪表盘生成只读 API token
+ step_2: 将您的 API token 粘贴到下方并点击保存
+ step_3: 连接成功后,前往账户标签页设置新账户
+ field_descriptions: "字段说明:"
+ optional: (可选)
+ required: (必填)
+ optional_with_default: (可选,默认为 %{default_value})
+ alternative_auth: 或者改用用户名/密码认证...
+ save_button: 保存配置
+ update_button: 更新配置
+ fields:
+ api_token:
+ label: API Token
+ description: 来自 Indexa Capital 仪表盘的只读 API token
+ placeholder_new: 在此粘贴您的 API token
+ placeholder_update: 输入新的 API token 以更新
+ username:
+ label: 用户名
+ description: 您的 Indexa Capital 用户名/邮箱
+ placeholder_new: 在此粘贴用户名
+ placeholder_update: 输入新的用户名以更新
+ document:
+ label: Document ID
+ description: 您的 Indexa Capital document/ID
+ placeholder_new: 在此粘贴 document ID
+ placeholder_update: 输入新的 document ID 以更新
+ password:
+ label: 密码
+ description: 您的 Indexa Capital 密码
+ placeholder_new: 在此粘贴密码
+ placeholder_update: 输入新的密码以更新
+ create:
+ success: Indexa Capital 连接创建成功
+ update:
+ success: Indexa Capital 连接已更新
+ destroy:
+ success: Indexa Capital 连接已移除
+ index:
+ title: Indexa Capital 连接
+ loading:
+ loading_message: 正在加载 Indexa Capital 账户...
+ loading_title: 正在加载
+ link_accounts:
+ all_already_linked:
+ one: 选定账户(%{names})已关联
+ other: 所有选定的 %{count} 个账户都已关联:%{names}
+ api_error: "API 错误:%{message}"
+ invalid_account_names:
+ one: 无法关联空名称账户
+ other: 无法关联 %{count} 个空名称账户
+ link_failed: 关联账户失败
+ no_accounts_selected: 请至少选择一个账户
+ no_api_key: 未找到 Indexa Capital 凭据。请在提供商设置中配置。
+ partial_invalid: "成功关联 %{created_count} 个账户,%{already_linked_count} 个已关联,%{invalid_count} 个账户名称无效"
+ partial_success: "成功关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联:%{already_linked_names}"
+ success:
+ one: "成功关联 %{count} 个账户"
+ other: "成功关联 %{count} 个账户"
+ indexa_capital_item:
+ accounts_need_setup: 账户需要设置
+ delete: 删除连接
+ deletion_in_progress: 正在删除...
+ error: 错误
+ more_accounts_available:
+ one: 还有 %{count} 个账户可用
+ other: 还有 %{count} 个账户可用
+ no_accounts_description: 此连接尚未关联任何账户。
+ no_accounts_title: 无账户
+ provider_name: Indexa Capital
+ requires_update: 连接需要更新
+ setup_action: 设置新账户
+ setup_description: 已关联 %{linked} / %{total} 个账户。请选择新导入的 Indexa Capital 账户类型。
+ setup_needed: 新账户已准备好设置
+ status: "%{timestamp} 前已同步 — %{summary}"
+ status_never: 从未同步
+ syncing: 正在同步...
+ total: 总计
+ unlinked: 未关联
+ update_credentials: 更新凭据
+ select_accounts:
+ accounts_selected: 已选择账户
+ api_error: "API 错误:%{message}"
+ cancel: 取消
+ configure_name_in_provider: 无法导入 - 请先在 Indexa Capital 中配置账户名称
+ description: 选择您想链接到 %{product_name} 账户的账户。
+ link_accounts: 链接所选账户
+ no_accounts_found: 未找到账户。请检查您的 Indexa Capital 凭据。
+ no_api_key: 未配置 Indexa Capital 凭据。请在设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Indexa Capital 凭据。
+ no_name_placeholder: (无名称)
+ title: 选择 Indexa Capital 账户
+ select_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ all_accounts_already_linked: 所有 Indexa Capital 账户都已关联
+ api_error: "API 错误:%{message}"
+ balance_label: 余额:
+ cancel: 取消
+ cancel_button: 取消
+ configure_name_in_provider: 无法导入 - 请先在 Indexa Capital 中配置账户名称
+ connect_hint: 连接一个 Indexa Capital 账户以启用自动同步。
+ description: 选择一个 Indexa Capital 账户与此账户关联。交易将自动同步并去重。
+ header: 关联到 Indexa Capital
+ link_account: 关联账户
+ link_button: 关联此账户
+ linking_to: 正在关联到:
+ no_account_specified: 未指定账户
+ no_accounts: 未找到未关联的 Indexa Capital 账户。
+ no_accounts_found: 未找到 Indexa Capital 账户。请检查您的凭据。
+ no_api_key: 未配置 Indexa Capital 凭据。请在设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Indexa Capital 凭据。
+ no_name_placeholder: (无名称)
+ settings_link: 前往提供商设置
+ subtitle: 选择一个 Indexa Capital 账户
+ title: 将 %{account_name} 与 Indexa Capital 关联
+ link_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ api_error: "API 错误:%{message}"
+ invalid_account_name: 无法关联空名称账户
+ provider_account_already_linked: 此 Indexa Capital 账户已关联到另一个账户
+ provider_account_not_found: 未找到 Indexa Capital 账户
+ missing_parameters: 缺少必需参数
+ no_api_key: 未找到 Indexa Capital 凭据。请在提供商设置中配置。
+ success: 已成功将 %{account_name} 与 Indexa Capital 关联
+ setup_accounts:
+ account_type_label: 账户类型:
+ accounts_count:
+ one: 有 %{count} 个账户可用
+ other: 有 %{count} 个账户可用
+ all_accounts_linked: 您的所有 Indexa Capital 账户都已设置完成。
+ api_error: "API 错误:%{message}"
+ creating: 正在创建账户...
+ fetch_failed: 获取账户失败
+ import_selected: 导入所选账户
+ instructions: 选择您想从 Indexa Capital 导入的账户。您可以选择多个账户。
+ no_accounts: 此 Indexa Capital 连接中未找到未关联账户。
+ no_accounts_to_setup: 没有需要设置的账户
+ no_api_key: 未配置 Indexa Capital 凭据。请检查您的连接设置。
+ select_all: 全选
+ account_types:
+ skip: 跳过此账户
+ depository: 支票账户或储蓄账户
+ credit_card: 信用卡
+ investment: 投资账户
+ crypto: 加密货币账户
+ loan: 贷款或抵押贷款
+ other_asset: 其他资产
+ subtype_labels:
+ depository: 账户子类型:
+ credit_card: ""
+ investment: 投资类型:
+ crypto: ""
+ loan: 贷款类型:
+ other_asset: ""
+ subtype_messages:
+ credit_card: 信用卡将自动设为信用卡账户。
+ other_asset: 其他资产无需额外选项。
+ crypto: 加密货币账户将用于跟踪持仓和交易。
+ subtypes:
+ depository:
+ checking: 支票账户
+ savings: 储蓄账户
+ hsa: 健康储蓄账户
+ cd: 定期存单
+ money_market: 货币市场
+ investment:
+ brokerage: 券商账户
+ pension: 养老金
+ retirement: 退休账户
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: 节俭储蓄计划
+ "529_plan": "529 计划"
+ hsa: 健康储蓄账户
+ mutual_fund: 共同基金
+ ira: 传统 IRA
+ roth_ira: Roth IRA
+ angel: 天使投资
+ loan:
+ mortgage: 抵押贷款
+ student: 学生贷款
+ auto: 汽车贷款
+ other: 其他贷款
+ balance: 余额
+ cancel: 取消
+ choose_account_type: "为每个 Indexa Capital 账户选择正确的账户类型:"
+ create_accounts: 创建账户
+ creating_accounts: 正在创建账户...
+ historical_data_range: "历史数据范围:"
+ subtitle: 为您导入的账户选择正确的账户类型
+ sync_start_date_help: 选择您希望向前同步交易历史的时间范围。
+ sync_start_date_label: "从以下日期开始同步交易:"
+ title: 设置您的 Indexa Capital 账户
+ complete_account_setup:
+ all_skipped: 所有账户都已跳过,未创建任何账户。
+ creation_failed: 创建账户失败:%{error}
+ no_accounts: 没有需要设置的账户。
+ success: 成功创建 %{count} 个账户。
+ preload_accounts:
+ no_credentials_configured: 请先在提供商设置中配置您的 Indexa Capital 凭据。
diff --git a/config/locales/views/investments/vi.yml b/config/locales/views/investments/vi.yml
new file mode 100644
index 000000000..7b3a504cd
--- /dev/null
+++ b/config/locales/views/investments/vi.yml
@@ -0,0 +1,196 @@
+---
+vi:
+ investments:
+ edit:
+ edit: Chỉnh sửa %{account}
+ form:
+ none: Không có
+ subtype_prompt: Chọn loại đầu tư
+ new:
+ title: Nhập số dư tài khoản
+ show:
+ chart_title: Tổng giá trị
+ subtypes:
+ # United States
+ brokerage:
+ short: Brokerage
+ long: Brokerage
+ 401k:
+ short: 401(k)
+ long: 401(k)
+ roth_401k:
+ short: Roth 401(k)
+ long: Roth 401(k)
+ 403b:
+ short: 403(b)
+ long: 403(b)
+ 457b:
+ short: 457(b)
+ long: 457(b)
+ tsp:
+ short: TSP
+ long: Thrift Savings Plan
+ ira:
+ short: IRA
+ long: Traditional IRA
+ roth_ira:
+ short: Roth IRA
+ long: Roth IRA
+ sep_ira:
+ short: SEP IRA
+ long: SEP IRA
+ simple_ira:
+ short: SIMPLE IRA
+ long: SIMPLE IRA
+ 529_plan:
+ short: 529 Plan
+ long: 529 Education Savings Plan
+ hsa:
+ short: HSA
+ long: Tài khoản tiết kiệm y tế
+ ugma:
+ short: UGMA
+ long: Tài khoản ủy thác UGMA
+ utma:
+ short: UTMA
+ long: Tài khoản ủy thác UTMA
+ # United Kingdom
+ isa:
+ short: ISA
+ long: Tài khoản tiết kiệm cá nhân
+ lisa:
+ short: LISA
+ long: Lifetime ISA
+ sipp:
+ short: SIPP
+ long: Quỹ hưu trí cá nhân tự quản
+ workplace_pension_uk:
+ short: Lương hưu
+ long: Lương hưu doanh nghiệp
+ # Canada
+ rrsp:
+ short: RRSP
+ long: Kế hoạch tiết kiệm hưu trí đăng ký
+ tfsa:
+ short: TFSA
+ long: Tài khoản tiết kiệm miễn thuế
+ resp:
+ short: RESP
+ long: Kế hoạch tiết kiệm giáo dục đăng ký
+ lira:
+ short: LIRA
+ long: Tài khoản hưu trí bị phong tỏa
+ rrif:
+ short: RRIF
+ long: Quỹ thu nhập hưu trí đăng ký
+ # Australia
+ super:
+ short: Super
+ long: Siêu quỹ hưu trí
+ smsf:
+ short: SMSF
+ long: Quỹ siêu hưu trí tự quản
+ # Europe
+ pea:
+ short: PEA
+ long: Plan d'Épargne en Actions
+ pillar_3a:
+ short: Pillar 3a
+ long: Lương hưu tư nhân (Pillar 3a)
+ riester:
+ short: Riester
+ long: Riester-Rente
+ # India
+ nps:
+ short: NPS
+ long: Hệ thống lương hưu quốc gia
+ apy:
+ short: APY
+ long: Atal Pension Yojana
+ indian_stocks:
+ short: Cổ phiếu Ấn Độ
+ long: Cổ phiếu Ấn Độ (Demat)
+ indian_equity:
+ short: Cổ phần Ấn Độ
+ long: Cổ phần Ấn Độ
+ indian_etf:
+ short: ETF Ấn Độ
+ long: ETF Ấn Độ
+ life_insurance:
+ short: Bảo hiểm nhân thọ
+ long: Bảo hiểm nhân thọ
+ ppf:
+ short: PPF
+ long: Quỹ tiết kiệm công cộng
+ ssy:
+ short: SSY
+ long: Sukanya Samriddhi Yojana
+ nsc:
+ short: NSC
+ long: Chứng chỉ tiết kiệm quốc gia
+ scss:
+ short: SCSS
+ long: Kế hoạch tiết kiệm người cao tuổi
+ fd:
+ short: FD
+ long: Tiền gửi cố định
+ rd:
+ short: RD
+ long: Tiền gửi định kỳ
+ pomis:
+ short: POMIS
+ long: Kế hoạch thu nhập hàng tháng bưu điện
+ kvp:
+ short: KVP
+ long: Kisan Vikas Patra
+ gold_etf:
+ short: ETF Vàng
+ long: ETF Vàng
+ gold_mf:
+ short: Quỹ tương hỗ Vàng
+ long: Quỹ tương hỗ Vàng
+ sgb:
+ short: SGB
+ long: Trái phiếu vàng có bảo đảm
+ g_sec:
+ short: G-Sec
+ long: Chứng khoán chính phủ (G-Secs)
+ sdl:
+ short: SDL
+ long: Khoản vay phát triển tiểu bang (SDLs)
+ corporate_bond:
+ short: Trái phiếu doanh nghiệp
+ long: Trái phiếu doanh nghiệp
+ infrastructure_bond:
+ short: Trái phiếu cơ sở hạ tầng
+ long: Trái phiếu cơ sở hạ tầng
+ tax_free_bond:
+ short: Trái phiếu miễn thuế
+ long: Trái phiếu miễn thuế
+ # Generic
+ pension:
+ short: Lương hưu
+ long: Lương hưu
+ retirement:
+ short: Hưu trí
+ long: Tài khoản hưu trí
+ mutual_fund:
+ short: Quỹ tương hỗ
+ long: Quỹ tương hỗ
+ gold:
+ short: Vàng
+ long: Vàng (vật chất hoặc kỹ thuật số)
+ angel:
+ short: Angel
+ long: Đầu tư thiên thần
+ trust:
+ short: Quỹ tín thác
+ long: Quỹ tín thác
+ other:
+ short: Khác
+ long: Đầu tư khác
+ value_tooltip:
+ cash: Tiền mặt
+ holdings: Danh mục nắm giữ
+ total: Số dư danh mục
+ total_value_tooltip: Tổng số dư danh mục là tổng tiền mặt tại sàn giao dịch (có sẵn để giao dịch) và giá trị thị trường hiện tại của các tài sản nắm giữ.
diff --git a/config/locales/views/investments/zh-CN.yml b/config/locales/views/investments/zh-CN.yml
index 80f59bef0..0cc681e3c 100644
--- a/config/locales/views/investments/zh-CN.yml
+++ b/config/locales/views/investments/zh-CN.yml
@@ -2,16 +2,188 @@
zh-CN:
investments:
edit:
- edit: 编辑%{account}
+ edit: 编辑 %{account}
form:
none: 无
subtype_prompt: 选择投资类型
new:
- title: 请输入账户余额
+ title: 输入账户余额
show:
chart_title: 总价值
+ subtypes:
+ brokerage:
+ short: 券商
+ long: 券商账户
+ 401k:
+ short: 401(k)
+ long: 401(k)
+ roth_401k:
+ short: Roth 401(k)
+ long: Roth 401(k)
+ 403b:
+ short: 403(b)
+ long: 403(b)
+ 457b:
+ short: 457(b)
+ long: 457(b)
+ tsp:
+ short: TSP
+ long: 节俭储蓄计划
+ ira:
+ short: IRA
+ long: 传统 IRA
+ roth_ira:
+ short: Roth IRA
+ long: Roth IRA
+ sep_ira:
+ short: SEP IRA
+ long: SEP IRA
+ simple_ira:
+ short: SIMPLE IRA
+ long: SIMPLE IRA
+ 529_plan:
+ short: 529 计划
+ long: 529 教育储蓄计划
+ hsa:
+ short: HSA
+ long: 健康储蓄账户
+ ugma:
+ short: UGMA
+ long: UGMA 托管账户
+ utma:
+ short: UTMA
+ long: UTMA 托管账户
+ isa:
+ short: ISA
+ long: 个人储蓄账户
+ lisa:
+ short: LISA
+ long: 终身 ISA
+ sipp:
+ short: SIPP
+ long: 自主投资个人养老金
+ workplace_pension_uk:
+ short: 养老金
+ long: 工作场所养老金
+ rrsp:
+ short: RRSP
+ long: 注册退休储蓄计划
+ tfsa:
+ short: TFSA
+ long: 免税储蓄账户
+ resp:
+ short: RESP
+ long: 注册教育储蓄计划
+ lira:
+ short: LIRA
+ long: 锁定退休账户
+ rrif:
+ short: RRIF
+ long: 注册退休收入基金
+ super:
+ short: Super
+ long: 养老金储蓄
+ smsf:
+ short: SMSF
+ long: 自主管理超级年金基金
+ pea:
+ short: PEA
+ long: 股票储蓄计划
+ pillar_3a:
+ short: 第三支柱 3a
+ long: 私人养老金(第三支柱 3a)
+ riester:
+ short: Riester
+ long: Riester 养老金
+ nps:
+ short: NPS
+ long: 国家养老金系统
+ apy:
+ short: APY
+ long: Atal 养老金计划
+ indian_stocks:
+ short: 印度股票
+ long: 印度股票(Demat)
+ indian_equity:
+ short: 印度权益
+ long: 印度权益
+ indian_etf:
+ short: 印度 ETF
+ long: 印度 ETF
+ life_insurance:
+ short: 人寿保险
+ long: 人寿保险
+ ppf:
+ short: PPF
+ long: 公共公积金
+ ssy:
+ short: SSY
+ long: Sukanya Samriddhi 计划
+ nsc:
+ short: NSC
+ long: 国家储蓄证书
+ scss:
+ short: SCSS
+ long: 老年人储蓄计划
+ fd:
+ short: 定存
+ long: 定期存款
+ rd:
+ short: RD
+ long: 定期储蓄存款
+ pomis:
+ short: POMIS
+ long: 邮政月收益计划
+ kvp:
+ short: KVP
+ long: Kisan Vikas Patra
+ gold_etf:
+ short: 黄金 ETF
+ long: 黄金 ETF
+ gold_mf:
+ short: 黄金基金
+ long: 黄金共同基金
+ sgb:
+ short: SGB
+ long: 主权黄金债券
+ g_sec:
+ short: 国债
+ long: 政府证券(G-Secs)
+ sdl:
+ short: SDL
+ long: 州发展贷款(SDLs)
+ corporate_bond:
+ short: 公司债
+ long: 公司债
+ infrastructure_bond:
+ short: 基建债
+ long: 基础设施债券
+ tax_free_bond:
+ short: 免税债
+ long: 免税债券
+ pension:
+ short: 养老金
+ long: 养老金
+ retirement:
+ short: 退休
+ long: 退休账户
+ mutual_fund:
+ short: 共同基金
+ long: 共同基金
+ gold:
+ short: 黄金
+ long: 黄金(实物或数字)
+ angel:
+ short: 天使投资
+ long: 天使投资
+ trust:
+ short: 信托
+ long: 信托
+ other:
+ short: 其他
+ long: 其他投资
value_tooltip:
cash: 现金
holdings: 持仓
total: 投资组合余额
- total_value_tooltip: 总投资组合余额等于经纪账户现金(可用于交易)与持仓当前市值的总和。
+ total_value_tooltip: 投资组合总余额是券商现金(可用于交易)与当前持仓市值之和。
diff --git a/config/locales/views/invitation_mailer/vi.yml b/config/locales/views/invitation_mailer/vi.yml
new file mode 100644
index 000000000..6e57ba19f
--- /dev/null
+++ b/config/locales/views/invitation_mailer/vi.yml
@@ -0,0 +1,8 @@
+---
+vi:
+ invitation_mailer:
+ invite_email:
+ accept_button: Chấp nhận lời mời
+ body: "%{inviter} đã mời bạn tham gia %{family} %{moniker} trên %{product_name}!"
+ expiry_notice: Lời mời này sẽ hết hạn sau %{days} ngày
+ greeting: Chào mừng đến với %{product_name}!
diff --git a/config/locales/views/invitations/en.yml b/config/locales/views/invitations/en.yml
index 45c2d51a5..bc4dfded3 100644
--- a/config/locales/views/invitations/en.yml
+++ b/config/locales/views/invitations/en.yml
@@ -9,6 +9,7 @@ en:
title: Join %{family}
create:
existing_user_added: User has been added to your household.
+ existing_user_has_family_data: That user already owns a household with accounts. They need to remove or transfer those accounts before joining yours.
failure: Could not send invitation
success: Invitation sent successfully
destroy:
diff --git a/config/locales/views/invitations/vi.yml b/config/locales/views/invitations/vi.yml
new file mode 100644
index 000000000..68df7ddce
--- /dev/null
+++ b/config/locales/views/invitations/vi.yml
@@ -0,0 +1,28 @@
+---
+vi:
+ invitations:
+ accept_choice:
+ create_account: Tạo tài khoản mới
+ joined_household: Bạn đã tham gia hộ gia đình.
+ message: "%{inviter} đã mời bạn tham gia với tư cách %{role}."
+ sign_in_existing: Tôi đã có tài khoản
+ title: Tham gia %{family}
+ create:
+ existing_user_added: Người dùng đã được thêm vào hộ gia đình của bạn.
+ existing_user_has_family_data: Người dùng đó đã sở hữu một hộ gia đình có tài khoản. Họ cần xóa hoặc chuyển những tài khoản đó trước khi tham gia vào hộ gia đình của bạn.
+ failure: Không thể gửi lời mời
+ success: Lời mời đã được gửi thành công
+ destroy:
+ failure: Đã xảy ra sự cố khi xóa lời mời.
+ not_authorized: Bạn không được phép quản lý lời mời.
+ success: Lời mời đã được xóa thành công.
+ new:
+ email_label: Địa chỉ email
+ email_placeholder: Nhập địa chỉ email
+ role_admin: Quản trị viên
+ role_guest: Khách
+ role_label: Vai trò
+ role_member: Thành viên
+ submit: Gửi lời mời
+ subtitle: Gửi lời mời để tham gia tài khoản %{moniker} của bạn trên %{product_name}
+ title: Mời ai đó
diff --git a/config/locales/views/invitations/zh-CN.yml b/config/locales/views/invitations/zh-CN.yml
index 9529f4b01..048879292 100644
--- a/config/locales/views/invitations/zh-CN.yml
+++ b/config/locales/views/invitations/zh-CN.yml
@@ -1,19 +1,27 @@
---
zh-CN:
invitations:
+ accept_choice:
+ create_account: 创建新账户
+ joined_household: 您已加入该家庭。
+ message: "%{inviter} 邀请您以 %{role} 身份加入。"
+ sign_in_existing: 我已经有账户了
+ title: 加入 %{family}
create:
- failure: 发送邀请失败
- success: 邀请发送成功
+ existing_user_added: 用户已添加到您的家庭中。
+ failure: 无法发送邀请
+ success: 邀请已成功发送
destroy:
- failure: 删除邀请时出现问题。
- not_authorized: 您没有管理邀请的权限。
- success: 邀请已成功删除。
+ failure: 移除邀请时出现问题。
+ not_authorized: 您无权管理邀请。
+ success: 邀请已成功移除。
new:
email_label: 邮箱地址
- email_placeholder: 请输入邮箱地址
+ email_placeholder: 输入邮箱地址
role_admin: 管理员
+ role_guest: 访客
role_label: 角色
role_member: 成员
submit: 发送邀请
- subtitle: 发送邀请,让对方加入您在 %{product_name} 的家庭账户
- title: 邀请成员
+ subtitle: 向 %{product_name} 中的 %{moniker} 账户发送邀请
+ title: 邀请他人
diff --git a/config/locales/views/invite_codes/vi.yml b/config/locales/views/invite_codes/vi.yml
new file mode 100644
index 000000000..339fa8d58
--- /dev/null
+++ b/config/locales/views/invite_codes/vi.yml
@@ -0,0 +1,10 @@
+---
+vi:
+ invite_codes:
+ create:
+ success: "Mã đã được tạo"
+ destroy:
+ success: "Mã đã được xóa"
+ index:
+ invite_code_description: Tạo mã mới để xem nó được hiển thị ở đây. Các mã đã được tạo và sử dụng sẽ không còn được hiển thị nữa.
+ no_invite_codes: Không có mã nào để hiển thị
diff --git a/config/locales/views/invite_codes/zh-CN.yml b/config/locales/views/invite_codes/zh-CN.yml
index 3aba8c480..615c06350 100644
--- a/config/locales/views/invite_codes/zh-CN.yml
+++ b/config/locales/views/invite_codes/zh-CN.yml
@@ -1,6 +1,10 @@
---
zh-CN:
invite_codes:
+ create:
+ success: 代码已生成
+ destroy:
+ success: 代码已删除
index:
- invite_code_description: 生成新代码后将会显示在此处。已使用的生成代码将不再显示。
- no_invite_codes: 暂无邀请码
+ invite_code_description: 生成一个新代码后,它会显示在这里。已使用的已生成代码将不再显示。
+ no_invite_codes: 没有可显示的代码
diff --git a/config/locales/views/kraken_items/vi.yml b/config/locales/views/kraken_items/vi.yml
new file mode 100644
index 000000000..4878281ec
--- /dev/null
+++ b/config/locales/views/kraken_items/vi.yml
@@ -0,0 +1,85 @@
+---
+vi:
+ kraken_items:
+ provider_connection:
+ default_name: Kraken
+ default_description: Liên kết với tài khoản sàn giao dịch Kraken
+ name: "Kraken - %{name}"
+ description: "Liên kết với %{name}"
+ create:
+ default_name: Kraken
+ success: Đã kết nối thành công với Kraken. Tài khoản sàn giao dịch của bạn đang được đồng bộ.
+ update:
+ success: Đã cập nhật kết nối Kraken thành công.
+ destroy:
+ success: Đã lên lịch xóa kết nối Kraken.
+ select_accounts:
+ select_connection: Chọn kết nối Kraken trong Cài đặt nhà cung cấp.
+ no_credentials_configured: Thêm thông tin API Kraken trước khi thiết lập tài khoản.
+ link_accounts:
+ select_connection: Chọn kết nối Kraken trước khi liên kết tài khoản.
+ select_existing_account:
+ title: Liên kết tài khoản Kraken
+ no_accounts_found: Không tìm thấy tài khoản Kraken nào.
+ wait_for_sync: Chờ Kraken hoàn tất đồng bộ.
+ check_provider_health: Kiểm tra xem thông tin API Kraken của bạn có hợp lệ không.
+ link: Liên kết
+ cancel: Hủy
+ link_existing_account:
+ success: Đã liên kết thành công với tài khoản Kraken
+ select_connection: Chọn kết nối Kraken trước khi liên kết tài khoản.
+ errors:
+ only_manual: Chỉ tài khoản tiền điện tử thủ công chưa có liên kết nhà cung cấp mới có thể liên kết với Kraken
+ invalid_kraken_account: Tài khoản Kraken không hợp lệ
+ kraken_account_already_linked: Tài khoản Kraken này đã được liên kết
+ setup_accounts:
+ title: Nhập tài khoản Kraken
+ subtitle: Chọn tài khoản sàn giao dịch để theo dõi
+ instructions: Kraken nhập một tài khoản sàn giao dịch tiền điện tử kết hợp cho kết nối này, chỉ với các tài sản nắm giữ và lệnh giao ngay.
+ no_accounts: Tất cả tài khoản Kraken đã được nhập.
+ accounts_count:
+ one: "%{count} tài khoản có sẵn"
+ other: "%{count} tài khoản có sẵn"
+ select_all: Chọn tất cả
+ import_selected: Nhập đã chọn
+ cancel: Hủy
+ creating: Đang nhập...
+ complete_account_setup:
+ success:
+ one: "Đã nhập %{count} tài khoản"
+ other: "Đã nhập %{count} tài khoản"
+ none_selected: Chưa chọn tài khoản nào
+ no_accounts: Không có tài khoản nào để nhập
+ kraken_item:
+ provider_name: Kraken
+ syncing: Đang đồng bộ...
+ reconnect: Thông tin xác thực cần cập nhật
+ deletion_in_progress: Đang xóa...
+ sync_status:
+ no_accounts: Không tìm thấy tài khoản
+ all_synced:
+ one: "%{count} tài khoản đã đồng bộ"
+ other: "%{count} tài khoản đã đồng bộ"
+ partial_sync: "%{linked_count} đã đồng bộ, %{unlinked_count} cần thiết lập"
+ status: "Đồng bộ lần cuối %{timestamp} trước"
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước - %{summary}"
+ status_never: Chưa bao giờ đồng bộ
+ delete: Xóa
+ no_accounts_title: Không tìm thấy tài khoản
+ no_accounts_message: Tài khoản sàn giao dịch Kraken của bạn sẽ xuất hiện ở đây sau khi đồng bộ.
+ setup_needed: Tài khoản sẵn sàng để nhập
+ setup_description: Nhập kết nối Kraken này dưới dạng tài khoản sàn giao dịch tiền điện tử.
+ setup_action: Nhập tài khoản
+ import_accounts_menu: Nhập tài khoản
+ stale_rate_warning: "Số dư là xấp xỉ vì tỷ giá hối đoái chính xác cho ngày %{date} không có sẵn. Sẽ cập nhật lần đồng bộ tiếp theo."
+ kraken_item:
+ syncer:
+ checking_credentials: Đang kiểm tra thông tin xác thực...
+ credentials_invalid: Thông tin API Kraken không hợp lệ. Vui lòng kiểm tra khóa API và bí mật của bạn.
+ importing_accounts: Đang nhập tài khoản từ Kraken...
+ checking_configuration: Đang kiểm tra cấu hình tài khoản...
+ accounts_need_setup:
+ one: "%{count} tài khoản cần thiết lập"
+ other: "%{count} tài khoản cần thiết lập"
+ processing_accounts: Đang xử lý dữ liệu tài khoản...
+ calculating_balances: Đang tính toán số dư...
diff --git a/config/locales/views/kraken_items/zh-CN.yml b/config/locales/views/kraken_items/zh-CN.yml
new file mode 100644
index 000000000..31907c263
--- /dev/null
+++ b/config/locales/views/kraken_items/zh-CN.yml
@@ -0,0 +1,85 @@
+---
+zh-CN:
+ kraken_items:
+ provider_connection:
+ default_name: Kraken
+ default_description: 连接到 Kraken 交易所账户
+ name: "Kraken - %{name}"
+ description: "连接到 %{name}"
+ create:
+ default_name: Kraken
+ success: 已成功连接到 Kraken。您的交易所账户正在同步。
+ update:
+ success: Kraken 连接更新成功。
+ destroy:
+ success: Kraken 连接已安排删除。
+ select_accounts:
+ select_connection: 请在提供商设置中选择一个 Kraken 连接。
+ no_credentials_configured: 在设置账户之前,请先添加 Kraken API 凭据。
+ link_accounts:
+ select_connection: 在关联账户之前请选择一个 Kraken 连接。
+ select_existing_account:
+ title: 关联 Kraken 账户
+ no_accounts_found: 未找到 Kraken 账户。
+ wait_for_sync: 等待 Kraken 完成同步。
+ check_provider_health: 检查您的 Kraken API 凭据是否有效。
+ link: 关联
+ cancel: 取消
+ link_existing_account:
+ success: 已成功关联到 Kraken 账户
+ select_connection: 在关联账户之前请选择一个 Kraken 连接。
+ errors:
+ only_manual: 只有没有现有提供商关联的手动加密交易所账户才能关联到 Kraken
+ invalid_kraken_account: 无效的 Kraken 账户
+ kraken_account_already_linked: 此 Kraken 账户已关联
+ setup_accounts:
+ title: 导入 Kraken 账户
+ subtitle: 选择要跟踪的交易所账户
+ instructions: Kraken 会为此连接导入一个合并后的加密交易所账户,仅包含持仓和现货成交。
+ no_accounts: 所有 Kraken 账户都已导入。
+ accounts_count:
+ one: 有 %{count} 个账户可用
+ other: 有 %{count} 个账户可用
+ select_all: 全选
+ import_selected: 导入所选
+ cancel: 取消
+ creating: 正在导入...
+ complete_account_setup:
+ success:
+ one: 已导入 %{count} 个账户
+ other: 已导入 %{count} 个账户
+ none_selected: 未选择账户
+ no_accounts: 没有可导入的账户
+ kraken_item:
+ provider_name: Kraken
+ syncing: 正在同步...
+ reconnect: 凭据需要更新
+ deletion_in_progress: 正在删除...
+ sync_status:
+ no_accounts: 未找到账户
+ all_synced:
+ one: 已同步 %{count} 个账户
+ other: 已同步 %{count} 个账户
+ partial_sync: 已同步 %{linked_count} 个,%{unlinked_count} 个需要设置
+ status: "%{timestamp} 前上次同步"
+ status_with_summary: "%{timestamp} 前上次同步 - %{summary}"
+ status_never: 从未同步
+ delete: 删除
+ no_accounts_title: 未找到账户
+ no_accounts_message: 您的 Kraken 交易所账户在同步后会显示在这里。
+ setup_needed: 账户已准备好导入
+ setup_description: 将此 Kraken 连接作为加密交易所账户导入。
+ setup_action: 导入账户
+ import_accounts_menu: 导入账户
+ stale_rate_warning: "余额为近似值,因为 %{date} 的准确汇率不可用。将在下次同步时更新。"
+ kraken_item:
+ syncer:
+ checking_credentials: 正在检查凭据...
+ credentials_invalid: Kraken API 凭据无效。请检查您的 API key 和 secret。
+ importing_accounts: 正在从 Kraken 导入账户...
+ checking_configuration: 正在检查账户配置...
+ accounts_need_setup:
+ one: "%{count} 个账户需要设置"
+ other: "%{count} 个账户需要设置"
+ processing_accounts: 正在处理账户数据...
+ calculating_balances: 正在计算余额...
diff --git a/config/locales/views/layout/vi.yml b/config/locales/views/layout/vi.yml
new file mode 100644
index 000000000..87d2f4ded
--- /dev/null
+++ b/config/locales/views/layout/vi.yml
@@ -0,0 +1,29 @@
+---
+vi:
+ layouts:
+ application:
+ privacy_mode: Bật/tắt chế độ riêng tư
+ skip_to_main: Chuyển đến nội dung chính
+ nav:
+ assistant: Trợ lý
+ budgets: Ngân sách
+ home: Trang chủ
+ reports: Báo cáo
+ transactions: Giao dịch
+ auth:
+ existing_account: Đã có tài khoản?
+ no_account: Mới dùng %{product_name}?
+ sign_in: Đăng nhập
+ sign_up: Tạo tài khoản
+ shared:
+ footer:
+ privacy_policy: Chính sách bảo mật
+ terms_of_service: Điều khoản dịch vụ
+ confirm_dialog:
+ are_you_sure: Bạn có chắc không?
+ cannot_be_undone: Hành động này không thể hoàn tác.
+ confirm: Xác nhận
+ trial:
+ open_demo: Mở bản demo
+ data_deleted_in_days: Dữ liệu sẽ bị xóa sau %{days} ngày
+ contribute: Đóng góp
diff --git a/config/locales/views/layout/zh-CN.yml b/config/locales/views/layout/zh-CN.yml
index 0cd552d11..afcd7112d 100644
--- a/config/locales/views/layout/zh-CN.yml
+++ b/config/locales/views/layout/zh-CN.yml
@@ -1,26 +1,29 @@
-
---
zh-CN:
layouts:
application:
privacy_mode: 切换隐私模式
- skip_to_main: 跳到主要内容
+ skip_to_main: 跳转到主要内容
nav:
- assistant: 智能助手
- budgets: 预算管理
- home: 首页
- reports: 财务报告
- transactions: 交易记录
+ assistant: 助手
+ budgets: 预算
+ home: 主页
+ reports: 报表
+ transactions: 交易
auth:
- existing_account: 已有账户?
- no_account: 新用户?
+ existing_account: 已经有账户?
+ no_account: 新加入 %{product_name}?
sign_in: 登录
- sign_up: 注册账户
+ sign_up: 创建账户
shared:
footer:
privacy_policy: 隐私政策
terms_of_service: 服务条款
+ confirm_dialog:
+ are_you_sure: 您确定吗?
+ cannot_be_undone: 此操作无法撤销。
+ confirm: 确认
trial:
- open_demo: 开放演示
+ open_demo: 打开演示
data_deleted_in_days: 数据将在 %{days} 天后删除
- contribute: 贡献
\ No newline at end of file
+ contribute: 贡献支持
diff --git a/config/locales/views/loans/vi.yml b/config/locales/views/loans/vi.yml
new file mode 100644
index 000000000..14de162ec
--- /dev/null
+++ b/config/locales/views/loans/vi.yml
@@ -0,0 +1,37 @@
+---
+vi:
+ loans:
+ edit:
+ edit: Chỉnh sửa %{account}
+ form:
+ interest_rate: Lãi suất
+ interest_rate_placeholder: '5.25'
+ initial_balance: Số dư khoản vay ban đầu
+ rate_type: Loại lãi suất
+ term_months: Kỳ hạn (tháng)
+ term_months_placeholder: '360'
+ none: Không có
+ subtype_prompt: Chọn loại khoản vay
+ subtype_none: Không có
+ new:
+ title: Nhập thông tin khoản vay
+ overview:
+ interest_rate: Lãi suất
+ monthly_payment: Thanh toán hàng tháng
+ not_applicable: N/A
+ original_principal: Vốn gốc ban đầu
+ remaining_principal: Vốn gốc còn lại
+ term: Kỳ hạn
+ type: Loại
+ unknown: Không xác định
+ tabs:
+ overview:
+ interest_rate: Lãi suất
+ monthly_payment: Thanh toán hàng tháng
+ not_applicable: N/A
+ original_principal: Vốn gốc ban đầu
+ remaining_principal: Vốn gốc còn lại
+ term: Kỳ hạn
+ type: Loại
+ unknown: Không xác định
+ edit_loan_details: "Chỉnh sửa thông tin khoản vay"
diff --git a/config/locales/views/lunchflow_items/vi.yml b/config/locales/views/lunchflow_items/vi.yml
new file mode 100644
index 000000000..fa02ef27c
--- /dev/null
+++ b/config/locales/views/lunchflow_items/vi.yml
@@ -0,0 +1,166 @@
+---
+vi:
+ lunchflow_items:
+ api_error:
+ title: Lỗi kết nối Lunchflow
+ unable_to_connect: Không thể kết nối với Lunchflow
+ common_issues: "Các vấn đề thường gặp:"
+ invalid_api_key_label: Khóa API không hợp lệ
+ invalid_api_key_desc: Kiểm tra khóa API của bạn trong Cài đặt nhà cung cấp
+ expired_credentials_label: Thông tin xác thực hết hạn
+ expired_credentials_desc: Tạo khóa API mới từ Lunchflow
+ network_issue_label: Sự cố mạng
+ network_issue_desc: Kiểm tra kết nối internet của bạn
+ service_down_label: Dịch vụ gián đoạn
+ service_down_desc: API Lunchflow có thể tạm thời không khả dụng
+ check_provider_settings: Kiểm tra Cài đặt nhà cung cấp
+ setup_required:
+ title: Cần thiết lập Lunchflow
+ api_key_not_configured: Khóa API chưa được cấu hình
+ api_key_description: Trước khi liên kết tài khoản Lunchflow, bạn cần cấu hình khóa API Lunchflow.
+ setup_steps_title: "Các bước thiết lập:"
+ setup_step_1_html: "Đi đến Cài đặt → Nhà cung cấp"
+ setup_step_2_html: "Tìm phần Lunchflow"
+ setup_step_3: Nhập khóa API Lunchflow của bạn
+ setup_step_4: Quay lại đây để liên kết tài khoản
+ go_to_provider_settings: Đi đến Cài đặt nhà cung cấp
+ create:
+ success: Kết nối Lunchflow đã được tạo thành công
+ destroy:
+ success: Kết nối Lunchflow đã được xóa
+ index:
+ title: Kết nối Lunchflow
+ loading:
+ loading_message: Đang tải tài khoản Lunchflow...
+ loading_title: Đang tải
+ link_accounts:
+ all_already_linked:
+ one: "Tài khoản đã chọn (%{names}) đã được liên kết"
+ other: "Tất cả %{count} tài khoản đã chọn đã được liên kết: %{names}"
+ api_error: "Lỗi API: %{message}"
+ invalid_account_names:
+ one: "Không thể liên kết tài khoản không có tên"
+ other: "Không thể liên kết %{count} tài khoản không có tên"
+ link_failed: Liên kết tài khoản thất bại
+ no_accounts_selected: Vui lòng chọn ít nhất một tài khoản
+ partial_invalid: "Đã liên kết thành công %{created_count} tài khoản, %{already_linked_count} đã được liên kết, %{invalid_count} tài khoản có tên không hợp lệ"
+ partial_success: "Đã liên kết thành công %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết: %{already_linked_names}"
+ success:
+ one: "Đã liên kết thành công %{count} tài khoản"
+ other: "Đã liên kết thành công %{count} tài khoản"
+ lunchflow_item:
+ accounts_need_setup: Tài khoản cần thiết lập
+ delete: Xóa kết nối
+ deletion_in_progress: đang xóa...
+ error: Lỗi
+ no_accounts_description: Kết nối này chưa có tài khoản nào được liên kết.
+ no_accounts_title: Không có tài khoản
+ setup_action: Thiết lập tài khoản mới
+ setup_description: "%{linked} trong số %{total} tài khoản đã liên kết. Chọn loại tài khoản cho các tài khoản Lunchflow mới được nhập."
+ setup_needed: Tài khoản mới sẵn sàng để thiết lập
+ status: "Đồng bộ %{timestamp} trước"
+ status_never: Chưa bao giờ đồng bộ
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước • %{summary}"
+ syncing: Đang đồng bộ...
+ total: Tổng cộng
+ unlinked: Chưa liên kết
+ select_accounts:
+ accounts_selected: tài khoản đã chọn
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_lunchflow: Không thể nhập - vui lòng cấu hình tên tài khoản trong Lunchflow
+ description: Chọn tài khoản bạn muốn liên kết với tài khoản %{product_name} của bạn.
+ link_accounts: Liên kết tài khoản đã chọn
+ no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình khóa API.
+ no_api_key: Khóa API Lunchflow chưa được cấu hình. Vui lòng cấu hình trong Cài đặt.
+ no_name_placeholder: "(Không có tên)"
+ title: Chọn tài khoản Lunchflow
+ select_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ all_accounts_already_linked: Tất cả tài khoản Lunchflow đã được liên kết
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_lunchflow: Không thể nhập - vui lòng cấu hình tên tài khoản trong Lunchflow
+ description: Chọn tài khoản Lunchflow để liên kết với tài khoản này. Giao dịch sẽ được đồng bộ và loại bỏ trùng lặp tự động.
+ link_account: Liên kết tài khoản
+ no_account_specified: Chưa chỉ định tài khoản
+ no_accounts_found: Không tìm thấy tài khoản Lunchflow. Vui lòng kiểm tra cấu hình khóa API.
+ no_api_key: Khóa API Lunchflow chưa được cấu hình. Vui lòng cấu hình trong Cài đặt.
+ no_name_placeholder: "(Không có tên)"
+ title: "Liên kết %{account_name} với Lunchflow"
+ link_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ api_error: "Lỗi API: %{message}"
+ invalid_account_name: Không thể liên kết tài khoản không có tên
+ lunchflow_account_already_linked: Tài khoản Lunchflow này đã được liên kết với tài khoản khác
+ lunchflow_account_not_found: Không tìm thấy tài khoản Lunchflow
+ missing_parameters: Thiếu tham số bắt buộc
+ success: "Đã liên kết thành công %{account_name} với Lunchflow"
+ setup_accounts:
+ account_type_label: "Loại tài khoản:"
+ all_accounts_linked: "Tất cả tài khoản Lunchflow của bạn đã được thiết lập."
+ api_error: "Lỗi API: %{message}"
+ fetch_failed: "Tải tài khoản thất bại"
+ no_accounts_to_setup: "Không có tài khoản nào để thiết lập"
+ no_api_key: "Khóa API Lunchflow chưa được cấu hình. Vui lòng kiểm tra cài đặt kết nối."
+ account_types:
+ skip: Bỏ qua tài khoản này
+ depository: Tài khoản thanh toán hoặc tiết kiệm
+ credit_card: Thẻ tín dụng
+ investment: Tài khoản đầu tư
+ loan: Vay hoặc thế chấp
+ other_asset: Tài sản khác
+ subtype_labels:
+ depository: "Loại phụ tài khoản:"
+ credit_card: ""
+ investment: "Loại đầu tư:"
+ loan: "Loại vay:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Thẻ tín dụng sẽ được tự động thiết lập dưới dạng tài khoản thẻ tín dụng."
+ other_asset: "Không cần tùy chọn bổ sung cho Tài sản khác."
+ subtypes:
+ depository:
+ checking: Tài khoản thanh toán
+ savings: Tài khoản tiết kiệm
+ hsa: Tài khoản tiết kiệm y tế
+ cd: Chứng chỉ tiền gửi
+ money_market: Thị trường tiền tệ
+ investment:
+ brokerage: Brokerage
+ pension: Lương hưu
+ retirement: Hưu trí
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: Thrift Savings Plan
+ "529_plan": "529 Plan"
+ hsa: Tài khoản tiết kiệm y tế
+ mutual_fund: Quỹ tương hỗ
+ ira: Traditional IRA
+ roth_ira: Roth IRA
+ angel: Angel
+ loan:
+ mortgage: Thế chấp
+ student: Vay sinh viên
+ auto: Vay mua xe
+ other: Vay khác
+ balance: Số dư
+ cancel: Hủy
+ choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Lunchflow:"
+ create_accounts: Tạo tài khoản
+ creating_accounts: Đang tạo tài khoản...
+ historical_data_range: "Phạm vi dữ liệu lịch sử:"
+ subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập
+ sync_start_date_help: Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ. Tối đa 3 năm lịch sử có sẵn.
+ sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:"
+ title: Thiết lập tài khoản Lunchflow của bạn
+ complete_account_setup:
+ all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo."
+ creation_failed: "Tạo tài khoản thất bại: %{error}"
+ no_accounts: "Không có tài khoản nào để thiết lập."
+ success: "Đã tạo thành công %{count} tài khoản."
+ sync:
+ success: Đã bắt đầu đồng bộ
+ update:
+ success: Kết nối Lunchflow đã được cập nhật
diff --git a/config/locales/views/lunchflow_items/zh-CN.yml b/config/locales/views/lunchflow_items/zh-CN.yml
index e778ace97..ac963800c 100644
--- a/config/locales/views/lunchflow_items/zh-CN.yml
+++ b/config/locales/views/lunchflow_items/zh-CN.yml
@@ -1,148 +1,166 @@
---
zh-CN:
lunchflow_items:
- complete_account_setup:
- all_skipped: 所有账户已跳过。未创建任何账户。
- creation_failed: 创建账户失败:%{error}
- no_accounts: 无待设置账户。
- success: 成功创建 %{count} 个账户。
+ api_error:
+ title: Lunch Flow 连接错误
+ unable_to_connect: 无法连接到 Lunch Flow
+ common_issues: 常见问题:
+ invalid_api_key_label: API 密钥无效
+ invalid_api_key_desc: 请在提供商设置中检查您的 API 密钥
+ expired_credentials_label: 凭据已过期
+ expired_credentials_desc: 请从 Lunch Flow 生成新的 API 密钥
+ network_issue_label: 网络问题
+ network_issue_desc: 请检查您的互联网连接
+ service_down_label: 服务不可用
+ service_down_desc: Lunch Flow API 可能暂时不可用
+ check_provider_settings: 检查提供商设置
+ setup_required:
+ title: 需要设置 Lunch Flow
+ api_key_not_configured: 未配置 API 密钥
+ api_key_description: 在连接 Lunch Flow 账户之前,您需要先配置 Lunch Flow API 密钥。
+ setup_steps_title: 设置步骤:
+ setup_step_1_html: 前往 设置 → 提供商
+ setup_step_2_html: 找到 Lunch Flow 区块
+ setup_step_3: 输入您的 Lunch Flow API 密钥
+ setup_step_4: 返回此处连接您的账户
+ go_to_provider_settings: 前往提供商设置
create:
- success: Lunch Flow 连接已创建成功
+ success: Lunch Flow 连接创建成功
destroy:
success: Lunch Flow 连接已移除
index:
- title: Lunch Flow 连接管理
- link_accounts:
- all_already_linked:
- one: 所选账户(%{names})已关联
- other: 所有 %{count} 个所选账户均已关联:%{names}
- api_error: API 错误:%{message}
- invalid_account_names:
- one: 无法关联名称为空的账户
- other: 无法关联 %{count} 个名称为空的账户
- link_failed: 关联账户失败
- no_accounts_selected: 请至少选择一个账户
- no_api_key: 未发现 API 密钥
- partial_invalid: 成功关联 %{created_count} 个账户,%{already_linked_count} 个已关联,%{invalid_count}
- 个账户名称无效
- partial_success: 成功关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联:%{already_linked_names}
- success:
- one: 成功关联 %{count} 个账户
- other: 成功关联 %{count} 个账户
- link_existing_account:
- account_already_linked: 此账户已关联到其他服务提供商
- api_error: API 错误:%{message}
- invalid_account_name: 无法关联名称为空的账户
- lunchflow_account_already_linked: 此 Lunch Flow 账户已关联到其他账户
- lunchflow_account_not_found: 未找到 Lunch Flow 账户
- missing_parameters: 缺少必要参数
- no_api_key: 未发现 API 密钥
- success: "%{account_name} 已成功与 Lunch Flow 关联"
+ title: Lunch Flow 连接
loading:
loading_message: 正在加载 Lunch Flow 账户...
- loading_title: 加载中
+ loading_title: 正在加载
+ link_accounts:
+ all_already_linked:
+ one: 所选账户(%{names})已经连接
+ other: 所选的 %{count} 个账户都已连接:%{names}
+ api_error: API 错误:%{message}
+ invalid_account_names:
+ one: 无法连接空名称账户
+ other: 无法连接 %{count} 个空名称账户
+ link_failed: 连接账户失败
+ no_accounts_selected: 请至少选择一个账户
+ partial_invalid: 成功连接 %{created_count} 个账户,%{already_linked_count} 个已连接,%{invalid_count} 个账户名称无效
+ partial_success: 成功连接 %{created_count} 个账户。%{already_linked_count} 个账户已连接:%{already_linked_names}
+ success:
+ one: 成功连接 %{count} 个账户
+ other: 成功连接 %{count} 个账户
lunchflow_item:
accounts_need_setup: 账户需要设置
delete: 删除连接
deletion_in_progress: 删除进行中...
error: 错误
- no_accounts_description: 此连接尚未关联任何账户。
- no_accounts_title: 暂无账户
+ no_accounts_description: 此连接尚未链接任何账户。
+ no_accounts_title: 无账户
setup_action: 设置新账户
- setup_description: 已关联 %{linked} / %{total} 个账户。请为您新导入的 Lunch Flow 账户选择账户类型。
- setup_needed: 新账户待设置
- status: 同步于 %{timestamp} 前
+ setup_description: 已连接 %{linked} / %{total} 个账户。请为您新导入的 Lunch Flow 账户选择账户类型。
+ setup_needed: 新账户已准备好设置
+ status: 上次同步于 %{timestamp} 前
status_never: 从未同步
status_with_summary: 上次同步于 %{timestamp} 前 • %{summary}
- syncing: 同步中...
+ syncing: 正在同步...
total: 总计
- unlinked: 未关联
+ unlinked: 未链接
select_accounts:
- accounts_selected: 个账户已选
+ accounts_selected: 已选择账户
api_error: API 错误:%{message}
cancel: 取消
- configure_name_in_lunchflow: 无法导入 - 请在 Lunchflow 中配置账户名称
- description: 选择您想要关联到 %{product_name} 账户的 Lunch Flow 账户。
- link_accounts: 关联所选账户
+ configure_name_in_lunchflow: 无法导入——请在 Lunch Flow 中配置账户名称
+ description: 选择您想链接到 %{product_name} 账户的账户。
+ link_accounts: 链接所选账户
no_accounts_found: 未找到账户。请检查您的 API 密钥配置。
- no_api_key: Lunch Flow API 密钥未配置。请在设置中配置。
- no_credentials_configured: 请先在服务商设置中配置您的 Lunch Flow API 密钥。
- no_name_placeholder: "(无名称)"
+ no_api_key: 未配置 Lunch Flow API 密钥。请在设置中配置。
+ no_name_placeholder: (无名称)
title: 选择 Lunch Flow 账户
select_existing_account:
- account_already_linked: 此账户已关联到其他服务提供商
- all_accounts_already_linked: 所有 Lunch Flow 账户均已关联
+ account_already_linked: 此账户已连接到某个提供商
+ all_accounts_already_linked: 所有 Lunch Flow 账户都已连接
api_error: API 错误:%{message}
cancel: 取消
- configure_name_in_lunchflow: 无法导入 - 请在 Lunchflow 中配置账户名称
+ configure_name_in_lunchflow: 无法导入——请在 Lunch Flow 中配置账户名称
description: 选择一个 Lunch Flow 账户与此账户关联。交易将自动同步并去重。
- link_account: 关联账户
+ link_account: 连接账户
no_account_specified: 未指定账户
no_accounts_found: 未找到 Lunch Flow 账户。请检查您的 API 密钥配置。
- no_api_key: Lunch Flow API 密钥未配置。请在设置中配置。
- no_credentials_configured: 请先在服务商设置中配置您的 Lunch Flow API 密钥。
- no_name_placeholder: "(无名称)"
+ no_api_key: 未配置 Lunch Flow API 密钥。请在设置中配置。
+ no_name_placeholder: (无名称)
title: 将 %{account_name} 与 Lunch Flow 关联
+ link_existing_account:
+ account_already_linked: 此账户已连接到某个提供商
+ api_error: API 错误:%{message}
+ invalid_account_name: 无法连接空名称账户
+ lunchflow_account_already_linked: 该 Lunch Flow 账户已连接到其他账户
+ lunchflow_account_not_found: 未找到 Lunch Flow 账户
+ missing_parameters: 缺少必需参数
+ success: 已成功将 %{account_name} 与 Lunch Flow 关联
setup_accounts:
account_type_label: 账户类型:
+ all_accounts_linked: 您的所有 Lunch Flow 账户都已设置完成。
+ api_error: API 错误:%{message}
+ fetch_failed: 获取账户失败
+ no_accounts_to_setup: 没有需要设置的账户
+ no_api_key: 未配置 Lunch Flow API 密钥。请检查您的连接设置。
account_types:
+ skip: 跳过此账户
+ depository: 支票账户或储蓄账户
credit_card: 信用卡
- depository: 支票或储蓄账户
investment: 投资账户
loan: 贷款或抵押贷款
other_asset: 其他资产
- skip: 跳过此账户
- all_accounts_linked: 您的所有 Lunch Flow 账户已完成设置。
- api_error: API 错误:%{message}
- balance: 余额
- cancel: 取消
- choose_account_type: 请为每个 Lunch Flow 账户选择正确的账户类型:
- create_accounts: 创建账户
- creating_accounts: 正在创建账户...
- fetch_failed: 获取账户失败
- historical_data_range: 历史数据范围:
- no_accounts_to_setup: 无待设置账户
- no_api_key: Lunch Flow API 密钥未配置。请检查您的连接设置。
- subtitle: 为您的导入账户选择正确的账户类型
subtype_labels:
- credit_card: ''
depository: 账户子类型:
+ credit_card: ""
investment: 投资类型:
loan: 贷款类型:
- other_asset: ''
+ other_asset: ""
subtype_messages:
credit_card: 信用卡将自动设置为信用卡账户。
other_asset: 其他资产无需额外选项。
subtypes:
depository:
- cd: 定期存款
checking: 支票账户
- hsa: 健康储蓄账户
- money_market: 货币市场账户
savings: 储蓄账户
- investment:
- 401k: 401(k) 计划
- 403b: 403(b) 计划
- 529_plan: 529 教育储蓄计划
- angel: 天使投资
- brokerage: 经纪账户
hsa: 健康储蓄账户
- ira: 传统 IRA
- mutual_fund: 共同基金
- pension: 养老金账户
+ cd: 定期存单
+ money_market: 货币市场账户
+ investment:
+ brokerage: 券商账户
+ pension: 养老金
retirement: 退休账户
- roth_401k: Roth 401(k) 计划
- roth_ira: Roth IRA
+ "401k": 401(k)
+ roth_401k: Roth 401(k)
+ "403b": 403(b)
tsp: 节俭储蓄计划
+ "529_plan": 529 教育储蓄计划
+ hsa: 健康储蓄账户
+ mutual_fund: 共同基金
+ ira: 传统 IRA
+ roth_ira: Roth IRA
+ angel: 天使投资
loan:
- auto: 汽车贷款
- mortgage: 抵押贷款
- other: 其他贷款
+ mortgage: 房贷
student: 学生贷款
- sync_start_date_help: 选择您希望同步交易历史的时间范围。最多可同步 3 年历史数据。
- sync_start_date_label: 开始同步交易的日期:
+ auto: 车贷
+ other: 其他贷款
+ balance: 余额
+ cancel: 取消
+ choose_account_type: 为每个 Lunch Flow 账户选择正确的账户类型:
+ create_accounts: 创建账户
+ creating_accounts: 正在创建账户...
+ historical_data_range: 历史数据范围:
+ subtitle: 为您导入的账户选择正确的账户类型
+ sync_start_date_help: 选择要同步交易历史的回溯范围。最多可获取 3 年历史。
+ sync_start_date_label: 从以下日期开始同步交易:
title: 设置您的 Lunch Flow 账户
+ complete_account_setup:
+ all_skipped: 所有账户都已跳过。未创建任何账户。
+ creation_failed: 创建账户失败:%{error}
+ no_accounts: 没有需要设置的账户。
+ success: 成功创建 %{count} 个账户。
sync:
- success: 同步已开始
+ success: 已开始同步
update:
success: Lunch Flow 连接已更新
diff --git a/config/locales/views/merchants/vi.yml b/config/locales/views/merchants/vi.yml
new file mode 100644
index 000000000..6ed4d6765
--- /dev/null
+++ b/config/locales/views/merchants/vi.yml
@@ -0,0 +1,73 @@
+---
+vi:
+ family_merchants:
+ create:
+ error: 'Lỗi khi tạo nhà cung cấp: %{error}'
+ success: Nhà cung cấp mới đã được tạo thành công
+ destroy:
+ success: Nhà cung cấp đã được xóa thành công
+ unlinked_success: Nhà cung cấp đã được xóa khỏi giao dịch của bạn
+ edit:
+ title: Chỉnh sửa nhà cung cấp
+ form:
+ name_placeholder: Tên nhà cung cấp
+ website_placeholder: "Trang web (ví dụ: starbucks.com)"
+ website_hint: Nhập trang web của nhà cung cấp để tự động hiển thị logo của họ
+ index:
+ empty: Chưa có nhà cung cấp nào
+ new: Nhà cung cấp mới
+ merge: Hợp nhất nhà cung cấp
+ title: Nhà cung cấp
+ family_title: "Nhà cung cấp của %{moniker}"
+ family_empty: "Chưa có nhà cung cấp nào của %{moniker}"
+ provider_title: Nhà cung cấp từ dịch vụ
+ provider_empty: "Chưa có nhà cung cấp dịch vụ nào được liên kết với %{moniker} này"
+ provider_read_only: Nhà cung cấp từ dịch vụ được đồng bộ từ các tổ chức tài chính đã kết nối. Không thể chỉnh sửa ở đây.
+ provider_info: Các nhà cung cấp này được phát hiện tự động bởi kết nối ngân hàng hoặc AI của bạn. Bạn có thể chỉnh sửa chúng để tạo bản sao riêng hoặc xóa chúng để hủy liên kết khỏi giao dịch của bạn.
+ enhance_info:
+ one: "%{count} nhà cung cấp dịch vụ đang thiếu thông tin trang web. Cải thiện với AI để phát hiện trang web, hiển thị logo và hợp nhất các nhà cung cấp trùng lặp."
+ other: "%{count} nhà cung cấp dịch vụ đang thiếu thông tin trang web. Cải thiện với AI để phát hiện trang web, hiển thị logo và hợp nhất các nhà cung cấp trùng lặp."
+ enhance_button: Cải thiện với AI
+ unlinked_title: Vừa hủy liên kết
+ unlinked_info: Các nhà cung cấp này vừa được xóa khỏi giao dịch của bạn. Họ sẽ biến mất khỏi danh sách này sau 30 ngày trừ khi được gán lại cho một giao dịch.
+ table:
+ merchant: Nhà cung cấp
+ actions: Hành động
+ source: Nguồn
+ family_merchant:
+ edit: Chỉnh sửa
+ delete: Xóa
+ merchant:
+ confirm_accept: Xóa nhà cung cấp
+ confirm_body: Bạn có chắc chắn muốn xóa nhà cung cấp này không? Xóa nhà cung cấp này sẽ hủy liên kết tất cả giao dịch liên quan và có thể ảnh hưởng đến báo cáo của bạn.
+ confirm_title: Xóa nhà cung cấp?
+ delete: Xóa nhà cung cấp
+ edit: Chỉnh sửa nhà cung cấp
+ merge:
+ title: Hợp nhất nhà cung cấp
+ description: Chọn nhà cung cấp đích và các nhà cung cấp cần hợp nhất vào đó. Tất cả giao dịch từ các nhà cung cấp được hợp nhất sẽ được chuyển sang nhà cung cấp đích.
+ target_label: Hợp nhất vào (đích)
+ select_target: Chọn nhà cung cấp đích...
+ sources_label: Nhà cung cấp cần hợp nhất
+ sources_hint: Các nhà cung cấp được chọn sẽ được hợp nhất vào đích. Nhà cung cấp gia đình sẽ bị xóa, nhà cung cấp dịch vụ sẽ bị hủy liên kết.
+ submit: Hợp nhất đã chọn
+ new:
+ title: Nhà cung cấp mới
+ perform_merge:
+ success:
+ one: Đã hợp nhất thành công %{count} nhà cung cấp
+ other: Đã hợp nhất thành công %{count} nhà cung cấp
+ no_merchants_selected: Không có nhà cung cấp nào được chọn để hợp nhất
+ target_not_found: Không tìm thấy nhà cung cấp đích
+ invalid_merchants: Nhà cung cấp được chọn không hợp lệ
+ provider_merchant:
+ edit: Chỉnh sửa
+ remove: Xóa
+ remove_confirm_title: Xóa nhà cung cấp?
+ remove_confirm_body: Bạn có chắc chắn muốn xóa %{name} không? Thao tác này sẽ hủy liên kết tất cả giao dịch liên quan khỏi nhà cung cấp này nhưng sẽ không xóa nhà cung cấp.
+ enhance:
+ success: Quá trình cải thiện nhà cung cấp dịch vụ đã bắt đầu. Các nhà cung cấp sẽ được cải thiện và các bản trùng lặp sẽ được hợp nhất trong thời gian ngắn.
+ already_running: Quá trình cải thiện đang diễn ra. Vui lòng chờ cho đến khi hoàn tất.
+ update:
+ success: Nhà cung cấp đã được cập nhật thành công
+ converted_success: Nhà cung cấp đã được chuyển đổi và cập nhật thành công
diff --git a/config/locales/views/merchants/zh-CN.yml b/config/locales/views/merchants/zh-CN.yml
index 90c0358fc..b82bd8c85 100644
--- a/config/locales/views/merchants/zh-CN.yml
+++ b/config/locales/views/merchants/zh-CN.yml
@@ -2,34 +2,72 @@
zh-CN:
family_merchants:
create:
- error: 创建商户失败:%{error}
- success: 商户创建成功
+ error: 创建商户出错:%{error}
+ success: 新商户创建成功
destroy:
success: 商户删除成功
+ unlinked_success: 商户已从您的交易中移除
edit:
title: 编辑商户
form:
name_placeholder: 商户名称
+ website_placeholder: 网站(例如 starbucks.com)
+ website_hint: 输入商户网站以自动显示其徽标
index:
- empty: 暂无商户
- family_empty: 暂无家庭商户
- family_title: 家庭商户
- new: 新建商户
- provider_empty: 此家庭尚未关联任何服务提供商商户
- provider_read_only: 服务提供商商户从您关联的金融机构同步而来,无法在此编辑。
- provider_title: 服务提供商商户
+ empty: 还没有商户
+ new: 新商户
+ merge: 合并商户
+ title: 商户
+ family_title: "%{moniker} 商户"
+ family_empty: 还没有 %{moniker} 商户
+ provider_title: 提供商商户
+ provider_empty: 还没有链接到此 %{moniker} 的提供商商户
+ provider_read_only: 提供商商户来自您已连接的机构同步,无法在此编辑。
+ provider_info: 这些商户是由您的银行连接或 AI 自动识别的。您可以编辑它们以创建自己的副本,或将其移除以解除与交易的关联。
+ enhance_info:
+ one: 有 %{count} 个提供商商户缺少网站信息。可使用 AI 增强以检测网站、显示徽标并合并重复商户。
+ other: 有 %{count} 个提供商商户缺少网站信息。可使用 AI 增强以检测网站、显示徽标并合并重复商户。
+ enhance_button: 使用 AI 增强
+ unlinked_title: 最近解除关联
+ unlinked_info: 这些商户最近已从您的交易中移除。除非重新分配到交易,否则它们会在 30 天后从列表中消失。
table:
+ merchant: 商户
actions: 操作
- merchant: 商户名称
source: 来源
- title: 商户管理
+ family_merchant:
+ edit: 编辑
+ delete: 删除
merchant:
- confirm_accept: 确认删除
- confirm_body: 确定要删除此商户吗?删除后将解除所有关联交易,并可能影响您的报表数据。
- confirm_title: 确认删除商户?
+ confirm_accept: 删除商户
+ confirm_body: 确定要删除此商户吗?删除后会解除所有相关交易的关联,并可能影响您的报表。
+ confirm_title: 删除商户?
delete: 删除商户
edit: 编辑商户
+ merge:
+ title: 合并商户
+ description: 选择一个目标商户以及要合并进去的商户。所有来自被合并商户的交易都会重新分配到目标商户。
+ target_label: 合并到(目标)
+ select_target: 选择目标商户...
+ sources_label: 要合并的商户
+ sources_hint: 所选商户将合并到目标商户。家庭商户将被删除,提供商商户将解除关联。
+ submit: 合并所选项
new:
title: 新建商户
+ perform_merge:
+ success:
+ one: 成功合并 %{count} 个商户
+ other: 成功合并 %{count} 个商户
+ no_merchants_selected: 未选择要合并的商户
+ target_not_found: 未找到目标商户
+ invalid_merchants: 选择了无效商户
+ provider_merchant:
+ edit: 编辑
+ remove: 移除
+ remove_confirm_title: 移除商户?
+ remove_confirm_body: 确定要移除 %{name} 吗?这会解除该商户下所有相关交易的关联,但不会删除商户本身。
+ enhance:
+ success: 已开始增强提供商商户。商户很快会得到增强并合并重复项。
+ already_running: 增强已在进行中,请等待完成。
update:
success: 商户更新成功
+ converted_success: 商户已成功转换并更新
diff --git a/config/locales/views/mercury_items/vi.yml b/config/locales/views/mercury_items/vi.yml
new file mode 100644
index 000000000..01c2c3ea1
--- /dev/null
+++ b/config/locales/views/mercury_items/vi.yml
@@ -0,0 +1,208 @@
+---
+vi:
+ mercury_items:
+ api_error:
+ title: Lỗi kết nối Mercury
+ unable_to_connect: Không thể kết nối với Mercury
+ common_issues: "Các vấn đề thường gặp:"
+ invalid_api_token_label: Mã thông báo API không hợp lệ
+ invalid_api_token_desc: Kiểm tra mã thông báo API của bạn trong Cài đặt nhà cung cấp
+ expired_credentials_label: Thông tin xác thực hết hạn
+ expired_credentials_desc: Tạo mã thông báo API mới từ Mercury
+ insufficient_permissions_label: Quyền không đủ
+ insufficient_permissions_desc: Đảm bảo mã thông báo có quyền chỉ đọc
+ network_issue_label: Sự cố mạng
+ network_issue_desc: Kiểm tra kết nối internet của bạn
+ service_down_label: Dịch vụ gián đoạn
+ service_down_desc: API Mercury có thể tạm thời không khả dụng
+ check_provider_settings: Kiểm tra Cài đặt nhà cung cấp
+ setup_required:
+ title: Cần thiết lập Mercury
+ api_token_not_configured: Mã thông báo API chưa được cấu hình
+ api_token_description: Trước khi liên kết tài khoản Mercury, bạn cần cấu hình mã thông báo API Mercury.
+ setup_steps_title: "Các bước thiết lập:"
+ setup_step_1_html: "Đi đến Cài đặt > Nhà cung cấp"
+ setup_step_2_html: "Tìm phần Mercury"
+ setup_step_3: Nhập mã thông báo API Mercury của bạn
+ setup_step_4: Quay lại đây để liên kết tài khoản
+ go_to_provider_settings: Đi đến Cài đặt nhà cung cấp
+ create:
+ success: Kết nối Mercury đã được tạo thành công
+ destroy:
+ success: Kết nối Mercury đã được xóa
+ index:
+ title: Kết nối Mercury
+ loading:
+ loading_message: Đang tải tài khoản Mercury...
+ loading_title: Đang tải
+ link_accounts:
+ all_already_linked:
+ one: "Tài khoản đã chọn (%{names}) đã được liên kết"
+ other: "Tất cả %{count} tài khoản đã chọn đã được liên kết: %{names}"
+ api_error: "Lỗi API: %{message}"
+ invalid_account_names:
+ one: "Không thể liên kết tài khoản không có tên"
+ other: "Không thể liên kết %{count} tài khoản không có tên"
+ link_failed: Liên kết tài khoản thất bại
+ no_accounts_selected: Vui lòng chọn ít nhất một tài khoản
+ no_api_token: Không tìm thấy mã thông báo API Mercury. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ partial_invalid: "Đã liên kết thành công %{created_count} tài khoản, %{already_linked_count} đã được liên kết, %{invalid_count} tài khoản có tên không hợp lệ"
+ partial_success: "Đã liên kết thành công %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết: %{already_linked_names}"
+ select_connection: Chọn kết nối Mercury trước khi liên kết tài khoản.
+ success:
+ one: "Đã liên kết thành công %{count} tài khoản"
+ other: "Đã liên kết thành công %{count} tài khoản"
+ mercury_item:
+ accounts_need_setup: Tài khoản cần thiết lập
+ delete: Xóa kết nối
+ deletion_in_progress: đang xóa...
+ error: Lỗi
+ no_accounts_description: Kết nối này chưa có tài khoản nào được liên kết.
+ no_accounts_title: Không có tài khoản
+ setup_action: Thiết lập tài khoản mới
+ setup_description: "%{linked} trong số %{total} tài khoản đã liên kết. Chọn loại tài khoản cho các tài khoản Mercury mới được nhập."
+ setup_needed: Tài khoản mới sẵn sàng để thiết lập
+ status: "Đồng bộ %{timestamp} trước"
+ status_never: Chưa bao giờ đồng bộ
+ status_with_summary: "Đồng bộ lần cuối %{timestamp} trước - %{summary}"
+ syncing: Đang đồng bộ...
+ total: Tổng cộng
+ unlinked: Chưa liên kết
+ provider_panel:
+ add_connection: Thêm kết nối Mercury
+ base_url_label: URL cơ sở (tùy chọn)
+ base_url_placeholder: https://api.mercury.com/api/v1 (mặc định)
+ connection_name_label: Tên kết nối
+ connection_name_placeholder: Tài khoản kinh doanh
+ default_connection_name: Kết nối Mercury
+ disconnect_confirm: "Ngắt kết nối %{name}?"
+ instructions:
+ copy_token_html: "Sao chép toàn bộ mã thông báo (bao gồm tiền tố secret-token:) và thêm làm kết nối có tên bên dưới"
+ create_token: Tạo mã thông báo API mới với quyền truy cập "Chỉ đọc"
+ open_tokens: Đi đến Cài đặt > Nhà phát triển > Mã thông báo API
+ sign_in_html: "Truy cập %{link} và đăng nhập vào tài khoản bạn muốn kết nối"
+ whitelist_ip_html: "Quan trọng: Thêm địa chỉ IP máy chủ của bạn vào danh sách trắng của mã thông báo"
+ keep_token_placeholder: Để trống để giữ mã thông báo hiện tại
+ sandbox_note_html: "Sử dụng kết nối có tên riêng cho mỗi đăng nhập Mercury/mã thông báo API bạn muốn đồng bộ. Để kiểm thử sandbox, dùng https://api-sandbox.mercury.com/api/v1 làm URL cơ sở. Mercury yêu cầu đưa vào danh sách trắng IP - đảm bảo thêm IP của bạn trong bảng điều khiển Mercury."
+ setup_accounts: Thiết lập tài khoản
+ setup_title: "Hướng dẫn thiết lập:"
+ sync: Đồng bộ
+ token_label: Mã thông báo
+ token_placeholder: Dán mã thông báo vào đây
+ update_connection: Cập nhật kết nối
+ provider_connection:
+ default_description: Kết nối với ngân hàng của bạn qua Mercury
+ default_name: Mercury
+ description: "Kết nối bằng %{name}"
+ name: "Mercury - %{name}"
+ select_accounts:
+ accounts_selected: tài khoản đã chọn
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_mercury: Không thể nhập - vui lòng cấu hình tên tài khoản trong Mercury
+ description: Chọn tài khoản bạn muốn liên kết với tài khoản %{product_name} của bạn.
+ link_accounts: Liên kết tài khoản đã chọn
+ no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình mã thông báo API.
+ no_api_token: Mã thông báo API Mercury chưa được cấu hình. Vui lòng cấu hình trong Cài đặt.
+ no_credentials_configured: Vui lòng cấu hình mã thông báo API Mercury trước trong Cài đặt nhà cung cấp.
+ no_name_placeholder: "(Không có tên)"
+ select_connection: Chọn kết nối Mercury trong Cài đặt nhà cung cấp.
+ title: Chọn tài khoản Mercury
+ select_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ all_accounts_already_linked: Tất cả tài khoản Mercury đã được liên kết
+ api_error: "Lỗi API: %{message}"
+ cancel: Hủy
+ configure_name_in_mercury: Không thể nhập - vui lòng cấu hình tên tài khoản trong Mercury
+ description: Chọn tài khoản Mercury để liên kết với tài khoản này. Giao dịch sẽ được đồng bộ và loại bỏ trùng lặp tự động.
+ link_account: Liên kết tài khoản
+ no_account_specified: Chưa chỉ định tài khoản
+ no_accounts_found: Không tìm thấy tài khoản Mercury. Vui lòng kiểm tra cấu hình mã thông báo API.
+ no_api_token: Mã thông báo API Mercury chưa được cấu hình. Vui lòng cấu hình trong Cài đặt.
+ no_credentials_configured: Vui lòng cấu hình mã thông báo API Mercury trước trong Cài đặt nhà cung cấp.
+ no_name_placeholder: "(Không có tên)"
+ select_connection: Chọn kết nối Mercury trong Cài đặt nhà cung cấp.
+ title: "Liên kết %{account_name} với Mercury"
+ link_existing_account:
+ account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp
+ api_error: "Lỗi API: %{message}"
+ invalid_account_name: Không thể liên kết tài khoản không có tên
+ mercury_account_already_linked: Tài khoản Mercury này đã được liên kết với tài khoản khác
+ mercury_account_not_found: Không tìm thấy tài khoản Mercury
+ missing_parameters: Thiếu tham số bắt buộc
+ no_api_token: Không tìm thấy mã thông báo API Mercury. Vui lòng cấu hình trong Cài đặt nhà cung cấp.
+ select_connection: Chọn kết nối Mercury trước khi liên kết tài khoản.
+ success: "Đã liên kết thành công %{account_name} với Mercury"
+ setup_accounts:
+ account_type_label: "Loại tài khoản:"
+ all_accounts_linked: "Tất cả tài khoản Mercury của bạn đã được thiết lập."
+ api_error: "Lỗi API: %{message}"
+ fetch_failed: "Tải tài khoản thất bại"
+ no_accounts_to_setup: "Không có tài khoản nào để thiết lập"
+ no_api_token: "Mã thông báo API Mercury chưa được cấu hình. Vui lòng kiểm tra cài đặt kết nối."
+ account_types:
+ skip: Bỏ qua tài khoản này
+ depository: Tài khoản thanh toán hoặc tiết kiệm
+ credit_card: Thẻ tín dụng
+ investment: Tài khoản đầu tư
+ loan: Vay hoặc thế chấp
+ other_asset: Tài sản khác
+ subtype_labels:
+ depository: "Loại phụ tài khoản:"
+ credit_card: ""
+ investment: "Loại đầu tư:"
+ loan: "Loại vay:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Thẻ tín dụng sẽ được tự động thiết lập dưới dạng tài khoản thẻ tín dụng."
+ other_asset: "Không cần tùy chọn bổ sung cho Tài sản khác."
+ subtypes:
+ depository:
+ checking: Tài khoản thanh toán
+ savings: Tài khoản tiết kiệm
+ hsa: Tài khoản tiết kiệm y tế
+ cd: Chứng chỉ tiền gửi
+ money_market: Thị trường tiền tệ
+ investment:
+ brokerage: Brokerage
+ pension: Lương hưu
+ retirement: Hưu trí
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: Thrift Savings Plan
+ "529_plan": "529 Plan"
+ hsa: Tài khoản tiết kiệm y tế
+ mutual_fund: Quỹ tương hỗ
+ ira: Traditional IRA
+ roth_ira: Roth IRA
+ angel: Angel
+ loan:
+ mortgage: Thế chấp
+ student: Vay sinh viên
+ auto: Vay mua xe
+ other: Vay khác
+ balance: Số dư
+ cancel: Hủy
+ choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Mercury:"
+ create_accounts: Tạo tài khoản
+ creating_accounts: Đang tạo tài khoản...
+ historical_data_range: "Phạm vi dữ liệu lịch sử:"
+ subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập
+ sync_start_date_help: Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ. Tối đa 3 năm lịch sử có sẵn.
+ sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:"
+ title: Thiết lập tài khoản Mercury của bạn
+ complete_account_setup:
+ all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo."
+ creation_failed: "Tạo tài khoản thất bại: %{error}"
+ no_accounts: "Không có tài khoản nào để thiết lập."
+ success: "Đã tạo thành công %{count} tài khoản."
+ sync:
+ success: Đã bắt đầu đồng bộ
+ update:
+ success: Kết nối Mercury đã được cập nhật
+ mercury_item_selection_error_payload:
+ select_connection: Chọn kết nối Mercury trước khi tải tài khoản.
+ render_mercury_item_selection_failure:
+ select_connection: Chọn kết nối Mercury trong Cài đặt nhà cung cấp.
+ no_credentials_configured: Vui lòng cấu hình mã thông báo API Mercury trước trong Cài đặt nhà cung cấp.
diff --git a/config/locales/views/mercury_items/zh-CN.yml b/config/locales/views/mercury_items/zh-CN.yml
new file mode 100644
index 000000000..0aef76625
--- /dev/null
+++ b/config/locales/views/mercury_items/zh-CN.yml
@@ -0,0 +1,208 @@
+---
+zh-CN:
+ mercury_items:
+ api_error:
+ title: Mercury 连接错误
+ unable_to_connect: 无法连接到 Mercury
+ common_issues: "常见问题:"
+ invalid_api_token_label: API Token 无效
+ invalid_api_token_desc: 请在提供商设置中检查您的 API token
+ expired_credentials_label: 凭据已过期
+ expired_credentials_desc: 请从 Mercury 生成新的 API token
+ insufficient_permissions_label: 权限不足
+ insufficient_permissions_desc: 请确保您的 token 具有只读访问权限
+ network_issue_label: 网络问题
+ network_issue_desc: 请检查您的互联网连接
+ service_down_label: 服务不可用
+ service_down_desc: Mercury API 可能暂时不可用
+ check_provider_settings: 检查提供商设置
+ setup_required:
+ title: 需要设置 Mercury
+ api_token_not_configured: 未配置 API Token
+ api_token_description: 在链接 Mercury 账户之前,您需要先配置 Mercury API token。
+ setup_steps_title: "设置步骤:"
+ setup_step_1_html: "前往 设置 > 提供商"
+ setup_step_2_html: "找到 Mercury 区块"
+ setup_step_3: 输入您的 Mercury API token
+ setup_step_4: 返回这里链接您的账户
+ go_to_provider_settings: 前往提供商设置
+ create:
+ success: Mercury 连接创建成功
+ destroy:
+ success: Mercury 连接已移除
+ index:
+ title: Mercury 连接
+ loading:
+ loading_message: 正在加载 Mercury 账户...
+ loading_title: 正在加载
+ link_accounts:
+ all_already_linked:
+ one: 选定账户(%{names})已关联
+ other: 所有 %{count} 个选定账户都已关联:%{names}
+ api_error: "API 错误:%{message}"
+ invalid_account_names:
+ one: 无法关联空名称账户
+ other: 无法关联 %{count} 个空名称账户
+ link_failed: 关联账户失败
+ no_accounts_selected: 请至少选择一个账户
+ no_api_token: 未找到 Mercury API token。请在提供商设置中配置。
+ partial_invalid: "成功关联 %{created_count} 个账户,%{already_linked_count} 个已关联,%{invalid_count} 个账户名称无效"
+ partial_success: "成功关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联:%{already_linked_names}"
+ select_connection: 在关联账户之前请选择一个 Mercury 连接。
+ success:
+ one: "成功关联 %{count} 个账户"
+ other: "成功关联 %{count} 个账户"
+ mercury_item:
+ accounts_need_setup: 账户需要设置
+ delete: 删除连接
+ deletion_in_progress: 正在删除...
+ error: 错误
+ no_accounts_description: 此连接尚未关联任何账户。
+ no_accounts_title: 无账户
+ setup_action: 设置新账户
+ setup_description: 已关联 %{linked} / %{total} 个账户。请选择新导入的 Mercury 账户类型。
+ setup_needed: 新账户已准备好设置
+ status: "%{timestamp} 前已同步"
+ status_never: 从未同步
+ status_with_summary: "%{timestamp} 前上次同步 - %{summary}"
+ syncing: 正在同步...
+ total: 总计
+ unlinked: 未关联
+ provider_panel:
+ add_connection: 添加 Mercury 连接
+ base_url_label: Base URL(可选)
+ base_url_placeholder: https://api.mercury.com/api/v1(默认)
+ connection_name_label: 连接名称
+ connection_name_placeholder: 企业支票账户
+ default_connection_name: Mercury 连接
+ disconnect_confirm: "断开 %{name}?"
+ instructions:
+ copy_token_html: "复制完整 token(包括 secret-token: 前缀),并将其作为命名连接添加到下方"
+ create_token: 创建一个具有“只读”访问权限的新 API token
+ open_tokens: 前往 设置 > 开发者 > API Tokens
+ sign_in_html: "访问 %{link} 并登录您想连接的账户"
+ whitelist_ip_html: "重要:将您的服务器 IP 地址加入 token 白名单"
+ keep_token_placeholder: 留空以保留当前 token
+ sandbox_note_html: "为每个想同步的 Mercury 登录/API token 使用一个单独的命名连接。沙盒测试时,将 https://api-sandbox.mercury.com/api/v1 作为 Base URL。Mercury 需要 IP 白名单——请确保在 Mercury 仪表盘中添加您的 IP。"
+ setup_accounts: 设置账户
+ setup_title: "设置说明:"
+ sync: 同步
+ token_label: Token
+ token_placeholder: 在此粘贴 token
+ update_connection: 更新连接
+ provider_connection:
+ default_description: 通过 Mercury 连接您的银行
+ default_name: Mercury
+ description: "使用 %{name} 连接"
+ name: "Mercury - %{name}"
+ select_accounts:
+ accounts_selected: 已选择账户
+ api_error: "API 错误:%{message}"
+ cancel: 取消
+ configure_name_in_mercury: 无法导入 - 请先在 Mercury 中配置账户名称
+ description: 选择您想链接到 %{product_name} 账户的账户。
+ link_accounts: 链接所选账户
+ no_accounts_found: 未找到账户。请检查您的 API token 配置。
+ no_api_token: 未配置 Mercury API token。请在设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Mercury API token。
+ no_name_placeholder: (无名称)
+ select_connection: 请在提供商设置中选择一个 Mercury 连接。
+ title: 选择 Mercury 账户
+ select_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ all_accounts_already_linked: 所有 Mercury 账户都已关联
+ api_error: "API 错误:%{message}"
+ cancel: 取消
+ configure_name_in_mercury: 无法导入 - 请先在 Mercury 中配置账户名称
+ description: 选择一个 Mercury 账户与此账户关联。交易将自动同步并去重。
+ link_account: 关联账户
+ no_account_specified: 未指定账户
+ no_accounts_found: 未找到 Mercury 账户。请检查您的 API token 配置。
+ no_api_token: Mercury API token 未配置。请在设置中配置。
+ no_credentials_configured: 请先在提供商设置中配置您的 Mercury API token。
+ no_name_placeholder: (无名称)
+ select_connection: 请在提供商设置中选择一个 Mercury 连接。
+ title: 将 %{account_name} 与 Mercury 关联
+ link_existing_account:
+ account_already_linked: 此账户已关联到某个提供商
+ api_error: "API 错误:%{message}"
+ invalid_account_name: 无法关联空名称账户
+ mercury_account_already_linked: 此 Mercury 账户已关联到另一个账户
+ mercury_account_not_found: 未找到 Mercury 账户
+ missing_parameters: 缺少必需参数
+ no_api_token: 未找到 Mercury API token。请在提供商设置中配置。
+ select_connection: 在关联账户之前请选择一个 Mercury 连接。
+ success: 已成功将 %{account_name} 与 Mercury 关联
+ setup_accounts:
+ account_type_label: "账户类型:"
+ all_accounts_linked: "您的所有 Mercury 账户都已完成设置。"
+ api_error: "API 错误:%{message}"
+ fetch_failed: "获取账户失败"
+ no_accounts_to_setup: "没有需要设置的账户"
+ no_api_token: "Mercury API token 未配置。请检查您的连接设置。"
+ account_types:
+ skip: 跳过此账户
+ depository: 支票账户或储蓄账户
+ credit_card: 信用卡
+ investment: 投资账户
+ loan: 贷款或抵押贷款
+ other_asset: 其他资产
+ subtype_labels:
+ depository: "账户子类型:"
+ credit_card: ""
+ investment: "投资类型:"
+ loan: "贷款类型:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "信用卡将自动设为信用卡账户。"
+ other_asset: "其他资产无需额外选项。"
+ subtypes:
+ depository:
+ checking: 支票账户
+ savings: 储蓄账户
+ hsa: 健康储蓄账户
+ cd: 定期存单
+ money_market: 货币市场
+ investment:
+ brokerage: 券商账户
+ pension: 养老金
+ retirement: 退休账户
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: 节俭储蓄计划
+ "529_plan": "529 计划"
+ hsa: 健康储蓄账户
+ mutual_fund: 共同基金
+ ira: 传统 IRA
+ roth_ira: Roth IRA
+ angel: 天使投资
+ loan:
+ mortgage: 抵押贷款
+ student: 学生贷款
+ auto: 汽车贷款
+ other: 其他贷款
+ balance: 余额
+ cancel: 取消
+ choose_account_type: "为每个 Mercury 账户选择正确的账户类型:"
+ create_accounts: 创建账户
+ creating_accounts: 正在创建账户...
+ historical_data_range: "历史数据范围:"
+ subtitle: 为您导入的账户选择正确的账户类型
+ sync_start_date_help: 选择您希望向前同步交易历史的时间范围。最多可用 3 年历史记录。
+ sync_start_date_label: "从以下日期开始同步交易:"
+ title: 设置您的 Mercury 账户
+ complete_account_setup:
+ all_skipped: "所有账户都已跳过,未创建任何账户。"
+ creation_failed: "创建账户失败:%{error}"
+ no_accounts: "没有需要设置的账户。"
+ success: "成功创建 %{count} 个账户。"
+ sync:
+ success: 同步已开始
+ update:
+ success: Mercury 连接已更新
+ mercury_item_selection_error_payload:
+ select_connection: 在加载账户之前请选择一个 Mercury 连接。
+ render_mercury_item_selection_failure:
+ select_connection: 请在提供商设置中选择一个 Mercury 连接。
+ no_credentials_configured: 请先在提供商设置中配置您的 Mercury API token。
diff --git a/config/locales/views/messages/vi.yml b/config/locales/views/messages/vi.yml
new file mode 100644
index 000000000..aeb02e98c
--- /dev/null
+++ b/config/locales/views/messages/vi.yml
@@ -0,0 +1,6 @@
+---
+vi:
+ messages:
+ chat_form:
+ placeholder: "Hỏi bất cứ điều gì ..."
+ disclaimer: "Phản hồi của AI chỉ mang tính thông tin. Không phải tư vấn tài chính!"
diff --git a/config/locales/views/messages/zh-CN.yml b/config/locales/views/messages/zh-CN.yml
new file mode 100644
index 000000000..8d2932160
--- /dev/null
+++ b/config/locales/views/messages/zh-CN.yml
@@ -0,0 +1,6 @@
+---
+zh-CN:
+ messages:
+ chat_form:
+ placeholder: 任何问题都可以问……
+ disclaimer: AI 回复仅供参考,不构成财务建议!
diff --git a/config/locales/views/mfa/vi.yml b/config/locales/views/mfa/vi.yml
new file mode 100644
index 000000000..7b3afae37
--- /dev/null
+++ b/config/locales/views/mfa/vi.yml
@@ -0,0 +1,41 @@
+---
+vi:
+ mfa:
+ backup_codes:
+ backup_codes_description: Mỗi mã chỉ có thể được sử dụng một lần. Hãy giữ các mã này an toàn và bảo mật.
+ backup_codes_title: Mã dự phòng của bạn
+ continue: Tiếp tục đến Cài đặt bảo mật
+ description: Lưu trữ các mã dự phòng này ở nơi an toàn - bạn sẽ cần chúng nếu mất quyền truy cập vào ứng dụng xác thực
+ page_title: Mã dự phòng
+ title: Lưu mã dự phòng của bạn
+ create:
+ invalid_code: Mã xác minh không hợp lệ. Vui lòng thử lại.
+ disable:
+ success: Xác thực hai yếu tố đã bị tắt
+ new:
+ code_label: Mã xác minh
+ code_placeholder: Nhập mã 6 chữ số
+ description: Tăng cường bảo mật tài khoản của bạn bằng cách thiết lập xác thực hai yếu tố
+ page_title: Thiết lập xác thực hai yếu tố
+ scan_description: Sử dụng ứng dụng xác thực như Google Authenticator hoặc 1Password để quét mã QR này
+ scan_title: 1. Quét mã QR
+ secret_description: Nếu bạn không thể quét mã QR, hãy nhập mã bí mật này theo cách thủ công vào ứng dụng xác thực của bạn
+ secret_title: Mã nhập thủ công
+ title: Thiết lập xác thực hai yếu tố
+ verify_button: Xác minh và bật 2FA
+ verify_description: Nhập mã 6 chữ số từ ứng dụng xác thực của bạn
+ verify_title: 2. Nhập mã xác minh
+ verify:
+ description: Nhập mã từ ứng dụng xác thực của bạn để tiếp tục
+ or: hoặc
+ page_title: Xác minh xác thực hai yếu tố
+ title: Xác thực hai yếu tố
+ verify_button: Xác minh
+ webauthn_button: Sử dụng khóa passkey hoặc khóa bảo mật
+ webauthn_unsupported: Trình duyệt này không hỗ trợ passkey hoặc khóa bảo mật.
+ verify_code:
+ invalid_code: Mã xác thực không hợp lệ. Vui lòng thử lại.
+ verify_webauthn:
+ invalid_credential: Không thể xác minh passkey hoặc khóa bảo mật đó. Vui lòng thử lại.
+ webauthn_options:
+ unavailable: Không có passkey hoặc khóa bảo mật nào cho tài khoản này.
diff --git a/config/locales/views/mfa/zh-CN.yml b/config/locales/views/mfa/zh-CN.yml
index 9a517fc06..f13c069ad 100644
--- a/config/locales/views/mfa/zh-CN.yml
+++ b/config/locales/views/mfa/zh-CN.yml
@@ -2,33 +2,40 @@
zh-CN:
mfa:
backup_codes:
- backup_codes_description: 每个代码仅能使用一次。请妥善保管这些代码。
+ backup_codes_description: 每个代码只能使用一次。请妥善保管这些代码。
backup_codes_title: 您的备用代码
continue: 继续前往安全设置
- description: 请将这些备用代码保存在安全的地方——当您无法访问身份验证器应用时将需要它们
+ description: 请将这些备用代码存放在安全的地方——如果您无法访问身份验证器应用,它们会派上用场。
page_title: 备用代码
title: 保存您的备用代码
create:
- invalid_code: 验证码无效,请重试。
+ invalid_code: 验证码无效。请重试。
disable:
- success: 双重认证已停用
+ success: 双重认证已禁用
new:
code_label: 验证码
- code_placeholder: 请输入6位数字代码
- description: 设置双重认证以增强账户安全性
+ code_placeholder: 输入 6 位验证码
+ description: 通过设置双重认证来增强账户安全性
page_title: 双重认证设置
scan_description: 使用 Google Authenticator 或 1Password 等身份验证器应用扫描此二维码
scan_title: 1. 扫描二维码
secret_description: 如果无法扫描二维码,请在身份验证器应用中手动输入此密钥
- secret_title: 手动输入密钥
+ secret_title: 手动输入代码
title: 设置双重认证
- verify_button: 验证并启用双重认证
- verify_description: 请输入来自身份验证器应用的6位数字代码
+ verify_button: 验证并启用 2FA
+ verify_description: 输入您身份验证器应用中的 6 位验证码
verify_title: 2. 输入验证码
verify:
- description: 请输入身份验证器应用中的代码以继续
+ description: 输入您身份验证器应用中的验证码以继续
+ or: 或
page_title: 验证双重认证
title: 双重认证
verify_button: 验证
+ webauthn_button: 使用通行密钥或安全密钥
+ webauthn_unsupported: 此浏览器不支持通行密钥或安全密钥。
verify_code:
- invalid_code: 身份验证码无效,请重试。
+ invalid_code: 验证码无效。请重试。
+ verify_webauthn:
+ invalid_credential: 无法验证该通行密钥或安全密钥。请重试。
+ webauthn_options:
+ unavailable: 此账户没有可用的通行密钥或安全密钥。
diff --git a/config/locales/views/oidc_accounts/vi.yml b/config/locales/views/oidc_accounts/vi.yml
new file mode 100644
index 000000000..16fd6f008
--- /dev/null
+++ b/config/locales/views/oidc_accounts/vi.yml
@@ -0,0 +1,42 @@
+---
+vi:
+ oidc_accounts:
+ link:
+ no_pending_oidc: Không tìm thấy xác thực OIDC đang chờ xử lý
+ title_link: Liên kết tài khoản OIDC
+ title_create: Tạo tài khoản
+ verify_heading: Xác minh danh tính của bạn
+ verify_description_html: "Để liên kết tài khoản %{provider} của bạn%{email_suffix}, vui lòng xác minh danh tính bằng cách nhập mật khẩu."
+ email_suffix_html: " (%{email})"
+ email_label: Email
+ email_placeholder: Nhập email của bạn
+ password_label: Mật khẩu
+ password_placeholder: Nhập mật khẩu của bạn
+ verify_hint: Điều này giúp đảm bảo chỉ bạn mới có thể liên kết các tài khoản bên ngoài với hồ sơ của mình.
+ submit_link: Liên kết tài khoản
+ create_heading: Tạo tài khoản mới
+ create_description_html: "Không tìm thấy tài khoản nào với email %{email}. Nhấp bên dưới để tạo tài khoản mới bằng danh tính %{provider} của bạn."
+ info_email: "Email:"
+ info_name: "Tên:"
+ submit_create: Tạo tài khoản
+ submit_accept_invitation: Chấp nhận lời mời
+ account_creation_disabled: Tạo tài khoản mới qua đăng nhập một lần đã bị tắt. Vui lòng liên hệ với quản trị viên để tạo tài khoản của bạn.
+ cancel: Hủy
+ create_link:
+ no_pending_oidc: Không tìm thấy xác thực OIDC đang chờ xử lý
+ new_user:
+ no_pending_oidc: Không tìm thấy xác thực OIDC đang chờ xử lý
+ title: Hoàn tất tài khoản của bạn
+ heading: Tạo tài khoản của bạn
+ description: Vui lòng xác nhận thông tin của bạn để hoàn tất tạo tài khoản với danh tính %{provider}.
+ email_label: Email (từ nhà cung cấp SSO)
+ first_name_label: Tên
+ first_name_placeholder: Nhập tên của bạn
+ last_name_label: Họ
+ last_name_placeholder: Nhập họ của bạn
+ submit: Tạo tài khoản
+ cancel: Hủy
+ create_user:
+ no_pending_oidc: Không tìm thấy xác thực OIDC đang chờ xử lý
+ account_creation_disabled: Tạo tài khoản SSO đã bị tắt. Vui lòng liên hệ với quản trị viên.
+ account_created: "Chào mừng! Tài khoản của bạn đã được tạo."
diff --git a/config/locales/views/onboardings/vi.yml b/config/locales/views/onboardings/vi.yml
new file mode 100644
index 000000000..b11bd6a43
--- /dev/null
+++ b/config/locales/views/onboardings/vi.yml
@@ -0,0 +1,66 @@
+---
+vi:
+ onboardings:
+ header:
+ sign_out: Đăng xuất
+ setup: Thiết lập
+ preferences: Tùy chọn
+ goals: Mục tiêu
+ start: Bắt đầu
+ logout:
+ sign_out: Đăng xuất
+ show:
+ title: Hãy thiết lập tài khoản của bạn
+ subtitle: Trước tiên, hãy thiết lập hồ sơ của bạn.
+ first_name: Tên
+ first_name_placeholder: Tên
+ last_name: Họ
+ last_name_placeholder: Họ
+ group_name: Tên nhóm
+ group_name_placeholder: Tên nhóm
+ household_name: Tên hộ gia đình
+ household_name_placeholder: Tên hộ gia đình
+ moniker_prompt: "Sẽ sử dụng %{product_name} cùng với..."
+ moniker_family: Thành viên gia đình (chỉ bạn hoặc cùng vợ/chồng, con cái, v.v.)
+ moniker_group: Nhóm người (công ty, câu lạc bộ, hiệp hội, loại khác)
+ country: Quốc gia
+ submit: Tiếp tục
+ preferences:
+ title: Cấu hình tùy chọn của bạn
+ subtitle: Hãy cấu hình tùy chọn của bạn.
+ example: Tài khoản mẫu
+ preview: Xem trước cách hiển thị dữ liệu theo tùy chọn.
+ color_theme: Giao diện màu sắc
+ theme_system: Hệ thống
+ theme_light: Sáng
+ theme_dark: Tối
+ locale: Ngôn ngữ
+ currency: Tiền tệ
+ date_format: Định dạng ngày
+ submit: Hoàn tất
+ goals:
+ title: Điều gì đưa bạn đến đây?
+ subtitle: Chọn một hoặc nhiều mục tiêu bạn muốn đạt được khi sử dụng %{product_name} làm công cụ tài chính cá nhân.
+ unified_accounts: Xem tất cả tài khoản ở một nơi
+ cashflow: Hiểu dòng tiền và chi tiêu
+ budgeting: Quản lý kế hoạch tài chính và ngân sách
+ partner: Quản lý tài chính cùng người thân
+ investments: Theo dõi đầu tư
+ ai_insights: Để AI giúp tôi hiểu tài chính
+ optimization: Phân tích và tối ưu hóa tài khoản
+ reduce_stress: Giảm căng thẳng hoặc lo lắng về tài chính
+ submit: Tiếp theo
+ trial:
+ title: Dùng thử Sure trong 45 ngày
+ data_deletion: Dữ liệu sẽ bị xóa sau đó
+ description_html: Bắt đầu từ hôm nay bạn có thể trải nghiệm sản phẩm.Bạn sẽ không thể hoàn tác quyết định này
" + cancel: Hủy + title: Bạn có chắc không? + money_field: + label: Số tiền + exchange_rate_tabs: + calculate_rate_tab: Tính tỷ giá ngoại tệ + convert_tab: Chuyển đổi với tỷ giá + destination_amount: Số tiền đích + exchange_rate: Tỷ giá + exchange_rate_help: Chọn cách nhập số tiền. + syncing_notice: + syncing: Đang đồng bộ dữ liệu tài khoản... + require_admin: "Chỉ quản trị viên mới có thể thực hiện hành động này" + custom_confirm: + default_title: "Bạn có chắc không?" + default_body: "Thao tác này không thể hoàn tác." + default_btn_text: "Xác nhận" + family_moniker: + group_plural: Nhóm + group_singular: Nhóm + plural: Hộ gia đình + singular: Hộ gia đình + trend_change: + no_change: "không thay đổi" + cancel: Hủy + transaction_tabs: + expense: Chi tiêu + income: Thu nhập + transfer: Chuyển khoản diff --git a/config/locales/views/shared/zh-CN.yml b/config/locales/views/shared/zh-CN.yml index a84a48379..2f5964b75 100644 --- a/config/locales/views/shared/zh-CN.yml +++ b/config/locales/views/shared/zh-CN.yml @@ -1,24 +1,38 @@ --- zh-CN: + concerns: + self_hostable: + redis_configured: Redis 现已正确配置!您现在可以设置您的 Sure 应用了。 shared: confirm_modal: accept: 确认 - body_html: "此操作不可撤销
" + body_html: "此决定无法撤销
" cancel: 取消 - title: 确定要执行此操作? + title: 确定吗? money_field: label: 金额 exchange_rate_tabs: calculate_rate_tab: 计算汇率 - convert_tab: 用汇率转换 + convert_tab: 使用汇率换算 destination_amount: 目标金额 exchange_rate: 汇率 exchange_rate_help: 选择如何输入金额。 syncing_notice: - syncing: 账户数据同步中... + syncing: 账户数据同步中…… + require_admin: 只有管理员才能执行此操作 + custom_confirm: + default_title: 确定吗? + default_body: 此操作不可逆。 + default_btn_text: 确认 + family_moniker: + group_plural: 组 + group_singular: 组 + plural: 家庭 + singular: 家庭 + trend_change: + no_change: 无变化 + cancel: 取消 transaction_tabs: expense: 支出 income: 收入 transfer: 转账 - trend_change: - no_change: 无变化 diff --git a/config/locales/views/simplefin_items/vi.yml b/config/locales/views/simplefin_items/vi.yml new file mode 100644 index 000000000..11445ed09 --- /dev/null +++ b/config/locales/views/simplefin_items/vi.yml @@ -0,0 +1,159 @@ +--- +vi: + simplefin_items: + new: + title: Kết nối SimpleFIN + setup_token: Mã thiết lập + setup_token_placeholder: dán mã thiết lập SimpleFIN của bạn vào đây + connect: Kết nối + cancel: Hủy + create: + success: Kết nối SimpleFIN đã được thêm thành công! Các tài khoản của bạn sẽ xuất hiện sau khi đồng bộ trong nền. + errors: + blank_token: Vui lòng nhập mã thiết lập SimpleFIN. + invalid_token: Mã thiết lập không hợp lệ. Vui lòng kiểm tra rằng bạn đã sao chép toàn bộ mã từ SimpleFIN Bridge. + token_compromised: Mã thiết lập có thể đã bị xâm phạm, hết hạn hoặc đã được sử dụng. Vui lòng tạo mã mới. + create_failed: "Kết nối thất bại: %{message}" + unexpected: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại. + destroy: + success: Kết nối SimpleFIN sẽ được gỡ bỏ + update: + success: Kết nối SimpleFIN đã được cập nhật thành công! Các tài khoản của bạn đang được kết nối lại. + errors: + blank_token: Vui lòng nhập mã thiết lập SimpleFIN. + invalid_token: Mã thiết lập không hợp lệ. Vui lòng kiểm tra rằng bạn đã sao chép toàn bộ mã từ SimpleFIN Bridge. + token_compromised: Mã thiết lập có thể đã bị xâm phạm, hết hạn hoặc đã được sử dụng. Vui lòng tạo mã mới. + update_failed: "Cập nhật kết nối thất bại: %{message}" + unexpected: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại. + edit: + setup_token: + label: "Mã thiết lập SimpleFIN:" + placeholder: "Dán mã thiết lập SimpleFIN của bạn vào đây..." + help_text: "Mã phải là một chuỗi dài bắt đầu bằng chữ cái và số" + title: Cập nhật kết nối SimpleFIN + header_subtitle: Lấy mã thiết lập mới để kết nối lại tài khoản SimpleFIN của bạn + connection_needs_update: "Kết nối SimpleFIN của bạn cần được cập nhật:" + step_1_html: "Truy cập SimpleFIN Bridge để tạo mã thiết lập mới" + step_2: Sao chép mã và dán vào bên dưới + step_3: "Nhấn \"Cập nhật\" để khôi phục quyền truy cập" + update: Cập nhật + cancel: Hủy + setup_accounts: + title: Thiết lập tài khoản SimpleFIN của bạn + header_subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập + choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản SimpleFIN:" + account_type_checking_savings: Tài khoản thanh toán hoặc tiết kiệm + account_type_checking_savings_desc: Tài khoản ngân hàng thông thường + account_type_credit_card: Thẻ tín dụng + account_type_credit_card_desc: Tài khoản thẻ tín dụng + account_type_investment: Đầu tư + account_type_investment_desc: "Tài khoản Brokerage, 401(k), IRA" + account_type_loan: Vay hoặc thế chấp + account_type_loan_desc: Tài khoản nợ + account_type_other_asset: Tài sản khác + account_type_other_asset_desc: Tất cả các loại khác + transaction_history_title: "Lịch sử giao dịch:" + transaction_history_description_html: "SimpleFIN thường cung cấp 60-90 ngày lịch sử giao dịch, tùy thuộc vào ngân hàng của bạn. Sau khi thiết lập ban đầu, các giao dịch mới sẽ tự động đồng bộ. Dữ liệu lịch sử có sẵn tùy theo tổ chức và loại tài khoản." + account_type_label: "Loại tài khoản:" + create_accounts: Tạo tài khoản + creating_accounts: Đang tạo tài khoản... + cancel: Hủy + account_card: + balance: "Số dư" + activity: + recent: + one: "1 giao dịch • mới nhất %{when}" + other: "%{count} giao dịch • mới nhất %{when}" + dormant: "Không có hoạt động trong %{days} ngày" + empty: "Chưa nhập giao dịch nào" + likely_closed: "Không có hoạt động gần đây và số dư bằng không — thẻ này có thể đã đóng hoặc bị thay thế" + today: "hôm nay" + yesterday: "hôm qua" + days_ago: + one: "1 ngày trước" + other: "%{count} ngày trước" + stale_accounts: + title: "Tài khoản không còn trong SimpleFIN" + description: "Các tài khoản này tồn tại trong cơ sở dữ liệu của bạn nhưng không còn được SimpleFIN cung cấp. Điều này có thể xảy ra khi cấu hình tài khoản thay đổi ở phía trên." + action_prompt: "Bạn muốn làm gì?" + action_delete: "Xóa tài khoản và tất cả giao dịch" + action_move: "Chuyển giao dịch sang:" + action_skip: "Bỏ qua lúc này" + transaction_count: + one: "%{count} giao dịch" + other: "%{count} giao dịch" + complete_account_setup: + all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo." + no_accounts: "Không có tài khoản nào để thiết lập." + success: + one: "Đã tạo thành công %{count} tài khoản SimpleFIN! Các giao dịch và tài sản của bạn đang được nhập trong nền." + other: "Đã tạo thành công %{count} tài khoản SimpleFIN! Các giao dịch và tài sản của bạn đang được nhập trong nền." + stale_accounts_processed: "Tài khoản cũ: %{deleted} đã xóa, %{moved} đã chuyển." + stale_accounts_errors: + one: "%{count} thao tác tài khoản cũ thất bại. Kiểm tra nhật ký để biết chi tiết." + other: "%{count} thao tác tài khoản cũ thất bại. Kiểm tra nhật ký để biết chi tiết." + simplefin_item: + add_new: Thêm kết nối mới + confirm_accept: Xóa kết nối + confirm_body: Thao tác này sẽ xóa vĩnh viễn tất cả tài khoản trong nhóm này và tất cả dữ liệu liên quan. + confirm_title: Xóa kết nối SimpleFIN? + delete: Xóa + deletion_in_progress: "(đang xóa...)" + error: Đã xảy ra lỗi khi đồng bộ dữ liệu + no_accounts_description: Kết nối này chưa có tài khoản nào được đồng bộ. + no_accounts_title: Không tìm thấy tài khoản + requires_update: Kết nối lại + setup_needed: Tài khoản mới sẵn sàng để thiết lập + setup_description: Chọn loại tài khoản cho các tài khoản SimpleFIN mới được nhập. + setup_action: Thiết lập tài khoản mới + setup_accounts_menu: Thiết lập tài khoản + more_accounts_available: + one: "%{count} tài khoản khác có sẵn để thiết lập" + other: "%{count} tài khoản khác có sẵn để thiết lập" + accounts_skipped_tooltip: "Một số tài khoản đã bị bỏ qua do lỗi trong quá trình đồng bộ" + accounts_skipped_label: "Đã bỏ qua: %{count}" + rate_limited_ago: "Bị giới hạn tốc độ (%{time} trước)" + rate_limited_recently: "Bị giới hạn tốc độ gần đây" + status: Đồng bộ lần cuối %{timestamp} trước + status_never: Chưa bao giờ đồng bộ + status_with_summary: "Đồng bộ lần cuối %{timestamp} trước • %{summary}" + syncing: Đang đồng bộ... + update: Cập nhật + stale_pending_note: "(loại trừ khỏi ngân sách)" + stale_pending_accounts: "trong: %{accounts}" + reconciled_details_note: "(xem tóm tắt đồng bộ để biết chi tiết)" + duplicate_accounts_skipped: "Một số tài khoản đã bị bỏ qua vì trùng lặp — dùng 'Liên kết tài khoản hiện có' để hợp nhất." + select_existing_account: + title: "Liên kết %{account_name} với SimpleFIN" + description: Chọn tài khoản SimpleFIN để liên kết với tài khoản hiện có của bạn + cancel: Hủy + link_account: Liên kết tài khoản + no_accounts_found: "Không tìm thấy tài khoản SimpleFIN nào cho %{moniker} này." + wait_for_sync: Nếu bạn vừa kết nối hoặc đồng bộ, hãy thử lại sau khi quá trình đồng bộ hoàn tất. + unlink_to_move: Để chuyển liên kết, trước tiên hãy hủy liên kết từ menu thao tác của tài khoản. + all_accounts_already_linked: Tất cả tài khoản SimpleFIN có vẻ đã được liên kết. + currently_linked_to: "Hiện đang liên kết với: %{account_name}" + + link_existing_account: + success: Tài khoản đã được liên kết thành công với SimpleFIN + errors: + only_manual: Chỉ tài khoản thủ công mới có thể được liên kết + different_provider: Tài khoản này đang liên kết với nhà cung cấp khác. Hãy hủy liên kết từ nhà cung cấp đó trước, sau đó liên kết với SimpleFIN. + invalid_simplefin_account: Tài khoản SimpleFIN được chọn không hợp lệ + dismiss_replacement_suggestion: + dismissed: Đề xuất thay thế đã bị bỏ qua + replacement_prompt: + title: "Thẻ %{institution} của bạn có thể đã được thay thế" + description: '"%{account_name}" được liên kết với "%{old_name}", vốn không có hoạt động gần đây và số dư bằng không. Một thẻ mới, "%{new_name}", hiện đang hoạt động tại cùng tổ chức. Liên kết lại để giữ nguyên lịch sử của bạn.' + relink: Liên kết lại với thẻ mới + confirm_title: Liên kết lại với thẻ mới? + confirm_body: '"%{account_name}" sẽ được liên kết với "%{new_name}". Lịch sử giao dịch vẫn được giữ; các giao dịch tương lai sẽ đến từ thẻ mới.' + dismiss_aria: Bỏ qua đề xuất thay thế + reconciled_status: + message: + one: "%{count} giao dịch đang chờ trùng lặp đã được đối soát" + other: "%{count} giao dịch đang chờ trùng lặp đã được đối soát" + stale_pending_status: + message: + one: "%{count} giao dịch đang chờ cũ hơn %{days} ngày" + other: "%{count} giao dịch đang chờ cũ hơn %{days} ngày" diff --git a/config/locales/views/simplefin_items/zh-CN.yml b/config/locales/views/simplefin_items/zh-CN.yml index 1000cdd8d..65ca9fe27 100644 --- a/config/locales/views/simplefin_items/zh-CN.yml +++ b/config/locales/views/simplefin_items/zh-CN.yml @@ -1,71 +1,159 @@ --- zh-CN: simplefin_items: - complete_account_setup: - all_skipped: 所有账户已跳过。未创建任何账户。 - no_accounts: 无待设置账户。 - success: 已成功创建 %{count} 个 SimpleFIN 账户!您的交易和投资数据正在后台导入。 - create: - errors: - blank_token: 请输入 SimpleFIN 设置令牌。 - create_failed: 连接失败:%{message} - invalid_token: 设置令牌无效。请检查是否从 SimpleFIN Bridge 复制了完整的令牌。 - token_compromised: 设置令牌可能已泄露、过期或已被使用。请创建新令牌。 - unexpected: 发生意外错误。请重试或联系支持团队。 - success: SimpleFIN 连接已成功添加!您的账户将在后台同步后显示。 - destroy: - success: SimpleFIN 连接将被移除 - edit: - setup_token: - help_text: 令牌应为以字母和数字开头的长字符串 - label: SimpleFIN 设置令牌: - placeholder: 在此粘贴您的 SimpleFIN 设置令牌... - link_existing_account: - errors: - invalid_simplefin_account: 所选 SimpleFIN 账户无效 - only_manual: 只能关联手动账户 - success: 账户已成功关联到 SimpleFIN new: - cancel: 取消 - connect: 连接 + title: 连接 SimpleFIN setup_token: 设置令牌 setup_token_placeholder: 粘贴您的 SimpleFIN 设置令牌 - title: 连接 SimpleFIN - select_existing_account: - all_accounts_already_linked: 所有 SimpleFIN 账户似乎均已关联。 + connect: 连接 cancel: 取消 - check_provider_health: 检查服务提供商状态 - currently_linked_to: "当前关联到:%{account_name}" - description: 选择一个 SimpleFIN 账户与您的现有账户关联 - link_account: 关联账户 - no_accounts_found: 此家庭未找到 SimpleFIN 账户。 - title: 将 %{account_name} 关联到 SimpleFIN - unlink_to_move: 如需移动关联,请先从账户操作菜单中解除当前关联。 - wait_for_sync: 如果您刚刚连接或同步完成,请等待同步完成后再试。 + create: + success: SimpleFIN 连接添加成功!您的账户将在后台同步完成后很快显示。 + errors: + blank_token: 请输入 SimpleFIN 设置令牌。 + invalid_token: 设置令牌无效。请确认您已从 SimpleFIN Bridge 复制完整令牌。 + token_compromised: 设置令牌可能已泄露、过期或已使用过。请创建新的令牌。 + create_failed: 连接失败:%{message} + unexpected: 发生意外错误,请稍后再试。 + destroy: + success: SimpleFIN 连接将被移除 + update: + success: SimpleFIN 连接更新成功!您的账户正在重新连接。 + errors: + blank_token: 请输入 SimpleFIN 设置令牌。 + invalid_token: 设置令牌无效。请确认您已从 SimpleFIN Bridge 复制完整令牌。 + token_compromised: 设置令牌可能已泄露、过期或已使用过。请创建新的令牌。 + update_failed: 更新连接失败:%{message} + unexpected: 发生意外错误,请稍后再试。 + edit: + setup_token: + label: SimpleFIN 设置令牌: + placeholder: 在此粘贴您的 SimpleFIN 设置令牌…… + help_text: 令牌应为以字母和数字开头的长字符串 + title: 更新 SimpleFIN 连接 + header_subtitle: 获取新的设置令牌以重新连接您的 SimpleFIN 账户 + connection_needs_update: 您的 SimpleFIN 连接需要更新: + step_1_html: 前往 SimpleFIN Bridge 创建新的设置令牌 + step_2: 复制令牌并粘贴到下方 + step_3: 点击“更新”以恢复访问 + update: 更新 + cancel: 取消 + setup_accounts: + title: 设置您的 SimpleFIN 账户 + header_subtitle: 为导入的账户选择正确的账户类型 + choose_account_type: 为每个 SimpleFIN 账户选择正确的账户类型: + account_type_checking_savings: 支票账户或储蓄账户 + account_type_checking_savings_desc: 普通银行账户 + account_type_credit_card: 信用卡 + account_type_credit_card_desc: 信用卡账户 + account_type_investment: 投资账户 + account_type_investment_desc: 券商、401(k)、IRA 账户 + account_type_loan: 贷款或抵押贷款 + account_type_loan_desc: 债务账户 + account_type_other_asset: 其他资产 + account_type_other_asset_desc: 其他所有类型 + transaction_history_title: 交易历史: + transaction_history_description_html: SimpleFIN 通常会提供60–90 天的交易历史,具体取决于您的银行。完成初始设置后,新的交易将继续自动同步。历史数据可用性因机构和账户类型而异。 + account_type_label: 账户类型: + create_accounts: 创建账户 + creating_accounts: 正在创建账户…… + cancel: 取消 + account_card: + balance: 余额 + activity: + recent: + one: 1 笔交易 • 最近于 %{when} + other: "%{count} 笔交易 • 最近于 %{when}" + dormant: "%{days} 天内无活动" + empty: 尚未导入交易 + likely_closed: 无近期活动且余额为零——这可能是已关闭或已替换的卡片 + today: 今天 + yesterday: 昨天 + days_ago: + one: 1 天前 + other: "%{count} 天前" + stale_accounts: + title: 不再属于 SimpleFIN 的账户 + description: 这些账户仍存在于您的数据库中,但已不再由 SimpleFIN 提供。这种情况可能发生在上游账户配置变更时。 + action_prompt: 您希望怎么处理? + action_delete: 删除账户及其所有交易 + action_move: 将交易移动到: + action_skip: 暂时跳过 + transaction_count: + one: "%{count} 笔交易" + other: "%{count} 笔交易" + complete_account_setup: + all_skipped: 所有账户都已跳过。未创建任何账户。 + no_accounts: 没有可设置的账户。 + success: + one: 成功创建 %{count} 个 SimpleFIN 账户!您的交易和持仓正在后台导入。 + other: 成功创建 %{count} 个 SimpleFIN 账户!您的交易和持仓正在后台导入。 + stale_accounts_processed: 过期账户:已删除 %{deleted} 个,已移动 %{moved} 个。 + stale_accounts_errors: + one: "%{count} 个过期账户操作失败,请查看日志了解详情。" + other: "%{count} 个过期账户操作失败,请查看日志了解详情。" simplefin_item: add_new: 添加新连接 confirm_accept: 删除连接 - confirm_body: 此操作将永久删除该组下的所有账户及相关数据。 - confirm_title: 确认删除 SimpleFIN 连接? + confirm_body: 这将永久删除此组中的所有账户及其相关数据。 + confirm_title: 删除 SimpleFIN 连接? delete: 删除 - deletion_in_progress: "(删除进行中...)" + deletion_in_progress: (正在删除中……) error: 同步数据时发生错误 - no_accounts_description: 此连接尚未同步任何账户。 + no_accounts_description: 此连接尚未包含任何已同步账户。 no_accounts_title: 未找到账户 - requires_update: 需要重新认证 + requires_update: 重新连接 + setup_needed: 新账户已准备就绪,需要设置 + setup_description: 为您新导入的 SimpleFIN 账户选择账户类型。 setup_action: 设置新账户 - setup_description: 请为您新导入的 SimpleFIN 账户选择账户类型。 - setup_needed: 新账户待设置 + setup_accounts_menu: 设置账户 + more_accounts_available: + one: 还有 %{count} 个账户可设置 + other: 还有 %{count} 个账户可设置 + accounts_skipped_tooltip: 某些账户因同步错误被跳过 + accounts_skipped_label: 已跳过:%{count} + rate_limited_ago: (%{time} 前被限流) + rate_limited_recently: 最近被限流 status: 上次同步于 %{timestamp} 前 status_never: 从未同步 - status_with_summary: 同步于 %{timestamp} 前 • %{summary} - syncing: 同步中... - update: 更新连接 - update: + status_with_summary: 上次同步于 %{timestamp} 前 • %{summary} + syncing: 正在同步…… + update: 更新 + stale_pending_note: (不计入预算) + stale_pending_accounts: 在:%{accounts} + reconciled_details_note: (详情请查看同步摘要) + duplicate_accounts_skipped: 某些账户因重复被跳过——请使用“链接现有账户”进行合并。 + select_existing_account: + title: 将 %{account_name} 连接到 SimpleFIN + description: 选择一个 SimpleFIN 账户以连接到您现有的账户 + cancel: 取消 + link_account: 连接账户 + no_accounts_found: 此 %{moniker} 未找到任何 SimpleFIN 账户。 + wait_for_sync: 如果您刚刚连接或同步,请等待同步完成后再试。 + unlink_to_move: 若要移动连接,请先在账户操作菜单中取消其连接。 + all_accounts_already_linked: 所有 SimpleFIN 账户似乎都已连接。 + currently_linked_to: 当前连接到:%{account_name} + check_provider_health: 请检查您的 API 凭据和同步状态。 + link_existing_account: + success: 账户已成功连接到 SimpleFIN errors: - blank_token: 请输入 SimpleFIN 设置令牌。 - invalid_token: 设置令牌无效。请检查是否从 SimpleFIN Bridge 复制了完整的令牌。 - token_compromised: 设置令牌可能已泄露、过期或已被使用。请创建新令牌。 - unexpected: 发生意外错误。请重试或联系支持团队。 - update_failed: 更新连接失败:%{message} - success: SimpleFIN 连接更新成功!您的账户正在重新连接中。 + only_manual: 只有手动账户才能连接 + different_provider: 此账户已连接到其他提供商。请先取消与该提供商的连接,然后再连接到 SimpleFIN。 + invalid_simplefin_account: 所选 SimpleFIN 账户无效 + dismiss_replacement_suggestion: + dismissed: 已忽略替换建议 + replacement_prompt: + title: 您的 %{institution} 卡片可能已被替换 + description: “%{account_name}” 连接到 “%{old_name}”,该卡片近期没有活动且余额为零。现在,同一机构中有一张新卡 “%{new_name}” 已激活。重新关联可保留您的历史记录。 + relink: 重新关联到新卡 + confirm_title: 重新关联到新卡? + confirm_body: “%{account_name}” 将连接到 “%{new_name}”。您的交易历史将保留,未来交易将来自新卡。 + dismiss_aria: 忽略替换建议 + reconciled_status: + message: + one: 已勾稽 %{count} 笔重复待处理交易 + other: 已勾稽 %{count} 笔重复待处理交易 + stale_pending_status: + message: + one: 1 笔待处理交易早于 %{days} 天 + other: "%{count} 笔待处理交易早于 %{days} 天" diff --git a/config/locales/views/snaptrade_items/vi.yml b/config/locales/views/snaptrade_items/vi.yml new file mode 100644 index 000000000..61a95981d --- /dev/null +++ b/config/locales/views/snaptrade_items/vi.yml @@ -0,0 +1,191 @@ +vi: + snaptrade_items: + default_name: "Kết nối SnapTrade" + link_accounts: + use_setup_flow: Sử dụng luồng thiết lập tài khoản + create: + success: "Đã cấu hình SnapTrade thành công." + update: + success: "Đã cập nhật cấu hình SnapTrade thành công." + destroy: + success: "Đã lên lịch xóa kết nối SnapTrade." + connect: + decryption_failed: "Không thể đọc thông tin xác thực SnapTrade. Vui lòng xóa và tạo lại kết nối này." + connection_failed: "Kết nối SnapTrade thất bại: %{message}" + callback: + success: "Đã kết nối brokerage! Vui lòng chọn tài khoản muốn liên kết." + no_item: "Không tìm thấy cấu hình SnapTrade." + complete_account_setup: + success: + one: "Đã liên kết thành công %{count} tài khoản." + other: "Đã liên kết thành công %{count} tài khoản." + partial_success: + one: "Đã liên kết %{count} tài khoản. %{failed_count} liên kết thất bại." + other: "Đã liên kết %{count} tài khoản. %{failed_count} liên kết thất bại." + link_failed: "Liên kết tài khoản thất bại: %{errors}" + no_accounts: "Không có tài khoản nào được chọn để liên kết." + preload_accounts: + not_configured: "SnapTrade chưa được cấu hình." + select_accounts: + not_configured: "SnapTrade chưa được cấu hình." + select_existing_account: + not_found: "Không tìm thấy tài khoản hoặc cấu hình SnapTrade." + title: "Liên kết với tài khoản SnapTrade" + header: "Liên kết tài khoản hiện có" + subtitle: "Chọn tài khoản SnapTrade để liên kết" + no_accounts: "Không có tài khoản SnapTrade chưa liên kết." + connect_hint: "Bạn có thể cần kết nối brokerage trước." + settings_link: "Đi đến Cài đặt nhà cung cấp" + linking_to: "Đang liên kết với tài khoản:" + balance_label: "Số dư:" + link_button: "Liên kết" + cancel_button: "Hủy" + link_existing_account: + success: "Đã liên kết thành công với tài khoản SnapTrade." + failed: "Liên kết tài khoản thất bại: %{message}" + not_found: "Không tìm thấy tài khoản." + connections: + unknown_brokerage: "Brokerage không xác định" + delete_connection: + success: "Đã xóa kết nối thành công. Một slot đã được giải phóng." + failed: "Xóa kết nối thất bại: %{message}" + missing_authorization_id: "Thiếu ID ủy quyền" + api_deletion_failed: "Không thể xóa kết nối khỏi SnapTrade - thiếu thông tin xác thực. Kết nối có thể vẫn tồn tại trong tài khoản SnapTrade của bạn." + delete_orphaned_user: + success: "Đã xóa đăng ký mồ côi thành công." + failed: "Xóa đăng ký mồ côi thất bại." + setup_accounts: + title: "Thiết lập tài khoản SnapTrade" + header: "Thiết lập tài khoản SnapTrade của bạn" + subtitle: "Chọn tài khoản brokerage muốn liên kết" + syncing: "Đang lấy tài khoản của bạn..." + loading: "Đang tải tài khoản từ SnapTrade..." + loading_hint: "Nhấn Làm mới để kiểm tra tài khoản." + refresh: "Làm mới" + info_title: "Dữ liệu đầu tư SnapTrade" + info_holdings: "Tài sản nắm giữ với giá hiện tại và số lượng" + info_cost_basis: "Cơ sở chi phí theo vị thế (nếu có)" + info_activities: "Lịch sử giao dịch với nhãn hoạt động (Mua, Bán, Cổ tức, v.v.)" + info_history: "Lịch sử giao dịch tối đa 3 năm" + free_tier_note: "Gói miễn phí SnapTrade cho phép 5 kết nối brokerage. Kiểm tra bảng điều khiển SnapTrade để xem mức sử dụng hiện tại." + no_accounts_title: "Không tìm thấy tài khoản" + no_accounts_message: "Không tìm thấy tài khoản brokerage nào. Điều này có thể xảy ra nếu bạn đã hủy kết nối hoặc brokerage không được hỗ trợ." + try_again: "Kết nối Brokerage" + back_to_settings: "Quay lại Cài đặt" + available_accounts: "Tài khoản có sẵn" + balance_label: "Số dư:" + account_number: "Tài khoản:" + sync_start_date_label: "Nhập giao dịch từ:" + sync_start_date_help: "Để trống để lấy toàn bộ lịch sử có sẵn" + create_button: "Tạo tài khoản đã chọn" + cancel_button: "Hủy" + creating: "Đang tạo tài khoản..." + done_button: "Xong" + or_link_existing: "Hoặc liên kết với tài khoản hiện có thay vì tạo mới:" + select_account: "Chọn một tài khoản..." + link_button: "Liên kết" + linked_accounts: "Đã liên kết" + linked_to: "Liên kết với:" + snaptrade_item: + accounts_need_setup: + one: "%{count} tài khoản cần thiết lập" + other: "%{count} tài khoản cần thiết lập" + deletion_in_progress: "Đang xóa..." + syncing: "Đang đồng bộ..." + requires_update: "Kết nối cần cập nhật" + error: "Lỗi đồng bộ" + status: "Đồng bộ lần cuối %{timestamp} trước - %{summary}" + status_never: "Chưa bao giờ đồng bộ" + reconnect: "Kết nối lại" + connect_brokerage: "Kết nối Brokerage" + add_another_brokerage: "Kết nối thêm brokerage" + delete: "Xóa" + setup_needed: "Tài khoản cần thiết lập" + setup_description: "Một số tài khoản từ SnapTrade cần được liên kết với tài khoản Sure." + setup_action: "Thiết lập tài khoản" + setup_accounts_menu: "Thiết lập tài khoản" + manage_connections: "Quản lý kết nối" + more_accounts_available: + one: "%{count} tài khoản khác có sẵn để thiết lập" + other: "%{count} tài khoản khác có sẵn để thiết lập" + no_accounts_title: "Chưa phát hiện tài khoản nào" + no_accounts_description: "Kết nối brokerage để nhập tài khoản đầu tư của bạn." + + providers: + snaptrade: + name: "SnapTrade" + connection_description: "Kết nối với brokerage qua SnapTrade (hỗ trợ 25+ sàn)" + description: "SnapTrade kết nối với 25+ brokerage lớn (Fidelity, Vanguard, Schwab, Robinhood, v.v.) và cung cấp lịch sử giao dịch đầy đủ với nhãn hoạt động và cơ sở chi phí." + setup_title: "Hướng dẫn thiết lập:" + step_1_html: "Tạo tài khoản tại dashboard.snaptrade.com" + step_2: "Sao chép Client ID và Consumer Key từ bảng điều khiển" + step_3: "Nhập thông tin xác thực bên dưới và nhấn Lưu" + step_4: "Đi đến trang Tài khoản và dùng 'Kết nối thêm brokerage' để liên kết tài khoản đầu tư" + free_tier_warning: "Gói miễn phí SnapTrade hỗ trợ 5 kết nối brokerage. Nâng cấp trên SnapTrade để có thêm." + client_id_label: "Client ID" + client_id_placeholder: "Nhập Client ID SnapTrade của bạn" + client_id_update_placeholder: "Nhập Client ID mới để cập nhật" + consumer_key_label: "Consumer Key" + consumer_key_placeholder: "Nhập Consumer Key SnapTrade của bạn" + consumer_key_update_placeholder: "Nhập Consumer Key mới để cập nhật" + save_button: "Lưu cấu hình" + update_button: "Cập nhật cấu hình" + status_connected: + one: "%{count} tài khoản từ SnapTrade" + other: "%{count} tài khoản từ SnapTrade" + status_needs_registration: "Đã lưu thông tin xác thực. Hoàn tất thiết lập để kết nối brokerage." + needs_setup: + one: "%{count} cần thiết lập" + other: "%{count} cần thiết lập" + status_ready: "Sẵn sàng kết nối brokerage" + setup_accounts_button: "Thiết lập tài khoản" + connect_button: "Kết nối Brokerage" + connected_brokerages: "Đã kết nối:" + manage_connections: "Quản lý kết nối" + loading_connections: "Đang tải kết nối..." + connections_error: "Tải kết nối thất bại: %{message}" + accounts_count: + one: "%{count} tài khoản" + other: "%{count} tài khoản" + orphaned_connection: "Kết nối mồ côi (không đồng bộ cục bộ)" + needs_linking: "cần liên kết" + no_connections: "Không tìm thấy kết nối brokerage nào." + delete_connection: "Xóa" + delete_connection_title: "Xóa kết nối Brokerage?" + delete_connection_body: "Thao tác này sẽ xóa vĩnh viễn kết nối %{brokerage} khỏi SnapTrade. Tất cả tài khoản từ brokerage này sẽ bị hủy liên kết. Bạn cần kết nối lại để đồng bộ các tài khoản này." + delete_connection_confirm: "Xóa kết nối" + orphaned_users_title: + one: "%{count} đăng ký mồ côi" + other: "%{count} đăng ký mồ côi" + orphaned_users_description: "Đây là các đăng ký người dùng SnapTrade cũ đang sử dụng slot kết nối của bạn. Xóa chúng để giải phóng slot." + orphaned_user: "Đăng ký mồ côi" + delete_orphaned_user: "Xóa" + delete_orphaned_user_title: "Xóa đăng ký mồ côi?" + delete_orphaned_user_body: "Thao tác này sẽ xóa vĩnh viễn người dùng SnapTrade mồ côi này và tất cả kết nối brokerage của họ, giải phóng slot kết nối." + delete_orphaned_user_confirm: "Xóa đăng ký" + + snaptrade_item: + sync_status: + no_accounts: "Không tìm thấy tài khoản" + synced: + one: "%{count} tài khoản đã đồng bộ" + other: "%{count} tài khoản đã đồng bộ" + synced_with_setup: "%{linked} đã đồng bộ, %{unlinked} cần thiết lập" + institution_summary: + none: "Chưa kết nối tổ chức nào" + count: + one: "%{count} tổ chức" + other: "%{count} tổ chức" + brokerage_summary: + none: "Chưa kết nối brokerage nào" + count: + one: "%{count} brokerage" + other: "%{count} brokerage" + syncer: + discovering: "Đang phát hiện tài khoản..." + importing: "Đang nhập tài khoản từ SnapTrade..." + processing: "Đang xử lý tài sản nắm giữ và hoạt động..." + calculating: "Đang tính toán số dư..." + checking_config: "Đang kiểm tra cấu hình tài khoản..." + needs_setup: "%{count} tài khoản cần thiết lập..." + activities_fetching_async: "Các hoạt động đang được lấy trong nền. Quá trình này có thể mất đến một phút đối với kết nối brokerage mới." diff --git a/config/locales/views/snaptrade_items/zh-CN.yml b/config/locales/views/snaptrade_items/zh-CN.yml new file mode 100644 index 000000000..caac3138d --- /dev/null +++ b/config/locales/views/snaptrade_items/zh-CN.yml @@ -0,0 +1,190 @@ +--- +zh-CN: + snaptrade_items: + default_name: "SnapTrade 连接" + link_accounts: + use_setup_flow: 请改用账户设置流程 + create: + success: SnapTrade 配置成功。 + update: + success: SnapTrade 配置已更新。 + destroy: + success: SnapTrade 连接已安排删除。 + connect: + decryption_failed: 无法读取 SnapTrade 凭据。请删除并重新创建此连接。 + connection_failed: 连接 SnapTrade 失败:%{message} + callback: + success: 券商已连接!请选择要关联的账户。 + no_item: 未找到 SnapTrade 配置。 + complete_account_setup: + success: + one: 已成功关联 %{count} 个账户。 + other: 已成功关联 %{count} 个账户。 + partial_success: + one: 已关联 %{count} 个账户。%{failed_count} 个关联失败。 + other: 已关联 %{count} 个账户。%{failed_count} 个关联失败。 + link_failed: 关联账户失败:%{errors} + no_accounts: 未选择任何要关联的账户。 + preload_accounts: + not_configured: SnapTrade 尚未配置。 + select_accounts: + not_configured: SnapTrade 尚未配置。 + select_existing_account: + not_found: 未找到账户或 SnapTrade 配置。 + title: 关联到 SnapTrade 账户 + header: 关联现有账户 + subtitle: 选择要关联的 SnapTrade 账户 + no_accounts: 没有可关联的未链接 SnapTrade 账户。 + connect_hint: 您可能需要先连接一个券商。 + settings_link: 前往提供商设置 + linking_to: 正在关联到账户: + balance_label: 余额: + link_button: 关联 + cancel_button: 取消 + link_existing_account: + success: 已成功关联到 SnapTrade 账户。 + failed: 关联账户失败:%{message} + not_found: 未找到账户。 + connections: + unknown_brokerage: 未知券商 + delete_connection: + success: 连接删除成功。已释放一个名额。 + failed: 删除连接失败:%{message} + missing_authorization_id: 缺少授权 ID + api_deletion_failed: 无法从 SnapTrade 删除连接 - 缺少凭据。该连接可能仍存在于您的 SnapTrade 账户中。 + delete_orphaned_user: + success: 孤立注册已成功删除。 + failed: 删除孤立注册失败。 + setup_accounts: + title: 设置 SnapTrade 账户 + header: 设置您的 SnapTrade 账户 + subtitle: 选择要关联的券商账户 + syncing: 正在获取您的账户... + loading: 正在从 SnapTrade 获取账户... + loading_hint: 点击刷新检查是否有账户。 + refresh: 刷新 + info_title: SnapTrade 投资数据 + info_holdings: 含当前价格和数量的持仓 + info_cost_basis: 每个持仓的成本基础(如可用) + info_activities: 带活动标签的交易历史(买入、卖出、分红等) + info_history: 最多 3 年的交易历史 + free_tier_note: SnapTrade 免费套餐允许 5 个券商连接。当前使用情况请查看您的 SnapTrade 仪表盘。 + no_accounts_title: 未找到账户 + no_accounts_message: 未找到券商账户。这可能是因为您取消了连接,或者该券商不受支持。 + try_again: 连接券商 + back_to_settings: 返回设置 + available_accounts: 可用账户 + balance_label: 余额: + account_number: 账户: + sync_start_date_label: 从以下日期导入交易: + sync_start_date_help: 留空则使用全部可用历史 + create_button: 创建所选账户 + cancel_button: 取消 + creating: 正在创建账户... + done_button: 完成 + or_link_existing: 或者关联到现有账户,而不是创建新账户: + select_account: 选择一个账户... + link_button: 关联 + linked_accounts: 已关联 + linked_to: 已关联到: + snaptrade_item: + accounts_need_setup: + one: "%{count} 个账户需要设置" + other: "%{count} 个账户需要设置" + deletion_in_progress: 正在删除... + syncing: 正在同步... + requires_update: 连接需要更新 + error: 同步错误 + status: "%{timestamp} 前上次同步 - %{summary}" + status_never: 从未同步 + reconnect: 重新连接 + connect_brokerage: 连接券商 + add_another_brokerage: 再连接一个券商 + delete: 删除 + setup_needed: 账户需要设置 + setup_description: SnapTrade 的部分账户需要关联到 Sure 账户。 + setup_action: 设置账户 + setup_accounts_menu: 设置账户 + manage_connections: 管理连接 + more_accounts_available: + one: 还有 %{count} 个账户可设置 + other: 还有 %{count} 个账户可设置 + no_accounts_title: 未发现账户 + no_accounts_description: 连接一个券商以导入您的投资账户。 + providers: + snaptrade: + name: "SnapTrade" + connection_description: 通过 SnapTrade 连接您的券商(支持 25+ 券商) + description: "SnapTrade 连接 25+ 主流券商(Fidelity、Vanguard、Schwab、Robinhood 等),并提供带活动标签和成本基础的完整交易历史。" + setup_title: "设置说明:" + step_1_html: "在 dashboard.snaptrade.com 创建账户" + step_2: "从仪表盘复制您的 Client ID 和 Consumer Key" + step_3: "在下方输入您的凭据并点击保存" + step_4: "前往账户页面,使用“连接另一个券商”来关联您的投资账户" + free_tier_warning: "SnapTrade 免费套餐覆盖 5 个券商连接。更多功能请在 SnapTrade 升级。" + client_id_label: "Client ID" + client_id_placeholder: "输入您的 SnapTrade Client ID" + client_id_update_placeholder: "输入新的 Client ID 以更新" + consumer_key_label: "Consumer Key" + consumer_key_placeholder: "输入您的 SnapTrade Consumer Key" + consumer_key_update_placeholder: "输入新的 Consumer Key 以更新" + save_button: "保存配置" + update_button: "更新配置" + status_connected: + one: "%{count} 个来自 SnapTrade 的账户" + other: "%{count} 个来自 SnapTrade 的账户" + status_needs_registration: "凭据已保存。完成设置后即可连接券商。" + needs_setup: + one: "%{count} 个需要设置" + other: "%{count} 个需要设置" + status_ready: "已准备好连接券商" + setup_accounts_button: "设置账户" + connect_button: "连接券商" + connected_brokerages: "已连接:" + manage_connections: "管理连接" + loading_connections: "正在加载连接..." + connections_error: "加载连接失败:%{message}" + accounts_count: + one: "%{count} 个账户" + other: "%{count} 个账户" + orphaned_connection: "孤立连接(未在本地同步)" + needs_linking: "需要关联" + no_connections: "未找到券商连接。" + delete_connection: "删除" + delete_connection_title: "删除券商连接?" + delete_connection_body: "这将永久从 SnapTrade 中移除 %{brokerage} 连接。该券商的所有账户都会取消关联。您需要重新连接才能再次同步这些账户。" + delete_connection_confirm: "删除连接" + orphaned_users_title: + one: "%{count} 个孤立注册" + other: "%{count} 个孤立注册" + orphaned_users_description: "这些是之前使用您连接名额的 SnapTrade 用户注册。删除它们可释放名额。" + orphaned_user: "孤立注册" + delete_orphaned_user: "删除" + delete_orphaned_user_title: "删除孤立注册?" + delete_orphaned_user_body: "这将永久删除该孤立的 SnapTrade 用户及其所有券商连接,释放连接名额。" + delete_orphaned_user_confirm: "删除注册" + snaptrade_item: + sync_status: + no_accounts: "未找到账户" + synced: + one: "%{count} 个账户已同步" + other: "%{count} 个账户已同步" + synced_with_setup: "%{linked} 个已同步,%{unlinked} 个需要设置" + institution_summary: + none: "未连接任何机构" + count: + one: "%{count} 个机构" + other: "%{count} 个机构" + brokerage_summary: + none: "未连接任何券商" + count: + one: "%{count} 个券商" + other: "%{count} 个券商" + syncer: + discovering: 正在发现账户... + importing: 正在从 SnapTrade 导入账户... + processing: 正在处理持仓和活动... + calculating: 正在计算余额... + checking_config: 正在检查账户配置... + needs_setup: "%{count} 个账户需要设置..." + activities_fetching_async: 活动正在后台获取中。对于新的券商连接,这可能需要最多一分钟。 \ No newline at end of file diff --git a/config/locales/views/sophtron_items/vi.yml b/config/locales/views/sophtron_items/vi.yml new file mode 100644 index 000000000..e6a7ba75c --- /dev/null +++ b/config/locales/views/sophtron_items/vi.yml @@ -0,0 +1,313 @@ +--- +vi: + sophtron_items: + defaults: + name: Kết nối Sophtron + new: + title: Kết nối Sophtron + user_id: ID người dùng + user_id_placeholder: dán ID người dùng Sophtron của bạn vào đây + access_key: Khóa truy cập + access_key_placeholder: dán Khóa truy cập Sophtron của bạn vào đây + connect: Kết nối + cancel: Hủy + create: + success: Kết nối Sophtron đã được tạo thành công + destroy: + success: Kết nối Sophtron đã được xóa + update: + success: Kết nối Sophtron đã được cập nhật thành công! Các tài khoản của bạn đang được kết nối lại. + errors: + blank_user_id: Vui lòng nhập ID người dùng Sophtron. + invalid_user_id: ID người dùng không hợp lệ. Vui lòng kiểm tra rằng bạn đã sao chép toàn bộ ID người dùng từ Sophtron. + user_id_compromised: ID người dùng có thể đã bị xâm phạm, hết hạn hoặc đã được sử dụng. Vui lòng tạo mới. + blank_access_key: Vui lòng nhập Khóa truy cập Sophtron. + invalid_access_key: Khóa truy cập không hợp lệ. Vui lòng kiểm tra rằng bạn đã sao chép toàn bộ Khóa truy cập từ Sophtron. + access_key_compromised: Khóa truy cập có thể đã bị xâm phạm, hết hạn hoặc đã được sử dụng. Vui lòng tạo mới. + update_failed: "Cập nhật kết nối thất bại: %{message}" + unexpected: Đã xảy ra lỗi không mong muốn. Vui lòng thử lại hoặc liên hệ hỗ trợ. + edit: + user_id: + label: "ID người dùng Sophtron:" + placeholder: "Dán ID người dùng Sophtron của bạn vào đây..." + help_text: "ID người dùng phải là một chuỗi dài bắt đầu bằng chữ cái và số" + access_key: + label: "Khóa truy cập Sophtron:" + placeholder: "Dán Khóa truy cập Sophtron của bạn vào đây..." + help_text: "Khóa truy cập phải là một chuỗi dài bắt đầu bằng chữ cái và số" + index: + title: Kết nối Sophtron + loading: + loading_message: Đang tải tài khoản Sophtron... + loading_title: Đang tải + link_accounts: + all_already_linked: + one: "Tài khoản đã chọn (%{names}) đã được liên kết" + other: "Tất cả %{count} tài khoản đã chọn đã được liên kết: %{names}" + api_error: "Lỗi kết nối API" + invalid_account_names: + one: "Không thể liên kết tài khoản không có tên" + other: "Không thể liên kết %{count} tài khoản không có tên" + link_failed: Liên kết tài khoản thất bại + no_accounts_selected: Vui lòng chọn ít nhất một tài khoản + partial_invalid: "Đã liên kết thành công %{created_count} tài khoản, %{already_linked_count} đã được liên kết, %{invalid_count} tài khoản có tên không hợp lệ" + partial_success: "Đã liên kết thành công %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết: %{already_linked_names}" + success: + one: "Đã liên kết thành công %{count} tài khoản" + other: "Đã liên kết thành công %{count} tài khoản" + no_credentials_configured: "Vui lòng cấu hình ID người dùng và Khóa truy cập API Sophtron trước trong Cài đặt nhà cung cấp." + no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình khóa API. + no_access_key: Khóa truy cập Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_user_id: ID người dùng Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_institution_connected: Vui lòng kết nối tổ chức ngân hàng với Sophtron trước. + connect: + cancel: Hủy + captcha: Captcha + connect: Kết nối + institution_search_label: Tổ chức + institution_search_placeholder: Tìm theo tên ngân hàng + no_institutions: Không tìm thấy tổ chức phù hợp. + password: Mật khẩu + search: Tìm kiếm + search_too_short: Nhập ít nhất hai ký tự để tìm kiếm. + title: Kết nối tổ chức Sophtron + username: Tên đăng nhập + connect_institution: + api_error: "Kết nối Sophtron thất bại: %{message}" + missing_parameters: Chọn tổ chức và nhập thông tin đăng nhập ngân hàng của bạn. + connection_status: + api_error: "Lỗi kết nối API: %{message}" + attempt: "Lần thử %{attempt} trong %{max}" + check_again: Kiểm tra lại + failed: Sophtron không thể hoàn tất kết nối tổ chức này. + failed_timeout: Sophtron hết thời gian chờ trong khi tổ chức đang hoàn tất đăng nhập. + timeout: Sophtron không hoàn tất kết nối trong thời gian dự kiến. Bạn có thể kiểm tra lại hoặc thử kết nối lại sau. + title: Đang kết nối Sophtron + waiting: Sophtron vẫn đang kết nối với tổ chức của bạn. + mfa: + captcha: Văn bản Captcha + captcha_alt: Sophtron captcha + phone_confirmed: Tôi đã xác nhận qua điện thoại + submit: Gửi + title: Xác minh Sophtron + token: Mã xác minh + submit_mfa: + api_error: "Xác minh thất bại: %{message}" + invalid_security_answers: Câu trả lời bảo mật bị thiếu hoặc quá dài. + unknown_challenge: Bước xác minh Sophtron không xác định. + sophtron_item: + accounts_need_setup: Tài khoản cần thiết lập + automatic_sync: Sử dụng đồng bộ tự động + delete: Xóa kết nối + deletion_in_progress: đang xóa... + error: Lỗi + no_accounts_description: Kết nối này chưa có tài khoản nào được liên kết. + no_accounts_title: Không có tài khoản + manual_sync: Đồng bộ thủ công + manual_sync_action: Yêu cầu đồng bộ thủ công + manual_sync_action_for: "Yêu cầu đồng bộ thủ công cho %{institution}" + automatic_sync_for: "Sử dụng đồng bộ tự động cho %{institution}" + setup_action: Thiết lập tài khoản mới + setup_description: "%{linked} trong số %{total} tài khoản đã liên kết. Chọn loại tài khoản cho các tài khoản Sophtron mới được nhập." + setup_needed: Tài khoản mới sẵn sàng để thiết lập + status: "Đồng bộ %{timestamp} trước" + status_never: Chưa bao giờ đồng bộ + status_with_summary: "Đồng bộ lần cuối %{timestamp} trước • %{summary}" + sync_now: Đồng bộ ngay + syncing: Đang đồng bộ... + total: Tổng cộng + unlinked: Chưa liên kết + preload_accounts: + preload_accounts: tải trước tài khoản + api_error: "Lỗi kết nối API" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + no_credentials_configured: "Vui lòng cấu hình ID người dùng và Khóa truy cập API Sophtron trước trong Cài đặt nhà cung cấp." + no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình khóa API. + no_access_key: Khóa truy cập Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_user_id: ID người dùng Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + select_accounts: + accounts_selected: tài khoản đã chọn + api_error: "Lỗi kết nối API" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + cancel: Hủy + configure_name_in_sophtron: Không thể nhập - vui lòng cấu hình tên tài khoản trong Sophtron + description: Chọn tài khoản bạn muốn liên kết với tài khoản %{product_name} của bạn. + link_accounts: Liên kết tài khoản đã chọn + no_accounts_found: Không tìm thấy tài khoản. Vui lòng kiểm tra cấu hình khóa API. + no_access_key: Khóa truy cập Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_user_id: ID người dùng Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_credentials_configured: "Vui lòng cấu hình ID người dùng và Khóa truy cập API Sophtron trước trong Cài đặt nhà cung cấp." + no_institution_connected: Vui lòng kết nối tổ chức ngân hàng với Sophtron trước. + no_name_placeholder: "(Không có tên)" + title: Chọn tài khoản Sophtron + select_existing_account: + account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp + all_accounts_already_linked: Tất cả tài khoản Sophtron đã được liên kết + api_error: "Lỗi kết nối API" + cancel: Hủy + configure_name_in_sophtron: Không thể nhập - vui lòng cấu hình tên tài khoản trong Sophtron + description: Chọn tài khoản Sophtron để liên kết với tài khoản này. Giao dịch sẽ được đồng bộ và loại bỏ trùng lặp tự động. + link_account: Liên kết tài khoản + no_account_specified: Chưa chỉ định tài khoản + no_accounts_found: Không tìm thấy tài khoản Sophtron. Vui lòng kiểm tra cấu hình khóa API. + no_access_key: Khóa truy cập Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_user_id: ID người dùng Sophtron chưa được cấu hình. Vui lòng cấu hình trong Cài đặt. + no_institution_connected: Vui lòng kết nối tổ chức ngân hàng với Sophtron trước. + no_name_placeholder: "(Không có tên)" + title: "Liên kết %{account_name} với Sophtron" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + link_existing_account: + account_already_linked: Tài khoản này đã được liên kết với nhà cung cấp + api_error: "Lỗi kết nối API" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + invalid_account_name: Không thể liên kết tài khoản không có tên + sophtron_account_already_linked: Tài khoản Sophtron này đã được liên kết với tài khoản khác + sophtron_account_not_found: Không tìm thấy tài khoản Sophtron + missing_parameters: Thiếu tham số bắt buộc + no_institution_connected: Vui lòng kết nối tổ chức ngân hàng với Sophtron trước. + success: "Đã liên kết thành công %{account_name} với Sophtron" + setup_accounts: + account_type_label: "Loại tài khoản:" + all_accounts_linked: "Tất cả tài khoản Sophtron của bạn đã được thiết lập." + api_error: "Lỗi kết nối API" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + fetch_failed: "Tải tài khoản thất bại" + no_accounts_to_setup: "Không có tài khoản nào để thiết lập" + no_access_key: "Khóa truy cập Sophtron chưa được cấu hình. Vui lòng kiểm tra cài đặt kết nối." + no_user_id: "ID người dùng Sophtron chưa được cấu hình. Vui lòng kiểm tra cài đặt kết nối." + no_institution_connected: "Tổ chức Sophtron chưa được kết nối." + account_types: + skip: Bỏ qua tài khoản này + depository: Tài khoản thanh toán hoặc tiết kiệm + credit_card: Thẻ tín dụng + investment: Tài khoản đầu tư + loan: Vay hoặc thế chấp + other_asset: Tài sản khác + subtype_labels: + depository: "Loại phụ tài khoản:" + credit_card: "" + investment: "Loại đầu tư:" + loan: "Loại vay:" + other_asset: "" + subtype_messages: + credit_card: "Thẻ tín dụng sẽ được tự động thiết lập dưới dạng tài khoản thẻ tín dụng." + other_asset: "Không cần tùy chọn bổ sung cho Tài sản khác." + balance: Số dư + cancel: Hủy + choose_account_type: "Chọn đúng loại tài khoản cho mỗi tài khoản Sophtron:" + create_accounts: Tạo tài khoản + creating_accounts: Đang tạo tài khoản... + historical_data_range: "Phạm vi dữ liệu lịch sử:" + subtitle: Chọn đúng loại tài khoản cho các tài khoản đã nhập + sync_start_date_help: Chọn khoảng thời gian lịch sử giao dịch bạn muốn đồng bộ. Tối đa 3 năm lịch sử có sẵn. + sync_start_date_label: "Bắt đầu đồng bộ giao dịch từ:" + title: Thiết lập tài khoản Sophtron của bạn + complete_account_setup: + all_skipped: "Tất cả tài khoản đã bị bỏ qua. Không có tài khoản nào được tạo." + creation_failed: "Tạo tài khoản thất bại" + api_error: "Lỗi kết nối API" + unexpected_error: "Đã xảy ra lỗi không mong muốn" + no_accounts: "Không có tài khoản nào để thiết lập." + success: "Đã tạo thành công %{count} tài khoản." + sync: + already_running: Đồng bộ thủ công Sophtron đang chạy. + api_error: "Đồng bộ thủ công Sophtron thất bại: %{message}" + failed: Đồng bộ thủ công Sophtron thất bại + no_linked_accounts: Tổ chức Sophtron này không có tài khoản nào được liên kết để đồng bộ. + processing_failed: Đồng bộ thủ công Sophtron không thể xử lý các giao dịch đã làm mới. + success: Đã bắt đầu đồng bộ + toggle_manual_sync: + success_disabled: Tổ chức Sophtron sẽ đồng bộ tự động. + success_enabled: Tổ chức Sophtron hiện yêu cầu đồng bộ thủ công. + manual_sync_complete: + close: Đóng + description: Số dư tài khoản sẽ được cập nhật xong trong nền. + message: Các giao dịch đã được tải xuống sau khi xác minh Sophtron. + title: Đã bắt đầu đồng bộ Sophtron + sophtron_setup_required: + title: Cần thiết lập Sophtron + message: > + Để hoàn tất thiết lập kết nối Sophtron của bạn, vui lòng đến trang Cài đặt nhà cung cấp và làm theo hướng dẫn để ủy quyền và cấu hình kết nối Sophtron. + go_to_provider_settings: Đi đến Cài đặt nhà cung cấp + heading: "ID người dùng và Khóa truy cập chưa được cấu hình" + description: "Trước khi liên kết tài khoản Sophtron, bạn cần cấu hình ID người dùng và Khóa truy cập Sophtron." + setup_steps_title: "Các bước thiết lập:" + step_1_html: "Đi đến Cài đặt → Nhà cung cấp đồng bộ ngân hàng" + step_2_html: "Tìm phần Sophtron" + step_3_html: "Nhập ID người dùng và Khóa truy cập Sophtron của bạn" + step_4: "Quay lại đây để liên kết tài khoản" + api_error: + title: "Lỗi kết nối Sophtron" + unable_to_connect: "Không thể kết nối với Sophtron" + institution_unable_to_connect: "Không thể kết nối với tổ chức" + common_issues_title: "Các vấn đề thường gặp:" + incorrect_user_id: "ID người dùng không chính xác: Xác minh ID người dùng trong Cài đặt nhà cung cấp" + invalid_access_key: "Khóa truy cập không hợp lệ: Kiểm tra Khóa truy cập trong Cài đặt nhà cung cấp" + expired_credentials: "Thông tin xác thực hết hạn: Tạo ID người dùng và Khóa truy cập mới từ Sophtron" + network_issue: "Sự cố mạng: Kiểm tra kết nối internet của bạn" + service_down: "Dịch vụ gián đoạn: API Sophtron có thể tạm thời không khả dụng" + bad_credentials: "Thông tin ngân hàng: Kiểm tra tên đăng nhập và mật khẩu có chính xác không" + verification_code: "Mã xác minh: Đảm bảo đã nhập mã mới nhất trước khi hết hạn" + institution_timeout: "Hết thời gian tổ chức: Trang đăng nhập ngân hàng không hoàn tất kịp thời" + unsupported_mfa: "Hỗ trợ MFA: Sophtron có thể không hỗ trợ quy trình xác minh hiện tại của tổ chức này" + check_provider_settings: "Kiểm tra Cài đặt nhà cung cấp" + try_again: "Thử kết nối lại" + select_option: "Chọn %{type}" + subtype: "loại phụ" + type: "loại" + sophtron_panel: + setup_instructions_title: "Hướng dẫn thiết lập:" + setup_instructions: + step_1_html: 'Truy cập Sophtron để lấy thông tin xác thực API' + step_2: "Sao chép ID người dùng và Khóa truy cập từ cài đặt tài khoản Sophtron" + step_3: "Dán thông tin xác thực bên dưới và nhấn Lưu; Sure sẽ tự động tạo hoặc tái sử dụng ID khách hàng Sophtron" + field_descriptions_title: "Mô tả các trường:" + field_descriptions: + user_id_html: "ID người dùng: Thông tin xác thực ID người dùng Sophtron của bạn" + access_key_html: "Khóa truy cập: Thông tin xác thực Khóa truy cập Sophtron của bạn" + base_url_html: "URL cơ sở: URL endpoint API Sophtron, thường là https://api.sophtron.com/api" + fields: + user_id: + label: "ID người dùng" + placeholder_new: "Dán ID người dùng Sophtron của bạn" + placeholder_edit: "••••••••" + access_key: + label: "Khóa truy cập" + placeholder_new: "Dán Khóa truy cập Sophtron của bạn" + placeholder_edit: "••••••••" + base_url: + label: "URL cơ sở" + placeholder: "https://api.sophtron.com/api" + save: "Lưu cấu hình" + update: "Cập nhật cấu hình" + syncer: + manual_sync_required: "Cần đồng bộ thủ công Sophtron cho tổ chức này; bỏ qua các tài khoản đó trong quá trình đồng bộ tự động." + importing_accounts: "Đang nhập tài khoản từ Sophtron..." + checking_account_configuration: "Đang kiểm tra cấu hình tài khoản..." + accounts_need_setup: "%{count} tài khoản cần thiết lập" + processing_transactions: "Đang xử lý giao dịch cho các tài khoản đã liên kết..." + calculating_balances: "Đang tính toán số dư cho các tài khoản đã liên kết..." + sophtron_entry: + processor: + unknown_transaction: "Giao dịch không xác định" + render_connection_timeout: + timeout: "Kết nối hết thời gian chờ. Vui lòng thử lại." + redirect_after_account_link: + invalid_account_names: + one: "Không thể liên kết %{count} tài khoản không có tên" + other: "Không thể liên kết %{count} tài khoản không có tên" + partial_invalid: "Đã liên kết %{created_count} tài khoản. %{already_linked_count} đã liên kết, %{invalid_count} có tên không hợp lệ." + partial_success: "Đã liên kết %{created_count} tài khoản. %{already_linked_count} tài khoản đã được liên kết." + success: + one: "Đã liên kết thành công %{count} tài khoản." + other: "Đã liên kết thành công %{count} tài khoản." + all_already_linked: + one: "Tài khoản đã chọn đã được liên kết" + other: "Tất cả %{count} tài khoản đã chọn đã được liên kết" + link_failed: "Liên kết tài khoản thất bại" + start_manual_sync: + already_running: "Đồng bộ đang chạy." + no_linked_accounts: "Không có tài khoản nào được liên kết để đồng bộ." + api_error: "Lỗi API: %{message}" + start_manual_sync_for_account: + failed: "Đồng bộ tài khoản thất bại" diff --git a/config/locales/views/sophtron_items/zh-CN.yml b/config/locales/views/sophtron_items/zh-CN.yml new file mode 100644 index 000000000..51e6dcc6e --- /dev/null +++ b/config/locales/views/sophtron_items/zh-CN.yml @@ -0,0 +1,313 @@ +--- +zh-CN: + sophtron_items: + defaults: + name: Sophtron 连接 + new: + title: 连接 Sophtron + user_id: User ID + user_id_placeholder: 粘贴您的 Sophtron User ID + access_key: Access Key + access_key_placeholder: 粘贴您的 Sophtron Access Key + connect: 连接 + cancel: 取消 + create: + success: Sophtron 连接创建成功 + destroy: + success: Sophtron 连接已移除 + update: + success: Sophtron 连接更新成功!您的账户正在重新连接。 + errors: + blank_user_id: 请输入 Sophtron User ID。 + invalid_user_id: 无效的 User ID。请检查您是否从 Sophtron 复制了完整的 User ID。 + user_id_compromised: 该 User ID 可能已泄露、过期或已使用。请创建一个新的。 + blank_access_key: 请输入 Sophtron Access Key。 + invalid_access_key: 无效的 Access Key。请检查您是否从 Sophtron 复制了完整的 Access Key。 + access_key_compromised: 该 Access Key 可能已泄露、过期或已使用。请创建一个新的。 + update_failed: "更新连接失败:%{message}" + unexpected: 发生意外错误。请重试或联系支持。 + edit: + user_id: + label: "Sophtron User ID:" + placeholder: "请在此粘贴您的 Sophtron User ID..." + help_text: "User ID 应该是以字母和数字开头的长字符串" + access_key: + label: "Sophtron Access Key:" + placeholder: "请在此粘贴您的 Sophtron Access Key..." + help_text: "Access Key 应该是以字母和数字开头的长字符串" + index: + title: Sophtron 连接 + loading: + loading_message: 正在加载 Sophtron 账户... + loading_title: 正在加载 + link_accounts: + all_already_linked: + one: 选定账户(%{names})已关联 + other: 所有 %{count} 个选定账户都已关联:%{names} + api_error: "API 连接错误" + invalid_account_names: + one: 无法关联空名称账户 + other: 无法关联 %{count} 个空名称账户 + link_failed: 关联账户失败 + no_accounts_selected: 请至少选择一个账户 + partial_invalid: "成功关联 %{created_count} 个账户,%{already_linked_count} 个已关联,%{invalid_count} 个账户名称无效" + partial_success: "成功关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联:%{already_linked_names}" + success: + one: "成功关联 %{count} 个账户" + other: "成功关联 %{count} 个账户" + no_credentials_configured: "请先在提供商设置中配置您的 Sophtron API User ID 和 Access Key。" + no_accounts_found: 未找到账户。请检查您的 Access Key 配置。 + no_access_key: Sophtron Access Key 未配置。请在设置中配置。 + no_user_id: Sophtron User ID 未配置。请在设置中配置。 + no_institution_connected: 请先使用 Sophtron 连接一个银行机构。 + connect: + cancel: 取消 + captcha: 验证码 + connect: 连接 + institution_search_label: 机构 + institution_search_placeholder: 按银行名称搜索 + no_institutions: 未找到匹配的机构。 + password: 密码 + search: 搜索 + search_too_short: 请至少输入两个字符进行搜索。 + title: 连接 Sophtron 机构 + username: 用户名 + connect_institution: + api_error: "Sophtron 连接失败:%{message}" + missing_parameters: 请选择一个机构并输入您的银行登录凭据。 + connection_status: + api_error: "API 连接错误:%{message}" + attempt: 第 %{attempt} 次,共 %{max} 次 + check_again: 再次检查 + failed: Sophtron 无法完成此机构连接。 + failed_timeout: 在机构完成登录时,Sophtron 超时。 + timeout: Sophtron 未能在预期时间内完成连接。您可以再次检查,或稍后重连。 + title: 正在连接 Sophtron + waiting: Sophtron 仍在连接您的机构。 + mfa: + captcha: 验证码文本 + captcha_alt: Sophtron 验证码 + phone_confirmed: 我已通过电话确认 + submit: 提交 + title: Sophtron 验证 + token: 验证码 + submit_mfa: + api_error: "验证失败:%{message}" + invalid_security_answers: 安全问题答案缺失或过长。 + unknown_challenge: 未知的 Sophtron 验证步骤。 + sophtron_item: + accounts_need_setup: 账户需要设置 + automatic_sync: 使用自动同步 + delete: 删除连接 + deletion_in_progress: 正在删除... + error: 错误 + no_accounts_description: 此连接尚未关联任何账户。 + no_accounts_title: 无账户 + manual_sync: 手动同步 + manual_sync_action: 需要手动同步 + manual_sync_action_for: "为 %{institution} 需要手动同步" + automatic_sync_for: "为 %{institution} 使用自动同步" + setup_action: 设置新账户 + setup_description: 已关联 %{linked} / %{total} 个账户。请选择新导入的 Sophtron 账户类型。 + setup_needed: 新账户已准备好设置 + status: "%{timestamp} 前已同步" + status_never: 从未同步 + status_with_summary: "%{timestamp} 前上次同步 • %{summary}" + sync_now: 立即同步 + syncing: 正在同步... + total: 总计 + unlinked: 未关联 + preload_accounts: + preload_accounts: 预加载账户 + api_error: "API 连接错误" + unexpected_error: "发生意外错误" + no_credentials_configured: "请先在提供商设置中配置您的 Sophtron API User ID 和 Access Key。" + no_accounts_found: 未找到账户。请检查您的 Access Key 配置。 + no_access_key: Sophtron Access Key 未配置。请在设置中配置。 + no_user_id: Sophtron User ID 未配置。请在设置中配置。 + select_accounts: + accounts_selected: 已选择账户 + api_error: "API 连接错误" + unexpected_error: "发生意外错误" + cancel: 取消 + configure_name_in_sophtron: 无法导入 - 请先在 Sophtron 中配置账户名称 + description: 选择您想链接到 %{product_name} 账户的账户。 + link_accounts: 链接所选账户 + no_accounts_found: 未找到账户。请检查您的 Access Key 配置。 + no_access_key: Sophtron Access Key 未配置。请在设置中配置。 + no_user_id: Sophtron User ID 未配置。请在设置中配置。 + no_credentials_configured: "请先在提供商设置中配置您的 Sophtron API User ID 和 Access Key。" + no_institution_connected: 请先使用 Sophtron 连接一个银行机构。 + no_name_placeholder: (无名称) + title: 选择 Sophtron 账户 + select_existing_account: + account_already_linked: 此账户已关联到某个提供商 + all_accounts_already_linked: 所有 Sophtron 账户都已关联 + api_error: "API 连接错误" + cancel: 取消 + configure_name_in_sophtron: 无法导入 - 请先在 Sophtron 中配置账户名称 + description: 选择一个 Sophtron 账户与此账户关联。交易将自动同步并去重。 + link_account: 关联账户 + no_account_specified: 未指定账户 + no_accounts_found: 未找到 Sophtron 账户。请检查您的 Access Key 配置。 + no_access_key: Sophtron Access Key 未配置。请在设置中配置。 + no_user_id: Sophtron User ID 未配置。请在设置中配置。 + no_institution_connected: 请先使用 Sophtron 连接一个银行机构。 + no_name_placeholder: (无名称) + title: 将 %{account_name} 与 Sophtron 关联 + unexpected_error: "发生意外错误" + link_existing_account: + account_already_linked: 此账户已关联到某个提供商 + api_error: "API 连接错误" + unexpected_error: "发生意外错误" + invalid_account_name: 无法关联空名称账户 + sophtron_account_already_linked: 此 Sophtron 账户已关联到另一个账户 + sophtron_account_not_found: 未找到 Sophtron 账户 + missing_parameters: 缺少必需参数 + no_institution_connected: 请先使用 Sophtron 连接一个银行机构。 + success: "已成功将 %{account_name} 与 Sophtron 关联" + setup_accounts: + account_type_label: "账户类型:" + all_accounts_linked: "您的所有 Sophtron 账户都已完成设置。" + api_error: "API 连接错误" + unexpected_error: "发生意外错误" + fetch_failed: "获取账户失败" + no_accounts_to_setup: "没有需要设置的账户" + no_access_key: "Sophtron Access Key 未配置。请检查您的连接设置。" + no_user_id: "Sophtron User ID 未配置。请检查您的连接设置。" + no_institution_connected: "Sophtron 机构尚未连接。" + account_types: + skip: 跳过此账户 + depository: 支票账户或储蓄账户 + credit_card: 信用卡 + investment: 投资账户 + loan: 贷款或抵押贷款 + other_asset: 其他资产 + subtype_labels: + depository: "账户子类型:" + credit_card: "" + investment: "投资类型:" + loan: "贷款类型:" + other_asset: "" + subtype_messages: + credit_card: "信用卡将自动设为信用卡账户。" + other_asset: "其他资产无需额外选项。" + balance: 余额 + cancel: 取消 + choose_account_type: "为每个 Sophtron 账户选择正确的账户类型:" + create_accounts: 创建账户 + creating_accounts: 正在创建账户... + historical_data_range: "历史数据范围:" + subtitle: 为您导入的账户选择正确的账户类型 + sync_start_date_help: 选择您希望向前同步交易历史的时间范围。最多可用 3 年历史记录。 + sync_start_date_label: "从以下日期开始同步交易:" + title: 设置您的 Sophtron 账户 + complete_account_setup: + all_skipped: "所有账户都已跳过,未创建任何账户。" + creation_failed: "创建账户失败" + api_error: "API 连接错误" + unexpected_error: "发生意外错误" + no_accounts: "没有需要设置的账户。" + success: "成功创建 %{count} 个账户。" + sync: + already_running: Sophtron 手动同步正在进行中。 + api_error: "Sophtron 手动同步失败:%{message}" + failed: Sophtron 手动同步失败 + no_linked_accounts: 此 Sophtron 机构没有任何已关联账户可同步。 + processing_failed: Sophtron 手动同步无法处理刷新后的交易。 + success: 已开始同步 + toggle_manual_sync: + success_disabled: Sophtron 机构将自动同步。 + success_enabled: Sophtron 机构现在需要手动同步。 + manual_sync_complete: + close: 关闭 + description: 账户余额会在后台继续更新完成。 + message: 交易已在 Sophtron 验证后下载。 + title: Sophtron 同步已开始 + sophtron_setup_required: + title: 需要设置 Sophtron + message: > + 要完成 Sophtron 连接设置,请前往提供商设置页面,并按照说明授权和配置您的 Sophtron 连接。 + go_to_provider_settings: 前往提供商设置 + heading: "未配置 User ID 和 Access Key" + description: "在链接 Sophtron 账户之前,您需要先配置 Sophtron User ID 和 Access Key。" + setup_steps_title: "设置步骤:" + step_1_html: "前往 设置 → 银行同步提供商" + step_2_html: "找到 Sophtron 区块" + step_3_html: "输入您的 Sophtron User ID 和 Access Key" + step_4: "返回这里链接您的账户" + api_error: + title: "Sophtron 连接错误" + unable_to_connect: "无法连接到 Sophtron" + institution_unable_to_connect: "无法连接到该机构" + common_issues_title: "常见问题:" + incorrect_user_id: "User ID 错误:请在提供商设置中核对您的 User ID" + invalid_access_key: "Access Key 无效:请在提供商设置中检查您的 Access Key" + expired_credentials: "凭据已过期:请从 Sophtron 生成新的 User ID 和 Access Key" + network_issue: "网络问题:请检查您的互联网连接" + service_down: "服务不可用:Sophtron API 可能暂时不可用" + bad_credentials: "银行登录凭据:请检查用户名和密码是否正确" + verification_code: "验证码:请确保在过期前输入了最新验证码" + institution_timeout: "机构超时:银行登录页未能及时完成" + unsupported_mfa: "MFA 支持:Sophtron 可能不支持该机构当前的验证流程" + check_provider_settings: "检查提供商设置" + try_again: "请再次尝试连接" + select_option: "选择 %{type}" + subtype: "子类型" + type: "类型" + sophtron_panel: + setup_instructions_title: "设置说明:" + setup_instructions: + step_1_html: '访问 Sophtron 获取您的 API 凭据' + step_2: "从 Sophtron 账户设置中复制您的 User ID 和 Access Key" + step_3: "将凭据粘贴到下方并点击保存;Sure 会自动创建或复用您的 Sophtron Customer ID" + field_descriptions_title: "字段说明:" + field_descriptions: + user_id_html: "User ID:您的 Sophtron User ID 凭据" + access_key_html: "Access Key:您的 Sophtron Access Key 凭据" + base_url_html: "Base URL:Sophtron API 端点 URL,通常为 https://api.sophtron.com/api" + fields: + user_id: + label: "User ID" + placeholder_new: "粘贴您的 Sophtron User ID" + placeholder_edit: "••••••••" + access_key: + label: "Access Key" + placeholder_new: "粘贴您的 Sophtron Access Key" + placeholder_edit: "••••••••" + base_url: + label: "Base URL" + placeholder: "https://api.sophtron.com/api" + save: "保存配置" + update: "更新配置" + syncer: + manual_sync_required: "此机构需要手动进行 Sophtron 同步;自动同步时将跳过这些账户。" + importing_accounts: "正在从 Sophtron 导入账户..." + checking_account_configuration: "正在检查账户配置..." + accounts_need_setup: "%{count} 个账户需要设置" + processing_transactions: "正在处理已关联账户的交易..." + calculating_balances: "正在计算已关联账户的余额..." + sophtron_entry: + processor: + unknown_transaction: "未知交易" + render_connection_timeout: + timeout: "连接超时。请重试。" + redirect_after_account_link: + invalid_account_names: + one: "无法关联 %{count} 个空名称账户" + other: "无法关联 %{count} 个空名称账户" + partial_invalid: "已关联 %{created_count} 个账户。%{already_linked_count} 个已关联,%{invalid_count} 个名称无效。" + partial_success: "已关联 %{created_count} 个账户。%{already_linked_count} 个账户已关联。" + success: + one: "成功关联 %{count} 个账户。" + other: "成功关联 %{count} 个账户。" + all_already_linked: + one: "所选账户已关联" + other: "所有 %{count} 个所选账户都已关联" + link_failed: "关联账户失败" + start_manual_sync: + already_running: "同步已在进行中。" + no_linked_accounts: "没有可同步的已关联账户。" + api_error: "API 错误:%{message}" + start_manual_sync_for_account: + failed: "同步账户失败" diff --git a/config/locales/views/splits/vi.yml b/config/locales/views/splits/vi.yml new file mode 100644 index 000000000..cbd5fad7b --- /dev/null +++ b/config/locales/views/splits/vi.yml @@ -0,0 +1,47 @@ +--- +vi: + splits: + new: + title: Chia nhỏ giao dịch + description: Chia giao dịch này thành nhiều mục với các danh mục và số tiền khác nhau. + submit: Chia nhỏ giao dịch + cancel: Hủy + add_row: Thêm phần chia + remove_row: Xóa + remaining: Còn lại + amounts_must_match: Tổng số tiền chia phải bằng số tiền giao dịch ban đầu. + name_label: Tên + name_placeholder: Tên phần chia + amount_label: Số tiền + category_label: Danh mục + uncategorized: "(chưa phân loại)" + original_name: "Tên:" + original_date: "Ngày:" + original_amount: "Số tiền" + split_number: "Phần #%{number}" + create: + success: Giao dịch đã được chia nhỏ thành công + not_splittable: Giao dịch này không thể chia nhỏ. + destroy: + success: Giao dịch đã được khôi phục thành công + show: + title: Các mục đã chia + description: Giao dịch này đã được chia thành các mục sau. + button_title: Chia nhỏ giao dịch + button_description: Chia giao dịch này thành nhiều mục với các danh mục và số tiền khác nhau. + button: Chia nhỏ + unsplit_title: Hủy chia nhỏ giao dịch + unsplit_button: Hủy chia nhỏ + unsplit_confirm: Thao tác này sẽ xóa tất cả các mục đã chia và khôi phục giao dịch ban đầu. + edit: + title: Chỉnh sửa phần chia + description: Chỉnh sửa các mục đã chia cho giao dịch này. + submit: Cập nhật phần chia + not_split: Giao dịch này chưa được chia nhỏ. + update: + success: Phần chia đã được cập nhật thành công + child: + title: Một phần của giao dịch chia nhỏ + description: Mục này là một phần của giao dịch đã chia nhỏ. + edit_split: Chỉnh sửa phần chia + unsplit: Hủy chia nhỏ diff --git a/config/locales/views/splits/zh-CN.yml b/config/locales/views/splits/zh-CN.yml new file mode 100644 index 000000000..a42d4063a --- /dev/null +++ b/config/locales/views/splits/zh-CN.yml @@ -0,0 +1,47 @@ +--- +zh-CN: + splits: + new: + title: 拆分交易 + description: 将此交易拆分成多个条目,并分别使用不同的分类和金额。 + submit: 拆分交易 + cancel: 取消 + add_row: 添加拆分 + remove_row: 移除 + remaining: 剩余 + amounts_must_match: 拆分金额必须等于原始交易金额。 + name_label: 名称 + name_placeholder: 拆分名称 + amount_label: 金额 + category_label: 分类 + uncategorized: (未分类) + original_name: "名称:" + original_date: "日期:" + original_amount: "金额" + split_number: "拆分 #%{number}" + create: + success: 交易拆分成功 + not_splittable: 此交易无法拆分。 + destroy: + success: 交易取消拆分成功 + show: + title: 拆分条目 + description: 这笔交易已拆分为以下条目。 + button_title: 拆分交易 + button_description: 将此交易拆分成多个条目,并分别使用不同的分类和金额。 + button: 拆分 + unsplit_title: 取消拆分交易 + unsplit_button: 取消拆分 + unsplit_confirm: 这将删除所有拆分条目并恢复原始交易。 + edit: + title: 编辑拆分 + description: 修改此交易的拆分条目。 + submit: 更新拆分 + not_split: 此交易未被拆分。 + update: + success: 拆分更新成功 + child: + title: 拆分的一部分 + description: 此条目属于一个拆分交易。 + edit_split: 编辑拆分 + unsplit: 取消拆分 diff --git a/config/locales/views/subscriptions/vi.yml b/config/locales/views/subscriptions/vi.yml new file mode 100644 index 000000000..06c136c0f --- /dev/null +++ b/config/locales/views/subscriptions/vi.yml @@ -0,0 +1,24 @@ +--- +vi: + subscriptions: + self_hosted_alert: "%{product_name} không khả dụng ở chế độ tự lưu trữ." + upgrade: + already_contributing: Bạn đã đang đóng góp. Cảm ơn bạn! + page_title: "Nâng cấp" + account_settings: "Cài đặt tài khoản" + sign_out: "Đăng xuất" + contribute_and_support_sure: "Đóng góp và hỗ trợ Sure" + cta: "Tiếp tục hỗ trợ sự phát triển của codebase này!" + header: + support: "Hỗ trợ" + sure: "Sure" + today: "hôm nay" + redirect_to_stripe: "Ở bước tiếp theo, bạn sẽ được chuyển hướng đến Stripe để xử lý thẻ tín dụng." + trialing: "Dữ liệu của bạn sẽ bị xóa trong %{days} ngày" + trial_over: "Thời gian dùng thử của bạn đã kết thúc" + create: + welcome: "Chào mừng đến với Sure!" + trial_already_used: "Bạn đã bắt đầu hoặc hoàn thành một lần dùng thử. Vui lòng nâng cấp để tiếp tục." + success: + welcome_with_contribution: "Chào mừng đến với Sure! Sự đóng góp của bạn được đánh giá cao." + contribution_failed: "Đã xảy ra sự cố khi xử lý đóng góp của bạn. Vui lòng thử lại." diff --git a/config/locales/views/tag/deletions/vi.yml b/config/locales/views/tag/deletions/vi.yml new file mode 100644 index 000000000..0a63ca01e --- /dev/null +++ b/config/locales/views/tag/deletions/vi.yml @@ -0,0 +1,14 @@ +--- +vi: + tag: + deletions: + create: + deleted: Nhãn đã được xóa + new: + delete_and_leave_uncategorized: Xóa "%{tag_name}" + delete_and_recategorize: Xóa "%{tag_name}" và gán nhãn mới + delete_and_reassign: Xóa và gán lại + delete_tag: Xóa nhãn? + explanation: "%{tag_name} sẽ bị xóa khỏi các giao dịch và các thực thể có thể gắn nhãn khác. Thay vì để chúng không có nhãn, bạn cũng có thể gán một nhãn mới bên dưới." + replacement_tag_prompt: Chọn nhãn + tag: Nhãn diff --git a/config/locales/views/tags/vi.yml b/config/locales/views/tags/vi.yml new file mode 100644 index 000000000..5c4952c48 --- /dev/null +++ b/config/locales/views/tags/vi.yml @@ -0,0 +1,26 @@ +--- +vi: + tags: + create: + created: Nhãn đã được tạo + error: 'Lỗi khi tạo nhãn: %{error}' + destroy: + deleted: Nhãn đã được xóa + destroy_all: + all_deleted: Tất cả nhãn đã được xóa + edit: + edit: Chỉnh sửa nhãn + form: + placeholder: Tên nhãn + index: + empty: Chưa có nhãn nào + new: Nhãn mới + tags: Nhãn + delete_all: Xóa tất cả + new: + new: Nhãn mới + tag: + delete: Xóa + edit: Chỉnh sửa + update: + updated: Nhãn đã được cập nhật diff --git a/config/locales/views/trades/vi.yml b/config/locales/views/trades/vi.yml new file mode 100644 index 000000000..3bd370c16 --- /dev/null +++ b/config/locales/views/trades/vi.yml @@ -0,0 +1,57 @@ +--- +vi: + trades: + form: + account: Tài khoản chuyển (tùy chọn) + account_prompt: Tìm kiếm tài khoản + amount: Số tiền + fee: Phí giao dịch + holding: Mã ticker + holding_optional: Mã ticker (tùy chọn) + price: Giá mỗi cổ phiếu + qty: Số lượng + submit: Thêm giao dịch + ticker_placeholder: AAPL + type: Loại + type_buy: Mua + type_sell: Bán + type_deposit: Nạp tiền + type_withdrawal: Rút tiền + type_dividend: Cổ tức + type_interest: Lãi + dividend_requires_security: Cổ tức yêu cầu phải có chứng khoán + header: + buy: Mua + sell: Bán + dividend: Cổ tức + interest: Lãi + current_market_price_label: Giá thị trường hiện tại + overview: Tổng quan + purchase_price_label: Giá mua + purchase_qty_label: Số lượng mua + symbol_label: Mã chứng khoán + total_return_label: Lãi/lỗ chưa thực hiện + new: + title: Giao dịch mới + show: + additional: Bổ sung + amount_label: Số tiền + buy: Mua + category_label: Danh mục + cost_per_share_label: Giá mỗi cổ phiếu + date_label: Ngày + delete: Xóa + fee_label: Phí giao dịch + delete_subtitle: Hành động này không thể hoàn tác + delete_title: Xóa giao dịch chứng khoán + details: Chi tiết + provider_disabled_warning: "Cập nhật giá tạm dừng — nhà cung cấp %{provider} đã bị tắt. Bật lại trong Cài đặt hoặc ánh xạ lại danh mục nắm giữ sang nhà cung cấp khác." + exclude_subtitle: Giao dịch này sẽ không được đưa vào báo cáo và tính toán + exclude_title: Loại trừ khỏi phân tích + no_category: Không có danh mục + note_label: Ghi chú + note_placeholder: Thêm ghi chú bổ sung tại đây... + quantity_label: Số lượng + sell: Bán + settings: Cài đặt + type_label: Loại diff --git a/config/locales/views/trades/zh-CN.yml b/config/locales/views/trades/zh-CN.yml index c735daadd..44570f535 100644 --- a/config/locales/views/trades/zh-CN.yml +++ b/config/locales/views/trades/zh-CN.yml @@ -5,34 +5,53 @@ zh-CN: account: 转账账户(可选) account_prompt: 搜索账户 amount: 金额 - holding: 股票代码 + fee: 交易手续费 + holding: 代码 + holding_optional: 代码(可选) price: 每股价格 qty: 数量 submit: 添加交易 ticker_placeholder: AAPL - type: 交易类型 + type: 类型 + type_buy: 买入 + type_sell: 卖出 + type_deposit: 存入 + type_withdrawal: 提现 + type_dividend: 分红 + type_interest: 利息 + dividend_requires_security: 分红需要证券 header: buy: 买入 - current_market_price_label: 当前市价 + sell: 卖出 + dividend: 分红 + interest: 利息 + current_market_price_label: 当前市场价 overview: 概览 purchase_price_label: 买入价格 purchase_qty_label: 买入数量 - sell: 卖出 - symbol_label: 股票代码 + symbol_label: 代码 total_return_label: 未实现盈亏 new: title: 新建交易 show: - additional: 附加信息 + additional: 其他 + amount_label: 金额 + buy: 买入 + category_label: 分类 cost_per_share_label: 每股成本 date_label: 日期 delete: 删除 - delete_subtitle: 此操作不可撤销 + fee_label: 交易手续费 + delete_subtitle: 此操作无法撤销 delete_title: 删除交易 details: 详情 - exclude_subtitle: 此交易将不计入报表和计算 + provider_disabled_warning: 价格更新已暂停——%{provider} 提供商已禁用。请在设置中重新启用,或将持仓重新映射到其他提供商。 + exclude_subtitle: 此交易不会计入报表和计算 exclude_title: 从分析中排除 + no_category: 无分类 note_label: 备注 - note_placeholder: 在此添加额外备注... + note_placeholder: 在此添加其他备注... quantity_label: 数量 + sell: 卖出 settings: 设置 + type_label: 类型 diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index d051ef130..9e45f810f 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -278,9 +278,17 @@ en: unexpected_error: "Unexpected error during conversion: %{error}" searches: filters: + account_filter: + filter_accounts: Filter accounts + category_filter: + filter_category: Filter category date_filter: start_date: "Start date" end_date: "End date" + merchant_filter: + filter_merchants: Filter merchants + tag_filter: + filter_tags: Filter tags amount_filter: equal_to: Equal to greater_than: Greater than diff --git a/config/locales/views/transactions/vi.yml b/config/locales/views/transactions/vi.yml new file mode 100644 index 000000000..0be994d36 --- /dev/null +++ b/config/locales/views/transactions/vi.yml @@ -0,0 +1,340 @@ +--- +vi: + transactions: + bulk_updates: + new: + cancel: Hủy + category_label: Danh mục + category_prompt: Chọn danh mục + date_label: Ngày + header_title: Chỉnh sửa giao dịch + merchant_label: Nhà cung cấp + merchant_prompt: Chọn nhà cung cấp + name_label: Tên + name_placeholder: Nhập tên sẽ được áp dụng cho các giao dịch đã chọn + none: "(không có)" + notes_label: Ghi chú + notes_placeholder: Nhập ghi chú sẽ được áp dụng cho các giao dịch đã chọn + overview: Tổng quan + save: Lưu + tags_label: Nhãn + transactions_section: Giao dịch + unknown_name: Giao dịch không xác định + selection_bar: + duplicate: Nhân đôi + edit: Chỉnh sửa + selected: đã chọn + form: + details: Chi tiết + account: Tài khoản + account_prompt: Chọn tài khoản + amount: Số tiền + category: Danh mục + category_label: Danh mục + category_prompt: Chọn danh mục + date: Ngày + description: Mô tả + description_placeholder: Mô tả giao dịch + expense: Chi tiêu + income: Thu nhập + merchant_label: Nhà cung cấp + none: (không có) + note_label: Ghi chú + note_placeholder: Nhập ghi chú + submit: Thêm giao dịch + tags_label: Nhãn + transfer: Chuyển khoản + create: + created: Đã tạo giao dịch + update: + updated: Đã cập nhật giao dịch + new: + new_transaction: Giao dịch mới + show: + keep_both: Không, giữ cả hai + loan_payment: Thanh toán khoản vay + mark_recurring: Đánh dấu là định kỳ + mark_recurring_subtitle: Theo dõi giao dịch định kỳ này. Mức biến động được tự động tính từ 6 tháng giao dịch tương tự. + mark_recurring_title: Giao dịch định kỳ + merge_duplicate: Có, gộp lại + potential_duplicate_description: Giao dịch đang chờ này có thể giống với giao dịch đã đăng bên dưới. Nếu vậy, hãy gộp lại để tránh tính hai lần. + potential_duplicate_title: Phát hiện trùng lặp có thể + transfer: Chuyển khoản + account_label: Tài khoản + amount: Số tiền + category_label: Danh mục + date_label: Ngày + delete: Xóa + delete_subtitle: Thao tác này xóa vĩnh viễn giao dịch, ảnh hưởng số dư lịch sử và không thể hoàn tác. + delete_title: Xóa giao dịch + details: Chi tiết + attachments: Tệp đính kèm + exclude: Loại trừ + exclude_description: Giao dịch bị loại trừ sẽ bị xóa khỏi tính toán ngân sách và báo cáo. + activity_type: Loại hoạt động + activity_type_description: Loại hoạt động đầu tư (Mua, Bán, Cổ tức, v.v.). Tự động phát hiện hoặc đặt thủ công. + one_time_title: "%{type} một lần" + one_time_description: Giao dịch một lần sẽ bị loại trừ khỏi một số tính toán ngân sách và báo cáo. + convert_to_trade_title: Chuyển đổi thành giao dịch chứng khoán + convert_to_trade_description: Chuyển giao dịch này thành giao dịch Mua hoặc Bán với thông tin chứng khoán để theo dõi danh mục. + convert_to_trade_button: Chuyển đổi thành giao dịch + transfer_matcher_description: Kết nối giao dịch này với giao dịch đối ứng trong tài khoản khác. + pending_duplicate_merger_title: Trùng lặp với giao dịch đã đăng? + pending_duplicate_merger_description: Gộp thủ công giao dịch đang chờ này với phiên bản đã đăng. + pending_duplicate_merger_button: Mở công cụ gộp + merchant_label: Nhà cung cấp + name_label: Tên + nature: Loại + none: "(không có)" + note_label: Ghi chú + note_placeholder: Nhập ghi chú + overview: Tổng quan + settings: Cài đặt + tags_label: Nhãn + tab_transactions: Giao dịch + tab_upcoming: Sắp tới + uncategorized: "(chưa phân loại)" + additional_details: "Thông tin bổ sung" + payee: "Người nhận" + description: "Mô tả" + memo: "Ghi nhớ" + provider_extras: "Thông tin nhà cung cấp" + transfer_or_debt_payment: "Chuyển khoản hay Thanh toán nợ?" + open_matcher: "Mở công cụ khớp" + convert: "Chuyển đổi" + activity_labels: + buy: Mua + sell: Bán + sweep_in: Chuyển vào + sweep_out: Chuyển ra + dividend: Cổ tức + reinvestment: Tái đầu tư + interest: Lãi + fee: Phí + transfer: Chuyển khoản + contribution: Đóng góp + withdrawal: Rút tiền + exchange: Trao đổi + other: Khác + mark_recurring: Đánh dấu là định kỳ + mark_recurring_subtitle: Theo dõi giao dịch định kỳ này. Mức biến động được tự động tính từ 6 tháng giao dịch tương tự. + mark_recurring_title: Giao dịch định kỳ + potential_duplicate_title: Phát hiện trùng lặp có thể + potential_duplicate_description: Giao dịch đang chờ này có thể giống với giao dịch đã đăng bên dưới. Nếu vậy, hãy gộp lại để tránh tính hai lần. + merge_duplicate_button: Có, gộp lại + keep_both: Không, giữ cả hai + split_parent_row: + split_label: "Phân chia" + transfer_match: + auto_matched: Tự động khớp + auto_matched_short: T/K + confirm_match: Xác nhận khớp + payment_confirmed: Thanh toán đã được xác nhận + reject_match: Từ chối khớp + transfer_confirmed: Chuyển khoản đã được xác nhận + transaction: + pending: Đang chờ + pending_tooltip: Giao dịch đang chờ — có thể thay đổi khi được đăng + linked_with_provider: Liên kết với %{provider} + activity_type_tooltip: Loại hoạt động đầu tư + possible_duplicate: Trùng lặp? + potential_duplicate_tooltip: Đây có thể là giao dịch trùng lặp + review_recommended: Xem xét + review_recommended_tooltip: Chênh lệch số tiền lớn — nên xem xét để kiểm tra trùng lặp + split: Phân chia + split_tooltip: Giao dịch này đã được phân chia thành nhiều mục + split_child_tooltip: Một phần của giao dịch phân chia + merge_duplicate: + success: Đã gộp giao dịch thành công + failure: Không thể gộp giao dịch + dismiss_duplicate: + success: Giữ nguyên là các giao dịch riêng biệt + failure: Không thể bỏ qua đề xuất trùng lặp + pending_duplicate_merge: + possible_duplicate: Trùng lặp? + possible_duplicate_short: Trùng? + review_recommended: Xem xét + review_recommended_short: Xem + confirm_title: "Gộp với giao dịch đã đăng (%{posted_amount})" + reject_title: Giữ nguyên là các giao dịch riêng biệt + summary: + total_transactions: Tổng giao dịch + income: Thu nhập + expenses: Chi tiêu + inflow: Dòng tiền vào + outflow: Dòng tiền ra + header: + edit_categories: Chỉnh sửa danh mục + edit_imports: Chỉnh sửa nhập dữ liệu + edit_merchants: Chỉnh sửa nhà cung cấp + edit_tags: Chỉnh sửa nhãn + import: Nhập + index: + title: "Giao dịch" + transaction: giao dịch + transactions: giao dịch + import: Nhập + new_rule: "Quy tắc mới" + edit_rules: "Chỉnh sửa quy tắc" + edit_categories: "Chỉnh sửa danh mục" + edit_tags: "Chỉnh sửa nhãn" + edit_merchants: "Chỉnh sửa nhà cung cấp" + edit_imports: "Chỉnh sửa nhập dữ liệu" + new_transaction: "Giao dịch mới" + categorize_button: + one: "Phân loại (1)" + other: "Phân loại (%{count})" + categorizes: + show: + exit: "Thoát" + skip: "Bỏ qua" + remaining: + one: "Còn 1 giao dịch chưa phân loại" + other: "Còn %{count} giao dịch chưa phân loại" + transaction_count: + one: "1 giao dịch" + other: "%{count} giao dịch" + transactions_hint: "Bỏ chọn để loại trừ giao dịch, hoặc gán danh mục khác trực tiếp trong hàng." + assign_category: "Gán danh mục" + assign_category_prompt: "→ gán" + filter_placeholder: "Tìm kiếm danh mục..." + col_transaction: "Giao dịch" + col_date: "Ngày" + col_amount: "Số tiền" + col_category: "Danh mục" + type_income: "Thu nhập" + type_expense: "Chi tiêu" + create_rule_label: "Tạo quy tắc phân loại" + rule_description_prefix: "Các giao dịch %{type} trong tương lai có tên chứa" + rule_description_suffix: "cũng nên được gán danh mục này." + no_categories: "Không có danh mục phù hợp" + all_done: "Tất cả giao dịch đã được phân loại" + create: + categorized: + one: "1 giao dịch đã được phân loại" + other: "%{count} giao dịch đã được phân loại" + rule_creation_failed: "Giao dịch đã được phân loại, nhưng không thể tạo quy tắc (có thể đã tồn tại)." + entry_row: + include_checkbox: "Bao gồm %{name}" + assign_category_select: "Gán danh mục cho %{name}" + list: + drag_drop_title: Thả CSV để nhập + drag_drop_subtitle: Tải lên giao dịch trực tiếp + transaction: giao dịch + transactions: giao dịch + toggle_recurring_section: Bật/tắt giao dịch định kỳ sắp tới + search: + filters: + account: Tài khoản + date: Ngày + type: Loại + status: Trạng thái + amount: Số tiền + category: Danh mục + tag: Nhãn + merchant: Nhà cung cấp + convert_to_trade: + title: Chuyển đổi thành giao dịch chứng khoán + description: Chuyển giao dịch này thành giao dịch với thông tin chứng khoán + date_label: "Ngày:" + account_label: "Tài khoản:" + amount_label: "Số tiền:" + security_label: Chứng khoán + security_prompt: Chọn chứng khoán... + security_custom: "+ Nhập mã ticker tùy chỉnh" + security_not_listed_hint: Không thấy chứng khoán? Chọn "Nhập mã ticker tùy chỉnh" ở cuối danh sách. + ticker_placeholder: AAPL + ticker_hint: "Nhập mã cổ phiếu/ETF (ví dụ: AAPL, MSFT)" + ticker_search_placeholder: Tìm kiếm mã ticker... + ticker_search_hint: Tìm theo mã ticker hoặc tên công ty, hoặc nhập mã ticker tùy chỉnh + price_mismatch_title: Giá có thể không khớp + price_mismatch_message: "Giá của bạn (%{entered_price}/cổ phiếu) khác đáng kể so với giá thị trường hiện tại của %{ticker} (%{market_price}). Nếu có vẻ sai, bạn có thể đã chọn nhầm chứng khoán — thử \"Nhập mã ticker tùy chỉnh\"." + quantity_label: Số lượng (Cổ phiếu) + quantity_placeholder: ví dụ 20 + quantity_hint: Số cổ phiếu giao dịch + price_label: Giá mỗi cổ phiếu + price_placeholder: ví dụ 52.15 + price_hint: Giá mỗi cổ phiếu (%{currency}) + qty_or_price_hint: Nhập ít nhất số lượng HOẶC giá. Giá trị còn lại sẽ được tính từ số tiền giao dịch (%{amount}). + trade_type_label: Loại giao dịch + trade_type_hint: Mua hoặc Bán cổ phiếu của một chứng khoán + exchange_label: Sàn giao dịch (Tùy chọn) + exchange_placeholder: XNAS + exchange_hint: Để trống để tự động phát hiện + cancel: Hủy + submit: Chuyển đổi thành giao dịch + success: Giao dịch đã được chuyển đổi thành giao dịch chứng khoán + conversion_note: "Chuyển đổi từ giao dịch: %{original_name} (%{original_date})" + errors: + not_investment_account: Chỉ giao dịch trong tài khoản đầu tư mới có thể chuyển đổi + already_converted: Giao dịch này đã được chuyển đổi hoặc loại trừ + enter_ticker: Vui lòng nhập mã ticker + security_not_found: Chứng khoán đã chọn không còn tồn tại. Vui lòng chọn chứng khoán khác. + select_security: Vui lòng chọn hoặc nhập chứng khoán + enter_qty_or_price: Vui lòng nhập số lượng hoặc giá mỗi cổ phiếu. Giá trị còn lại sẽ được tính từ số tiền giao dịch. + invalid_qty_or_price: Số lượng hoặc giá không hợp lệ. Vui lòng nhập giá trị dương hợp lệ. + conversion_failed: "Chuyển đổi giao dịch thất bại: %{error}" + unexpected_error: "Lỗi không mong muốn trong quá trình chuyển đổi: %{error}" + searches: + filters: + date_filter: + start_date: "Ngày bắt đầu" + end_date: "Ngày kết thúc" + amount_filter: + equal_to: Bằng + greater_than: Lớn hơn + less_than: Nhỏ hơn + placeholder: '0' + badge: + expense: Chi tiêu + income: Thu nhập + on_or_after: từ ngày %{date} trở đi + on_or_before: đến ngày %{date} + transfer: Chuyển khoản + confirmed: Đã xác nhận + pending: Đang chờ + type_filter: + expense: Chi tiêu + income: Thu nhập + transfer: Chuyển khoản + status_filter: + confirmed: Đã xác nhận + pending: Đang chờ + menu: + account_filter: Tài khoản + amount_filter: Số tiền + apply: Áp dụng + cancel: Hủy + category_filter: Danh mục + clear_filters: Xóa bộ lọc + date_filter: Ngày + merchant_filter: Nhà cung cấp + status_filter: Trạng thái + tag_filter: Nhãn + type_filter: Loại + search: + equal_to: bằng + greater_than: lớn hơn + less_than: nhỏ hơn + form: + toggle_selection_checkboxes: Bật/tắt tất cả ô chọn + search_placeholder: "Tìm kiếm giao dịch ..." + filter: "Lọc" + attachments: + cannot_exceed: "Không được vượt quá %{count} tệp đính kèm mỗi giao dịch" + uploaded_one: "Đã tải lên tệp đính kèm thành công" + uploaded_many: "Đã tải lên %{count} tệp đính kèm thành công" + failed_upload: "Tải lên tệp đính kèm thất bại: %{error}" + no_files_selected: "Chưa chọn tệp để tải lên" + attachment_deleted: "Đã xóa tệp đính kèm thành công" + failed_delete: "Xóa tệp đính kèm thất bại: %{error}" + upload_failed: "Tải lên tệp đính kèm thất bại. Vui lòng thử lại hoặc liên hệ hỗ trợ." + delete_failed: "Xóa tệp đính kèm thất bại. Vui lòng thử lại hoặc liên hệ hỗ trợ." + upload: "Tải lên" + no_attachments: "Chưa có tệp đính kèm" + select_up_to: "Chọn tối đa %{count} tệp (ảnh hoặc PDF, tối đa %{size}MB mỗi tệp) • %{used}/%{count} đã dùng" + files: + one: "Tệp (1)" + other: "Tệp (%{count})" + browse_to_add: "Duyệt để thêm tệp" + max_reached: "Đã đạt giới hạn tệp tối đa (%{count}/%{max}). Xóa tệp hiện có để tải lên tệp khác." diff --git a/config/locales/views/transactions/zh-CN.yml b/config/locales/views/transactions/zh-CN.yml index 11fa2e3e5..01138ef0f 100644 --- a/config/locales/views/transactions/zh-CN.yml +++ b/config/locales/views/transactions/zh-CN.yml @@ -1,40 +1,271 @@ --- zh-CN: transactions: + bulk_updates: + new: + cancel: 取消 + category_label: 分类 + category_prompt: 选择分类 + date_label: 日期 + header_title: 编辑交易 + merchant_label: 商户 + merchant_prompt: 选择商户 + name_label: 名称 + name_placeholder: 输入一个名称,该名称将应用于所选交易 + none: (无) + notes_label: 备注 + notes_placeholder: 输入将应用于所选交易的备注 + overview: 概览 + save: 保存 + tags_label: 标签 + transactions_section: 交易 + unknown_name: 未知交易 + selection_bar: + duplicate: 重复 + edit: 编辑 + selected: 已选中 form: + details: 详情 account: 账户 account_prompt: 选择账户 amount: 金额 category: 分类 + category_label: 分类 category_prompt: 选择分类 date: 日期 description: 描述 - description_placeholder: 请输入交易描述 - details: 详情 + description_placeholder: 描述交易 expense: 支出 income: 收入 - none: "(无)" + merchant_label: 商户 + none: (无) note_label: 备注 - note_placeholder: 请输入备注 + note_placeholder: 输入备注 submit: 添加交易 tags_label: 标签 transfer: 转账 + create: + created: 交易已创建 + update: + updated: 交易已更新 + new: + new_transaction: 新建交易 + show: + keep_both: 不,保留两笔 + loan_payment: 贷款还款 + mark_recurring: 标记为循环 + mark_recurring_subtitle: 将此交易跟踪为循环交易。金额差异会根据过去 6 个月的相似交易自动计算。 + mark_recurring_title: 循环交易 + merge_duplicate: 是,合并它们 + potential_duplicate_description: 这笔待处理交易可能与下方已入账交易相同。如果是这样,请合并以避免重复计算。 + potential_duplicate_title: 检测到可能的重复项 + transfer: 转账 + account_label: 账户 + amount: 金额 + category_label: 分类 + date_label: 日期 + delete: 删除 + delete_subtitle: 这将永久删除该交易,影响您的历史余额,并且无法撤销。 + delete_title: 删除交易 + details: 详情 + attachments: 附件 + exclude: 排除 + exclude_description: 被排除的交易将从预算计算和报表中移除。 + activity_type: 活动类型 + activity_type_description: 投资活动类型(买入、卖出、分红等)。可自动检测或手动设置。 + one_time_title: 一次性%{type} + one_time_description: 一次性交易将从某些预算计算和报表中排除,以帮助您看清真正重要的内容。 + convert_to_trade_title: 转换为证券交易 + convert_to_trade_description: 将此交易转换为带证券详情的买入或卖出交易,以用于投资组合跟踪。 + convert_to_trade_button: 转换为交易 + transfer_matcher_description: 将此交易与其他账户中的对应交易关联。 + pending_duplicate_merger_title: 已入账交易的重复项? + pending_duplicate_merger_description: 手动将此待处理交易与其已入账版本合并。 + pending_duplicate_merger_button: 打开合并器 + merchant_label: 商户 + name_label: 名称 + nature: 类型 + none: (无) + note_label: 备注 + note_placeholder: 输入备注 + overview: 概览 + settings: 设置 + tags_label: 标签 + tab_transactions: 交易 + tab_upcoming: 待发生 + uncategorized: (未分类) + additional_details: 其他详情 + payee: 收款方 + description: 描述 + memo: 备忘录 + provider_extras: 提供商附加信息 + transfer_or_debt_payment: 转账还是债务还款? + open_matcher: 打开匹配器 + convert: 转换 + activity_labels: + buy: 买入 + sell: 卖出 + sweep_in: 资金划入 + sweep_out: 资金划出 + dividend: 分红 + reinvestment: 再投资 + interest: 利息 + fee: 费用 + transfer: 转账 + contribution: 供款 + withdrawal: 提取 + exchange: 兑换 + other: 其他 + mark_recurring: 标记为循环 + mark_recurring_subtitle: 将此交易跟踪为循环交易。金额差异会根据过去 6 个月的相似交易自动计算。 + mark_recurring_title: 循环交易 + potential_duplicate_title: 检测到可能的重复项 + keep_both: 不,保留两笔 + split_parent_row: + split_label: 拆分 + transfer_match: + auto_matched: 自动匹配 + auto_matched_short: 自/配 + confirm_match: 确认匹配 + payment_confirmed: 付款已确认 + reject_match: 拒绝匹配 + transfer_confirmed: 转账已确认 + merge_duplicate: + success: 交易已成功合并 + failure: 无法合并交易 + dismiss_duplicate: + success: 已保留为独立交易 + failure: 无法忽略重复建议 + pending_duplicate_merge: + possible_duplicate: 重复? + possible_duplicate_short: 重? + review_recommended: 建议复核 + review_recommended_short: 复核 + confirm_title: 与已入账交易合并(%{posted_amount}) + reject_title: 保持为独立交易 + summary: + total_transactions: 交易总数 + income: 收入 + expenses: 支出 + inflow: 流入 + outflow: 流出 header: edit_categories: 编辑分类 edit_imports: 编辑导入 edit_merchants: 编辑商户 edit_tags: 编辑标签 import: 导入 - transaction: - linked_with_provider: 已与 %{provider} 关联 index: - import: 导入 + title: 交易 transaction: 交易 - transactions: 交易记录 - new: + transactions: 交易 + import: 导入 + new_rule: 新建规则 + edit_rules: 编辑规则 + edit_categories: 编辑分类 + edit_tags: 编辑标签 + edit_merchants: 编辑商户 + edit_imports: 编辑导入 new_transaction: 新建交易 + categorize_button: + one: 分类(1) + other: 分类(%{count}) + categorizes: + show: + exit: 退出 + skip: 跳过 + remaining: + one: 还剩 1 笔未分类交易 + other: 还剩 %{count} 笔未分类交易 + transaction_count: + one: 1 笔交易 + other: "%{count} 笔交易" + transactions_hint: 取消勾选可排除交易,或直接在该行中为其分配其他分类。 + assign_category: 分配分类 + assign_category_prompt: → 分配 + filter_placeholder: 搜索分类... + col_transaction: 交易 + col_date: 日期 + col_amount: 金额 + col_category: 分类 + type_income: 收入 + type_expense: 支出 + create_rule_label: 创建分类规则 + rule_description_prefix: 未来名称包含以下内容的 %{type} 交易 + rule_description_suffix: 也应使用此分类。 + no_categories: 没有匹配的分类 + all_done: 所有交易都已分类 + create: + categorized: + one: 1 笔交易已分类 + other: "%{count} 笔交易已分类" + rule_creation_failed: 交易已分类,但规则无法创建(可能已存在)。 + entry_row: + include_checkbox: 包含 %{name} + assign_category_select: 为 %{name} 分配分类 + list: + drag_drop_title: 拖放 CSV 即可导入 + drag_drop_subtitle: 直接上传交易 + transaction: 交易 + transactions: 交易 + toggle_recurring_section: 切换待发生循环交易 + search: + filters: + account: 账户 + date: 日期 + type: 类型 + status: 状态 + amount: 金额 + category: 分类 + tag: 标签 + merchant: 商户 + convert_to_trade: + title: 转换为证券交易 + description: 将此交易转换为带证券详情的交易 + date_label: 日期: + account_label: 账户: + amount_label: 金额: + security_label: 证券 + security_prompt: 选择一只证券... + security_custom: + 输入自定义代码 + security_not_listed_hint: 找不到您的证券?请在列表底部选择“输入自定义代码”。 + ticker_placeholder: AAPL + ticker_hint: 输入股票/ETF 代码(例如 AAPL、MSFT) + ticker_search_placeholder: 搜索代码... + ticker_search_hint: 可按代码或公司名称搜索,或直接输入自定义代码 + price_mismatch_title: 价格可能不匹配 + price_mismatch_message: "您的价格(%{entered_price}/股)与 %{ticker} 当前市场价格(%{market_price})差异较大。如果这看起来不对,您可能选错了证券——请尝试使用“输入自定义代码”指定正确的证券。" + quantity_label: 数量(股) + quantity_placeholder: 例如 20 + quantity_hint: 交易的股数 + price_label: 每股价格 + price_placeholder: 例如 52.15 + price_hint: 每股价格(%{currency}) + qty_or_price_hint: 至少输入数量或价格之一。另一个值将根据交易金额(%{amount})计算。 + trade_type_label: 交易类型 + trade_type_hint: 买入或卖出证券 + exchange_label: 交易所(可选) + exchange_placeholder: XNAS + exchange_hint: 留空以自动识别 + cancel: 取消 + submit: 转换为交易 + success: 交易已转换为证券交易 + conversion_note: "由交易转换而来:%{original_name}(%{original_date})" + errors: + not_investment_account: 只有投资账户中的交易才能转换为证券交易 + already_converted: 此交易已转换或已排除 + enter_ticker: 请输入证券代码 + security_not_found: 所选证券已不存在。请选择其他证券。 + select_security: 请选择或输入证券 + enter_qty_or_price: 请输入数量或每股价格之一。另一个值将根据交易金额计算。 + invalid_qty_or_price: 数量或价格无效。请输入合法的正值。 + conversion_failed: 转换交易失败:%{error} + unexpected_error: 转换过程中发生意外错误:%{error} searches: filters: + date_filter: + start_date: 开始日期 + end_date: 结束日期 amount_filter: equal_to: 等于 greater_than: 大于 @@ -43,13 +274,18 @@ zh-CN: badge: expense: 支出 income: 收入 - on_or_after: 在 %{date} 或之后 - on_or_before: 在 %{date} 或之前 + on_or_after: 于 %{date} 当日或之后 + on_or_before: 于 %{date} 当日或之前 transfer: 转账 + confirmed: 已确认 + pending: 待处理 type_filter: expense: 支出 income: 收入 transfer: 转账 + status_filter: + confirmed: 已确认 + pending: 待处理 menu: account_filter: 账户 amount_filter: 金额 @@ -59,6 +295,7 @@ zh-CN: clear_filters: 清除筛选 date_filter: 日期 merchant_filter: 商户 + status_filter: 状态 tag_filter: 标签 type_filter: 类型 search: @@ -67,26 +304,23 @@ zh-CN: less_than: 小于 form: toggle_selection_checkboxes: 切换所有复选框 - show: - account_label: 账户 - amount: 金额 - category_label: 分类 - date_label: 日期 - delete: 删除 - delete_subtitle: 此操作将永久删除该交易,影响历史余额数据,且不可恢复。 - delete_title: 删除交易 - details: 详情 - mark_recurring: 标记为定期交易 - mark_recurring_subtitle: 将此交易标记为定期交易跟踪。金额波动将根据过去6个月的类似交易自动计算。 - mark_recurring_title: 定期交易设置 - merchant_label: 商户 - name_label: 名称 - nature: 类型 - none: "(无)" - note_label: 备注 - note_placeholder: 请输入备注 - overview: 概览 - settings: 设置 - tags_label: 标签 - uncategorized: "(未分类)" - toggle_recurring_section: 切换即将发生的定期交易显示 + search_placeholder: 搜索交易... + filter: 筛选 + attachments: + cannot_exceed: 每笔交易不能超过 %{count} 个附件 + uploaded_one: 附件上传成功 + uploaded_many: 成功上传 %{count} 个附件 + failed_upload: 上传附件失败:%{error} + no_files_selected: 未选择要上传的文件 + attachment_deleted: 附件删除成功 + failed_delete: 删除附件失败:%{error} + upload_failed: 上传附件失败。请重试或联系支持。 + delete_failed: 删除附件失败。请重试或联系支持。 + upload: 上传 + no_attachments: 还没有附件 + select_up_to: 选择最多 %{count} 个文件(图片或 PDF,每个最大 %{size}MB)• 已使用 %{used} / %{count} + files: + one: 文件(1) + other: 文件(%{count}) + browse_to_add: 浏览并添加文件 + max_reached: 已达到最大文件数(%{count}/%{max})。删除现有文件后才能上传另一个。 diff --git a/config/locales/views/transfer_matches/vi.yml b/config/locales/views/transfer_matches/vi.yml new file mode 100644 index 000000000..f63ecd84d --- /dev/null +++ b/config/locales/views/transfer_matches/vi.yml @@ -0,0 +1,24 @@ +--- +vi: + transfer_matches: + create: + success: Chuyển khoản đã được tạo + new: + header: + title: Khớp chuyển khoản hoặc thanh toán + subtitle: Khớp giao dịch tương ứng trong tài khoản khác hoặc tạo mới nếu chưa tồn tại. + from_account: Tài khoản nguồn + from_account_named: "Tài khoản nguồn: %{name}" + to_account: Tài khoản đích + to_account_named: "Tài khoản đích: %{name}" + outflow_transaction: Giao dịch chi ra + inflow_transaction: Giao dịch thu vào + create_transfer_match: Tạo khớp chuyển khoản + matching_fields: + select_method: Chọn phương thức khớp giao dịch của bạn. + match_existing_recommended: Khớp giao dịch hiện có (khuyến nghị) + create_new_transaction: Tạo giao dịch mới + matching_method: Phương thức khớp + matching_transaction: Giao dịch khớp + target_account: Tài khoản đích + no_matching_transactions: Chúng tôi không tìm thấy giao dịch nào để khớp từ các tài khoản khác của bạn. Vui lòng chọn một tài khoản và chúng tôi sẽ tạo giao dịch thu vào mới cho bạn. diff --git a/config/locales/views/transfer_matches/zh-CN.yml b/config/locales/views/transfer_matches/zh-CN.yml new file mode 100644 index 000000000..67e3e7c85 --- /dev/null +++ b/config/locales/views/transfer_matches/zh-CN.yml @@ -0,0 +1,24 @@ +--- +zh-CN: + transfer_matches: + create: + success: 转账已创建 + new: + header: + title: 匹配转账或付款 + subtitle: 将另一账户中的对应交易进行匹配,若不存在则创建一笔。 + from_account: 来源账户 + from_account_named: "来源账户:%{name}" + to_account: 目标账户 + to_account_named: "目标账户:%{name}" + outflow_transaction: 流出交易 + inflow_transaction: 流入交易 + create_transfer_match: 创建转账匹配 + matching_fields: + select_method: 选择一种交易匹配方式。 + match_existing_recommended: 匹配现有交易(推荐) + create_new_transaction: 创建新交易 + matching_method: 匹配方式 + matching_transaction: 匹配交易 + target_account: 目标账户 + no_matching_transactions: 我们未能在您的其他账户中找到可匹配的交易。请选择一个账户,我们会为您创建一笔新的流入交易。 diff --git a/config/locales/views/transfers/vi.yml b/config/locales/views/transfers/vi.yml new file mode 100644 index 000000000..05c23cf04 --- /dev/null +++ b/config/locales/views/transfers/vi.yml @@ -0,0 +1,47 @@ +--- +vi: + transfers: + create: + success: Chuyển khoản đã được tạo + destroy: + success: Chuyển khoản đã được xóa + form: + amount: Số tiền + calculate_rate_tab: Tính tỷ giá ngoại hối + convert_tab: Chuyển đổi với tỷ giá ngoại hối + date: Ngày + destination_amount: Số tiền đích + destination_amount_display: "Số tiền đích: %{amount}" + exchange_rate: Tỷ giá + exchange_rate_display: "Tỷ giá: %{rate}" + exchange_rate_help: Chọn cách nhập số tiền chuyển khoản. + expense: Chi tiêu + from: Từ + income: Thu nhập + select_account: Chọn tài khoản + source_amount: Số tiền nguồn + submit: Tạo chuyển khoản + to: Đến + transfer: Chuyển khoản + new: + title: Chuyển khoản mới + show: + delete: Xóa chuyển khoản + delete_subtitle: Thao tác này sẽ xóa chuyển khoản. Các giao dịch liên quan sẽ không bị xóa. + delete_title: Xóa chuyển khoản? + details: Chi tiết + mark_recurring: Đánh dấu là định kỳ + mark_recurring_subtitle: Theo dõi chuyển khoản này như một mẫu định kỳ trong nguồn cấp sắp tới và trang định kỳ. + mark_recurring_title: Đánh dấu chuyển khoản là định kỳ + note_label: Ghi chú + note_placeholder: Thêm ghi chú cho chuyển khoản này + overview: Tổng quan + settings: Cài đặt + from: Từ + to: Đến + date: Ngày + amount: Số tiền + category: Danh mục + uncategorized: Chưa phân loại + update: + success: Chuyển khoản đã được cập nhật diff --git a/config/locales/views/users/vi.yml b/config/locales/views/users/vi.yml new file mode 100644 index 000000000..805405f6d --- /dev/null +++ b/config/locales/views/users/vi.yml @@ -0,0 +1,29 @@ +--- +vi: + users: + destroy: + success: Tài khoản của bạn đã được xóa. + update: + email_change_failed: Không thể thay đổi địa chỉ email. + email_change_initiated: Vui lòng kiểm tra địa chỉ email mới của bạn để xác nhận thay đổi. + success: Hồ sơ của bạn đã được cập nhật. + resend_confirmation_email: + success: Email xác nhận mới đã được xếp vào hàng chờ gửi. + no_pending_change: Hiện không có thay đổi email nào đang chờ xử lý! + reset: + success: Tài khoản của bạn đã được đặt lại. Dữ liệu sẽ được xóa trong nền sau một thời gian. + unauthorized: Bạn không được phép thực hiện hành động này + reset_with_sample_data: + success: Tài khoản của bạn đã được đặt lại và dữ liệu mẫu đang được chuẩn bị. Bạn sẽ thấy dữ liệu demo trong giây lát. + roles: + admin: Quản trị viên + member: Thành viên + super_admin: Quản trị viên cấp cao + user_menu: + aria_label: Mở menu tài khoản + version: Phiên bản + settings: Cài đặt + changelog: Nhật ký thay đổi + feedback: Phản hồi + contact: Liên hệ + log_out: Đăng xuất diff --git a/config/locales/views/valuations/vi.yml b/config/locales/views/valuations/vi.yml new file mode 100644 index 000000000..e5635bf4a --- /dev/null +++ b/config/locales/views/valuations/vi.yml @@ -0,0 +1,60 @@ +--- +vi: + valuations: + confirmation_contents: + this_will: "Thao tác này sẽ %{action_verb} giá trị tài khoản vào" + to_colon: "thành:" + total_account_value: Tổng giá trị tài khoản + holdings_value: Giá trị danh mục nắm giữ + brokerage_cash: Tiền mặt môi giới + account_balance: số dư tài khoản + credit_card_balance: số dư thẻ tín dụng + loan_balance: số dư khoản vay + property_value: giá trị bất động sản + vehicle_value: giá trị xe + crypto_balance: số dư tiền mã hóa + asset_value: giá trị tài sản + liability_balance: số dư nợ phải trả + balance: số dư + "on": "vào" + to: "thành" + recalculate_notice: "Tất cả giao dịch và số dư trong tương lai sẽ được tính lại dựa trên %{change_or_update} này." + change: thay đổi + update: cập nhật + create: + account_updated: Tài khoản đã được cập nhật + update: + account_updated: Tài khoản đã được cập nhật + entry_updated: Mục nhập đã được cập nhật + errors: + amount_required: Số tiền là bắt buộc + form: + amount: Số tiền + submit: Thêm cập nhật số dư + header: + balance: Số dư + index: + change: thay đổi + date: ngày + new_entry: Mục nhập mới + no_valuations: Chưa có định giá nào cho tài khoản này + valuations: Giá trị + value: giá trị + new: + title: Số dư mới + show: + amount: Số tiền + amount_label: Giá trị tài khoản vào ngày + date_label: Ngày + delete: Xóa + delete_subtitle: Hành động này không thể hoàn tác + delete_title: Xóa mục nhập + details: Chi tiết + name_label: Tên + name_placeholder: Nhập tên cho mục nhập này + note_label: Ghi chú + note_placeholder: Thêm bất kỳ thông tin chi tiết nào về mục nhập này + overview: Tổng quan + settings: Cài đặt + opening_balance: "Số dư đầu kỳ" + update_value: Cập nhật giá trị diff --git a/config/locales/views/valuations/zh-CN.yml b/config/locales/views/valuations/zh-CN.yml index 2cc6bfc63..d0b1efc7a 100644 --- a/config/locales/views/valuations/zh-CN.yml +++ b/config/locales/views/valuations/zh-CN.yml @@ -1,33 +1,60 @@ --- zh-CN: valuations: + confirmation_contents: + this_will: 这将把账户价值在 + to_colon: 改为: + total_account_value: 账户总价值 + holdings_value: 持仓价值 + brokerage_cash: 券商现金 + account_balance: 账户余额 + credit_card_balance: 信用卡余额 + loan_balance: 贷款余额 + property_value: 房产价值 + vehicle_value: 车辆价值 + crypto_balance: 加密资产余额 + asset_value: 资产价值 + liability_balance: 负债余额 + balance: 余额 + on: 在 + to: 到 + recalculate_notice: 所有未来的交易和余额都将根据此%{change_or_update}重新计算。 + change: 变更 + update: 更新 + create: + account_updated: 账户已更新 + update: + account_updated: 账户已更新 + entry_updated: 条目已更新 + errors: + amount_required: 需要金额 form: amount: 金额 submit: 添加余额更新 header: balance: 余额 index: - change: 变动 + change: 变更 date: 日期 - new_entry: 新建记录 - no_valuations: 此账户暂无估值记录 + new_entry: 新条目 + no_valuations: 此账户尚无估值 valuations: 估值 value: 价值 new: - amount: 金额 - submit: 提交 - title: 新建余额记录 + title: 新余额 show: amount: 金额 + amount_label: 此日期的账户价值 date_label: 日期 delete: 删除 - delete_subtitle: 此操作不可撤销 - delete_title: 删除记录 + delete_subtitle: 此操作无法撤销 + delete_title: 删除条目 details: 详情 name_label: 名称 - name_placeholder: 请输入此记录的名称 + name_placeholder: 为此条目输入名称 note_label: 备注 - note_placeholder: 添加此记录的其他详细信息 + note_placeholder: 为此条目添加任何补充详情 overview: 概览 settings: 设置 - opening_balance: 初始余额 + opening_balance: 开始余额 + update_value: 更新价值 diff --git a/config/locales/views/vehicles/vi.yml b/config/locales/views/vehicles/vi.yml new file mode 100644 index 000000000..44d5a0522 --- /dev/null +++ b/config/locales/views/vehicles/vi.yml @@ -0,0 +1,35 @@ +--- +vi: + vehicles: + edit: + edit: Chỉnh sửa %{account} + form: + make: Hãng xe + make_placeholder: Toyota + mileage: Số dặm + mileage_placeholder: '15000' + mileage_unit: Đơn vị + model: Mẫu xe + model_placeholder: Camry + year: Năm sản xuất + year_placeholder: '2023' + new: + title: Nhập thông tin xe + overview: + current_price: Giá hiện tại + make_model: Hãng & Mẫu xe + mileage: Số dặm + purchase_price: Giá mua + trend: Xu hướng + unknown: Không xác định + year: Năm sản xuất + tabs: + overview: + current_price: Giá hiện tại + make_model: Hãng & Mẫu xe + mileage: Số dặm + purchase_price: Giá mua + trend: Xu hướng + unknown: Không xác định + year: Năm sản xuất + edit_account_details: "Chỉnh sửa thông tin tài khoản" diff --git a/config/routes.rb b/config/routes.rb index e115dff1f..dd5290dfa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -282,6 +282,8 @@ Rails.application.routes.draw do resources :categories, except: :show do resources :deletions, only: %i[new create], module: :category + get :merge, on: :collection + post :perform_merge, on: :collection post :bootstrap, on: :collection delete :destroy_all, on: :collection end diff --git a/db/migrate/20260513120000_add_account_statement_to_imports.rb b/db/migrate/20260513120000_add_account_statement_to_imports.rb new file mode 100644 index 000000000..3d346212a --- /dev/null +++ b/db/migrate/20260513120000_add_account_statement_to_imports.rb @@ -0,0 +1,5 @@ +class AddAccountStatementToImports < ActiveRecord::Migration[7.2] + def change + add_reference :imports, :account_statement, type: :uuid, foreign_key: { on_delete: :nullify }, index: true + end +end diff --git a/db/schema.rb b/db/schema.rb index ed52f7da1..7ddff9358 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1013,6 +1013,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_29_120150) do t.jsonb "extracted_data" t.jsonb "expected_record_counts", default: {}, null: false t.jsonb "readback_verification", default: {}, null: false + t.uuid "account_statement_id" + t.index ["account_statement_id"], name: "index_imports_on_account_statement_id" t.index ["family_id"], name: "index_imports_on_family_id" end @@ -2062,6 +2064,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_29_120150) do add_foreign_key "impersonation_sessions", "users", column: "impersonated_id" add_foreign_key "impersonation_sessions", "users", column: "impersonator_id" add_foreign_key "import_rows", "imports" + add_foreign_key "imports", "account_statements", on_delete: :nullify add_foreign_key "imports", "families" add_foreign_key "indexa_capital_accounts", "indexa_capital_items" add_foreign_key "indexa_capital_items", "families" diff --git a/test/controllers/binance_items_controller_test.rb b/test/controllers/binance_items_controller_test.rb index 91b13d60b..451705428 100644 --- a/test/controllers/binance_items_controller_test.rb +++ b/test/controllers/binance_items_controller_test.rb @@ -55,6 +55,47 @@ class BinanceItemsControllerTest < ActionDispatch::IntegrationTest assert_equal "Crypto", binance_account.current_account.accountable_type end + test "complete_account_setup updates sync_start_date when provided with a valid past date" do + binance_account = @binance_item.binance_accounts.create!( + name: "Spot Portfolio", + account_type: "spot", + currency: "USD", + current_balance: 1000.0 + ) + + past_date = (Date.current - 7.days).to_s + + post complete_account_setup_binance_item_url(@binance_item), params: { + selected_accounts: [ binance_account.id ], + sync_start_date: past_date + } + + assert_response :redirect + @binance_item.reload + assert_equal Date.parse(past_date), @binance_item.sync_start_date + end + + test "complete_account_setup rejects a future sync_start_date and sets flash alert" do + binance_account = @binance_item.binance_accounts.create!( + name: "Spot Portfolio", + account_type: "spot", + currency: "USD", + current_balance: 1000.0 + ) + + future_date = (Date.current + 2.days).to_s + original_sync_date = @binance_item.sync_start_date + + post complete_account_setup_binance_item_url(@binance_item), params: { + selected_accounts: [ binance_account.id ], + sync_start_date: future_date + } + + @binance_item.reload + assert_nil @binance_item.sync_start_date + assert_equal "Sync start date must be a valid date in the past.", flash[:alert] + end + test "complete_account_setup with no selection shows message" do @binance_item.binance_accounts.create!( name: "Spot Portfolio", diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb index 33bdf6641..2f8bed13d 100644 --- a/test/controllers/categories_controller_test.rb +++ b/test/controllers/categories_controller_test.rb @@ -2,7 +2,8 @@ require "test_helper" class CategoriesControllerTest < ActionDispatch::IntegrationTest setup do - sign_in users(:family_admin) + sign_in @user = users(:family_admin) + @family = @user.family @transaction = transactions :one ensure_tailwind_build end @@ -95,4 +96,208 @@ class CategoriesControllerTest < ActionDispatch::IntegrationTest assert_redirected_to categories_url end + + test "merge renders in the settings layout" do + get merge_categories_path + + assert_response :success + assert_select "#mobile-settings-nav" + end + + test "merge renders without the settings layout for modal frame requests" do + get merge_categories_path, headers: { "Turbo-Frame" => "modal" } + + assert_response :success + assert_no_match(/<\/turbo-frame>/, response.body) + assert_select "dialog" + end + + test "merge selected categories into an existing category" do + target = @family.categories.create!( + name: "Dining", + color: "#111111", + lucide_icon: "utensils" + ) + source = @family.categories.create!( + name: "Coffee Shops", + color: "#000000", + lucide_icon: "coffee" + ) + transaction = Transaction.create!(category: source) + Entry.create!( + account: accounts(:depository), + entryable: transaction, + name: "Coffee transaction", + date: Date.current, + amount: 10, + currency: "USD" + ) + + assert_difference "Category.count", -1 do + post perform_merge_categories_path, params: { + target_id: target.id, + source_ids: [ source.id ] + } + end + + assert_redirected_to categories_path + assert_equal target, transaction.reload.category + assert_not Category.exists?(source.id) + end + + test "merge redirects when a source category cannot be destroyed" do + target = @family.categories.create!( + name: "Destroy Failure Target", + color: "#000000", + lucide_icon: "shapes" + ) + source = @family.categories.create!( + name: "Destroy Failure Source", + color: "#111111", + lucide_icon: "shapes" + ) + + Category::Merger.any_instance + .stubs(:merge!) + .raises(ActiveRecord::RecordNotDestroyed.new("cannot destroy category", source)) + + post perform_merge_categories_path, params: { + target_id: target.id, + source_ids: [ source.id ] + } + + assert_redirected_to merge_categories_path + assert Category.exists?(source.id) + end + + test "merge rejects selecting the target as a source" do + target = @family.categories.create!( + name: "Self Target", + color: "#000000", + lucide_icon: "shapes" + ) + source = @family.categories.create!( + name: "Self Source", + color: "#111111", + lucide_icon: "shapes" + ) + + post perform_merge_categories_path, params: { + target_id: target.id, + source_ids: [ target.id, source.id ] + } + + assert_redirected_to merge_categories_path + assert Category.exists?(target.id) + assert Category.exists?(source.id) + end + + test "merge rejects parent category into any descendant" do + parent = @family.categories.create!( + name: "Parent Category", + color: "#000000", + lucide_icon: "folder" + ) + child = @family.categories.create!( + name: "Child Category", + color: "#111111", + lucide_icon: "folder", + parent: parent + ) + grandchild = @family.categories.create!( + name: "Grandchild Category", + color: "#222222", + lucide_icon: "folder" + ) + # Category validation normally prevents this depth; the merger still guards + # against stale or imported data with deeper hierarchies. + grandchild.update_column(:parent_id, child.id) + + post perform_merge_categories_path, params: { + target_id: grandchild.id, + source_ids: [ parent.id ] + } + + assert_redirected_to merge_categories_path + assert Category.exists?(parent.id) + end + + test "merge reparents source children to target category" do + target = @family.categories.create!( + name: "Target Category", + color: "#000000", + lucide_icon: "folder" + ) + source = @family.categories.create!( + name: "Source Category", + color: "#111111", + lucide_icon: "folder" + ) + child = @family.categories.create!( + name: "Source Child Category", + color: "#222222", + lucide_icon: "folder", + parent: source + ) + + post perform_merge_categories_path, params: { + target_id: target.id, + source_ids: [ source.id ] + } + + assert_redirected_to categories_path + assert_equal target.id, child.reload.parent_id + assert_not Category.exists?(source.id) + end + + test "merge rejects moving source children under a subcategory target" do + parent = @family.categories.create!( + name: "Target Parent Category", + color: "#000000", + lucide_icon: "folder" + ) + target = @family.categories.create!( + name: "Target Subcategory", + color: "#111111", + lucide_icon: "folder", + parent: parent + ) + source = @family.categories.create!( + name: "Source With Child", + color: "#222222", + lucide_icon: "folder" + ) + child = @family.categories.create!( + name: "Source Child", + color: "#333333", + lucide_icon: "folder", + parent: source + ) + + post perform_merge_categories_path, params: { + target_id: target.id, + source_ids: [ source.id ] + } + + assert_redirected_to merge_categories_path + assert Category.exists?(source.id) + assert_equal source.id, child.reload.parent_id + end + + test "merge ignores categories outside current family" do + other = families(:empty).categories.create!( + name: "Other Family Category", + color: "#000000", + lucide_icon: "shapes" + ) + + post perform_merge_categories_path, params: { + target_id: categories(:income).id, + source_ids: [ other.id ] + } + + assert_redirected_to merge_categories_path + assert Category.exists?(other.id) + end end diff --git a/test/controllers/imports_controller_test.rb b/test/controllers/imports_controller_test.rb index eea32693a..1b48bd953 100644 --- a/test/controllers/imports_controller_test.rb +++ b/test/controllers/imports_controller_test.rb @@ -1,6 +1,8 @@ require "test_helper" class ImportsControllerTest < ActionDispatch::IntegrationTest + include ActiveJob::TestHelper + setup do sign_in @user = users(:family_admin) ensure_tailwind_build @@ -85,18 +87,242 @@ class ImportsControllerTest < ActionDispatch::IntegrationTest @user.family.expects(:upload_document).never assert_difference "Import.count", 1 do + assert_difference "AccountStatement.count", 1 do + post imports_url, params: { + import: { + type: "DocumentImport", + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + + created_import = Import.order(:created_at).last + assert_equal "PdfImport", created_import.type + assert_equal AccountStatement.order(:created_at).last, created_import.account_statement + assert_not created_import.pdf_file.attached? + assert_redirected_to import_url(created_import) + assert_equal I18n.t("imports.create.pdf_processing"), flash[:notice] + end + + test "uploads pdf import through account statement" do + assert_difference "AccountStatement.count", 1 do + assert_difference "Import.where(type: 'PdfImport').count", 1 do + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + + statement = AccountStatement.order(:created_at).last + created_import = PdfImport.order(:created_at).last + assert_equal statement, created_import.account_statement + assert_not created_import.pdf_file.attached? + assert_redirected_to import_url(created_import) + assert_equal I18n.t("imports.create.pdf_processing"), flash[:notice] + end + + test "guest cannot create statement backed pdf import" do + sign_in users(:intro_user) + + assert_no_difference [ "AccountStatement.count", "Import.where(type: 'PdfImport').count" ] do + assert_no_enqueued_jobs only: ProcessPdfJob do + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + + assert_redirected_to new_import_url + assert_equal I18n.t("accounts.not_authorized"), flash[:alert] + end + + test "duplicate pdf import reuses account statement" do + statement = AccountStatement.create_from_upload!( + family: @user.family, + account: nil, + file: uploaded_file( + filename: "existing_statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + + assert_no_difference "AccountStatement.count" do + assert_difference "Import.where(type: 'PdfImport').count", 1 do + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + + created_import = PdfImport.order(:created_at).last + assert_equal statement, created_import.account_statement + assert_redirected_to import_url(created_import) + end + + test "duplicate pdf import does not enqueue processing twice for reused import" do + assert_difference "AccountStatement.count", 1 do + assert_difference "Import.where(type: 'PdfImport').count", 1 do + assert_enqueued_jobs 1, only: ProcessPdfJob do + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + end + + created_import = PdfImport.order(:created_at).last + assert_equal "importing", created_import.status + assert_redirected_to import_url(created_import) + end + + test "duplicate pdf import for inaccessible statement does not create import" do + AccountStatement.create_from_upload!( + family: @user.family, + account: accounts(:investment), + file: uploaded_file( + filename: "existing_statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + + sign_in users(:family_member) + + assert_no_difference [ "AccountStatement.count", "Import.where(type: 'PdfImport').count" ] do post imports_url, params: { import: { - type: "DocumentImport", import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") } } end - created_import = Import.order(:created_at).last - assert_equal "PdfImport", created_import.type - assert_redirected_to import_url(created_import) - assert_equal I18n.t("imports.create.pdf_processing"), flash[:notice] + assert_redirected_to new_import_url + assert_equal I18n.t("imports.create.duplicate_pdf_unavailable"), flash[:alert] + end + + test "read only shared user cannot reuse duplicate statement backed pdf import" do + AccountStatement.create_from_upload!( + family: @user.family, + account: accounts(:credit_card), + file: uploaded_file( + filename: "existing_statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + + sign_in users(:family_member) + + assert_no_difference [ "AccountStatement.count", "Import.where(type: 'PdfImport').count" ] do + assert_no_enqueued_jobs only: ProcessPdfJob do + post imports_url, params: { + import: { + import_file: file_fixture_upload("imports/sample_bank_statement.pdf", "application/pdf") + } + } + end + end + + assert_redirected_to new_import_url + assert_equal I18n.t("imports.create.duplicate_pdf_unavailable"), flash[:alert] + end + + test "setting statement backed pdf import account links source statement" do + statement = AccountStatement.create_from_upload!( + family: @user.family, + account: nil, + file: uploaded_file( + filename: "statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + pdf_import = PdfImport.create_from_statement!(statement: statement) + account = accounts(:depository) + + patch import_url(pdf_import), params: { import: { account_id: account.id } } + + assert_redirected_to import_url(pdf_import) + assert_equal I18n.t("imports.update.account_saved", default: "Account saved."), flash[:notice] + assert_equal account, pdf_import.reload.account + assert_equal account, statement.reload.account + end + + test "read only shared user cannot link source statement through pdf import account update" do + account = accounts(:credit_card) + statement = AccountStatement.create_from_upload!( + family: @user.family, + account: nil, + file: uploaded_file( + filename: "statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + pdf_import = PdfImport.create_from_statement!(statement: statement) + + sign_in users(:family_member) + patch import_url(pdf_import), params: { import: { account_id: account.id } } + + assert_redirected_to account_url(account) + assert_equal I18n.t("accounts.not_authorized"), flash[:alert] + assert_nil pdf_import.reload.account + assert_nil statement.reload.account + end + + test "user cannot view statement backed pdf import for inaccessible statement" do + statement = AccountStatement.create_from_upload!( + family: @user.family, + account: accounts(:investment), + file: uploaded_file( + filename: "statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + pdf_import = PdfImport.create_from_statement!(statement: statement) + + sign_in users(:family_member) + get import_url(pdf_import) + + assert_response :not_found + end + + test "read only shared user cannot publish statement backed pdf import" do + account = accounts(:credit_card) + statement = AccountStatement.create_from_upload!( + family: @user.family, + account: account, + file: uploaded_file( + filename: "statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + pdf_import = PdfImport.create_from_statement!(statement: statement) + PdfImport.any_instance.expects(:publish_later).never + + sign_in users(:family_member) + post publish_import_url(pdf_import) + + assert_redirected_to account_url(account) + assert_equal I18n.t("accounts.not_authorized"), flash[:alert] end test "rejects unsupported document type for DocumentImport option" do diff --git a/test/controllers/plaid_items_controller_test.rb b/test/controllers/plaid_items_controller_test.rb index e21c4ebf6..9e171014e 100644 --- a/test/controllers/plaid_items_controller_test.rb +++ b/test/controllers/plaid_items_controller_test.rb @@ -6,6 +6,58 @@ class PlaidItemsControllerTest < ActionDispatch::IntegrationTest sign_in @user = users(:family_admin) end + test "new redirects with friendly alert when Plaid rejects link_token request for unauthorized products" do + # Reproduces issue #1792: the Plaid client account isn't enabled for the + # requested products, so Plaid returns an actionable error message. We + # should surface that message instead of letting the modal frame render + # blank. + plaid_provider = mock + Provider::Registry.stubs(:plaid_provider_for_region).with(:us).returns(plaid_provider) + + error_body = { + "error_code" => "INVALID_PRODUCT", + "error_message" => "Your account is not enabled for the following products: [\"investments\" \"liabilities\" \"transactions\"]. To request access, visit https://dashboard.plaid.com/overview/request-products or contact Sales or your Account Manager." + }.to_json + plaid_provider.expects(:get_link_token).raises( + Plaid::ApiError.new(code: 400, response_body: error_body) + ) + + get new_plaid_item_url(accountable_type: "Investment") + + assert_redirected_to accounts_path + assert_match(/not enabled for the following products/, flash[:alert]) + end + + test "new redirects with generic alert when Plaid raises an unclassified error" do + plaid_provider = mock + Provider::Registry.stubs(:plaid_provider_for_region).with(:us).returns(plaid_provider) + + plaid_provider.expects(:get_link_token).raises( + Plaid::ApiError.new(code: 500, response_body: { "error_code" => "INTERNAL_SERVER_ERROR" }.to_json) + ) + + get new_plaid_item_url + + assert_redirected_to accounts_path + assert_equal I18n.t("plaid_items.errors.link_token_generic"), flash[:alert] + end + + test "edit redirects with friendly alert when Plaid rejects update link_token request" do + plaid_item = plaid_items(:one) + error_body = { + "error_code" => "INVALID_PRODUCT", + "error_message" => "Your account is not enabled for the following products: [\"transactions\"]." + }.to_json + PlaidItem.any_instance.expects(:get_update_link_token).raises( + Plaid::ApiError.new(code: 400, response_body: error_body) + ) + + get edit_plaid_item_url(plaid_item) + + assert_redirected_to accounts_path + assert_match(/not enabled for the following products/, flash[:alert]) + end + test "create" do @plaid_provider = mock Provider::Registry.expects(:plaid_provider_for_region).with("us").returns(@plaid_provider) diff --git a/test/controllers/settings/profiles_controller_test.rb b/test/controllers/settings/profiles_controller_test.rb index 9228a5ab5..fd0e6fc6f 100644 --- a/test/controllers/settings/profiles_controller_test.rb +++ b/test/controllers/settings/profiles_controller_test.rb @@ -59,6 +59,25 @@ class Settings::ProfilesControllerTest < ActionDispatch::IntegrationTest assert User.find(@admin.id) end + test "admin cannot destroy a member who owns accounts in another family" do + other_family = families(:empty) + legacy_account = other_family.accounts.create!( + name: "Legacy savings", balance: 250, currency: "USD", + accountable: Depository.new + ) + legacy_account.update_columns(owner_id: @member.id) + + sign_in @admin + + assert_no_difference("User.count") do + delete settings_profile_path(user_id: @member) + end + + assert_redirected_to settings_profile_path + assert_equal I18n.t("settings.profiles.destroy.member_owns_other_family_data"), flash[:alert] + assert User.find(@member.id), "user row must be preserved so historical access can be restored" + end + test "admin removing a family member also destroys their invitation" do # Create an invitation for the member invitation = @admin.family.invitations.create!( diff --git a/test/jobs/identify_recurring_transactions_job_test.rb b/test/jobs/identify_recurring_transactions_job_test.rb new file mode 100644 index 000000000..22a07ef66 --- /dev/null +++ b/test/jobs/identify_recurring_transactions_job_test.rb @@ -0,0 +1,57 @@ +require "test_helper" + +class IdentifyRecurringTransactionsJobTest < ActiveJob::TestCase + setup do + @family = families(:dylan_family) + @scheduled_at = Time.current.to_f + end + + test "skips identification while a Coinbase provider sync is in flight" do + coinbase_item = @family.coinbase_items.create!( + name: "Coinbase Pro", + api_key: "test-api-key-#{SecureRandom.hex(4)}", + api_secret: "test-api-secret-#{SecureRandom.hex(8)}" + ) + Sync.create!(syncable: coinbase_item, status: :syncing) + + RecurringTransaction::Identifier.any_instance.expects(:identify_recurring_patterns).never + + IdentifyRecurringTransactionsJob.new.perform(@family.id, @scheduled_at) + end + + test "skips identification while a Mercury provider sync is in flight" do + mercury_item = mercury_items(:one) + Sync.create!(syncable: mercury_item, status: :pending) + + RecurringTransaction::Identifier.any_instance.expects(:identify_recurring_patterns).never + + IdentifyRecurringTransactionsJob.new.perform(@family.id, @scheduled_at) + end + + test "runs identification when no provider syncs are in flight" do + # Sanity: there are no incomplete syncs in the fixture set by default. + Sync.for_family(@family).incomplete.find_each(&:destroy) + + RecurringTransaction::Identifier.any_instance.expects(:identify_recurring_patterns).once + + IdentifyRecurringTransactionsJob.new.perform(@family.id, @scheduled_at) + end + + test "skips when family is missing" do + RecurringTransaction::Identifier.any_instance.expects(:identify_recurring_patterns).never + + IdentifyRecurringTransactionsJob.new.perform(SecureRandom.uuid, @scheduled_at) + end + + test "skips when a newer scheduled run supersedes this one" do + # Rails.cache is NullStore in the test env (writes are no-ops), so we stub + # the read directly to simulate a newer scheduled-at landing in the cache + # between this job being enqueued and being picked up. + cache_key = "recurring_transaction_identify:#{@family.id}" + Rails.cache.stubs(:read).with(cache_key).returns(@scheduled_at + 10) + + RecurringTransaction::Identifier.any_instance.expects(:identify_recurring_patterns).never + + assert_nil IdentifyRecurringTransactionsJob.new.perform(@family.id, @scheduled_at) + end +end diff --git a/test/jobs/process_pdf_job_test.rb b/test/jobs/process_pdf_job_test.rb index 29d4a4977..1d9540dcc 100644 --- a/test/jobs/process_pdf_job_test.rb +++ b/test/jobs/process_pdf_job_test.rb @@ -18,12 +18,22 @@ class ProcessPdfJobTest < ActiveJob::TestCase test "skips if PDF not uploaded" do assert_not @import.pdf_uploaded? + @import.update_columns(status: "importing", updated_at: 31.minutes.ago) ProcessPdfJob.perform_now(@import) assert_equal "pending", @import.reload.status end + test "skips if PDF not uploaded without releasing fresh processing claim" do + assert_not @import.pdf_uploaded? + @import.update!(status: :importing) + + ProcessPdfJob.perform_now(@import) + + assert_equal "importing", @import.reload.status + end + test "skips if already processed" do processed_import = imports(:pdf_processed) @@ -33,6 +43,29 @@ class ProcessPdfJobTest < ActiveJob::TestCase assert_equal "complete", processed_import.reload.status end + test "skips already processed importing import and releases processing claim" do + processed_import = imports(:pdf_processed) + attach_pdf!(processed_import) + processed_import.update!(document_type: "financial_document") + processed_import.update_columns(status: "importing", updated_at: 31.minutes.ago) + processed_import.expects(:process_with_ai).never + + ProcessPdfJob.perform_now(processed_import) + + assert_equal "pending", processed_import.reload.status + end + + test "skips already processed importing import without releasing fresh processing claim" do + processed_import = imports(:pdf_processed) + attach_pdf!(processed_import) + processed_import.update!(status: :importing, document_type: "financial_document") + processed_import.expects(:process_with_ai).never + + ProcessPdfJob.perform_now(processed_import) + + assert_equal "importing", processed_import.reload.status + end + test "uploads non-bank PDF to vector store with classified type metadata" do pdf_content = attach_pdf!(@import) process_result = Struct.new(:document_type).new("financial_document") diff --git a/test/models/account/provider_import_adapter_test.rb b/test/models/account/provider_import_adapter_test.rb index a62c30209..29ab62fb8 100644 --- a/test/models/account/provider_import_adapter_test.rb +++ b/test/models/account/provider_import_adapter_test.rb @@ -1445,4 +1445,77 @@ class Account::ProviderImportAdapterTest < ActiveSupport::TestCase pending1.reload assert_equal "plaid_pending_1", pending1.external_id end + + # ========================================================================= + # Same-external-id pending → booked (e.g. Revolut Italy via Enable Banking) + # Some ASPSPs reuse the same transaction_id for pending and booked, so the + # entry is found by find_or_initialize_by (persisted), bypassing auto-claim. + # ========================================================================= + + test "clears pending flag when same external_id is reused for booked version (not user-modified)" do + pending_entry = @adapter.import_transaction( + external_id: "eb_same_id_123", + amount: 30.0, + currency: "EUR", + date: Date.today - 2.days, + name: "Piero Fiorista", + source: "enable_banking", + extra: { "enable_banking" => { "pending" => true } } + ) + assert pending_entry.transaction.pending?, "entry should start as pending" + + # Booked version arrives with the SAME external_id (Revolut Italy behaviour). + # extra is nil (no FX, no MCC) so the deep-merge block would normally be skipped. + assert_no_difference "@account.entries.count" do + booked_entry = @adapter.import_transaction( + external_id: "eb_same_id_123", + amount: 30.0, + currency: "EUR", + date: Date.today, + name: "Piero Fiorista", + source: "enable_banking", + extra: nil + ) + + assert_equal pending_entry.id, booked_entry.id, "should update the same entry" + assert_not booked_entry.transaction.pending?, + "pending flag should be cleared even when extra is nil" + end + end + + test "clears pending flag when same external_id reused and entry is user-modified (Revolut Italy)" do + pending_entry = @adapter.import_transaction( + external_id: "eb_same_id_user_mod", + amount: 50.0, + currency: "EUR", + date: Date.today - 3.days, + name: "Clean Center", + source: "enable_banking", + extra: { "enable_banking" => { "pending" => true } } + ) + assert pending_entry.transaction.pending?, "entry should start as pending" + + # User categorises the pending entry — sets user_modified = true + pending_entry.mark_user_modified! + assert pending_entry.reload.user_modified?, "entry should be marked user-modified" + + # Booked version with the same external_id arrives — protection check would normally + # return early and leave the pending badge intact. + assert_no_difference "@account.entries.count" do + booked_entry = @adapter.import_transaction( + external_id: "eb_same_id_user_mod", + amount: 50.0, + currency: "EUR", + date: Date.today, + name: "Clean Center", + source: "enable_banking", + extra: nil + ) + + assert_equal pending_entry.id, booked_entry.id, "should reference the same entry" + booked_entry.reload + assert_not booked_entry.transaction.pending?, + "pending flag must be cleared even for user-modified entries" + end + end end diff --git a/test/models/assistant/function/get_budget_test.rb b/test/models/assistant/function/get_budget_test.rb new file mode 100644 index 000000000..84fb10adf --- /dev/null +++ b/test/models/assistant/function/get_budget_test.rb @@ -0,0 +1,171 @@ +require "test_helper" + +class Assistant::Function::GetBudgetTest < ActiveSupport::TestCase + setup do + @user = users(:family_admin) + @family = @user.family + @function = Assistant::Function::GetBudget.new(@user) + end + + test "has correct name" do + assert_equal "get_budget", @function.name + end + + test "has a description" do + assert_not_empty @function.description + end + + test "is not in strict mode" do + refute @function.strict_mode? + end + + test "params_schema declares optional month and prior_months" do + schema = @function.params_schema + assert schema[:properties].key?(:month) + assert schema[:properties].key?(:prior_months) + assert_empty schema[:required] + end + + test "returns current month when no month given" do + result = @function.call({}) + + assert_equal @family.currency, result[:currency] + assert_equal 1, result[:months].length + + month = result[:months].first + assert month[:is_current] + assert_equal Date.current.beginning_of_month, month[:period][:start_date] + assert_equal Date.current.end_of_month, month[:period][:end_date] + end + + test "returns N+1 months sorted oldest first when prior_months is set" do + current_start = Date.current.beginning_of_month + 2.times do |i| + prior_start = current_start << (i + 1) + Budget.create!( + family: @family, + start_date: prior_start, + end_date: prior_start.end_of_month, + currency: @family.currency + ) + end + + result = @function.call("prior_months" => 2) + + assert_equal 3, result[:months].length + starts = result[:months].map { |m| m[:period][:start_date] } + assert_equal starts.sort, starts + assert_equal current_start, starts.last + end + + test "does not bootstrap budgets for prior_months that do not exist" do + initial_count = Budget.count + + result = @function.call("prior_months" => 3) + + assert_equal initial_count, Budget.count, "no prior budgets should be created as a side effect" + assert_equal 1, result[:months].length + assert_equal 3, result[:months_unavailable] + end + + test "clamps prior_months above MAX_PRIOR_MONTHS" do + result = @function.call("prior_months" => 99) + considered = result[:months].length + (result[:months_unavailable] || 0) + assert_operator considered, :<=, Assistant::Function::GetBudget::MAX_PRIOR_MONTHS + 1 + end + + test "accepts YYYY-MM month format" do + target = Date.current.beginning_of_month << 1 + result = @function.call("month" => target.strftime("%Y-%m")) + + assert_equal 1, result[:months].length + assert_equal target, result[:months].first[:period][:start_date] + end + + test "accepts MMM-YYYY month format" do + target = Date.current.beginning_of_month << 1 + result = @function.call("month" => target.strftime("%b-%Y").downcase) + + assert_equal target, result[:months].first[:period][:start_date] + end + + test "respects custom month_start_day so slug input roundtrips" do + @family.update!(month_start_day: 15) + target = Date.new(Date.current.year, Date.current.month, 15) - 2.months + slug = target.strftime("%b-%Y").downcase + + result = @function.call("month" => slug) + + assert_equal 1, result[:months].length + month = result[:months].first + assert_equal slug, month[:month] + assert_equal target, month[:period][:start_date] + end + + test "raises on invalid month format" do + assert_raises(Assistant::Error) do + @function.call("month" => "not-a-month") + end + end + + test "rejects month strings with trailing characters" do + [ "2026-05-01", "2026-05foo", "may-2026foo" ].each do |raw| + assert_raises(Assistant::Error, "Expected #{raw.inspect} to be rejected") do + @function.call("month" => raw) + end + end + end + + test "nests subcategories under their parent" do + result = @function.call({}) + categories = result[:months].first[:categories] + + food = categories.find { |c| c[:name] == "Food & Drink" } + assert food, "Food & Drink parent should be present" + sub_names = food[:subcategories].map { |s| s[:name] } + assert_includes sub_names, "Restaurants" + end + + test "category status reflects over_budget helper" do + budget = Budget.find_or_bootstrap(@family, start_date: Date.current.beginning_of_month, user: @user) + food_bc = budget.budget_categories.find { |bc| bc.category == categories(:food_and_drink) } + food_bc.update!(budgeted_spending: 100) + + BudgetCategory.any_instance.stubs(:actual_spending).returns(150) + + result = @function.call({}) + food = result[:months].first[:categories].find { |c| c[:name] == "Food & Drink" } + assert_equal "over_budget", food[:status] + end + + test "suggested_daily_spending omitted on non-current months" do + target = Date.current.beginning_of_month << 1 + result = @function.call("month" => target.strftime("%Y-%m")) + past = result[:months].first + + refute past[:is_current] + past[:categories].each do |cat| + refute cat.key?(:suggested_daily_spending), "Past months should not include suggested_daily_spending" + cat[:subcategories].each do |sub| + refute sub.key?(:suggested_daily_spending), "Past month subcategories should not include suggested_daily_spending" + end + end + end + + test "includes color on parent categories" do + result = @function.call({}) + result[:months].first[:categories].each do |cat| + assert cat.key?(:color), "Each parent category should expose a color" + end + end + + test "totals expose budget pacing fields" do + result = @function.call({}) + totals = result[:months].first[:totals] + + %i[budgeted_spending allocated_spending available_to_allocate actual_spending + available_to_spend percent_of_budget_spent overage_percent].each do |key| + assert totals.key?(key), "totals should include #{key}" + end + end +end diff --git a/test/models/balance/reverse_calculator_test.rb b/test/models/balance/reverse_calculator_test.rb index cee497bc8..11f4463e3 100644 --- a/test/models/balance/reverse_calculator_test.rb +++ b/test/models/balance/reverse_calculator_test.rb @@ -231,6 +231,66 @@ class Balance::ReverseCalculatorTest < ActiveSupport::TestCase ) end + # Regression for #2007: a reconciliation waypoint on a day that ALSO has a transaction. + # The waypoint is an END-of-day balance, so the day's own flow must derive the START + # (waypoint - flow), and the flow must NOT be added on top of the waypoint (double-count). + # Critically, the derived start (not the waypoint) is what carries back, so the prior + # gap day must not show a phantom jump. + test "reconciliation waypoint with a same-day transaction is not double-counted" do + account = create_account_with_ledger( + account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, + entries: [ + { type: "current_anchor", date: Date.current, balance: 20000 }, + { type: "reconciliation", date: 2.days.ago, balance: 17000 }, # Waypoint (end-of-day) + { type: "transaction", date: 2.days.ago, amount: -2000 }, # 2000 deposit ON the waypoint day + { type: "opening_anchor", date: 4.days.ago, balance: 15000 } + ] + ) + + calculated = Balance::ReverseCalculator.new(account).calculate + + assert_calculated_ledger_balances( + calculated_data: calculated, + expected_data: [ + { + date: Date.current, + legacy_balances: { balance: 20000, cash_balance: 20000 }, + balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, + flows: 0, + adjustments: 0 + }, + { + date: 1.day.ago, + legacy_balances: { balance: 20000, cash_balance: 20000 }, + balances: { start: 20000, start_cash: 20000, start_non_cash: 0, end_cash: 20000, end_non_cash: 0, end: 20000 }, + flows: 0, + adjustments: 0 + }, + { + date: 2.days.ago, # End forced to waypoint 17000; start derived from the +2000 deposit = 15000 + legacy_balances: { balance: 17000, cash_balance: 17000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 17000, end_non_cash: 0, end: 17000 }, + flows: { cash_inflows: 2000, cash_outflows: 0 }, + adjustments: 0 + }, + { + date: 3.days.ago, # No phantom jump: stays at 15000 (the deposit is not re-applied here) + legacy_balances: { balance: 15000, cash_balance: 15000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 15000, end_non_cash: 0, end: 15000 }, + flows: 0, + adjustments: 0 + }, + { + date: 4.days.ago, + legacy_balances: { balance: 15000, cash_balance: 15000 }, + balances: { start: 15000, start_cash: 15000, start_non_cash: 0, end_cash: 15000, end_non_cash: 0, end: 15000 }, + flows: 0, + adjustments: 0 + } # Opening anchor + ] + ) + end + # Investment account balances are made of two components: cash and holdings. test "anchors on investment accounts calculate cash balance dynamically based on holdings value" do diff --git a/test/models/balance/waypoint_gui_breakdown_test.rb b/test/models/balance/waypoint_gui_breakdown_test.rb new file mode 100644 index 000000000..0a43c389f --- /dev/null +++ b/test/models/balance/waypoint_gui_breakdown_test.rb @@ -0,0 +1,113 @@ +require "test_helper" + +# Proves what the day-detail "Details" popup (UI::Account::BalanceReconciliation) +# shows for a daily-waypoint (EnableBanking-style) account with a transaction on +# every waypoint day. Persists real balances and reads the actual PG generated +# columns through the real GUI component. +class Balance::WaypointGuiBreakdownTest < ActiveSupport::TestCase + include LedgerTestingHelper + + def persist_and_load(account) + calculated = Balance::ReverseCalculator.new(account).calculate + account.balances.upsert_all( + calculated.map { |b| + b.attributes.slice( + "date", "balance", "cash_balance", "currency", + "start_cash_balance", "start_non_cash_balance", + "cash_inflows", "cash_outflows", + "non_cash_inflows", "non_cash_outflows", + "net_market_flows", "cash_adjustments", "non_cash_adjustments", + "flows_factor" + ).merge("updated_at" => Time.now) + }, + unique_by: %i[account_id date currency] + ) + account.balances.order(:date).to_a + end + + # Reads the three lines exactly as the GUI Details popup renders them. + def gui_lines(balance, account) + items = UI::Account::BalanceReconciliation.new(balance: balance, account: account).reconciliation_items + { + start: items.find { |i| i[:style] == :start }[:value].amount, + net_flow: items.find { |i| i[:style] == :flow }[:value].amount, + final: items.find { |i| i[:style] == :final }[:value].amount + } + end + + def build_account + create_account_with_ledger( + account: { type: Depository, balance: 20000, cash_balance: 20000, currency: "USD" }, + entries: [ + { type: "current_anchor", date: Date.current, balance: 20000 }, + { type: "reconciliation", date: 1.day.ago, balance: 19000 }, + { type: "transaction", date: 1.day.ago, amount: -1000 }, # +1000 deposit + { type: "reconciliation", date: 2.days.ago, balance: 17000 }, + { type: "transaction", date: 2.days.ago, amount: -2000 }, # +2000 deposit + { type: "reconciliation", date: 3.days.ago, balance: 16000 }, + { type: "transaction", date: 3.days.ago, amount: 500 }, # -500 expense + # 4.days.ago is a GAP day: no waypoint, no transaction + { type: "opening_anchor", date: 5.days.ago, balance: 15000 } + ] + ) + end + + # Hard assertions for the fix currently in the tree (derive-start). + test "derive-start fix: Net cash flow preserved on every waypoint day and it reconciles" do + account = build_account + balances = persist_and_load(account).index_by(&:date) + + expectations = { + 1.day.ago.to_date => { start: 18000, net_flow: 1000, final: 19000 }, + 2.days.ago.to_date => { start: 15000, net_flow: 2000, final: 17000 }, + 3.days.ago.to_date => { start: 16500, net_flow: -500, final: 16000 } + } + + expectations.each do |date, exp| + g = gui_lines(balances[date], account) + assert_equal exp[:start], g[:start], "Start balance wrong on #{date}" + assert_equal exp[:net_flow], g[:net_flow], "Net cash flow wrong on #{date} (should NOT be zeroed)" + assert_equal exp[:final], g[:final], "Final balance wrong on #{date}" + assert_equal g[:start] + g[:net_flow], g[:final], "Breakdown does not reconcile on #{date}" + refute_equal 0, g[:net_flow], "Net cash flow was zeroed on #{date} — transaction hidden" + end + end + + # Investment account: reconciliation waypoint on a day with a same-day trade. + # End total must match the API-reported waypoint (not waypoint + trade flows). + # Cash/non-cash split must be preserved and no double-count on the non-cash side. + test "investment reconciliation waypoint with same-day trade is not double-counted" do + account = create_account_with_ledger( + account: { type: Investment, balance: 20000, cash_balance: 10000, currency: "USD" }, + entries: [ + { type: "current_anchor", date: Date.current, balance: 20000 }, + { type: "reconciliation", date: 1.day.ago, balance: 18000 }, # Bank says total was 18000 + { type: "trade", date: 1.day.ago, ticker: "AAPL", qty: 10, price: 100 }, # Buy $1000 of AAPL + { type: "opening_anchor", date: 3.days.ago, balance: 15000 } + ], + holdings: [ + { date: Date.current, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }, + { date: 1.day.ago.to_date, ticker: "AAPL", qty: 10, price: 100, amount: 1000 }, + { date: 2.days.ago.to_date, ticker: "AAPL", qty: 0, price: 100, amount: 0 }, + { date: 3.days.ago.to_date, ticker: "AAPL", qty: 0, price: 100, amount: 0 } + ] + ) + + balances = persist_and_load(account).index_by(&:date) + waypoint = balances[1.day.ago.to_date] + + # End total must equal the API-reported waypoint — not 18000 + trade flows (which would be 20000). + assert_equal 18000, waypoint.end_balance, "Investment waypoint end_balance must equal 18000, not double-count the trade" + + # Cash/non-cash flows from the trade must still be present (not zeroed). + assert waypoint.cash_outflows > 0 || waypoint.non_cash_inflows > 0, + "Trade flows should be preserved on the waypoint day, not zeroed" + + # A trade moves cash → holdings but doesn't change the total balance, so + # the gap day correctly stays at the same total as the waypoint — this is + # expected, not a phantom. The key check is that the waypoint day itself + # didn't inflate above 18000 by double-counting the trade. + gap_day = balances[2.days.ago.to_date] + assert_equal 18000, gap_day.end_balance, "Gap day total should equal pre-trade balance (trade doesn't change total)" + end +end diff --git a/test/models/binance_account/processor_test.rb b/test/models/binance_account/processor_test.rb index 9a08ab158..0e363508e 100644 --- a/test/models/binance_account/processor_test.rb +++ b/test/models/binance_account/processor_test.rb @@ -86,4 +86,120 @@ class BinanceAccount::ProcessorTest < ActiveSupport::TestCase assert_equal "USD", @account.currency assert_in_delta 1000.0, @account.balance, 0.01 end + + test "processes futures trades correctly" do + @family.update!(currency: "USD") + @ba.update!(raw_payload: { "assets" => [ { "symbol" => "BTC", "total" => "1.0" } ] }) + + provider = mock + @item.stubs(:binance_provider).returns(provider) + @ba.stubs(:binance_item).returns(@item) + provider.stubs(:get_spot_trades).returns([]) + provider.stubs(:get_spot_price).returns("50000.0") + provider.stubs(:get_all_p2p_trades).returns([]) # Skip P2P + + # Mock futures trades + provider.stubs(:get_futures_trades).returns([]) + provider.stubs(:get_futures_trades).with("BTCUSDT", limit: 1000, from_id: nil, startTime: nil).returns([ + { "id" => 1, "time" => 1610000000000, "qty" => "0.1", "price" => "40000.0", "quoteQty" => "4000.0", "commission" => "0.0", "commissionAsset" => "USDT", "buyer" => true } + ]) + + Security.create!(ticker: "CRYPTO:BTC", name: "Bitcoin", price_provider: "binance_public") + + assert_difference "Entry.count", 1 do + BinanceAccount::Processor.new(@ba).process + end + + assert @account.entries.exists?(external_id: "binance_futures_BTCUSDT_1") + end + + test "processes P2P BUY trades with double-entry logic and exact native fiat" do + @family.update!(currency: "USD") + @account.update!(currency: "USD") + + provider = mock + @item.stubs(:binance_provider).returns(provider) + @ba.stubs(:binance_item).returns(@item) + + # Silence other importers + provider.stubs(:get_spot_trades).returns([]) + provider.stubs(:get_futures_trades).returns([]) + + # Mock the exact TZS/USDT payload with actual fiat transfer amounts + provider.stubs(:get_all_p2p_trades).returns([ + { + "orderNumber" => "22883918231657005056", + "createTime" => 1777736533166, + "tradeType" => "BUY", + "asset" => "USDT", + "fiat" => "TZS", + "totalPrice" => "31500.00", + "unitPrice" => "2746.29", + "amount" => "11.47", # Gross crypto + "takerAmount" => "11.41", # Net crypto + "takerCommission" => "0.06" # Crypto fee + } + ]) + + Security.create!(ticker: "CRYPTO:USDT", name: "Tether", price_provider: "binance_public") + + # It MUST create 2 entries: 1 Deposit (Transaction) and 1 Purchase (Trade) + assert_difference "Entry.count", 2 do + BinanceAccount::Processor.new(@ba).process + end + + # Verify the Deposit (Transaction) - Should be native fiat + deposit = @account.entries.find_by(external_id: "binance_p2p_22883918231657005056_funding") + assert_not_nil deposit + assert_equal "Transaction", deposit.entryable_type + assert_equal (-31500.00), deposit.amount.to_f # Negative = Fiat Cash INFLOW + assert_equal "TZS", deposit.currency + + # Verify the Buy (Trade) - Should reflect the fiat cost basis + trade = @account.entries.find_by(external_id: "binance_p2p_22883918231657005056") + assert_not_nil trade + assert_equal "Trade", trade.entryable_type + assert_equal 31500.00, trade.amount.to_f # Positive = Fiat Cash OUTFLOW + assert_equal "TZS", trade.currency + assert_equal "Buy", trade.entryable.investment_activity_label + + # Verify the specific crypto math and fiat fee conversion + assert_equal 11.41, trade.entryable.qty.to_f + + # Fiat Fee = Crypto Fee (0.06) * Unit Price (2746.29) = 164.7774 (rounds to 164.78) + assert_equal 164.78, trade.entryable.fee.to_f + end + + test "skips processing if P2P external_id already exists" do + @family.update!(currency: "USD") + @account.update!(currency: "USD") + + # Pre-create the trade in the database + @account.entries.create!( + date: Date.current, + name: "Existing P2P", + amount: 10, + currency: "USD", + external_id: "binance_p2p_existing_123", + entryable: Transaction.new + ) + + provider = mock + @item.stubs(:binance_provider).returns(provider) + @ba.stubs(:binance_item).returns(@item) + provider.stubs(:get_spot_trades).returns([]) + provider.stubs(:get_futures_trades).returns([]) + + # Mock a payload with the SAME orderNumber + provider.stubs(:get_all_p2p_trades).returns([ + { "orderNumber" => "existing_123", "tradeType" => "BUY", "asset" => "USDT", "amount" => "10.0" } + ]) + + Security.create!(ticker: "CRYPTO:USDT", name: "Tether", price_provider: "binance_public") + + # Assert that NO new entries are created + assert_no_difference "Entry.count" do + BinanceAccount::Processor.new(@ba).process + end + end end diff --git a/test/models/binance_item/futures_importer_test.rb b/test/models/binance_item/futures_importer_test.rb new file mode 100644 index 000000000..6ce9dba1a --- /dev/null +++ b/test/models/binance_item/futures_importer_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "test_helper" + +class BinanceItem::FuturesImporterTest < ActiveSupport::TestCase + setup do + @provider = mock + @family = families(:dylan_family) + @item = BinanceItem.create!(family: @family, name: "B", api_key: "k", api_secret: "s") + end + + test "returns normalized assets from USDⓈ-M futures with source=futures" do + @provider.stubs(:get_futures_account).returns({ + "assets" => [ + { "asset" => "USDT", "walletBalance" => "100.0", "unrealizedProfit" => "5.0", "availableBalance" => "90.0" }, + { "asset" => "BUSD", "walletBalance" => "0.0", "unrealizedProfit" => "0.0", "availableBalance" => "0.0" } + ], + "positions" => [ + { "symbol" => "BTCUSDT", "positionAmt" => "0.5" } + ] + }) + + result = BinanceItem::FuturesImporter.new(@item, provider: @provider).import + + assert_equal "futures", result[:source] + assert_equal 1, result[:assets].size + usdt = result[:assets].first + assert_equal "USDT", usdt[:symbol] + assert_equal "105.0", usdt[:total] # walletBalance + unrealizedProfit + assert_equal "90.0", usdt[:free] + assert_equal "10.0", usdt[:locked] # walletBalance - availableBalance + end + + test "returns empty on API error" do + @provider.stubs(:get_futures_account).raises(Provider::Binance::ApiError, "WAF") + + result = BinanceItem::FuturesImporter.new(@item, provider: @provider).import + + assert_equal "futures", result[:source] + assert_equal [], result[:assets] + end +end diff --git a/test/models/binance_item/importer_test.rb b/test/models/binance_item/importer_test.rb index 0e9506155..a38bf60a5 100644 --- a/test/models/binance_item/importer_test.rb +++ b/test/models/binance_item/importer_test.rb @@ -12,6 +12,7 @@ class BinanceItem::ImporterTest < ActiveSupport::TestCase stub_spot_result([ { symbol: "BTC", free: "1.0", locked: "0.0", total: "1.0" } ]) stub_margin_result([]) stub_earn_result([]) + stub_futures_result([]) end test "creates a binance_account of type combined" do @@ -48,6 +49,7 @@ class BinanceItem::ImporterTest < ActiveSupport::TestCase stub_spot_result([]) stub_margin_result([]) stub_earn_result([]) + stub_futures_result([]) assert_no_difference "@item.binance_accounts.count" do BinanceItem::Importer.new(@item, binance_provider: @provider).import @@ -61,6 +63,7 @@ class BinanceItem::ImporterTest < ActiveSupport::TestCase assert ba.raw_payload.key?("spot") assert ba.raw_payload.key?("margin") assert ba.raw_payload.key?("earn") + assert ba.raw_payload.key?("futures") end private @@ -82,4 +85,10 @@ class BinanceItem::ImporterTest < ActiveSupport::TestCase { assets: assets, raw: {}, source: "earn" } ) end + + def stub_futures_result(assets) + BinanceItem::FuturesImporter.any_instance.stubs(:import).returns( + { assets: assets, raw: {}, source: "futures" } + ) + end end diff --git a/test/models/budget_category_test.rb b/test/models/budget_category_test.rb index d6408c530..2486f9a75 100644 --- a/test/models/budget_category_test.rb +++ b/test/models/budget_category_test.rb @@ -255,4 +255,20 @@ class BudgetCategoryTest < ActiveSupport::TestCase @subcategory_inheriting_bc.stubs(:actual_spending).returns(10) assert @subcategory_inheriting_bc.visible_on_track? end + + test "suggested_daily_spending uses budget.end_date for custom month periods" do + @family.update!(month_start_day: 15) + + # Today (Jun 1) is in the calendar month after the budget period start (May 15). + # The pre-fix helper compared start_date.month to Date.current.month and returned nil here. + travel_to Date.new(2026, 6, 1) do + @budget.update!(start_date: Date.new(2026, 5, 15), end_date: Date.new(2026, 6, 14)) + @parent_budget_category.stubs(:actual_spending).returns(0) + + suggestion = @parent_budget_category.suggested_daily_spending + + assert suggestion, "expected suggested_daily_spending when current period spans calendar months" + assert_equal 14, suggestion[:days_remaining] + end + end end diff --git a/test/models/category/merger_test.rb b/test/models/category/merger_test.rb new file mode 100644 index 000000000..46e9803cc --- /dev/null +++ b/test/models/category/merger_test.rb @@ -0,0 +1,100 @@ +require "test_helper" + +class Category::MergerTest < ActiveSupport::TestCase + setup do + @family = families(:dylan_family) + @other_family = families(:empty) + end + + test "merge only reassigns and deletes categories inside the current family" do + target = create_category(@family, "Cross Family Merge Target") + source = create_category(@family, "Cross Family Merge Source") + source_child = create_category(@family, "Cross Family Merge Child", parent: source) + transaction = create_transaction_for(@family, source) + budget = create_budget(@family, 1.month.from_now.to_date.beginning_of_month) + target_budget_category = create_budget_category(budget, target, 8) + source_budget_category = create_budget_category(budget, source, 12) + + other_target = create_category(@other_family, target.name) + other_source = create_category(@other_family, source.name) + other_child = create_category(@other_family, source_child.name, parent: other_source) + other_transaction = create_transaction_for(@other_family, other_source) + other_budget = create_budget(@other_family, 1.month.from_now.to_date.beginning_of_month) + other_source_budget_category = create_budget_category(other_budget, other_source, 30) + + other_family_snapshot = -> { + { + target_exists: Category.exists?(other_target.id), + source_exists: Category.exists?(other_source.id), + child_parent_id: other_child.reload.parent_id, + transaction_category_id: other_transaction.reload.category_id, + source_budgeted_spending: other_source_budget_category.reload.budgeted_spending + } + } + + assert_no_changes other_family_snapshot do + merger = Category::Merger.new( + family: @family, + target_category: target, + source_categories: [ source ] + ) + + assert merger.merge! + end + + assert_equal target.id, transaction.reload.category_id + assert_equal target.id, source_child.reload.parent_id + assert_equal 20.to_d, target_budget_category.reload.budgeted_spending + assert_not BudgetCategory.exists?(source_budget_category.id) + assert_not Category.exists?(source.id) + end + + private + def create_category(family, name, parent: nil) + family.categories.create!( + name: name, + color: "#000000", + lucide_icon: "shapes", + parent: parent + ) + end + + def create_transaction_for(family, category) + transaction = Transaction.create!(category: category) + Entry.create!( + account: account_for(family), + entryable: transaction, + name: "#{category.name} transaction", + date: Date.current, + amount: 10, + currency: family.currency || "USD" + ) + + transaction + end + + def account_for(family) + family.accounts.first || family.accounts.create!( + accountable: Depository.create!(subtype: "checking"), + name: "#{family.name} Checking", + balance: 0, + currency: family.currency || "USD" + ) + end + + def create_budget(family, start_date) + family.budgets.create!( + start_date: start_date, + end_date: start_date.end_of_month, + currency: family.currency || "USD" + ) + end + + def create_budget_category(budget, category, budgeted_spending) + budget.budget_categories.create!( + category: category, + budgeted_spending: budgeted_spending, + currency: budget.currency + ) + end +end diff --git a/test/models/holding/materializer_test.rb b/test/models/holding/materializer_test.rb index 605f00478..e975de3cd 100644 --- a/test/models/holding/materializer_test.rb +++ b/test/models/holding/materializer_test.rb @@ -155,6 +155,203 @@ class Holding::MaterializerTest < ActiveSupport::TestCase assert_equal [ account_provider.id ], today_holdings.pluck(:account_provider_id) end + test "carries forward provider cost_basis to calculated rows past the provider snapshot date" do + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "USD") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + # Provider snapshot two days ago with known cost basis, but no trades. + # This mirrors IBKR Flex where the export ends on Friday but today is Sunday. + Holding.create!( + account: @account, + security: @aapl, + qty: 10, + price: 200, + amount: 2000, + currency: "USD", + date: 2.days.ago.to_date, + account_provider: account_provider, + cost_basis: BigDecimal("125.50"), + cost_basis_source: "provider" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_nil today_holding.account_provider_id, + "Today's row is calculated, not a provider snapshot" + assert_equal BigDecimal("125.50"), today_holding.cost_basis, + "Today's calculated row should inherit the provider's cost_basis so trend/return calcs work" + assert_equal "provider", today_holding.cost_basis_source + end + + test "does not overwrite an existing calculated cost_basis with provider carry-forward" do + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "USD") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + Holding.create!( + account: @account, + security: @aapl, + qty: 10, + price: 200, + amount: 2000, + currency: "USD", + date: 2.days.ago.to_date, + account_provider: account_provider, + cost_basis: BigDecimal("125.50"), + cost_basis_source: "provider" + ) + + # Pre-existing calculated row for today (e.g., from a prior trade-derived run) + Holding.create!( + account: @account, + security: @aapl, + qty: 10, + price: 200, + amount: 2000, + currency: "USD", + date: Date.current, + cost_basis: BigDecimal("180.00"), + cost_basis_source: "calculated" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_equal BigDecimal("180.00"), today_holding.cost_basis, + "Existing calculated cost_basis must beat provider carry-forward" + assert_equal "calculated", today_holding.cost_basis_source + end + + test "refreshes stale provider carry-forward when a newer provider snapshot arrives" do + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "USD") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + # With no entries, start_date = yesterday, so materializer only descends to + # yesterday. Use an older date so the second snapshot doesn't land on a date + # the materializer already owns. + Holding.create!( + account: @account, security: @aapl, qty: 10, price: 200, amount: 2000, + currency: "USD", date: 5.days.ago.to_date, + account_provider: account_provider, + cost_basis: BigDecimal("100.00"), cost_basis_source: "provider" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_equal BigDecimal("100.00"), today_holding.cost_basis + + # Provider publishes a newer snapshot with an updated cost_basis on a date + # that falls outside the materializer's window (older than start_date). + Holding.create!( + account: @account, security: @aapl, qty: 10, price: 210, amount: 2100, + currency: "USD", date: 3.days.ago.to_date, + account_provider: account_provider, + cost_basis: BigDecimal("150.00"), cost_basis_source: "provider" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding.reload + assert_equal BigDecimal("150.00"), today_holding.cost_basis, + "Carry-forward should update to the newer provider snapshot value" + assert_equal "provider", today_holding.cost_basis_source + end + + test "carry-forward is a no-op for forward-strategy accounts without provider holdings" do + create_trade(@aapl, account: @account, qty: 5, price: 200, date: Date.current) + + assert_nothing_raised do + Holding::Materializer.new(@account, strategy: :forward).materialize_holdings + end + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_equal "calculated", today_holding.cost_basis_source + assert_equal BigDecimal("200.00"), today_holding.cost_basis, + "Forward strategy with no provider rows should compute cost_basis from trades normally" + end + + test "does not overwrite a zero-valued manual cost_basis with provider carry-forward" do + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "USD") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + Holding.create!( + account: @account, security: @aapl, + qty: 10, price: 200, amount: 2000, currency: "USD", + date: 2.days.ago.to_date, + account_provider: account_provider, + cost_basis: BigDecimal("125.50"), cost_basis_source: "provider" + ) + + # Free shares: legitimate zero-cost basis recorded manually + Holding.create!( + account: @account, security: @aapl, + qty: 10, price: 200, amount: 2000, currency: "USD", + date: Date.current, + cost_basis: BigDecimal("0"), cost_basis_source: "manual" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_equal BigDecimal("0"), today_holding.cost_basis, + "Zero-valued manual cost_basis (e.g., free shares) must not be overwritten by provider carry-forward" + assert_equal "manual", today_holding.cost_basis_source + end + + test "carry-forward converts provider cost_basis currency when provider and calculated currencies differ" do + snap_date = 2.days.ago.to_date + ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: snap_date, rate: 1.2) + + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "EUR") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + Holding.create!( + account: @account, security: @aapl, + qty: 10, price: 200, amount: 2000, currency: "EUR", + date: snap_date, + account_provider: account_provider, + cost_basis: BigDecimal("100.00"), cost_basis_source: "provider" + ) + + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_in_delta BigDecimal("120.00"), today_holding.cost_basis, BigDecimal("0.01"), + "Provider cost_basis in EUR should be converted to USD at the snapshot-date exchange rate" + assert_equal "provider", today_holding.cost_basis_source + end + + test "carry-forward skips provider cost_basis when FX conversion raises Money::ConversionError" do + snap_date = 2.days.ago.to_date + # No ExchangeRate created — EUR→USD conversion will raise Money::ConversionError + + coinstats_item = @family.coinstats_items.create!(name: "CoinStats", api_key: "test-key") + coinstats_account = coinstats_item.coinstats_accounts.create!(name: "Brokerage", currency: "EUR") + account_provider = AccountProvider.create!(account: @account, provider: coinstats_account) + + Holding.create!( + account: @account, security: @aapl, + qty: 10, price: 200, amount: 2000, currency: "EUR", + date: snap_date, + account_provider: account_provider, + cost_basis: BigDecimal("100.00"), cost_basis_source: "provider" + ) + + assert_nothing_raised do + Holding::Materializer.new(@account, strategy: :reverse).materialize_holdings + end + + today_holding = @account.holdings.find_by!(security: @aapl, date: Date.current, currency: "USD") + assert_nil today_holding.cost_basis, + "Carry-forward should be skipped gracefully when currency conversion fails" + end + test "preserves same-day non-provider holdings for securities absent from the provider snapshot" do ExchangeRate.create!(from_currency: "EUR", to_currency: "USD", date: Date.current, rate: 1.2) diff --git a/test/models/investment_statement_test.rb b/test/models/investment_statement_test.rb index 3bb50dff8..064553969 100644 --- a/test/models/investment_statement_test.rb +++ b/test/models/investment_statement_test.rb @@ -177,6 +177,51 @@ class InvestmentStatementTest < ActiveSupport::TestCase assert_in_delta 3980, trend.previous.amount, 0.001 end + test "period_return_trend returns nil when no balance data in period" do + period = Period.custom(start_date: 10.years.ago.to_date, end_date: 9.years.ago.to_date) + assert_nil @statement.period_return_trend(period: period) + end + + test "period_return_trend returns nil when start portfolio value is zero" do + account = create_investment_account(balance: 5000) + period = Period.custom(start_date: Date.current.beginning_of_month, end_date: Date.current) + # Balance only inside the period — nothing strictly before period_start means start_value = 0 + account.balances.create!( + date: period.date_range.begin, + balance: 5000, + currency: @family.currency, + net_market_flows: 200 + ) + assert_nil @statement.period_return_trend(period: period) + end + + test "period_return_trend returns Trend with correct absolute and percent return" do + account = create_investment_account(balance: 10_500) + period = Period.custom(start_date: Date.current.beginning_of_month, end_date: Date.current) + + # Pre-period row: start_non_cash_balance drives end_balance (virtual stored column) + account.balances.create!( + date: period.date_range.begin - 1.day, + balance: 10_000, + currency: @family.currency, + start_non_cash_balance: 10_000, + net_market_flows: 0 + ) + # In-period row: 500 of market gains + account.balances.create!( + date: period.date_range.begin, + balance: 10_500, + currency: @family.currency, + start_non_cash_balance: 10_000, + net_market_flows: 500 + ) + + trend = @statement.period_return_trend(period: period) + assert_not_nil trend + assert_in_delta 500, trend.value.amount, 1 + assert_in_delta 5.0, trend.percent, 0.1 + end + private def create_investment_account(balance:, cash_balance: 0, currency: "USD") @family.accounts.create!( diff --git a/test/models/invitation_test.rb b/test/models/invitation_test.rb index 9895538e8..f54e63d4d 100644 --- a/test/models/invitation_test.rb +++ b/test/models/invitation_test.rb @@ -122,6 +122,65 @@ class InvitationTest < ActiveSupport::TestCase assert_includes invitation.errors[:email], "has already been invited to this family" end + test "accept_for refuses when invitee owns accounts that would be orphaned" do + owner = users(:empty) + owner_family = families(:empty) + owner.update_columns(family_id: owner_family.id, role: "admin") + account = owner_family.accounts.create!( + name: "Prior savings", balance: 100, currency: "USD", + accountable: Depository.new + ) + account.update_columns(owner_id: owner.id) + + invitation = @family.invitations.create!(email: owner.email, role: "member", inviter: @inviter) + + result = invitation.accept_for(owner) + + assert_not result, "accept_for must refuse to rehome a user away from accounts they own" + owner.reload + assert_equal owner_family.id, owner.family_id, "user.family_id must not be silently overwritten" + invitation.reload + assert_nil invitation.accepted_at, "invitation must remain pending so a new flow can recover" + assert owner_family.accounts.exists?, "original family's accounts must remain intact" + end + + test "accept_for allows a member who owns no accounts to join another family" do + member = users(:empty) + other_owner = users(:sure_support_staff) + source_family = families(:empty) + member.update_columns(family_id: source_family.id, role: "member") + other_owner.update_columns(family_id: source_family.id, role: "admin") + account = source_family.accounts.create!( + name: "Shared savings", balance: 100, currency: "USD", + accountable: Depository.new + ) + account.update_columns(owner_id: other_owner.id) + + invitation = @family.invitations.create!(email: member.email, role: "member", inviter: @inviter) + + result = invitation.accept_for(member) + + assert result, "a non-owner member must be free to join another family" + member.reload + assert_equal @family.id, member.family_id + end + + test "would_orphan_owned_accounts? is false when invitee owns no accounts" do + user = users(:empty) + user.update_columns(family_id: families(:empty).id, role: "admin") + invitation = @family.invitations.create!(email: user.email, role: "member", inviter: @inviter) + + assert_not invitation.would_orphan_owned_accounts?(user) + end + + test "would_orphan_owned_accounts? is false when same-family role change" do + user = users(:family_member) + user.update!(family_id: @family.id, role: "member") + invitation = @family.invitations.create!(email: user.email, role: "admin", inviter: @inviter) + + assert_not invitation.would_orphan_owned_accounts?(user) + end + test "accept_for applies guest role defaults" do user = users(:family_member) user.update!( diff --git a/test/models/pdf_import_test.rb b/test/models/pdf_import_test.rb index e6d89bfe2..df4ebdc91 100644 --- a/test/models/pdf_import_test.rb +++ b/test/models/pdf_import_test.rb @@ -13,6 +13,16 @@ class PdfImportTest < ActiveSupport::TestCase assert_not @import.pdf_uploaded? end + test "pdf_uploaded? returns true for statement backed import" do + statement = create_pdf_statement + import = PdfImport.create_from_statement!(statement: statement) + + assert import.pdf_uploaded? + assert import.statement_backed? + assert_equal statement.original_file.download, import.pdf_file_content + assert_equal statement.filename, import.pdf_filename + end + test "ai_processed? returns false when no summary present" do assert_not @import.ai_processed? end @@ -77,9 +87,40 @@ class PdfImportTest < ActiveSupport::TestCase end test "process_with_ai_later enqueues ProcessPdfJob" do - assert_enqueued_with job: ProcessPdfJob, args: [ @import ] do - @import.process_with_ai_later + import = PdfImport.create_from_statement!(statement: create_pdf_statement) + + assert_enqueued_with job: ProcessPdfJob, args: [ import ] do + assert import.process_with_ai_later end + + assert_equal "importing", import.reload.status + end + + test "process_with_ai_later does not enqueue duplicate jobs while importing" do + import = PdfImport.create_from_statement!(statement: create_pdf_statement) + + assert_enqueued_jobs 1, only: ProcessPdfJob do + assert import.process_with_ai_later + assert_not import.reload.process_with_ai_later + end + + assert_equal "importing", import.reload.status + end + + test "process_with_ai_later does not claim import without pdf content" do + assert_no_enqueued_jobs only: ProcessPdfJob do + assert_not @import.process_with_ai_later + end + + assert_equal "pending", @import.reload.status + end + + test "process_with_ai_later resets pending when enqueue fails" do + import = PdfImport.create_from_statement!(statement: create_pdf_statement) + ProcessPdfJob.stubs(:perform_later).raises(StandardError, "queue offline") + + assert_not import.process_with_ai_later + assert_equal "pending", import.reload.status end test "generate_rows_from_extracted_data creates import rows" do @@ -163,4 +204,111 @@ class PdfImportTest < ActiveSupport::TestCase assert_not ActiveStorage::Attachment.exists?(attachment_id) end + + test "destroying statement backed import keeps statement file" do + statement = create_pdf_statement + import = PdfImport.create_from_statement!(statement: statement) + attachment_id = statement.original_file.id + + perform_enqueued_jobs do + import.destroy! + end + + assert ActiveStorage::Attachment.exists?(attachment_id) + end + + test "statement backed import prevents source statement destroy" do + statement = create_pdf_statement + import = PdfImport.create_from_statement!(statement: statement) + + assert_no_difference "AccountStatement.count" do + assert_not statement.destroy + end + + assert_equal statement, import.reload.account_statement + end + + test "statement backed import memoizes pdf content" do + statement = create_pdf_statement + import = PdfImport.create_from_statement!(statement: statement) + statement.original_file.expects(:download).once.returns("%PDF-test") + + assert_equal "%PDF-test", import.pdf_file_content + assert_equal "%PDF-test", import.pdf_file_content + end + + test "statement backed import reuse requires current account and date format" do + statement = create_pdf_statement + stale_import = PdfImport.create_from_statement!(statement: statement) + formats = Family::DATE_FORMATS.map(&:last) + alternate_date_format = (formats - [ statement.family.date_format ]).first || "#{statement.family.date_format}-alternate" + stale_import.update!(account: nil, date_format: alternate_date_format) + + fresh_import = PdfImport.create_from_statement!(statement: statement) + + assert_not_equal stale_import, fresh_import + assert_equal statement.account, fresh_import.account + assert_equal statement.family.date_format, fresh_import.date_format + end + + test "statement backed import reuses matching reusable import" do + statement = create_pdf_statement + existing_import = PdfImport.create_from_statement!(statement: statement) + + assert_equal existing_import, PdfImport.create_from_statement!(statement: statement) + end + + test "assigning account links statement backed import statement" do + statement = create_pdf_statement(account: nil) + import = PdfImport.create_from_statement!(statement: statement) + account = accounts(:depository) + + import.assign_account!(account) + + assert_equal account, import.reload.account + assert_equal account, statement.reload.account + assert statement.linked? + end + + test "statement backed import requires pdf statement" do + csv_statement = AccountStatement.create_from_upload!( + family: @import.family, + account: nil, + file: uploaded_file(filename: "statement.csv", content_type: "text/csv", content: "date,amount\n2024-01-01,1\n") + ) + import = PdfImport.new(family: @import.family, account_statement: csv_statement) + + assert_not import.valid? + assert import.errors[:account_statement].present? + end + + test "statement backed import requires statement from same family" do + statement = AccountStatement.create_from_upload!( + family: families(:empty), + account: nil, + file: uploaded_file( + filename: "other_family_statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + import = PdfImport.new(family: @import.family, account_statement: statement) + + assert_not import.valid? + assert import.errors[:account_statement].present? + end + + private + + def create_pdf_statement(account: accounts(:depository)) + AccountStatement.create_from_upload!( + family: @import.family, + account: account, + file: uploaded_file( + filename: "sample_bank_statement.pdf", + content_type: "application/pdf", + content: file_fixture("imports/sample_bank_statement.pdf").binread + ) + ) + end end diff --git a/test/models/plaid_item_test.rb b/test/models/plaid_item_test.rb index b0d55bb5d..214a5a1dc 100644 --- a/test/models/plaid_item_test.rb +++ b/test/models/plaid_item_test.rb @@ -44,4 +44,46 @@ class PlaidItemTest < ActiveSupport::TestCase @plaid_item.destroy end end + + test "get_update_link_token marks item as requires_update and returns nil on ITEM_NOT_FOUND" do + error_response = { "error_code" => "ITEM_NOT_FOUND", "error_message" => "not found" }.to_json + Family.any_instance.expects(:get_link_token).raises( + Plaid::ApiError.new(code: 400, response_body: error_response) + ) + + result = @plaid_item.get_update_link_token(webhooks_url: "https://x", redirect_url: "https://x") + + assert_nil result + assert_predicate @plaid_item.reload, :requires_update? + end + + test "get_update_link_token re-raises other Plaid errors so the controller can surface them" do + # Issue #1792: silently swallowing all Plaid errors here is what made the + # "modal closes with nothing happening" experience so opaque. + error_response = { "error_code" => "INVALID_PRODUCT", "error_message" => "Your account is not enabled..." }.to_json + Family.any_instance.expects(:get_link_token).raises( + Plaid::ApiError.new(code: 400, response_body: error_response) + ) + + assert_raises(Plaid::ApiError) do + @plaid_item.get_update_link_token(webhooks_url: "https://x", redirect_url: "https://x") + end + assert_predicate @plaid_item.reload, :good? + end + + test "get_update_link_token tolerates a Plaid::ApiError with a nil/blank response_body" do + # Plaid clients have been observed raising ApiError without a response + # body (network-layer failures, early aborts). The old JSON.parse would + # blow up with TypeError before the rescue could fire; we now coerce + # to String so the parse falls back to {} and the error re-raises + # cleanly for the controller to handle. + Family.any_instance.expects(:get_link_token).raises( + Plaid::ApiError.new(code: 500, response_body: nil) + ) + + assert_raises(Plaid::ApiError) do + @plaid_item.get_update_link_token(webhooks_url: "https://x", redirect_url: "https://x") + end + assert_predicate @plaid_item.reload, :good? + end end diff --git a/test/models/sync_test.rb b/test/models/sync_test.rb index 6fa8be3c9..92dcba289 100644 --- a/test/models/sync_test.rb +++ b/test/models/sync_test.rb @@ -234,6 +234,29 @@ class SyncTest < ActiveSupport::TestCase assert_equal syncs.map(&:id).sort, Sync.for_family(family).where(id: syncs.map(&:id)).pluck(:id).sort end + test "any_incomplete_for? fires on a Sync against any Syncable provider item association" do + family = families(:dylan_family) + Sync.for_family(family).incomplete.find_each(&:destroy) + assert_not Sync.any_incomplete_for?(family) + + mercury_item = mercury_items(:one) + incomplete = Sync.create!(syncable: mercury_item, status: :pending) + assert Sync.any_incomplete_for?(family), + "any_incomplete_for? should report true for an in-flight Mercury sync" + + incomplete.update!(status: :completed) + assert_not Sync.any_incomplete_for?(family) + end + + test "any_incomplete_for? fires on a Sync against the family itself" do + family = families(:dylan_family) + Sync.for_family(family).incomplete.find_each(&:destroy) + assert_not Sync.any_incomplete_for?(family) + + Sync.create!(syncable: family, status: :syncing) + assert Sync.any_incomplete_for?(family) + end + test "api error payload is present for failed syncs without raw error text" do sync = Sync.create!(syncable: accounts(:depository), status: :failed)