feat: Implement multi-language system (ID/EN) for member dashboard

- Create translation files (locales/id.ts, locales/en.ts)
- Add LanguageContext with useLanguage hook
- Add LanguageToggle component in sidebar
- Default language: Indonesian (ID)
- Translate WalletDialog and TransactionDialog
- Language preference persisted in localStorage
- Type-safe translations with autocomplete

Next: Translate remaining pages (Overview, Wallets, Transactions, Profile)
This commit is contained in:
dwindown
2025-10-12 08:51:48 +07:00
parent c0df4a7c2a
commit 371b5e0a66
10 changed files with 676 additions and 73 deletions

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from "react"
import { toast } from "sonner"
import { useLanguage } from "@/contexts/LanguageContext"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -45,17 +46,19 @@ interface TransactionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
transaction?: Transaction | null
walletId?: string | null
onSuccess: () => void
}
const API = "/api"
export function TransactionDialog({ open, onOpenChange, transaction, onSuccess }: TransactionDialogProps) {
export function TransactionDialog({ open, onOpenChange, transaction, walletId: initialWalletId, onSuccess }: TransactionDialogProps) {
const { t } = useLanguage()
const [wallets, setWallets] = useState<Wallet[]>([])
const [categoryOptions, setCategoryOptions] = useState<Option[]>([])
const [loading, setLoading] = useState(false)
const [amount, setAmount] = useState(transaction?.amount?.toString() || "")
const [walletId, setWalletId] = useState(transaction?.walletId || "")
const [walletId, setWalletId] = useState(transaction?.walletId || initialWalletId || "")
const [direction, setDirection] = useState<"in" | "out">(transaction?.direction || "out")
const [categories, setCategories] = useState<string[]>(
transaction?.category ? transaction.category.split(',').map(c => c.trim()) : []
@@ -222,18 +225,18 @@ export function TransactionDialog({ open, onOpenChange, transaction, onSuccess }
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{isEditing ? "Edit Transaction" : "Add New Transaction"}</DialogTitle>
<DialogTitle>{isEditing ? t.transactionDialog.editTitle : t.transactionDialog.addTitle}</DialogTitle>
<DialogDescription>
{isEditing ? "Update your transaction details." : "Record a new transaction."}
{isEditing ? t.transactionDialog.editTitle : t.transactionDialog.addTitle}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="wallet">Wallet</Label>
<Label htmlFor="wallet">{t.transactionDialog.wallet}</Label>
<Select value={walletId} onValueChange={setWalletId}>
<SelectTrigger>
<SelectValue placeholder="Select a wallet" />
<SelectValue placeholder={t.transactionDialog.selectWallet} />
</SelectTrigger>
<SelectContent>
{wallets.map(wallet => (
@@ -247,58 +250,58 @@ export function TransactionDialog({ open, onOpenChange, transaction, onSuccess }
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="amount">Amount</Label>
<Label htmlFor="amount">{t.transactionDialog.amount}</Label>
<Input
id="amount"
type="number"
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
placeholder={t.transactionDialog.amountPlaceholder}
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="direction">Type</Label>
<Label htmlFor="direction">{t.transactionDialog.direction}</Label>
<Select value={direction} onValueChange={(value: "in" | "out") => setDirection(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="in">Income</SelectItem>
<SelectItem value="out">Expense</SelectItem>
<SelectItem value="in">{t.transactionDialog.income}</SelectItem>
<SelectItem value="out">{t.transactionDialog.expense}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="date">Date</Label>
<Label htmlFor="date">{t.transactionDialog.date}</Label>
<DatePicker
date={selectedDate}
onDateChange={(date) => date && setSelectedDate(date)}
placeholder="Select date"
placeholder={t.transactionDialog.selectDate}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="category">Categories</Label>
<Label htmlFor="category">{t.transactionDialog.category}</Label>
<MultipleSelector
options={categoryOptions}
selected={categories}
onChange={setCategories}
placeholder="Select or create categories..."
placeholder={t.transactionDialog.categoryPlaceholder}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="memo">Memo</Label>
<Label htmlFor="memo">{t.transactionDialog.memo}</Label>
<Input
id="memo"
value={memo}
onChange={(e) => setMemo(e.target.value)}
placeholder="Optional description"
placeholder={t.transactionDialog.memoPlaceholder}
/>
</div>
@@ -310,10 +313,10 @@ export function TransactionDialog({ open, onOpenChange, transaction, onSuccess }
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
{t.common.cancel}
</Button>
<Button type="submit" disabled={loading}>
{loading ? "Saving..." : isEditing ? "Update" : "Create"}
{loading ? t.common.loading : isEditing ? t.common.save : t.common.add}
</Button>
</DialogFooter>
</form>