feat: add EN/DE internationalization to cospend section

Move cospend routes to parameterized [cospendRoot=cospendRoot] supporting
both /cospend (DE) and /expenses (EN). Add cospendI18n.ts with 100+
translation keys covering all pages, components, categories, frequency
descriptions, and error messages. Translate BarChart legend, ImageUpload,
UsersList, SplitMethodSelector, DebtBreakdown, EnhancedBalance, and
PaymentModal. Update LanguageSelector and hooks.server.ts for /expenses.
This commit is contained in:
2026-04-08 15:46:00 +02:00
parent b7c7b37c94
commit 9af36b0c14
35 changed files with 991 additions and 458 deletions
+29 -34
View File
@@ -4,14 +4,19 @@
import { page } from '$app/stores';
import ProfilePicture from './ProfilePicture.svelte';
import EditButton from '$lib/components/EditButton.svelte';
import { getCategoryEmoji, getCategoryName, PAYMENT_CATEGORIES } from '$lib/utils/categories';
import { getCategoryEmoji } from '$lib/utils/categories';
import { formatCurrency as formatCurrencyUtil } from '$lib/utils/formatters';
import { detectCospendLang, cospendRoot, t, locale, splitDescription, paymentCategoryName } from '$lib/js/cospendI18n';
let { paymentId, onclose, onpaymentDeleted } = $props();
// Get session from page store
let session = $derived($page.data?.session);
const lang = $derived(detectCospendLang($page.url.pathname));
const root = $derived(cospendRoot(lang));
const loc = $derived(locale(lang));
/**
* @typedef {{
* _id?: string,
@@ -76,7 +81,7 @@
function closeModal() {
// Use shallow routing to go back to dashboard without full navigation
goto('/cospend/dash', { replaceState: true, noScroll: true, keepFocus: true });
goto(`/${root}/dash`, { replaceState: true, noScroll: true, keepFocus: true });
onclose?.();
}
@@ -89,33 +94,23 @@
/** @param {number} amount */
function formatCurrency(amount) {
return formatCurrencyUtil(Math.abs(amount), 'CHF', 'de-CH');
return formatCurrencyUtil(Math.abs(amount), 'CHF', loc);
}
/** @param {string} dateString */
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('de-CH');
return new Date(dateString).toLocaleDateString(loc);
}
/** @param {PaymentData} payment */
function getSplitDescription(payment) {
if (!payment.splits || payment.splits.length === 0) return 'No splits';
if (payment.splitMethod === 'equal') {
return `Split equally among ${payment.splits.length} people`;
} else if (payment.splitMethod === 'full') {
return `Paid in full by ${payment.paidBy}`;
} else if (payment.splitMethod === 'personal_equal') {
return `Personal amounts + equal split among ${payment.splits.length} people`;
} else {
return `Custom split among ${payment.splits.length} people`;
}
return splitDescription(payment, lang);
}
let deleting = $state(false);
async function deletePayment() {
if (!confirm('Are you sure you want to delete this payment? This action cannot be undone.')) {
if (!confirm(t('delete_payment_confirm', lang))) {
return;
}
@@ -143,7 +138,7 @@
<div class="panel-content" bind:this={modal}>
<div class="panel-header">
<h2>Payment Details</h2>
<h2>{t('payment_details', lang)}</h2>
<button class="close-button" onclick={closeModal} aria-label="Close modal">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
@@ -154,9 +149,9 @@
<div class="panel-body">
{#if loading}
<div class="loading">Loading payment...</div>
<div class="loading">{t('loading_payments', lang)}</div>
{:else if error}
<div class="error">Error: {error}</div>
<div class="error">{t('error_prefix', lang)}: {error}</div>
{:else if payment}
<div class="payment-details">
<div class="payment-header">
@@ -171,7 +166,7 @@
</div>
{#if payment.image}
<div class="receipt-image">
<img src={payment.image} alt="Receipt" />
<img src={payment.image} alt={t('receipt', lang)} />
</div>
{/if}
</div>
@@ -179,30 +174,30 @@
<div class="payment-info">
<div class="info-grid">
<div class="info-item">
<span class="label">Date:</span>
<span class="label">{t('date', lang)}</span>
<span class="value">{formatDate(payment.date)}</span>
</div>
<div class="info-item">
<span class="label">Paid by:</span>
<span class="label">{t('paid_by_label', lang)}</span>
<span class="value">{payment.paidBy}</span>
</div>
<div class="info-item">
<span class="label">Created by:</span>
<span class="label">{t('created_by', lang)}</span>
<span class="value">{payment.createdBy}</span>
</div>
<div class="info-item">
<span class="label">Category:</span>
<span class="value">{getCategoryName(payment.category || 'groceries')}</span>
<span class="label">{t('category_label', lang)}</span>
<span class="value">{paymentCategoryName(payment.category || 'groceries', lang)}</span>
</div>
<div class="info-item">
<span class="label">Split method:</span>
<span class="label">{t('split_method_label', lang)}</span>
<span class="value">{getSplitDescription(payment)}</span>
</div>
</div>
{#if payment.description}
<div class="description">
<h3>Description</h3>
<h3>{t('description', lang)}</h3>
<p>{payment.description}</p>
</div>
{/if}
@@ -210,7 +205,7 @@
{#if payment.splits && payment.splits.length > 0}
<div class="splits-section">
<h3>Split Details</h3>
<h3>{t('split_details', lang)}</h3>
<div class="splits-list">
{#each payment.splits as split}
<div class="split-item" class:current-user={split.username === session?.user?.nickname}>
@@ -219,17 +214,17 @@
<div class="user-info">
<span class="username">{split.username}</span>
{#if split.username === session?.user?.nickname}
<span class="you-badge">You</span>
<span class="you-badge">{t('you', lang)}</span>
{/if}
</div>
</div>
<div class="split-amount" class:positive={split.amount < 0} class:negative={split.amount > 0}>
{#if split.amount > 0}
owes {formatCurrency(split.amount)}
{t('owes', lang)} {formatCurrency(split.amount)}
{:else if split.amount < 0}
owed {formatCurrency(split.amount)}
{t('owed', lang)} {formatCurrency(split.amount)}
{:else}
even
{t('even', lang)}
{/if}
</div>
</div>
@@ -239,7 +234,7 @@
{/if}
<div class="panel-actions">
<button class="btn-secondary" onclick={closeModal}>Close</button>
<button class="btn-secondary" onclick={closeModal}>{t('close', lang)}</button>
</div>
</div>
{/if}
@@ -247,7 +242,7 @@
</div>
{#if payment}
<EditButton href="/cospend/payments/edit/{paymentId}" />
<EditButton href="/{root}/payments/edit/{paymentId}" />
{/if}
<style>