Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
.vscode/*
!.vscode/extensions.json
!.vscode/*.code-snippets
*.code-workspace

# Ignore macOS specific files
*/.DS_Store
Expand All @@ -68,9 +69,7 @@ coverage

# Ignore node related files
node_modules

compose.yml

plaid_test_accounts/

# Added by Claude Task Master
Expand Down Expand Up @@ -107,4 +106,3 @@ scripts/
.windsurfrules
.cursor/rules/dev_workflow.mdc
.cursor/rules/taskmaster.mdc

9 changes: 8 additions & 1 deletion app/controllers/concerns/breadcrumbable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ module Breadcrumbable
private
# The default, unless specific controller or action explicitly overrides
def set_breadcrumbs
@breadcrumbs = [ [ "Home", root_path ], [ controller_name.titleize, nil ] ]
key = "shared.breadcrumbs.#{controller_name}"
label = I18n.t(key, default: controller_name.titleize)
home_label = I18n.t("shared.breadcrumbs.home", default: "Home")

@breadcrumbs = [
[ home_label, root_path ],
[ label, nil ]
]
end
end
2 changes: 1 addition & 1 deletion app/controllers/family_merchants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class FamilyMerchantsController < ApplicationController
before_action :set_merchant, only: %i[edit update destroy]

def index
@breadcrumbs = [ [ "Home", root_path ], [ "Merchants", nil ] ]
@breadcrumbs = [ [ "Home", root_path ], [ t("shared.breadcrumbs.merchants", default: "Merchants"), nil ] ]

# Show all merchants for this family
@family_merchants = Current.family.merchants.alphabetically
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/imports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def index
@exports = Current.user.admin? ? Current.family.family_exports.ordered.limit(10) : nil
@breadcrumbs = [
[ "Home", root_path ],
[ "Import/Export", imports_path ]
[ t("shared.breadcrumbs.import_export", default: "Import/Export"), imports_path ]
]
render layout: "settings"
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def index
# Build reports sections for collapsible/reorderable UI
@reports_sections = build_reports_sections

@breadcrumbs = [ [ "Home", root_path ], [ "Reports", nil ] ]
@breadcrumbs = [ [ "Home", root_path ], [ t("shared.breadcrumbs.reports", default: "Reports"), nil ] ]
end

def update_preferences
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/guides_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Settings::GuidesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Guides", nil ]
[ t("shared.breadcrumbs.guides", default: "Guides"), nil ]
]
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML,
autolink: true,
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/llm_usages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Settings::LlmUsagesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "LLM Usage", nil ]
[ t("shared.breadcrumbs.llm_usage", default: "LLM Usage"), nil ]
]
@family = Current.family

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def show
@pending_invitations = Current.family.invitations.pending
@breadcrumbs = [
[ "Home", root_path ],
[ "Profile Info", nil ]
[ t("shared.breadcrumbs.profile", default: "Profile Info"), nil ]
]
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Settings::ProvidersController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Bank Sync Providers", nil ]
[ t("shared.breadcrumbs.providers", default: "Bank Sync Providers"), nil ]
]

prepare_show_context
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/settings/securities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class Settings::SecuritiesController < ApplicationController
def show
@breadcrumbs = [
[ "Home", root_path ],
[ "Security", nil ]
[ t("shared.breadcrumbs.security", default: "Security"), nil ]
]
end
end
3 changes: 2 additions & 1 deletion app/helpers/languages_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@
"tr", # Turkish - 57 translation files
"nb", # Norwegian Bokmål - 56 translation files
"ca", # Catalan - 56 translation files
"ro", # Romanian - 61 translation files
"ro", # Romanian - 61 translation files
"it" # Italian - 61 translation files
"pt-BR" # Brazilian Portuguese - 60 translation files

Check failure on line 166 in app/helpers/languages_helper.rb

View workflow job for this annotation

GitHub Actions / ci / lint

