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

468 lines
23 KiB
HTML

{{template "base.html" .}}
{{define "content"}}
{{$d := .}}
<style>
.prop-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(340px,1fr)); gap:16px; }
.prop-card {
background:var(--surface); border:1px solid var(--border);
border-radius:var(--radius); padding:22px; position:relative;
transition:border-color 0.18s;
}
.prop-card:hover { border-color:var(--border2); }
.prop-badge {
display:inline-block; font-size:11px; font-weight:600; padding:2px 8px;
border-radius:20px; margin-bottom:12px; text-transform:uppercase; letter-spacing:.5px;
}
.badge-owned { background:rgba(20,184,166,.12); color:#14b8a6; border:1px solid rgba(20,184,166,.25); }
.badge-building { background:rgba(245,158,11,.12); color:#f59e0b; border:1px solid rgba(245,158,11,.25); }
.badge-sold { background:rgba(148,163,184,.1); color:var(--text3); border:1px solid var(--border); }
.prop-name { font-size:17px; font-weight:700; margin:0 0 2px; color:var(--text); }
.prop-addr { font-size:12px; color:var(--text3); margin:0 0 16px; }
.prop-stats { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:16px; }
.prop-stat { background:var(--surface2); border-radius:8px; padding:10px 12px; }
.prop-stat-label { font-size:11px; color:var(--text3); margin-bottom:3px; }
.prop-stat-value { font-size:15px; font-weight:700; color:var(--text); }
.prop-stat-value.positive { color:var(--green); }
.prop-stat-value.negative { color:var(--red); }
.equity-bar-wrap { margin-bottom:16px; }
.equity-bar-label { display:flex; justify-content:space-between; font-size:12px; color:var(--text3); margin-bottom:5px; }
.equity-bar { height:6px; background:var(--surface2); border-radius:3px; overflow:hidden; }
.equity-bar-fill { height:100%; border-radius:3px; background:linear-gradient(90deg,#14b8a6,#6366f1); transition:width .6s ease; }
.loan-mini {
background:var(--surface2); border-radius:8px; padding:12px 14px;
font-size:12px; color:var(--text2); margin-bottom:12px;
}
.loan-mini-title { font-weight:600; color:var(--text); margin-bottom:6px; font-size:13px; }
.loan-mini-row { display:flex; justify-content:space-between; margin-bottom:3px; }
.loan-progress { height:4px; background:var(--surface); border-radius:2px; margin-top:8px; overflow:hidden; }
.loan-progress-fill { height:100%; background:var(--accent2); border-radius:2px; }
.prop-actions { display:flex; gap:8px; flex-wrap:wrap; }
.loan-card {
background:var(--surface); border:1px solid var(--border);
border-radius:var(--radius); padding:20px; transition:border-color .18s;
}
.loan-card:hover { border-color:var(--border2); }
.loan-type-badge {
display:inline-block; font-size:11px; font-weight:600; padding:2px 8px;
border-radius:20px; margin-bottom:10px; text-transform:capitalize;
background:rgba(99,102,241,.1); color:#6366f1; border:1px solid rgba(99,102,241,.2);
}
/* summary cards */
.summary-row { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:28px; }
@media(max-width:600px){ .summary-row{grid-template-columns:1fr;} .prop-grid{grid-template-columns:1fr;} }
/* section header */
.section-header { display:flex; align-items:center; justify-content:space-between; margin:28px 0 14px; }
.section-title { font-size:16px; font-weight:700; color:var(--text); }
/* modal */
.modal-overlay {
display:none; position:fixed; inset:0; z-index:1000;
background:rgba(0,0,0,.6); backdrop-filter:blur(4px);
align-items:center; justify-content:center; padding:16px;
}
.modal-box {
background:var(--surface); border:1px solid var(--border2);
border-radius:var(--radius); padding:28px; width:100%; max-width:480px;
max-height:90vh; overflow-y:auto;
}
.modal-title { font-size:17px; font-weight:700; margin:0 0 20px; }
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media(max-width:500px){ .form-row{grid-template-columns:1fr;} }
.form-group { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
.form-group label { font-size:12px; color:var(--text3); font-weight:500; }
.form-group input, .form-group select, .form-group textarea {
background:var(--surface2); border:1px solid var(--border);
border-radius:8px; padding:9px 12px; color:var(--text);
font-size:14px; outline:none; transition:border-color .18s; width:100%; box-sizing:border-box;
}
.form-group input:focus, .form-group select:focus { border-color:var(--accent2); }
.form-hint { font-size:11px; color:var(--text3); margin-top:2px; }
.modal-footer { display:flex; gap:10px; justify-content:flex-end; margin-top:8px; }
</style>
<!-- ── Summary ── -->
<div class="summary-row">
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "property.summary_total_value"}}</h2>
<div class="value animate-counter" data-target="{{$d.TotalPropertyValueCents}}" data-prefix="€">€0</div>
<p style="font-size:12px;color:var(--text3);margin-top:4px;">{{$d.T.Get "property.summary_total_value_sub"}}</p>
</div>
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "property.summary_outstanding_loans"}}</h2>
<div class="value animate-counter" data-target="{{$d.TotalLoanBalanceCents}}" data-prefix="€" style="color:var(--red);">€0</div>
<p style="font-size:12px;color:var(--text3);margin-top:4px;">{{$d.T.Get "property.summary_outstanding_sub"}}</p>
</div>
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "property.summary_net_equity"}}</h2>
<div class="value animate-counter" data-target="{{$d.TotalEquityCents}}" data-prefix="€"
style="color:{{if ge $d.TotalEquityCents 0}}var(--green){{else}}var(--red){{end}};">€0</div>
<p style="font-size:12px;color:var(--text3);margin-top:4px;">{{$d.T.Get "property.summary_net_equity_sub"}}</p>
</div>
</div>
<!-- ── Properties ── -->
<div class="section-header">
<span class="section-title">{{$d.T.Get "property.properties.section_title"}}</span>
<button class="btn btn-primary btn-sm" onclick="openModal('add-property-modal')">{{$d.T.Get "property.properties.btn_add"}}</button>
</div>
{{if $d.Properties}}
<div class="prop-grid">
{{range $d.Properties}}
<div class="prop-card animate-on-scroll">
<span class="prop-badge badge-{{.Status}}">{{.StatusLabel}}</span>
<p class="prop-name">{{.Name}}</p>
{{if .Address}}<p class="prop-addr">📍 {{.Address}}</p>{{end}}
<div class="prop-stats">
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.properties.stat_current_value"}}</div>
<div class="prop-stat-value">€{{cents .CurrentValueCents}}</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.properties.stat_equity"}}</div>
<div class="prop-stat-value {{if ge .EquityCents 0}}positive{{else}}negative{{end}}">
€{{cents .EquityCents}}
</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.properties.stat_purchase_price"}}</div>
<div class="prop-stat-value">€{{cents .PurchasePriceCents}}</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.properties.stat_gain"}}</div>
<div class="prop-stat-value {{if ge .GainCents 0}}positive{{else}}negative{{end}}">
{{printf "%.1f" .GainPct}}%
</div>
</div>
</div>
{{if .LinkedLoan}}
<div class="equity-bar-wrap">
<div class="equity-bar-label">
<span>{{$d.T.Get "property.properties.stat_equity"}} {{.EquityPct}}%</span>
<span>{{$d.T.Get "property.loans.stat_balance"}} {{.LoanPct}}%</span>
</div>
<div class="equity-bar">
<div class="equity-bar-fill" style="width:{{.EquityPct}}%;"></div>
</div>
</div>
<div class="loan-mini">
<div class="loan-mini-title">🏦 {{.LinkedLoan.Name}}</div>
<div class="loan-mini-row"><span>{{$d.T.Get "property.properties.loan_remaining"}}</span><span style="font-weight:600;color:var(--red);">€{{cents .LinkedLoan.BalanceCents}}</span></div>
<div class="loan-mini-row"><span>{{$d.T.Get "property.properties.loan_monthly_payment"}}</span><span>€{{cents .LinkedLoan.EffectiveMonthlyPaymentCents}}</span></div>
<div class="loan-mini-row"><span>{{$d.T.Get "property.properties.loan_payoff"}}</span><span>{{.LinkedLoan.PayoffDate.Format "Jan 2006"}}</span></div>
<div class="loan-mini-row"><span>{{$d.T.Get "property.properties.loan_rate"}}</span><span>{{printf "%.2f" .LinkedLoan.InterestRatePct}}%</span></div>
<div class="loan-progress">
<div class="loan-progress-fill" style="width:{{.LinkedLoan.PaidPct}}%;"></div>
</div>
</div>
{{end}}
<div class="prop-actions">
<button class="btn btn-outline btn-sm"
onclick="openEditProperty('{{.ID}}','{{.Name}}','{{.Address}}',{{.PurchasePriceCents}},{{.CurrentValueCents}},{{printf "%.2f" .AppreciationPct}},'{{.PurchaseDate.Format "2006-01-02"}}','{{.Status}}','{{.Notes}}')">
{{$d.T.Get "property.properties.btn_edit"}}
</button>
<form method="POST" action="/property" style="display:inline;">
<input type="hidden" name="action" value="delete_property">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--red);border-color:var(--red)33;"
onclick="return confirm('Remove {{.Name}}?')">{{$d.T.Get "property.properties.btn_remove"}}</button>
</form>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:48px;margin-bottom:16px;">🏠</div>
<h3>{{$d.T.Get "property.properties.empty.title"}}</h3>
<p style="margin-bottom:20px;">{{$d.T.Get "property.properties.empty.desc"}}</p>
<button class="btn btn-primary" onclick="openModal('add-property-modal')">{{$d.T.Get "property.properties.empty.btn_add_first"}}</button>
</div>
{{end}}
<!-- ── Standalone loans ── -->
{{if or $d.UnlinkedLoans (not $d.Properties)}}
<div class="section-header" style="margin-top:32px;">
<span class="section-title">{{$d.T.Get "property.loans.section_title"}}</span>
<button class="btn btn-outline btn-sm" onclick="openModal('add-loan-modal')">{{$d.T.Get "property.loans.btn_add"}}</button>
</div>
{{end}}
{{if $d.UnlinkedLoans}}
<div class="prop-grid">
{{range $d.UnlinkedLoans}}
<div class="loan-card animate-on-scroll">
<span class="loan-type-badge">{{.Type}}</span>
<p class="prop-name" style="margin-bottom:4px;">{{.Name}}</p>
<div class="prop-stats" style="margin-top:12px;">
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.loans.stat_balance"}}</div>
<div class="prop-stat-value negative">€{{cents .BalanceCents}}</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.loans.stat_monthly"}}</div>
<div class="prop-stat-value">€{{cents .EffectiveMonthlyPaymentCents}}</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.loans.stat_payoff"}}</div>
<div class="prop-stat-value">{{.PayoffDate.Format "Jan 2006"}}</div>
</div>
<div class="prop-stat">
<div class="prop-stat-label">{{$d.T.Get "property.loans.stat_rate"}}</div>
<div class="prop-stat-value">{{printf "%.2f" .InterestRatePct}}%</div>
</div>
</div>
<div class="equity-bar-wrap">
<div class="equity-bar-label">
<span>{{$d.T.Get "property.loans.stat_monthly"}} {{.PaidPct}}%</span>
<span>€{{cents .TotalRemainingInterestCents}} {{$d.T.Get "property.loans.interest_left"}}</span>
</div>
<div class="equity-bar">
<div class="equity-bar-fill" style="width:{{.PaidPct}}%;"></div>
</div>
</div>
<div class="prop-actions">
<form method="POST" action="/property" style="display:inline;">
<input type="hidden" name="action" value="payoff_loan">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--green);border-color:var(--green)33;"
onclick="return confirm('{{$d.T.Get "property.loans.confirm_paid_off"}}')">{{$d.T.Get "property.loans.btn_mark_paid"}}</button>
</form>
<form method="POST" action="/property" style="display:inline;">
<input type="hidden" name="action" value="delete_loan">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--red);border-color:var(--red)33;"
onclick="return confirm('Remove {{.Name}}?')">{{$d.T.Get "property.loans.btn_remove"}}</button>
</form>
</div>
</div>
{{end}}
</div>
{{end}}
{{if $d.Properties}}
<div style="margin-top:24px;">
<button class="btn btn-outline btn-sm" onclick="openModal('add-loan-modal')">{{$d.T.Get "property.loans.btn_add"}}</button>
</div>
{{end}}
<!-- ── Add Property Modal ── -->
<div id="add-property-modal" class="modal-overlay" onclick="if(event.target===this)closeModal('add-property-modal')">
<div class="modal-box">
<p class="modal-title">{{$d.T.Get "property.modal_add.title_property"}}</p>
<form method="POST" action="/property">
<input type="hidden" name="action" value="add_property">
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_name"}}</label>
<input type="text" name="name" placeholder="{{$d.T.Get "property.modal_add.placeholder_name"}}" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_address"}}</label>
<input type="text" name="address" placeholder="{{$d.T.Get "property.modal_add.placeholder_address"}}">
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_purchase_price"}}</label>
<input type="number" name="purchase_price" placeholder="{{$d.T.Get "property.modal_add.placeholder_purchase_price"}}" step="0.01" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_current_value"}}</label>
<input type="number" name="current_value" placeholder="{{$d.T.Get "property.modal_add.placeholder_current_value"}}" step="0.01">
<span class="form-hint">{{$d.T.Get "property.modal_add.hint_current_value"}}</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_purchase_date"}}</label>
<input type="date" name="purchase_date">
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_appreciation"}}</label>
<input type="number" name="appreciation_pct" placeholder="{{$d.T.Get "property.modal_add.placeholder_appreciation"}}" step="0.1" value="2.0">
</div>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_status"}}</label>
<select name="status">
<option value="owned">{{$d.T.Get "property.modal_add.status_owned"}}</option>
<option value="building">{{$d.T.Get "property.modal_add.status_building"}}</option>
<option value="sold">{{$d.T.Get "property.modal_add.status_sold"}}</option>
</select>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_add.label_notes"}}</label>
<textarea name="notes" rows="2" placeholder="{{$d.T.Get "property.modal_add.placeholder_notes"}}"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('add-property-modal')">{{$d.T.Get "property.modal_add.btn_cancel"}}</button>
<button type="submit" class="btn btn-primary">{{$d.T.Get "property.modal_add.btn_add"}}</button>
</div>
</form>
</div>
</div>
<!-- ── Edit Property Modal ── -->
<div id="edit-property-modal" class="modal-overlay" onclick="if(event.target===this)closeModal('edit-property-modal')">
<div class="modal-box">
<p class="modal-title">{{$d.T.Get "property.modal_edit.title_property"}}</p>
<form method="POST" action="/property" id="edit-property-form">
<input type="hidden" name="action" value="update_property">
<input type="hidden" name="id" id="edit-prop-id">
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_name"}}</label>
<input type="text" name="name" id="edit-prop-name" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_address"}}</label>
<input type="text" name="address" id="edit-prop-addr">
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_purchase_price"}}</label>
<input type="number" name="purchase_price" id="edit-prop-purchase" step="0.01">
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_current_value"}}</label>
<input type="number" name="current_value" id="edit-prop-value" step="0.01">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_purchase_date"}}</label>
<input type="date" name="purchase_date" id="edit-prop-date">
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_appreciation"}}</label>
<input type="number" name="appreciation_pct" id="edit-prop-appr" step="0.1">
</div>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_status"}}</label>
<select name="status" id="edit-prop-status">
<option value="owned">{{$d.T.Get "property.modal_edit.status_owned"}}</option>
<option value="building">{{$d.T.Get "property.modal_edit.status_building"}}</option>
<option value="sold">{{$d.T.Get "property.modal_edit.status_sold"}}</option>
</select>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_edit.label_notes"}}</label>
<textarea name="notes" id="edit-prop-notes" rows="2"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('edit-property-modal')">{{$d.T.Get "property.modal_edit.btn_cancel"}}</button>
<button type="submit" class="btn btn-primary">{{$d.T.Get "property.modal_edit.btn_save"}}</button>
</div>
</form>
</div>
</div>
<!-- ── Add Loan Modal ── -->
<div id="add-loan-modal" class="modal-overlay" onclick="if(event.target===this)closeModal('add-loan-modal')">
<div class="modal-box">
<p class="modal-title">{{$d.T.Get "property.modal_loan.title_loan"}}</p>
<form method="POST" action="/property">
<input type="hidden" name="action" value="add_loan">
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_name"}}</label>
<input type="text" name="name" placeholder="{{$d.T.Get "property.modal_loan.placeholder_name"}}" required>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_type"}}</label>
<select name="loan_type">
<option value="mortgage">{{$d.T.Get "property.modal_loan.type_mortgage"}}</option>
<option value="construction">{{$d.T.Get "property.modal_loan.type_construction"}}</option>
<option value="personal">{{$d.T.Get "property.modal_loan.type_personal"}}</option>
</select>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_linked_property"}}</label>
<select name="property_id">
<option value="">{{$d.T.Get "property.modal_loan.option_none_property"}}</option>
{{range $d.Properties}}
<option value="{{.ID}}">{{.Name}}</option>
{{end}}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_principal"}}</label>
<input type="number" name="principal" placeholder="{{$d.T.Get "property.modal_loan.placeholder_principal"}}" step="0.01" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_balance"}}</label>
<input type="number" name="balance" placeholder="{{$d.T.Get "property.modal_loan.placeholder_balance"}}" step="0.01">
<span class="form-hint">{{$d.T.Get "property.modal_loan.hint_balance"}}</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_interest_rate"}}</label>
<input type="number" name="interest_rate" placeholder="{{$d.T.Get "property.modal_loan.placeholder_rate"}}" step="0.01" required>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_term"}}</label>
<input type="number" name="term_months" placeholder="{{$d.T.Get "property.modal_loan.placeholder_term"}}" required>
<span class="form-hint">{{$d.T.Get "property.modal_loan.hint_term"}}</span>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_monthly_payment"}}</label>
<input type="number" name="monthly_payment" placeholder="{{$d.T.Get "property.modal_loan.placeholder_monthly_payment"}}" step="0.01">
<span class="form-hint">{{$d.T.Get "property.modal_loan.hint_monthly_payment"}}</span>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_start_date"}}</label>
<input type="date" name="start_date">
</div>
</div>
<div class="form-group">
<label>{{$d.T.Get "property.modal_loan.label_notes"}}</label>
<textarea name="notes" rows="2" placeholder="{{$d.T.Get "property.modal_loan.placeholder_notes"}}"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeModal('add-loan-modal')">{{$d.T.Get "property.modal_loan.btn_cancel"}}</button>
<button type="submit" class="btn btn-primary">{{$d.T.Get "property.modal_loan.btn_add"}}</button>
</div>
</form>
</div>
</div>
<script>
function openModal(id) { document.getElementById(id).style.display = 'flex'; }
function closeModal(id) { document.getElementById(id).style.display = 'none'; }
function openEditProperty(id, name, addr, purchaseCents, valueCents, appr, date, status, notes) {
document.getElementById('edit-prop-id').value = id;
document.getElementById('edit-prop-name').value = name;
document.getElementById('edit-prop-addr').value = addr;
document.getElementById('edit-prop-purchase').value = (purchaseCents / 100).toFixed(2);
document.getElementById('edit-prop-value').value = (valueCents / 100).toFixed(2);
document.getElementById('edit-prop-appr').value = appr;
document.getElementById('edit-prop-date').value = date;
document.getElementById('edit-prop-status').value = status;
document.getElementById('edit-prop-notes').value = notes;
openModal('edit-property-modal');
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
['add-property-modal','edit-property-modal','add-loan-modal'].forEach(closeModal);
}
});
</script>
{{end}}