@@ -411,7 +411,7 @@ export default class extends Controller {
datum.trend.value === 0
? ``
: `
-
+
${this._extractFormattedValue(datum.trend.value)} (${datum.trend.percent_formatted})
`
diff --git a/app/javascript/utils/chart_tooltip.js b/app/javascript/utils/chart_tooltip.js
index 3b3b6f145..228095602 100644
--- a/app/javascript/utils/chart_tooltip.js
+++ b/app/javascript/utils/chart_tooltip.js
@@ -11,8 +11,22 @@
// Not to be confused with DS::Tooltip — that is the info-icon hint primitive
// (bg-inverse, aria-describedby, anchored to a static trigger). This is a
// data-card surface created and updated inside D3 handler code.
+// The surface itself lives in the design system as `.chart-tooltip`
+// (sure-design-system/components.css): container bg, 10px radius, 12x14
+// padding, hairline ring composed with a soft 8/24 drop shadow, 80ms
+// left/top glide. It's a component class because Tailwind shadow utilities
+// can't compose a ring with a custom drop shadow. This constant adds the
+// behavioural classes shared by every chart tooltip.
export const CHART_TOOLTIP_CLASSES =
- "bg-container text-primary text-sm font-sans absolute p-2 border border-secondary rounded-lg pointer-events-none z-50 privacy-sensitive";
+ "chart-tooltip text-primary text-sm font-sans absolute pointer-events-none z-50 privacy-sensitive";
+
+// Content conventions (kept here so the controllers stay aligned):
+// - context line (date / node title): `text-xs text-secondary mb-1`
+// - money / numeric figures: tabular-nums so digits don't jitter while the
+// scrubber moves (sans, not mono — the app's money convention everywhere
+// else); secondary parentheticals in `text-secondary`
+export const CHART_TOOLTIP_CONTEXT_CLASSES = "text-xs text-secondary mb-1";
+export const CHART_TOOLTIP_VALUE_CLASSES = "font-medium tabular-nums";
// Convenience factory for the raw-DOM idiom (no d3.select). Creates a hidden
// tooltip div carrying the shared contract and appends it to `parent`.
diff --git a/app/models/account/reconciliation_manager.rb b/app/models/account/reconciliation_manager.rb
index 774e9b330..8d2a3331e 100644
--- a/app/models/account/reconciliation_manager.rb
+++ b/app/models/account/reconciliation_manager.rb
@@ -9,10 +9,14 @@ class Account::ReconciliationManager
def reconcile_balance(balance:, date: Date.current, dry_run: false, existing_valuation_entry: nil)
old_balance_components = old_balance_components(reconciliation_date: date, existing_valuation_entry: existing_valuation_entry)
prepared_valuation = prepare_reconciliation(balance, date, existing_valuation_entry)
+ # Captured before save!: the amount this valuation already had on disk
+ # (nil when this reconciliation creates it). See valuation_contribution.
+ prior_valuation_amount = prepared_valuation.amount_in_database
unless dry_run
prepared_valuation.save!
- GoalPledge::Reconciler.new(prepared_valuation).run
+ contribution = valuation_contribution(prepared_valuation, prior_valuation_amount, old_balance_components)
+ GoalPledge::Reconciler.new(prepared_valuation, valuation_delta: contribution).run
end
ReconciliationResult.new(
@@ -42,6 +46,40 @@ class Account::ReconciliationManager
keyword_init: true
)
+ # Contribution recorded by this reconciliation: how much the balance moved
+ # vs. the prior balance. This (not the full new balance) is what a
+ # manual_save GoalPledge matches against.
+ #
+ # The prior balance is resolved in freshness order:
+ # 1. The valuation's own pre-save amount, when this reconciliation
+ # updates an existing valuation. The balances table recomputes
+ # asynchronously (sync_later fires after this manager returns), so a
+ # same-date re-reconcile racing that sync would otherwise read the
+ # pre-first-reconcile row and over- or under-state the delta — an
+ # overstated delta could wrongly close a larger pledge, which never
+ # self-heals. The valuation's own prior amount is immune to that
+ # race, and once the sync lands the two sources are identical (the
+ # valuation anchors that date's end_balance).
+ # 2. The balances-table row for the date (first reconcile on a date).
+ # This can still be stale in one residual window: a reconcile on a
+ # NEW date racing the previous date's pending sync reads a
+ # carried-forward row. A missed match self-heals on the next re-save
+ # — the pledge stays open and retryable until `expires_at`, and the
+ # date/amount tolerance in GoalPledge#matches? accepts the retry.
+ # 3. 0, for a brand-new account with no balance record yet, so the
+ # first reconciliation's full balance is its contribution.
+ #
+ # The delta is only ever consumed for goal-linked accounts, which Goal
+ # validates to be Depository assets (Goal#linked_accounts_must_be_depository).
+ # Balances there are positive, so a save (deposit) is a positive delta and
+ # the reconciler's positive-delta guard is correct. There is no liability
+ # sign concern: a credit-card/loan paydown can't reach pledge matching
+ # because no manual_save pledge can be attached to a non-depository account.
+ def valuation_contribution(valuation, prior_valuation_amount, old_balance_components)
+ prior_balance = prior_valuation_amount || old_balance_components[:balance] || 0
+ valuation.amount.to_d - prior_balance.to_d
+ end
+
def prepare_reconciliation(balance, date, existing_valuation)
valuation_record = existing_valuation ||
account.entries.valuations.find_by(date: date) || # In case of conflict, where existing valuation is not passed as arg, but one exists
diff --git a/app/models/assistant/function/search_family_files.rb b/app/models/assistant/function/search_family_files.rb
index c9c917f0a..3f5f914ff 100644
--- a/app/models/assistant/function/search_family_files.rb
+++ b/app/models/assistant/function/search_family_files.rb
@@ -71,7 +71,10 @@ class Assistant::Function::SearchFamilyFiles < Assistant::Function
return {
success: false,
error: "provider_not_configured",
- message: "No vector store is configured. Set VECTOR_STORE_PROVIDER or configure OpenAI."
+ message: "No vector store is configured. Set VECTOR_STORE_PROVIDER " \
+ "(openai | pgvector | qdrant), configure OpenAI, or — for " \
+ "Anthropic-only installs — enable the pgvector adapter and " \
+ "point EMBEDDING_URI_BASE at an embeddings endpoint."
}
end
diff --git a/app/models/coinstats_item/importer.rb b/app/models/coinstats_item/importer.rb
index b3e35250d..8c2075daf 100644
--- a/app/models/coinstats_item/importer.rb
+++ b/app/models/coinstats_item/importer.rb
@@ -3,6 +3,12 @@
class CoinstatsItem::Importer
include CoinstatsTransactionIdentifiable
+ FETCH_FAILED = Object.new.freeze
+ DEFI_UNSUPPORTED_BLOCKCHAINS = %w[
+ bitcoin btc litecoin ltc dogecoin doge ripple xrp stellar xlm
+ cardano ada polkadot dot cosmos atom
+ ].freeze
+
attr_reader :coinstats_item, :coinstats_provider
# @param coinstats_item [CoinstatsItem] Item containing accounts to import
@@ -43,6 +49,19 @@ class CoinstatsItem::Importer
bulk_balance_data = fetch_balances_for_accounts(wallet_accounts)
bulk_transactions_data = fetch_transactions_for_accounts(wallet_accounts)
+
+ if fetch_failed?(bulk_balance_data) || fetch_failed?(bulk_transactions_data)
+ accounts_failed += wallet_accounts.size
+ Rails.logger.warn "CoinstatsItem::Importer - Wallet bulk fetch failed; preserving existing wallet snapshots for item #{coinstats_item.id}"
+
+ return {
+ success: false,
+ accounts_updated: 0,
+ accounts_failed: accounts_failed,
+ transactions_imported: 0
+ }
+ end
+
portfolio_coins_data = fetch_portfolio_coins_for_exchange(exchange_accounts)
portfolio_transactions_data = fetch_portfolio_transactions_for_exchange(exchange_accounts)
@@ -110,7 +129,7 @@ class CoinstatsItem::Importer
# Fetch balance data for all linked accounts using the bulk endpoint
# @param linked_accounts [Array] Accounts to fetch balances for
- # @return [Array, nil] Bulk balance data, or nil on error
+ # @return [Array, Object] Bulk balance data, or FETCH_FAILED on error
def fetch_balances_for_accounts(linked_accounts)
# Extract unique wallet addresses and blockchains
wallets = linked_accounts.filter_map do |account|
@@ -122,16 +141,16 @@ class CoinstatsItem::Importer
{ address: address, blockchain: blockchain }
end.uniq { |w| [ w[:address].downcase, w[:blockchain].downcase ] }
- return nil if wallets.empty?
+ return [] if wallets.empty?
Rails.logger.info "CoinstatsItem::Importer - Fetching balances for #{wallets.size} wallet(s) via bulk endpoint"
# Build comma-separated string in format "blockchain:address"
wallets_param = wallets.map { |w| "#{w[:blockchain]}:#{w[:address]}" }.join(",")
response = coinstats_provider.get_wallet_balances(wallets_param)
- response.success? ? response.data : nil
+ response.success? ? response.data : FETCH_FAILED
rescue => e
Rails.logger.warn "CoinstatsItem::Importer - Bulk balance fetch failed: #{e.message}"
- nil
+ FETCH_FAILED
end
def fetch_portfolio_coins_for_exchange(linked_accounts)
@@ -148,7 +167,7 @@ class CoinstatsItem::Importer
# Fetch transaction data for all linked accounts using the bulk endpoint
# @param linked_accounts [Array] Accounts to fetch transactions for
- # @return [Array, nil] Bulk transaction data, or nil on error
+ # @return [Array, Object] Bulk transaction data, or FETCH_FAILED on error
def fetch_transactions_for_accounts(linked_accounts)
# Extract unique wallet addresses and blockchains
wallets = linked_accounts.filter_map do |account|
@@ -160,16 +179,16 @@ class CoinstatsItem::Importer
{ address: address, blockchain: blockchain }
end.uniq { |w| [ w[:address].downcase, w[:blockchain].downcase ] }
- return nil if wallets.empty?
+ return [] if wallets.empty?
Rails.logger.info "CoinstatsItem::Importer - Fetching transactions for #{wallets.size} wallet(s) via bulk endpoint"
# Build comma-separated string in format "blockchain:address"
wallets_param = wallets.map { |w| "#{w[:blockchain]}:#{w[:address]}" }.join(",")
response = coinstats_provider.get_wallet_transactions(wallets_param)
- response.success? ? response.data : nil
+ response.success? ? response.data : FETCH_FAILED
rescue => e
Rails.logger.warn "CoinstatsItem::Importer - Bulk transaction fetch failed: #{e.message}"
- nil
+ FETCH_FAILED
end
def fetch_portfolio_transactions_for_exchange(linked_accounts)
@@ -472,6 +491,10 @@ class CoinstatsItem::Importer
end
end
+ def fetch_failed?(data)
+ data.equal?(FETCH_FAILED)
+ end
+
def find_matching_portfolio_coin(balance_data, coinstats_account)
Array(balance_data).map(&:with_indifferent_access).find do |coin_data|
coin = coin_data[:coin].to_h.with_indifferent_access
@@ -598,6 +621,7 @@ class CoinstatsItem::Importer
unique_wallets = (wallet_accounts + defi_accounts).uniq(&:id).filter_map do |account|
raw = account.raw_payload.to_h.with_indifferent_access
next unless raw[:address].present? && raw[:blockchain].present?
+ next unless defi_supported_blockchain?(raw[:blockchain])
{ address: raw[:address], blockchain: raw[:blockchain] }
end.uniq { |w| [ w[:address].downcase, w[:blockchain].downcase ] }
@@ -617,4 +641,8 @@ class CoinstatsItem::Importer
Rails.logger.warn "CoinstatsItem::Importer - DeFi sync failed: #{e.message}"
Set.new
end
+
+ def defi_supported_blockchain?(blockchain)
+ blockchain.present? && !DEFI_UNSUPPORTED_BLOCKCHAINS.include?(blockchain.to_s.downcase)
+ end
end
diff --git a/app/models/goal_pledge.rb b/app/models/goal_pledge.rb
index a72b2cb5b..a204933cf 100644
--- a/app/models/goal_pledge.rb
+++ b/app/models/goal_pledge.rb
@@ -38,19 +38,39 @@ class GoalPledge < ApplicationRecord
# Tolerance check: entry date within [created_at − 5d, expires_at] (so
# extend! widens the upper bound) and amount within ±$0.50 OR ±1%.
- # Transfer pledges only fire on inflows (Sure convention: inflow < 0).
- # Without this guard, .abs below lets a $200 outflow satisfy a $200
- # transfer pledge as readily as a $200 deposit.
- def matches?(entry)
+ #
+ # The amount being compared is the money that actually moved IN:
+ # - transfer pledges resolve against a Transaction inflow (Sure
+ # convention: inflow < 0), so the entry amount IS the contribution.
+ # - manual_save pledges resolve against a Valuation, whose amount is the
+ # account's full new TOTAL balance — not the contribution. The caller
+ # (Account::ReconciliationManager) passes the balance delta
+ # (new_balance − prior_balance) via `valuation_delta`; that delta is
+ # the contribution we match against. Comparing the raw valuation amount
+ # would only ever match on an account whose entire balance equals the
+ # pledge (i.e. one starting from ~$0).
+ #
+ # Both kinds only fire on money coming in: transfers require an inflow
+ # entry, manual_save requires a positive balance delta. Without these
+ # guards, .abs below would let a $200 outflow / a $200 drawdown satisfy a
+ # $200 pledge as readily as a $200 deposit.
+ def matches?(entry, valuation_delta: nil)
return false unless status_open?
return false unless entry.account_id == account_id
- return false if kind_transfer? && !entry.amount.to_d.negative?
+
+ is_valuation = entry.entryable.is_a?(Valuation)
+
+ if is_valuation
+ return false if valuation_delta.nil? || valuation_delta.to_d <= 0
+ elsif kind_transfer? && !entry.amount.to_d.negative?
+ return false
+ end
earliest = created_at.to_date - MATCH_DATE_TOLERANCE_DAYS.days
latest = [ created_at.to_date + MATCH_DATE_TOLERANCE_DAYS.days, expires_at.to_date ].max
return false unless entry.date >= earliest && entry.date <= latest
- txn_amount = entry.amount.to_d.abs
+ txn_amount = (is_valuation ? valuation_delta.to_d : entry.amount.to_d).abs
pledge_amount = amount.to_d
diff_abs = (txn_amount - pledge_amount).abs
diff --git a/app/models/goal_pledge/reconciler.rb b/app/models/goal_pledge/reconciler.rb
index f3309dc9e..81de43746 100644
--- a/app/models/goal_pledge/reconciler.rb
+++ b/app/models/goal_pledge/reconciler.rb
@@ -1,8 +1,13 @@
class GoalPledge::Reconciler
attr_reader :entry
- def initialize(entry)
+ # `valuation_delta` is the contribution (new_balance − prior_balance) for
+ # Valuation entries, supplied by Account::ReconciliationManager which knows
+ # the prior balance. It is ignored for Transaction entries, whose own
+ # amount is already the contribution.
+ def initialize(entry, valuation_delta: nil)
@entry = entry
+ @valuation_delta = valuation_delta
end
def run
@@ -17,7 +22,7 @@ class GoalPledge::Reconciler
.where("expires_at >= ?", Time.current)
.order(:created_at, :id)
.each do |pledge|
- next unless pledge.matches?(entry)
+ next unless pledge.matches?(entry, valuation_delta: @valuation_delta)
begin
if entry.entryable.is_a?(Transaction)
diff --git a/app/models/provider/coinstats.rb b/app/models/provider/coinstats.rb
index 7dda1f5ac..5c6289fbb 100644
--- a/app/models/provider/coinstats.rb
+++ b/app/models/provider/coinstats.rb
@@ -2,12 +2,26 @@
# Handles authentication and requests to the CoinStats OpenAPI.
class Provider::Coinstats < Provider
include HTTParty
+ include Provider::RateLimitable
extend SslConfigurable
# Subclass so errors caught in this provider are raised as Provider::Coinstats::Error
Error = Class.new(Provider::Error)
+ class RateLimitError < Error
+ attr_reader :retry_after
+
+ def initialize(message, details: nil, retry_after: nil)
+ super(message, details: details)
+ @retry_after = retry_after
+ end
+ end
BASE_URL = "https://openapiv1.coinstats.app"
+ PROVIDER_ENV_PREFIX = "COINSTATS"
+ MIN_REQUEST_INTERVAL = 0.5
+ MAX_RETRIES = 2
+ INITIAL_RETRY_DELAY = 1
+ MAX_RETRY_DELAY = 10
headers "User-Agent" => "Sure Finance CoinStats Client (https://github.com/we-promise/sure)"
default_options.merge!({ timeout: 120 }.merge(httparty_ssl_options))
@@ -322,12 +336,14 @@ class Provider::Coinstats < Provider
# @return [Provider::Response] Response with DeFi position data
def get_wallet_defi(address:, connection_id:)
with_provider_response do
- res = self.class.get(
- "#{BASE_URL}/wallet/defi",
- headers: auth_headers,
- query: { address: address, connectionId: connection_id }
- )
- handle_response(res)
+ with_retries("GET /wallet/defi") do
+ res = self.class.get(
+ "#{BASE_URL}/wallet/defi",
+ headers: auth_headers,
+ query: { address: address, connectionId: connection_id }
+ )
+ handle_response(res)
+ end
end
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "CoinStats API: GET /wallet/defi failed: #{e.class}: #{e.message}"
@@ -343,12 +359,14 @@ class Provider::Coinstats < Provider
return with_provider_response { [] } if wallets.blank?
with_provider_response do
- res = self.class.get(
- "#{BASE_URL}/wallet/balances",
- headers: auth_headers,
- query: { wallets: wallets }
- )
- handle_response(res)
+ with_retries("GET /wallet/balances") do
+ res = self.class.get(
+ "#{BASE_URL}/wallet/balances",
+ headers: auth_headers,
+ query: { wallets: wallets }
+ )
+ handle_response(res)
+ end
end
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "CoinStats API: GET /wallet/balances failed: #{e.class}: #{e.message}"
@@ -385,12 +403,14 @@ class Provider::Coinstats < Provider
return with_provider_response { [] } if wallets.blank?
with_provider_response do
- res = self.class.get(
- "#{BASE_URL}/wallet/transactions",
- headers: auth_headers,
- query: { wallets: wallets }
- )
- handle_response(res)
+ with_retries("GET /wallet/transactions") do
+ res = self.class.get(
+ "#{BASE_URL}/wallet/transactions",
+ headers: auth_headers,
+ query: { wallets: wallets }
+ )
+ handle_response(res)
+ end
end
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "CoinStats API: GET /wallet/transactions failed: #{e.class}: #{e.message}"
@@ -429,6 +449,12 @@ class Provider::Coinstats < Provider
private
+ def default_error_transformer(error)
+ return error if error.is_a?(Error)
+
+ super
+ end
+
def auth_headers
{
"X-API-KEY" => api_key,
@@ -436,6 +462,36 @@ class Provider::Coinstats < Provider
}
end
+ def with_retries(operation_name, max_retries: MAX_RETRIES)
+ retries = 0
+
+ begin
+ throttle_request
+ yield
+ rescue RateLimitError => e
+ retries += 1
+
+ if retries <= max_retries
+ delay = e.retry_after.presence || calculate_retry_delay(retries)
+ Rails.logger.warn(
+ "CoinStats API: #{operation_name} rate limited " \
+ "(attempt #{retries}/#{max_retries}). Retrying in #{delay}s..."
+ )
+ sleep(delay) if delay.to_f.positive?
+ retry
+ end
+
+ Rails.logger.error "CoinStats API: #{operation_name} rate limited after #{max_retries} retries"
+ raise
+ end
+ end
+
+ def calculate_retry_delay(retry_count)
+ base_delay = INITIAL_RETRY_DELAY * (2 ** (retry_count - 1))
+ jitter = base_delay * rand * 0.25
+ [ base_delay + jitter, MAX_RETRY_DELAY ].min
+ end
+
# The CoinStats API uses standard HTTP status codes to indicate the success or failure of requests.
# https://coinstats.app/api-docs/errors
def handle_response(response)
@@ -454,12 +510,15 @@ class Provider::Coinstats < Provider
when 404
log_api_error(response, "Not Found")
raise_api_error(response, fallback: "CoinStats: Resource not found")
+ when 406
+ log_api_error(response, "Not Acceptable")
+ raise_api_error(response, fallback: "CoinStats: Credits limit reached")
when 409
log_api_error(response, "Conflict")
raise_api_error(response, fallback: "CoinStats: Resource conflict")
when 429
log_api_error(response, "Too Many Requests")
- raise_api_error(response, fallback: "CoinStats: Rate limit exceeded, try again later")
+ raise_api_error(response, fallback: "CoinStats: Rate limit exceeded, try again later", error_class: RateLimitError)
when 500
log_api_error(response, "Internal Server Error")
raise_api_error(response, fallback: "CoinStats: Server error, try again later")
@@ -476,14 +535,19 @@ class Provider::Coinstats < Provider
Rails.logger.error "CoinStats API: #{response.code} #{error_type} - #{response.body}"
end
- def raise_api_error(response, fallback:)
+ def raise_api_error(response, fallback:, error_class: Error)
error_payload = parse_error_payload(response.body)
message = error_payload[:message].presence || fallback
request_id = error_payload[:request_id].presence
+ retry_after = retry_after_seconds(response)
message = "#{message} (requestId: #{request_id})" if request_id.present?
- raise Error.new(message, details: error_payload.compact.presence)
+ if error_class == RateLimitError
+ raise error_class.new(message, details: error_payload.compact.presence, retry_after: retry_after)
+ end
+
+ raise error_class.new(message, details: error_payload.compact.presence)
end
def parse_error_payload(body)
@@ -498,4 +562,13 @@ class Provider::Coinstats < Provider
rescue JSON::ParserError
{}
end
+
+ def retry_after_seconds(response)
+ retry_after = response.headers["Retry-After"] || response.headers["retry-after"]
+ return nil if retry_after.blank?
+
+ Integer(retry_after)
+ rescue ArgumentError, NoMethodError
+ nil
+ end
end
diff --git a/app/models/vector_store/pgvector.rb b/app/models/vector_store/pgvector.rb
index a434ec1f5..2331bd335 100644
--- a/app/models/vector_store/pgvector.rb
+++ b/app/models/vector_store/pgvector.rb
@@ -15,6 +15,25 @@ class VectorStore::Pgvector < VectorStore::Base
PGVECTOR_SUPPORTED_EXTENSIONS = (VectorStore::Embeddable::TEXT_EXTENSIONS + [ ".pdf" ]).uniq.freeze
+ TABLE_NAME = "vector_store_chunks"
+
+ # True when this adapter can actually operate: the chunks table already
+ # exists, or the server has the pgvector extension available so
+ # ensure_schema! can provision it on first use. The Registry consults this
+ # before building the adapter, so an install without pgvector degrades to
+ # the assistant's friendly "provider_not_configured" message instead of
+ # raising raw PG errors mid-chat.
+ def self.available?
+ conn = ActiveRecord::Base.connection
+ return true if conn.table_exists?(TABLE_NAME)
+
+ conn.select_value(
+ "SELECT 1 FROM pg_available_extensions WHERE name = 'vector' LIMIT 1"
+ ).present?
+ rescue StandardError
+ false
+ end
+
def supported_extensions
PGVECTOR_SUPPORTED_EXTENSIONS
end
@@ -27,6 +46,7 @@ class VectorStore::Pgvector < VectorStore::Base
def delete_store(store_id:)
with_response do
+ ensure_schema!
connection.exec_delete(
"DELETE FROM vector_store_chunks WHERE store_id = $1",
"VectorStore::Pgvector DeleteStore",
@@ -37,6 +57,7 @@ class VectorStore::Pgvector < VectorStore::Base
def upload_file(store_id:, file_content:, filename:)
with_response do
+ ensure_schema!
text = extract_text(file_content, filename)
raise VectorStore::Error, "Could not extract text from #{filename}" if text.blank?
@@ -81,6 +102,7 @@ class VectorStore::Pgvector < VectorStore::Base
def remove_file(store_id:, file_id:)
with_response do
+ ensure_schema!
connection.exec_delete(
"DELETE FROM vector_store_chunks WHERE store_id = $1 AND file_id = $2",
"VectorStore::Pgvector RemoveFile",
@@ -94,6 +116,7 @@ class VectorStore::Pgvector < VectorStore::Base
def search(store_id:, query:, max_results: 10)
with_response do
+ ensure_schema!
query_vector = embed(query)
vector_literal = "[#{query_vector.join(',')}]"
@@ -127,6 +150,44 @@ class VectorStore::Pgvector < VectorStore::Base
private
+ # Provisions the chunks table on first use, mirroring the
+ # CreateVectorStoreChunks migration. Migrations cover db:migrate
+ # upgrades, but fresh installs go through db:prepare → schema:load
+ # (bin/docker-entrypoint), which marks conditional migrations as applied
+ # without running them — and the table can't live in schema.rb because it
+ # requires the vector extension. Idempotent; memoized per instance.
+ def ensure_schema!
+ return if @schema_ensured
+ if connection.table_exists?(TABLE_NAME)
+ @schema_ensured = true
+ return
+ end
+
+ connection.enable_extension("vector") unless connection.extension_enabled?("vector")
+ # if_not_exists on the DDL (not a Mutex) is the right concurrency guard
+ # here: adapter instances are built per call and never shared across
+ # threads, so the realistic race is two *processes* (e.g. web + Sidekiq)
+ # provisioning at once. IF NOT EXISTS makes the loser's DDL a no-op
+ # instead of a duplicate-relation error.
+ connection.create_table(TABLE_NAME, id: :uuid, if_not_exists: true) do |t|
+ t.string :store_id, null: false
+ t.string :file_id, null: false
+ t.string :filename
+ t.integer :chunk_index, null: false, default: 0
+ t.text :content, null: false
+ t.column :embedding, "vector(#{ENV.fetch('EMBEDDING_DIMENSIONS', '1024')})", null: false
+ t.jsonb :metadata, null: false, default: {}
+ t.timestamps null: false
+ end
+ connection.add_index TABLE_NAME, :store_id, if_not_exists: true
+ connection.add_index TABLE_NAME, :file_id, if_not_exists: true
+ connection.add_index TABLE_NAME, [ :store_id, :file_id, :chunk_index ], unique: true,
+ name: "index_vector_store_chunks_on_store_file_chunk", if_not_exists: true
+ @schema_ensured = true
+ rescue StandardError => e
+ raise VectorStore::Error, "pgvector store unavailable: #{e.message}"
+ end
+
def connection
ActiveRecord::Base.connection
end
diff --git a/app/models/vector_store/registry.rb b/app/models/vector_store/registry.rb
index 10c73d770..fcc470661 100644
--- a/app/models/vector_store/registry.rb
+++ b/app/models/vector_store/registry.rb
@@ -7,8 +7,10 @@ class VectorStore::Registry
class << self
# Returns the configured adapter instance.
- # Reads from VECTOR_STORE_PROVIDER env var, falling back to :openai
- # when OpenAI credentials are present.
+ # Reads from VECTOR_STORE_PROVIDER env var; without an explicit override,
+ # Anthropic installs (Setting.llm_provider == "anthropic") default to
+ # :pgvector, and anything else falls back to :openai when OpenAI
+ # credentials are present.
def adapter
name = adapter_name
return nil unless name
@@ -24,10 +26,27 @@ class VectorStore::Registry
explicit = ENV["VECTOR_STORE_PROVIDER"].presence
return explicit.to_sym if explicit && ADAPTERS.key?(explicit.to_sym)
- # Default: use OpenAI when credentials are available
+ # Default routing:
+ # - When the configured LLM provider is Anthropic (which has no hosted
+ # vector store), fall back to the local pgvector adapter. The
+ # Embeddable concern still pulls embeddings from EMBEDDING_URI_BASE /
+ # OPENAI_ACCESS_TOKEN — Anthropic users typically point this at
+ # Voyage AI, a local Ollama instance, or OpenAI embeddings.
+ # - Otherwise, use OpenAI when credentials are available.
+ return :pgvector if Setting.llm_provider == "anthropic"
:openai if openai_access_token.present?
end
+ # True when pgvector is the effective vector store — whether set explicitly
+ # via VECTOR_STORE_PROVIDER or selected by the Anthropic default above.
+ # Single source of truth shared with the migration that provisions
+ # `vector_store_chunks`, so the table is created exactly when pgvector is in
+ # use (an Anthropic-default install would otherwise skip it and fail on the
+ # missing table).
+ def pgvector_effective?
+ adapter_name == :pgvector
+ end
+
private
def build_adapter(name)
@@ -53,6 +72,12 @@ class VectorStore::Registry
end
def build_pgvector
+ # Gate on availability (extension present, or table already created)
+ # so an Anthropic-default install on a Postgres without pgvector
+ # degrades to the assistant's "provider_not_configured" message
+ # instead of raising raw PG errors mid-chat.
+ return nil unless VectorStore::Pgvector.available?
+
VectorStore::Pgvector.new
end
diff --git a/app/views/admin/sso_providers/index.html.erb b/app/views/admin/sso_providers/index.html.erb
index c799d374d..6120b52ea 100644
--- a/app/views/admin/sso_providers/index.html.erb
+++ b/app/views/admin/sso_providers/index.html.erb
@@ -26,7 +26,7 @@
<% end %>
diff --git a/app/views/transactions/categorizes/_group_title.html.erb b/app/views/transactions/categorizes/_group_title.html.erb
index 948b504ac..a23ab1961 100644
--- a/app/views/transactions/categorizes/_group_title.html.erb
+++ b/app/views/transactions/categorizes/_group_title.html.erb
@@ -2,7 +2,7 @@
<%= display_name %>
<% if transaction_type == "income" %>
- <%= t("transactions.categorizes.show.type_income") %>
+ <%= t("transactions.categorizes.show.type_income") %>
<% elsif transaction_type == "expense" %>
<%= t("transactions.categorizes.show.type_expense") %>
<% end %>
diff --git a/charts/sure/Chart.yaml b/charts/sure/Chart.yaml
index 028a2ca59..1991d11ca 100644
--- a/charts/sure/Chart.yaml
+++ b/charts/sure/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: sure
description: Official Helm chart for deploying the Sure Rails app (web + Sidekiq) on Kubernetes with optional HA PostgreSQL (CloudNativePG) and Redis.
type: application
-version: 0.7.2-alpha.3
-appVersion: "0.7.2-alpha.3"
+version: 0.7.2-alpha.4
+appVersion: "0.7.2-alpha.4"
kubeVersion: ">=1.25.0-0"
diff --git a/config/locales/views/account_sharings/nl.yml b/config/locales/views/account_sharings/nl.yml
new file mode 100644
index 000000000..3aea7a0a7
--- /dev/null
+++ b/config/locales/views/account_sharings/nl.yml
@@ -0,0 +1,29 @@
+---
+nl:
+ account_sharings:
+ show:
+ title: Account delen
+ subtitle: Bepaal wie dit account kan zien en ermee kan werken
+ member: Lid
+ permission: Toestemming
+ shared: Gedeeld
+ no_members: Geen andere leden in uw %{moniker} om mee te delen
+ permissions:
+ full_control: Volledige controle
+ full_control_description: Kan transacties bekijken, bewerken en beheren
+ read_write: Kan annoteren
+ read_write_description: Kan categoriseren, taggen en notities toevoegen
+ read_only: Alleen weergeven
+ read_only_description: Kan alleen accountgegevens bekijken
+ save: Deelinstellingen opslaan
+ owner_label: "Eigenaar: %{name}"
+ shared_with_count:
+ one: Gedeeld met 1 lid
+ other: "Gedeeld met %{count} leden"
+ include_in_finances: Opnemen in mijn budgetten en rapporten
+ exclude_from_finances: Uitsluiten van mijn budgetten en rapporten
+ finance_toggle_description: Dit account meetellen in uw vermogen, budgetten en rapporten
+ update:
+ success: Deelinstellingen bijgewerkt
+ not_owner: Alleen de accounteigenaar kan het delen beheren
+ finance_toggle_success: Voorkeur voor financiële opname bijgewerkt
diff --git a/config/locales/views/account_statements/nl.yml b/config/locales/views/account_statements/nl.yml
new file mode 100644
index 000000000..bc33505b9
--- /dev/null
+++ b/config/locales/views/account_statements/nl.yml
@@ -0,0 +1,116 @@
+---
+nl:
+ account_statements:
+ account_tab:
+ coverage_title: Dekking van afschriften
+ coverage_description: Historische maanden onderbouwd door geüploade afschriften en saldocontroles.
+ coverage_range: "%{start} - %{end}"
+ empty: Nog geen afschriften gekoppeld aan dit account.
+ open_inbox: Inbox
+ statements_title: Afschriften
+ year_label: Dekkingsjaar
+ balance:
+ unknown: Onbekend
+ coverage:
+ status:
+ ambiguous: Dubbelzinnig
+ covered: Gedekt
+ duplicate: Duplicaat
+ mismatched: Komt niet overeen
+ missing: Ontbreekt
+ not_expected: Niet verwacht
+ create:
+ duplicates:
+ one: 1 dubbel afschrift is overgeslagen.
+ other: "%{count} dubbele afschriften zijn overgeslagen."
+ invalid_file_type: Upload een afschrift als PDF, CSV of XLSX binnen de groottelimiet.
+ no_files: Selecteer ten minste één afschriftbestand.
+ success:
+ one: 1 afschrift geüpload.
+ other: "%{count} afschriften geüpload."
+ destroy:
+ failure: Afschrift kon niet worden verwijderd.
+ success: Afschrift verwijderd.
+ form:
+ account_upload: Afschrift uploaden
+ files_hint: PDF, CSV of XLSX. Max. %{max_size} MB per bestand.
+ files_label: Afschriftbestanden
+ inbox_upload: Uploaden
+ index:
+ account_label: Account
+ confidence: "%{confidence} overeenkomst"
+ empty_linked: Nog geen gekoppelde afschriften.
+ empty_unmatched: De afschriften-inbox is leeg.
+ leave_unmatched: Niet-toegewezen laten
+ linked_title: Gekoppelde afschriften
+ no_suggestion: Geen suggestie
+ storage_used: Gebruikte opslag
+ title: Afschriftenkluis
+ unmatched_title: Niet-toegewezen inbox
+ upload_description: Upload afschriften naar de inbox of kies een account om ze meteen te koppelen.
+ upload_title: Afschriften uploaden
+ link:
+ no_account: Kies een account voordat u dit afschrift koppelt.
+ success: Afschrift gekoppeld aan %{account}.
+ period:
+ unknown: Periode onbekend
+ reconciliation:
+ checks:
+ closing_balance: Eindsaldo
+ opening_balance: Beginsaldo
+ period_movement: Mutatie in periode
+ unknown_check: Onbekende controle
+ matched: Komt overeen
+ mismatched: Komt niet overeen
+ unavailable: Niet gecontroleerd
+ reject:
+ success: Afschriftkoppeling afgewezen.
+ show:
+ account_label: Account
+ account_last4_hint: Laatste vier cijfers van het account
+ account_name_hint: Hint voor accountnaam
+ closing_balance: Eindsaldo
+ currency: Valuta
+ delete: Verwijderen
+ difference: Verschil
+ download: Downloaden
+ institution_name_hint: Hint voor instelling
+ ledger_amount: Sure-grootboek
+ linked_to: Gekoppeld aan %{account}.
+ linking_title: Accountkoppeling
+ link_suggestion: Koppelsuggestie
+ metadata_title: Afschriftmetagegevens
+ no_suggestion: Nog geen accountsuggestie.
+ opening_balance: Beginsaldo
+ period_end_on: Einde periode
+ period_start_on: Begin periode
+ reconciliation_title: Afstemming
+ reconciliation_unavailable: Voeg een afschriftperiode en een begin- of eindsaldo toe en zorg ervoor dat Sure een saldogeschiedenis heeft voor die datums.
+ reject: Afwijzen
+ save: Afschrift opslaan
+ statement_amount: Afschrift
+ suggested_account: Voorgesteld account is %{account} (%{confidence} betrouwbaarheid).
+ title: Afschrift
+ unlink: Ontkoppelen
+ unmatched_account: Niet-toegewezen inbox
+ unknown_value: Onbekend
+ status:
+ linked: Gekoppeld
+ rejected: Afgewezen
+ unmatched: Niet-toegewezen
+ table:
+ account: Account
+ actions: Acties
+ download: Downloaden
+ file: Bestand
+ link_suggestion: Koppelsuggestie
+ period: Periode
+ reconciliation: Afstemming
+ reject: Suggestie afwijzen
+ suggestion: Suggestie
+ unlink: Ontkoppelen
+ view: Bekijken
+ unlink:
+ success: Afschrift teruggezet naar de niet-toegewezen inbox.
+ update:
+ success: Afschrift bijgewerkt.
diff --git a/config/locales/views/account_statements/pt-BR.yml b/config/locales/views/account_statements/pt-BR.yml
new file mode 100644
index 000000000..7f8f84057
--- /dev/null
+++ b/config/locales/views/account_statements/pt-BR.yml
@@ -0,0 +1,116 @@
+---
+pt-BR:
+ account_statements:
+ account_tab:
+ coverage_title: Cobertura de extratos
+ coverage_description: Meses históricos respaldados por extratos enviados e verificações de saldo.
+ coverage_range: "%{start} - %{end}"
+ empty: Ainda não há extratos vinculados a esta conta.
+ open_inbox: Caixa de entrada
+ statements_title: Extratos
+ year_label: Ano de cobertura
+ balance:
+ unknown: Desconhecido
+ coverage:
+ status:
+ ambiguous: Ambíguo
+ covered: Coberto
+ duplicate: Duplicado
+ mismatched: Divergente
+ missing: Ausente
+ not_expected: Não esperado
+ create:
+ duplicates:
+ one: 1 extrato duplicado foi ignorado.
+ other: "%{count} extratos duplicados foram ignorados."
+ invalid_file_type: Envie um extrato em PDF, CSV ou XLSX dentro do limite de tamanho.
+ no_files: Selecione pelo menos um arquivo de extrato.
+ success:
+ one: 1 extrato enviado.
+ other: "%{count} extratos enviados."
+ destroy:
+ failure: Não foi possível excluir o extrato.
+ success: Extrato excluído.
+ form:
+ account_upload: Enviar extrato
+ files_hint: PDF, CSV ou XLSX. Máximo de %{max_size} MB por arquivo.
+ files_label: Arquivos de extrato
+ inbox_upload: Enviar
+ index:
+ account_label: Conta
+ confidence: "%{confidence} de correspondência"
+ empty_linked: Ainda não há extratos vinculados.
+ empty_unmatched: A caixa de entrada de extratos está vazia.
+ leave_unmatched: Deixar sem atribuição
+ linked_title: Extratos vinculados
+ no_suggestion: Sem sugestão
+ storage_used: Armazenamento usado
+ title: Cofre de extratos
+ unmatched_title: Caixa de entrada sem atribuição
+ upload_description: Envie extratos para a caixa de entrada ou escolha uma conta para vinculá-los imediatamente.
+ upload_title: Enviar extratos
+ link:
+ no_account: Escolha uma conta antes de vincular este extrato.
+ success: Extrato vinculado a %{account}.
+ period:
+ unknown: Período desconhecido
+ reconciliation:
+ checks:
+ closing_balance: Saldo de fechamento
+ opening_balance: Saldo de abertura
+ period_movement: Movimentação do período
+ unknown_check: Verificação desconhecida
+ matched: Correspondente
+ mismatched: Divergente
+ unavailable: Não verificado
+ reject:
+ success: Correspondência de extrato rejeitada.
+ show:
+ account_label: Conta
+ account_last4_hint: Últimos quatro dígitos da conta
+ account_name_hint: Dica do nome da conta
+ closing_balance: Saldo de fechamento
+ currency: Moeda
+ delete: Excluir
+ difference: Diferença
+ download: Baixar
+ institution_name_hint: Dica da instituição
+ ledger_amount: Registro do Sure
+ linked_to: Vinculado a %{account}.
+ linking_title: Vínculo de conta
+ link_suggestion: Sugestão de vínculo
+ metadata_title: Metadados do extrato
+ no_suggestion: Ainda não há sugestão de conta.
+ opening_balance: Saldo de abertura
+ period_end_on: Fim do período
+ period_start_on: Início do período
+ reconciliation_title: Conciliação
+ reconciliation_unavailable: Adicione um período de extrato e um saldo de abertura ou fechamento e verifique se o Sure tem histórico de saldos para essas datas.
+ reject: Rejeitar
+ save: Salvar extrato
+ statement_amount: Extrato
+ suggested_account: A conta sugerida é %{account} (confiança de %{confidence}).
+ title: Extrato
+ unlink: Desvincular
+ unmatched_account: Caixa de entrada sem atribuição
+ unknown_value: Desconhecido
+ status:
+ linked: Vinculado
+ rejected: Rejeitado
+ unmatched: Sem atribuição
+ table:
+ account: Conta
+ actions: Ações
+ download: Baixar
+ file: Arquivo
+ link_suggestion: Sugestão de vínculo
+ period: Período
+ reconciliation: Conciliação
+ reject: Rejeitar sugestão
+ suggestion: Sugestão
+ unlink: Desvincular
+ view: Visualizar
+ unlink:
+ success: Extrato devolvido à caixa de entrada sem atribuição.
+ update:
+ success: Extrato atualizado.
diff --git a/config/locales/views/binance_items/nl.yml b/config/locales/views/binance_items/nl.yml
new file mode 100644
index 000000000..22ee29759
--- /dev/null
+++ b/config/locales/views/binance_items/nl.yml
@@ -0,0 +1,75 @@
+---
+nl:
+ binance_items:
+ create:
+ default_name: Binance
+ success: Succesvol verbonden met Binance! Uw account wordt gesynchroniseerd.
+ update:
+ success: Binance-configuratie succesvol bijgewerkt.
+ destroy:
+ success: Binance-verbinding is in de wachtrij voor verwijdering geplaatst.
+ setup_accounts:
+ title: Binance-account importeren
+ subtitle: Selecteer welke portefeuilles u wilt volgen
+ instructions: Selecteer de Binance-portefeuilles die u wilt importeren. Alleen portefeuilles met saldo worden weergegeven.
+ no_accounts: Alle accounts zijn geïmporteerd.
+ accounts_count:
+ one: "%{count} account beschikbaar"
+ other: "%{count} accounts beschikbaar"
+ select_all: Alles selecteren
+ import_selected: Geselecteerde importeren
+ cancel: Annuleren
+ creating: Importeren...
+ complete_account_setup:
+ success:
+ one: "%{count} account geïmporteerd"
+ other: "%{count} accounts geïmporteerd"
+ none_selected: Geen accounts geselecteerd
+ no_accounts: Geen accounts om te importeren
+ binance_item:
+ provider_name: Binance
+ syncing: Synchroniseren...
+ reconnect: Inloggegevens moeten worden bijgewerkt
+ deletion_in_progress: Verwijderen...
+ sync_status:
+ no_accounts: Geen accounts gevonden
+ all_synced:
+ one: "%{count} account gesynchroniseerd"
+ other: "%{count} accounts gesynchroniseerd"
+ partial_sync: "%{linked_count} gesynchroniseerd, %{unlinked_count} moeten worden ingesteld"
+ status: "Laatst gesynchroniseerd %{timestamp} geleden"
+ status_with_summary: "Laatst gesynchroniseerd %{timestamp} geleden - %{summary}"
+ status_never: Nooit gesynchroniseerd
+ update_credentials: Inloggegevens bijwerken
+ delete: Verwijderen
+ no_accounts_title: Geen accounts gevonden
+ no_accounts_message: Uw Binance-portefeuille verschijnt hier na de synchronisatie.
+ setup_needed: Account klaar om te importeren
+ setup_description: Selecteer welke Binance-portefeuilles u wilt volgen.
+ setup_action: Account importeren
+ import_accounts_menu: Account importeren
+ stale_rate_warning: "Het saldo is bij benadering — de exacte wisselkoers voor %{date} was niet beschikbaar. Wordt bijgewerkt bij de volgende synchronisatie."
+ select_existing_account:
+ title: Binance-account koppelen
+ no_accounts_found: Geen Binance-accounts gevonden.
+ wait_for_sync: Wacht tot Binance klaar is met synchroniseren
+ check_provider_health: Controleer of uw Binance-API-inloggegevens geldig zijn
+ currently_linked_to: "Momenteel gekoppeld aan: %{account_name}"
+ link: Koppelen
+ cancel: Annuleren
+ link_existing_account:
+ success: Succesvol gekoppeld aan Binance-account
+ errors:
+ only_manual: Alleen handmatige accounts kunnen aan Binance worden gekoppeld
+ invalid_binance_account: Ongeldig Binance-account
+ binance_item:
+ syncer:
+ checking_credentials: Inloggegevens controleren...
+ credentials_invalid: Ongeldige API-inloggegevens. Controleer uw API-sleutel en secret.
+ importing_accounts: Accounts importeren vanuit Binance...
+ checking_configuration: Accountconfiguratie controleren...
+ accounts_need_setup:
+ one: "%{count} account moet worden ingesteld"
+ other: "%{count} accounts moeten worden ingesteld"
+ processing_accounts: Accountgegevens verwerken...
+ calculating_balances: Saldi berekenen...
diff --git a/config/locales/views/binance_items/pt-BR.yml b/config/locales/views/binance_items/pt-BR.yml
new file mode 100644
index 000000000..a022996db
--- /dev/null
+++ b/config/locales/views/binance_items/pt-BR.yml
@@ -0,0 +1,75 @@
+---
+pt-BR:
+ binance_items:
+ create:
+ default_name: Binance
+ success: Conexão com a Binance estabelecida com sucesso! Sua conta está sendo sincronizada.
+ update:
+ success: Configuração da Binance atualizada com sucesso.
+ destroy:
+ success: Conexão da Binance programada para exclusão.
+ setup_accounts:
+ title: Importar conta da Binance
+ subtitle: Selecione quais carteiras você quer acompanhar
+ instructions: Selecione as carteiras da Binance que você quer importar. Apenas carteiras com saldo são exibidas.
+ no_accounts: Todas as contas foram importadas.
+ accounts_count:
+ one: "%{count} conta disponível"
+ other: "%{count} contas disponíveis"
+ select_all: Selecionar todas
+ import_selected: Importar selecionadas
+ cancel: Cancelar
+ creating: Importando...
+ complete_account_setup:
+ success:
+ one: "%{count} conta importada"
+ other: "%{count} contas importadas"
+ none_selected: Nenhuma conta selecionada
+ no_accounts: Nenhuma conta para importar
+ binance_item:
+ provider_name: Binance
+ syncing: Sincronizando...
+ reconnect: As credenciais precisam ser atualizadas
+ deletion_in_progress: Excluindo...
+ sync_status:
+ no_accounts: Nenhuma conta encontrada
+ all_synced:
+ one: "%{count} conta sincronizada"
+ other: "%{count} contas sincronizadas"
+ partial_sync: "%{linked_count} sincronizadas, %{unlinked_count} precisam de configuração"
+ status: "Sincronizado há %{timestamp}"
+ status_with_summary: "Sincronizado há %{timestamp} - %{summary}"
+ status_never: Nunca sincronizado
+ update_credentials: Atualizar credenciais
+ delete: Excluir
+ no_accounts_title: Nenhuma conta encontrada
+ no_accounts_message: Sua carteira da Binance aparecerá aqui após a sincronização.
+ setup_needed: Conta pronta para importar
+ setup_description: Selecione quais carteiras da Binance você quer acompanhar.
+ setup_action: Importar conta
+ import_accounts_menu: Importar conta
+ stale_rate_warning: "O saldo é aproximado — a taxa de câmbio exata para %{date} não estava disponível. Será atualizado na próxima sincronização."
+ select_existing_account:
+ title: Vincular conta da Binance
+ no_accounts_found: Nenhuma conta da Binance encontrada.
+ wait_for_sync: Aguarde a Binance concluir a sincronização
+ check_provider_health: Verifique se suas credenciais da API da Binance são válidas
+ currently_linked_to: "Atualmente vinculada a: %{account_name}"
+ link: Vincular
+ cancel: Cancelar
+ link_existing_account:
+ success: Vinculado com sucesso à conta da Binance
+ errors:
+ only_manual: Apenas contas manuais podem ser vinculadas à Binance
+ invalid_binance_account: Conta da Binance inválida
+ binance_item:
+ syncer:
+ checking_credentials: Verificando credenciais...
+ credentials_invalid: Credenciais da API inválidas. Verifique sua chave de API e seu segredo.
+ importing_accounts: Importando contas da Binance...
+ checking_configuration: Verificando a configuração da conta...
+ accounts_need_setup:
+ one: "%{count} conta precisa de configuração"
+ other: "%{count} contas precisam de configuração"
+ processing_accounts: Processando os dados da conta...
+ calculating_balances: Calculando saldos...
diff --git a/config/locales/views/brex_items/nl.yml b/config/locales/views/brex_items/nl.yml
new file mode 100644
index 000000000..081147174
--- /dev/null
+++ b/config/locales/views/brex_items/nl.yml
@@ -0,0 +1,277 @@
+---
+nl:
+ brex_items:
+ default_connection_name: Brex-verbinding
+ account_metadata:
+ provider: Brex
+ separator: " • "
+ kinds:
+ cash: Contant
+ card: Kaart
+ statuses:
+ ACTIVE: Actief
+ active: Actief
+ CLOSED: Gesloten
+ closed: Gesloten
+ frozen: Bevroren
+ FROZEN: Bevroren
+ create:
+ success: Brex-verbinding succesvol aangemaakt
+ default_card_name: Brex-kaart
+ default_cash_name: "Brex Cash %{id}"
+ destroy:
+ success: Brex-verbinding verwijderd
+ index:
+ title: Brex-verbindingen
+ institution_summary:
+ none: Geen instellingen verbonden
+ one: "%{name}"
+ count:
+ one: "%{count} instelling"
+ other: "%{count} instellingen"
+ sync_status:
+ no_accounts: Geen accounts gevonden
+ all_synced:
+ one: "%{count} account gesynchroniseerd"
+ other: "%{count} accounts gesynchroniseerd"
+ partial_setup: "%{synced} gesynchroniseerd, %{pending} moeten worden ingesteld"
+ api_error:
+ common_issues: "Veelvoorkomende problemen:"
+ expired_credentials: Genereer een nieuw API-token bij Brex.
+ expired_credentials_label: "Verlopen inloggegevens:"
+ heading: Kan geen verbinding maken met Brex
+ invalid_token: Controleer uw API-token in de Provider-instellingen.
+ invalid_token_label: "Ongeldig API-token:"
+ network: Controleer uw internetverbinding.
+ network_label: "Netwerkprobleem:"
+ permissions: Zorg ervoor dat uw token over de vereiste alleen-lezen-scopes voor account en transacties beschikt.
+ permissions_label: "Onvoldoende rechten:"
+ service: De Brex-API is mogelijk tijdelijk niet beschikbaar.
+ service_label: "Dienst niet beschikbaar:"
+ settings_link: Provider-instellingen controleren
+ title: Brex-verbindingsfout
+ errors:
+ unexpected_error: Er is een onverwachte fout opgetreden. Probeer het later opnieuw.
+ entries:
+ default_name: Brex-transactie
+ loading:
+ loading_message: Brex-accounts laden...
+ loading_title: Laden
+ link_accounts:
+ all_already_linked:
+ one: "Het geselecteerde account (%{names}) is al gekoppeld"
+ other: "Alle %{count} geselecteerde accounts zijn al gekoppeld: %{names}"
+ api_error: "API-fout: %{message}"
+ invalid_account_names:
+ one: "Kan account met lege naam niet koppelen"
+ other: "Kan %{count} accounts met lege namen niet koppelen"
+ invalid_account_type: Niet-ondersteund Brex-accounttype
+ link_failed: Accounts koppelen mislukt
+ no_accounts_selected: Selecteer ten minste één account
+ no_api_token: Brex-API-token niet gevonden. Configureer het in de Provider-instellingen.
+ partial_invalid: "%{created_count} account(s) succesvol gekoppeld, %{already_linked_count} account(s) waren al gekoppeld, %{invalid_count} account(s) hadden ongeldige namen"
+ partial_success: "%{created_count} account(s) succesvol gekoppeld. %{already_linked_count} account(s) waren al gekoppeld: %{already_linked_names}"
+ select_connection: Kies een Brex-verbinding voordat u accounts koppelt.
+ success:
+ one: "%{count} account succesvol gekoppeld"
+ other: "%{count} accounts succesvol gekoppeld"
+ brex_item:
+ accounts_need_setup: Accounts moeten worden ingesteld
+ delete: Verbinding verwijderen
+ deletion_in_progress: verwijderen bezig...
+ error: Fout
+ no_accounts_description: Deze verbinding heeft nog geen gekoppelde accounts.
+ no_accounts_title: Geen accounts
+ setup_action: Nieuwe accounts instellen
+ setup_description: "%{linked} van %{total} accounts gekoppeld. Kies accounttypes voor uw nieuw geïmporteerde Brex-accounts."
+ setup_needed: Nieuwe accounts klaar om in te stellen
+ status: "Gesynchroniseerd %{timestamp} geleden"
+ status_never: Nooit gesynchroniseerd
+ status_with_summary: "Laatst gesynchroniseerd %{timestamp} geleden - %{summary}"
+ syncing: Synchroniseren...
+ total: Totaal
+ unlinked: Niet gekoppeld
+ provider_panel:
+ accounts_link: Accounts
+ add_connection: Brex-verbinding toevoegen
+ base_url_label: Basis-URL (optioneel)
+ base_url_placeholder: https://api.brex.com
+ configured_html: "Geconfigureerd en klaar voor gebruik. Ga naar het tabblad %{accounts_link} om accounts te beheren en in te stellen."
+ connection_name_label: Verbindingsnaam
+ connection_name_placeholder: Zakelijke betaalrekening
+ default_connection_name: Brex-verbinding
+ disconnect_label: "%{name} loskoppelen"
+ disconnect_confirm: "%{name} loskoppelen?"
+ encryption_warning:
+ title: Databaseversleuteling is niet geconfigureerd
+ message: Configureer de Active Record-versleutelingssleutels voordat u Brex-tokens in productie toevoegt. Zonder versleutelingssleutels slaat Sure de Brex-provider-inloggegevens en momentopnamen op in platte tekst, net als andere providerrecords.
+ instructions:
+ copy_token_html: "Kopieer het token en voeg het hieronder toe als een benoemde verbinding. Sure bewaart het token alleen om dit gezin te synchroniseren."
+ create_token: "Maak een API-token met deze alleen-lezen-scopes: accounts.cash.readonly, accounts.card.readonly, transactions.cash.readonly, transactions.card.readonly"
+ open_tokens: Ga naar de Brex-instellingen voor ontwikkelaars-/API-tokens voor het bedrijf dat u wilt verbinden
+ sign_in_html: "Ga naar %{link} en log in op het account dat u wilt verbinden"
+ keep_token_placeholder: Laat leeg om het huidige token te behouden
+ not_configured: Niet geconfigureerd
+ sandbox_note_html: "Gebruik een aparte benoemde verbinding voor elk Brex-bedrijf/API-token dat u wilt synchroniseren. Laat de Basis-URL leeg voor productie. Staging is beperkt tot door Brex goedgekeurde tests en werkt niet met klanttokens."
+ setup_accounts: Accounts instellen
+ setup_title: "Installatie-instructies:"
+ sync: Synchroniseren
+ token_label: Token
+ token_placeholder: Plak het token hier
+ update_connection: Verbinding bijwerken
+ provider_connection:
+ default_description: Verbinden met uw Brex-account
+ default_name: Brex
+ description: "Verbinden via %{name}"
+ name: "Brex - %{name}"
+ select_accounts:
+ accounts_selected: accounts geselecteerd
+ api_error: "API-fout: %{message}"
+ cancel: Annuleren
+ configure_name_in_brex: "Kan niet importeren - configureer de accountnaam in Brex"
+ description: Selecteer de accounts die u wilt koppelen aan uw %{product_name}-account.
+ link_accounts: Geselecteerde accounts koppelen
+ no_accounts_found: Geen accounts gevonden. Controleer uw API-tokenconfiguratie.
+ no_api_token: Brex-API-token niet gevonden. Configureer het in de Provider-instellingen.
+ no_credentials_configured: Configureer eerst uw Brex-API-token in de Provider-instellingen.
+ no_name_placeholder: "(Geen naam)"
+ select_connection: Kies een Brex-verbinding in de Provider-instellingen.
+ title: Brex-accounts selecteren
+ unexpected_error: Er is een onverwachte fout opgetreden. Probeer het later opnieuw.
+ select_existing_account:
+ account_already_linked: Dit account is al gekoppeld aan een provider
+ all_accounts_already_linked: Alle Brex-accounts zijn al gekoppeld
+ api_error: "API-fout: %{message}"
+ cancel: Annuleren
+ configure_name_in_brex: "Kan niet importeren - configureer de accountnaam in Brex"
+ description: Selecteer een Brex-account om aan dit account te koppelen. Transacties worden automatisch gesynchroniseerd en ontdubbeld.
+ link_account: Account koppelen
+ no_account_specified: Geen account opgegeven
+ no_accounts_found: Geen Brex-accounts gevonden. Controleer uw API-tokenconfiguratie.
+ no_api_token: Brex-API-token niet gevonden. Configureer het in de Provider-instellingen.
+ no_credentials_configured: Configureer eerst uw Brex-API-token in de Provider-instellingen.
+ no_name_placeholder: "(Geen naam)"
+ select_connection: Kies een Brex-verbinding in de Provider-instellingen.
+ title: "%{account_name} koppelen aan Brex"
+ unexpected_error: Er is een onverwachte fout opgetreden. Probeer het later opnieuw.
+ setup_required:
+ description: Voordat u Brex-accounts kunt koppelen, moet u uw Brex-API-token configureren.
+ heading: API-token niet geconfigureerd
+ settings_link: Ga naar Provider-instellingen
+ setup_steps: "Installatiestappen:"
+ steps:
+ enter_token: Voer uw Brex-API-token in
+ find_section_html: "Zoek het gedeelte Brex"
+ open_settings_html: "Ga naar Instellingen > Providers"
+ return_to_link: Kom hier terug om uw accounts te koppelen
+ title: Brex-installatie vereist
+ subtype_select:
+ placeholder:
+ subtype: Selecteer subtype
+ type: Selecteer type
+ link_existing_account:
+ account_already_linked: Dit account is al gekoppeld aan een provider
+ api_error: "API-fout: %{message}"
+ invalid_account_name: Kan account met lege naam niet koppelen
+ missing_parameters: Vereiste parameters ontbreken
+ no_account_specified: Geen account opgegeven
+ no_api_token: Brex-API-token niet gevonden. Configureer het in de Provider-instellingen.
+ provider_account_already_linked: Dit Brex-account is al gekoppeld aan een ander account
+ provider_account_not_found: Brex-account niet gevonden
+ select_connection: Kies een Brex-verbinding voordat u accounts koppelt.
+ success: "%{account_name} succesvol gekoppeld aan Brex"
+ setup_accounts:
+ account_type_label: "Accounttype:"
+ all_accounts_linked: "Al uw Brex-accounts zijn al ingesteld."
+ api_error: "API-fout: %{message}"
+ fetch_failed: "Accounts ophalen mislukt"
+ no_accounts_to_setup: "Geen accounts om in te stellen"
+ no_api_token: Brex-API-token niet gevonden. Configureer het in de Provider-instellingen.
+ account_types:
+ skip: Dit account overslaan
+ depository: Betaal- of spaarrekening
+ credit_card: Creditcard
+ investment: Beleggingsaccount
+ loan: Lening of hypotheek
+ other_asset: Overig actief
+ subtype_labels:
+ depository: "Accountsubtype:"
+ credit_card: ""
+ investment: "Beleggingstype:"
+ loan: "Leningtype:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Creditcards worden automatisch ingesteld als creditcardaccounts."
+ other_asset: "Voor overige activa zijn geen aanvullende opties nodig."
+ subtypes:
+ depository:
+ checking: Betaalrekening
+ savings: Spaarrekening
+ hsa: Zorgspaarrekening
+ cd: Termijndeposito
+ money_market: Geldmarktrekening
+ investment:
+ brokerage: Effectenrekening
+ pension: Pensioen
+ retirement: Pensioenvoorziening
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: Thrift Savings Plan
+ "529_plan": "529-plan"
+ hsa: Zorgspaarrekening
+ mutual_fund: Beleggingsfonds
+ ira: Traditionele IRA
+ roth_ira: Roth IRA
+ angel: Angel
+ loan:
+ mortgage: Hypotheek
+ student: Studielening
+ auto: Autolening
+ other: Overige lening
+ balance: Saldo
+ cancel: Annuleren
+ choose_account_type: "Kies het juiste accounttype voor elk Brex-account:"
+ create_accounts: Accounts aanmaken
+ creating_accounts: Accounts aanmaken...
+ historical_data_range: "Historisch gegevensbereik:"
+ subtitle: Kies de juiste accounttypes voor uw geïmporteerde accounts
+ sync_start_date_help: Selecteer hoe ver terug u de transactiegeschiedenis wilt synchroniseren. Maximaal 3 jaar geschiedenis beschikbaar.
+ sync_start_date_label: "Transacties synchroniseren vanaf:"
+ title: Stel uw Brex-accounts in
+ complete_account_setup:
+ all_skipped: "Alle accounts zijn overgeslagen. Er zijn geen accounts aangemaakt."
+ creation_failed: "Accounts konden niet worden aangemaakt: %{error}"
+ creation_failed_count: "%{count} account(s) konden niet worden aangemaakt."
+ no_accounts: "Geen accounts om in te stellen."
+ partial_skipped: "%{created_count} account(s) succesvol aangemaakt; %{skipped_count} account(s) zijn overgeslagen."
+ partial_success: "%{created_count} account(s) succesvol aangemaakt, maar %{failed_count} account(s) mislukten."
+ success: "%{count} account(s) succesvol aangemaakt."
+ unexpected_error: Er is een onverwachte fout opgetreden.
+ sync:
+ success: Synchronisatie gestart
+ syncer:
+ account_processing_failed:
+ one: "%{count} Brex-account mislukte tijdens de verwerking."
+ other: "%{count} Brex-accounts mislukten tijdens de verwerking."
+ account_sync_failed:
+ one: "%{count} Brex-accountsynchronisatie kon niet worden ingepland."
+ other: "%{count} Brex-accountsynchronisaties konden niet worden ingepland."
+ accounts_need_setup:
+ one: "%{count} account moet worden ingesteld..."
+ other: "%{count} accounts moeten worden ingesteld..."
+ accounts_failed:
+ one: "%{count} Brex-account kon niet worden geïmporteerd."
+ other: "%{count} Brex-accounts konden niet worden geïmporteerd."
+ calculating_balances: Saldi berekenen...
+ checking_account_configuration: Accountconfiguratie controleren...
+ credentials_invalid: Ongeldig Brex-API-token of onvoldoende accountrechten
+ failed: Synchronisatie mislukt. Probeer het opnieuw of neem contact op met de ondersteuning.
+ import_failed: Brex-import mislukt.
+ importing_accounts: Accounts importeren vanuit Brex...
+ processing_transactions: Transacties verwerken...
+ transactions_failed:
+ one: "%{count} Brex-account had fouten bij het importeren van transacties."
+ other: "%{count} Brex-accounts hadden fouten bij het importeren van transacties."
+ update:
+ success: Brex-verbinding bijgewerkt
diff --git a/config/locales/views/brex_items/pt-BR.yml b/config/locales/views/brex_items/pt-BR.yml
new file mode 100644
index 000000000..ae1df3e8e
--- /dev/null
+++ b/config/locales/views/brex_items/pt-BR.yml
@@ -0,0 +1,277 @@
+---
+pt-BR:
+ brex_items:
+ default_connection_name: Conexão da Brex
+ account_metadata:
+ provider: Brex
+ separator: " • "
+ kinds:
+ cash: Dinheiro
+ card: Cartão
+ statuses:
+ ACTIVE: Ativa
+ active: Ativa
+ CLOSED: Fechada
+ closed: Fechada
+ frozen: Congelada
+ FROZEN: Congelada
+ create:
+ success: Conexão da Brex criada com sucesso
+ default_card_name: Cartão Brex
+ default_cash_name: "Brex Cash %{id}"
+ destroy:
+ success: Conexão da Brex removida
+ index:
+ title: Conexões da Brex
+ institution_summary:
+ none: Nenhuma instituição conectada
+ one: "%{name}"
+ count:
+ one: "%{count} instituição"
+ other: "%{count} instituições"
+ sync_status:
+ no_accounts: Nenhuma conta encontrada
+ all_synced:
+ one: "%{count} conta sincronizada"
+ other: "%{count} contas sincronizadas"
+ partial_setup: "%{synced} sincronizadas, %{pending} precisam de configuração"
+ api_error:
+ common_issues: "Problemas comuns:"
+ expired_credentials: Gere um novo token de API na Brex.
+ expired_credentials_label: "Credenciais expiradas:"
+ heading: Não é possível conectar à Brex
+ invalid_token: Verifique seu token de API nas Configurações do provedor.
+ invalid_token_label: "Token de API inválido:"
+ network: Verifique sua conexão com a internet.
+ network_label: "Problema de rede:"
+ permissions: Certifique-se de que seu token tenha os escopos de somente leitura necessários para conta e transações.
+ permissions_label: "Permissões insuficientes:"
+ service: A API da Brex pode estar temporariamente indisponível.
+ service_label: "Serviço indisponível:"
+ settings_link: Verificar Configurações do provedor
+ title: Erro de conexão com a Brex
+ errors:
+ unexpected_error: Ocorreu um erro inesperado. Tente novamente mais tarde.
+ entries:
+ default_name: Transação da Brex
+ loading:
+ loading_message: Carregando contas da Brex...
+ loading_title: Carregando
+ link_accounts:
+ all_already_linked:
+ one: "A conta selecionada (%{names}) já está vinculada"
+ other: "Todas as %{count} contas selecionadas já estão vinculadas: %{names}"
+ api_error: "Erro da API: %{message}"
+ invalid_account_names:
+ one: "Não é possível vincular uma conta com o nome em branco"
+ other: "Não é possível vincular %{count} contas com os nomes em branco"
+ invalid_account_type: Tipo de conta da Brex não suportado
+ link_failed: Não foi possível vincular as contas
+ no_accounts_selected: Selecione pelo menos uma conta
+ no_api_token: Token de API da Brex não encontrado. Configure-o nas Configurações do provedor.
+ partial_invalid: "%{created_count} conta(s) vinculada(s) com sucesso, %{already_linked_count} conta(s) já estavam vinculadas, %{invalid_count} conta(s) tinham nomes inválidos"
+ partial_success: "%{created_count} conta(s) vinculada(s) com sucesso. %{already_linked_count} conta(s) já estavam vinculadas: %{already_linked_names}"
+ select_connection: Escolha uma conexão da Brex antes de vincular contas.
+ success:
+ one: "%{count} conta vinculada com sucesso"
+ other: "%{count} contas vinculadas com sucesso"
+ brex_item:
+ accounts_need_setup: As contas precisam de configuração
+ delete: Excluir conexão
+ deletion_in_progress: exclusão em andamento...
+ error: Erro
+ no_accounts_description: Esta conexão ainda não tem contas vinculadas.
+ no_accounts_title: Sem contas
+ setup_action: Configurar novas contas
+ setup_description: "%{linked} de %{total} contas vinculadas. Escolha os tipos de conta para suas contas da Brex recém-importadas."
+ setup_needed: Novas contas prontas para configurar
+ status: "Sincronizado há %{timestamp}"
+ status_never: Nunca sincronizado
+ status_with_summary: "Sincronizado há %{timestamp} - %{summary}"
+ syncing: Sincronizando...
+ total: Total
+ unlinked: Não vinculada
+ provider_panel:
+ accounts_link: Contas
+ add_connection: Adicionar conexão da Brex
+ base_url_label: URL base (opcional)
+ base_url_placeholder: https://api.brex.com
+ configured_html: "Configurado e pronto para uso. Acesse a aba %{accounts_link} para gerenciar e configurar contas."
+ connection_name_label: Nome da conexão
+ connection_name_placeholder: Conta corrente empresarial
+ default_connection_name: Conexão da Brex
+ disconnect_label: "Desconectar %{name}"
+ disconnect_confirm: "Desconectar %{name}?"
+ encryption_warning:
+ title: A criptografia do banco de dados não está configurada
+ message: Configure as chaves de criptografia do Active Record antes de adicionar tokens da Brex em produção. Sem chaves de criptografia, o Sure armazena as credenciais e os instantâneos do provedor Brex em texto não criptografado, assim como outros registros de provedores.
+ instructions:
+ copy_token_html: "Copie o token e adicione-o abaixo como uma conexão nomeada. O Sure armazena o token apenas para sincronizar esta família."
+ create_token: "Crie um token de API com estes escopos de somente leitura: accounts.cash.readonly, accounts.card.readonly, transactions.cash.readonly, transactions.card.readonly"
+ open_tokens: Acesse as configurações de tokens de desenvolvedor/API da Brex da empresa que você quer conectar
+ sign_in_html: "Acesse %{link} e faça login na conta que você quer conectar"
+ keep_token_placeholder: Deixe em branco para manter o token atual
+ not_configured: Não configurado
+ sandbox_note_html: "Use uma conexão nomeada separada para cada empresa/token de API da Brex que você quer sincronizar. Deixe a URL base em branco para produção. O ambiente de staging é limitado a testes aprovados pela Brex e não funciona com tokens de clientes."
+ setup_accounts: Configurar contas
+ setup_title: "Instruções de configuração:"
+ sync: Sincronizar
+ token_label: Token
+ token_placeholder: Cole o token aqui
+ update_connection: Atualizar conexão
+ provider_connection:
+ default_description: Conectar à sua conta da Brex
+ default_name: Brex
+ description: "Conectar via %{name}"
+ name: "Brex - %{name}"
+ select_accounts:
+ accounts_selected: contas selecionadas
+ api_error: "Erro da API: %{message}"
+ cancel: Cancelar
+ configure_name_in_brex: "Não é possível importar - configure o nome da conta na Brex"
+ description: Selecione as contas que você quer vincular à sua conta do %{product_name}.
+ link_accounts: Vincular contas selecionadas
+ no_accounts_found: Nenhuma conta encontrada. Verifique a configuração do seu token de API.
+ no_api_token: Token de API da Brex não encontrado. Configure-o nas Configurações do provedor.
+ no_credentials_configured: Configure primeiro seu token de API da Brex nas Configurações do provedor.
+ no_name_placeholder: "(Sem nome)"
+ select_connection: Escolha uma conexão da Brex nas Configurações do provedor.
+ title: Selecionar contas da Brex
+ unexpected_error: Ocorreu um erro inesperado. Tente novamente mais tarde.
+ select_existing_account:
+ account_already_linked: Esta conta já está vinculada a um provedor
+ all_accounts_already_linked: Todas as contas da Brex já estão vinculadas
+ api_error: "Erro da API: %{message}"
+ cancel: Cancelar
+ configure_name_in_brex: "Não é possível importar - configure o nome da conta na Brex"
+ description: Selecione uma conta da Brex para vincular a esta conta. As transações serão sincronizadas e desduplicadas automaticamente.
+ link_account: Vincular conta
+ no_account_specified: Nenhuma conta especificada
+ no_accounts_found: Nenhuma conta da Brex encontrada. Verifique a configuração do seu token de API.
+ no_api_token: Token de API da Brex não encontrado. Configure-o nas Configurações do provedor.
+ no_credentials_configured: Configure primeiro seu token de API da Brex nas Configurações do provedor.
+ no_name_placeholder: "(Sem nome)"
+ select_connection: Escolha uma conexão da Brex nas Configurações do provedor.
+ title: "Vincular %{account_name} à Brex"
+ unexpected_error: Ocorreu um erro inesperado. Tente novamente mais tarde.
+ setup_required:
+ description: Antes de poder vincular contas da Brex, você precisa configurar seu token de API da Brex.
+ heading: Token de API não configurado
+ settings_link: Ir para Configurações do provedor
+ setup_steps: "Etapas de configuração:"
+ steps:
+ enter_token: Insira seu token de API da Brex
+ find_section_html: "Encontre a seção Brex"
+ open_settings_html: "Acesse Configurações > Provedores"
+ return_to_link: Volte aqui para vincular suas contas
+ title: Configuração da Brex necessária
+ subtype_select:
+ placeholder:
+ subtype: Selecione o subtipo
+ type: Selecione o tipo
+ link_existing_account:
+ account_already_linked: Esta conta já está vinculada a um provedor
+ api_error: "Erro da API: %{message}"
+ invalid_account_name: Não é possível vincular uma conta com o nome em branco
+ missing_parameters: Parâmetros obrigatórios ausentes
+ no_account_specified: Nenhuma conta especificada
+ no_api_token: Token de API da Brex não encontrado. Configure-o nas Configurações do provedor.
+ provider_account_already_linked: Esta conta da Brex já está vinculada a outra conta
+ provider_account_not_found: Conta da Brex não encontrada
+ select_connection: Escolha uma conexão da Brex antes de vincular contas.
+ success: "%{account_name} vinculada com sucesso à Brex"
+ setup_accounts:
+ account_type_label: "Tipo de conta:"
+ all_accounts_linked: "Todas as suas contas da Brex já foram configuradas."
+ api_error: "Erro da API: %{message}"
+ fetch_failed: "Não foi possível buscar as contas"
+ no_accounts_to_setup: "Nenhuma conta para configurar"
+ no_api_token: Token de API da Brex não encontrado. Configure-o nas Configurações do provedor.
+ account_types:
+ skip: Ignorar esta conta
+ depository: Conta corrente ou poupança
+ credit_card: Cartão de crédito
+ investment: Conta de investimento
+ loan: Empréstimo ou financiamento
+ other_asset: Outro ativo
+ subtype_labels:
+ depository: "Subtipo de conta:"
+ credit_card: ""
+ investment: "Tipo de investimento:"
+ loan: "Tipo de empréstimo:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Os cartões de crédito serão configurados automaticamente como contas de cartão de crédito."
+ other_asset: "Não são necessárias opções adicionais para Outros ativos."
+ subtypes:
+ depository:
+ checking: Conta corrente
+ savings: Conta poupança
+ hsa: Conta poupança de saúde
+ cd: Certificado de depósito
+ money_market: Conta do mercado monetário
+ investment:
+ brokerage: Conta de corretagem
+ pension: Pensão
+ retirement: Aposentadoria
+ "401k": "401(k)"
+ roth_401k: "Roth 401(k)"
+ "403b": "403(b)"
+ tsp: Thrift Savings Plan
+ "529_plan": "Plano 529"
+ hsa: Conta poupança de saúde
+ mutual_fund: Fundo de investimento
+ ira: IRA tradicional
+ roth_ira: Roth IRA
+ angel: Investimento-anjo
+ loan:
+ mortgage: Financiamento imobiliário
+ student: Empréstimo estudantil
+ auto: Financiamento de veículo
+ other: Outro empréstimo
+ balance: Saldo
+ cancel: Cancelar
+ choose_account_type: "Escolha o tipo de conta correto para cada conta da Brex:"
+ create_accounts: Criar contas
+ creating_accounts: Criando contas...
+ historical_data_range: "Intervalo de dados históricos:"
+ subtitle: Escolha os tipos de conta corretos para suas contas importadas
+ sync_start_date_help: Selecione até que ponto no passado você quer sincronizar o histórico de transações. Há no máximo 3 anos de histórico disponíveis.
+ sync_start_date_label: "Começar a sincronizar transações a partir de:"
+ title: Configure suas contas da Brex
+ complete_account_setup:
+ all_skipped: "Todas as contas foram ignoradas. Nenhuma conta foi criada."
+ creation_failed: "Não foi possível criar as contas: %{error}"
+ creation_failed_count: "Não foi possível criar %{count} conta(s)."
+ no_accounts: "Nenhuma conta para configurar."
+ partial_skipped: "%{created_count} conta(s) criada(s) com sucesso; %{skipped_count} conta(s) foram ignoradas."
+ partial_success: "%{created_count} conta(s) criada(s) com sucesso, mas %{failed_count} conta(s) falharam."
+ success: "%{count} conta(s) criada(s) com sucesso."
+ unexpected_error: Ocorreu um erro inesperado.
+ sync:
+ success: Sincronização iniciada
+ syncer:
+ account_processing_failed:
+ one: "%{count} conta da Brex falhou durante o processamento."
+ other: "%{count} contas da Brex falharam durante o processamento."
+ account_sync_failed:
+ one: "Não foi possível agendar a sincronização de %{count} conta da Brex."
+ other: "Não foi possível agendar a sincronização de %{count} contas da Brex."
+ accounts_need_setup:
+ one: "%{count} conta precisa de configuração..."
+ other: "%{count} contas precisam de configuração..."
+ accounts_failed:
+ one: "Não foi possível importar %{count} conta da Brex."
+ other: "Não foi possível importar %{count} contas da Brex."
+ calculating_balances: Calculando saldos...
+ checking_account_configuration: Verificando a configuração da conta...
+ credentials_invalid: Token de API da Brex inválido ou permissões de conta insuficientes
+ failed: A sincronização falhou. Tente novamente ou entre em contato com o suporte.
+ import_failed: A importação da Brex falhou.
+ importing_accounts: Importando contas da Brex...
+ processing_transactions: Processando transações...
+ transactions_failed:
+ one: "%{count} conta da Brex teve falhas na importação de transações."
+ other: "%{count} contas da Brex tiveram falhas na importação de transações."
+ update:
+ success: Conexão da Brex atualizada
diff --git a/config/locales/views/goals/en.yml b/config/locales/views/goals/en.yml
index cf2a5a57a..c8b04b9d2 100644
--- a/config/locales/views/goals/en.yml
+++ b/config/locales/views/goals/en.yml
@@ -153,6 +153,7 @@ en:
today_marker: Today
tooltip_projected: "Projected: %{amount}"
tooltip_saved: "Saved: %{amount}"
+ tooltip_target_relation: "%{percent}% of %{target} target"
catch_up:
title: "Save %{amount}/mo more to catch up"
body: "Current pace %{avg}/mo · required %{required}/mo to hit your target."
diff --git a/config/locales/views/ibkr_items/nl.yml b/config/locales/views/ibkr_items/nl.yml
new file mode 100644
index 000000000..56f6cd01d
--- /dev/null
+++ b/config/locales/views/ibkr_items/nl.yml
@@ -0,0 +1,92 @@
+---
+nl:
+ providers:
+ ibkr:
+ name: Interactive Brokers
+ connection_description: Verbind een Interactive Brokers Flex Web Service-rapport
+ institution_name: Interactive Brokers
+ ibkr_items:
+ defaults:
+ name: Interactive Brokers
+ ibkr_item:
+ deletion_in_progress: Verwijderen bezig
+ flex_web_service: Flex Web Service
+ syncing: Synchroniseren
+ requires_update: Inloggegevens vereisen aandacht
+ error: Fout
+ synced: Gesynchroniseerd %{time} geleden. %{summary}.
+ never_synced: Nooit gesynchroniseerd.
+ setup_accounts: Accounts instellen
+ delete: Verwijderen
+ accounts_need_setup: Accounts moeten worden ingesteld
+ accounts_need_setup_description: Sommige accounts van IBKR moeten worden gekoppeld aan Sure-accounts.
+ no_accounts_discovered: Nog geen IBKR-accounts gevonden.
+ no_accounts_discovered_description: Voer een synchronisatie uit nadat u uw Flex-query hebt geconfigureerd om accounts te vinden.
+ setup_accounts:
+ page_title: Interactive Brokers-accounts instellen
+ dialog_title: Stel uw Interactive Brokers-accounts in
+ subtitle: Selecteer welke IBKR-effectenaccounts u wilt koppelen.
+ info_box:
+ title: IBKR Flex-query-import
+ items:
+ item_1: Posities met actuele koersen en aantallen
+ item_2: Kostprijsbasis per positie
+ item_3: Transacties, dividenden, commissies en kasstortingen of -opnames
+ warning: Historische activiteit is beperkt tot het rapportvenster van de Flex-query
+ status:
+ fetching_accounts: Accounts ophalen van Interactive Brokers...
+ no_accounts_found_title: Geen accounts gevonden.
+ no_accounts_found_description: Sure kon geen IBKR-accounts vinden in het meest recente Flex-rapport.
+ available_accounts:
+ title: Beschikbare accounts
+ account_type_investment: Beleggingen
+ account_summary: "%{account_type} • Saldo: %{balance}"
+ account_id: "Account-ID: %{account_id}"
+ link_existing:
+ description: Of koppel een gevonden IBKR-account aan een bestaand handmatig beleggingsaccount.
+ manual_account_option: "%{name} (%{balance})"
+ select_prompt: Selecteer een account...
+ linked_accounts:
+ title: Al gekoppeld
+ linked_to_html: "Gekoppeld aan: %{account}"
+ buttons:
+ refresh: Vernieuwen
+ cancel: Annuleren
+ back_to_settings: Terug naar instellingen
+ create_selected_accounts: Geselecteerde accounts aanmaken
+ link: Koppelen
+ done: Klaar
+ sync_status:
+ no_accounts: Nog geen IBKR-accounts gevonden
+ all_linked:
+ one: 1 account gekoppeld
+ other: "%{count} accounts gekoppeld"
+ partial: "%{linked} gekoppeld, %{unlinked} moeten worden ingesteld"
+ select_existing_account:
+ title: Interactive Brokers-account koppelen
+ no_accounts_available: Er zijn nog geen niet-gekoppelde Interactive Brokers-accounts beschikbaar.
+ run_sync_hint: "Voer een synchronisatie uit via Instellingen > Providers nadat u uw Flex-query hebt bijgewerkt."
+ wait_for_sync: Wacht tot de synchronisatie voor accountdetectie is voltooid.
+ balance: Saldo
+ link: Koppelen
+ cancel: Annuleren
+ create:
+ success: Interactive Brokers succesvol geconfigureerd.
+ update:
+ success: Interactive Brokers-configuratie succesvol bijgewerkt.
+ destroy:
+ success: Interactive Brokers-verbinding is in de wachtrij voor verwijdering geplaatst.
+ select_accounts:
+ not_configured: Interactive Brokers is niet geconfigureerd.
+ link_existing_account:
+ not_found: Account of Interactive Brokers-configuratie niet gevonden.
+ only_manual_investment: Alleen handmatige beleggingsaccounts kunnen aan Interactive Brokers worden gekoppeld.
+ already_linked: Dit Interactive Brokers-account is al gekoppeld.
+ success: Succesvol gekoppeld aan Interactive Brokers-account.
+ failed: Koppelen aan Interactive Brokers-account mislukt.
+ complete_account_setup:
+ success:
+ one: "%{count} Interactive Brokers-account succesvol aangemaakt."
+ other: "%{count} Interactive Brokers-accounts succesvol aangemaakt."
+ none_selected: Er zijn geen accounts geselecteerd.
+ none_created: Er zijn geen accounts aangemaakt.
diff --git a/config/locales/views/ibkr_items/pt-BR.yml b/config/locales/views/ibkr_items/pt-BR.yml
new file mode 100644
index 000000000..4eeaa4962
--- /dev/null
+++ b/config/locales/views/ibkr_items/pt-BR.yml
@@ -0,0 +1,92 @@
+---
+pt-BR:
+ providers:
+ ibkr:
+ name: Interactive Brokers
+ connection_description: Conecte um relatório do Flex Web Service da Interactive Brokers
+ institution_name: Interactive Brokers
+ ibkr_items:
+ defaults:
+ name: Interactive Brokers
+ ibkr_item:
+ deletion_in_progress: Exclusão em andamento
+ flex_web_service: Flex Web Service
+ syncing: Sincronizando
+ requires_update: As credenciais exigem atenção
+ error: Erro
+ synced: Sincronizado há %{time}. %{summary}.
+ never_synced: Nunca sincronizado.
+ setup_accounts: Configurar contas
+ delete: Excluir
+ accounts_need_setup: As contas precisam de configuração
+ accounts_need_setup_description: Algumas contas da IBKR precisam ser vinculadas a contas do Sure.
+ no_accounts_discovered: Nenhuma conta da IBKR encontrada ainda.
+ no_accounts_discovered_description: Execute uma sincronização após configurar sua consulta Flex para encontrar contas.
+ setup_accounts:
+ page_title: Configurar contas da Interactive Brokers
+ dialog_title: Configure suas contas da Interactive Brokers
+ subtitle: Selecione quais contas de corretagem da IBKR você quer vincular.
+ info_box:
+ title: Importação por consulta Flex da IBKR
+ items:
+ item_1: Posições com preços e quantidades atuais
+ item_2: Custo de aquisição por posição
+ item_3: Negociações, dividendos, comissões e depósitos ou saques em dinheiro
+ warning: A atividade histórica é limitada ao período do relatório da consulta Flex
+ status:
+ fetching_accounts: Buscando contas na Interactive Brokers...
+ no_accounts_found_title: Nenhuma conta encontrada.
+ no_accounts_found_description: O Sure não conseguiu encontrar nenhuma conta da IBKR no relatório Flex mais recente.
+ available_accounts:
+ title: Contas disponíveis
+ account_type_investment: Investimento
+ account_summary: "%{account_type} • Saldo: %{balance}"
+ account_id: "ID da conta: %{account_id}"
+ link_existing:
+ description: Ou vincule uma conta da IBKR encontrada a uma conta de investimento manual existente.
+ manual_account_option: "%{name} (%{balance})"
+ select_prompt: Selecione uma conta...
+ linked_accounts:
+ title: Já vinculadas
+ linked_to_html: "Vinculada a: %{account}"
+ buttons:
+ refresh: Atualizar
+ cancel: Cancelar
+ back_to_settings: Voltar às Configurações
+ create_selected_accounts: Criar contas selecionadas
+ link: Vincular
+ done: Concluído
+ sync_status:
+ no_accounts: Nenhuma conta da IBKR encontrada ainda
+ all_linked:
+ one: 1 conta vinculada
+ other: "%{count} contas vinculadas"
+ partial: "%{linked} vinculadas, %{unlinked} precisam de configuração"
+ select_existing_account:
+ title: Vincular conta da Interactive Brokers
+ no_accounts_available: Ainda não há contas da Interactive Brokers não vinculadas disponíveis.
+ run_sync_hint: "Execute uma sincronização em Configurações > Provedores após atualizar sua consulta Flex."
+ wait_for_sync: Aguarde a conclusão da sincronização de descoberta de contas.
+ balance: Saldo
+ link: Vincular
+ cancel: Cancelar
+ create:
+ success: Interactive Brokers configurada com sucesso.
+ update:
+ success: Configuração da Interactive Brokers atualizada com sucesso.
+ destroy:
+ success: Conexão da Interactive Brokers programada para exclusão.
+ select_accounts:
+ not_configured: A Interactive Brokers não está configurada.
+ link_existing_account:
+ not_found: Conta ou configuração da Interactive Brokers não encontrada.
+ only_manual_investment: Apenas contas de investimento manuais podem ser vinculadas à Interactive Brokers.
+ already_linked: Esta conta da Interactive Brokers já está vinculada.
+ success: Vinculado com sucesso à conta da Interactive Brokers.
+ failed: Não foi possível vincular a conta da Interactive Brokers.
+ complete_account_setup:
+ success:
+ one: "%{count} conta da Interactive Brokers criada com sucesso."
+ other: "%{count} contas da Interactive Brokers criadas com sucesso."
+ none_selected: Nenhuma conta foi selecionada.
+ none_created: Nenhuma conta foi criada.
diff --git a/config/locales/views/kraken_items/nl.yml b/config/locales/views/kraken_items/nl.yml
new file mode 100644
index 000000000..d57362e04
--- /dev/null
+++ b/config/locales/views/kraken_items/nl.yml
@@ -0,0 +1,85 @@
+---
+nl:
+ kraken_items:
+ provider_connection:
+ default_name: Kraken
+ default_description: Koppelen aan een Kraken-beursaccount
+ name: "Kraken - %{name}"
+ description: "Koppelen aan %{name}"
+ create:
+ default_name: Kraken
+ success: Succesvol verbonden met Kraken. Uw beursaccount wordt gesynchroniseerd.
+ update:
+ success: Kraken-verbinding succesvol bijgewerkt.
+ destroy:
+ success: Kraken-verbinding is in de wachtrij voor verwijdering geplaatst.
+ select_accounts:
+ select_connection: Kies een Kraken-verbinding in de Provider-instellingen.
+ no_credentials_configured: Voeg Kraken-API-inloggegevens toe voordat u accounts instelt.
+ link_accounts:
+ select_connection: Kies een Kraken-verbinding voordat u accounts koppelt.
+ select_existing_account:
+ title: Kraken-account koppelen
+ no_accounts_found: Geen Kraken-accounts gevonden.
+ wait_for_sync: Wacht tot Kraken klaar is met synchroniseren.
+ check_provider_health: Controleer of uw Kraken-API-inloggegevens geldig zijn.
+ link: Koppelen
+ cancel: Annuleren
+ link_existing_account:
+ success: Succesvol gekoppeld aan Kraken-account
+ select_connection: Kies een Kraken-verbinding voordat u accounts koppelt.
+ errors:
+ only_manual: Alleen handmatige crypto-beursaccounts zonder bestaande providerkoppeling kunnen aan Kraken worden gekoppeld
+ invalid_kraken_account: Ongeldig Kraken-account
+ kraken_account_already_linked: Dit Kraken-account is al gekoppeld
+ setup_accounts:
+ title: Kraken-account importeren
+ subtitle: Selecteer het beursaccount dat u wilt volgen
+ instructions: Kraken importeert voor deze verbinding één gecombineerd crypto-beursaccount, met alleen posities en spot-transactie-uitvoeringen.
+ no_accounts: Alle Kraken-accounts zijn geïmporteerd.
+ accounts_count:
+ one: "%{count} account beschikbaar"
+ other: "%{count} accounts beschikbaar"
+ select_all: Alles selecteren
+ import_selected: Geselecteerde importeren
+ cancel: Annuleren
+ creating: Importeren...
+ complete_account_setup:
+ success:
+ one: "%{count} account geïmporteerd"
+ other: "%{count} accounts geïmporteerd"
+ none_selected: Geen accounts geselecteerd
+ no_accounts: Geen accounts om te importeren
+ kraken_item:
+ provider_name: Kraken
+ syncing: Synchroniseren...
+ reconnect: Inloggegevens moeten worden bijgewerkt
+ deletion_in_progress: Verwijderen...
+ sync_status:
+ no_accounts: Geen accounts gevonden
+ all_synced:
+ one: "%{count} account gesynchroniseerd"
+ other: "%{count} accounts gesynchroniseerd"
+ partial_sync: "%{linked_count} gesynchroniseerd, %{unlinked_count} moeten worden ingesteld"
+ status: "Laatst gesynchroniseerd %{timestamp} geleden"
+ status_with_summary: "Laatst gesynchroniseerd %{timestamp} geleden - %{summary}"
+ status_never: Nooit gesynchroniseerd
+ delete: Verwijderen
+ no_accounts_title: Geen accounts gevonden
+ no_accounts_message: Uw Kraken-beursaccount verschijnt hier na de synchronisatie.
+ setup_needed: Account klaar om te importeren
+ setup_description: Importeer deze Kraken-verbinding als een crypto-beursaccount.
+ setup_action: Account importeren
+ import_accounts_menu: Account importeren
+ stale_rate_warning: "Het saldo is bij benadering omdat de exacte wisselkoers voor %{date} niet beschikbaar was. Wordt bijgewerkt bij de volgende synchronisatie."
+ kraken_item:
+ syncer:
+ checking_credentials: Inloggegevens controleren...
+ credentials_invalid: Ongeldige Kraken-API-inloggegevens. Controleer uw API-sleutel en secret.
+ importing_accounts: Accounts importeren vanuit Kraken...
+ checking_configuration: Accountconfiguratie controleren...
+ accounts_need_setup:
+ one: "%{count} account moet worden ingesteld"
+ other: "%{count} accounts moeten worden ingesteld"
+ processing_accounts: Accountgegevens verwerken...
+ calculating_balances: Saldi berekenen...
diff --git a/config/locales/views/kraken_items/pt-BR.yml b/config/locales/views/kraken_items/pt-BR.yml
new file mode 100644
index 000000000..6a6bef331
--- /dev/null
+++ b/config/locales/views/kraken_items/pt-BR.yml
@@ -0,0 +1,85 @@
+---
+pt-BR:
+ kraken_items:
+ provider_connection:
+ default_name: Kraken
+ default_description: Vincular a uma conta da corretora Kraken
+ name: "Kraken - %{name}"
+ description: "Vincular a %{name}"
+ create:
+ default_name: Kraken
+ success: Conexão com a Kraken estabelecida com sucesso. Sua conta da corretora está sendo sincronizada.
+ update:
+ success: Conexão da Kraken atualizada com sucesso.
+ destroy:
+ success: Conexão da Kraken programada para exclusão.
+ select_accounts:
+ select_connection: Escolha uma conexão da Kraken nas Configurações do provedor.
+ no_credentials_configured: Adicione as credenciais da API da Kraken antes de configurar as contas.
+ link_accounts:
+ select_connection: Escolha uma conexão da Kraken antes de vincular contas.
+ select_existing_account:
+ title: Vincular conta da Kraken
+ no_accounts_found: Nenhuma conta da Kraken encontrada.
+ wait_for_sync: Aguarde a Kraken concluir a sincronização.
+ check_provider_health: Verifique se suas credenciais da API da Kraken são válidas.
+ link: Vincular
+ cancel: Cancelar
+ link_existing_account:
+ success: Vinculado com sucesso à conta da Kraken
+ select_connection: Escolha uma conexão da Kraken antes de vincular contas.
+ errors:
+ only_manual: Apenas contas manuais de corretora de criptomoedas sem um vínculo de provedor existente podem ser vinculadas à Kraken
+ invalid_kraken_account: Conta da Kraken inválida
+ kraken_account_already_linked: Esta conta da Kraken já está vinculada
+ setup_accounts:
+ title: Importar conta da Kraken
+ subtitle: Selecione a conta da corretora que você quer acompanhar
+ instructions: A Kraken importa uma única conta combinada de corretora de criptomoedas para esta conexão, apenas com posições e execuções de negociações à vista.
+ no_accounts: Todas as contas da Kraken foram importadas.
+ accounts_count:
+ one: "%{count} conta disponível"
+ other: "%{count} contas disponíveis"
+ select_all: Selecionar todas
+ import_selected: Importar selecionadas
+ cancel: Cancelar
+ creating: Importando...
+ complete_account_setup:
+ success:
+ one: "%{count} conta importada"
+ other: "%{count} contas importadas"
+ none_selected: Nenhuma conta selecionada
+ no_accounts: Nenhuma conta para importar
+ kraken_item:
+ provider_name: Kraken
+ syncing: Sincronizando...
+ reconnect: As credenciais precisam ser atualizadas
+ deletion_in_progress: Excluindo...
+ sync_status:
+ no_accounts: Nenhuma conta encontrada
+ all_synced:
+ one: "%{count} conta sincronizada"
+ other: "%{count} contas sincronizadas"
+ partial_sync: "%{linked_count} sincronizadas, %{unlinked_count} precisam de configuração"
+ status: "Sincronizado há %{timestamp}"
+ status_with_summary: "Sincronizado há %{timestamp} - %{summary}"
+ status_never: Nunca sincronizado
+ delete: Excluir
+ no_accounts_title: Nenhuma conta encontrada
+ no_accounts_message: Sua conta da corretora Kraken aparecerá aqui após a sincronização.
+ setup_needed: Conta pronta para importar
+ setup_description: Importe esta conexão da Kraken como uma conta de corretora de criptomoedas.
+ setup_action: Importar conta
+ import_accounts_menu: Importar conta
+ stale_rate_warning: "O saldo é aproximado porque a taxa de câmbio exata para %{date} não estava disponível. Será atualizado na próxima sincronização."
+ kraken_item:
+ syncer:
+ checking_credentials: Verificando credenciais...
+ credentials_invalid: Credenciais da API da Kraken inválidas. Verifique sua chave de API e seu segredo.
+ importing_accounts: Importando contas da Kraken...
+ checking_configuration: Verificando a configuração da conta...
+ accounts_need_setup:
+ one: "%{count} conta precisa de configuração"
+ other: "%{count} contas precisam de configuração"
+ processing_accounts: Processando os dados da conta...
+ calculating_balances: Calculando saldos...
diff --git a/config/locales/views/messages/nl.yml b/config/locales/views/messages/nl.yml
new file mode 100644
index 000000000..29a4d2ecb
--- /dev/null
+++ b/config/locales/views/messages/nl.yml
@@ -0,0 +1,6 @@
+---
+nl:
+ messages:
+ chat_form:
+ placeholder: "Vraag wat u wilt ..."
+ disclaimer: "AI-antwoorden zijn alleen informatief. Geen financieel advies!"
diff --git a/config/locales/views/messages/pt-BR.yml b/config/locales/views/messages/pt-BR.yml
new file mode 100644
index 000000000..b4779fe6d
--- /dev/null
+++ b/config/locales/views/messages/pt-BR.yml
@@ -0,0 +1,6 @@
+---
+pt-BR:
+ messages:
+ chat_form:
+ placeholder: "Pergunte qualquer coisa ..."
+ disclaimer: "As respostas da IA são apenas informativas. Não constituem aconselhamento financeiro!"
diff --git a/config/locales/views/pending_duplicate_merges/nl.yml b/config/locales/views/pending_duplicate_merges/nl.yml
new file mode 100644
index 000000000..328fba74b
--- /dev/null
+++ b/config/locales/views/pending_duplicate_merges/nl.yml
@@ -0,0 +1,21 @@
+---
+nl:
+ pending_duplicate_merges:
+ create:
+ no_posted_selected: Selecteer een geboekte transactie om mee samen te voegen
+ invalid_transaction: Ongeldige transactie geselecteerd om samen te voegen
+ merge_success: Lopende transactie samengevoegd met geboekte transactie
+ merge_failed: Transacties konden niet worden samengevoegd
+ set_transaction:
+ pending_only: Deze functie is alleen beschikbaar voor lopende transacties
+ new:
+ title: Samenvoegen met geboekte transactie
+ warning_title: Handmatig duplicaten samenvoegen
+ warning_description: Gebruik dit om een lopende transactie handmatig samen te voegen met de geboekte versie. Hiermee wordt de lopende transactie verwijderd en alleen de geboekte behouden.
+ pending_transaction: Lopende transactie
+ select_posted: Selecteer de geboekte transactie om mee samen te voegen
+ showing_range: "%{start} - %{end} worden weergegeven"
+ previous: "← Vorige 10"
+ next: "Volgende 10 →"
+ no_candidates: Geen geboekte transacties gevonden in dit account.
+ submit_button: Transacties samenvoegen
diff --git a/config/locales/views/pending_duplicate_merges/pt-BR.yml b/config/locales/views/pending_duplicate_merges/pt-BR.yml
new file mode 100644
index 000000000..24a118e60
--- /dev/null
+++ b/config/locales/views/pending_duplicate_merges/pt-BR.yml
@@ -0,0 +1,21 @@
+---
+pt-BR:
+ pending_duplicate_merges:
+ create:
+ no_posted_selected: Selecione uma transação lançada para mesclar
+ invalid_transaction: Transação selecionada para mesclagem inválida
+ merge_success: Transação pendente mesclada com a transação lançada
+ merge_failed: Não foi possível mesclar as transações
+ set_transaction:
+ pending_only: Este recurso está disponível apenas para transações pendentes
+ new:
+ title: Mesclar com transação lançada
+ warning_title: Mesclagem manual de duplicatas
+ warning_description: Use isto para mesclar manualmente uma transação pendente com sua versão lançada. Isso excluirá a transação pendente e manterá apenas a lançada.
+ pending_transaction: Transação pendente
+ select_posted: Selecione a transação lançada para mesclar
+ showing_range: "Exibindo %{start} - %{end}"
+ previous: "← 10 anteriores"
+ next: "Próximas 10 →"
+ no_candidates: Nenhuma transação lançada encontrada nesta conta.
+ submit_button: Mesclar transações
diff --git a/config/locales/views/securities/nl.yml b/config/locales/views/securities/nl.yml
new file mode 100644
index 000000000..7b5f10074
--- /dev/null
+++ b/config/locales/views/securities/nl.yml
@@ -0,0 +1,14 @@
+---
+nl:
+ securities:
+ combobox:
+ display: "%{symbol} - %{name} (%{exchange})"
+ exchange_label: "%{symbol} (%{exchange})"
+ providers:
+ twelve_data: Twelve Data
+ yahoo_finance: Yahoo Finance
+ tiingo: Tiingo
+ eodhd: EODHD
+ alpha_vantage: Alpha Vantage
+ mfapi: MFAPI.in
+ binance_public: Binance
diff --git a/config/locales/views/securities/pt-BR.yml b/config/locales/views/securities/pt-BR.yml
new file mode 100644
index 000000000..fbf4cf46b
--- /dev/null
+++ b/config/locales/views/securities/pt-BR.yml
@@ -0,0 +1,14 @@
+---
+pt-BR:
+ securities:
+ combobox:
+ display: "%{symbol} - %{name} (%{exchange})"
+ exchange_label: "%{symbol} (%{exchange})"
+ providers:
+ twelve_data: Twelve Data
+ yahoo_finance: Yahoo Finance
+ tiingo: Tiingo
+ eodhd: EODHD
+ alpha_vantage: Alpha Vantage
+ mfapi: MFAPI.in
+ binance_public: Binance
diff --git a/config/locales/views/sophtron_items/nl.yml b/config/locales/views/sophtron_items/nl.yml
new file mode 100644
index 000000000..1dad593af
--- /dev/null
+++ b/config/locales/views/sophtron_items/nl.yml
@@ -0,0 +1,313 @@
+---
+nl:
+ sophtron_items:
+ defaults:
+ name: Sophtron-verbinding
+ new:
+ title: Sophtron verbinden
+ user_id: Gebruikers-ID
+ user_id_placeholder: plak uw Sophtron-gebruikers-ID
+ access_key: Toegangssleutel
+ access_key_placeholder: plak uw Sophtron-toegangssleutel
+ connect: Verbinden
+ cancel: Annuleren
+ create:
+ success: Sophtron-verbinding succesvol aangemaakt
+ destroy:
+ success: Sophtron-verbinding verwijderd
+ update:
+ success: Sophtron-verbinding succesvol bijgewerkt! Uw accounts worden opnieuw verbonden.
+ errors:
+ blank_user_id: Voer een Sophtron-gebruikers-ID in.
+ invalid_user_id: Ongeldige gebruikers-ID. Controleer of u de volledige gebruikers-ID van Sophtron hebt gekopieerd.
+ user_id_compromised: De gebruikers-ID is mogelijk gecompromitteerd, verlopen of al gebruikt. Maak een nieuwe aan.
+ blank_access_key: Voer een Sophtron-toegangssleutel in.
+ invalid_access_key: Ongeldige toegangssleutel. Controleer of u de volledige toegangssleutel van Sophtron hebt gekopieerd.
+ access_key_compromised: De toegangssleutel is mogelijk gecompromitteerd, verlopen of al gebruikt. Maak een nieuwe aan.
+ update_failed: "Verbinding kon niet worden bijgewerkt: %{message}"
+ unexpected: Er is een onverwachte fout opgetreden. Probeer het opnieuw of neem contact op met de ondersteuning.
+ edit:
+ user_id:
+ label: "Sophtron-gebruikers-ID:"
+ placeholder: "Plak hier uw Sophtron-gebruikers-ID..."
+ help_text: "De gebruikers-ID moet een lange tekenreeks zijn die begint met letters en cijfers"
+ access_key:
+ label: "Sophtron-toegangssleutel:"
+ placeholder: "Plak hier uw Sophtron-toegangssleutel..."
+ help_text: "De toegangssleutel moet een lange tekenreeks zijn die begint met letters en cijfers"
+ index:
+ title: Sophtron-verbindingen
+ loading:
+ loading_message: Sophtron-accounts laden...
+ loading_title: Laden
+ link_accounts:
+ all_already_linked:
+ one: "Het geselecteerde account (%{names}) is al gekoppeld"
+ other: "Alle %{count} geselecteerde accounts zijn al gekoppeld: %{names}"
+ api_error: "API-verbindingsfout"
+ invalid_account_names:
+ one: "Kan account met lege naam niet koppelen"
+ other: "Kan %{count} accounts met lege namen niet koppelen"
+ link_failed: Accounts koppelen mislukt
+ no_accounts_selected: Selecteer ten minste één account
+ partial_invalid: "%{created_count} account(s) succesvol gekoppeld, %{already_linked_count} waren al gekoppeld, %{invalid_count} account(s) hadden ongeldige namen"
+ partial_success: "%{created_count} account(s) succesvol gekoppeld. %{already_linked_count} account(s) waren al gekoppeld: %{already_linked_names}"
+ success:
+ one: "%{count} account succesvol gekoppeld"
+ other: "%{count} accounts succesvol gekoppeld"
+ no_credentials_configured: "Configureer eerst uw Sophtron-API-gebruikers-ID en -toegangssleutel in de Provider-instellingen."
+ no_accounts_found: Geen accounts gevonden. Controleer uw API-sleutelconfiguratie.
+ no_access_key: Sophtron-toegangssleutel is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_user_id: Sophtron-gebruikers-ID is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_institution_connected: Verbind eerst een bankinstelling met Sophtron.
+ connect:
+ cancel: Annuleren
+ captcha: Captcha
+ connect: Verbinden
+ institution_search_label: Instelling
+ institution_search_placeholder: Zoeken op banknaam
+ no_institutions: Geen overeenkomende instellingen gevonden.
+ password: Wachtwoord
+ search: Zoeken
+ search_too_short: Voer ten minste twee tekens in om te zoeken.
+ title: Sophtron-instelling verbinden
+ username: Gebruikersnaam
+ connect_institution:
+ api_error: "Sophtron-verbinding mislukt: %{message}"
+ missing_parameters: Selecteer een instelling en voer uw bankinloggegevens in.
+ connection_status:
+ api_error: "API-verbindingsfout: %{message}"
+ attempt: "Poging %{attempt} van %{max}"
+ check_again: Opnieuw controleren
+ failed: Sophtron kon deze instellingsverbinding niet voltooien.
+ failed_timeout: Sophtron kreeg een time-out terwijl de instelling de aanmelding voltooide.
+ timeout: Sophtron heeft het verbinden niet binnen de verwachte tijd voltooid. U kunt opnieuw controleren of later opnieuw proberen te verbinden.
+ title: Sophtron verbinden
+ waiting: Sophtron is nog verbinding aan het maken met uw instelling.
+ mfa:
+ captcha: Captcha-tekst
+ captcha_alt: Sophtron-captcha
+ phone_confirmed: Ik heb telefonisch bevestigd
+ submit: Verzenden
+ title: Sophtron-verificatie
+ token: Verificatiecode
+ submit_mfa:
+ api_error: "Verificatie mislukt: %{message}"
+ invalid_security_answers: Beveiligingsantwoorden ontbreken of zijn te lang.
+ unknown_challenge: Onbekende Sophtron-verificatiestap.
+ sophtron_item:
+ accounts_need_setup: Accounts moeten worden ingesteld
+ automatic_sync: Automatische synchronisatie gebruiken
+ delete: Verbinding verwijderen
+ deletion_in_progress: verwijderen bezig...
+ error: Fout
+ no_accounts_description: Deze verbinding heeft nog geen gekoppelde accounts.
+ no_accounts_title: Geen accounts
+ manual_sync: Handmatige synchronisatie
+ manual_sync_action: Handmatige synchronisatie vereisen
+ manual_sync_action_for: "Handmatige synchronisatie vereisen voor %{institution}"
+ automatic_sync_for: "Automatische synchronisatie gebruiken voor %{institution}"
+ setup_action: Nieuwe accounts instellen
+ setup_description: "%{linked} van %{total} accounts gekoppeld. Kies accounttypes voor uw nieuw geïmporteerde Sophtron-accounts."
+ setup_needed: Nieuwe accounts klaar om in te stellen
+ status: "Gesynchroniseerd %{timestamp} geleden"
+ status_never: Nooit gesynchroniseerd
+ status_with_summary: "Laatst gesynchroniseerd %{timestamp} geleden • %{summary}"
+ sync_now: Nu synchroniseren
+ syncing: Synchroniseren...
+ total: Totaal
+ unlinked: Niet gekoppeld
+ preload_accounts:
+ preload_accounts: accounts vooraf laden
+ api_error: "API-verbindingsfout"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ no_credentials_configured: "Configureer eerst uw Sophtron-API-gebruikers-ID en -toegangssleutel in de Provider-instellingen."
+ no_accounts_found: Geen accounts gevonden. Controleer uw API-sleutelconfiguratie.
+ no_access_key: Sophtron-toegangssleutel is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_user_id: Sophtron-gebruikers-ID is niet geconfigureerd. Configureer deze in de Instellingen.
+ select_accounts:
+ accounts_selected: accounts geselecteerd
+ api_error: "API-verbindingsfout"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ cancel: Annuleren
+ configure_name_in_sophtron: "Kan niet importeren - configureer de accountnaam in Sophtron"
+ description: Selecteer de accounts die u wilt koppelen aan uw %{product_name}-account.
+ link_accounts: Geselecteerde accounts koppelen
+ no_accounts_found: Geen accounts gevonden. Controleer uw API-sleutelconfiguratie.
+ no_access_key: Sophtron-toegangssleutel is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_user_id: Sophtron-gebruikers-ID is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_credentials_configured: "Configureer eerst uw Sophtron-API-gebruikers-ID en -toegangssleutel in de Provider-instellingen."
+ no_institution_connected: Verbind eerst een bankinstelling met Sophtron.
+ no_name_placeholder: "(Geen naam)"
+ title: Sophtron-accounts selecteren
+ select_existing_account:
+ account_already_linked: Dit account is al gekoppeld aan een provider
+ all_accounts_already_linked: Alle Sophtron-accounts zijn al gekoppeld
+ api_error: "API-verbindingsfout"
+ cancel: Annuleren
+ configure_name_in_sophtron: "Kan niet importeren - configureer de accountnaam in Sophtron"
+ description: Selecteer een Sophtron-account om aan dit account te koppelen. Transacties worden automatisch gesynchroniseerd en ontdubbeld.
+ link_account: Account koppelen
+ no_account_specified: Geen account opgegeven
+ no_accounts_found: Geen Sophtron-accounts gevonden. Controleer uw API-sleutelconfiguratie.
+ no_access_key: Sophtron-toegangssleutel is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_user_id: Sophtron-gebruikers-ID is niet geconfigureerd. Configureer deze in de Instellingen.
+ no_institution_connected: Verbind eerst een bankinstelling met Sophtron.
+ no_name_placeholder: "(Geen naam)"
+ title: "%{account_name} koppelen aan Sophtron"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ link_existing_account:
+ account_already_linked: Dit account is al gekoppeld aan een provider
+ api_error: "API-verbindingsfout"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ invalid_account_name: Kan account met lege naam niet koppelen
+ sophtron_account_already_linked: Dit Sophtron-account is al gekoppeld aan een ander account
+ sophtron_account_not_found: Sophtron-account niet gevonden
+ missing_parameters: Vereiste parameters ontbreken
+ no_institution_connected: Verbind eerst een bankinstelling met Sophtron.
+ success: "%{account_name} succesvol gekoppeld aan Sophtron"
+ setup_accounts:
+ account_type_label: "Accounttype:"
+ all_accounts_linked: "Al uw Sophtron-accounts zijn al ingesteld."
+ api_error: "API-verbindingsfout"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ fetch_failed: "Accounts ophalen mislukt"
+ no_accounts_to_setup: "Geen accounts om in te stellen"
+ no_access_key: "Sophtron-toegangssleutel is niet geconfigureerd. Controleer uw verbindingsinstellingen."
+ no_user_id: "Sophtron-gebruikers-ID is niet geconfigureerd. Controleer uw verbindingsinstellingen."
+ no_institution_connected: "Sophtron-instelling is nog niet verbonden."
+ account_types:
+ skip: Dit account overslaan
+ depository: Betaal- of spaarrekening
+ credit_card: Creditcard
+ investment: Beleggingsaccount
+ loan: Lening of hypotheek
+ other_asset: Overig actief
+ subtype_labels:
+ depository: "Accountsubtype:"
+ credit_card: ""
+ investment: "Beleggingstype:"
+ loan: "Leningtype:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Creditcards worden automatisch ingesteld als creditcardaccounts."
+ other_asset: "Voor overige activa zijn geen aanvullende opties nodig."
+ balance: Saldo
+ cancel: Annuleren
+ choose_account_type: "Kies het juiste accounttype voor elk Sophtron-account:"
+ create_accounts: Accounts aanmaken
+ creating_accounts: Accounts aanmaken...
+ historical_data_range: "Historisch gegevensbereik:"
+ subtitle: Kies de juiste accounttypes voor uw geïmporteerde accounts
+ sync_start_date_help: Selecteer hoe ver terug u de transactiegeschiedenis wilt synchroniseren. Maximaal 3 jaar geschiedenis beschikbaar.
+ sync_start_date_label: "Transacties synchroniseren vanaf:"
+ title: Stel uw Sophtron-accounts in
+ complete_account_setup:
+ all_skipped: "Alle accounts zijn overgeslagen. Er zijn geen accounts aangemaakt."
+ creation_failed: "Accounts konden niet worden aangemaakt"
+ api_error: "API-verbindingsfout"
+ unexpected_error: "Er is een onverwachte fout opgetreden"
+ no_accounts: "Geen accounts om in te stellen."
+ success: "%{count} account(s) succesvol aangemaakt."
+ sync:
+ already_running: Een handmatige Sophtron-synchronisatie is al bezig.
+ api_error: "Handmatige Sophtron-synchronisatie mislukt: %{message}"
+ failed: Handmatige Sophtron-synchronisatie mislukt
+ no_linked_accounts: Deze Sophtron-instelling heeft geen gekoppelde accounts om te synchroniseren.
+ processing_failed: De handmatige Sophtron-synchronisatie kon de bijgewerkte transacties niet verwerken.
+ success: Synchronisatie gestart
+ toggle_manual_sync:
+ success_disabled: Sophtron-instelling wordt automatisch gesynchroniseerd.
+ success_enabled: Sophtron-instelling vereist nu handmatige synchronisatie.
+ manual_sync_complete:
+ close: Sluiten
+ description: De accountsaldi worden op de achtergrond verder bijgewerkt.
+ message: De transacties zijn gedownload na de Sophtron-verificatie.
+ title: Sophtron-synchronisatie gestart
+ sophtron_setup_required:
+ title: Sophtron-installatie vereist
+ message: >
+ Ga naar de pagina met Provider-instellingen en volg de instructies om uw Sophtron-verbinding te autoriseren en te configureren om de installatie van uw Sophtron-verbinding te voltooien.
+ go_to_provider_settings: Ga naar Provider-instellingen
+ heading: "Gebruikers-ID en toegangssleutel niet geconfigureerd"
+ description: "Voordat u Sophtron-accounts kunt koppelen, moet u uw Sophtron-gebruikers-ID en -toegangssleutel configureren."
+ setup_steps_title: "Installatiestappen:"
+ step_1_html: "Ga naar Instellingen → Bank-sync-providers"
+ step_2_html: "Zoek het gedeelte Sophtron"
+ step_3_html: "Voer uw Sophtron-gebruikers-ID en -toegangssleutel in"
+ step_4: "Kom hier terug om uw accounts te koppelen"
+ api_error:
+ title: "Sophtron-verbindingsfout"
+ unable_to_connect: "Kan geen verbinding maken met Sophtron"
+ institution_unable_to_connect: "Kan geen verbinding maken met de instelling"
+ common_issues_title: "Veelvoorkomende problemen:"
+ incorrect_user_id: "Onjuiste gebruikers-ID: Controleer uw gebruikers-ID in de Provider-instellingen"
+ invalid_access_key: "Ongeldige toegangssleutel: Controleer uw toegangssleutel in de Provider-instellingen"
+ expired_credentials: "Verlopen inloggegevens: Genereer een nieuwe gebruikers-ID en toegangssleutel bij Sophtron"
+ network_issue: "Netwerkprobleem: Controleer uw internetverbinding"
+ service_down: "Dienst niet beschikbaar: De Sophtron-API is mogelijk tijdelijk niet beschikbaar"
+ bad_credentials: "Bankinloggegevens: Controleer of de gebruikersnaam en het wachtwoord correct zijn"
+ verification_code: "Verificatiecode: Zorg ervoor dat de meest recente code is ingevoerd voordat deze verliep"
+ institution_timeout: "Time-out instelling: De bankaanmeldpagina werd niet op tijd voltooid"
+ unsupported_mfa: "MFA-ondersteuning: Sophtron ondersteunt mogelijk de huidige verificatiestroom van deze instelling niet"
+ check_provider_settings: "Provider-instellingen controleren"
+ try_again: "Opnieuw proberen te verbinden"
+ select_option: "%{type} selecteren"
+ subtype: "subtype"
+ type: "type"
+ sophtron_panel:
+ setup_instructions_title: "Installatie-instructies:"
+ setup_instructions:
+ step_1_html: 'Ga naar Sophtron om uw API-inloggegevens te verkrijgen'
+ step_2: "Kopieer uw gebruikers-ID en toegangssleutel uit uw Sophtron-accountinstellingen"
+ step_3: "Plak de inloggegevens hieronder en klik op Opslaan; Sure maakt of hergebruikt automatisch uw Sophtron-klant-ID"
+ field_descriptions_title: "Veldbeschrijvingen:"
+ field_descriptions:
+ user_id_html: "Gebruikers-ID: Uw Sophtron-gebruikers-ID-inloggegeven"
+ access_key_html: "Toegangssleutel: Uw Sophtron-toegangssleutel-inloggegeven"
+ base_url_html: "Basis-URL: De URL van het Sophtron-API-eindpunt, meestal https://api.sophtron.com/api"
+ fields:
+ user_id:
+ label: "Gebruikers-ID"
+ placeholder_new: "Plak uw Sophtron-gebruikers-ID"
+ placeholder_edit: "••••••••"
+ access_key:
+ label: "Toegangssleutel"
+ placeholder_new: "Plak uw Sophtron-toegangssleutel"
+ placeholder_edit: "••••••••"
+ base_url:
+ label: "Basis-URL"
+ placeholder: "https://api.sophtron.com/api"
+ save: "Configuratie opslaan"
+ update: "Configuratie bijwerken"
+ syncer:
+ manual_sync_required: "Voor deze instelling is een handmatige Sophtron-synchronisatie vereist; deze accounts worden overgeslagen tijdens de automatische synchronisatie."
+ importing_accounts: "Accounts importeren vanuit Sophtron..."
+ checking_account_configuration: "Accountconfiguratie controleren..."
+ accounts_need_setup: "%{count} account(s) moeten worden ingesteld"
+ processing_transactions: "Transacties voor gekoppelde accounts verwerken..."
+ calculating_balances: "Saldi voor gekoppelde accounts berekenen..."
+ sophtron_entry:
+ processor:
+ unknown_transaction: "Onbekende transactie"
+ render_connection_timeout:
+ timeout: "Time-out bij verbinding. Probeer het opnieuw."
+ redirect_after_account_link:
+ invalid_account_names:
+ one: "Kan %{count} account met lege naam niet koppelen"
+ other: "Kan %{count} accounts met lege namen niet koppelen"
+ partial_invalid: "%{created_count} account(s) gekoppeld. %{already_linked_count} al gekoppeld, %{invalid_count} hadden ongeldige namen."
+ partial_success: "%{created_count} account(s) gekoppeld. %{already_linked_count} account(s) waren al gekoppeld."
+ success:
+ one: "%{count} account succesvol gekoppeld."
+ other: "%{count} accounts succesvol gekoppeld."
+ all_already_linked:
+ one: "Het geselecteerde account is al gekoppeld"
+ other: "Alle %{count} geselecteerde accounts zijn al gekoppeld"
+ link_failed: "Accounts koppelen mislukt"
+ start_manual_sync:
+ already_running: "Er is al een synchronisatie bezig."
+ no_linked_accounts: "Geen gekoppelde accounts beschikbaar om te synchroniseren."
+ api_error: "API-fout: %{message}"
+ start_manual_sync_for_account:
+ failed: "Account synchroniseren mislukt"
diff --git a/config/locales/views/sophtron_items/pt-BR.yml b/config/locales/views/sophtron_items/pt-BR.yml
new file mode 100644
index 000000000..b88ca97b3
--- /dev/null
+++ b/config/locales/views/sophtron_items/pt-BR.yml
@@ -0,0 +1,313 @@
+---
+pt-BR:
+ sophtron_items:
+ defaults:
+ name: Conexão da Sophtron
+ new:
+ title: Conectar a Sophtron
+ user_id: ID de usuário
+ user_id_placeholder: cole seu ID de usuário da Sophtron
+ access_key: Chave de acesso
+ access_key_placeholder: cole sua chave de acesso da Sophtron
+ connect: Conectar
+ cancel: Cancelar
+ create:
+ success: Conexão da Sophtron criada com sucesso
+ destroy:
+ success: Conexão da Sophtron removida
+ update:
+ success: Conexão da Sophtron atualizada com sucesso! Suas contas estão sendo reconectadas.
+ errors:
+ blank_user_id: Insira um ID de usuário da Sophtron.
+ invalid_user_id: ID de usuário inválido. Verifique se você copiou o ID de usuário completo da Sophtron.
+ user_id_compromised: O ID de usuário pode estar comprometido, expirado ou já usado. Crie um novo.
+ blank_access_key: Insira uma chave de acesso da Sophtron.
+ invalid_access_key: Chave de acesso inválida. Verifique se você copiou a chave de acesso completa da Sophtron.
+ access_key_compromised: A chave de acesso pode estar comprometida, expirada ou já usada. Crie uma nova.
+ update_failed: "Não foi possível atualizar a conexão: %{message}"
+ unexpected: Ocorreu um erro inesperado. Tente novamente ou entre em contato com o suporte.
+ edit:
+ user_id:
+ label: "ID de usuário da Sophtron:"
+ placeholder: "Cole aqui seu ID de usuário da Sophtron..."
+ help_text: "O ID de usuário deve ser uma sequência longa que começa com letras e números"
+ access_key:
+ label: "Chave de acesso da Sophtron:"
+ placeholder: "Cole aqui sua chave de acesso da Sophtron..."
+ help_text: "A chave de acesso deve ser uma sequência longa que começa com letras e números"
+ index:
+ title: Conexões da Sophtron
+ loading:
+ loading_message: Carregando contas da Sophtron...
+ loading_title: Carregando
+ link_accounts:
+ all_already_linked:
+ one: "A conta selecionada (%{names}) já está vinculada"
+ other: "Todas as %{count} contas selecionadas já estão vinculadas: %{names}"
+ api_error: "Erro de conexão com a API"
+ invalid_account_names:
+ one: "Não é possível vincular uma conta com o nome em branco"
+ other: "Não é possível vincular %{count} contas com os nomes em branco"
+ link_failed: Não foi possível vincular as contas
+ no_accounts_selected: Selecione pelo menos uma conta
+ partial_invalid: "%{created_count} conta(s) vinculada(s) com sucesso, %{already_linked_count} já estavam vinculadas, %{invalid_count} conta(s) tinham nomes inválidos"
+ partial_success: "%{created_count} conta(s) vinculada(s) com sucesso. %{already_linked_count} conta(s) já estavam vinculadas: %{already_linked_names}"
+ success:
+ one: "%{count} conta vinculada com sucesso"
+ other: "%{count} contas vinculadas com sucesso"
+ no_credentials_configured: "Configure primeiro seu ID de usuário e sua chave de acesso da API da Sophtron nas Configurações do provedor."
+ no_accounts_found: Nenhuma conta encontrada. Verifique a configuração da sua chave de API.
+ no_access_key: A chave de acesso da Sophtron não está configurada. Configure-a nas Configurações.
+ no_user_id: O ID de usuário da Sophtron não está configurado. Configure-o nas Configurações.
+ no_institution_connected: Conecte primeiro uma instituição bancária à Sophtron.
+ connect:
+ cancel: Cancelar
+ captcha: Captcha
+ connect: Conectar
+ institution_search_label: Instituição
+ institution_search_placeholder: Pesquisar pelo nome do banco
+ no_institutions: Nenhuma instituição correspondente encontrada.
+ password: Senha
+ search: Pesquisar
+ search_too_short: Insira pelo menos dois caracteres para pesquisar.
+ title: Conectar instituição da Sophtron
+ username: Nome de usuário
+ connect_institution:
+ api_error: "A conexão com a Sophtron falhou: %{message}"
+ missing_parameters: Selecione uma instituição e insira as credenciais de acesso do seu banco.
+ connection_status:
+ api_error: "Erro de conexão com a API: %{message}"
+ attempt: "Tentativa %{attempt} de %{max}"
+ check_again: Verificar novamente
+ failed: A Sophtron não conseguiu concluir esta conexão com a instituição.
+ failed_timeout: A Sophtron atingiu o tempo limite enquanto a instituição concluía o acesso.
+ timeout: A Sophtron não concluiu a conexão dentro do tempo esperado. Você pode verificar novamente ou tentar reconectar mais tarde.
+ title: Conectando à Sophtron
+ waiting: A Sophtron ainda está se conectando à sua instituição.
+ mfa:
+ captcha: Texto do captcha
+ captcha_alt: Captcha da Sophtron
+ phone_confirmed: Confirmei por telefone
+ submit: Enviar
+ title: Verificação da Sophtron
+ token: Código de verificação
+ submit_mfa:
+ api_error: "A verificação falhou: %{message}"
+ invalid_security_answers: As respostas de segurança estão ausentes ou são muito longas.
+ unknown_challenge: Etapa de verificação da Sophtron desconhecida.
+ sophtron_item:
+ accounts_need_setup: As contas precisam de configuração
+ automatic_sync: Usar sincronização automática
+ delete: Excluir conexão
+ deletion_in_progress: exclusão em andamento...
+ error: Erro
+ no_accounts_description: Esta conexão ainda não tem contas vinculadas.
+ no_accounts_title: Sem contas
+ manual_sync: Sincronização manual
+ manual_sync_action: Exigir sincronização manual
+ manual_sync_action_for: "Exigir sincronização manual para %{institution}"
+ automatic_sync_for: "Usar sincronização automática para %{institution}"
+ setup_action: Configurar novas contas
+ setup_description: "%{linked} de %{total} contas vinculadas. Escolha os tipos de conta para suas contas da Sophtron recém-importadas."
+ setup_needed: Novas contas prontas para configurar
+ status: "Sincronizado há %{timestamp}"
+ status_never: Nunca sincronizado
+ status_with_summary: "Sincronizado há %{timestamp} • %{summary}"
+ sync_now: Sincronizar agora
+ syncing: Sincronizando...
+ total: Total
+ unlinked: Não vinculada
+ preload_accounts:
+ preload_accounts: pré-carregar contas
+ api_error: "Erro de conexão com a API"
+ unexpected_error: "Ocorreu um erro inesperado"
+ no_credentials_configured: "Configure primeiro seu ID de usuário e sua chave de acesso da API da Sophtron nas Configurações do provedor."
+ no_accounts_found: Nenhuma conta encontrada. Verifique a configuração da sua chave de API.
+ no_access_key: A chave de acesso da Sophtron não está configurada. Configure-a nas Configurações.
+ no_user_id: O ID de usuário da Sophtron não está configurado. Configure-o nas Configurações.
+ select_accounts:
+ accounts_selected: contas selecionadas
+ api_error: "Erro de conexão com a API"
+ unexpected_error: "Ocorreu um erro inesperado"
+ cancel: Cancelar
+ configure_name_in_sophtron: "Não é possível importar - configure o nome da conta na Sophtron"
+ description: Selecione as contas que você quer vincular à sua conta do %{product_name}.
+ link_accounts: Vincular contas selecionadas
+ no_accounts_found: Nenhuma conta encontrada. Verifique a configuração da sua chave de API.
+ no_access_key: A chave de acesso da Sophtron não está configurada. Configure-a nas Configurações.
+ no_user_id: O ID de usuário da Sophtron não está configurado. Configure-o nas Configurações.
+ no_credentials_configured: "Configure primeiro seu ID de usuário e sua chave de acesso da API da Sophtron nas Configurações do provedor."
+ no_institution_connected: Conecte primeiro uma instituição bancária à Sophtron.
+ no_name_placeholder: "(Sem nome)"
+ title: Selecionar contas da Sophtron
+ select_existing_account:
+ account_already_linked: Esta conta já está vinculada a um provedor
+ all_accounts_already_linked: Todas as contas da Sophtron já estão vinculadas
+ api_error: "Erro de conexão com a API"
+ cancel: Cancelar
+ configure_name_in_sophtron: "Não é possível importar - configure o nome da conta na Sophtron"
+ description: Selecione uma conta da Sophtron para vincular a esta conta. As transações serão sincronizadas e desduplicadas automaticamente.
+ link_account: Vincular conta
+ no_account_specified: Nenhuma conta especificada
+ no_accounts_found: Nenhuma conta da Sophtron encontrada. Verifique a configuração da sua chave de API.
+ no_access_key: A chave de acesso da Sophtron não está configurada. Configure-a nas Configurações.
+ no_user_id: O ID de usuário da Sophtron não está configurado. Configure-o nas Configurações.
+ no_institution_connected: Conecte primeiro uma instituição bancária à Sophtron.
+ no_name_placeholder: "(Sem nome)"
+ title: "Vincular %{account_name} à Sophtron"
+ unexpected_error: "Ocorreu um erro inesperado"
+ link_existing_account:
+ account_already_linked: Esta conta já está vinculada a um provedor
+ api_error: "Erro de conexão com a API"
+ unexpected_error: "Ocorreu um erro inesperado"
+ invalid_account_name: Não é possível vincular uma conta com o nome em branco
+ sophtron_account_already_linked: Esta conta da Sophtron já está vinculada a outra conta
+ sophtron_account_not_found: Conta da Sophtron não encontrada
+ missing_parameters: Parâmetros obrigatórios ausentes
+ no_institution_connected: Conecte primeiro uma instituição bancária à Sophtron.
+ success: "%{account_name} vinculada com sucesso à Sophtron"
+ setup_accounts:
+ account_type_label: "Tipo de conta:"
+ all_accounts_linked: "Todas as suas contas da Sophtron já foram configuradas."
+ api_error: "Erro de conexão com a API"
+ unexpected_error: "Ocorreu um erro inesperado"
+ fetch_failed: "Não foi possível buscar as contas"
+ no_accounts_to_setup: "Nenhuma conta para configurar"
+ no_access_key: "A chave de acesso da Sophtron não está configurada. Verifique as configurações da sua conexão."
+ no_user_id: "O ID de usuário da Sophtron não está configurado. Verifique as configurações da sua conexão."
+ no_institution_connected: "A instituição da Sophtron ainda não está conectada."
+ account_types:
+ skip: Ignorar esta conta
+ depository: Conta corrente ou poupança
+ credit_card: Cartão de crédito
+ investment: Conta de investimento
+ loan: Empréstimo ou financiamento
+ other_asset: Outro ativo
+ subtype_labels:
+ depository: "Subtipo de conta:"
+ credit_card: ""
+ investment: "Tipo de investimento:"
+ loan: "Tipo de empréstimo:"
+ other_asset: ""
+ subtype_messages:
+ credit_card: "Os cartões de crédito serão configurados automaticamente como contas de cartão de crédito."
+ other_asset: "Não são necessárias opções adicionais para Outros ativos."
+ balance: Saldo
+ cancel: Cancelar
+ choose_account_type: "Escolha o tipo de conta correto para cada conta da Sophtron:"
+ create_accounts: Criar contas
+ creating_accounts: Criando contas...
+ historical_data_range: "Intervalo de dados históricos:"
+ subtitle: Escolha os tipos de conta corretos para suas contas importadas
+ sync_start_date_help: Selecione até que ponto no passado você quer sincronizar o histórico de transações. Há no máximo 3 anos de histórico disponíveis.
+ sync_start_date_label: "Começar a sincronizar transações a partir de:"
+ title: Configure suas contas da Sophtron
+ complete_account_setup:
+ all_skipped: "Todas as contas foram ignoradas. Nenhuma conta foi criada."
+ creation_failed: "Não foi possível criar as contas"
+ api_error: "Erro de conexão com a API"
+ unexpected_error: "Ocorreu um erro inesperado"
+ no_accounts: "Nenhuma conta para configurar."
+ success: "%{count} conta(s) criada(s) com sucesso."
+ sync:
+ already_running: Uma sincronização manual da Sophtron já está em andamento.
+ api_error: "A sincronização manual da Sophtron falhou: %{message}"
+ failed: A sincronização manual da Sophtron falhou
+ no_linked_accounts: Esta instituição da Sophtron não tem contas vinculadas para sincronizar.
+ processing_failed: A sincronização manual da Sophtron não conseguiu processar as transações atualizadas.
+ success: Sincronização iniciada
+ toggle_manual_sync:
+ success_disabled: A instituição da Sophtron será sincronizada automaticamente.
+ success_enabled: A instituição da Sophtron agora exige sincronização manual.
+ manual_sync_complete:
+ close: Fechar
+ description: Os saldos das contas terminarão de ser atualizados em segundo plano.
+ message: As transações foram baixadas após a verificação da Sophtron.
+ title: Sincronização da Sophtron iniciada
+ sophtron_setup_required:
+ title: Configuração da Sophtron necessária
+ message: >
+ Para concluir a configuração da sua conexão da Sophtron, acesse a página de Configurações do provedor e siga as instruções para autorizar e configurar sua conexão da Sophtron.
+ go_to_provider_settings: Ir para Configurações do provedor
+ heading: "ID de usuário e chave de acesso não configurados"
+ description: "Antes de poder vincular contas da Sophtron, você precisa configurar seu ID de usuário e sua chave de acesso da Sophtron."
+ setup_steps_title: "Etapas de configuração:"
+ step_1_html: "Acesse Configurações → Provedores de sincronização bancária"
+ step_2_html: "Encontre a seção Sophtron"
+ step_3_html: "Insira seu ID de usuário e sua chave de acesso da Sophtron"
+ step_4: "Volte aqui para vincular suas contas"
+ api_error:
+ title: "Erro de conexão com a Sophtron"
+ unable_to_connect: "Não é possível conectar à Sophtron"
+ institution_unable_to_connect: "Não é possível conectar à instituição"
+ common_issues_title: "Problemas comuns:"
+ incorrect_user_id: "ID de usuário incorreto: Verifique seu ID de usuário nas Configurações do provedor"
+ invalid_access_key: "Chave de acesso inválida: Verifique sua chave de acesso nas Configurações do provedor"
+ expired_credentials: "Credenciais expiradas: Gere um novo ID de usuário e uma nova chave de acesso na Sophtron"
+ network_issue: "Problema de rede: Verifique sua conexão com a internet"
+ service_down: "Serviço indisponível: A API da Sophtron pode estar temporariamente indisponível"
+ bad_credentials: "Credenciais bancárias: Verifique se o nome de usuário e a senha estão corretos"
+ verification_code: "Código de verificação: Certifique-se de que o código mais recente foi inserido antes de expirar"
+ institution_timeout: "Tempo limite da instituição: A página de acesso do banco não foi concluída a tempo"
+ unsupported_mfa: "Suporte a MFA: A Sophtron pode não oferecer suporte ao fluxo de verificação atual desta instituição"
+ check_provider_settings: "Verificar Configurações do provedor"
+ try_again: "Tentar conectar novamente"
+ select_option: "Selecionar %{type}"
+ subtype: "subtipo"
+ type: "tipo"
+ sophtron_panel:
+ setup_instructions_title: "Instruções de configuração:"
+ setup_instructions:
+ step_1_html: 'Acesse Sophtron para obter suas credenciais de API'
+ step_2: "Copie seu ID de usuário e sua chave de acesso das configurações da sua conta da Sophtron"
+ step_3: "Cole as credenciais abaixo e clique em Salvar; o Sure criará ou reutilizará seu ID de cliente da Sophtron automaticamente"
+ field_descriptions_title: "Descrições dos campos:"
+ field_descriptions:
+ user_id_html: "ID de usuário: Sua credencial de ID de usuário da Sophtron"
+ access_key_html: "Chave de acesso: Sua credencial de chave de acesso da Sophtron"
+ base_url_html: "URL base: A URL do endpoint da API da Sophtron, geralmente https://api.sophtron.com/api"
+ fields:
+ user_id:
+ label: "ID de usuário"
+ placeholder_new: "Cole seu ID de usuário da Sophtron"
+ placeholder_edit: "••••••••"
+ access_key:
+ label: "Chave de acesso"
+ placeholder_new: "Cole sua chave de acesso da Sophtron"
+ placeholder_edit: "••••••••"
+ base_url:
+ label: "URL base"
+ placeholder: "https://api.sophtron.com/api"
+ save: "Salvar configuração"
+ update: "Atualizar configuração"
+ syncer:
+ manual_sync_required: "É necessária uma sincronização manual da Sophtron para esta instituição; essas contas são ignoradas durante a sincronização automática."
+ importing_accounts: "Importando contas da Sophtron..."
+ checking_account_configuration: "Verificando a configuração da conta..."
+ accounts_need_setup: "%{count} conta(s) precisam de configuração"
+ processing_transactions: "Processando transações das contas vinculadas..."
+ calculating_balances: "Calculando saldos das contas vinculadas..."
+ sophtron_entry:
+ processor:
+ unknown_transaction: "Transação desconhecida"
+ render_connection_timeout:
+ timeout: "Tempo limite de conexão esgotado. Tente novamente."
+ redirect_after_account_link:
+ invalid_account_names:
+ one: "Não é possível vincular %{count} conta com o nome em branco"
+ other: "Não é possível vincular %{count} contas com os nomes em branco"
+ partial_invalid: "%{created_count} conta(s) vinculada(s). %{already_linked_count} já vinculadas, %{invalid_count} tinham nomes inválidos."
+ partial_success: "%{created_count} conta(s) vinculada(s). %{already_linked_count} conta(s) já estavam vinculadas."
+ success:
+ one: "%{count} conta vinculada com sucesso."
+ other: "%{count} contas vinculadas com sucesso."
+ all_already_linked:
+ one: "A conta selecionada já está vinculada"
+ other: "Todas as %{count} contas selecionadas já estão vinculadas"
+ link_failed: "Não foi possível vincular as contas"
+ start_manual_sync:
+ already_running: "Uma sincronização já está em andamento."
+ no_linked_accounts: "Não há contas vinculadas disponíveis para sincronizar."
+ api_error: "Erro da API: %{message}"
+ start_manual_sync_for_account:
+ failed: "Não foi possível sincronizar a conta"
diff --git a/config/locales/views/splits/nl.yml b/config/locales/views/splits/nl.yml
new file mode 100644
index 000000000..b509b12d4
--- /dev/null
+++ b/config/locales/views/splits/nl.yml
@@ -0,0 +1,47 @@
+---
+nl:
+ splits:
+ new:
+ title: Transactie splitsen
+ description: Splits deze transactie in meerdere boekingen met verschillende categorieën en bedragen.
+ submit: Transactie splitsen
+ cancel: Annuleren
+ add_row: Splitsing toevoegen
+ remove_row: Verwijderen
+ remaining: Resterend
+ amounts_must_match: De splitsbedragen moeten gelijk zijn aan het oorspronkelijke transactiebedrag.
+ name_label: Naam
+ name_placeholder: Naam van splitsing
+ amount_label: Bedrag
+ category_label: Categorie
+ uncategorized: "(geen categorie)"
+ original_name: "Naam:"
+ original_date: "Datum:"
+ original_amount: "Bedrag"
+ split_number: "Splitsing #%{number}"
+ create:
+ success: Transactie succesvol gesplitst
+ not_splittable: Deze transactie kan niet worden gesplitst.
+ destroy:
+ success: Splitsing van transactie succesvol ongedaan gemaakt
+ show:
+ title: Gesplitste boekingen
+ description: Deze transactie is gesplitst in de volgende boekingen.
+ button_title: Transactie splitsen
+ button_description: Splits deze transactie in meerdere boekingen met verschillende categorieën en bedragen.
+ button: Splitsen
+ unsplit_title: Splitsing ongedaan maken
+ unsplit_button: Splitsing ongedaan maken
+ unsplit_confirm: Hiermee worden alle gesplitste boekingen verwijderd en wordt de oorspronkelijke transactie hersteld.
+ edit:
+ title: Splitsing bewerken
+ description: Pas de gesplitste boekingen voor deze transactie aan.
+ submit: Splitsing bijwerken
+ not_split: Deze transactie is niet gesplitst.
+ update:
+ success: Splitsing succesvol bijgewerkt
+ child:
+ title: Onderdeel van splitsing
+ description: Deze boeking maakt deel uit van een gesplitste transactie.
+ edit_split: Splitsing bewerken
+ unsplit: Splitsing ongedaan maken
diff --git a/config/locales/views/splits/pt-BR.yml b/config/locales/views/splits/pt-BR.yml
new file mode 100644
index 000000000..c07c90f52
--- /dev/null
+++ b/config/locales/views/splits/pt-BR.yml
@@ -0,0 +1,47 @@
+---
+pt-BR:
+ splits:
+ new:
+ title: Dividir transação
+ description: Divida esta transação em vários lançamentos com categorias e valores diferentes.
+ submit: Dividir transação
+ cancel: Cancelar
+ add_row: Adicionar divisão
+ remove_row: Remover
+ remaining: Restante
+ amounts_must_match: Os valores das divisões devem ser iguais ao valor original da transação.
+ name_label: Nome
+ name_placeholder: Nome da divisão
+ amount_label: Valor
+ category_label: Categoria
+ uncategorized: "(sem categoria)"
+ original_name: "Nome:"
+ original_date: "Data:"
+ original_amount: "Valor"
+ split_number: "Divisão nº %{number}"
+ create:
+ success: Transação dividida com sucesso
+ not_splittable: Esta transação não pode ser dividida.
+ destroy:
+ success: Divisão da transação desfeita com sucesso
+ show:
+ title: Lançamentos divididos
+ description: Esta transação foi dividida nos seguintes lançamentos.
+ button_title: Dividir transação
+ button_description: Divida esta transação em vários lançamentos com categorias e valores diferentes.
+ button: Dividir
+ unsplit_title: Desfazer divisão
+ unsplit_button: Desfazer divisão
+ unsplit_confirm: Isso removerá todos os lançamentos divididos e restaurará a transação original.
+ edit:
+ title: Editar divisão
+ description: Modifique os lançamentos divididos desta transação.
+ submit: Atualizar divisão
+ not_split: Esta transação não está dividida.
+ update:
+ success: Divisão atualizada com sucesso
+ child:
+ title: Parte de uma divisão
+ description: Este lançamento faz parte de uma transação dividida.
+ edit_split: Editar divisão
+ unsplit: Desfazer divisão
diff --git a/config/locales/views/transfer_matches/nl.yml b/config/locales/views/transfer_matches/nl.yml
new file mode 100644
index 000000000..2c4631f45
--- /dev/null
+++ b/config/locales/views/transfer_matches/nl.yml
@@ -0,0 +1,24 @@
+---
+nl:
+ transfer_matches:
+ create:
+ success: Overboeking aangemaakt
+ new:
+ header:
+ title: Overboeking of betaling koppelen
+ subtitle: Koppel de bijbehorende transactie in een ander account of maak er een aan als die niet bestaat.
+ from_account: Vanaf account
+ from_account_named: "Vanaf account: %{name}"
+ to_account: Naar account
+ to_account_named: "Naar account: %{name}"
+ outflow_transaction: Uitgaande transactie
+ inflow_transaction: Inkomende transactie
+ create_transfer_match: Overboekingskoppeling aanmaken
+ matching_fields:
+ select_method: Selecteer een methode om uw transacties te koppelen.
+ match_existing_recommended: Bestaande transactie koppelen (aanbevolen)
+ create_new_transaction: Nieuwe transactie aanmaken
+ matching_method: Koppelmethode
+ matching_transaction: Te koppelen transactie
+ target_account: Doelaccount
+ no_matching_transactions: We konden geen transacties vinden om te koppelen vanuit uw andere accounts. Selecteer een account en wij maken een nieuwe inkomende transactie voor u aan.
diff --git a/config/locales/views/transfer_matches/pt-BR.yml b/config/locales/views/transfer_matches/pt-BR.yml
new file mode 100644
index 000000000..11b064995
--- /dev/null
+++ b/config/locales/views/transfer_matches/pt-BR.yml
@@ -0,0 +1,24 @@
+---
+pt-BR:
+ transfer_matches:
+ create:
+ success: Transferência criada
+ new:
+ header:
+ title: Vincular transferência ou pagamento
+ subtitle: Vincule a transação correspondente em outra conta ou crie uma caso não exista.
+ from_account: Conta de origem
+ from_account_named: "Conta de origem: %{name}"
+ to_account: Conta de destino
+ to_account_named: "Conta de destino: %{name}"
+ outflow_transaction: Transação de saída
+ inflow_transaction: Transação de entrada
+ create_transfer_match: Criar vínculo de transferência
+ matching_fields:
+ select_method: Selecione um método para vincular suas transações.
+ match_existing_recommended: Vincular transação existente (recomendado)
+ create_new_transaction: Criar nova transação
+ matching_method: Método de vínculo
+ matching_transaction: Transação a vincular
+ target_account: Conta de destino
+ no_matching_transactions: Não encontramos nenhuma transação para vincular em suas outras contas. Selecione uma conta e criaremos uma nova transação de entrada para você.
diff --git a/db/migrate/20260316120000_create_vector_store_chunks.rb b/db/migrate/20260316120000_create_vector_store_chunks.rb
index 216768486..c65e33c1a 100644
--- a/db/migrate/20260316120000_create_vector_store_chunks.rb
+++ b/db/migrate/20260316120000_create_vector_store_chunks.rb
@@ -28,14 +28,20 @@ class CreateVectorStoreChunks < ActiveRecord::Migration[7.2]
private
- # Only run this migration when pgvector is explicitly configured as the
- # vector store provider AND the extension is actually available on the
- # PostgreSQL server. Previously we only checked server availability,
- # which caused failures in production Docker environments where the
- # extension may be present but the DB user lacks superuser privileges
+ # Only run this migration when pgvector is the effective vector store AND
+ # the extension is actually available on the PostgreSQL server.
+ #
+ # Provider selection goes through VectorStore::Registry.pgvector_effective?
+ # (the single source of truth) rather than a raw VECTOR_STORE_PROVIDER check,
+ # so an Anthropic-default install — which selects pgvector implicitly via
+ # Setting.llm_provider without setting VECTOR_STORE_PROVIDER — still
+ # provisions the table instead of failing later on a missing relation.
+ #
+ # The server-availability check stays: production Docker environments may
+ # have the extension present but the DB user may lack superuser privileges
# to enable it.
def pgvector_available?
- return false unless ENV["VECTOR_STORE_PROVIDER"].to_s.downcase == "pgvector"
+ return false unless VectorStore::Registry.pgvector_effective?
result = ActiveRecord::Base.connection.execute(
"SELECT 1 FROM pg_available_extensions WHERE name = 'vector' LIMIT 1"
diff --git a/db/migrate/20260601120000_ensure_vector_store_chunks_for_default_pgvector.rb b/db/migrate/20260601120000_ensure_vector_store_chunks_for_default_pgvector.rb
new file mode 100644
index 000000000..452717d7b
--- /dev/null
+++ b/db/migrate/20260601120000_ensure_vector_store_chunks_for_default_pgvector.rb
@@ -0,0 +1,55 @@
+class EnsureVectorStoreChunksForDefaultPgvector < ActiveRecord::Migration[7.2]
+ # CreateVectorStoreChunks only provisions the table when
+ # VECTOR_STORE_PROVIDER == "pgvector" is set explicitly. Since #1986 makes
+ # pgvector the *default* vector store for Anthropic installs (no
+ # VECTOR_STORE_PROVIDER needed), a fresh Anthropic-only install would migrate
+ # without the table and then fail on uploads/searches. Backfill it whenever
+ # pgvector is the effective store, idempotently, so fresh and already-migrated
+ # installs converge. Gating uses the same VectorStore::Registry predicate as
+ # the runtime adapter selection, so the two can't drift again.
+ def up
+ return unless pgvector_effective?
+ return unless pgvector_extension_available?
+ return if table_exists?(:vector_store_chunks)
+
+ enable_extension "vector" unless extension_enabled?("vector")
+
+ create_table :vector_store_chunks, id: :uuid do |t|
+ t.string :store_id, null: false
+ t.string :file_id, null: false
+ t.string :filename
+ t.integer :chunk_index, null: false, default: 0
+ t.text :content, null: false
+ t.column :embedding, "vector(#{ENV.fetch('EMBEDDING_DIMENSIONS', '1024')})", null: false
+ t.jsonb :metadata, null: false, default: {}
+ t.timestamps null: false
+ end
+
+ add_index :vector_store_chunks, :store_id
+ add_index :vector_store_chunks, :file_id
+ add_index :vector_store_chunks, [ :store_id, :file_id, :chunk_index ], unique: true,
+ name: "index_vector_store_chunks_on_store_file_chunk"
+ end
+
+ def down
+ # No-op: the table's lifecycle is owned by CreateVectorStoreChunks. This
+ # migration only backfills it for the pgvector-by-default case, so reverting
+ # must not drop a table other installs rely on.
+ end
+
+ private
+
+ def pgvector_effective?
+ VectorStore::Registry.pgvector_effective?
+ rescue StandardError
+ false
+ end
+
+ def pgvector_extension_available?
+ ActiveRecord::Base.connection.execute(
+ "SELECT 1 FROM pg_available_extensions WHERE name = 'vector' LIMIT 1"
+ ).any?
+ rescue StandardError
+ false
+ end
+end
diff --git a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
index 976c65844..cee847f2a 100644
--- a/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
+++ b/mobile/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
@@ -50,6 +50,11 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin path_provider_android, io.flutter.plugins.pathprovider.PathProviderPlugin", e);
}
+ try {
+ flutterEngine.getPlugins().add(new io.sentry.flutter.SentryFlutterPlugin());
+ } catch (Exception e) {
+ Log.e(TAG, "Error registering plugin sentry_flutter, io.sentry.flutter.SentryFlutterPlugin", e);
+ }
try {
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
} catch (Exception e) {
@@ -65,10 +70,5 @@ public final class GeneratedPluginRegistrant {
} catch (Exception e) {
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
}
- try {
- flutterEngine.getPlugins().add(new io.flutter.plugins.webviewflutter.WebViewFlutterPlugin());
- } catch (Exception e) {
- Log.e(TAG, "Error registering plugin webview_flutter_android, io.flutter.plugins.webviewflutter.WebViewFlutterPlugin", e);
- }
}
}
diff --git a/mobile/ios/Runner/GeneratedPluginRegistrant.m b/mobile/ios/Runner/GeneratedPluginRegistrant.m
index 67307bbf3..d33d0bda0 100644
--- a/mobile/ios/Runner/GeneratedPluginRegistrant.m
+++ b/mobile/ios/Runner/GeneratedPluginRegistrant.m
@@ -42,6 +42,12 @@
@import path_provider_foundation;
#endif
+#if __has_include()
+#import
+#else
+@import sentry_flutter;
+#endif
+
#if __has_include()
#import
#else
@@ -60,12 +66,6 @@
@import url_launcher_ios;
#endif
-#if __has_include()
-#import
-#else
-@import webview_flutter_wkwebview;
-#endif
-
@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject*)registry {
@@ -75,10 +75,10 @@
[LocalAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"LocalAuthPlugin"]];
[FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]];
[PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]];
+ [SentryFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"SentryFlutterPlugin"]];
[SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
[URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]];
- [WebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"WebViewFlutterPlugin"]];
}
@end
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index 6f28c77bd..a657dea83 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -19,6 +19,7 @@ import 'services/api_config.dart';
import 'services/connectivity_service.dart';
import 'services/log_service.dart';
import 'services/preferences_service.dart';
+import 'services/telemetry_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -27,7 +28,9 @@ void main() async {
// Add initial log entry
LogService.instance.info('App', 'Sure app starting...');
- runApp(const SureApp());
+ await TelemetryService.instance.initialize(
+ appRunner: () => runApp(const SureApp()),
+ );
}
class SureApp extends StatelessWidget {
@@ -73,91 +76,93 @@ class SureApp extends StatelessWidget {
),
],
child: Consumer(
- builder: (context, themeProvider, _) => MaterialApp(
- title: 'Sure Finances',
- debugShowCheckedModeBanner: false,
- theme: ThemeData(
- fontFamily: 'Geist',
- fontFamilyFallback: const [
- 'Inter',
- 'Arial',
- 'sans-serif',
- ],
- colorScheme: ColorScheme.fromSeed(
- seedColor: const Color(0xFF6366F1),
- brightness: Brightness.light,
- ),
- useMaterial3: true,
- appBarTheme: const AppBarTheme(
- centerTitle: true,
- elevation: 0,
- ),
- cardTheme: CardThemeData(
- elevation: 2,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- inputDecorationTheme: InputDecorationTheme(
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- filled: true,
- ),
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- minimumSize: const Size(double.infinity, 50),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- ),
- ),
- darkTheme: ThemeData(
- fontFamily: 'Geist',
- fontFamilyFallback: const [
- 'Inter',
- 'Arial',
- 'sans-serif',
- ],
- colorScheme: ColorScheme.fromSeed(
- seedColor: const Color(0xFF6366F1),
- brightness: Brightness.dark,
- ),
- useMaterial3: true,
- appBarTheme: const AppBarTheme(
- centerTitle: true,
- elevation: 0,
- ),
- cardTheme: CardThemeData(
- elevation: 2,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- inputDecorationTheme: InputDecorationTheme(
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- filled: true,
- ),
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- minimumSize: const Size(double.infinity, 50),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- ),
- ),
- themeMode: themeProvider.themeMode,
- routes: {
- '/config': (context) => const BackendConfigScreen(),
- '/login': (context) => const LoginScreen(),
- '/home': (context) => const MainNavigationScreen(),
- },
- home: const AppWrapper(),
- )),
+ builder: (context, themeProvider, _) => MaterialApp(
+ title: 'Sure Finances',
+ debugShowCheckedModeBanner: false,
+ navigatorObservers:
+ TelemetryService.instance.navigatorObservers,
+ theme: ThemeData(
+ fontFamily: 'Geist',
+ fontFamilyFallback: const [
+ 'Inter',
+ 'Arial',
+ 'sans-serif',
+ ],
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF6366F1),
+ brightness: Brightness.light,
+ ),
+ useMaterial3: true,
+ appBarTheme: const AppBarTheme(
+ centerTitle: true,
+ elevation: 0,
+ ),
+ cardTheme: CardThemeData(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ filled: true,
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ minimumSize: const Size(double.infinity, 50),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ darkTheme: ThemeData(
+ fontFamily: 'Geist',
+ fontFamilyFallback: const [
+ 'Inter',
+ 'Arial',
+ 'sans-serif',
+ ],
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF6366F1),
+ brightness: Brightness.dark,
+ ),
+ useMaterial3: true,
+ appBarTheme: const AppBarTheme(
+ centerTitle: true,
+ elevation: 0,
+ ),
+ cardTheme: CardThemeData(
+ elevation: 2,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ filled: true,
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ minimumSize: const Size(double.infinity, 50),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ ),
+ themeMode: themeProvider.themeMode,
+ routes: {
+ '/config': (context) => const BackendConfigScreen(),
+ '/login': (context) => const LoginScreen(),
+ '/home': (context) => const MainNavigationScreen(),
+ },
+ home: const AppWrapper(),
+ )),
);
}
}
@@ -224,29 +229,71 @@ class _AppWrapperState extends State with WidgetsBindingObserver {
// Handle deep link that launched the app (cold start)
_appLinks.getInitialLink().then((uri) {
- if (uri != null) _handleDeepLink(uri);
+ if (uri != null) {
+ TelemetryService.instance.addBreadcrumb(
+ 'deep_links',
+ 'initial_link_received',
+ data: {'recognized': _isSsoCallback(uri)},
+ );
+ _handleDeepLink(uri);
+ }
}).catchError((e, stackTrace) {
- LogService.instance.error('DeepLinks', 'Initial link error: $e\n$stackTrace');
+ LogService.instance.error(
+ 'DeepLinks',
+ 'Initial link failed with ${e.runtimeType}',
+ );
+ unawaited(TelemetryService.instance.captureHandledException(
+ e,
+ stackTrace,
+ operation: 'deep_links.initial_link',
+ ));
});
// Listen for deep links while app is running
_linkSubscription = _appLinks.uriLinkStream.listen(
(uri) => _handleDeepLink(uri),
onError: (e, stackTrace) {
- LogService.instance.error('DeepLinks', 'Link stream error: $e\n$stackTrace');
+ LogService.instance.error(
+ 'DeepLinks',
+ 'Link stream failed with ${e.runtimeType}',
+ );
+ unawaited(TelemetryService.instance.captureHandledException(
+ e,
+ stackTrace,
+ operation: 'deep_links.stream',
+ ));
},
);
}
void _handleDeepLink(Uri uri) {
- if (uri.scheme == 'sureapp' && uri.host == 'oauth') {
+ final isSsoCallback = _isSsoCallback(uri);
+ TelemetryService.instance.addBreadcrumb(
+ 'deep_links',
+ 'link_received',
+ data: {'recognized': isSsoCallback},
+ );
+
+ if (isSsoCallback) {
final authProvider = Provider.of(context, listen: false);
authProvider.handleSsoCallback(uri);
}
}
+ bool _isSsoCallback(Uri uri) =>
+ uri.scheme == 'sureapp' && uri.host == 'oauth';
+
Future _checkBackendConfig() async {
- final hasUrl = await ApiConfig.initialize();
+ final hasUrl = await TelemetryService.instance.traceAsync(
+ 'app.backend_config_check',
+ 'Backend configuration check',
+ ApiConfig.initialize,
+ );
+ TelemetryService.instance.addBreadcrumb(
+ 'app',
+ 'backend_config_checked',
+ data: {'configured': hasUrl},
+ );
if (mounted) {
setState(() {
_hasBackendUrl = hasUrl;
diff --git a/mobile/lib/models/account.dart b/mobile/lib/models/account.dart
index 046aa0601..23bf00bbd 100644
--- a/mobile/lib/models/account.dart
+++ b/mobile/lib/models/account.dart
@@ -1,28 +1,66 @@
+import '../utils/json_parsing.dart';
+
class Account {
final String id;
final String name;
final String balance;
+ final int? balanceCents;
+ final String? cashBalance;
+ final int? cashBalanceCents;
final String currency;
final String? classification;
final String accountType;
+ final String? subtype;
+ final String? status;
+ final String? institutionName;
+ final String? institutionDomain;
+ final DateTime? createdAt;
+ final DateTime? updatedAt;
Account({
required this.id,
required this.name,
required this.balance,
+ this.balanceCents,
+ this.cashBalance,
+ this.cashBalanceCents,
required this.currency,
this.classification,
required this.accountType,
+ this.subtype,
+ this.status,
+ this.institutionName,
+ this.institutionDomain,
+ this.createdAt,
+ this.updatedAt,
});
factory Account.fromJson(Map json) {
return Account(
id: json['id'].toString(),
- name: json['name'] as String,
- balance: json['balance'] as String,
- currency: json['currency'] as String,
- classification: json['classification'] as String?,
- accountType: json['account_type'] as String,
+ name: JsonParsing.parseRequiredString(json['name'], 'account name'),
+ balance: JsonParsing.parseRequiredString(
+ json['balance'],
+ 'account balance',
+ ),
+ balanceCents: JsonParsing.parseInt(json['balance_cents']),
+ cashBalance: JsonParsing.parseString(json['cash_balance']),
+ cashBalanceCents: JsonParsing.parseInt(json['cash_balance_cents']),
+ currency: JsonParsing.parseRequiredString(
+ json['currency'],
+ 'account currency',
+ ),
+ classification: JsonParsing.parseString(json['classification']),
+ accountType: JsonParsing.parseRequiredString(
+ json['account_type'],
+ 'account type',
+ ),
+ subtype: JsonParsing.parseString(json['subtype']),
+ status: JsonParsing.parseString(json['status']),
+ institutionName: JsonParsing.parseString(json['institution_name']),
+ institutionDomain: JsonParsing.parseString(json['institution_domain']),
+ createdAt: JsonParsing.parseDateTime(json['created_at']),
+ updatedAt: JsonParsing.parseDateTime(json['updated_at']),
);
}
diff --git a/mobile/lib/models/account_balance.dart b/mobile/lib/models/account_balance.dart
new file mode 100644
index 000000000..66d171152
--- /dev/null
+++ b/mobile/lib/models/account_balance.dart
@@ -0,0 +1,39 @@
+import '../utils/json_parsing.dart';
+
+class AccountBalance {
+ final String id;
+ final DateTime date;
+ final String currency;
+ final String balance;
+ final int? balanceCents;
+ final String? cashBalance;
+ final int? cashBalanceCents;
+
+ AccountBalance({
+ required this.id,
+ required this.date,
+ required this.currency,
+ required this.balance,
+ this.balanceCents,
+ this.cashBalance,
+ this.cashBalanceCents,
+ });
+
+ factory AccountBalance.fromJson(Map json) {
+ return AccountBalance(
+ id: json['id'].toString(),
+ date: JsonParsing.parseRequiredDateTime(json['date'], 'account balance'),
+ currency: JsonParsing.parseRequiredString(
+ json['currency'],
+ 'account balance currency',
+ ),
+ balance: JsonParsing.parseRequiredString(
+ json['balance'],
+ 'account balance',
+ ),
+ balanceCents: JsonParsing.parseInt(json['balance_cents']),
+ cashBalance: JsonParsing.parseString(json['cash_balance']),
+ cashBalanceCents: JsonParsing.parseInt(json['cash_balance_cents']),
+ );
+ }
+}
diff --git a/mobile/lib/models/account_holding.dart b/mobile/lib/models/account_holding.dart
new file mode 100644
index 000000000..dc2537b2c
--- /dev/null
+++ b/mobile/lib/models/account_holding.dart
@@ -0,0 +1,51 @@
+import '../utils/json_parsing.dart';
+
+class AccountHolding {
+ final String id;
+ final DateTime date;
+ final String quantity;
+ final String price;
+ final String amount;
+ final String currency;
+ final String? ticker;
+ final String? securityName;
+
+ AccountHolding({
+ required this.id,
+ required this.date,
+ required this.quantity,
+ required this.price,
+ required this.amount,
+ required this.currency,
+ this.ticker,
+ this.securityName,
+ });
+
+ factory AccountHolding.fromJson(Map json) {
+ final securityJson = json['security'];
+ final security = securityJson is Map ? securityJson : null;
+
+ return AccountHolding(
+ id: json['id'].toString(),
+ date: JsonParsing.parseRequiredDateTime(json['date'], 'account holding'),
+ quantity: JsonParsing.parseRequiredString(
+ json['qty'],
+ 'account holding quantity',
+ ),
+ price: JsonParsing.parseRequiredString(
+ json['price'],
+ 'account holding price',
+ ),
+ amount: JsonParsing.parseRequiredString(
+ json['amount'],
+ 'account holding amount',
+ ),
+ currency: JsonParsing.parseRequiredString(
+ json['currency'],
+ 'account holding currency',
+ ),
+ ticker: JsonParsing.parseString(security?['ticker']),
+ securityName: JsonParsing.parseString(security?['name']),
+ );
+ }
+}
diff --git a/mobile/lib/providers/accounts_provider.dart b/mobile/lib/providers/accounts_provider.dart
index 35c85a928..885163c40 100644
--- a/mobile/lib/providers/accounts_provider.dart
+++ b/mobile/lib/providers/accounts_provider.dart
@@ -54,7 +54,8 @@ class AccountsProvider with ChangeNotifier {
Map get assetTotalsByCurrency {
final totals = {};
for (var account in _accounts.where((a) => a.isAsset)) {
- totals[account.currency] = (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
+ totals[account.currency] =
+ (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
}
return totals;
}
@@ -62,7 +63,8 @@ class AccountsProvider with ChangeNotifier {
Map get liabilityTotalsByCurrency {
final totals = {};
for (var account in _accounts.where((a) => a.isLiability)) {
- totals[account.currency] = (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
+ totals[account.currency] =
+ (totals[account.currency] ?? 0.0) + account.balanceAsDouble;
}
return totals;
}
@@ -120,7 +122,8 @@ class AccountsProvider with ChangeNotifier {
);
if (result['success'] == true && result.containsKey('accounts')) {
- final serverAccounts = (result['accounts'] as List?)?.cast() ?? [];
+ final serverAccounts =
+ (result['accounts'] as List?)?.cast() ?? [];
_pagination = result['pagination'] as Map?;
// Save to local cache
@@ -133,11 +136,13 @@ class AccountsProvider with ChangeNotifier {
} else {
// If server fetch failed but we have cached data, that's OK
if (_accounts.isEmpty) {
- _errorMessage = result['error'] as String? ?? 'Failed to fetch accounts';
+ _errorMessage =
+ result['error'] as String? ?? 'Failed to fetch accounts';
}
}
} else if (!isOnline && _accounts.isEmpty) {
- _errorMessage = 'You are offline. Please connect to the internet to load accounts.';
+ _errorMessage =
+ 'You are offline. Please connect to the internet to load accounts.';
}
// Fetch balance sheet independently — works even with cached accounts
@@ -150,30 +155,31 @@ class AccountsProvider with ChangeNotifier {
notifyListeners();
return _accounts.isNotEmpty;
} catch (e) {
- _log.error('AccountsProvider', 'Error in fetchAccounts: $e');
+ _log.error(
+ 'AccountsProvider',
+ 'fetchAccounts failed with ${e.runtimeType}',
+ );
// If we have cached accounts, show them even if sync fails
if (_accounts.isEmpty) {
// Provide more specific error messages based on exception type
if (e is SocketException) {
- _errorMessage = 'Network error. Please check your internet connection and try again.';
- _log.error('AccountsProvider', 'SocketException: $e');
+ _errorMessage =
+ 'Network error. Please check your internet connection and try again.';
} else if (e is TimeoutException) {
- _errorMessage = 'Request timed out. Please check your connection and try again.';
- _log.error('AccountsProvider', 'TimeoutException: $e');
+ _errorMessage =
+ 'Request timed out. Please check your connection and try again.';
} else if (e is FormatException) {
_errorMessage = 'Server response error. Please try again later.';
- _log.error('AccountsProvider', 'FormatException: $e');
- } else if (e.toString().contains('401') || e.toString().contains('unauthorized')) {
+ } else if (e.toString().contains('401') ||
+ e.toString().contains('unauthorized')) {
_errorMessage = 'unauthorized';
- _log.error('AccountsProvider', 'Unauthorized error: $e');
} else if (e.toString().contains('HandshakeException') ||
- e.toString().contains('certificate') ||
- e.toString().contains('SSL')) {
- _errorMessage = 'Secure connection error. Please check your internet connection and try again.';
- _log.error('AccountsProvider', 'SSL/Certificate error: $e');
+ e.toString().contains('certificate') ||
+ e.toString().contains('SSL')) {
+ _errorMessage =
+ 'Secure connection error. Please check your internet connection and try again.';
} else {
_errorMessage = 'Something went wrong. Please try again.';
- _log.error('AccountsProvider', 'Unhandled exception: $e');
}
}
_isLoading = false;
@@ -188,7 +194,8 @@ class AccountsProvider with ChangeNotifier {
/// values as stale rather than clearing them.
Future _fetchBalanceSheet(String accessToken) async {
try {
- final result = await _balanceSheetService.getBalanceSheet(accessToken: accessToken);
+ final result =
+ await _balanceSheetService.getBalanceSheet(accessToken: accessToken);
if (result['success'] == true) {
_familyCurrency = result['currency'] as String?;
final netWorth = result['net_worth'] as Map?;
@@ -205,7 +212,10 @@ class AccountsProvider with ChangeNotifier {
}
}
} catch (e) {
- _log.error('AccountsProvider', 'Error fetching balance sheet: $e');
+ _log.error(
+ 'AccountsProvider',
+ 'Balance sheet fetch failed with ${e.runtimeType}',
+ );
// Keep existing values but mark as stale
if (_netWorthFormatted != null) {
_isBalanceSheetStale = true;
diff --git a/mobile/lib/providers/auth_provider.dart b/mobile/lib/providers/auth_provider.dart
index c9be3741f..d3d570d50 100644
--- a/mobile/lib/providers/auth_provider.dart
+++ b/mobile/lib/providers/auth_provider.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -7,6 +9,7 @@ import '../services/auth_service.dart';
import '../services/device_service.dart';
import '../services/api_config.dart';
import '../services/log_service.dart';
+import '../services/telemetry_service.dart';
class AuthProvider with ChangeNotifier {
final AuthService _authService = AuthService();
@@ -99,11 +102,26 @@ class AuthProvider with ChangeNotifier {
}
}
}
- } catch (e) {
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'stored_auth_loaded',
+ data: {
+ 'authenticated': isAuthenticated,
+ 'api_key_mode': _isApiKeyAuth,
+ },
+ );
+ } catch (e, stackTrace) {
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.load_stored',
+ );
_tokens = null;
_user = null;
_apiKey = null;
_isApiKeyAuth = false;
+ _clearTelemetryUser();
}
_isLoading = false;
@@ -144,6 +162,8 @@ class AuthProvider with ChangeNotifier {
_user = result['user'] as User?;
_mfaRequired = false;
_showMfaInput = false; // Reset on successful login
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb('auth', 'login_success');
_isLoading = false;
notifyListeners();
return true;
@@ -171,12 +191,25 @@ class AuthProvider with ChangeNotifier {
_showMfaInput = true;
}
}
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'login_failed',
+ data: {
+ 'mfa_required': _mfaRequired,
+ 'otp_submitted': otpCode != null,
+ },
+ );
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('Login', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.login',
+ );
_errorMessage =
'Unable to connect. Please check your network and try again.';
_isLoading = false;
@@ -204,17 +237,31 @@ class AuthProvider with ChangeNotifier {
_apiKey = apiKey;
_isApiKeyAuth = true;
ApiConfig.setApiKeyAuth(apiKey);
+ _clearTelemetryUser();
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'api_key_login_success',
+ );
_isLoading = false;
notifyListeners();
return true;
} else {
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'api_key_login_failed',
+ );
_errorMessage = result['error'] as String?;
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('API key login', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.api_key_login',
+ );
_errorMessage =
'Unable to connect. Please check your network and try again.';
_isLoading = false;
@@ -248,17 +295,25 @@ class AuthProvider with ChangeNotifier {
if (result['success'] == true) {
_tokens = result['tokens'] as AuthTokens?;
_user = result['user'] as User?;
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb('auth', 'signup_success');
_isLoading = false;
notifyListeners();
return true;
} else {
+ _addTelemetryBreadcrumb('auth', 'signup_failed');
_errorMessage = result['error'] as String?;
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('Signup', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.signup',
+ );
_errorMessage =
'Unable to connect. Please check your network and try again.';
_isLoading = false;
@@ -281,11 +336,21 @@ class AuthProvider with ChangeNotifier {
final launched = await launchUrl(Uri.parse(ssoUrl),
mode: LaunchMode.externalApplication);
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_launch_result',
+ data: {'launched': launched},
+ );
if (!launched) {
_errorMessage = 'Unable to open browser for sign-in.';
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('SSO launch', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.sso_launch',
+ );
_errorMessage = 'Unable to start sign-in. Please try again.';
} finally {
_isLoading = false;
@@ -305,6 +370,11 @@ class AuthProvider with ChangeNotifier {
_tokens = result['tokens'] as AuthTokens?;
_user = result['user'] as User?;
_ssoOnboardingPending = false;
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_callback_success',
+ );
_isLoading = false;
notifyListeners();
return true;
@@ -317,17 +387,34 @@ class AuthProvider with ChangeNotifier {
_ssoLastName = result['last_name'] as String?;
_ssoAllowAccountCreation = result['allow_account_creation'] == true;
_ssoHasPendingInvitation = result['has_pending_invitation'] == true;
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_onboarding_required',
+ data: {
+ 'account_creation_allowed': _ssoAllowAccountCreation,
+ 'has_pending_invitation': _ssoHasPendingInvitation,
+ },
+ );
_isLoading = false;
notifyListeners();
return false;
} else {
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_callback_failed',
+ );
_errorMessage = result['error'] as String?;
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('SSO callback', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.sso_callback',
+ );
_errorMessage = 'Sign-in failed. Please try again.';
_isLoading = false;
notifyListeners();
@@ -360,17 +447,28 @@ class AuthProvider with ChangeNotifier {
_tokens = result['tokens'] as AuthTokens?;
_user = result['user'] as User?;
_clearSsoOnboardingState();
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_link_success',
+ );
_isLoading = false;
notifyListeners();
return true;
} else {
+ _addTelemetryBreadcrumb('auth', 'sso_link_failed');
_errorMessage = result['error'] as String?;
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('SSO link', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.sso_link',
+ );
_errorMessage = 'Failed to link account. Please try again.';
_isLoading = false;
notifyListeners();
@@ -403,17 +501,31 @@ class AuthProvider with ChangeNotifier {
_tokens = result['tokens'] as AuthTokens?;
_user = result['user'] as User?;
_clearSsoOnboardingState();
+ _updateTelemetryUser(_user);
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_create_account_success',
+ );
_isLoading = false;
notifyListeners();
return true;
} else {
+ _addTelemetryBreadcrumb(
+ 'auth',
+ 'sso_create_account_failed',
+ );
_errorMessage = result['error'] as String?;
_isLoading = false;
notifyListeners();
return false;
}
- } catch (e) {
+ } catch (e, stackTrace) {
_logAuthException('SSO create account', e);
+ _captureTelemetryException(
+ e,
+ stackTrace,
+ operation: 'auth.sso_create_account',
+ );
_errorMessage = 'Failed to create account. Please try again.';
_isLoading = false;
notifyListeners();
@@ -445,6 +557,10 @@ class AuthProvider with ChangeNotifier {
_errorMessage = null;
_mfaRequired = false;
ApiConfig.clearApiKeyAuth();
+ _safeTelemetry(() async {
+ TelemetryService.instance.addBreadcrumb('auth', 'logout');
+ await TelemetryService.instance.clearUser();
+ });
notifyListeners();
}
@@ -472,6 +588,64 @@ class AuthProvider with ChangeNotifier {
}
}
+ Future _setTelemetryUser(User? user) async {
+ if (user == null) {
+ await TelemetryService.instance.clearUser();
+ return;
+ }
+
+ await TelemetryService.instance.setUserId(user.id);
+ }
+
+ void _updateTelemetryUser(User? user) {
+ _safeTelemetry(() => _setTelemetryUser(user));
+ }
+
+ void _clearTelemetryUser() {
+ _safeTelemetry(() => TelemetryService.instance.clearUser());
+ }
+
+ void _addTelemetryBreadcrumb(
+ String category,
+ String message, {
+ Map? data,
+ }) {
+ _safeTelemetry(
+ () => TelemetryService.instance.addBreadcrumb(
+ category,
+ message,
+ data: data,
+ ),
+ );
+ }
+
+ void _captureTelemetryException(
+ Object error,
+ StackTrace stackTrace, {
+ required String operation,
+ }) {
+ _safeTelemetry(
+ () => TelemetryService.instance.captureHandledException(
+ error,
+ stackTrace,
+ operation: operation,
+ ),
+ );
+ }
+
+ void _safeTelemetry(FutureOr Function() action) {
+ unawaited(Future(() async {
+ try {
+ await action();
+ } catch (error) {
+ LogService.instance.warning(
+ 'AuthProvider',
+ 'Telemetry operation failed: ${error.runtimeType}',
+ );
+ }
+ }));
+ }
+
Future getValidAccessToken() async {
if (_isApiKeyAuth && _apiKey != null) {
return _apiKey;
diff --git a/mobile/lib/providers/transactions_provider.dart b/mobile/lib/providers/transactions_provider.dart
index 8d63a2ed6..e27364103 100644
--- a/mobile/lib/providers/transactions_provider.dart
+++ b/mobile/lib/providers/transactions_provider.dart
@@ -71,7 +71,10 @@ class TransactionsProvider with ChangeNotifier {
}
}).catchError((e) {
if (!_isDisposed) {
- _log.error('TransactionsProvider', 'Auto-sync failed: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Auto-sync failed with ${e.runtimeType}',
+ );
}
}).whenComplete(() {
if (!_isDisposed) {
@@ -142,7 +145,10 @@ class TransactionsProvider with ChangeNotifier {
}
}
} catch (e) {
- _log.error('TransactionsProvider', 'Error in fetchTransactions: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'fetchTransactions failed with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
} finally {
_isLoading = false;
@@ -219,6 +225,8 @@ class TransactionsProvider with ChangeNotifier {
categoryId: categoryId,
merchantId: merchantId,
tagIds: tagIds == null || tagIds.isEmpty ? null : tagIds,
+ externalId: localTransaction.localId,
+ source: TransactionsService.mobileIdempotencySource,
)
.then((result) async {
if (_isDisposed) return;
@@ -243,7 +251,10 @@ class TransactionsProvider with ChangeNotifier {
}).catchError((e) {
if (_isDisposed) return;
- _log.error('TransactionsProvider', 'Exception during upload: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Upload failed with ${e.runtimeType}',
+ );
_error = 'Failed to upload transaction. It will sync when online.';
notifyListeners();
});
@@ -254,7 +265,10 @@ class TransactionsProvider with ChangeNotifier {
return true; // Always return true because it's saved locally
} catch (e) {
- _log.error('TransactionsProvider', 'Failed to create transaction: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Failed to create transaction with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
notifyListeners();
return false;
@@ -336,7 +350,10 @@ class TransactionsProvider with ChangeNotifier {
_error = result['error'] as String? ?? 'Failed to update transaction';
return false;
} catch (e) {
- _log.error('TransactionsProvider', 'Failed to update transaction: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Failed to update transaction with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
return false;
} finally {
@@ -386,7 +403,10 @@ class TransactionsProvider with ChangeNotifier {
return true;
}
} catch (e) {
- _log.error('TransactionsProvider', 'Failed to delete transaction: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Failed to delete transaction with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
notifyListeners();
return false;
@@ -439,7 +459,9 @@ class TransactionsProvider with ChangeNotifier {
}
} catch (e) {
_log.error(
- 'TransactionsProvider', 'Failed to delete multiple transactions: $e');
+ 'TransactionsProvider',
+ 'Failed to delete multiple transactions with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
notifyListeners();
return false;
@@ -475,7 +497,10 @@ class TransactionsProvider with ChangeNotifier {
return false;
}
} catch (e) {
- _log.error('TransactionsProvider', 'Failed to undo transaction: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Failed to undo transaction with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
notifyListeners();
return false;
@@ -507,7 +532,10 @@ class TransactionsProvider with ChangeNotifier {
_error = result.error;
}
} catch (e) {
- _log.error('TransactionsProvider', 'Failed to sync transactions: $e');
+ _log.error(
+ 'TransactionsProvider',
+ 'Failed to sync transactions with ${e.runtimeType}',
+ );
_error = 'Something went wrong. Please try again.';
} finally {
_isLoading = false;
diff --git a/mobile/lib/screens/backend_config_screen.dart b/mobile/lib/screens/backend_config_screen.dart
index a3c1afe96..f6e90bfa4 100644
--- a/mobile/lib/screens/backend_config_screen.dart
+++ b/mobile/lib/screens/backend_config_screen.dart
@@ -53,7 +53,7 @@ class _BackendConfigScreenState extends State {
// sensible defaults; the user can re-enter and re-save.
LogService.instance.warning(
'BackendConfigScreen',
- 'Failed to load saved backend config: $e',
+ 'Failed to load saved backend config with ${e.runtimeType}',
);
} finally {
if (mounted) {
diff --git a/mobile/lib/screens/settings_screen.dart b/mobile/lib/screens/settings_screen.dart
index 316b8d400..89ae9f067 100644
--- a/mobile/lib/screens/settings_screen.dart
+++ b/mobile/lib/screens/settings_screen.dart
@@ -115,7 +115,7 @@ class _SettingsScreenState extends State {
} catch (e) {
LogService.instance.warning(
'SettingsScreen',
- 'Failed to load custom headers: $e',
+ 'Failed to load custom headers with ${e.runtimeType}',
);
// Keep the existing _customHeaders state so the screen remains usable.
}
@@ -172,14 +172,17 @@ class _SettingsScreenState extends State {
}
} catch (e) {
final log = LogService.instance;
- log.error('Settings', 'Failed to clear local data: $e');
+ log.error(
+ 'Settings',
+ 'Failed to clear local data with ${e.runtimeType}',
+ );
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text('Failed to clear local data: $e'),
+ const SnackBar(
+ content: Text('Failed to clear local data.'),
backgroundColor: Colors.red,
- duration: const Duration(seconds: 3),
+ duration: Duration(seconds: 3),
),
);
}
@@ -417,9 +420,13 @@ class _SettingsScreenState extends State {
);
} catch (e) {
if (!mounted) return;
+ LogService.instance.warning(
+ 'Settings',
+ 'Failed to save custom proxy headers with ${e.runtimeType}',
+ );
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
- content: Text('Failed to save custom proxy headers: $e'),
+ content: const Text('Failed to save custom proxy headers.'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
diff --git a/mobile/lib/screens/transactions_list_screen.dart b/mobile/lib/screens/transactions_list_screen.dart
index 9369fe35c..8e9cb4100 100644
--- a/mobile/lib/screens/transactions_list_screen.dart
+++ b/mobile/lib/screens/transactions_list_screen.dart
@@ -8,6 +8,7 @@ import '../providers/categories_provider.dart';
import '../providers/transactions_provider.dart';
import '../screens/transaction_edit_screen.dart';
import '../screens/transaction_form_screen.dart';
+import '../widgets/account_detail_header.dart';
import '../widgets/category_filter.dart';
import '../widgets/sync_status_badge.dart';
import '../services/log_service.dart';
@@ -356,8 +357,12 @@ class _TransactionsListScreenState extends State {
),
],
),
- body: Consumer(
- builder: (context, transactionsProvider, child) {
+ body: Column(
+ children: [
+ AccountDetailHeader(account: widget.account),
+ Expanded(
+ child: Consumer(
+ builder: (context, transactionsProvider, child) {
if (transactionsProvider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
@@ -708,7 +713,10 @@ class _TransactionsListScreenState extends State {
],
),
);
- },
+ },
+ ),
+ ),
+ ],
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTransactionForm,
diff --git a/mobile/lib/services/account_detail_service.dart b/mobile/lib/services/account_detail_service.dart
new file mode 100644
index 000000000..378d85eff
--- /dev/null
+++ b/mobile/lib/services/account_detail_service.dart
@@ -0,0 +1,258 @@
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+import '../models/account.dart';
+import '../models/account_balance.dart';
+import '../models/account_holding.dart';
+import '../utils/json_parsing.dart';
+import 'api_config.dart';
+import 'log_service.dart';
+
+class AccountDetailService {
+ static const int _holdingsPageSize = 100;
+
+ final http.Client _client;
+ final bool _ownsClient;
+
+ AccountDetailService({http.Client? client})
+ : _client = client ?? http.Client(),
+ _ownsClient = client == null;
+
+ void close() {
+ if (_ownsClient) {
+ _client.close();
+ }
+ }
+
+ Future