diff --git a/MULTI_LANGUAGE_IMPLEMENTATION.md b/MULTI_LANGUAGE_IMPLEMENTATION.md new file mode 100644 index 0000000..168150b --- /dev/null +++ b/MULTI_LANGUAGE_IMPLEMENTATION.md @@ -0,0 +1,87 @@ +# Multi-Language Implementation + +## Overview +Implemented a simple, lightweight multi-language system for the member dashboard with Indonesian (default) and English support. + +## Features +- ✅ **Default Language**: Indonesian (ID) +- ✅ **Optional Language**: English (EN) +- ✅ **Persistent**: Language preference saved in localStorage +- ✅ **Easy Toggle**: Language switcher in sidebar footer +- ✅ **Type-Safe**: Full TypeScript support with autocomplete + +## Structure + +### Translation Files +- `/apps/web/src/locales/id.ts` - Indonesian translations +- `/apps/web/src/locales/en.ts` - English translations + +### Context & Hook +- `/apps/web/src/contexts/LanguageContext.tsx` - Language context provider +- Hook: `useLanguage()` - Access translations and language state + +### Components +- `/apps/web/src/components/LanguageToggle.tsx` - Language switcher button + +## Usage + +### In Components +```typescript +import { useLanguage } from '@/contexts/LanguageContext' + +function MyComponent() { + const { t, language, setLanguage } = useLanguage() + + return ( +
+

{t.overview.title}

+

{t.overview.totalBalance}

