Gonçalo Rodrigues 4b7c01e632 feat(finance): i18n — TOML-based translations for all personal finance templates
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 <noreply@anthropic.com>
2026-06-17 22:32:49 +01:00

171 lines
7.1 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "content"}}
{{$d := .}}
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:8px;">
<h1>{{$d.T.Get "networth.title"}}</h1>
<span class="text-muted">{{if $d.Email}}{{$d.Email}}{{end}}</span>
</div>
<!-- Hero -->
<div class="card animate-on-scroll" style="margin-bottom:16px; padding:24px 28px;">
<div style="font-size:12px; color:var(--text2); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">
{{$d.T.Get "networth.hero_label"}}
<span style="font-size:11px; background:var(--bg3); color:var(--text3); padding:2px 8px; border-radius:99px; font-weight:400; text-transform:none; letter-spacing:0;">
{{$d.T.Get "networth.hero_formula"}}
</span>
</div>
<div class="animate-counter {{if lt $d.NetWorthCents 0}}negative{{else}}positive{{end}}"
style="font-size:42px; font-weight:500; letter-spacing:-1.5px; line-height:1;"
data-target="{{$d.NetWorthCents}}" data-prefix="€">€0.00</div>
<div style="margin-top:14px; display:flex; flex-wrap:wrap; gap:18px; font-size:13px; color:var(--text3);">
<span>{{$d.T.Get "networth.cash_label"}} <strong style="color:var(--text2);">€{{cents $d.CashCents}}</strong></span>
<span>{{$d.T.Get "networth.portfolio_label"}} <strong style="color:var(--text2);">€{{cents $d.PortfolioCents}}</strong></span>
{{if $d.PropertyValueCents}}
<span>{{$d.T.Get "networth.property_equity_label"}} <strong style="color:var(--text2);">€{{cents $d.PropertyEquityCents}}</strong></span>
{{end}}
{{if $d.CreditCents}}
<span>{{$d.T.Get "networth.credit_label"}} <strong style="color:var(--red);">-€{{cents $d.CreditCents}}</strong></span>
{{end}}
</div>
</div>
<!-- Breakdown cards -->
<div class="grid" style="margin-bottom:16px;">
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "networth.cards.cash_balance"}}</h2>
<div class="value animate-counter {{if lt $d.CashCents 0}}negative{{else}}positive{{end}}"
data-target="{{$d.CashCents}}" data-prefix="€">€0.00</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;">{{$d.T.Get "networth.cards.cash_balance_sub"}}</p>
</div>
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "networth.cards.portfolio"}}{{if not $d.PortfolioPricesAvailable}} ({{$d.T.Get "networth.cards.portfolio_cost_basis"}}){{end}}</h2>
<div class="value animate-counter positive"
data-target="{{$d.PortfolioCents}}" data-prefix="€">€0.00</div>
{{if $d.PortfolioPricesAvailable}}
<p style="font-size:12px; color:var(--text3); margin-top:6px;">{{$d.T.Get "networth.cards.portfolio_market"}}</p>
{{else}}
<p style="font-size:12px; color:var(--text3); margin-top:6px;">{{$d.T.Get "networth.cards.portfolio_cost_shown"}}</p>
{{end}}
</div>
{{if $d.PropertyValueCents}}
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "networth.cards.property_equity"}}</h2>
<div class="value animate-counter {{if lt $d.PropertyEquityCents 0}}negative{{else}}positive{{end}}"
data-target="{{$d.PropertyEquityCents}}" data-prefix="€">€0.00</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;">
€{{cents $d.PropertyValueCents}} value €{{cents $d.LoanBalanceCents}} loans
</p>
</div>
{{end}}
{{if $d.CreditCents}}
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "networth.cards.credit_liabilities"}}</h2>
<div class="value negative animate-counter" data-target="{{$d.CreditCents}}" data-prefix="€">€0.00</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;">{{$d.T.Get "networth.cards.outstanding_balance"}}</p>
</div>
{{end}}
</div>
<!-- Historical chart -->
{{if $d.History}}
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>{{$d.T.Get "networth.chart.section_title"}}</h2>
<span style="font-size:11px; color:var(--text3);">{{$d.T.Get "networth.chart.subtitle"}}</span>
</div>
<canvas id="nw-chart" height="220"></canvas>
</div>
<script>
(function() {
const labels = [{{range $d.History}}"{{.Month}}",{{end}}];
const netData = [{{range $d.History}}{{.NetCents}},{{end}}];
const cashData = [{{range $d.History}}{{.AssetCents}},{{end}}]; // assets (cash+portfolio+property)
const liabData = [{{range $d.History}}{{.LiabCents}},{{end}}]; // loan balances
const hasProperty = {{if $d.PropertyValueCents}}true{{else}}false{{end}};
const ctx = document.getElementById('nw-chart').getContext('2d');
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const style = getComputedStyle(document.documentElement);
const accent = style.getPropertyValue('--accent').trim() || '#6979f8';
const green = style.getPropertyValue('--green').trim() || '#4ade80';
const red = style.getPropertyValue('--red').trim() || '#f87171';
const teal = '#14b8a6';
const pts = netData.length > 24 ? 0 : 3;
const datasets = [{
label: '{{$d.T.Get "networth.chart.legend_net_worth"}}',
data: netData.map(v => v / 100),
borderColor: accent,
backgroundColor: isDark ? 'rgba(105,121,248,0.07)' : 'rgba(67,85,232,0.05)',
fill: true,
tension: 0.35,
pointRadius: pts,
pointHoverRadius: 5,
borderWidth: 2.5,
order: 0,
}];
if (hasProperty) {
datasets.push({
label: '{{$d.T.Get "networth.chart.legend_loans"}}',
data: liabData.map(v => -(v / 100)),
borderColor: red,
backgroundColor: isDark ? 'rgba(248,113,113,0.06)' : 'rgba(239,68,68,0.04)',
fill: true,
tension: 0.35,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 1.5,
borderDash: [4, 3],
order: 1,
});
}
new Chart(ctx, {
type: 'line',
data: { labels, datasets },
options: {
responsive: true,
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { display: hasProperty, labels: { color: isDark ? '#8892b0' : '#64748b', boxWidth: 12, font: { size: 12 } } },
tooltip: {
callbacks: {
label: c => c.dataset.label + ': €' + Math.abs(c.parsed.y).toLocaleString('pt-PT', {minimumFractionDigits:2})
}
}
},
scales: {
x: {
grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.05)' },
ticks: { color: isDark ? '#5c6585' : '#9fa8c7', maxTicksLimit: 12 }
},
y: {
grid: { color: isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.05)' },
ticks: {
color: isDark ? '#5c6585' : '#9fa8c7',
callback: v => '€' + (v / 1000).toFixed(0) + 'k'
}
}
}
}
});
})();
</script>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:48px; margin-bottom:16px;">📊</div>
<h3>{{$d.T.Get "networth.empty.title"}}</h3>
<p style="margin-bottom:20px;">{{$d.T.Get "networth.empty.desc"}}</p>
<a href="/import" class="btn btn-primary">{{$d.T.Get "networth.empty.btn_import"}}</a>
</div>
{{end}}
{{end}}