@@ -11,9 +11,10 @@ File này lưu trữ các quyết định thiết kế dài hạn và trạng th
-**Nguồn tài nguyên Fonts**: Load thông qua thẻ `<link>` của Google Fonts trực tiếp trong `index.html` để tối ưu thời gian tải trang.
-**Phong cách Icon**: Tự thiết kế các inline SVG dạng blob dày, tròn trịa, nhiều màu sắc pastel thay vì dùng icon nét mảnh phẳng thông thường.
-**Hệ thống Light/Dark Theme**: Màu nền, surface, chữ, border, trạng thái và bóng Claymorphism phải đi qua semantic CSS variables được ánh xạ trong `tailwind.config.js`; không gắn màu light-only trực tiếp trong component. Lựa chọn `light`/`dark` được lưu cục bộ bằng Zustand, áp dụng lên `data-theme` và `zaui-theme`, đồng thời mọi màn hình dùng toggle chung để chuyển đổi nhất quán.
-**Đa ngôn ngữ frontend**: UI hỗ trợ `vi` và `en` qua `src/i18n/`, lưu lựa chọn bằng khóa `finwise.locale` và dùng `Intl` với `vi-VN`/`en-US` cho tiền tệ, số và ngày. Chỉ dịch text hiển thị; enum, mã tiền tệ, ID và payload API giữ nguyên. Khi thêm locale mới, thêm resource/config locale và translation key tương ứng, không đưa text hiển thị trực tiếp vào component.
-**Quản lý Routing**: Sử dụng cấu hình router của ZMP UI / React Router tích hợp bên trong template để dẫn hướng giữa các màn hình nghiệp vụ và `/style-guide`.
-**Tiêu đề trang**: Mỗi route cập nhật `document.title` theo mẫu `<Tên trang> | FinWise` thông qua component dùng chung trong router; route chưa nhận diện dùng tiêu đề mô tả sản phẩm mặc định.
-**Khoảng trống cho header hệ thống**: Zalo Mini App mặc định hiển thị `zaui-header` ở phía trên. Mọi page/layout phải chừa đủ khoảng cách phía trên (tính cả safe area khi cần) để nội dung và phần tử tương tác không bị header che khuất.
-**Khoảng trống và nút điều khiển trên header hệ thống**: Zalo Mini App mặc định hiển thị `zaui-header` ở phía trên. Mọi page/layout phải chừa đủ khoảng cách phía trên (tính cả safe area khi cần) để nội dung và phần tử tương tác không bị header che khuất. Cụm điều khiển riêng của app (hiện gồm theme và ngôn ngữ) phải nằm trong `.finwise-header-controls`, đặt về bên trái vùng native `right-buttons` rộng 96px; đồng thời `zaui-header` phải dành đủ `padding-right` cho cả `right-buttons` và cụm này. Không đặt từng nút bằng offset `right` rời rạc vì có thể làm switch ngôn ngữ bị che khuất.
-**Cấu hình API**: Base URL mặc định là `http://localhost:7777/api/v1` (tương tác trực tiếp với port 7777 của Backend).
-**Vite/ZMP entry**: Giữ `index.html` tại root repository và không cấu hình Vite `root: "./src"`. ZMP CLI khởi chạy dev server với project root; cấu hình khác sẽ khiến iframe app trả 404. Build output chuẩn là `www/` tại root.
-**Luồng xác thực**: Khi app mount, `AuthInitializer` gọi `/auth/me`; `AuthGuard` chỉ render private route sau khi khởi tạo xong và chuyển người dùng chưa đăng nhập tới `/login`. Cookie HTTP-only là cơ chế xác thực ưu tiên.
"avatarReadFailed":"Could not read the selected image. Try another one."
},
"avatarEditor":{
"title":"Edit Profile Picture",
"delete":"Delete picture",
"save":"Save picture",
"dragLabel":"Drag to select the profile picture area",
"previewAlt":"Profile picture preview",
"selectToStart":"Choose an image to get started",
"instruction":"Drag the image in the circle or use the sliders to choose the visible area.",
"horizontal":"Horizontal",
"vertical":"Vertical",
"locked":"Locked",
"horizontalLocked":"This image ratio has no extra horizontal area to adjust.",
"verticalLocked":"This image ratio has no extra vertical area to adjust.",
"moveLeft":"Move image left",
"moveRight":"Move image right",
"moveUp":"Move image up",
"moveDown":"Move image down",
"chooseAnother":"Choose another image",
"choose":"Choose image",
"fileHint":"JPEG, PNG, WebP · up to 5 MB"
},
"wallet":{
"header":"My Wallets",
"detailHeader":"Wallet Details",
"summary":"Total balance by currency",
"noBalance":"No balance yet",
"activeCount":"{{count}} active wallets",
"list":"Wallet list",
"syncing":"Syncing...",
"create":"Create wallet",
"createNew":"Create new wallet",
"searchLabel":"Search wallets",
"searchPlaceholder":"Search by name, description, or currency...",
"sortLabel":"Sort wallets",
"includeArchived":"Archived",
"loadingList":"Loading wallet list",
"loadFailed":"Could not load wallets",
"connectionFailed":"Check your connection and try again.",
"notFound":"No matching wallets found",
"empty":"No wallets yet",
"first":"Start with your first wallet",
"searchHint":"Try another keyword or include archived wallets.",
"emptyHint":"Create a wallet to track balances and manage your finances.",
"paginationLabel":"Wallet pagination",
"createSuccess":"Wallet created successfully.",
"createFailed":"Could not create the wallet. Please try again.",
"defaultUpdated":"Default wallet updated.",
"defaultSet":"Set as default wallet.",
"defaultFailed":"Could not set the default wallet.",
"setDefault":"Set as default wallet",
"detailLoadFailed":"Could not load wallet details",
"missing":"The wallet does not exist or the connection was interrupted.",
"listButton":"Wallet list",
"defaultWallet":"Default wallet",
"info":"Wallet information",
"status":"Status",
"createdAt":"Created",
"updatedAt":"Last updated",
"management":"Manage wallet",
"edit":"Edit information",
"archive":"Archive wallet",
"archiveDefaultHint":"Set another wallet as default before archiving this one.",
"restore":"Restore wallet",
"restoring":"Restoring...",
"restoreHint":"If there is no active default wallet, this wallet will become the default after restoration.",
"updateSuccess":"Wallet changes saved.",
"updateFailed":"Could not update the wallet.",
"archiveSuccess":"Wallet archived; transaction history remains available.",
"archiveFailed":"Could not archive the wallet.",
"restoreSuccess":"Wallet restored successfully.",
"restoreFailed":"Could not restore the wallet.",
"archiveTitle":"Archive wallet?",
"archiving":"Archiving...",
"archiveConfirm":"Confirm archive",
"archiveDescription":"The wallet can no longer be used for new transactions, but its history and balance will be preserved. You can restore it at any time.",
"form":{
"editTitle":"Edit wallet",
"createTitle":"Create wallet",
"saveChanges":"Save changes",
"preview":"Wallet preview",
"previewHint":"Choose an identifying icon and color",
"name":"Wallet name *",
"namePlaceholder":"For example: Cash",
"balance":"Balance *",
"currency":"Currency *",
"icon":"Icon",
"color":"Wallet color",
"chooseColor":"Choose color {{color}}",
"description":"Description",
"descriptionPlaceholder":"What this wallet is used for...",
"defaultTitle":"Set as default wallet",
"defaultHint":"The first wallet always becomes the default automatically."
"checkEmailDescription":"Chúng tôi đã gửi một liên kết kích hoạt đến email của bạn. Vui lòng nhấp vào liên kết để kích hoạt tài khoản trước khi đăng nhập.",
"backToLogin":"Quay lại Đăng Nhập"
},
"forgot":{
"header":"Quên Mật Khẩu",
"title":"Quên Mật Khẩu?",
"subtitle":"Đừng lo lắng, hãy nhập email của bạn để bắt đầu khôi phục",
"linkedEmail":"Email liên kết",
"submitting":"Đang gửi...",
"submit":"Gửi Yêu Cầu",
"rememberPassword":"Nhớ mật khẩu?",
"login":"Đăng nhập",
"successToast":"Liên kết khôi phục mật khẩu đã được gửi đến email của bạn! ✉️",
"failed":"Gửi yêu cầu thất bại",
"failedDetail":"Gửi yêu cầu thất bại, vui lòng thử lại.",
"sentTitle":"Đã gửi Email thành công",
"sentDescription":"Chúng tôi đã gửi hướng dẫn đặt lại mật khẩu đến email của bạn. Vui lòng kiểm tra hộp thư đến (và cả hộp thư rác).",
"enterResetCode":"Nhập mã đặt lại mật khẩu"
},
"reset":{
"header":"Đặt Lại Mật Khẩu",
"title":"Đặt Lại Mật Khẩu",
"subtitle":"Nhập mã đặt lại nhận được trong email và mật khẩu mới của bạn",
"token":"Mã khôi phục (Token UUID)",
"newPassword":"Mật khẩu mới",
"confirmNewPassword":"Xác nhận mật khẩu mới",
"submit":"Xác Nhận Đổi Mật Khẩu",
"back":"Quay lại",
"login":"Đăng nhập",
"success":"Đặt lại mật khẩu thành công! Bạn có thể đăng nhập ngay.",
"failed":"Đặt lại mật khẩu thất bại",
"expired":"Mã token không hợp lệ hoặc đã hết hạn."
}
},
"home":{
"header":"FinWise Mini App",
"greeting":"Chào, {{name}}!",
"subtitle":"Sổ tay Chi tiêu & Báo cáo Tài chính",
"account":"Tài khoản:",
"authTitle":"Xác thực hệ thống",
"authDescription":"Bạn đã đăng nhập thành công. Các API tiếp theo sẽ tự động được xác thực nhờ cookie an toàn và cơ chế JWT.",
"updateSuccess":"Cập nhật thông tin cá nhân thành công! 🎉",
"updateFailed":"Cập nhật thất bại, vui lòng thử lại.",
"changePassword":"Đổi mật khẩu",
"currentPassword":"Mật khẩu hiện tại",
"currentPasswordPlaceholder":"Nhập mật khẩu hiện tại...",
"newPassword":"Mật khẩu mới",
"newPasswordPlaceholder":"Mật khẩu mới ít nhất 8 ký tự...",
"confirmNewPassword":"Xác nhận mật khẩu mới",
"confirmNewPasswordPlaceholder":"Nhập lại mật khẩu mới...",
"passwordSubmit":"Đổi Mật Khẩu",
"passwordSuccess":"Thay đổi mật khẩu thành công! 🔑",
"passwordFailed":"Thay đổi mật khẩu thất bại.",
"sessionManagement":"Quản lý phiên đăng nhập",
"sessionDescription":"Bảo vệ tài khoản bằng cách đăng xuất khỏi tất cả các thiết bị và trình duyệt khác mà bạn đã đăng nhập trước đó.",
"logoutOthers":"Đăng xuất các thiết bị khác",
"logoutOthersSuccess":"Đã đăng xuất khỏi tất cả các thiết bị khác thành công! 📱",
"logoutOthersFailed":"Không thể đăng xuất các thiết bị khác.",
"activeDevices":"Thiết bị đang hoạt động",
"sessionsLoading":"Đang tải danh sách phiên",
"sessionsFailed":"Không thể tải danh sách phiên. Vui lòng thử lại.",
"unnamedDevice":"Thiết bị không tên",
"thisDevice":"Thiết bị này",
"activeSince":"Hoạt động từ: {{date}}",
"ipAddress":"IP: {{address}}",
"logout":"Đăng Xuất",
"logoutDevice":"Đăng xuất",
"logoutSuccess":"Đăng xuất thành công. Hẹn gặp lại bạn! 👋",
"revokeSuccess":"Đã đóng phiên đăng nhập thành công.",
"revokeFailed":"Không thể đóng phiên đăng nhập.",
"noSessions":"Không có phiên đăng nhập nào khác.",
"avatarSelectRequired":"Vui lòng chọn ảnh đại diện trước khi lưu.",
"avatarSaveFailed":"Không thể lưu ảnh đại diện vào hồ sơ.",
"avatarSaveSuccess":"Đã lưu ảnh và vùng hiển thị!",
"avatarUploadFailed":"Tải ảnh đại diện thất bại.",
"avatarDeleteFailed":"Không thể xóa ảnh đại diện.",
"avatarDeleteSuccess":"Đã xóa ảnh đại diện.",
"avatarTypeInvalid":"Ảnh đại diện phải là JPEG, PNG hoặc WebP.",
"avatarSizeInvalid":"Ảnh đại diện không được vượt quá 5 MB.",
"avatarReadFailed":"Không thể đọc ảnh đã chọn. Vui lòng thử ảnh khác."
},
"avatarEditor":{
"title":"Chỉnh ảnh đại diện",
"delete":"Xóa ảnh",
"save":"Lưu ảnh",
"dragLabel":"Kéo để chọn vùng ảnh đại diện",
"previewAlt":"Xem trước ảnh đại diện",
"selectToStart":"Chọn một ảnh để bắt đầu",
"instruction":"Kéo ảnh trong khung tròn hoặc dùng thanh trượt để chọn vùng hiển thị.",
"horizontal":"Ngang",
"vertical":"Dọc",
"locked":"Đang khóa",
"horizontalLocked":"Tỷ lệ ảnh không có vùng dư để chỉnh theo chiều ngang.",
"verticalLocked":"Tỷ lệ ảnh không có vùng dư để chỉnh theo chiều dọc.",
"moveLeft":"Dịch ảnh sang trái",
"moveRight":"Dịch ảnh sang phải",
"moveUp":"Dịch ảnh lên trên",
"moveDown":"Dịch ảnh xuống dưới",
"chooseAnother":"Chọn ảnh khác",
"choose":"Chọn ảnh",
"fileHint":"JPEG, PNG, WebP · tối đa 5 MB"
},
"wallet":{
"header":"Ví của tôi",
"detailHeader":"Chi tiết ví",
"summary":"Tổng số dư theo tiền tệ",
"noBalance":"Chưa có số dư",
"activeCount":"{{count}} ví đang hoạt động",
"list":"Danh sách ví",
"syncing":"Đang đồng bộ...",
"create":"Tạo ví",
"createNew":"Tạo ví mới",
"searchLabel":"Tìm kiếm ví",
"searchPlaceholder":"Tìm theo tên, mô tả, tiền tệ...",
"sortLabel":"Sắp xếp ví",
"includeArchived":"Đã lưu trữ",
"loadingList":"Đang tải danh sách ví",
"loadFailed":"Không thể tải danh sách ví",
"connectionFailed":"Vui lòng kiểm tra kết nối và thử lại.",
"notFound":"Không tìm thấy ví phù hợp",
"empty":"Chưa có ví nào",
"first":"Bắt đầu với ví đầu tiên",
"searchHint":"Thử một từ khóa khác hoặc bật danh sách đã lưu trữ.",
"emptyHint":"Tạo ví để theo dõi số dư và quản lý tài chính của bạn.",
"paginationLabel":"Phân trang ví",
"createSuccess":"Đã tạo ví mới thành công.",
"createFailed":"Không thể tạo ví. Vui lòng thử lại.",
"defaultUpdated":"Đã cập nhật ví mặc định.",
"defaultSet":"Đã đặt làm ví mặc định.",
"defaultFailed":"Không thể đặt ví mặc định.",
"setDefault":"Đặt làm ví mặc định",
"detailLoadFailed":"Không thể tải thông tin ví",
"missing":"Ví không tồn tại hoặc kết nối bị gián đoạn.",
"listButton":"Danh sách ví",
"defaultWallet":"Ví mặc định",
"info":"Thông tin ví",
"status":"Trạng thái",
"createdAt":"Ngày tạo",
"updatedAt":"Cập nhật gần nhất",
"management":"Quản lý ví",
"edit":"Chỉnh sửa thông tin",
"archive":"Lưu trữ ví",
"archiveDefaultHint":"Hãy đặt một ví khác làm mặc định trước khi lưu trữ ví này.",
"restore":"Khôi phục ví",
"restoring":"Đang khôi phục...",
"restoreHint":"Nếu chưa có ví hoạt động mặc định, ví này sẽ tự động được chọn sau khi khôi phục.",
"updateSuccess":"Đã lưu thay đổi của ví.",
"updateFailed":"Không thể cập nhật ví.",
"archiveSuccess":"Ví đã được lưu trữ; lịch sử giao dịch vẫn được giữ nguyên.",
"archiveFailed":"Không thể lưu trữ ví.",
"restoreSuccess":"Đã khôi phục ví thành công.",
"restoreFailed":"Không thể khôi phục ví.",
"archiveTitle":"Lưu trữ ví?",
"archiving":"Đang lưu trữ...",
"archiveConfirm":"Xác nhận lưu trữ",
"archiveDescription":"Ví sẽ không còn dùng được cho giao dịch mới, nhưng toàn bộ lịch sử giao dịch và số dư vẫn được bảo toàn. Bạn có thể khôi phục ví bất cứ lúc nào.",
"form":{
"editTitle":"Chỉnh sửa ví",
"createTitle":"Tạo ví mới",
"saveChanges":"Lưu thay đổi",
"preview":"Xem trước ví",
"previewHint":"Chọn biểu tượng và màu nhận diện",
"name":"Tên ví *",
"namePlaceholder":"Ví dụ: Tiền mặt",
"balance":"Số dư *",
"currency":"Tiền tệ *",
"icon":"Biểu tượng",
"color":"Màu ví",
"chooseColor":"Chọn màu {{color}}",
"description":"Mô tả",
"descriptionPlaceholder":"Mục đích sử dụng ví...",
"defaultTitle":"Đặt làm ví mặc định",
"defaultHint":"Ví đầu tiên luôn tự động trở thành ví mặc định."
"sort":{"createdDesc":"Mới tạo gần đây","updatedDesc":"Mới cập nhật","nameAsc":"Tên A → Z","nameDesc":"Tên Z → A","balanceDesc":"Số dư cao nhất","balanceAsc":"Số dư thấp nhất"}
},
"styleGuide":{
"header":"Style Guide & Design System",
"title":"Hệ thống giao diện Claymorphism",
"subtitle":"Design token thích ứng sáng/tối cho giao diện đất sét mềm mại của FinWise",
<pclassName="clay-body text-sm">Ví sẽ không còn dùng được cho giao dịch mới, nhưng toàn bộ lịch sử giao dịch và số dư vẫn được bảo toàn. Bạn có thể khôi phục ví bất cứ lúc nào.</p>
<h2className="clay-title-h3">{deferredSearch?"Không tìm thấy ví phù hợp":includeArchived?"Chưa có ví nào":"Bắt đầu với ví đầu tiên"}</h2>
<pclassName="clay-caption mt-1 max-w-xs">{deferredSearch?"Thử một từ khóa khác hoặc bật danh sách đã lưu trữ.":"Tạo ví để theo dõi số dư và quản lý tài chính của bạn."}</p>