+
+ ) +} +``` + +### Translation Structure +```typescript +{ + common: { search, filter, add, edit, delete, ... }, + nav: { overview, transactions, wallets, profile, logout }, + overview: { ... }, + transactions: { ... }, + wallets: { ... }, + profile: { ... }, + // etc +} +``` + +## Implementation Status + +### ✅ Completed +1. Translation files (ID & EN) +2. Language context & provider +3. Language toggle component +4. Sidebar navigation translated +5. Build & lint passing + +### 🔄 Next Steps (To be implemented) +1. Translate all member dashboard pages: + - Overview page + - Wallets page + - Transactions page + - Profile page +2. Translate dialogs: + - WalletDialog + - TransactionDialog +3. Update toast messages to use translations +4. Test language switching + +## Admin Dashboard +- **Remains English-only** (as requested) +- No translation needed for admin pages + +## Benefits +- ✅ No external dependencies +- ✅ Type-safe with autocomplete +- ✅ Easy to maintain +- ✅ Fast performance +- ✅ Can migrate to react-i18next later if needed diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index cbdc72d..354b610 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' import { AuthProvider, useAuth } from './contexts/AuthContext' +import { LanguageProvider } from './contexts/LanguageContext' import { ThemeProvider } from './components/ThemeProvider' import { Toaster } from './components/ui/sonner' import { Dashboard } from './components/Dashboard' @@ -59,9 +60,10 @@ export default function App() { return ( - - - + + + + {/* Public Routes */} } /> } /> @@ -81,7 +83,8 @@ export default function App() { {/* Protected Routes */} } /> - + + ) diff --git a/apps/web/src/components/LanguageToggle.tsx b/apps/web/src/components/LanguageToggle.tsx new file mode 100644 index 0000000..7e06783 --- /dev/null +++ b/apps/web/src/components/LanguageToggle.tsx @@ -0,0 +1,24 @@ +import { useLanguage } from '@/contexts/LanguageContext' +import { Button } from '@/components/ui/button' +import { Languages } from 'lucide-react' + +export function LanguageToggle() { + const { language, setLanguage } = useLanguage() + + const toggleLanguage = () => { + setLanguage(language === 'id' ? 'en' : 'id') + } + + return ( + + ) +} diff --git a/apps/web/src/components/dialogs/TransactionDialog.tsx b/apps/web/src/components/dialogs/TransactionDialog.tsx index 095b050..ea7364f 100644 --- a/apps/web/src/components/dialogs/TransactionDialog.tsx +++ b/apps/web/src/components/dialogs/TransactionDialog.tsx @@ -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([]) const [categoryOptions, setCategoryOptions] = useState([]) 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( transaction?.category ? transaction.category.split(',').map(c => c.trim()) : [] @@ -222,18 +225,18 @@ export function TransactionDialog({ open, onOpenChange, transaction, onSuccess } - {isEditing ? "Edit Transaction" : "Add New Transaction"} + {isEditing ? t.transactionDialog.editTitle : t.transactionDialog.addTitle} - {isEditing ? "Update your transaction details." : "Record a new transaction."} + {isEditing ? t.transactionDialog.editTitle : t.transactionDialog.addTitle}
- + setAmount(e.target.value)} - placeholder="0.00" + placeholder={t.transactionDialog.amountPlaceholder} required />
- +
- + date && setSelectedDate(date)} - placeholder="Select date" + placeholder={t.transactionDialog.selectDate} />
- +
- + setMemo(e.target.value)} - placeholder="Optional description" + placeholder={t.transactionDialog.memoPlaceholder} />
@@ -310,10 +313,10 @@ export function TransactionDialog({ open, onOpenChange, transaction, onSuccess }
diff --git a/apps/web/src/components/dialogs/WalletDialog.tsx b/apps/web/src/components/dialogs/WalletDialog.tsx index f237f83..98f4505 100644 --- a/apps/web/src/components/dialogs/WalletDialog.tsx +++ b/apps/web/src/components/dialogs/WalletDialog.tsx @@ -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, @@ -56,6 +57,7 @@ interface WalletDialogProps { const API = "/api" export function WalletDialog({ open, onOpenChange, wallet, onSuccess }: WalletDialogProps) { + const { t } = useLanguage() const [name, setName] = useState(wallet?.name || "") const [kind, setKind] = useState<"money" | "asset">(wallet?.kind || "money") const [currency, setCurrency] = useState(wallet?.currency || "IDR") @@ -71,7 +73,7 @@ export function WalletDialog({ open, onOpenChange, wallet, onSuccess }: WalletDi const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!name.trim()) { - setError("Name is required") + setError(t.walletDialog.name + " is required") return } @@ -141,40 +143,40 @@ export function WalletDialog({ open, onOpenChange, wallet, onSuccess }: WalletDi - {isEditing ? "Edit Wallet" : "Add New Wallet"} + {isEditing ? t.walletDialog.editTitle : t.walletDialog.addTitle} - {isEditing ? "Update your wallet details." : "Create a new wallet to track your finances."} + {isEditing ? t.walletDialog.editTitle : t.walletDialog.addTitle}
- + setName(e.target.value)} - placeholder="e.g., My Bank Account" + placeholder={t.walletDialog.namePlaceholder} required />
- +
{kind === "money" ? (
- + - + No currency found. @@ -222,37 +224,37 @@ export function WalletDialog({ open, onOpenChange, wallet, onSuccess }: WalletDi ) : ( <>
- + setUnit(e.target.value)} - placeholder="e.g., shares, kg, pieces" + placeholder={t.walletDialog.unitPlaceholder} />
- + setPricePerUnit(e.target.value)} - placeholder="0.00" + placeholder={t.walletDialog.pricePerUnitPlaceholder} />
)}
- + setInitialAmount(e.target.value)} - placeholder="0.00" + placeholder={t.walletDialog.initialAmountPlaceholder} />
@@ -264,10 +266,10 @@ export function WalletDialog({ open, onOpenChange, wallet, onSuccess }: WalletDi
diff --git a/apps/web/src/components/layout/AppSidebar.tsx b/apps/web/src/components/layout/AppSidebar.tsx index 66d1746..2f836c1 100644 --- a/apps/web/src/components/layout/AppSidebar.tsx +++ b/apps/web/src/components/layout/AppSidebar.tsx @@ -1,5 +1,6 @@ import { Home, Wallet, Receipt, User, LogOut } from "lucide-react" import { Logo } from "../Logo" +import { LanguageToggle } from "../LanguageToggle" import { Sidebar, SidebarContent, @@ -13,31 +14,9 @@ import { useSidebar, } from "@/components/ui/sidebar" import { useAuth } from "@/contexts/AuthContext" +import { useLanguage } from "@/contexts/LanguageContext" import { getAvatarUrl } from "@/lib/utils" -const items = [ - { - title: "Overview", - url: "/", - icon: Home, - }, - { - title: "Wallets", - url: "/wallets", - icon: Wallet, - }, - { - title: "Transactions", - url: "/transactions", - icon: Receipt, - }, - { - title: "Profile", - url: "/profile", - icon: User, - }, -] - interface AppSidebarProps { currentPage: string onNavigate: (page: string) => void @@ -46,6 +25,30 @@ interface AppSidebarProps { export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) { const { user, logout } = useAuth() const { isMobile, setOpenMobile } = useSidebar() + const { t } = useLanguage() + + const items = [ + { + title: t.nav.overview, + url: "/", + icon: Home, + }, + { + title: t.nav.wallets, + url: "/wallets", + icon: Wallet, + }, + { + title: t.nav.transactions, + url: "/transactions", + icon: Receipt, + }, + { + title: t.nav.profile, + url: "/profile", + icon: User, + }, + ] return ( @@ -109,13 +112,16 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
- +
+ + +
) diff --git a/apps/web/src/contexts/LanguageContext.tsx b/apps/web/src/contexts/LanguageContext.tsx new file mode 100644 index 0000000..73b673f --- /dev/null +++ b/apps/web/src/contexts/LanguageContext.tsx @@ -0,0 +1,58 @@ +import { createContext, useContext, useState, useEffect } from 'react' +import type { ReactNode } from 'react' +import { id } from '@/locales/id' +import { en } from '@/locales/en' + +type Language = 'id' | 'en' +type Translations = typeof id + +interface LanguageContextType { + language: Language + setLanguage: (lang: Language) => void + t: Translations +} + +const LanguageContext = createContext(undefined) + +export function LanguageProvider({ children }: { children: ReactNode }) { + const [language, setLanguageState] = useState(() => { + const stored = localStorage.getItem('language') + return (stored === 'en' || stored === 'id') ? stored : 'id' // Default to Indonesian + }) + + const translations: Record = { + id, + en, + } + + const setLanguage = (lang: Language) => { + setLanguageState(lang) + localStorage.setItem('language', lang) + } + + useEffect(() => { + // Save to localStorage whenever language changes + localStorage.setItem('language', language) + }, [language]) + + const value: LanguageContextType = { + language, + setLanguage, + t: translations[language], + } + + return ( + + {children} + + ) +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useLanguage() { + const context = useContext(LanguageContext) + if (context === undefined) { + throw new Error('useLanguage must be used within a LanguageProvider') + } + return context +} diff --git a/apps/web/src/locales/en.ts b/apps/web/src/locales/en.ts new file mode 100644 index 0000000..e072969 --- /dev/null +++ b/apps/web/src/locales/en.ts @@ -0,0 +1,210 @@ +export const en = { + common: { + search: 'Search', + filter: 'Filter', + add: 'Add', + edit: 'Edit', + delete: 'Delete', + cancel: 'Cancel', + save: 'Save', + close: 'Close', + loading: 'Loading...', + noData: 'No data', + confirm: 'Confirm', + success: 'Success', + error: 'Error', + total: 'Total', + date: 'Date', + amount: 'Amount', + status: 'Status', + actions: 'Actions', + all: 'All', + active: 'Active', + inactive: 'Inactive', + yes: 'Yes', + no: 'No', + }, + + nav: { + overview: 'Overview', + transactions: 'Transactions', + wallets: 'Wallets', + profile: 'Profile', + logout: 'Logout', + }, + + overview: { + title: 'Overview', + totalBalance: 'Total Balance', + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + acrossWallets: 'Across {count} wallets', + income: 'income', + expense: 'expense', + recentTransactions: 'Recent Transactions', + viewAll: 'View All', + noTransactions: 'No transactions yet', + addFirstTransaction: 'Add your first transaction', + wallets: 'Wallets', + addWallet: 'Add Wallet', + noWallets: 'No wallets yet', + createFirstWallet: 'Create your first wallet', + incomeByCategory: 'Income by Category', + expenseByCategory: 'Expense by Category', + last30Days: 'Last 30 days', + last7Days: 'Last 7 days', + thisMonth: 'This month', + lastMonth: 'Last month', + thisYear: 'This year', + custom: 'Custom', + }, + + transactions: { + title: 'Transactions', + addTransaction: 'Add Transaction', + editTransaction: 'Edit Transaction', + deleteConfirm: 'Are you sure you want to delete this transaction?', + income: 'Income', + expense: 'Expense', + category: 'Category', + memo: 'Memo', + wallet: 'Wallet', + direction: 'Type', + filterByWallet: 'Filter by Wallet', + filterByDirection: 'Filter by Type', + filterByCategory: 'Filter by Category', + searchPlaceholder: 'Search transactions...', + noTransactions: 'No transactions', + stats: { + totalIncome: 'Total Income', + totalExpense: 'Total Expense', + netAmount: 'Net Amount', + }, + }, + + wallets: { + title: 'Wallets', + addWallet: 'Add Wallet', + editWallet: 'Edit Wallet', + deleteConfirm: 'Are you sure you want to delete this wallet? All related transactions will be deleted.', + name: 'Name', + type: 'Type', + balance: 'Balance', + currency: 'Currency', + unit: 'Unit', + initialAmount: 'Initial Amount', + pricePerUnit: 'Price per Unit', + money: 'Money', + asset: 'Asset', + filterByType: 'Filter by Type', + searchPlaceholder: 'Search wallets...', + noWallets: 'No wallets yet', + createFirst: 'Create your first wallet to start tracking your finances', + totalBalance: 'Total Balance', + moneyWallets: 'Money Wallets', + assetWallets: 'Asset Wallets', + }, + + walletDialog: { + addTitle: 'Add Wallet', + editTitle: 'Edit Wallet', + name: 'Wallet Name', + namePlaceholder: 'e.g., Main Wallet, Savings', + type: 'Wallet Type', + money: 'Money', + asset: 'Asset', + currency: 'Currency', + selectCurrency: 'Select currency', + unit: 'Unit', + unitPlaceholder: 'e.g., grams, lots, shares', + initialAmount: 'Initial Amount (Optional)', + initialAmountPlaceholder: '0', + pricePerUnit: 'Price per Unit (Optional)', + pricePerUnitPlaceholder: '0', + pricePerUnitHelper: 'Price per {unit} in IDR', + }, + + transactionDialog: { + addTitle: 'Add Transaction', + editTitle: 'Edit Transaction', + amount: 'Amount', + amountPlaceholder: '0', + wallet: 'Wallet', + selectWallet: 'Select wallet', + direction: 'Transaction Type', + income: 'Income', + expense: 'Expense', + category: 'Category', + categoryPlaceholder: 'Select or type new category', + addCategory: 'Add', + memo: 'Memo (Optional)', + memoPlaceholder: 'Add a note...', + date: 'Date', + selectDate: 'Select date', + }, + + profile: { + title: 'Profile', + personalInfo: 'Personal Information', + name: 'Name', + email: 'Email', + emailVerified: 'Email Verified', + emailNotVerified: 'Email Not Verified', + avatar: 'Avatar', + changeAvatar: 'Change Avatar', + uploading: 'Uploading...', + + security: 'Security', + password: 'Password', + currentPassword: 'Current Password', + newPassword: 'New Password', + confirmPassword: 'Confirm New Password', + changePassword: 'Change Password', + setPassword: 'Set Password', + noPassword: 'You logged in with Google and haven\'t set a password yet', + + twoFactor: 'Two-Factor Authentication', + twoFactorDesc: 'Add an extra layer of security to your account', + phoneNumber: 'Phone Number', + phoneNumberPlaceholder: '+62812345678', + updatePhone: 'Update Phone', + + emailOtp: 'Email OTP', + emailOtpDesc: 'Receive verification codes via email', + enable: 'Enable', + disable: 'Disable', + enabled: 'Enabled', + disabled: 'Disabled', + sendCode: 'Send Code', + verifyCode: 'Verify Code', + enterCode: 'Enter code', + + whatsappOtp: 'WhatsApp OTP', + whatsappOtpDesc: 'Receive verification codes via WhatsApp', + + authenticatorApp: 'Authenticator App', + authenticatorDesc: 'Use an authenticator app like Google Authenticator', + setup: 'Setup', + scanQr: 'Scan QR Code', + scanQrDesc: 'Scan this QR code with your authenticator app', + manualEntry: 'Or enter this code manually:', + enterAuthCode: 'Enter code from your authenticator app', + + dangerZone: 'Danger Zone', + deleteAccount: 'Delete Account', + deleteAccountDesc: 'Permanently delete your account. This action cannot be undone.', + deleteAccountConfirm: 'Are you sure you want to delete your account? All data will be permanently lost.', + enterPasswordToDelete: 'Enter your password to confirm', + }, + + dateRange: { + last7Days: 'Last 7 days', + last30Days: 'Last 30 days', + thisMonth: 'This month', + lastMonth: 'Last month', + thisYear: 'This year', + custom: 'Custom', + from: 'From', + to: 'To', + }, +} diff --git a/apps/web/src/locales/id.ts b/apps/web/src/locales/id.ts new file mode 100644 index 0000000..1e63aed --- /dev/null +++ b/apps/web/src/locales/id.ts @@ -0,0 +1,210 @@ +export const id = { + common: { + search: 'Cari', + filter: 'Filter', + add: 'Tambah', + edit: 'Edit', + delete: 'Hapus', + cancel: 'Batal', + save: 'Simpan', + close: 'Tutup', + loading: 'Memuat...', + noData: 'Tidak ada data', + confirm: 'Konfirmasi', + success: 'Berhasil', + error: 'Gagal', + total: 'Total', + date: 'Tanggal', + amount: 'Jumlah', + status: 'Status', + actions: 'Aksi', + all: 'Semua', + active: 'Aktif', + inactive: 'Tidak Aktif', + yes: 'Ya', + no: 'Tidak', + }, + + nav: { + overview: 'Ringkasan', + transactions: 'Transaksi', + wallets: 'Dompet', + profile: 'Profil', + logout: 'Keluar', + }, + + overview: { + title: 'Ringkasan', + totalBalance: 'Total Saldo', + totalIncome: 'Total Pemasukan', + totalExpense: 'Total Pengeluaran', + acrossWallets: 'Dari {count} dompet', + income: 'pemasukan', + expense: 'pengeluaran', + recentTransactions: 'Transaksi Terkini', + viewAll: 'Lihat Semua', + noTransactions: 'Belum ada transaksi', + addFirstTransaction: 'Tambahkan transaksi pertama Anda', + wallets: 'Dompet', + addWallet: 'Tambah Dompet', + noWallets: 'Belum ada dompet', + createFirstWallet: 'Buat dompet pertama Anda', + incomeByCategory: 'Pemasukan per Kategori', + expenseByCategory: 'Pengeluaran per Kategori', + last30Days: '30 hari terakhir', + last7Days: '7 hari terakhir', + thisMonth: 'Bulan ini', + lastMonth: 'Bulan lalu', + thisYear: 'Tahun ini', + custom: 'Kustom', + }, + + transactions: { + title: 'Transaksi', + addTransaction: 'Tambah Transaksi', + editTransaction: 'Edit Transaksi', + deleteConfirm: 'Apakah Anda yakin ingin menghapus transaksi ini?', + income: 'Pemasukan', + expense: 'Pengeluaran', + category: 'Kategori', + memo: 'Catatan', + wallet: 'Dompet', + direction: 'Tipe', + filterByWallet: 'Filter berdasarkan Dompet', + filterByDirection: 'Filter berdasarkan Tipe', + filterByCategory: 'Filter berdasarkan Kategori', + searchPlaceholder: 'Cari transaksi...', + noTransactions: 'Tidak ada transaksi', + stats: { + totalIncome: 'Total Pemasukan', + totalExpense: 'Total Pengeluaran', + netAmount: 'Saldo Bersih', + }, + }, + + wallets: { + title: 'Dompet', + addWallet: 'Tambah Dompet', + editWallet: 'Edit Dompet', + deleteConfirm: 'Apakah Anda yakin ingin menghapus dompet ini? Semua transaksi terkait akan ikut terhapus.', + name: 'Nama', + type: 'Tipe', + balance: 'Saldo', + currency: 'Mata Uang', + unit: 'Satuan', + initialAmount: 'Jumlah Awal', + pricePerUnit: 'Harga per Satuan', + money: 'Uang', + asset: 'Aset', + filterByType: 'Filter berdasarkan Tipe', + searchPlaceholder: 'Cari dompet...', + noWallets: 'Belum ada dompet', + createFirst: 'Buat dompet pertama Anda untuk mulai melacak keuangan', + totalBalance: 'Total Saldo', + moneyWallets: 'Dompet Uang', + assetWallets: 'Dompet Aset', + }, + + walletDialog: { + addTitle: 'Tambah Dompet', + editTitle: 'Edit Dompet', + name: 'Nama Dompet', + namePlaceholder: 'Contoh: Dompet Utama, Tabungan', + type: 'Tipe Dompet', + money: 'Uang', + asset: 'Aset', + currency: 'Mata Uang', + selectCurrency: 'Pilih mata uang', + unit: 'Satuan', + unitPlaceholder: 'Contoh: gram, lot, lembar', + initialAmount: 'Jumlah Awal (Opsional)', + initialAmountPlaceholder: '0', + pricePerUnit: 'Harga per Satuan (Opsional)', + pricePerUnitPlaceholder: '0', + pricePerUnitHelper: 'Harga per {unit} dalam IDR', + }, + + transactionDialog: { + addTitle: 'Tambah Transaksi', + editTitle: 'Edit Transaksi', + amount: 'Jumlah', + amountPlaceholder: '0', + wallet: 'Dompet', + selectWallet: 'Pilih dompet', + direction: 'Tipe Transaksi', + income: 'Pemasukan', + expense: 'Pengeluaran', + category: 'Kategori', + categoryPlaceholder: 'Pilih atau ketik kategori baru', + addCategory: 'Tambah', + memo: 'Catatan (Opsional)', + memoPlaceholder: 'Tambahkan catatan...', + date: 'Tanggal', + selectDate: 'Pilih tanggal', + }, + + profile: { + title: 'Profil', + personalInfo: 'Informasi Pribadi', + name: 'Nama', + email: 'Email', + emailVerified: 'Email Terverifikasi', + emailNotVerified: 'Email Belum Terverifikasi', + avatar: 'Avatar', + changeAvatar: 'Ubah Avatar', + uploading: 'Mengunggah...', + + security: 'Keamanan', + password: 'Password', + currentPassword: 'Password Saat Ini', + newPassword: 'Password Baru', + confirmPassword: 'Konfirmasi Password Baru', + changePassword: 'Ubah Password', + setPassword: 'Atur Password', + noPassword: 'Anda login dengan Google dan belum mengatur password', + + twoFactor: 'Autentikasi Dua Faktor', + twoFactorDesc: 'Tambahkan lapisan keamanan ekstra ke akun Anda', + phoneNumber: 'Nomor Telepon', + phoneNumberPlaceholder: '+62812345678', + updatePhone: 'Update Nomor', + + emailOtp: 'Email OTP', + emailOtpDesc: 'Terima kode verifikasi via email', + enable: 'Aktifkan', + disable: 'Nonaktifkan', + enabled: 'Aktif', + disabled: 'Tidak Aktif', + sendCode: 'Kirim Kode', + verifyCode: 'Verifikasi Kode', + enterCode: 'Masukkan kode', + + whatsappOtp: 'WhatsApp OTP', + whatsappOtpDesc: 'Terima kode verifikasi via WhatsApp', + + authenticatorApp: 'Authenticator App', + authenticatorDesc: 'Gunakan aplikasi authenticator seperti Google Authenticator', + setup: 'Setup', + scanQr: 'Scan QR Code', + scanQrDesc: 'Scan QR code ini dengan aplikasi authenticator Anda', + manualEntry: 'Atau masukkan kode ini secara manual:', + enterAuthCode: 'Masukkan kode dari aplikator authenticator', + + dangerZone: 'Zona Berbahaya', + deleteAccount: 'Hapus Akun', + deleteAccountDesc: 'Hapus akun Anda secara permanen. Tindakan ini tidak dapat dibatalkan.', + deleteAccountConfirm: 'Apakah Anda yakin ingin menghapus akun Anda? Semua data akan hilang permanen.', + enterPasswordToDelete: 'Masukkan password Anda untuk konfirmasi', + }, + + dateRange: { + last7Days: '7 hari terakhir', + last30Days: '30 hari terakhir', + thisMonth: 'Bulan ini', + lastMonth: 'Bulan lalu', + thisYear: 'Tahun ini', + custom: 'Kustom', + from: 'Dari', + to: 'Sampai', + }, +} diff --git a/apps/web/tsconfig.app.tsbuildinfo b/apps/web/tsconfig.app.tsbuildinfo index 50f71ae..21cbeb9 100644 --- a/apps/web/tsconfig.app.tsbuildinfo +++ b/apps/web/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/breadcrumb.tsx","./src/components/dashboard.tsx","./src/components/logo.tsx","./src/components/themeprovider.tsx","./src/components/themetoggle.tsx","./src/components/admin/adminbreadcrumb.tsx","./src/components/admin/adminlayout.tsx","./src/components/admin/adminsidebar.tsx","./src/components/admin/pages/admindashboard.tsx","./src/components/admin/pages/adminpaymentmethods.tsx","./src/components/admin/pages/adminpayments.tsx","./src/components/admin/pages/adminplans.tsx","./src/components/admin/pages/adminsettings.tsx","./src/components/admin/pages/adminusers.tsx","./src/components/dialogs/transactiondialog.tsx","./src/components/dialogs/walletdialog.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/authlayout.tsx","./src/components/layout/dashboardlayout.tsx","./src/components/pages/authcallback.tsx","./src/components/pages/login.tsx","./src/components/pages/otpverification.tsx","./src/components/pages/overview.tsx","./src/components/pages/profile.tsx","./src/components/pages/register.tsx","./src/components/pages/transactions.tsx","./src/components/pages/wallets.tsx","./src/components/test/calendartest.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/command.tsx","./src/components/ui/date-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-select.tsx","./src/components/ui/multiselector.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/constants/currencies.ts","./src/contexts/authcontext.tsx","./src/hooks/use-mobile.ts","./src/hooks/usetheme.ts","./src/lib/utils.ts","./src/utils/exchangerate.ts","./src/utils/numberformat.ts"],"version":"5.9.2"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/breadcrumb.tsx","./src/components/dashboard.tsx","./src/components/languagetoggle.tsx","./src/components/logo.tsx","./src/components/themeprovider.tsx","./src/components/themetoggle.tsx","./src/components/admin/adminbreadcrumb.tsx","./src/components/admin/adminlayout.tsx","./src/components/admin/adminsidebar.tsx","./src/components/admin/pages/admindashboard.tsx","./src/components/admin/pages/adminpaymentmethods.tsx","./src/components/admin/pages/adminpayments.tsx","./src/components/admin/pages/adminplans.tsx","./src/components/admin/pages/adminsettings.tsx","./src/components/admin/pages/adminusers.tsx","./src/components/dialogs/transactiondialog.tsx","./src/components/dialogs/walletdialog.tsx","./src/components/layout/appsidebar.tsx","./src/components/layout/authlayout.tsx","./src/components/layout/dashboardlayout.tsx","./src/components/pages/authcallback.tsx","./src/components/pages/login.tsx","./src/components/pages/otpverification.tsx","./src/components/pages/overview.tsx","./src/components/pages/profile.tsx","./src/components/pages/register.tsx","./src/components/pages/transactions.tsx","./src/components/pages/wallets.tsx","./src/components/test/calendartest.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/command.tsx","./src/components/ui/date-picker.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-select.tsx","./src/components/ui/multiselector.tsx","./src/components/ui/popover.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/sonner.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/tooltip.tsx","./src/constants/currencies.ts","./src/contexts/authcontext.tsx","./src/contexts/languagecontext.tsx","./src/hooks/use-mobile.ts","./src/hooks/usetheme.ts","./src/lib/utils.ts","./src/locales/en.ts","./src/locales/id.ts","./src/utils/exchangerate.ts","./src/utils/numberformat.ts"],"version":"5.9.2"} \ No newline at end of file