From 63b0d3f662a3e711fcc2f6cfd8290c05e4f390b2 Mon Sep 17 00:00:00 2001 From: Thomas Steibl <309042+Rowdy@users.noreply.github.com> Date: Wed, 2 Sep 2026 19:43:07 +0200 Subject: [PATCH] fix(i18n): localize Lunchflow account setup (#3330) Translate the remaining provider-owned setup and status copy while reusing Accountable subtype labels before legacy English fallbacks. Cover German rendering, error handling, and pluralized English and German status summaries. Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> --- app/controllers/lunchflow_items_controller.rb | 14 ++-- app/models/lunchflow_item.rb | 11 ++- .../lunchflow_items/_subtype_select.html.erb | 3 +- .../lunchflow_items/setup_accounts.html.erb | 2 +- config/locales/views/lunchflow_items/de.yml | 27 +++++++ config/locales/views/lunchflow_items/en.yml | 14 ++++ .../lunchflow_items_controller_test.rb | 77 +++++++++++++++++++ test/models/lunchflow_item_test.rb | 27 +++++++ 8 files changed, 164 insertions(+), 11 deletions(-) diff --git a/app/controllers/lunchflow_items_controller.rb b/app/controllers/lunchflow_items_controller.rb index c52be446a..8abfc525d 100644 --- a/app/controllers/lunchflow_items_controller.rb +++ b/app/controllers/lunchflow_items_controller.rb @@ -534,15 +534,17 @@ class LunchflowItemsController < ApplicationController @account_type_options = [ [ t(".account_types.skip"), "skip" ] ] + all_account_type_options # Helper to translate subtype options - translate_subtypes = ->(type_key, subtypes_hash) { - subtypes_hash.map { |k, v| [ t(".subtypes.#{type_key}.#{k}", default: v[:long] || k.humanize), k ] } + translate_subtypes = ->(type_key, accountable_class) { + accountable_class::SUBTYPES.keys.map do |key| + [ t(".subtypes.#{type_key}.#{key}", default: accountable_class.long_subtype_label_for(key)), key ] + end } # Subtype options for each account type (only include supported types) all_subtype_options = { "Depository" => { label: t(".subtype_labels.depository"), - options: translate_subtypes.call("depository", Depository::SUBTYPES) + options: translate_subtypes.call("depository", Depository) }, "CreditCard" => { label: t(".subtype_labels.credit_card"), @@ -551,11 +553,11 @@ class LunchflowItemsController < ApplicationController }, "Investment" => { label: t(".subtype_labels.investment"), - options: translate_subtypes.call("investment", Investment::SUBTYPES) + options: translate_subtypes.call("investment", Investment) }, "Loan" => { label: t(".subtype_labels.loan"), - options: translate_subtypes.call("loan", Loan::SUBTYPES) + options: translate_subtypes.call("loan", Loan) }, "OtherAsset" => { label: t(".subtype_labels.other_asset").presence, @@ -642,7 +644,7 @@ class LunchflowItemsController < ApplicationController rescue StandardError => e Rails.logger.error("LunchFlow account setup failed unexpectedly: #{e.class} - #{e.message}") Rails.logger.error(e.backtrace.first(10).join("\n")) - flash[:alert] = t(".creation_failed", error: "An unexpected error occurred") + flash[:alert] = t(".creation_failed", error: t(".unexpected_error")) redirect_to accounts_path, status: :see_other return end diff --git a/app/models/lunchflow_item.rb b/app/models/lunchflow_item.rb index 94cf078e7..40a56e182 100644 --- a/app/models/lunchflow_item.rb +++ b/app/models/lunchflow_item.rb @@ -106,11 +106,16 @@ class LunchflowItem < ApplicationRecord unlinked_count = unlinked_accounts_count if total_accounts == 0 - "No accounts found" + I18n.t("lunchflow_items.lunchflow_item.sync_status.no_accounts") elsif unlinked_count == 0 - "#{linked_count} #{'account'.pluralize(linked_count)} synced" + I18n.t("lunchflow_items.lunchflow_item.sync_status.synced", count: linked_count) else - "#{linked_count} synced, #{unlinked_count} need setup" + I18n.t( + "lunchflow_items.lunchflow_item.sync_status.partial_setup", + count: unlinked_count, + linked: linked_count, + unlinked: unlinked_count + ) end end diff --git a/app/views/lunchflow_items/_subtype_select.html.erb b/app/views/lunchflow_items/_subtype_select.html.erb index 5d99dbcb8..307d9407f 100644 --- a/app/views/lunchflow_items/_subtype_select.html.erb +++ b/app/views/lunchflow_items/_subtype_select.html.erb @@ -14,8 +14,9 @@ <% selected_value = "money_market" %> <% end %> <% end %> + <% placeholder_key = account_type == "Depository" ? "subtype" : "type" %> <%= select_tag "account_subtypes[#{lunchflow_account.id}]", - options_for_select([["Select #{account_type == 'Depository' ? 'subtype' : 'type'}", ""]] + subtype_config[:options], selected_value), + options_for_select([[t(".placeholder.#{placeholder_key}"), ""]] + subtype_config[:options], selected_value), { class: "appearance-none bg-container border border-primary rounded-md px-3 py-2 text-sm leading-6 text-primary focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none w-full" } %> <% else %>
<%= subtype_config[:message] %>
diff --git a/app/views/lunchflow_items/setup_accounts.html.erb b/app/views/lunchflow_items/setup_accounts.html.erb index 551292c7e..8b9009950 100644 --- a/app/views/lunchflow_items/setup_accounts.html.erb +++ b/app/views/lunchflow_items/setup_accounts.html.erb @@ -1,4 +1,4 @@ -<% content_for :title, "Set Up Lunch Flow Accounts" %> +<% content_for :title, t(".page_title") %> <%= render DS::Dialog.new do |dialog| %> <% dialog.with_header(title: t(".title")) do %> diff --git a/config/locales/views/lunchflow_items/de.yml b/config/locales/views/lunchflow_items/de.yml index ff02c4b7f..b3448946d 100644 --- a/config/locales/views/lunchflow_items/de.yml +++ b/config/locales/views/lunchflow_items/de.yml @@ -40,8 +40,20 @@ de: status_never: Noch nie synchronisiert status_with_summary: "Zuletzt vor %{timestamp} synchronisiert • %{summary}" syncing: Wird synchronisiert... + sync_status: + no_accounts: Keine Konten gefunden + synced: + one: "%{count} Konto synchronisiert" + other: "%{count} Konten synchronisiert" + partial_setup: + one: "%{linked} synchronisiert, %{unlinked} muss eingerichtet werden" + other: "%{linked} synchronisiert, %{unlinked} müssen eingerichtet werden" total: Gesamt unlinked: Nicht verknüpft + subtype_select: + placeholder: + subtype: Untertyp auswählen + type: Typ auswählen select_accounts: accounts_selected: Konten ausgewählt api_error: "API-Fehler: %{message}" @@ -84,6 +96,7 @@ de: fetch_failed: "Konten konnten nicht geladen werden" no_accounts_to_setup: "Keine Konten zum Einrichten" no_api_key: "Der Lunch‑Flow-API-Schlüssel ist nicht konfiguriert. Bitte prüfe deine Verbindungseinstellungen." + page_title: Lunch‑Flow-Konten einrichten account_types: skip: Dieses Konto überspringen depository: Giro- oder Sparkonto @@ -121,10 +134,23 @@ de: ira: Traditionelles IRA roth_ira: Roth IRA angel: Business Angel + "non-registered": Nicht registriertes Anlagekonto + fhsa: FHSA (Sparkonto für den ersten Immobilienkauf) + rdsp: RDSP (registrierter Sparplan für Menschen mit Behinderung) + dpsp: DPSP (aufgeschobener Gewinnbeteiligungsplan) + prpp: PRPP (gemeinschaftlicher registrierter Altersvorsorgeplan) + lif: LIF (lebenslanger Einkommensfonds) + lrif: LRIF (gebundener Altersvorsorge-Einkommensfonds) + prif: PRIF (vorgeschriebener registrierter Altersvorsorge-Einkommensfonds) + rlif: RLIF (beschränkter lebenslanger Einkommensfonds) + assurance_vie: Assurance Vie loan: mortgage: Hypothek student: Studienkredit auto: Autokredit + home_equity: Eigenheimkredit + line_of_credit: Kreditlinie + business: Unternehmenskredit other: Sonstiges Darlehen balance: Saldo cancel: Abbrechen @@ -143,6 +169,7 @@ de: success: one: "%{count} Konto erfolgreich angelegt." other: "%{count} Konten erfolgreich angelegt." + unexpected_error: Ein unerwarteter Fehler ist aufgetreten. Versuch es noch einmal. sync: success: Synchronisierung gestartet update: diff --git a/config/locales/views/lunchflow_items/en.yml b/config/locales/views/lunchflow_items/en.yml index 50ebe2ced..28fdbef33 100644 --- a/config/locales/views/lunchflow_items/en.yml +++ b/config/locales/views/lunchflow_items/en.yml @@ -62,8 +62,20 @@ en: status_never: Never synced status_with_summary: "Last synced %{timestamp} ago • %{summary}" syncing: Syncing... + sync_status: + no_accounts: No accounts found + synced: + one: "%{count} account synced" + other: "%{count} accounts synced" + partial_setup: + one: "%{linked} synced, %{unlinked} needs setup" + other: "%{linked} synced, %{unlinked} need setup" total: Total unlinked: Unlinked + subtype_select: + placeholder: + subtype: Select subtype + type: Select type select_accounts: accounts_selected: accounts selected api_error: "API error: %{message}" @@ -103,6 +115,7 @@ en: fetch_failed: "Failed to Fetch Accounts" no_accounts_to_setup: "No Accounts to Set Up" no_api_key: "Lunch Flow API key is not configured. Please check your connection settings." + page_title: Set Up Lunch Flow Accounts account_types: skip: Skip this account depository: Checking or Savings Account @@ -160,6 +173,7 @@ en: creation_failed: "Failed to create accounts: %{error}" no_accounts: "No accounts to set up." success: "Successfully created %{count} account(s)." + unexpected_error: An unexpected error occurred sync: success: Sync started update: diff --git a/test/controllers/lunchflow_items_controller_test.rb b/test/controllers/lunchflow_items_controller_test.rb index c8e03fb1b..1097630d9 100644 --- a/test/controllers/lunchflow_items_controller_test.rb +++ b/test/controllers/lunchflow_items_controller_test.rb @@ -5,6 +5,83 @@ class LunchflowItemsControllerTest < ActionDispatch::IntegrationTest sign_in @user = users(:family_admin) end + test "setup accounts renders German subtype options without English fallbacks" do + ensure_tailwind_build + @user.update!(locale: "de") + + get setup_accounts_lunchflow_item_url(lunchflow_items(:one)) + + assert_response :success + assert_select "title", text: "Lunch‑Flow-Konten einrichten" + assert_select "option", text: "Untertyp auswählen" + assert_select "option", text: "Typ auswählen" + + [ + "Nicht registriertes Anlagekonto", + "FHSA (Sparkonto für den ersten Immobilienkauf)", + "RDSP (registrierter Sparplan für Menschen mit Behinderung)", + "DPSP (aufgeschobener Gewinnbeteiligungsplan)", + "PRPP (gemeinschaftlicher registrierter Altersvorsorgeplan)", + "LIF (lebenslanger Einkommensfonds)", + "LRIF (gebundener Altersvorsorge-Einkommensfonds)", + "PRIF (vorgeschriebener registrierter Altersvorsorge-Einkommensfonds)", + "RLIF (beschränkter lebenslanger Einkommensfonds)", + "Assurance Vie", + "Eigenheimkredit", + "Kreditlinie", + "Unternehmenskredit" + ].each do |label| + assert_select "option", text: label + end + + assert_select "option", text: "UGMA-Treuhandkonto" + + refute_includes response.body, "Select subtype" + refute_includes response.body, "Select type" + refute_includes response.body, "First Home Savings Account" + refute_includes response.body, "UGMA Custodial Account" + end + + test "setup accounts preserves the English title and placeholders" do + ensure_tailwind_build + @user.update!(locale: "en") + + get setup_accounts_lunchflow_item_url(lunchflow_items(:one)) + + assert_response :success + assert_select "title", text: "Set Up Lunch Flow Accounts" + assert_select "option", text: "Select subtype" + assert_select "option", text: "Select type" + end + + test "account setup localizes an unexpected error in German" do + @user.update!(locale: "de") + lunchflow_account = lunchflow_accounts(:investment_account) + Account.stubs(:create_and_sync).raises(StandardError, "Synthetic failure") + + post complete_account_setup_lunchflow_item_url(lunchflow_items(:one)), params: { + account_types: { lunchflow_account.id => "Investment" }, + account_subtypes: { lunchflow_account.id => "brokerage" } + } + + assert_redirected_to accounts_path + assert_equal "Konten konnten nicht angelegt werden: Ein unerwarteter Fehler ist aufgetreten. Versuch es noch einmal.", flash[:alert] + end + + test "account setup preserves the unexpected error in English" do + @user.update!(locale: "en") + lunchflow_account = lunchflow_accounts(:investment_account) + Account.stubs(:create_and_sync).raises(StandardError, "Synthetic failure") + + post complete_account_setup_lunchflow_item_url(lunchflow_items(:one)), params: { + account_types: { lunchflow_account.id => "Investment" }, + account_subtypes: { lunchflow_account.id => "brokerage" } + } + + assert_redirected_to accounts_path + assert_equal "Failed to create accounts: An unexpected error occurred", flash[:alert] + end + test "invalid non-Turbo create redirects instead of rendering a missing template" do assert_no_difference "LunchflowItem.count" do post lunchflow_items_url, params: { diff --git a/test/models/lunchflow_item_test.rb b/test/models/lunchflow_item_test.rb index 13b06f2fd..b2a9adf54 100644 --- a/test/models/lunchflow_item_test.rb +++ b/test/models/lunchflow_item_test.rb @@ -28,4 +28,31 @@ class LunchflowItemTest < ActiveSupport::TestCase assert_equal LunchflowItem::DEFAULT_BASE_URL, @lunchflow_item.effective_base_url end + + test "sync status summary covers every German count branch" do + assert_sync_status(:de, total: 0, linked: 0, unlinked: 0, expected: "Keine Konten gefunden") + assert_sync_status(:de, total: 1, linked: 1, unlinked: 0, expected: "1 Konto synchronisiert") + assert_sync_status(:de, total: 2, linked: 2, unlinked: 0, expected: "2 Konten synchronisiert") + assert_sync_status(:de, total: 2, linked: 1, unlinked: 1, expected: "1 synchronisiert, 1 muss eingerichtet werden") + assert_sync_status(:de, total: 3, linked: 1, unlinked: 2, expected: "1 synchronisiert, 2 müssen eingerichtet werden") + end + + test "sync status summary covers every English count branch" do + assert_sync_status(:en, total: 0, linked: 0, unlinked: 0, expected: "No accounts found") + assert_sync_status(:en, total: 1, linked: 1, unlinked: 0, expected: "1 account synced") + assert_sync_status(:en, total: 2, linked: 2, unlinked: 0, expected: "2 accounts synced") + assert_sync_status(:en, total: 2, linked: 1, unlinked: 1, expected: "1 synced, 1 needs setup") + assert_sync_status(:en, total: 3, linked: 1, unlinked: 2, expected: "1 synced, 2 need setup") + end + + private + def assert_sync_status(locale, total:, linked:, unlinked:, expected:) + @lunchflow_item.stubs(:total_accounts_count).returns(total) + @lunchflow_item.stubs(:linked_accounts_count).returns(linked) + @lunchflow_item.stubs(:unlinked_accounts_count).returns(unlinked) + + I18n.with_locale(locale) do + assert_equal expected, @lunchflow_item.sync_status_summary + end + end end