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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user