Files
sure/test/i18n_test.rb
T
Aland BabanandAland Baban 0cdab9a0bc Add first-class Trade Republic support (#3168)
* Add Trade Republic provider integration

Introduce authenticated web and QR login, resilient account synchronization, deterministic financial imports, account discovery, and provider diagnostics. Keep login state encrypted, PINs transient, and incomplete provider responses non-destructive.

* Address Trade Republic review findings

Keep QR-authenticated sessions syncable, preserve historical holding snapshots, correct dividend direction, handle unpriced positions safely, localize repair feedback, and align provider controls with the design system.

* Add Trade Republic translations for supported locales

* Restore German Trade Republic account labels

* Resolve remaining Trade Republic review findings

* Resolve remaining Trade Republic review findings

* Address latest Trade Republic review feedback

* Refactor Trade Republic panel buttons to use DS::Button component and add integration tests

* Fix 100x money inflation and missing positions locale key in TR views

Money.new takes major units, so multiplying by 100 displayed EUR 12.34
as EUR 1234 in the holdings category cards and expense summary. Also
add the pluralized holdings.index.positions key that t(".positions")
resolves to (previously only defined at the unused holdings.positions
root level), across all 18 locales.

* fix(db): repair merge artifacts in schema and migrations

- Remove duplicated icon/progress_basis columns on goals in schema.rb
- Renumber Trade Republic migrations to unique versions (clashed with
  main's 20260824120000_add_lifecycle_to_goals)
- Bump schema version to match latest migration

* Address remaining Trade Republic review feedback

* fix(trade-republic): address open PR #3168 review findings\n\n- Reject authenticated sessions without a securities account number so a\n  blank account does not mark the item connected on a broken session.\n- Derive a missing trade amount from |quantity| x price, and a missing\n  price from the resolved amount, without changing the signed import amount.\n- Regenerate db/schema.rb so the Trade Republic item/account tables and\n  indexes are present; a fresh test database was otherwise missing the\n  tables even though the migrations were marked up.\n

* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)

* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)

* fix(trade-republic): add activity labels i18n keys to all locale files

* Fix Trade Republic PR review follow-ups

* fix(trade-republic): i18n-aware category guard and ignore generated graphify cache

- Category matcher skipped core deposit/withdrawal labels; guard now compares
  against translated values so German etc skip correctly
- Remove committed graphify-out cache and ignore dir

* Protect holdings from malformed snapshots

* Consolidate Trade Republic migrations

* Address final Trade Republic review comments

* Address final Trade Republic review comments

- Remove hard-coded category matcher (merchant keyword taxonomy) and leave Trade Republic transactions uncategorized when no structured category exists; rely on Sure rules/AI
- Revert shared ProviderImportAdapter# import_trade extra: param; handle Trade Republic trade metadata locally in ActivitiesProcessor via post-import Trade extra merge (preserve existing extra, deep_merge)
- Preserve Trade Republic product distinctions (cash, brokerage/private_markets/interest_products/crypto_wallet via portfolio categories) without collapsing account kinds

---------

Co-authored-by: Aland Baban <snow@iBananaMac.fritz.box>
2026-09-03 00:24:49 +02:00

136 lines
5.0 KiB
Ruby