Lint/Syntax: unexpected string literal; expected a `,` separator for the array elements (Using Ruby 3.4 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)
].freeze

COUNTRY_MAPPING = {
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/controllers/bulk_select_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default class extends Controller {

_updateSelectionBar() {
const count = this.selectedIdsValue.length;
this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()} selected`;
this.selectionBarTextTarget.innerText = `${count} ${this._pluralizedResourceName()}`;
this.selectionBarTarget.classList.toggle("hidden", count === 0);
this.selectionBarTarget.querySelector("input[type='checkbox']").checked =
count > 0;
Expand Down
24 changes: 13 additions & 11 deletions app/models/balance_sheet/classification_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def initialize(classification:, currency:, accounts:)
end

def name
classification.titleize.pluralize
I18n.t("pages.dashboard.balance_sheet.#{classification}",
default: classification.titleize.pluralize
)
end

def icon
Expand All @@ -31,16 +33,16 @@ def syncing?
# For now, we group by accountable type. This can be extended in the future to support arbitrary user groupings.
def account_groups
groups = accounts.group_by(&:accountable_type)
.transform_keys { |at| Accountable.from_type(at) }
.map do |accountable, account_rows|
BalanceSheet::AccountGroup.new(
name: I18n.t("accounts.types.#{accountable.name.underscore}", default: accountable.display_name),
color: accountable.color,
accountable_type: accountable,
accounts: account_rows,
classification_group: self
)
end
.transform_keys { |at| Accountable.from_type(at) }
.map do |accountable, account_rows|
BalanceSheet::AccountGroup.new(
name: I18n.t("accounts.types.#{accountable.name.underscore}", default: accountable.display_name, count: 2),
color: accountable.color,
accountable_type: accountable,
accounts: account_rows,
classification_group: self
)
end

# Sort the groups using the manual order defined by Accountable::TYPES so that
# the UI displays account groups in a predictable, domain-specific sequence.
Expand Down
81 changes: 23 additions & 58 deletions app/models/period.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,19 @@ class InvalidKeyError < StandardError; end

PERIODS = {
"last_day" => {
date_range: -> { [ 1.day.ago.to_date, Date.current ] },
label_short: "1D",
label: "Last Day",
comparison_label: "vs. yesterday"
date_range: -> { [ 1.day.ago.to_date, Date.current ] }
},
"current_week" => {
date_range: -> { [ Date.current.beginning_of_week, Date.current ] },
label_short: "WTD",
label: "Current Week",
comparison_label: "vs. start of week"
date_range: -> { [ Date.current.beginning_of_week, Date.current ] }
},
"last_7_days" => {
date_range: -> { [ 7.days.ago.to_date, Date.current ] },
label_short: "7D",
label: "Last 7 Days",
comparison_label: "vs. last week"
date_range: -> { [ 7.days.ago.to_date, Date.current ] }
},
"current_month" => {
date_range: -> { [ Date.current.beginning_of_month, Date.current ] },
label_short: "MTD",
label: "Current Month",
comparison_label: "vs. start of month"
date_range: -> { [ Date.current.beginning_of_month, Date.current ] }
},
"last_month" => {
date_range: -> { [ 1.month.ago.beginning_of_month.to_date, 1.month.ago.end_of_month.to_date ] }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate "last_month" key in PERIODS hash

The PERIODS hash contains two entries with the key "last_month". Ruby hashes don't support duplicate keys, so the second entry will silently overwrite the first. This appears to be an incomplete refactoring where a new entry was added (lines 24-27) but the old entry (lines 28-33 with inline labels) wasn't removed. The second duplicate still contains hardcoded labels that should have been removed as part of the i18n migration.

Fix in Cursor Fix in Web

},
"last_month" => {
date_range: -> { [ 1.month.ago.beginning_of_month.to_date, 1.month.ago.end_of_month.to_date ] },
Expand All @@ -47,53 +38,32 @@ class InvalidKeyError < StandardError; end
comparison_label: "vs. last 30 days"
},
"last_90_days" => {
date_range: -> { [ 90.days.ago.to_date, Date.current ] },
label_short: "90D",
label: "Last 90 Days",
comparison_label: "vs. last quarter"
date_range: -> { [ 90.days.ago.to_date, Date.current ] }
},
"current_year" => {
date_range: -> { [ Date.current.beginning_of_year, Date.current ] },
label_short: "YTD",
label: "Current Year",
comparison_label: "vs. start of year"
date_range: -> { [ Date.current.beginning_of_year, Date.current ] }
},
"last_365_days" => {
date_range: -> { [ 365.days.ago.to_date, Date.current ] },
label_short: "365D",
label: "Last 365 Days",
comparison_label: "vs. 1 year ago"
date_range: -> { [ 365.days.ago.to_date, Date.current ] }
},
"last_5_years" => {
date_range: -> { [ 5.years.ago.to_date, Date.current ] },
label_short: "5Y",
label: "Last 5 Years",
comparison_label: "vs. 5 years ago"
date_range: -> { [ 5.years.ago.to_date, Date.current ] }
},
"last_10_years" => {
date_range: -> { [ 10.years.ago.to_date, Date.current ] },
label_short: "10Y",
label: "Last 10 Years",
comparison_label: "vs. 10 years ago"
date_range: -> { [ 10.years.ago.to_date, Date.current ] }
},
"all_time" => {
date_range: -> {
oldest_date = Current.family&.oldest_entry_date
# If no family or no entries exist, use a reasonable historical fallback
# to ensure "All Time" represents a meaningful range, not just today
start_date = if oldest_date && oldest_date < Date.current
oldest_date
else
5.years.ago.to_date
end
[ start_date, Date.current ]
},
label_short: "All",
label: "All Time",
comparison_label: "vs. beginning"
}
}
}

class << self
def from_key(key)
unless PERIODS.key?(key)
Expand Down Expand Up @@ -156,28 +126,23 @@ def interval
end
end

def i18n_scope
"periods.#{key}"
end
Comment on lines +129 to +131
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against nil key for cleaner i18n lookups.

When key is nil (for custom periods), this method returns "periods.", which creates malformed i18n keys like "periods..label" (double dot). While the defaults in the label methods prevent this from breaking functionality, it's not clean and could cause confusion or warnings.

Consider adding a guard:

 def i18n_scope
-  "periods.#{key}"
+  key ? "periods.#{key}" : nil
 end

Then update the label methods to skip i18n lookup when i18n_scope is nil:

 def label
-  I18n.t("#{i18n_scope}.label", default: "Custom Period")
+  return "Custom Period" unless i18n_scope
+  I18n.t("#{i18n_scope}.label", default: "Custom Period")
 end

(Apply similar changes to label_short and comparison_label.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/models/period.rb around lines 120–122, i18n_scope currently builds
"periods.#{key}" which produces "periods." when key is nil; change i18n_scope to
return nil (or false) when key is nil/blank instead of "periods.", and update
label, label_short, and comparison_label to skip the I18n lookup when i18n_scope
is nil (use the fallback/default string directly) so no malformed keys like
"periods..label" are generated.


def label
if key_metadata
key_metadata.fetch(:label)
else
"Custom Period"
end
I18n.t("#{i18n_scope}.label", default: "Custom Period")
end

def label_short
if key_metadata
key_metadata.fetch(:label_short)
else
"Custom"
end
I18n.t("#{i18n_scope}.label_short", default: "Custom")
end

def comparison_label
if key_metadata
key_metadata.fetch(:comparison_label)
else
"#{start_date.strftime(@date_format)} to #{end_date.strftime(@date_format)}"
end
I18n.t(
"#{i18n_scope}.comparison_label",
default: "#{start_date.strftime(@date_format)} to #{end_date.strftime(@date_format)}"
)
end

private
Expand Down
6 changes: 5 additions & 1 deletion app/views/accounts/_accountable_group.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@
</div>

<div class="my-2">
<%
group_type = account_group.accountable_type.name.underscore
account_group_name = I18n.t("accounts.types.#{group_type}", count: 1)
%>
<%= render DS::Link.new(
href: new_polymorphic_path(account_group.key, step: "method_select"),
text: t("accounts.sidebar.new_account_group", account_group: account_group.name.downcase.singularize),
text: t("accounts.sidebar.new_account_group", account_group: account_group_name.downcase),
icon: "plus",
full_width: true,
variant: "ghost",
Expand Down
14 changes: 7 additions & 7 deletions app/views/budget_categories/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%= render DS::Dialog.new(variant: :drawer) do |dialog| %>
<% dialog.with_header do %>
<div>
<p class="text-sm text-secondary">Category</p>
<p class="text-sm text-secondary"><%= t('budget_categories.labels.category', default: "Category") %></p>
<h3 class="text-2xl font-medium text-primary">
<%= @budget_category.name %>
</h3>
Expand All @@ -26,7 +26,7 @@
<% end %>

<% dialog.with_body do %>
<% dialog.with_section(title: "Overview", open: true) do %>
<% dialog.with_section(title: t('budget_categories.labels.overview', default: "Overview"), open: true) do %>
<div class="pb-4">
<dl class="space-y-3 px-3 py-2">
<div class="flex items-center justify-between text-sm">
Expand Down Expand Up @@ -63,22 +63,22 @@
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Budgeted</dt>
<dt class="text-secondary"><%= t('budget_categories.labels.budgeted', default: "Budgeted") %></dt>
<dd class="text-primary font-medium">
<%= format_money @budget_category.budgeted_spending_money %>
</dd>
</div>
<% end %>

<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Monthly average spending</dt>
<dt class="text-secondary"><%= t('budget_categories.labels.monthly_avg_spending', default: "Monthly average spending") %></dt>
<dd class="text-primary font-medium">
<%= @budget_category.avg_monthly_expense_money.format %>
</dd>
</div>

<div class="flex items-center justify-between text-sm">
<dt class="text-secondary">Monthly median spending</dt>
<dt class="text-secondary"><%= t('budget_categories.labels.monthly_med_spending', default: "Monthly median spending") %></dt>
<dd class="text-primary font-medium">
<%= @budget_category.median_monthly_expense_money.format %>
</dd>
Expand All @@ -87,7 +87,7 @@
</div>
<% end %>

<% dialog.with_section(title: "Recent Transactions", open: true) do %>
<% dialog.with_section(title: t('budget_categories.labels.recent_transactions', default: "Recent Transactions"), open: true) do %>
<div class="space-y-2">
<div class="px-3 py-4 space-y-2">
<% if @recent_transactions.any? %>
Expand Down Expand Up @@ -120,7 +120,7 @@
</ul>

<%= render DS::Link.new(
text: "View all category transactions",
text: t('budget_categories.labels.view_all', default: "View all category transactions"),
variant: "outline",
full_width: true,
href: transactions_path(q: {
Expand Down
10 changes: 5 additions & 5 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<% mobile_nav_items = [
{ name: "Home", path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: "Transactions", path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: "Reports", path: reports_path, icon: "chart-bar", icon_custom: false, active: page_active?(reports_path) },
{ name: "Budgets", path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
{ name: "Assistant", path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
{ name: t("navigation.home"), path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: t("navigation.transactions"), path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: t("navigation.reports"), path: reports_path, icon: "chart-bar", icon_custom: false, active: page_active?(reports_path) },
{ name: t("navigation.budgets"), path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
{ name: t("navigation.assistant"), path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
] %>

<% desktop_nav_items = mobile_nav_items.reject { |item| item[:mobile_only] } %>
Expand Down
Loading
Loading