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

193 lines
9.5 KiB
HTML

{{template "base" .}}
{{define "content"}}
{{$d := .}}
<style>
.tab-bar { display:flex; gap:4px; border-bottom:2px solid var(--border); margin-bottom:24px; }
.tab-bar a { padding:8px 18px; font-size:0.9rem; font-weight:600; color:var(--muted); text-decoration:none; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all 0.15s; }
.tab-bar a.active { color:var(--accent); border-bottom-color:var(--accent); }
.tab-bar a:hover:not(.active) { color:var(--text); }
.s-card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:22px 24px; margin-bottom:16px; }
.s-card h3 { margin:0 0 14px; font-size:0.95rem; font-weight:700; }
.form-row { display:flex; gap:10px; flex-wrap:wrap; }
.form-row input, .form-row select { flex:1; min-width:120px; padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text); font-size:0.875rem; }
.btn { padding:8px 18px; border:none; border-radius:8px; font-size:0.875rem; font-weight:600; cursor:pointer; }
.btn-primary { background:var(--accent); color:#fff; }
.btn-danger { background:transparent; border:1px solid #f4433660; color:#f44336; font-size:0.8rem; padding:4px 10px; border-radius:6px; }
.btn-danger:hover { background:#f4433615; }
table { width:100%; border-collapse:collapse; font-size:0.875rem; }
th { text-align:left; padding:8px 12px; color:var(--muted); font-weight:600; border-bottom:2px solid var(--border); }
td { padding:10px 12px; border-bottom:1px solid var(--border); vertical-align:middle; }
tr:last-child td { border-bottom:none; }
.color-dot { display:inline-block; width:12px; height:12px; border-radius:50%; margin-right:6px; vertical-align:middle; }
.type-badge { display:inline-block; padding:2px 8px; border-radius:20px; font-size:0.72rem; font-weight:600; background:var(--bg); border:1px solid var(--border); color:var(--muted); }
.empty-state { padding:32px; text-align:center; color:var(--muted); font-size:0.875rem; }
</style>
<h1 style="margin:0 0 20px;">{{$d.T.Get "settings.title"}}</h1>
<div class="tab-bar">
<a href="/settings?tab=accounts" class="{{if eq $d.Tab "accounts"}}active{{end}}">{{$d.T.Get "settings.tab_accounts"}}</a>
<a href="/settings?tab=categories" class="{{if eq $d.Tab "categories"}}active{{end}}">{{$d.T.Get "settings.tab_categories"}}</a>
</div>
{{if eq $d.Tab "accounts"}}
<div class="s-card">
<h3>{{$d.T.Get "settings.accounts.card_add_title"}}</h3>
<form method="post" action="/accounts" id="add-account-form">
<div class="form-row">
<input type="text" name="name" placeholder="{{$d.T.Get "settings.accounts.placeholder_name"}}" required>
<select name="type">
<option value="checking">{{$d.T.Get "settings.accounts.type_checking"}}</option>
<option value="savings">{{$d.T.Get "settings.accounts.type_savings"}}</option>
<option value="credit">{{$d.T.Get "settings.accounts.type_credit"}}</option>
<option value="securities">{{$d.T.Get "settings.accounts.type_securities"}}</option>
</select>
<button type="submit" class="btn btn-primary">{{$d.T.Get "settings.accounts.btn_add"}}</button>
</div>
</form>
</div>
<div class="s-card">
{{if $d.Accounts}}
<table>
<thead><tr><th>{{$d.T.Get "settings.accounts.col_name"}}</th><th>{{$d.T.Get "settings.accounts.col_type"}}</th><th></th></tr></thead>
<tbody>
{{range $d.Accounts}}
<tr>
<td style="font-weight:500;">{{.Name}}</td>
<td><span class="type-badge">{{.Type}}</span></td>
<td style="text-align:right;">
<button class="btn btn-danger" onclick="deleteAccount('{{.ID}}')">{{$d.T.Get "settings.accounts.btn_delete"}}</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-state">{{$d.T.Get "settings.accounts.empty_msg"}}</div>
{{end}}
</div>
{{else}}{{/* categories tab */}}
<div class="s-card">
<h3>{{$d.T.Get "settings.categories.card_add_title"}}</h3>
<form method="post" action="/categories" id="add-cat-form">
<div class="form-row">
<input type="text" name="name" placeholder="{{$d.T.Get "settings.categories.placeholder_name"}}" required>
<input type="number" name="budget_cents" placeholder="{{$d.T.Get "settings.categories.placeholder_budget"}}" min="0">
<input type="color" name="color" value="#6366f1" style="flex:0; width:44px; padding:4px; border-radius:8px; cursor:pointer;">
<button type="submit" class="btn btn-primary">{{$d.T.Get "settings.categories.btn_add"}}</button>
</div>
</form>
</div>
<div class="s-card">
{{if $d.Categories}}
<table>
<thead><tr><th>{{$d.T.Get "settings.categories.col_category"}}</th><th style="text-align:right;">{{$d.T.Get "settings.categories.col_budget"}}</th><th></th></tr></thead>
<tbody>
{{range $d.Categories}}
<tr id="cat-row-{{.ID}}">
<td>
<span class="color-dot" style="background:{{if .Color}}{{.Color}}{{else}}#9e9e9e{{end}};"></span>
<span id="cat-name-{{.ID}}">{{.Name}}</span>
</td>
<td style="text-align:right;">
{{if .BudgetCents}}
<span id="cat-budget-{{.ID}}">€{{cents .BudgetCents}}</span>
{{else}}
<span id="cat-budget-{{.ID}}" style="color:var(--muted);"></span>
{{end}}
<button onclick="editCat('{{.ID}}','{{.Name}}',{{.BudgetCents}},'{{.Color}}')"
style="margin-left:8px; background:none; border:none; cursor:pointer; color:var(--muted); font-size:0.85rem;"></button>
</td>
<td style="text-align:right;">
<button class="btn btn-danger" onclick="deleteCat('{{.ID}}')">{{$d.T.Get "settings.categories.btn_delete"}}</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-state">{{$d.T.Get "settings.categories.empty_msg"}}</div>
{{end}}
</div>
{{end}}
<!-- Edit category modal -->
<div id="edit-cat-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter:blur(4px); z-index:300; align-items:center; justify-content:center;">
<div class="s-card" style="width:380px; max-width:95vw; margin:0; box-shadow:var(--shadow-lg);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<span style="font-weight:700;">{{$d.T.Get "settings.categories.modal_edit.title"}}</span>
<button onclick="closeEditCat()" style="background:none; border:none; cursor:pointer; color:var(--muted); font-size:1.1rem;"></button>
</div>
<div style="display:flex; flex-direction:column; gap:10px;">
<input id="edit-cat-name" type="text" placeholder="{{$d.T.Get "settings.categories.modal_edit.placeholder_name"}}" style="padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text);">
<input id="edit-cat-budget" type="number" placeholder="{{$d.T.Get "settings.categories.modal_edit.placeholder_budget"}}" min="0" style="padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text);">
<input id="edit-cat-color" type="color" style="width:100%; height:38px; padding:4px; border:1px solid var(--border); border-radius:8px; cursor:pointer;">
</div>
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:16px;">
<button onclick="closeEditCat()" class="btn" style="background:var(--bg); border:1px solid var(--border); color:var(--text);">{{$d.T.Get "settings.categories.modal_edit.btn_cancel"}}</button>
<button onclick="saveEditCat()" class="btn btn-primary">{{$d.T.Get "settings.categories.modal_edit.btn_save"}}</button>
</div>
</div>
</div>
<script>
const CONFIRM_DELETE_ACCOUNT = {{$d.T.Get "settings.accounts.confirm_delete" | printf "%q"}};
const CONFIRM_DELETE_CAT = {{$d.T.Get "settings.categories.confirm_delete" | printf "%q"}};
let editingCatID = null;
function deleteAccount(id) {
if (!confirm(CONFIRM_DELETE_ACCOUNT)) return;
fetch('/accounts/' + id, { method: 'DELETE' })
.then(r => { if (r.ok) location.reload(); });
}
function deleteCat(id) {
if (!confirm(CONFIRM_DELETE_CAT)) return;
fetch('/categories/' + id, { method: 'DELETE' })
.then(r => { if (r.ok) location.reload(); });
}
function editCat(id, name, budgetCents, color) {
editingCatID = id;
document.getElementById('edit-cat-name').value = name;
document.getElementById('edit-cat-budget').value = budgetCents || '';
document.getElementById('edit-cat-color').value = color || '#6366f1';
document.getElementById('edit-cat-modal').style.display = 'flex';
}
function closeEditCat() {
document.getElementById('edit-cat-modal').style.display = 'none';
editingCatID = null;
}
function saveEditCat() {
if (!editingCatID) return;
const body = new URLSearchParams({
name: document.getElementById('edit-cat-name').value,
budget_cents: document.getElementById('edit-cat-budget').value || '0',
color: document.getElementById('edit-cat-color').value,
});
fetch('/categories/' + editingCatID, { method: 'PUT', body })
.then(r => { if (r.ok) { closeEditCat(); location.reload(); } });
}
document.getElementById('add-account-form')?.addEventListener('submit', function(e) {
e.preventDefault();
fetch(this.action, { method: 'POST', body: new FormData(this) })
.then(r => { if (r.ok || r.redirected) location.reload(); });
});
document.getElementById('add-cat-form')?.addEventListener('submit', function(e) {
e.preventDefault();
fetch(this.action, { method: 'POST', body: new FormData(this) })
.then(r => { if (r.ok || r.redirected) location.reload(); });
});
</script>
{{end}}