require "i18n/tasks"
require "pathname"
require "yaml"
# We're currently skipping some i18n tests to speed up development. Eventually, we'll make a dedicated
# project for getting i18n working. More details on that here:
# https://github.com/maybe-finance/maybe/issues/1225
class I18nTest < ActiveSupport::TestCase
GERMAN_COVERAGE_GLOBS = [
"config/locales/breadcrumbs/*.yml",
"config/locales/doorkeeper.*.yml",
"config/locales/mailers/**/*.yml",
"config/locales/models/**/*.yml",
"config/locales/views/**/*.yml"
]
def setup
@i18n = I18n::Tasks::BaseTask.new
end
def test_german_locale_files_parse
german_locale_paths.each do |path|
assert_nothing_raised do
YAML.load_file(path, aliases: true)
end
end
end
def test_no_missing_keys
skip "Skipping missing keys test"
missing_keys = @i18n.missing_keys(locales: [ :en ])
assert_empty missing_keys,
"Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them"
end
def test_no_unused_keys
skip "Skipping unused keys test"
unused_keys = @i18n.unused_keys(locales: [ :en ])
assert_empty unused_keys,
"#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them"
end
def test_files_are_normalized
skip "Skipping file normalization test"
non_normalized = @i18n.non_normalized_paths(locales: [ :en ])
error_message = "The following files need to be normalized:\n" \
"#{non_normalized.map { |path| " #{path}" }.join("\n")}\n" \
"Please run `i18n-tasks normalize' to fix"
assert_empty non_normalized, error_message
end
def test_no_inconsistent_interpolations
skip "Skipping inconsistent interpolations test"
inconsistent_interpolations = @i18n.inconsistent_interpolations(locales: [ :en ])
error_message = "#{inconsistent_interpolations.leaves.count} i18n keys have inconsistent interpolations.\n" \
"Please run `i18n-tasks check-consistent-interpolations' to show them"
assert_empty inconsistent_interpolations, error_message
end
# YAML silently resolves duplicate keys by letting the last occurrence win,
# so a duplicated key shadows the earlier definition without any warning
# (see #1506 / #1502, where a stale `transactions.merge_duplicate` string
# shadowed — or was shadowed by — the `merge_duplicate.success/failure`
# mapping in several locales). Parse the raw YAML AST so duplicates can't
# sneak back in.
def test_no_duplicate_keys_within_locale_files
offenses = []
Dir[File.expand_path("../config/locales/**/*.yml", __dir__)].sort.each do |file|
Psych.parse_stream(File.read(file), filename: file).children.each do |doc|
offenses.concat(duplicate_key_offenses(doc.root, [], file))
end
end
assert_empty offenses,
"Duplicate keys found in locale files (the last occurrence silently wins):\n" \
"#{offenses.map { |offense| " #{offense}" }.join("\n")}"
end
def test_trade_republic_activity_labels_exist_for_each_locale
required_labels = %w[
contribution withdrawal interest dividend card_payment cash_withdrawal
card_fee card_refund tax_refund buy sell
]
Dir[File.expand_path("../config/locales/views/trade_republic_items/*.yml", __dir__)].sort.each do |file|
locale = File.basename(file, ".yml")
labels = YAML.load_file(file, aliases: true)
.fetch(locale)
.dig("trade_republic_items", "activities", "labels")
assert labels.is_a?(Hash), "#{file} must define trade_republic_items.activities.labels"
assert_empty required_labels - labels.keys,
"#{file} is missing Trade Republic activity labels"
end
end
private
def german_locale_paths
@german_locale_paths ||= locale_paths.select { |path| path.basename.to_s.match?(/(^|[._-])de\.yml\z/) }
end
def locale_paths
@locale_paths ||= GERMAN_COVERAGE_GLOBS.flat_map { |glob| Pathname.pwd.glob(glob) }.uniq
end
def duplicate_key_offenses(node, path, file)
offenses = []
case node
when Psych::Nodes::Mapping
first_definition_lines = {}
node.children.each_slice(2) do |key_node, value_node|
if key_node.is_a?(Psych::Nodes::Scalar)
key_path = path + [ key_node.value ]
if (first_line = first_definition_lines[key_node.value])
offenses << "#{file}:#{key_node.start_line + 1} duplicate key `#{key_path.join(".")}` (first defined on line #{first_line})"
else
first_definition_lines[key_node.value] = key_node.start_line + 1
end
offenses.concat(duplicate_key_offenses(value_node, key_path, file))
else
offenses.concat(duplicate_key_offenses(value_node, path, file))
end
end
when Psych::Nodes::Sequence
node.children.each { |child| offenses.concat(duplicate_key_offenses(child, path, file)) }
end
offenses
end
end