diff --git a/config/locales/views/retirement/en.yml b/config/locales/views/retirement/en.yml
index eb7b8c09c..d10f5263a 100644
--- a/config/locales/views/retirement/en.yml
+++ b/config/locales/views/retirement/en.yml
@@ -4,7 +4,7 @@ en:
show:
title: Retirement
subtitle: Plan when you can stop working — and what you'll need to get there.
- preview_note: Preview — data entry only for now. Projections and the plan dashboard arrive in a later update.
+ preview_note: Preview — projections are a single deterministic path in today's money. The polished dashboard arrives in a later update.
sources_title: Pension sources
add_source: Add source
no_sources: No pension sources yet.
@@ -24,6 +24,30 @@ en:
delete: Delete
edit: Edit
delete_confirm: Are you sure? This cannot be undone.
+ update:
+ updated: Plan updated.
+ kpis:
+ set_birth_year_heading: Add your birth year to see projections
+ set_birth_year_body: Set your birth year (and tweak the levers below) to project your freedom date, Coast FIRE point, and how long your money lasts.
+ freedom_date: Freedom date
+ retire_at_age: Age %{age}
+ coast_fire: Coast FIRE
+ coast_hint: Stop saving and still retire on time.
+ not_yet: Not yet
+ money_lasts_to: Money lasts to
+ age: "age %{age}"
+ past_age: "past %{age}"
+ terminal: "~%{amount} left at the end"
+ what_if:
+ title: What-if
+ hint: Change a lever — the projection updates live.
+ save: Save plan
+ fields:
+ birth_year: Birth year
+ retire_age: Retire at age
+ target_spend: Target spend / mo
+ monthly_savings: Save / mo
+ real_return_pct: Real return %
pension_sources:
new_title: Add pension source
edit_title: Edit pension source
diff --git a/config/routes.rb b/config/routes.rb
index e115dff1f..300cf0e1f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -317,7 +317,8 @@ Rails.application.routes.draw do
end
end
- resource :retirement, only: %i[show], controller: "retirement" do
+ resource :retirement, only: %i[show update], controller: "retirement" do
+ patch :forecast, on: :member
scope module: :retirement do
resources :pension_sources, only: %i[new create edit update destroy]
resources :statements, only: %i[new create destroy]
diff --git a/test/controllers/retirement_controller_test.rb b/test/controllers/retirement_controller_test.rb
index 0f2f0e449..22add1116 100644
--- a/test/controllers/retirement_controller_test.rb
+++ b/test/controllers/retirement_controller_test.rb
@@ -55,4 +55,41 @@ class RetirementControllerTest < ActionDispatch::IntegrationTest
assert_select "a[href=?]", retirement_path, count: 0
end
+
+ test "show renders the KPI section" do
+ get retirement_url
+ assert_response :success
+ assert_select "#retirement_kpis"
+ end
+
+ test "show uses real translations, not humanized i18n keys" do
+ get retirement_url
+ assert_response :success
+ assert_match I18n.t("retirement.show.sources_title"), response.body
+ assert_no_match(/Sources Title/, response.body)
+ end
+
+ test "update persists retirement params" do
+ patch retirement_url, params: { retirement: {
+ birth_year: 1985, retire_age: 62, monthly_savings: 1500, target_spend: 2800, real_return_pct: 5
+ } }
+
+ assert_redirected_to retirement_path
+ plan = Goal::Retirement.for_owner(@user)
+ assert_equal "1985", plan.birth_year.to_s
+ assert_equal "62", plan.retire_age.to_s
+ end
+
+ test "forecast streams KPIs without persisting" do
+ Goal::Retirement.for_owner(@user).update!(retirement_params: { "birth_year" => 1980 })
+
+ patch forecast_retirement_url,
+ params: { retirement: { retire_age: 70 } },
+ headers: { "Accept" => "text/vnd.turbo-stream.html" }
+
+ assert_response :success
+ assert_match "retirement_kpis", response.body
+ # transient: the slider value is not written back to the plan
+ assert_nil Goal::Retirement.for_owner(@user).retire_age
+ end
end