From 4b7c01e6327be1b972af79fbc2ba4799bcac809a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= Date: Wed, 17 Jun 2026 22:32:49 +0100 Subject: [PATCH] =?UTF-8?q?feat(finance):=20i18n=20=E2=80=94=20TOML-based?= =?UTF-8?q?=20translations=20for=20all=20personal=20finance=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a full translation layer (English + European Portuguese) using BurntSushi/toml with go:embed. Locale detection reads the lang cookie, falls back to Accept-Language, then defaults to "en". A language switcher in the nav writes the cookie and redirects back. All 20 personal finance templates now use {{.T.Get "key"}} for every UI string. Co-Authored-By: Claude Sonnet 4.6 --- apps/finance/services/api/main/handler.go | 41 +- .../services/api/main/handler_property.go | 1 + apps/finance/services/api/main/i18n.go | 120 ++ .../finance/services/api/main/locales/en.toml | 1016 +++++++++++++++++ .../finance/services/api/main/locales/pt.toml | 1016 +++++++++++++++++ apps/finance/services/api/main/models.go | 13 + .../services/api/main/models_property.go | 1 + .../services/api/main/templates/accounts.html | 31 +- .../api/main/templates/auto_import.html | 38 +- .../services/api/main/templates/base.html | 82 +- .../api/main/templates/categories.html | 35 +- .../api/main/templates/dashboard.html | 78 +- .../services/api/main/templates/goals.html | 220 ++-- .../api/main/templates/household.html | 45 +- .../services/api/main/templates/import.html | 52 +- .../services/api/main/templates/networth.html | 44 +- .../services/api/main/templates/people.html | 65 +- .../services/api/main/templates/plan.html | 153 ++- .../api/main/templates/portfolio.html | 44 +- .../api/main/templates/projections.html | 26 +- .../services/api/main/templates/property.html | 192 ++-- .../services/api/main/templates/reports.html | 10 +- .../services/api/main/templates/settings.html | 57 +- .../services/api/main/templates/sharing.html | 27 +- .../api/main/templates/simulator.html | 89 +- .../services/api/main/templates/tax.html | 50 +- .../api/main/templates/transactions.html | 80 +- go.mod | 1 + go.sum | 2 + 29 files changed, 2917 insertions(+), 712 deletions(-) create mode 100644 apps/finance/services/api/main/i18n.go create mode 100644 apps/finance/services/api/main/locales/en.toml create mode 100644 apps/finance/services/api/main/locales/pt.toml diff --git a/apps/finance/services/api/main/handler.go b/apps/finance/services/api/main/handler.go index d10217f..11a4330 100644 --- a/apps/finance/services/api/main/handler.go +++ b/apps/finance/services/api/main/handler.go @@ -377,6 +377,11 @@ func NewHandler(store *Store, secret, googleID, googleSecret, baseURL string) *H } } +// t returns a Translator for the current request's preferred language. +func (h *Handler) t(r *http.Request) *Translator { + return newT(detectLang(r)) +} + // securityHeaders adds defence-in-depth HTTP headers to every response. func (h *Handler) securityHeaders(next http.Handler) http.Handler { csp := strings.Join([]string{ @@ -435,6 +440,7 @@ func (h *Handler) ownerOrViewerMW(next http.HandlerFunc) http.HandlerFunc { } } render(w, baseTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Access Denied", @@ -463,7 +469,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { txns, err := h.store.getTransactions(ctx, a.UserID, bson.M{}) if err != nil { slog.Error("get transactions", "err", err) - render(w, dashboardTmpl, &DashboardData{UserID: a.UserID, Email: a.Email, Title: "Dashboard", Route: "dashboard", IsOwner: true}) + render(w, dashboardTmpl, &DashboardData{T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Dashboard", Route: "dashboard", IsOwner: true}) return } @@ -819,6 +825,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { } render(w, dashboardTmpl, &DashboardData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Dashboard", @@ -906,6 +913,7 @@ func (h *Handler) Transactions(w http.ResponseWriter, r *http.Request) { } render(w, txnsTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Transactions", @@ -970,6 +978,7 @@ func (h *Handler) ImportPage(w http.ResponseWriter, r *http.Request) { a := h.getAuth(r) accounts, _ := h.store.getAccounts(r.Context(), a.UserID) render(w, importTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Import", @@ -1024,6 +1033,7 @@ func (h *Handler) ImportPreview(w http.ResponseWriter, r *http.Request) { if err != nil { accounts, _ := h.store.getAccounts(ctx, a.UserID) render(w, importTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Import", @@ -1086,6 +1096,7 @@ func (h *Handler) ImportPreview(w http.ResponseWriter, r *http.Request) { } render(w, importTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Import", @@ -1302,6 +1313,7 @@ func (h *Handler) Accounts(w http.ResponseWriter, r *http.Request) { slog.Error("get accounts", "err", err) } render(w, accountsTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Accounts", @@ -1351,6 +1363,7 @@ func (h *Handler) Categories(w http.ResponseWriter, r *http.Request) { slog.Error("get categories", "err", err) } render(w, categoriesTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Categories", @@ -1459,6 +1472,7 @@ func (h *Handler) Reports(w http.ResponseWriter, r *http.Request) { } render(w, reportsTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Reports", @@ -1559,6 +1573,7 @@ func (h *Handler) Projections(w http.ResponseWriter, r *http.Request) { }) render(w, projectionsTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Projections", @@ -1584,6 +1599,7 @@ func (h *Handler) Portfolio(w http.ResponseWriter, r *http.Request) { isins := uniqueISINs(trades) if len(isins) == 0 { render(w, portfolioTmpl, &PortfolioData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Portfolio", @@ -1616,6 +1632,7 @@ func (h *Handler) Portfolio(w http.ResponseWriter, r *http.Request) { pr := aggregatePortfolio(holdings) render(w, portfolioTmpl, &PortfolioData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Portfolio", @@ -1733,6 +1750,7 @@ func (h *Handler) Sharing(w http.ResponseWriter, r *http.Request) { } render(w, sharingTmpl, map[string]interface{}{ + "T": h.t(r), "UserID": a.UserID, "Email": a.Email, "Title": "Sharing", @@ -1986,6 +2004,7 @@ func (h *Handler) Goals(w http.ResponseWriter, r *http.Request) { } data := &GoalsData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Goals", @@ -2182,6 +2201,7 @@ func (h *Handler) Simulator(w http.ResponseWriter, r *http.Request) { } render(w, simulatorTmpl, &SimulatorData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "What If", @@ -2310,6 +2330,7 @@ func (h *Handler) NetWorth(w http.ResponseWriter, r *http.Request) { } render(w, networthTmpl, &NetWorthData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Net Worth", @@ -2491,6 +2512,7 @@ func (h *Handler) Tax(w http.ResponseWriter, r *http.Request) { } render(w, taxTmpl, &TaxData{ + T: h.t(r), UserID: auth.UserID, Email: auth.Email, Title: "Tax Summary", @@ -2549,6 +2571,7 @@ func (h *Handler) Household(w http.ResponseWriter, r *http.Request) { now := time.Now() data := &HouseholdData{ + T: h.t(r), UserID: auth.UserID, Email: auth.Email, Title: "Household", @@ -2644,6 +2667,7 @@ func (h *Handler) AutoImport(w http.ResponseWriter, r *http.Request) { auth := h.getAuth(r) accounts, _ := h.store.getAccounts(r.Context(), auth.UserID) render(w, autoImportTmpl, &AutoImportData{ + T: h.t(r), UserID: auth.UserID, Email: auth.Email, Title: "Import Guide", @@ -2715,6 +2739,7 @@ func (h *Handler) People(w http.ResponseWriter, r *http.Request) { } data := &PeopleData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "People", @@ -2795,6 +2820,7 @@ func (h *Handler) Settings(w http.ResponseWriter, r *http.Request) { categories, _ := h.store.getCategories(ctx, a.UserID) render(w, settingsTmpl, &SettingsData{ + T: h.t(r), UserID: a.UserID, Email: a.Email, Title: "Settings", @@ -2805,6 +2831,18 @@ func (h *Handler) Settings(w http.ResponseWriter, r *http.Request) { }) } +func (h *Handler) SetLang(w http.ResponseWriter, r *http.Request) { + lang := r.FormValue("lang") + if supportedLangs[lang] { + setLangCookie(w, lang, h.isSecure()) + } + back := r.Header.Get("Referer") + if back == "" { + back = "/dashboard" + } + http.Redirect(w, r, back, http.StatusSeeOther) +} + func (h *Handler) RegisterRoutes(mux *http.ServeMux) { // Auth (no authMW — these are public by definition) mux.HandleFunc("GET /auth/login", h.AuthLogin) @@ -2864,6 +2902,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /property", h.Properties) mux.HandleFunc("POST /property", h.Properties) mux.HandleFunc("GET /plan", h.Dream) + mux.HandleFunc("POST /lang", h.SetLang) h.RegisterOrgRoutes(mux) } diff --git a/apps/finance/services/api/main/handler_property.go b/apps/finance/services/api/main/handler_property.go index 7e9963e..f9156e0 100644 --- a/apps/finance/services/api/main/handler_property.go +++ b/apps/finance/services/api/main/handler_property.go @@ -184,6 +184,7 @@ func (h *Handler) propertiesGET(w http.ResponseWriter, r *http.Request, auth aut } render(w, propertyTmpl, PropertyData{ + T: h.t(r), UserID: auth.UserID, Email: auth.Email, Title: "Property", diff --git a/apps/finance/services/api/main/i18n.go b/apps/finance/services/api/main/i18n.go new file mode 100644 index 0000000..bf717f6 --- /dev/null +++ b/apps/finance/services/api/main/i18n.go @@ -0,0 +1,120 @@ +package main + +import ( + "embed" + "log/slog" + "net/http" + "strings" + + "github.com/BurntSushi/toml" +) + +//go:embed locales/*.toml +var localeFS embed.FS + +const defaultLang = "en" + +var supportedLangs = map[string]bool{"en": true, "pt": true} + +// catalogue holds the flattened key→string map for one language. +type catalogue map[string]string + +var catalogues = map[string]catalogue{} + +func init() { + for lang := range supportedLangs { + cat, err := loadCatalogue(lang) + if err != nil { + slog.Error("i18n: failed to load locale", "lang", lang, "err", err) + continue + } + catalogues[lang] = cat + } +} + +func loadCatalogue(lang string) (catalogue, error) { + data, err := localeFS.ReadFile("locales/" + lang + ".toml") + if err != nil { + return nil, err + } + var raw map[string]any + if err := toml.Unmarshal(data, &raw); err != nil { + return nil, err + } + cat := make(catalogue) + flattenTOML("", raw, cat) + return cat, nil +} + +// flattenTOML recursively flattens nested TOML tables into dot-notation keys. +func flattenTOML(prefix string, node map[string]any, out catalogue) { + for k, v := range node { + key := k + if prefix != "" { + key = prefix + "." + k + } + switch val := v.(type) { + case string: + out[key] = val + case map[string]any: + flattenTOML(key, val, out) + } + } +} + +// Translator wraps a locale lookup and exposes a Get method callable from +// Go templates as {{.T.Get "key"}}. +type Translator struct { + cat catalogue + en catalogue +} + +func (tr *Translator) Get(key string) string { + if s, ok := tr.cat[key]; ok { + return s + } + if s, ok := tr.en[key]; ok { + return s + } + return key +} + +// newT returns a Translator for the given language. +func newT(lang string) *Translator { + cat, ok := catalogues[lang] + if !ok { + cat = catalogues[defaultLang] + } + return &Translator{cat: cat, en: catalogues[defaultLang]} +} + +// detectLang reads the lang from a cookie, falling back to Accept-Language, +// then to the default. Only returns a supported language code. +func detectLang(r *http.Request) string { + if c, err := r.Cookie("lang"); err == nil { + if supportedLangs[c.Value] { + return c.Value + } + } + for _, part := range strings.Split(r.Header.Get("Accept-Language"), ",") { + tag := strings.TrimSpace(strings.SplitN(part, ";", 2)[0]) + code := strings.ToLower(strings.SplitN(tag, "-", 2)[0]) + if supportedLangs[code] { + return code + } + } + return defaultLang +} + +// setLangCookie writes the lang preference cookie. +func setLangCookie(w http.ResponseWriter, lang string, secure bool) { + http.SetCookie(w, &http.Cookie{ + Name: "lang", + Value: lang, + Path: "/", + HttpOnly: false, // JS may read it for future enhancement + Secure: secure, + MaxAge: 365 * 24 * 3600, + SameSite: http.SameSiteStrictMode, + }) +} diff --git a/apps/finance/services/api/main/locales/en.toml b/apps/finance/services/api/main/locales/en.toml new file mode 100644 index 0000000..ed40023 --- /dev/null +++ b/apps/finance/services/api/main/locales/en.toml @@ -0,0 +1,1016 @@ +# ── Navigation ────────────────────────────────────────────────────────────── + +[nav] +brand = "Personal" +dashboard = "Dashboard" +transactions = "Transactions" +portfolio = "Portfolio" +goals = "Goals" +property = "Property" +people = "People" +analysis = "Analysis" +settings = "Settings" +business = "🏢 Business" +hub_back = "← Hub" + +[nav.analysis] +reports = "Reports" +projections = "Projections" +tax = "Tax" +networth = "Net Worth" +simulator = "What If" + +[nav.settings] +accounts_categories = "Accounts & Categories" +import_csv = "Import CSV" +import_guide = "Import Guide" + +[nav.drawer] +analysis_label = "Analysis" +settings_label = "Settings" + +[nav.theme] +toggle_title = "Toggle dark/light mode" +menu_aria = "Menu" + +# ── Homepage ───────────────────────────────────────────────────────────────── + +[homepage] +title = "Finance Hub — Personal & Business Finance" +brand = "Finance Hub" +footer_tagline = "Self-hosted homelab" + +[homepage.nav] +signin = "Sign in" +personal = "Personal" +business = "Business" + +[homepage.hero] +eyebrow = "Self-hosted · Private · Open" +title_line1 = "Personal wealth." +title_line2 = "Business finances." +subtitle = "One self-hosted platform to track your personal wealth and manage your organisation's budget — without handing your data to anyone." +cta_get_started = "Get started" +cta_see_inside = "See what's inside" +cta_open_personal = "Open personal" +cta_open_business = "Open business" + +[homepage.trust] +data_privacy = "Your data, your server" +multi_bank = "Multi-bank CSV import" +dark_light = "Dark & light mode" +role_access = "Role-based access" +analytics = "Real-time analytics" + +[homepage.products.personal] +badge = "Personal" +name = "My Finance" +desc = "Full visibility into your personal wealth — spending, investments, goals, and net worth in one place." +feature_1 = "Spending dashboard & monthly trends" +feature_2 = "Transactions with category budgets" +feature_3 = "Investment portfolio tracking" +feature_4 = "Financial goals with progress" +feature_5 = "Net worth & what-if projections" +feature_6 = "Tax report export" +feature_7 = "Household sharing & multi-user view" +cta_open = "Open app" +cta_signin = "Sign in" + +[homepage.products.business] +badge = "Business" +name = "Organisations" +desc = "Manage organisations, plan fiscal years, control event budgets, and keep teams financially aligned." +feature_1 = "Multi-org with fiscal year lifecycle" +feature_2 = "Teams with emoji avatars & roles" +feature_3 = "Events with budget lines & goals" +feature_4 = "Purchase requests & approval flow" +feature_5 = "Ledger & bank CSV import" +feature_6 = "Variance analysis per event & team" +feature_7 = "End-of-year narrative report" +cta_open = "Open orgs" +cta_signin = "Sign in" + +[homepage.features.personal] +section_label = "Personal Finance" +section_title = "Complete visibility over your wealth" +section_sub = "From daily coffee to long-term portfolio — everything in one dashboard, updated as you import." +chip1_title = "Smart dashboard" +chip1_desc = "Monthly income vs spending, top categories, recent transactions, and budget health at a glance." +chip2_title = "Portfolio tracking" +chip2_desc = "Import trades, track ISIN-mapped tickers, see P&L and allocation by asset class." +chip3_title = "Goals & projections" +chip3_desc = "Set savings goals, model scenarios, and see your projected net worth over time." + +[homepage.features.business] +section_label = "Business Finance" +section_title = "Budget control for your org" +section_sub = "From fiscal year kick-off to the final report — with full traceability for every euro spent." +chip1_title = "Fiscal year lifecycle" +chip1_desc = "Plan events and budgets per year. Activate, lock, and close with an end-of-year report." +chip2_title = "Request & approval flow" +chip2_desc = "Teams submit purchase requests. Finance approves, tracks delivery, and settles against the ledger." +chip3_title = "Variance analysis" +chip3_desc = "Compare planned vs actual per event and per team. Identify overspend before the year closes." + +[homepage.signin_block] +title = "Ready to get started?" +body = "Finance Hub is self-hosted on your homelab. Sign in with your account to access your personal and business dashboards." +cta = "Sign in to Finance Hub" + +[homepage.mock] +net_income_label = "Net income" +net_income_value = "+€2,840" +net_income_sub = "vs last month +12%" +net_worth_label = "Net worth" +net_worth_sub = "↑ €1,240 this month" +budget_health = "Budget health" +groceries = "Groceries" +dining_out = "Dining out" +transport = "Transport" +entertainment = "Entertainment" +events_label = "Events — Summer Festival 2025" +budget_label = "Budget" +active_year = "Active year" +requests_label = "Requests" +sound_equipment = "Sound equipment rental" +catering_deposit = "Catering deposit" +stage_lighting = "Stage lighting" +approved = "Approved" +pending = "Pending" +in_transit = "In transit" + +# ── Auth ───────────────────────────────────────────────────────────────────── + +[auth.login] +page_title = "Sign in — Finance Hub" +brand = "Finance Hub" +heading = "Welcome back" +subtext = "Sign in to your account. No account?" +subtext_link = "Create one →" +field_email = "Email" +placeholder_email = "you@example.com" +field_password = "Password" +placeholder_password = "••••••••" +btn_submit = "Sign in →" +divider = "or" +btn_google = "Continue with Google" +footer_back = "← Back to home" +error_oauth = "Google sign-in failed. Please try again." + +[auth.register] +page_title = "Create account — Finance Hub" +brand = "Finance Hub" +heading = "Create your account" +subtext = "Already have an account?" +subtext_link = "Sign in →" +field_name = "Name" +name_optional = "(optional)" +placeholder_name = "Your name" +field_email = "Email" +placeholder_email = "you@example.com" +field_password = "Password" +placeholder_password = "••••••••" +hint_password = "At least 8 characters" +field_confirm = "Confirm password" +placeholder_confirm = "••••••••" +btn_submit = "Create account →" +divider_oauth = "or sign up with email" +btn_google = "Continue with Google" +footer_back = "← Back to home" + +# ── Dashboard ──────────────────────────────────────────────────────────────── + +[dashboard] +title = "Dashboard" +available_to_spend = "Available to spend this month" +available_formula = "income − fixed costs − spent so far" +disposable_label = "disposable" +month_progress = "Month progress: {{pct}}%" +month_spent = "Spent: {{pct}}%" + +[dashboard.cards] +bank_should_be = "Bank balance should be" +bank_should_be_sub = "upcoming fixed + safety buffer" +savings_rate = "Savings rate" +net_worth = "Net worth" +net_worth_link = "→ full breakdown" +portfolio_today = "Portfolio today" +portfolio_cost_basis = "cost basis · prices unavailable" +portfolio_no_trades = "No trades yet" +portfolio_import_link = "Import trades →" + +[dashboard.bank_math] +section_title = "What should be in your bank" +section_subtitle = "right now" +safety_buffer = "Safety buffer (2 weeks)" +minimum_recommended = "Minimum recommended" +no_recurring_msg = "No recurring expenses detected yet." +no_recurring_sub = "Import a few months of transactions." + +[dashboard.stocks] +section_title = "Stocks at a glance" +portfolio_link = "→ portfolio" +shares_label = "shares" +cost_basis = "cost basis" +total_label = "Total" +total_invested = "Total invested" +no_holdings_msg = "No holdings yet." +import_link = "Import trades →" + +[dashboard.budget_health] +section_title = "Budget health" +categories_link = "→ categories" + +[dashboard.recent] +section_title = "Recent activity" +all_txns_link = "→ all transactions" +no_txns_msg = "No transactions yet." +import_link = "Import some!" + +[dashboard.goals] +section_title = "Committed goals" +all_goals_link = "→ all goals" +months_left = "mo left" +per_month_needed = "/mo needed" + +[dashboard.fixed_costs] +section_title = "Fixed costs" +auto_detected = "auto-detected · 3-month average" +committed_goal = "committed goal" +recurring_expense = "recurring expense" +per_month = "/ month" +total_committed = "Total committed" + +[dashboard.alerts] +vs_last_month_up = "↑ vs last month" +vs_last_month_down = "↓ vs last month" + +# ── Transactions ───────────────────────────────────────────────────────────── + +[transactions] +title = "Transactions" +btn_add = "+ Add Transaction" +warning_all_dupes = "Every row in that file was already imported — nothing was added." + +[transactions.filter] +label_category = "Category" +option_all_cats = "All Categories" +label_period = "Period" +option_all_time = "All time" +option_30_days = "30 days" +option_90_days = "90 days" +option_1_year = "1 year" +label_search = "Search" +placeholder_search = "Description…" +btn_filter = "Filter" +btn_clear = "Clear" + +[transactions.table] +col_date = "Date" +col_description = "Description" +col_account = "Account" +col_category = "Category" +col_amount = "Amount" +btn_edit_category = "Edit category" +btn_delete = "Delete" +empty_msg = "No transactions found." +empty_import_link = "Import some" +empty_add_btn = "add manually" + +[transactions.modal_add] +title = "Add Transaction" +label_date = "Date" +label_description = "Description" +placeholder_desc = "e.g. Coffee at Starbucks" +label_amount = "Amount (€)" +option_expense = "− Expense" +option_income = "+ Income" +placeholder_amount = "0.00" +label_category = "Category" +label_account = "Account" +option_no_account = "— none —" +btn_cancel = "Cancel" +btn_save = "Save Transaction" +error_required = "Please fill in date, description, and a positive amount." +error_save_failed = "Failed to save." + +[transactions.confirm] +delete_msg = "Delete this transaction?" + +# ── Portfolio ──────────────────────────────────────────────────────────────── + +[portfolio] +title = "Portfolio" +missing_prices_warn = "Live price unavailable for {{n}} holding{{s}} — add a Yahoo Finance ticker to fix this" +lookup_link = "Look up ↗" +btn_save_ticker = "Save" +ticker_placeholder = "e.g. QDVE.DE" + +[portfolio.cards] +total_value = "Total Value" +total_cost = "Total Cost" +unrealized_pl = "Unrealized P&L" + +[portfolio.allocation] +section_title = "Allocation" + +[portfolio.holdings] +section_title = "Holdings" +col_asset = "Asset" +col_shares = "Shares" +col_avg_cost = "Avg Cost" +col_price = "Price" +col_value = "Value" +col_pl = "P&L" +add_trades_via = "Add trades via" +btn_import = "Import CSV" + +[portfolio.empty] +title = "No trades yet" +desc = "Import your Trade Republic securities CSV to see your portfolio." +btn_import = "Import Trades" + +# ── Goals ──────────────────────────────────────────────────────────────────── + +[goals] +title = "Goals" +tab_committed = "Committed goals" +tab_planner = "Goal Planner" + +[goals.summary_cards] +avg_monthly_savings = "Avg monthly savings" +last_3_months = "last 3 months" +disposable_income = "Disposable income" +before_goals = "before goals" +reserved_for_goals = "Reserved for goals" +per_month = "per month" +free_to_spend = "Free to spend" +after_goals = "after goals" + +[goals.goal_card] +type_once = "One-off purchase" +type_deposit = "Deposit / down-payment" +type_emergency = "Emergency fund" +type_recurring = "Recurring investment" +saved_of = "saved of" +need_per_month = "Need per month" +months_left = "Months left" +at_current_rate = "At current rate" +disposable_after = "Disposable after" +on_track = "✓ On track — your current savings rate covers €{{monthly}}/month with {{months}} months to go." +behind_warn = "⚠ At your current rate (€{{rate}}/mo) you'd reach this in {{actual}} months — {{diff}} months late." +btn_adjust_deadline = "Adjust deadline →" +btn_committed = "✓ Committed — click to uncommit" +btn_commit = "Commit to this goal" +btn_remove = "Remove" +confirm_remove = "Remove this goal?" + +[goals.empty] +title = "No goals yet" +desc = "Use the Goal Planner tab to simulate a goal and save it here." +btn_open_planner = "Open Goal Planner →" + +[goals.planner] +what_kind = "What kind of goal?" +purchase_title = "Save for a purchase" +purchase_desc = "Car, trip, gadget, fund — save up to a target by a date." +transition_title = "Sell & upgrade" +transition_desc = "Own an asset with a loan, acquire something new, sell the old to fund it." + +[goals.planner.purchase] +form_title = "Your purchase goal" +label_name = "Goal name" +placeholder_name = "e.g. New car, Europe trip, Emergency fund…" +label_target = "Target amount (€)" +placeholder_target = "e.g. 12000" +label_monthly_savings = "Monthly savings (€)" +placeholder_monthly = "e.g. 400" +label_deadline = "Target date (optional — leave blank to see when you'll get there)" +btn_calculate = "Calculate →" + +[goals.planner.purchase_result] +card_at_rate = "At your savings rate" +card_monthly_needed = "Monthly needed" +card_target = "Target" +your_goal_amount = "your goal amount" +to_hit_deadline = "to hit {{date}}" +set_target_date = "set a target date above" +enter_monthly = "enter monthly savings" +reach_label = "reach {{date}}" +on_track = "✓ On track — €{{monthly}}/mo covers the required €{{needed}}/mo." +behind_warn = "⚠ You need €{{needed}}/mo to hit the deadline but you're saving €{{monthly}}/mo. At your current rate you'll get there in {{months}} months ({{date}})." +save_as_goal_title = "Save as a goal" +save_as_goal_desc = "Adds this to your Goals tab so you can commit to it." +label_goal_name = "Goal name" +placeholder_goal_name = "e.g. New car" +btn_save_goal = "Save goal →" + +[goals.planner.transition] +form_title = "Your transition scenario" +label_current_asset = "Current asset (optional)" +option_none_asset = "— none selected —" +label_current_loan = "Current loan (optional)" +option_none_loan = "— none selected —" +label_dream_cost = "New goal cost (€)" +placeholder_dream_cost = "e.g. 350000" +label_down_pct = "Down payment (%)" +label_loan_rate = "New loan rate (% annual)" +label_loan_term = "New loan term (years)" +label_build_months = "Acquisition / build period (months)" +label_monthly_savings = "Monthly savings available (€)" +placeholder_savings = "e.g. 800" +label_sale_price = "Expected sale price of current asset (€)" +placeholder_sale_price = "leave blank to use current value" +btn_run = "Run simulation →" + +[goals.planner.transition_result] +card_total_timeline = "Total timeline" +until_paid_off = "until goal is fully paid off" +card_final_monthly = "Final monthly cost" +after_selling = "after selling current asset" +card_total_interest = "Total interest" +across_both_loans = "across both loans" +card_free_by = "Free by" +fully_paid_off = "fully paid off" +roadmap_title = "Your roadmap" +phase1_title = "Save down payment" +phase1_target = "Target:" +phase1_already_have = "Already have:" +phase1_still_need = "Still need:" +phase1_saving = "Saving:" +phase1_ready = "Ready now!" +phase1_equity_covers = "equity covers down payment" +phase1_down_payment = "Down payment:" +phase1_your_equity = "Your equity:" +phase2_title = "Acquire / build" +phase2_new_loan = "New loan:" +phase2_existing_loan = "Existing loan:" +phase2_new_emi = "New EMI:" +phase2_total_burden = "Total burden:" +phase3_title = "Sell & transition" +phase3_one_time = "One-time event" +phase3_after_acquisition = "after acquisition completes" +phase3_sale_price = "Sale price:" +phase3_pay_off = "Pay off loan:" +phase3_net_proceeds = "Net proceeds:" +phase3_applied = "Applied to new loan" +phase4_title = "Goal achieved" +phase4_remaining_loan = "Remaining loan:" +phase4_monthly = "Monthly:" +phase4_fully_paid = "Fully paid!" +phase4_sale_cleared = "sale proceeds cleared the loan" +phase4_no_remaining = "No remaining loan!" +chart_title = "Monthly cost over time" +chart_subtitle = "what you pay each month" +save_as_goal_title = "Save as a goal" +save_as_goal_desc = "Adds this to your Goals tab so you can commit to it." +label_goal_name = "Goal name" +placeholder_goal_name = "e.g. New property, Upgrade car…" +btn_save_goal = "Save goal →" + +# ── Property ───────────────────────────────────────────────────────────────── + +[property] +summary_total_value = "Total property value" +summary_total_value_sub = "current estimated value" +summary_outstanding_loans = "Outstanding loans" +summary_outstanding_sub = "remaining balance" +summary_net_equity = "Net equity" +summary_net_equity_sub = "value − loans" + +[property.properties] +section_title = "Properties" +btn_add = "+ Add property" +stat_current_value = "Current value" +stat_equity = "Equity" +stat_purchase_price = "Purchase price" +stat_gain = "Gain" +equity_label = "Equity {{pct}}%" +loan_label = "Loan {{pct}}%" +loan_remaining = "Remaining" +loan_monthly_payment = "Monthly payment" +loan_payoff = "Payoff" +loan_rate = "Rate" +btn_edit = "Edit" +btn_remove = "Remove" + +[property.properties.empty] +title = "No properties yet" +desc = "Add your home, investment property, or land to track equity and loans in one place." +btn_add_first = "Add your first property" + +[property.loans] +section_title = "Loans" +btn_add = "+ Add loan" +stat_balance = "Balance" +stat_monthly = "Monthly" +stat_payoff = "Payoff" +stat_rate = "Rate" +paid_label = "Paid {{pct}}%" +interest_left = "interest left" +btn_mark_paid = "Mark paid off" +btn_remove = "Remove" +confirm_paid_off = "Mark as paid off?" + +[property.modal_add] +title_property = "Add property" +label_name = "Property name *" +placeholder_name = "e.g. Main House" +label_address = "Address" +placeholder_address = "Street, city" +label_purchase_price = "Purchase price (€) *" +placeholder_purchase_price = "220000" +label_current_value = "Current value (€)" +placeholder_current_value = "Same as purchase" +hint_current_value = "Leave blank to use purchase price" +label_purchase_date = "Purchase date" +label_appreciation = "Est. appreciation (%/year)" +placeholder_appreciation = "2.0" +label_status = "Status" +status_owned = "Owned" +status_building = "Under construction" +status_sold = "Sold" +label_notes = "Notes" +placeholder_notes = "Optional notes" +btn_cancel = "Cancel" +btn_add = "Add property" + +[property.modal_edit] +title_property = "Edit property" +label_name = "Property name *" +label_address = "Address" +label_purchase_price = "Purchase price (€)" +label_current_value = "Current value (€)" +label_purchase_date = "Purchase date" +label_appreciation = "Appreciation (%/year)" +label_status = "Status" +status_owned = "Owned" +status_building = "Under construction" +status_sold = "Sold" +label_notes = "Notes" +btn_cancel = "Cancel" +btn_save = "Save changes" + +[property.modal_loan] +title_loan = "Add loan" +label_name = "Loan name *" +placeholder_name = "e.g. Home mortgage" +label_type = "Type" +type_mortgage = "Mortgage" +type_construction = "Construction loan" +type_personal = "Personal loan" +label_linked_property = "Linked property" +option_none_property = "— none —" +label_principal = "Original principal (€) *" +placeholder_principal = "200000" +label_balance = "Current balance (€)" +placeholder_balance = "Same as principal" +hint_balance = "Leave blank if just starting" +label_interest_rate = "Interest rate (%/year) *" +placeholder_rate = "3.2" +label_term = "Term (months) *" +placeholder_term = "360" +hint_term = "e.g. 360 = 30 years" +label_monthly_payment = "Monthly payment (€)" +placeholder_monthly_payment = "Auto-computed" +hint_monthly_payment = "Leave blank to calculate" +label_start_date = "Start date" +label_notes = "Notes" +placeholder_notes = "Bank name, reference, etc." +btn_cancel = "Cancel" +btn_add = "Add loan" + +# ── Net Worth ───────────────────────────────────────────────────────────────── + +[networth] +title = "Net Worth" +hero_label = "Total net worth" +hero_formula = "cash balance + portfolio" +cash_label = "💵 Cash" +portfolio_label = "📈 Portfolio" +property_equity_label = "🏠 Property equity" +credit_label = "💳 Credit" + +[networth.cards] +cash_balance = "Cash balance" +cash_balance_sub = "all transaction history" +portfolio = "Portfolio" +portfolio_cost_basis = "Portfolio (cost basis)" +portfolio_market = "market value" +portfolio_cost_shown = "prices unavailable · cost basis shown" +property_equity = "Property equity" +credit_liabilities = "Credit / liabilities" +outstanding_balance = "outstanding balance" + +[networth.chart] +section_title = "Net worth over time" +subtitle = "cumulative · all time" +legend_net_worth = "Net Worth" +legend_loans = "Loans outstanding" + +[networth.empty] +title = "No transaction history yet" +desc = "Import some transactions to see your net worth over time." +btn_import = "Import transactions →" + +# ── Reports ────────────────────────────────────────────────────────────────── + +[reports] +title = "Monthly Reports" +chart_title = "12-Month Spend by Category" +table_title = "Breakdown by Month" +col_month = "Month" +col_total = "Total" + +# ── Projections ────────────────────────────────────────────────────────────── + +[projections] +title = "Projections" +card_annual_spend = "Projected Annual Spend" +card_annual_sub = "Based on 6-month average" +card_monthly_spend = "Projected Monthly Spend" +card_monthly_sub = "Average across all categories" +chart_title = "Monthly Average by Category — Last 6 Months" +table_col_category = "Category" +table_col_monthly_avg = "Monthly Avg" +table_col_projected = "Projected Annual" +table_col_share = "Share of Spend" + +[projections.empty] +title = "No spending data yet" +desc = "Import at least a month of transactions to see projections." +btn_import = "Import Transactions" + +# ── Simulator ("What If") ──────────────────────────────────────────────────── + +[simulator] +title = "What If…" +subtitle = "adjust sliders to see the ripple effect" + +[simulator.cards] +disposable_income = "Disposable income" +disposable_sub = "after fixed costs & goals" +monthly_savings = "Monthly savings" +monthly_savings_sub = "income − all committed spend" +savings_rate = "Savings rate" +savings_rate_sub = "savings ÷ income" + +[simulator.controls] +section_title = "Adjustments" +btn_reset = "Reset" +label_income_change = "Income change" +label_one_off = "One-off expense" +label_fixed_costs = "Fixed costs change" +label_new_goal = "Hypothetical new goal" +label_goal_amount = "Amount (€)" +placeholder_goal_amount = "5 000" +label_goal_months = "Months to save" +placeholder_goal_months = "12" +goal_would_need = "Would need" +per_month = "€0/mo" +leaving = "— leaving" +disposable_after = "€0/mo disposable" + +[simulator.goal_impact] +section_title = "Goal timeline impact" +no_goals_msg = "No goals yet." +goals_link = "Add some →" +committed = "committed" +feasibility_not_achievable = "Not achievable at this savings level" +feasibility_on_track = "On track ✓" +feasibility_faster = "months faster ✓" +feasibility_over = "months over deadline" + +[simulator.history_chart] +section_title = "Savings rate history" +subtitle = "past months" + +# ── Tax ────────────────────────────────────────────────────────────────────── + +[tax] +title = "Tax Summary" +btn_export = "Export CSV" +year_label = "Tax year:" +gross_income = "Gross Income" +gross_income_sub = "from Income transactions" +total_expenses = "Total Expenses" +total_expenses_sub = "across all categories" +capital_gains = "Capital Gains" +capital_gains_sub = "realized this year" +capital_losses = "Capital Losses" +capital_losses_sub = "realized this year" +net_capital = "Net Capital" +net_capital_sub = "gains − losses" + +[tax.capital_table] +section_title = "Realized Capital Events" +col_name = "Name" +col_isin = "ISIN" +col_cost_basis = "Cost Basis" +col_proceeds = "Proceeds" +col_gain_loss = "Gain/Loss" +col_pct = "%" + +[tax.expenses_table] +section_title = "Expenses by Category" +col_category = "Category" +col_total_spent = "Total Spent" +row_total = "Total" +empty_msg = "No expense transactions for {{year}}" + +# ── Auto Import Guide ──────────────────────────────────────────────────────── + +[auto_import] +title = "Import Guide" +subtitle = "How to get your bank transactions into the app" +btn_upload = "Upload CSV →" + +[auto_import.steps] +step1_title = "Export a CSV from your bank" +step1_body = "Log into your bank's online portal and download a transaction extract for the period you want. Most Portuguese banks offer this under Movimentos or Extratos." +step1_cgd_name = "CGD (Caixa Geral de Depósitos)" +step1_cgd_desc = "Netbanco → Consultas → Movimentos de Conta → Exportar. Choose CSV." +step1_tr_name = "Trade Republic" +step1_tr_desc = "App → Profile → Documents → Activity. Export as CSV." +step1_generic_name = "Generic / Other banks" +step1_generic_desc = "Any CSV with columns: Date, Description, Amount. The importer auto-detects column order." +step2_title = "Upload and preview" +step2_body = "Go to Import, pick the account and format, then select your file. You'll see a preview of every row with auto-suggested categories. Rows already in the database are shown greyed out and will be skipped automatically — safe to re-upload the same file." +step3_title = "Review categories and confirm" +step3_body = "Adjust any auto-categorised rows using the dropdown selectors, then click Confirm Import. Only new transactions are saved." + +[auto_import.tip] +title = "Tip — avoid duplicates:" +body = "the importer fingerprints each row by date, description, amount, and account. Re-uploading a file that overlaps with a previous import is safe; duplicates are detected and skipped at both preview and confirm time." + +[auto_import.api] +title = "Automate with a script" +body = "If you export your CSV on a schedule (e.g. via a cron job or n8n), you can push it directly to the import endpoint without going through the UI:" +footer = "The preview endpoint returns an HTML page; for headless import pipe the confirmed rows to POST /import/confirm with the same account_id, format, raw_data, and categories[] fields." + +# ── Import ──────────────────────────────────────────────────────────────────── + +[import] +title = "Import" + +[import.preview] +section_title = "Preview" +rows_label = "rows" +already_imported = "already imported" +skip_note = "(shown greyed out, will be skipped)" +btn_back = "← Back" +col_date = "Date" +col_description = "Description" +col_amount = "Amount" +col_category = "Category" +duplicate_label = "duplicate" +btn_confirm = "✓ Confirm Import" +btn_cancel = "Cancel" + +[import.upload] +bank_section_title = "Bank Transactions" +label_account = "Account" +placeholder_account = "Select account…" +label_format = "Bank / Format" +format_cgd = "Caixa Geral de Depósitos (CGD)" +format_tr = "Trade Republic Card" +format_generic = "Generic CSV" +label_csv_file = "CSV File" +btn_preview = "Preview Import" +securities_title = "Securities Trades" +securities_desc = "Upload your Trade Republic securities CSV to import buy/sell trades into your portfolio." +label_securities_file = "Trade Republic Securities CSV" +btn_import_trades = "Import Trades" +after_import_note = "After importing, visit Portfolio to see live prices and P&L." + +# ── Settings ───────────────────────────────────────────────────────────────── + +[settings] +title = "Settings" +tab_accounts = "Accounts" +tab_categories = "Categories" + +[settings.accounts] +card_add_title = "Add account" +placeholder_name = "Account name" +type_checking = "Checking" +type_savings = "Savings" +type_credit = "Credit card" +type_securities = "Securities" +btn_add = "Add" +col_name = "Name" +col_type = "Type" +btn_delete = "Delete" +empty_msg = "No accounts yet — add one above." +confirm_delete = "Delete this account?" + +[settings.categories] +card_add_title = "Add category" +placeholder_name = "Category name" +placeholder_budget = "Monthly budget (cents)" +btn_add = "Add" +col_category = "Category" +col_budget = "Monthly Budget" +btn_delete = "Delete" +empty_msg = "No categories yet — add one above." +confirm_delete = "Delete this category?" + +[settings.categories.modal_edit] +title = "Edit Category" +placeholder_name = "Name" +placeholder_budget = "Budget (cents)" +btn_cancel = "Cancel" +btn_save = "Save" + +# ── Accounts (standalone page) ──────────────────────────────────────────────── + +[accounts] +title = "Accounts" +add_title = "Add Account" +label_name = "Account Name" +placeholder_name = "e.g. CGD Checking" +label_type = "Type" +type_checking = "Checking" +type_savings = "Savings" +type_credit = "Credit Card" +type_securities = "Securities" +btn_add = "Add" +col_name = "Name" +col_type = "Type" +empty_msg = "No accounts yet." +confirm_delete = "Delete this account?" +btn_delete = "Delete" + +# ── Categories (standalone page) ────────────────────────────────────────────── + +[categories] +title = "Categories" +add_title = "Add Category" +label_name = "Name" +placeholder_name = "e.g. Dining" +label_color = "Color" +btn_add = "Add" +col_color = "" +col_name = "Name" +col_budget = "Monthly Budget" +no_budget = "No budget" +btn_edit_budget = "Edit" +btn_save_budget = "Save" +btn_cancel_budget = "Cancel" +btn_delete = "Delete" +empty_msg = "No categories yet. Add one above." +confirm_delete = "Delete this category? Transactions keep their label." + +# ── Sharing ─────────────────────────────────────────────────────────────────── + +[sharing] +title = "Sharing" +grant_title = "Grant Read Access" +label_user_email = "User Email" +placeholder_email = "Search by email…" +btn_grant = "Grant Access" +my_finances_title = "People with access to my finances" +col_user = "User" +col_since = "Since" +btn_revoke = "Revoke" +empty_my_grants = "No access grants yet." +access_to_me_title = "Access granted to me" +col_owner = "Owner" +empty_access_to_me = "No one has shared with you yet." +confirm_revoke = "Revoke access for this user?" + +# ── People ──────────────────────────────────────────────────────────────────── + +[people] +title = "People" +tab_sharing = "Sharing" +tab_household = "Household" + +[people.sharing] +grant_title = "Grant read access" +grant_desc = "Enter another user's ID to let them view your finances in read-only mode." +placeholder_viewer = "User ID or email" +btn_add = "Add" +viewers_title = "People with access to your data" +btn_revoke = "Revoke" +granted_title = "Accounts you can view" +view_link = "View →" +no_sharing_msg = "No sharing configured yet." +confirm_revoke = "Revoke access for this user?" + +[people.household] +link_title = "Link a partner account" +link_desc = "Combine finances with a partner to see a shared monthly overview and goals." +placeholder_email = "Partner's email" +btn_link = "Link" +linked_partner = "Linked partner" +btn_unlink = "Unlink" +stat_combined_income = "Combined Income" +stat_my_income = "My Income" +stat_partner_income = "Partner Income" +stat_combined_expenses = "Combined Expenses" +stat_disposable = "Disposable" +your_goals = "Your Goals" +partner_goals = "Partner Goals" +no_goals = "No goals" +committed_badge = "committed" +confirm_unlink = "Unlink household? This only removes the link, not any data." + +# ── Household (standalone page) ─────────────────────────────────────────────── + +[household] +title = "Household" +link_title = "Link a partner account" +link_desc = "Combine your finances with a partner to see a shared dashboard. Enter their account email address below." +label_partner_email = "Partner email" +placeholder_email = "partner@example.com" +btn_link = "Link Partner" +linked_partner = "Linked partner" +btn_unlink = "Unlink" +this_month_title = "This Month — Combined View" +combined_income = "Combined Income" +my_income = "My Income" +partner_income = "Partner Income" +combined_expenses = "Combined Expenses" +disposable = "Disposable" +goals_title = "Goals" +your_goals = "YOUR GOALS" +partner_goals = "PARTNER GOALS" +no_goals = "No goals" +committed_badge = "committed" +confirm_unlink = "Unlink household? This only removes the link, not any data." + +# ── Goal Planner (plan.html — standalone) ───────────────────────────────────── + +[plan] +title = "Goal Planner" +scenario_title = "Your scenario" +scenario_desc = "Model any transition where you hold an asset with a loan, want to acquire a new one, then sell the old to fund the new." +label_current_asset = "Current asset (optional)" +option_none_asset = "— none selected —" +label_current_loan = "Current loan (optional)" +option_none_loan = "— none selected —" +label_dream_cost = "New goal cost (€)" +placeholder_dream_cost = "e.g. 350000" +label_down_pct = "Down payment (%)" +label_loan_rate = "New loan rate (% annual)" +label_loan_term = "New loan term (years)" +placeholder_loan_term = "30" +label_build_months = "Acquisition / build period (months)" +label_monthly_savings = "Monthly savings available (€)" +placeholder_savings = "e.g. 800" +label_sale_price = "Expected sale price of current asset (€)" +placeholder_sale_price = "leave blank to use current value" +btn_run = "Run simulation →" +result_total_timeline = "Total timeline" +result_until_paid = "until goal is fully paid off" +result_final_monthly = "Final monthly cost" +result_after_selling = "after selling current asset" +result_total_interest = "Total interest" +result_across_both = "across both loans combined" +result_free_by = "Free by" +result_fully_paid = "fully paid off" +roadmap_title = "Your roadmap" +phase1_title = "Save down payment" +phase1_target = "Target:" +phase1_already_have = "Already have:" +phase1_still_need = "Still need:" +phase1_saving = "Saving:" +phase1_ready = "Ready now!" +phase1_equity_covers = "equity covers down payment" +phase1_down_payment = "Down payment:" +phase1_your_equity = "Your equity:" +phase2_title = "Acquire / build" +phase2_new_loan = "New loan:" +phase2_existing_loan = "Existing loan:" +phase2_new_emi = "New EMI:" +phase2_total_burden = "Total burden:" +phase3_title = "Sell & transition" +phase3_one_time = "One-time event" +phase3_after_acquisition = "after acquisition completes" +phase3_sale_price = "Sale price:" +phase3_pay_off = "Pay off loan:" +phase3_net_proceeds = "Net proceeds:" +phase3_applied = "Applied to new loan" +phase4_title = "Goal achieved" +phase4_remaining_loan = "Remaining loan:" +phase4_monthly_payment = "Monthly payment:" +phase4_just_new_loan = "just the new loan" +phase4_fully_paid = "Fully paid!" +phase4_sale_cleared = "sale proceeds cleared the loan" +phase4_no_remaining = "No remaining loan!" +phase4_sale_covers = "The sale covers everything." +chart_title = "Monthly cost over time" +chart_subtitle = "what you pay each month" +levers_title = "Key levers" +lever1_title = "Save more monthly" +lever1_desc = "Each extra €100/mo shortens Phase 1 and gets you acquiring sooner." +lever2_title = "Increase the down payment" +lever2_desc = "A higher down % reduces the new loan and lowers the double-burden in Phase 2." +lever3_title = "Sell at a higher price" +lever3_desc = "Every extra euro from the sale goes straight to reducing the new loan balance." +lever4_title = "Negotiate the rate" +lever4_desc = "Even 0.5% less on the new loan saves thousands over the full term." +empty_title = "Plan your next big goal" +empty_desc = "Model any transition where you hold an asset with a loan, want to acquire something new, and plan to sell the old to fund it — including the double-payment period, the sale, and the final payoff date." +empty_tip = "Tip: add your current asset and loan first so the planner can pre-fill the numbers." diff --git a/apps/finance/services/api/main/locales/pt.toml b/apps/finance/services/api/main/locales/pt.toml new file mode 100644 index 0000000..6c6a47f --- /dev/null +++ b/apps/finance/services/api/main/locales/pt.toml @@ -0,0 +1,1016 @@ +# ── Navegação ──────────────────────────────────────────────────────────────── + +[nav] +brand = "Pessoal" +dashboard = "Painel" +transactions = "Transações" +portfolio = "Carteira" +goals = "Objetivos" +property = "Imóveis" +people = "Pessoas" +analysis = "Análise" +settings = "Definições" +business = "🏢 Empresa" +hub_back = "← Hub" + +[nav.analysis] +reports = "Relatórios" +projections = "Projeções" +tax = "Impostos" +networth = "Património" +simulator = "E Se…" + +[nav.settings] +accounts_categories = "Contas & Categorias" +import_csv = "Importar CSV" +import_guide = "Guia de Importação" + +[nav.drawer] +analysis_label = "Análise" +settings_label = "Definições" + +[nav.theme] +toggle_title = "Alternar modo escuro/claro" +menu_aria = "Menu" + +# ── Página inicial ──────────────────────────────────────────────────────────── + +[homepage] +title = "Finance Hub — Finanças Pessoais & Empresariais" +brand = "Finance Hub" +footer_tagline = "Homelab auto-hospedado" + +[homepage.nav] +signin = "Entrar" +personal = "Pessoal" +business = "Empresa" + +[homepage.hero] +eyebrow = "Auto-hospedado · Privado · Aberto" +title_line1 = "Património pessoal." +title_line2 = "Finanças empresariais." +subtitle = "Uma plataforma auto-hospedada para acompanhar o seu património pessoal e gerir o orçamento da sua organização — sem partilhar os seus dados com ninguém." +cta_get_started = "Começar" +cta_see_inside = "Ver o que inclui" +cta_open_personal = "Abrir pessoal" +cta_open_business = "Abrir empresa" + +[homepage.trust] +data_privacy = "Os seus dados, o seu servidor" +multi_bank = "Importação CSV multibanco" +dark_light = "Modo escuro & claro" +role_access = "Acesso por função" +analytics = "Análise em tempo real" + +[homepage.products.personal] +badge = "Pessoal" +name = "As Minhas Finanças" +desc = "Visibilidade total sobre o seu património pessoal — gastos, investimentos, objetivos e valor líquido num só lugar." +feature_1 = "Painel de gastos & tendências mensais" +feature_2 = "Transações com orçamentos por categoria" +feature_3 = "Acompanhamento de carteira de investimentos" +feature_4 = "Objetivos financeiros com progresso" +feature_5 = "Património líquido & projeções hipotéticas" +feature_6 = "Exportação de relatório fiscal" +feature_7 = "Partilha familiar & vista multiutilizador" +cta_open = "Abrir aplicação" +cta_signin = "Entrar" + +[homepage.products.business] +badge = "Empresa" +name = "Organizações" +desc = "Gerencie organizações, planeie anos fiscais, controle orçamentos de eventos e mantenha as equipas alinhadas financeiramente." +feature_1 = "Multi-org com ciclo de vida do ano fiscal" +feature_2 = "Equipas com avatares emoji & funções" +feature_3 = "Eventos com linhas de orçamento & objetivos" +feature_4 = "Pedidos de compra & fluxo de aprovação" +feature_5 = "Razão & importação de CSV bancário" +feature_6 = "Análise de variância por evento & equipa" +feature_7 = "Relatório narrativo de fim de ano" +cta_open = "Abrir orgs" +cta_signin = "Entrar" + +[homepage.features.personal] +section_label = "Finanças Pessoais" +section_title = "Visibilidade total sobre o seu património" +section_sub = "Do café diário à carteira de longo prazo — tudo num painel, atualizado à medida que importa." +chip1_title = "Painel inteligente" +chip1_desc = "Rendimento vs gastos mensais, principais categorias, transações recentes e saúde do orçamento de relance." +chip2_title = "Acompanhamento de carteira" +chip2_desc = "Importe negociações, acompanhe tickers mapeados por ISIN, veja P&L e alocação por classe de ativo." +chip3_title = "Objetivos & projeções" +chip3_desc = "Defina objetivos de poupança, modele cenários e veja o seu património líquido projetado ao longo do tempo." + +[homepage.features.business] +section_label = "Finanças Empresariais" +section_title = "Controlo orçamental para a sua org" +section_sub = "Do início do ano fiscal ao relatório final — com rastreabilidade total de cada euro gasto." +chip1_title = "Ciclo de vida do ano fiscal" +chip1_desc = "Planeie eventos e orçamentos por ano. Ative, bloqueie e feche com um relatório de fim de ano." +chip2_title = "Fluxo de pedido & aprovação" +chip2_desc = "As equipas submetem pedidos de compra. As finanças aprovam, acompanham a entrega e liquidam no razão." +chip3_title = "Análise de variância" +chip3_desc = "Compare planeado vs real por evento e por equipa. Identifique excesso de gastos antes do fecho do ano." + +[homepage.signin_block] +title = "Pronto para começar?" +body = "O Finance Hub é auto-hospedado no seu homelab. Inicie sessão com a sua conta para aceder aos seus painéis pessoal e empresarial." +cta = "Entrar no Finance Hub" + +[homepage.mock] +net_income_label = "Rendimento líquido" +net_income_value = "+€2.840" +net_income_sub = "vs mês anterior +12%" +net_worth_label = "Património líquido" +net_worth_sub = "↑ €1.240 este mês" +budget_health = "Saúde do orçamento" +groceries = "Supermercado" +dining_out = "Restaurantes" +transport = "Transportes" +entertainment = "Entretenimento" +events_label = "Eventos — Festival de Verão 2025" +budget_label = "Orçamento" +active_year = "Ano ativo" +requests_label = "Pedidos" +sound_equipment = "Aluguer de equipamento de som" +catering_deposit = "Depósito de catering" +stage_lighting = "Iluminação de palco" +approved = "Aprovado" +pending = "Pendente" +in_transit = "Em trânsito" + +# ── Autenticação ────────────────────────────────────────────────────────────── + +[auth.login] +page_title = "Entrar — Finance Hub" +brand = "Finance Hub" +heading = "Bem-vindo de volta" +subtext = "Inicie sessão na sua conta. Sem conta?" +subtext_link = "Criar uma →" +field_email = "E-mail" +placeholder_email = "voce@exemplo.com" +field_password = "Palavra-passe" +placeholder_password = "••••••••" +btn_submit = "Entrar →" +divider = "ou" +btn_google = "Continuar com o Google" +footer_back = "← Voltar ao início" +error_oauth = "Falha ao iniciar sessão com o Google. Por favor, tente novamente." + +[auth.register] +page_title = "Criar conta — Finance Hub" +brand = "Finance Hub" +heading = "Criar a sua conta" +subtext = "Já tem conta?" +subtext_link = "Entrar →" +field_name = "Nome" +name_optional = "(opcional)" +placeholder_name = "O seu nome" +field_email = "E-mail" +placeholder_email = "voce@exemplo.com" +field_password = "Palavra-passe" +placeholder_password = "••••••••" +hint_password = "Mínimo de 8 caracteres" +field_confirm = "Confirmar palavra-passe" +placeholder_confirm = "••••••••" +btn_submit = "Criar conta →" +divider_oauth = "ou registar com e-mail" +btn_google = "Continuar com o Google" +footer_back = "← Voltar ao início" + +# ── Painel ──────────────────────────────────────────────────────────────────── + +[dashboard] +title = "Painel" +available_to_spend = "Disponível para gastar este mês" +available_formula = "rendimento − custos fixos − já gasto" +disposable_label = "disponível" +month_progress = "Progresso do mês: {{pct}}%" +month_spent = "Gasto: {{pct}}%" + +[dashboard.cards] +bank_should_be = "Saldo bancário recomendado" +bank_should_be_sub = "próximos fixos + margem de segurança" +savings_rate = "Taxa de poupança" +net_worth = "Património líquido" +net_worth_link = "→ detalhe completo" +portfolio_today = "Carteira hoje" +portfolio_cost_basis = "custo de aquisição · preços indisponíveis" +portfolio_no_trades = "Sem negociações" +portfolio_import_link = "Importar negociações →" + +[dashboard.bank_math] +section_title = "O que deve ter no banco" +section_subtitle = "agora mesmo" +safety_buffer = "Margem de segurança (2 semanas)" +minimum_recommended = "Mínimo recomendado" +no_recurring_msg = "Nenhuma despesa recorrente detetada ainda." +no_recurring_sub = "Importe alguns meses de transações." + +[dashboard.stocks] +section_title = "Carteira em resumo" +portfolio_link = "→ carteira" +shares_label = "ações" +cost_basis = "custo de aquisição" +total_label = "Total" +total_invested = "Total investido" +no_holdings_msg = "Sem posições ainda." +import_link = "Importar negociações →" + +[dashboard.budget_health] +section_title = "Saúde do orçamento" +categories_link = "→ categorias" + +[dashboard.recent] +section_title = "Atividade recente" +all_txns_link = "→ todas as transações" +no_txns_msg = "Sem transações ainda." +import_link = "Importar algumas!" + +[dashboard.goals] +section_title = "Objetivos comprometidos" +all_goals_link = "→ todos os objetivos" +months_left = "meses restantes" +per_month_needed = "/mês necessários" + +[dashboard.fixed_costs] +section_title = "Custos fixos" +auto_detected = "detetado automaticamente · média de 3 meses" +committed_goal = "objetivo comprometido" +recurring_expense = "despesa recorrente" +per_month = "/ mês" +total_committed = "Total comprometido" + +[dashboard.alerts] +vs_last_month_up = "↑ vs mês anterior" +vs_last_month_down = "↓ vs mês anterior" + +# ── Transações ──────────────────────────────────────────────────────────────── + +[transactions] +title = "Transações" +btn_add = "+ Adicionar Transação" +warning_all_dupes = "Todas as linhas desse ficheiro já foram importadas — nada foi adicionado." + +[transactions.filter] +label_category = "Categoria" +option_all_cats = "Todas as Categorias" +label_period = "Período" +option_all_time = "Todo o tempo" +option_30_days = "30 dias" +option_90_days = "90 dias" +option_1_year = "1 ano" +label_search = "Pesquisar" +placeholder_search = "Descrição…" +btn_filter = "Filtrar" +btn_clear = "Limpar" + +[transactions.table] +col_date = "Data" +col_description = "Descrição" +col_account = "Conta" +col_category = "Categoria" +col_amount = "Valor" +btn_edit_category = "Editar categoria" +btn_delete = "Eliminar" +empty_msg = "Nenhuma transação encontrada." +empty_import_link = "Importar algumas" +empty_add_btn = "adicionar manualmente" + +[transactions.modal_add] +title = "Adicionar Transação" +label_date = "Data" +label_description = "Descrição" +placeholder_desc = "ex: Café no Starbucks" +label_amount = "Valor (€)" +option_expense = "− Despesa" +option_income = "+ Rendimento" +placeholder_amount = "0,00" +label_category = "Categoria" +label_account = "Conta" +option_no_account = "— nenhuma —" +btn_cancel = "Cancelar" +btn_save = "Guardar Transação" +error_required = "Por favor, preencha a data, a descrição e um valor positivo." +error_save_failed = "Falha ao guardar." + +[transactions.confirm] +delete_msg = "Eliminar esta transação?" + +# ── Carteira ────────────────────────────────────────────────────────────────── + +[portfolio] +title = "Carteira" +missing_prices_warn = "Preço em tempo real indisponível para {{n}} posição{{s}} — adicione um ticker do Yahoo Finance para corrigir" +lookup_link = "Pesquisar ↗" +btn_save_ticker = "Guardar" +ticker_placeholder = "ex: QDVE.DE" + +[portfolio.cards] +total_value = "Valor Total" +total_cost = "Custo Total" +unrealized_pl = "P&L Não Realizado" + +[portfolio.allocation] +section_title = "Alocação" + +[portfolio.holdings] +section_title = "Posições" +col_asset = "Ativo" +col_shares = "Ações" +col_avg_cost = "Custo Médio" +col_price = "Preço" +col_value = "Valor" +col_pl = "P&L" +add_trades_via = "Adicionar negociações via" +btn_import = "Importar CSV" + +[portfolio.empty] +title = "Sem negociações" +desc = "Importe o seu CSV de valores mobiliários do Trade Republic para ver a sua carteira." +btn_import = "Importar Negociações" + +# ── Objetivos ───────────────────────────────────────────────────────────────── + +[goals] +title = "Objetivos" +tab_committed = "Objetivos comprometidos" +tab_planner = "Planeador de Objetivos" + +[goals.summary_cards] +avg_monthly_savings = "Poupança mensal média" +last_3_months = "últimos 3 meses" +disposable_income = "Rendimento disponível" +before_goals = "antes dos objetivos" +reserved_for_goals = "Reservado para objetivos" +per_month = "por mês" +free_to_spend = "Livre para gastar" +after_goals = "após objetivos" + +[goals.goal_card] +type_once = "Compra pontual" +type_deposit = "Entrada / sinal" +type_emergency = "Fundo de emergência" +type_recurring = "Investimento recorrente" +saved_of = "poupado de" +need_per_month = "Necessário por mês" +months_left = "Meses restantes" +at_current_rate = "À taxa atual" +disposable_after = "Disponível depois" +on_track = "✓ No bom caminho — a sua taxa de poupança atual cobre €{{monthly}}/mês com {{months}} meses restantes." +behind_warn = "⚠ À sua taxa atual (€{{rate}}/mês) chegaria lá em {{actual}} meses — {{diff}} meses de atraso." +btn_adjust_deadline = "Ajustar prazo →" +btn_committed = "✓ Comprometido — clique para cancelar compromisso" +btn_commit = "Comprometer com este objetivo" +btn_remove = "Remover" +confirm_remove = "Remover este objetivo?" + +[goals.empty] +title = "Sem objetivos ainda" +desc = "Use o separador Planeador de Objetivos para simular um objetivo e guardá-lo aqui." +btn_open_planner = "Abrir Planeador de Objetivos →" + +[goals.planner] +what_kind = "Que tipo de objetivo?" +purchase_title = "Poupar para uma compra" +purchase_desc = "Carro, viagem, gadget, fundo — poupar até um valor-alvo até uma data." +transition_title = "Vender & atualizar" +transition_desc = "Ter um ativo com empréstimo, adquirir algo novo e vender o antigo para financiar." + +[goals.planner.purchase] +form_title = "O seu objetivo de compra" +label_name = "Nome do objetivo" +placeholder_name = "ex: Carro novo, Viagem à Europa, Fundo de emergência…" +label_target = "Valor-alvo (€)" +placeholder_target = "ex: 12000" +label_monthly_savings = "Poupança mensal (€)" +placeholder_monthly = "ex: 400" +label_deadline = "Data-alvo (opcional — deixe em branco para ver quando chegará lá)" +btn_calculate = "Calcular →" + +[goals.planner.purchase_result] +card_at_rate = "À sua taxa de poupança" +card_monthly_needed = "Necessário por mês" +card_target = "Valor-alvo" +your_goal_amount = "o valor do seu objetivo" +to_hit_deadline = "para atingir {{date}}" +set_target_date = "defina uma data-alvo acima" +enter_monthly = "introduza poupança mensal" +reach_label = "chega em {{date}}" +on_track = "✓ No bom caminho — €{{monthly}}/mês cobre o necessário €{{needed}}/mês." +behind_warn = "⚠ Precisa de €{{needed}}/mês para cumprir o prazo, mas está a poupar €{{monthly}}/mês. À taxa atual chegará lá em {{months}} meses ({{date}})." +save_as_goal_title = "Guardar como objetivo" +save_as_goal_desc = "Adiciona ao separador Objetivos para poder comprometer-se." +label_goal_name = "Nome do objetivo" +placeholder_goal_name = "ex: Carro novo" +btn_save_goal = "Guardar objetivo →" + +[goals.planner.transition] +form_title = "O seu cenário de transição" +label_current_asset = "Ativo atual (opcional)" +option_none_asset = "— nenhum selecionado —" +label_current_loan = "Empréstimo atual (opcional)" +option_none_loan = "— nenhum selecionado —" +label_dream_cost = "Custo do novo objetivo (€)" +placeholder_dream_cost = "ex: 350000" +label_down_pct = "Entrada (%)" +label_loan_rate = "Taxa do novo empréstimo (% anual)" +label_loan_term = "Prazo do novo empréstimo (anos)" +label_build_months = "Período de aquisição / construção (meses)" +label_monthly_savings = "Poupança mensal disponível (€)" +placeholder_savings = "ex: 800" +label_sale_price = "Preço de venda esperado do ativo atual (€)" +placeholder_sale_price = "deixe em branco para usar o valor atual" +btn_run = "Executar simulação →" + +[goals.planner.transition_result] +card_total_timeline = "Linha temporal total" +until_paid_off = "até ao pagamento total do objetivo" +card_final_monthly = "Custo mensal final" +after_selling = "após venda do ativo atual" +card_total_interest = "Juros totais" +across_both_loans = "em ambos os empréstimos" +card_free_by = "Livre em" +fully_paid_off = "totalmente pago" +roadmap_title = "O seu roteiro" +phase1_title = "Poupar entrada" +phase1_target = "Objetivo:" +phase1_already_have = "Já tem:" +phase1_still_need = "Ainda precisa:" +phase1_saving = "A poupar:" +phase1_ready = "Pronto já!" +phase1_equity_covers = "o capital cobre a entrada" +phase1_down_payment = "Entrada:" +phase1_your_equity = "O seu capital:" +phase2_title = "Adquirir / construir" +phase2_new_loan = "Novo empréstimo:" +phase2_existing_loan = "Empréstimo existente:" +phase2_new_emi = "Nova prestação:" +phase2_total_burden = "Encargo total:" +phase3_title = "Vender & transitar" +phase3_one_time = "Evento único" +phase3_after_acquisition = "após a conclusão da aquisição" +phase3_sale_price = "Preço de venda:" +phase3_pay_off = "Liquidar empréstimo:" +phase3_net_proceeds = "Receita líquida:" +phase3_applied = "Aplicado ao novo empréstimo" +phase4_title = "Objetivo atingido" +phase4_remaining_loan = "Empréstimo restante:" +phase4_monthly = "Mensal:" +phase4_fully_paid = "Totalmente pago!" +phase4_sale_cleared = "receita da venda liquidou o empréstimo" +phase4_no_remaining = "Sem empréstimo restante!" +chart_title = "Custo mensal ao longo do tempo" +chart_subtitle = "o que paga em cada mês" +save_as_goal_title = "Guardar como objetivo" +save_as_goal_desc = "Adiciona ao separador Objetivos para poder comprometer-se." +label_goal_name = "Nome do objetivo" +placeholder_goal_name = "ex: Novo imóvel, Atualizar carro…" +btn_save_goal = "Guardar objetivo →" + +# ── Imóveis ─────────────────────────────────────────────────────────────────── + +[property] +summary_total_value = "Valor total dos imóveis" +summary_total_value_sub = "valor estimado atual" +summary_outstanding_loans = "Empréstimos em dívida" +summary_outstanding_sub = "saldo restante" +summary_net_equity = "Capital líquido" +summary_net_equity_sub = "valor − empréstimos" + +[property.properties] +section_title = "Imóveis" +btn_add = "+ Adicionar imóvel" +stat_current_value = "Valor atual" +stat_equity = "Capital próprio" +stat_purchase_price = "Preço de compra" +stat_gain = "Mais-valia" +equity_label = "Capital {{pct}}%" +loan_label = "Empréstimo {{pct}}%" +loan_remaining = "Restante" +loan_monthly_payment = "Prestação mensal" +loan_payoff = "Data de liquidação" +loan_rate = "Taxa" +btn_edit = "Editar" +btn_remove = "Remover" + +[property.properties.empty] +title = "Sem imóveis ainda" +desc = "Adicione a sua casa, propriedade de investimento ou terreno para acompanhar o capital próprio e os empréstimos num só lugar." +btn_add_first = "Adicionar o primeiro imóvel" + +[property.loans] +section_title = "Empréstimos" +btn_add = "+ Adicionar empréstimo" +stat_balance = "Saldo" +stat_monthly = "Mensal" +stat_payoff = "Liquidação" +stat_rate = "Taxa" +paid_label = "Pago {{pct}}%" +interest_left = "juros restantes" +btn_mark_paid = "Marcar como liquidado" +btn_remove = "Remover" +confirm_paid_off = "Marcar como liquidado?" + +[property.modal_add] +title_property = "Adicionar imóvel" +label_name = "Nome do imóvel *" +placeholder_name = "ex: Casa Principal" +label_address = "Morada" +placeholder_address = "Rua, cidade" +label_purchase_price = "Preço de compra (€) *" +placeholder_purchase_price = "220000" +label_current_value = "Valor atual (€)" +placeholder_current_value = "Igual ao preço de compra" +hint_current_value = "Deixe em branco para usar o preço de compra" +label_purchase_date = "Data de compra" +label_appreciation = "Valorização estimada (%/ano)" +placeholder_appreciation = "2,0" +label_status = "Estado" +status_owned = "Próprio" +status_building = "Em construção" +status_sold = "Vendido" +label_notes = "Notas" +placeholder_notes = "Notas opcionais" +btn_cancel = "Cancelar" +btn_add = "Adicionar imóvel" + +[property.modal_edit] +title_property = "Editar imóvel" +label_name = "Nome do imóvel *" +label_address = "Morada" +label_purchase_price = "Preço de compra (€)" +label_current_value = "Valor atual (€)" +label_purchase_date = "Data de compra" +label_appreciation = "Valorização (%/ano)" +label_status = "Estado" +status_owned = "Próprio" +status_building = "Em construção" +status_sold = "Vendido" +label_notes = "Notas" +btn_cancel = "Cancelar" +btn_save = "Guardar alterações" + +[property.modal_loan] +title_loan = "Adicionar empréstimo" +label_name = "Nome do empréstimo *" +placeholder_name = "ex: Crédito habitação" +label_type = "Tipo" +type_mortgage = "Hipoteca" +type_construction = "Crédito à construção" +type_personal = "Crédito pessoal" +label_linked_property = "Imóvel associado" +option_none_property = "— nenhum —" +label_principal = "Capital inicial (€) *" +placeholder_principal = "200000" +label_balance = "Saldo atual (€)" +placeholder_balance = "Igual ao capital inicial" +hint_balance = "Deixe em branco se for um novo empréstimo" +label_interest_rate = "Taxa de juro (%/ano) *" +placeholder_rate = "3,2" +label_term = "Prazo (meses) *" +placeholder_term = "360" +hint_term = "ex: 360 = 30 anos" +label_monthly_payment = "Prestação mensal (€)" +placeholder_monthly_payment = "Calculada automaticamente" +hint_monthly_payment = "Deixe em branco para calcular" +label_start_date = "Data de início" +label_notes = "Notas" +placeholder_notes = "Nome do banco, referência, etc." +btn_cancel = "Cancelar" +btn_add = "Adicionar empréstimo" + +# ── Património Líquido ──────────────────────────────────────────────────────── + +[networth] +title = "Património Líquido" +hero_label = "Património líquido total" +hero_formula = "saldo em conta + carteira" +cash_label = "💵 Dinheiro" +portfolio_label = "📈 Carteira" +property_equity_label = "🏠 Capital de imóveis" +credit_label = "💳 Crédito" + +[networth.cards] +cash_balance = "Saldo em conta" +cash_balance_sub = "todo o histórico de transações" +portfolio = "Carteira" +portfolio_cost_basis = "Carteira (custo de aquisição)" +portfolio_market = "valor de mercado" +portfolio_cost_shown = "preços indisponíveis · custo de aquisição apresentado" +property_equity = "Capital de imóveis" +credit_liabilities = "Crédito / passivos" +outstanding_balance = "saldo em dívida" + +[networth.chart] +section_title = "Património líquido ao longo do tempo" +subtitle = "acumulado · desde sempre" +legend_net_worth = "Património Líquido" +legend_loans = "Empréstimos em dívida" + +[networth.empty] +title = "Sem histórico de transações" +desc = "Importe algumas transações para ver o seu património líquido ao longo do tempo." +btn_import = "Importar transações →" + +# ── Relatórios ──────────────────────────────────────────────────────────────── + +[reports] +title = "Relatórios Mensais" +chart_title = "Gastos por Categoria — 12 Meses" +table_title = "Detalhe por Mês" +col_month = "Mês" +col_total = "Total" + +# ── Projeções ───────────────────────────────────────────────────────────────── + +[projections] +title = "Projeções" +card_annual_spend = "Gasto Anual Projetado" +card_annual_sub = "Com base na média de 6 meses" +card_monthly_spend = "Gasto Mensal Projetado" +card_monthly_sub = "Média em todas as categorias" +chart_title = "Média Mensal por Categoria — Últimos 6 Meses" +table_col_category = "Categoria" +table_col_monthly_avg = "Média Mensal" +table_col_projected = "Projeção Anual" +table_col_share = "Quota do Gasto" + +[projections.empty] +title = "Sem dados de gastos ainda" +desc = "Importe pelo menos um mês de transações para ver as projeções." +btn_import = "Importar Transações" + +# ── Simulador ("E Se…") ─────────────────────────────────────────────────────── + +[simulator] +title = "E Se…" +subtitle = "ajuste os controlos para ver o efeito em cascata" + +[simulator.cards] +disposable_income = "Rendimento disponível" +disposable_sub = "após custos fixos & objetivos" +monthly_savings = "Poupança mensal" +monthly_savings_sub = "rendimento − todos os gastos comprometidos" +savings_rate = "Taxa de poupança" +savings_rate_sub = "poupança ÷ rendimento" + +[simulator.controls] +section_title = "Ajustes" +btn_reset = "Repor" +label_income_change = "Variação do rendimento" +label_one_off = "Despesa pontual" +label_fixed_costs = "Variação dos custos fixos" +label_new_goal = "Novo objetivo hipotético" +label_goal_amount = "Valor (€)" +placeholder_goal_amount = "5 000" +label_goal_months = "Meses para poupar" +placeholder_goal_months = "12" +goal_would_need = "Precisaria de" +per_month = "€0/mês" +leaving = "— deixando" +disposable_after = "€0/mês disponível" + +[simulator.goal_impact] +section_title = "Impacto na linha temporal dos objetivos" +no_goals_msg = "Sem objetivos ainda." +goals_link = "Adicionar →" +committed = "comprometido" +feasibility_not_achievable = "Não atingível com esta taxa de poupança" +feasibility_on_track = "No bom caminho ✓" +feasibility_faster = "meses mais cedo ✓" +feasibility_over = "meses acima do prazo" + +[simulator.history_chart] +section_title = "Histórico da taxa de poupança" +subtitle = "meses anteriores" + +# ── Impostos ────────────────────────────────────────────────────────────────── + +[tax] +title = "Resumo Fiscal" +btn_export = "Exportar CSV" +year_label = "Ano fiscal:" +gross_income = "Rendimento Bruto" +gross_income_sub = "das transações de Rendimento" +total_expenses = "Total de Despesas" +total_expenses_sub = "em todas as categorias" +capital_gains = "Mais-Valias" +capital_gains_sub = "realizadas este ano" +capital_losses = "Menos-Valias" +capital_losses_sub = "realizadas este ano" +net_capital = "Capital Líquido" +net_capital_sub = "ganhos − perdas" + +[tax.capital_table] +section_title = "Eventos de Capital Realizados" +col_name = "Nome" +col_isin = "ISIN" +col_cost_basis = "Custo de Aquisição" +col_proceeds = "Receitas" +col_gain_loss = "Ganho/Perda" +col_pct = "%" + +[tax.expenses_table] +section_title = "Despesas por Categoria" +col_category = "Categoria" +col_total_spent = "Total Gasto" +row_total = "Total" +empty_msg = "Sem transações de despesa para {{year}}" + +# ── Guia de Importação ──────────────────────────────────────────────────────── + +[auto_import] +title = "Guia de Importação" +subtitle = "Como importar as suas transações bancárias para a aplicação" +btn_upload = "Carregar CSV →" + +[auto_import.steps] +step1_title = "Exportar um CSV do seu banco" +step1_body = "Inicie sessão no portal online do seu banco e descarregue um extrato de transações para o período pretendido. A maioria dos bancos portugueses disponibiliza esta opção em Movimentos ou Extratos." +step1_cgd_name = "CGD (Caixa Geral de Depósitos)" +step1_cgd_desc = "Netbanco → Consultas → Movimentos de Conta → Exportar. Escolha CSV." +step1_tr_name = "Trade Republic" +step1_tr_desc = "App → Perfil → Documentos → Atividade. Exportar como CSV." +step1_generic_name = "Genérico / Outros bancos" +step1_generic_desc = "Qualquer CSV com colunas: Date, Description, Amount. O importador deteta automaticamente a ordem das colunas." +step2_title = "Carregar e pré-visualizar" +step2_body = "Vá a Importar, escolha a conta e o formato e selecione o ficheiro. Verá uma pré-visualização de cada linha com categorias sugeridas automaticamente. As linhas já presentes na base de dados aparecem a cinzento e serão ignoradas — pode carregar o mesmo ficheiro com segurança." +step3_title = "Rever categorias e confirmar" +step3_body = "Ajuste as linhas categorizadas automaticamente com os seletores, e depois clique em Confirmar Importação. Apenas as novas transações são guardadas." + +[auto_import.tip] +title = "Dica — evitar duplicados:" +body = "o importador identifica cada linha pela data, descrição, valor e conta. Voltar a carregar um ficheiro que se sobreponha a uma importação anterior é seguro; os duplicados são detetados e ignorados tanto na pré-visualização como na confirmação." + +[auto_import.api] +title = "Automatizar com um script" +body = "Se exportar o CSV com regularidade (ex: via cron job ou n8n), pode enviá-lo diretamente para o endpoint de importação sem passar pela interface:" +footer = "O endpoint de pré-visualização devolve uma página HTML; para importação sem interface redirecione as linhas confirmadas para POST /import/confirm com os campos account_id, format, raw_data e categories[]." + +# ── Importar ────────────────────────────────────────────────────────────────── + +[import] +title = "Importar" + +[import.preview] +section_title = "Pré-visualização" +rows_label = "linhas" +already_imported = "já importadas" +skip_note = "(mostradas a cinzento, serão ignoradas)" +btn_back = "← Voltar" +col_date = "Data" +col_description = "Descrição" +col_amount = "Valor" +col_category = "Categoria" +duplicate_label = "duplicado" +btn_confirm = "✓ Confirmar Importação" +btn_cancel = "Cancelar" + +[import.upload] +bank_section_title = "Transações Bancárias" +label_account = "Conta" +placeholder_account = "Selecionar conta…" +label_format = "Banco / Formato" +format_cgd = "Caixa Geral de Depósitos (CGD)" +format_tr = "Trade Republic Card" +format_generic = "CSV Genérico" +label_csv_file = "Ficheiro CSV" +btn_preview = "Pré-visualizar Importação" +securities_title = "Negociações de Valores Mobiliários" +securities_desc = "Carregue o seu CSV de valores mobiliários do Trade Republic para importar compras/vendas para a sua carteira." +label_securities_file = "CSV de Valores Mobiliários Trade Republic" +btn_import_trades = "Importar Negociações" +after_import_note = "Após importar, visite a Carteira para ver preços em tempo real e P&L." + +# ── Definições ──────────────────────────────────────────────────────────────── + +[settings] +title = "Definições" +tab_accounts = "Contas" +tab_categories = "Categorias" + +[settings.accounts] +card_add_title = "Adicionar conta" +placeholder_name = "Nome da conta" +type_checking = "Conta à ordem" +type_savings = "Poupança" +type_credit = "Cartão de crédito" +type_securities = "Valores mobiliários" +btn_add = "Adicionar" +col_name = "Nome" +col_type = "Tipo" +btn_delete = "Eliminar" +empty_msg = "Sem contas ainda — adicione uma acima." +confirm_delete = "Eliminar esta conta?" + +[settings.categories] +card_add_title = "Adicionar categoria" +placeholder_name = "Nome da categoria" +placeholder_budget = "Orçamento mensal (cêntimos)" +btn_add = "Adicionar" +col_category = "Categoria" +col_budget = "Orçamento Mensal" +btn_delete = "Eliminar" +empty_msg = "Sem categorias ainda — adicione uma acima." +confirm_delete = "Eliminar esta categoria?" + +[settings.categories.modal_edit] +title = "Editar Categoria" +placeholder_name = "Nome" +placeholder_budget = "Orçamento (cêntimos)" +btn_cancel = "Cancelar" +btn_save = "Guardar" + +# ── Contas (página autónoma) ────────────────────────────────────────────────── + +[accounts] +title = "Contas" +add_title = "Adicionar Conta" +label_name = "Nome da Conta" +placeholder_name = "ex: CGD À Ordem" +label_type = "Tipo" +type_checking = "Conta à ordem" +type_savings = "Poupança" +type_credit = "Cartão de Crédito" +type_securities = "Valores Mobiliários" +btn_add = "Adicionar" +col_name = "Nome" +col_type = "Tipo" +empty_msg = "Sem contas ainda." +confirm_delete = "Eliminar esta conta?" +btn_delete = "Eliminar" + +# ── Categorias (página autónoma) ────────────────────────────────────────────── + +[categories] +title = "Categorias" +add_title = "Adicionar Categoria" +label_name = "Nome" +placeholder_name = "ex: Restaurantes" +label_color = "Cor" +btn_add = "Adicionar" +col_color = "" +col_name = "Nome" +col_budget = "Orçamento Mensal" +no_budget = "Sem orçamento" +btn_edit_budget = "Editar" +btn_save_budget = "Guardar" +btn_cancel_budget = "Cancelar" +btn_delete = "Eliminar" +empty_msg = "Sem categorias ainda. Adicione uma acima." +confirm_delete = "Eliminar esta categoria? As transações mantêm a etiqueta." + +# ── Partilha ────────────────────────────────────────────────────────────────── + +[sharing] +title = "Partilha" +grant_title = "Conceder Acesso de Leitura" +label_user_email = "E-mail do utilizador" +placeholder_email = "Pesquisar por e-mail…" +btn_grant = "Conceder Acesso" +my_finances_title = "Pessoas com acesso às minhas finanças" +col_user = "Utilizador" +col_since = "Desde" +btn_revoke = "Revogar" +empty_my_grants = "Sem autorizações de acesso ainda." +access_to_me_title = "Acesso concedido a mim" +col_owner = "Proprietário" +empty_access_to_me = "Ninguém partilhou dados consigo ainda." +confirm_revoke = "Revogar o acesso deste utilizador?" + +# ── Pessoas ─────────────────────────────────────────────────────────────────── + +[people] +title = "Pessoas" +tab_sharing = "Partilha" +tab_household = "Agregado" + +[people.sharing] +grant_title = "Conceder acesso de leitura" +grant_desc = "Introduza o ID de outro utilizador para que ele possa ver as suas finanças em modo só de leitura." +placeholder_viewer = "ID ou e-mail do utilizador" +btn_add = "Adicionar" +viewers_title = "Pessoas com acesso aos seus dados" +btn_revoke = "Revogar" +granted_title = "Contas que pode ver" +view_link = "Ver →" +no_sharing_msg = "Sem partilha configurada ainda." +confirm_revoke = "Revogar o acesso deste utilizador?" + +[people.household] +link_title = "Ligar conta de parceiro" +link_desc = "Combine finanças com um parceiro para ver um resumo mensal partilhado e objetivos." +placeholder_email = "E-mail do parceiro" +btn_link = "Ligar" +linked_partner = "Parceiro ligado" +btn_unlink = "Desligar" +stat_combined_income = "Rendimento Conjunto" +stat_my_income = "O Meu Rendimento" +stat_partner_income = "Rendimento do Parceiro" +stat_combined_expenses = "Despesas Conjuntas" +stat_disposable = "Disponível" +your_goals = "Os Meus Objetivos" +partner_goals = "Objetivos do Parceiro" +no_goals = "Sem objetivos" +committed_badge = "comprometido" +confirm_unlink = "Desligar agregado? Apenas remove a ligação, não os dados." + +# ── Agregado (página autónoma) ──────────────────────────────────────────────── + +[household] +title = "Agregado" +link_title = "Ligar conta de parceiro" +link_desc = "Combine as suas finanças com um parceiro para ver um painel partilhado. Introduza o e-mail da conta do parceiro abaixo." +label_partner_email = "E-mail do parceiro" +placeholder_email = "parceiro@exemplo.com" +btn_link = "Ligar Parceiro" +linked_partner = "Parceiro ligado" +btn_unlink = "Desligar" +this_month_title = "Este Mês — Vista Conjunta" +combined_income = "Rendimento Conjunto" +my_income = "O Meu Rendimento" +partner_income = "Rendimento do Parceiro" +combined_expenses = "Despesas Conjuntas" +disposable = "Disponível" +goals_title = "Objetivos" +your_goals = "OS MEUS OBJETIVOS" +partner_goals = "OBJETIVOS DO PARCEIRO" +no_goals = "Sem objetivos" +committed_badge = "comprometido" +confirm_unlink = "Desligar agregado? Apenas remove a ligação, não os dados." + +# ── Planeador de Objetivos (plan.html — página autónoma) ───────────────────── + +[plan] +title = "Planeador de Objetivos" +scenario_title = "O seu cenário" +scenario_desc = "Modele qualquer transição em que detém um ativo com empréstimo, quer adquirir um novo e depois vender o antigo para financiar a transição." +label_current_asset = "Ativo atual (opcional)" +option_none_asset = "— nenhum selecionado —" +label_current_loan = "Empréstimo atual (opcional)" +option_none_loan = "— nenhum selecionado —" +label_dream_cost = "Custo do novo objetivo (€)" +placeholder_dream_cost = "ex: 350000" +label_down_pct = "Entrada (%)" +label_loan_rate = "Taxa do novo empréstimo (% anual)" +label_loan_term = "Prazo do novo empréstimo (anos)" +placeholder_loan_term = "30" +label_build_months = "Período de aquisição / construção (meses)" +label_monthly_savings = "Poupança mensal disponível (€)" +placeholder_savings = "ex: 800" +label_sale_price = "Preço de venda esperado do ativo atual (€)" +placeholder_sale_price = "deixe em branco para usar o valor atual" +btn_run = "Executar simulação →" +result_total_timeline = "Linha temporal total" +result_until_paid = "até ao pagamento total do objetivo" +result_final_monthly = "Custo mensal final" +result_after_selling = "após venda do ativo atual" +result_total_interest = "Juros totais" +result_across_both = "em ambos os empréstimos combinados" +result_free_by = "Livre em" +result_fully_paid = "totalmente pago" +roadmap_title = "O seu roteiro" +phase1_title = "Poupar entrada" +phase1_target = "Objetivo:" +phase1_already_have = "Já tem:" +phase1_still_need = "Ainda precisa:" +phase1_saving = "A poupar:" +phase1_ready = "Pronto já!" +phase1_equity_covers = "o capital cobre a entrada" +phase1_down_payment = "Entrada:" +phase1_your_equity = "O seu capital:" +phase2_title = "Adquirir / construir" +phase2_new_loan = "Novo empréstimo:" +phase2_existing_loan = "Empréstimo existente:" +phase2_new_emi = "Nova prestação:" +phase2_total_burden = "Encargo total:" +phase3_title = "Vender & transitar" +phase3_one_time = "Evento único" +phase3_after_acquisition = "após a conclusão da aquisição" +phase3_sale_price = "Preço de venda:" +phase3_pay_off = "Liquidar empréstimo:" +phase3_net_proceeds = "Receita líquida:" +phase3_applied = "Aplicado ao novo empréstimo" +phase4_title = "Objetivo atingido" +phase4_remaining_loan = "Empréstimo restante:" +phase4_monthly_payment = "Prestação mensal:" +phase4_just_new_loan = "apenas o novo empréstimo" +phase4_fully_paid = "Totalmente pago!" +phase4_sale_cleared = "receita da venda liquidou o empréstimo" +phase4_no_remaining = "Sem empréstimo restante!" +phase4_sale_covers = "A venda cobre tudo." +chart_title = "Custo mensal ao longo do tempo" +chart_subtitle = "o que paga em cada mês" +levers_title = "Alavancas principais" +lever1_title = "Poupar mais por mês" +lever1_desc = "Cada €100 extra/mês encurta a Fase 1 e aproxima a data de aquisição." +lever2_title = "Aumentar a entrada" +lever2_desc = "Uma entrada maior reduz o novo empréstimo e diminui o encargo duplo na Fase 2." +lever3_title = "Vender a um preço mais alto" +lever3_desc = "Cada euro extra da venda vai diretamente para reduzir o saldo do novo empréstimo." +lever4_title = "Negociar a taxa" +lever4_desc = "Mesmo 0,5% a menos no novo empréstimo poupa milhares ao longo do prazo total." +empty_title = "Planeie o seu próximo grande objetivo" +empty_desc = "Modele qualquer transição em que detém um ativo com empréstimo, quer adquirir algo novo e planeia vender o antigo para financiar — incluindo o período de duplo pagamento, a venda e a data final de liquidação." +empty_tip = "Dica: adicione primeiro o seu ativo atual e o empréstimo para que o planeador preencha os valores automaticamente." diff --git a/apps/finance/services/api/main/models.go b/apps/finance/services/api/main/models.go index 541ef8a..769209a 100644 --- a/apps/finance/services/api/main/models.go +++ b/apps/finance/services/api/main/models.go @@ -121,6 +121,7 @@ type RecurringExpense struct { } type DashboardData struct { + T *Translator UserID string Email string Title string @@ -173,6 +174,7 @@ type BalancePoint struct { } type ReportData struct { + T *Translator UserID string Email string Title string @@ -188,6 +190,7 @@ type MonthlyCategorySummary struct { } type ProjectionData struct { + T *Translator UserID string Email string Title string @@ -198,6 +201,7 @@ type ProjectionData struct { } type PortfolioData struct { + T *Translator UserID string Email string Title string @@ -213,6 +217,7 @@ type PortfolioData struct { } type SharingData struct { + T *Translator UserID string Email string Title string @@ -244,6 +249,7 @@ type CapitalGainEntry struct { } type TaxData struct { + T *Translator UserID string Email string Title string @@ -275,6 +281,7 @@ type Household struct { // PeopleData combines Sharing and Household into a single page. type PeopleData struct { + T *Translator UserID string Email string Title string @@ -302,6 +309,7 @@ type PeopleData struct { // SettingsData combines Accounts and Categories into a single page. type SettingsData struct { + T *Translator UserID string Email string Title string @@ -312,6 +320,7 @@ type SettingsData struct { } type HouseholdData struct { + T *Translator UserID string Email string Title string @@ -348,6 +357,7 @@ type ImportSchedule struct { } type AutoImportData struct { + T *Translator UserID string Email string Title string @@ -377,6 +387,7 @@ type SimulatorGoal struct { } type SimulatorData struct { + T *Translator UserID string Email string Title string @@ -409,6 +420,7 @@ type NetWorthPoint struct { } type NetWorthData struct { + T *Translator UserID string Email string Title string @@ -463,6 +475,7 @@ type GoalPlan struct { } type GoalsData struct { + T *Translator UserID string Email string Title string diff --git a/apps/finance/services/api/main/models_property.go b/apps/finance/services/api/main/models_property.go index 090a4da..c2247dc 100644 --- a/apps/finance/services/api/main/models_property.go +++ b/apps/finance/services/api/main/models_property.go @@ -80,6 +80,7 @@ type PropertyView struct { } type PropertyData struct { + T *Translator UserID string Email string Title string diff --git a/apps/finance/services/api/main/templates/accounts.html b/apps/finance/services/api/main/templates/accounts.html index 8aac44c..0b70051 100644 --- a/apps/finance/services/api/main/templates/accounts.html +++ b/apps/finance/services/api/main/templates/accounts.html @@ -1,24 +1,24 @@ {{define "content"}} {{$d := .}} -

Accounts

+

{{$d.T.Get "accounts.title"}}

-

Add Account

+

{{$d.T.Get "accounts.add_title"}}

- - + +
- +
- +
@@ -27,8 +27,8 @@ - - + + @@ -46,12 +46,12 @@ {{else}} - + {{end}} @@ -60,8 +60,9 @@
NameType{{$d.T.Get "accounts.col_name"}}{{$d.T.Get "accounts.col_type"}}
- +
No accounts yet.{{$d.T.Get "accounts.empty_msg"}}