diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..116d685 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "inlang.vs-code-extension" + ] +} \ No newline at end of file diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..7eebd66 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "common_no_list": "Hiện tại chưa có dữ liệu nào!", + "common_is_required": "{field} is required.", + "role_tags_admin": "Administrator", + "role_tags_user": "User", + "role_tags_member": "Member", + "role_tags_owner": "Owner", + "ui_login_btn": "Sign in", + "ui_logout_btn": "Sign out", + "ui_cancel_btn": "Cancel", + "ui_close_btn": "Close", + "ui_confirm_btn": "Confirm", + "ui_signup_btn": "Sign up", + "ui_view_btn": "View", + "ui_save_btn": "Save", + "ui_update_btn": "Update", + "ui_view_all_notifications": "View All Notifications", + "ui_label_notifications": "Notifications", + "ui_change_password_btn": "Change password", + "nav_home": "Home", + "nav_dashboard": "Dashboard", + "nav_settings": "Settings", + "nav_add_new": "Add new", + "nav_edit": "Edit", + "nav_change_password": "Change password", + "nav_log": "Audit log", + "nav_roles": "Vai trò & quyền hạn", + "nav_box": "Box", + "nav_account": "Account", + "nav_profile": "Profile", + "login_page_form_email": "Email", + "login_page_form_password": "Password", + "login_page_ui_welcome_back": "Welcome back", + "login_page_ui_forgot_password": "Forgot your password?", + "login_page_ui_not_have_account": "Don't have an account?", + "login_page_messages_logout_success": "Sign out successfully!", + "login_page_messages_login_success": "Sign in successfully!", + "login_page_messages_email_invalid": "Email invalid!", + "sign_up_page_ui_title": "Sign up", + "sign_up_page_ui_create_account": "Create account", + "change_password_form_current_password": "Current password", + "change_password_form_new_password": "New password", + "change_password_form_confirm_password": "Confirm password", + "change_password_ui_title": "Change password", + "change_password_messages_password_not_match": "Password not match", + "change_password_messages_change_password_success": "Password changed successfully!", + "profile_form_name": "Name", + "profile_form_email": "Email", + "profile_form_role": "Role", + "profile_ui_title": "Profile", + "profile_messages_update_success": "Updated profile successfully!", + "settings_form_name": "Website name", + "settings_form_description": "Description", + "settings_form_keywords": "keywords", + "settings_form_language": "Language", + "settings_ui_title": "Settings", + "settings_messages_update_success": "Updated settings successfully!", + "settings_messages_update_fail": "Update fail!", + "backend_INVALID_EMAIL_OR_PASSWORD": "Email or password incorrect!", + "backend_INVALID_PASSWORD": "Password incorrect!" +} diff --git a/messages/vi.json b/messages/vi.json new file mode 100644 index 0000000..9d86956 --- /dev/null +++ b/messages/vi.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "common_no_list": "Hiện tại chưa có dữ liệu nào!", + "common_is_required": "{field} là bắt buộc.", + "role_tags_admin": "Quản lý", + "role_tags_user": "Người dùng", + "role_tags_member": "Thành viên", + "role_tags_owner": "Người sở hữu", + "ui_login_btn": "Đăng nhập", + "ui_logout_btn": "Đăng xuất", + "ui_cancel_btn": "Hủy", + "ui_close_btn": "Đóng", + "ui_confirm_btn": "Xác nhận", + "ui_signup_btn": "Đăng ký", + "ui_view_btn": "Xem", + "ui_save_btn": "Lưu", + "ui_update_btn": "Cập nhật", + "ui_view_all_notifications": "Xem tất cả thông báo", + "ui_label_notifications": "Thông báo", + "ui_change_password_btn": "Đổi mật khẩu", + "nav_home": "Trang chủ", + "nav_dashboard": "Bảng điều khiển", + "nav_settings": "Cài đặt", + "nav_add_new": "Thêm mới", + "nav_edit": "Chỉnh sửa", + "nav_change_password": "Đổi mật khẩu", + "nav_log": "Lịch sử", + "nav_roles": "Vai trò & quyền hạn", + "nav_box": "Hộp chứa", + "nav_account": "Tài khoản", + "nav_profile": "Hồ sơ", + "login_page_form_email": "Email", + "login_page_form_password": "Mật khẩu", + "login_page_ui_welcome_back": "Chào mừng trở lại", + "login_page_ui_forgot_password": "Quên mật khẩu?", + "login_page_ui_not_have_account": "Chưa có tài khoản!?", + "login_page_messages_logout_success": "Đăng xuất thành công!", + "login_page_messages_login_success": "Đăng nhập thành công!", + "login_page_messages_email_invalid": "Email không đúng định dạng!", + "sign_up_page_ui_title": "Đăng ký", + "sign_up_page_ui_create_account": "Tạo tài khoản", + "change_password_form_current_password": "Mật khẩu hiện tại", + "change_password_form_new_password": "Mật khẩu mới", + "change_password_form_confirm_password": "Nhập lại mật khẩu mới", + "change_password_ui_title": "Đổi mật khẩu", + "change_password_messages_password_not_match": "Mật khẩu không khớp", + "change_password_messages_change_password_success": "Đổi mật khẩu thành công!", + "profile_form_name": "Tên", + "profile_form_email": "Email", + "profile_form_role": "Vai trò", + "profile_ui_title": "Hồ sơ", + "profile_messages_update_success": "Cập nhật hồ sơ thành công!", + "settings_form_name": "Tên website", + "settings_form_description": "Mô tả website", + "settings_form_keywords": "Từ khóa", + "settings_form_language": "Ngôn ngữ", + "settings_ui_title": "Cài đặt", + "settings_messages_update_success": "Cập nhật cài đặt thành công!", + "settings_messages_update_fail": "Cập nhật cài đặt thất bại!", + "backend_INVALID_EMAIL_OR_PASSWORD": "Email hoặc mật khẩu không đúng!", + "backend_INVALID_PASSWORD": "Mật khẩu hiện tại không đúng!" +} diff --git a/package.json b/package.json index f6cb91b..45eae4f 100644 --- a/package.json +++ b/package.json @@ -37,13 +37,11 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "i18next": "^25.7.3", - "i18next-browser-languagedetector": "^8.2.0", "next-themes": "^0.4.6", "prisma": "^7.1.0", "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-i18next": "^16.5.0", "shadcn": "^3.6.1", "sonner": "^2.0.7", "tailwind-merge": "^3.0.2", @@ -53,6 +51,7 @@ "zod": "^4.1.11" }, "devDependencies": { + "@inlang/paraglide-js": "2.7.1", "@tanstack/devtools-vite": "^0.3.11", "@tanstack/eslint-config": "^0.3.0", "@testing-library/dom": "^10.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5523293..5d77308 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: i18next: specifier: ^25.7.3 version: 25.7.3(typescript@5.9.3) - i18next-browser-languagedetector: - specifier: ^8.2.0 - version: 8.2.0 next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -83,9 +80,6 @@ importers: react-dom: specifier: ^19.2.0 version: 19.2.3(react@19.2.3) - react-i18next: - specifier: ^16.5.0 - version: 16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) shadcn: specifier: ^3.6.1 version: 3.6.2(@types/node@22.19.3)(hono@4.10.6)(typescript@5.9.3) @@ -108,6 +102,9 @@ importers: specifier: ^4.1.11 version: 4.2.1 devDependencies: + '@inlang/paraglide-js': + specifier: 2.7.1 + version: 2.7.1 '@tanstack/devtools-vite': specifier: ^0.3.11 version: 0.3.12(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) @@ -683,6 +680,17 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inlang/paraglide-js@2.7.1': + resolution: {integrity: sha512-wCpnS9iRTRYMilvWBjB0ndf8+moon+AXz23Uh4wbpQjWhRJyvCytkGFzm7jeqAGggK4v3oeuyjva91TDMS+qhw==} + hasBin: true + + '@inlang/recommend-sherlock@0.2.1': + resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} + + '@inlang/sdk@2.4.10': + resolution: {integrity: sha512-MLcYSb9ERwvyfxMsMXGjmAYfh6Bn/rkT58ffkibWxbFLiL3ejSdmLGP8Wl7118dMo6nj2PXTcEPzUw+2YPQ62Q==} + engines: {node: '>=18.0.0'} + '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -742,6 +750,13 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lix-js/sdk@0.4.7': + resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==} + engines: {node: '>=18'} + + '@lix-js/server-protocol-schema@0.1.1': + resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + '@modelcontextprotocol/sdk@1.25.1': resolution: {integrity: sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==} engines: {node: '>=18'} @@ -1694,6 +1709,9 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sinclair/typebox@0.31.28': + resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} + '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} @@ -1728,6 +1746,10 @@ packages: peerDependencies: solid-js: ^1.6.12 + '@sqlite.org/sqlite-wasm@3.48.0-build4': + resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} + hasBin: true + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2388,6 +2410,9 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2619,6 +2644,10 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} + comment-json@4.5.1: + resolution: {integrity: sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==} + engines: {node: '>= 6'} + comment-parser@1.4.1: resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} engines: {node: '>= 12.0.0'} @@ -2629,6 +2658,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -2659,6 +2692,9 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -2719,6 +2755,14 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dedent@1.7.1: resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: @@ -3246,9 +3290,6 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} - htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} @@ -3267,6 +3308,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3275,9 +3320,6 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} - i18next-browser-languagedetector@8.2.0: - resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} - i18next@25.7.3: resolution: {integrity: sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==} peerDependencies: @@ -3420,6 +3462,9 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + js-sha256@0.11.1: + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3481,6 +3526,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kysely@0.27.6: + resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} + engines: {node: '>=14.0.0'} + kysely@0.28.9: resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} engines: {node: '>=20.0.0'} @@ -4037,22 +4086,6 @@ packages: peerDependencies: react: ^19.2.3 - react-i18next@16.5.0: - resolution: {integrity: sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==} - peerDependencies: - i18next: '>= 25.6.2' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - typescript: ^5 - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - typescript: - optional: true - react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -4290,6 +4323,11 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sqlite-wasm-kysely@0.3.0: + resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==} + peerDependencies: + kysely: '*' + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} @@ -4549,6 +4587,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -4577,6 +4618,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -4678,10 +4723,6 @@ packages: jsdom: optional: true - void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - vue-eslint-parser@10.2.0: resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5340,6 +5381,32 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inlang/paraglide-js@2.7.1': + dependencies: + '@inlang/recommend-sherlock': 0.2.1 + '@inlang/sdk': 2.4.10 + commander: 11.1.0 + consola: 3.4.0 + json5: 2.2.3 + unplugin: 2.3.11 + urlpattern-polyfill: 10.1.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@inlang/recommend-sherlock@0.2.1': + dependencies: + comment-json: 4.5.1 + + '@inlang/sdk@2.4.10': + dependencies: + '@lix-js/sdk': 0.4.7 + '@sinclair/typebox': 0.31.28 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 10.0.0 + transitivePeerDependencies: + - babel-plugin-macros + '@inquirer/ansi@1.0.2': {} '@inquirer/confirm@5.1.21(@types/node@22.19.3)': @@ -5393,6 +5460,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lix-js/sdk@0.4.7': + dependencies: + '@lix-js/server-protocol-schema': 0.1.1 + dedent: 1.5.1 + human-id: 4.1.3 + js-sha256: 0.11.1 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 10.0.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@lix-js/server-protocol-schema@0.1.1': {} + '@modelcontextprotocol/sdk@1.25.1(hono@4.10.6)(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.7(hono@4.10.6) @@ -6397,6 +6478,8 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@sinclair/typebox@0.31.28': {} + '@sindresorhus/merge-streams@4.0.0': {} '@solid-primitives/event-listener@2.4.3(solid-js@1.9.10)': @@ -6433,6 +6516,8 @@ snapshots: dependencies: solid-js: 1.9.10 + '@sqlite.org/sqlite-wasm@3.48.0-build4': {} + '@standard-schema/spec@1.1.0': {} '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@2.6.1))': @@ -7232,6 +7317,8 @@ snapshots: dependencies: dequal: 2.0.3 + array-timsort@1.0.3: {} + assertion-error@2.0.1: {} ast-types@0.16.1: @@ -7468,12 +7555,20 @@ snapshots: commander@14.0.2: {} + comment-json@4.5.1: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + comment-parser@1.4.1: {} concat-map@0.0.1: {} confbox@0.2.2: {} + consola@3.4.0: {} + consola@3.4.2: {} content-disposition@1.0.1: {} @@ -7490,6 +7585,8 @@ snapshots: cookie@1.1.1: {} + core-util-is@1.0.3: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -7548,6 +7645,8 @@ snapshots: decimal.js@10.6.0: {} + dedent@1.5.1: {} + dedent@1.7.1: {} deep-eql@5.0.2: {} @@ -8113,10 +8212,6 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - html-parse-stringify@3.0.1: - dependencies: - void-elements: 3.1.0 - htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -8148,14 +8243,12 @@ snapshots: transitivePeerDependencies: - supports-color + human-id@4.1.3: {} + human-signals@2.1.0: {} human-signals@8.0.1: {} - i18next-browser-languagedetector@8.2.0: - dependencies: - '@babel/runtime': 7.28.4 - i18next@25.7.3(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 @@ -8247,6 +8340,8 @@ snapshots: jose@6.1.3: {} + js-sha256@0.11.1: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -8312,6 +8407,8 @@ snapshots: kleur@4.1.5: {} + kysely@0.27.6: {} + kysely@0.28.9: {} launch-editor@2.12.0: @@ -8874,17 +8971,6 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 - react-i18next@16.5.0(i18next@25.7.3(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): - dependencies: - '@babel/runtime': 7.28.4 - html-parse-stringify: 3.0.1 - i18next: 25.7.3(typescript@5.9.3) - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) - optionalDependencies: - react-dom: 19.2.3(react@19.2.3) - typescript: 5.9.3 - react-is@17.0.2: {} react-refresh@0.18.0: {} @@ -9165,6 +9251,11 @@ snapshots: split2@4.2.0: {} + sqlite-wasm-kysely@0.3.0(kysely@0.27.6): + dependencies: + '@sqlite.org/sqlite-wasm': 3.48.0-build4 + kysely: 0.27.6 + sqlstring@2.3.3: {} srvx@0.9.8: {} @@ -9397,6 +9488,8 @@ snapshots: dependencies: punycode: 2.3.1 + urlpattern-polyfill@10.1.0: {} + use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.3): dependencies: react: 19.2.3 @@ -9418,6 +9511,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@10.0.0: {} + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -9517,8 +9612,6 @@ snapshots: - tsx - yaml - void-elements@3.1.0: {} - vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 4.4.3 diff --git a/prisma/data.ts b/prisma/data.ts index 038b96f..adef299 100644 --- a/prisma/data.ts +++ b/prisma/data.ts @@ -1,9 +1,4 @@ export const settingsData = [ - { - key: 'site_language', - value: 'en', - description: 'The language of the site', - }, { key: 'site_name', value: 'Fuware', diff --git a/prisma/seed.ts b/prisma/seed.ts index 688a201..777c947 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -11,6 +11,7 @@ const prisma = new PrismaClient({ adapter }); async function main() { console.log('🌱 Seeding database...'); + let admin; // check mail exists const mailExists = await prisma.user.findFirst({ @@ -20,7 +21,7 @@ async function main() { }); if (!mailExists) { // add admin user - await auth.api.createUser({ + admin = await auth.api.createUser({ body: { email: 'luu.dat.tham@gmail.com', password: 'Th@m!S@m!040390', @@ -33,10 +34,21 @@ async function main() { console.log('---------------Created admin user-----------------'); await prisma.setting.deleteMany(); + const listSettings = [ + ...settingsData, + { + key: admin ? (admin?.user?.id as string) : (mailExists?.id as string), + value: 'en', + description: 'User Settings', + relation: 'user', + }, + ]; + await prisma.setting.createMany({ - data: settingsData, + data: listSettings, skipDuplicates: true, }); + console.log('---------------Created settings-----------------'); // // Clear existing todos // await prisma.todo.deleteMany() diff --git a/project.inlang/.gitignore b/project.inlang/.gitignore new file mode 100644 index 0000000..5e46596 --- /dev/null +++ b/project.inlang/.gitignore @@ -0,0 +1 @@ +cache \ No newline at end of file diff --git a/project.inlang/project_id b/project.inlang/project_id new file mode 100644 index 0000000..4e1c6c5 --- /dev/null +++ b/project.inlang/project_id @@ -0,0 +1 @@ +7QuMkVtKjVPs6zTX3s \ No newline at end of file diff --git a/project.inlang/settings.json b/project.inlang/settings.json new file mode 100644 index 0000000..05729a3 --- /dev/null +++ b/project.inlang/settings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "vi"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@latest/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + }, + "plugin.inlang.i18next": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1f76740..35efaa1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,7 +1,7 @@ -import { useSession } from '@/lib/auth-client'; +import { m } from '@/paraglide/messages'; import { Separator } from '@base-ui/react/separator'; import { BellIcon } from '@phosphor-icons/react'; -import { useTranslation } from 'react-i18next'; +import { useAuth } from './auth/auth-provider'; import RouterBreadcrumb from './sidebar/RouterBreadcrumb'; import { Badge } from './ui/badge'; import { Button } from './ui/button'; @@ -17,8 +17,7 @@ import { import { SidebarTrigger } from './ui/sidebar'; export default function Header() { - const { t } = useTranslation(); - const { data: session } = useSession(); + const { data: session } = useAuth(); return ( <> @@ -50,7 +49,7 @@ export default function Header() { - {t('ui.label_notifications')} + {m.ui_label_notifications()} @@ -66,7 +65,7 @@ export default function Header() { - {t('ui.view_all_notifications')} + {m.ui_view_all_notifications()} diff --git a/src/components/auth/auth-provider.tsx b/src/components/auth/auth-provider.tsx new file mode 100644 index 0000000..89c7729 --- /dev/null +++ b/src/components/auth/auth-provider.tsx @@ -0,0 +1,40 @@ +import { ClientSession, useSession } from '@/lib/auth-client'; +import { BetterFetchError } from 'better-auth/client'; +import { createContext, useContext, useMemo } from 'react'; + +export type UserContext = { + data: ClientSession; + isAuth: boolean; + isAdmin: boolean; + isPending: boolean; + error: BetterFetchError | null; +}; + +const AuthContext = createContext(null); +export function AuthProvider({ children }: { children: React.ReactNode }) { + const { data: session, isPending, error } = useSession(); + + const contextSession: UserContext = useMemo( + () => ({ + data: session as ClientSession, + isPending, + error, + isAuth: !!session, + isAdmin: session?.user?.role ? session.user.role === 'admin' : false, + }), + [session], + ); + + return ( + + {children} + + ); +} +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return ctx; +} diff --git a/src/components/avatar/AvatarUser.tsx b/src/components/avatar/AvatarUser.tsx index b66db8a..08949dc 100644 --- a/src/components/avatar/AvatarUser.tsx +++ b/src/components/avatar/AvatarUser.tsx @@ -1,5 +1,5 @@ -import { useSession } from '@/lib/auth-client'; import { cn } from '@/lib/utils'; +import { useAuth } from '../auth/auth-provider'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import RoleRing from './RoleRing'; @@ -9,10 +9,12 @@ interface AvatarUserProps { } const AvatarUser = ({ className, textSize = 'md' }: AvatarUserProps) => { - const { data: session } = useSession(); + const { data: session } = useAuth(); const imagePath = session?.user?.image - ? `./data/avatar/${session?.user?.image}` + ? new URL(`../../../data/avatar/${session?.user?.image}`, import.meta.url) + .href : undefined; + const shortName = session?.user?.name ?.split(' ') .slice(0, 2) diff --git a/src/components/avatar/RoleBadge.tsx b/src/components/avatar/RoleBadge.tsx index d4eded3..a3eacd4 100644 --- a/src/components/avatar/RoleBadge.tsx +++ b/src/components/avatar/RoleBadge.tsx @@ -1,5 +1,5 @@ +import { m } from '@/paraglide/messages'; import { VariantProps } from 'class-variance-authority'; -import { useTranslation } from 'react-i18next'; import { Badge, badgeVariants } from '../ui/badge'; type BadgeVariant = VariantProps['variant']; @@ -10,8 +10,6 @@ type RoleProps = { }; const RoleBadge = ({ type, className }: RoleProps) => { - const { t } = useTranslation(); - // List all valid badge variant keys const validBadgeVariants: BadgeVariant[] = [ 'default', @@ -27,10 +25,10 @@ const RoleBadge = ({ type, className }: RoleProps) => { ]; const LABEL_VALUE = { - admin: t('roleTags.admin'), - user: t('roleTags.user'), - member: t('roleTags.member'), - owner: t('roleTags.owner'), + admin: m.role_tags_admin(), + user: m.role_tags_user(), + member: m.role_tags_member(), + owner: m.role_tags_owner(), }; // Determine the actual variant to apply. diff --git a/src/components/form/change-password-form.tsx b/src/components/form/change-password-form.tsx index 3f78532..2fcc96e 100644 --- a/src/components/form/change-password-form.tsx +++ b/src/components/form/change-password-form.tsx @@ -1,30 +1,30 @@ -import { authClient } from '@/lib/auth-client' -import i18n from '@/lib/i18n' -import { KeyIcon } from '@phosphor-icons/react' -import { useForm } from '@tanstack/react-form' -import { useTranslation } from 'react-i18next' -import { toast } from 'sonner' -import z from 'zod' -import { Button } from '../ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '../ui/card' -import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field' -import { Input } from '../ui/input' +import { authClient } from '@/lib/auth-client'; +import { m } from '@/paraglide/messages'; +import { KeyIcon } from '@phosphor-icons/react'; +import { useForm } from '@tanstack/react-form'; +import i18next from 'i18next'; +import { toast } from 'sonner'; +import z from 'zod'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'; +import { Input } from '../ui/input'; const ChangePasswordFormSchema = z .object({ currentPassword: z.string().nonempty( - i18n.t('changePassword.messages.is_required', { - field: i18n.t('changePassword.form.current_password'), + m.common_is_required({ + field: m.change_password_form_current_password(), }), ), newPassword: z.string().nonempty( - i18n.t('changePassword.messages.is_required', { - field: i18n.t('changePassword.form.new_password'), + m.common_is_required({ + field: m.change_password_form_new_password(), }), ), confirmPassword: z.string().nonempty( - i18n.t('changePassword.messages.is_required', { - field: i18n.t('changePassword.form.confirm_password'), + m.common_is_required({ + field: m.change_password_form_confirm_password(), }), ), }) @@ -33,22 +33,20 @@ const ChangePasswordFormSchema = z ctx.addIssue({ path: ['confirmPassword'], code: z.ZodIssueCode.custom, - message: i18n.t('changePassword.messages.password_not_match'), - }) + message: m.change_password_messages_password_not_match(), + }); } - }) + }); -type ChangePassword = z.infer +type ChangePassword = z.infer; const defaultValues: ChangePassword = { currentPassword: '', newPassword: '', confirmPassword: '', -} +}; const ChangePasswordForm = () => { - const { t } = useTranslation() - const form = useForm({ defaultValues, validators: { @@ -64,33 +62,40 @@ const ChangePasswordForm = () => { }, { onSuccess: () => { - form.reset() - toast.success(t('changePassword.messages.change_password_success')) + form.reset(); + toast.success( + m.change_password_messages_change_password_success(), + { + richColors: true, + }, + ); }, onError: (ctx) => { - console.log(ctx.error.code) - toast.error(t(`backend.${ctx.error.code}` as any)) + console.log(ctx.error.code); + toast.error(i18next.t(`backend_${ctx.error.code}` as any), { + richColors: true, + }); }, }, - ) + ); }, - }) + }); return ( - {t('changePassword.ui.title')} + {m.change_password_ui_title()}
{ - e.preventDefault() - e.stopPropagation() - form.handleSubmit() + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); }} > @@ -98,11 +103,11 @@ const ChangePasswordForm = () => { name="currentPassword" children={(field) => { const isInvalid = - field.state.meta.isTouched && !field.state.meta.isValid + field.state.meta.isTouched && !field.state.meta.isValid; return ( - {t('changePassword.form.current_password')}: + {m.change_password_form_current_password()}: { )} - ) + ); }} /> { const isInvalid = - field.state.meta.isTouched && !field.state.meta.isValid + field.state.meta.isTouched && !field.state.meta.isValid; return ( - {t('changePassword.form.new_password')}: + {m.change_password_form_new_password()}: { )} - ) + ); }} /> { const isInvalid = - field.state.meta.isTouched && !field.state.meta.isValid + field.state.meta.isTouched && !field.state.meta.isValid; return ( - {t('changePassword.form.confirm_password')}: + {m.change_password_form_confirm_password()}: { )} - ) + ); }} /> - +
- ) -} + ); +}; -export default ChangePasswordForm +export default ChangePasswordForm; diff --git a/src/components/form/profile-form.tsx b/src/components/form/profile-form.tsx index b752a9d..83df190 100644 --- a/src/components/form/profile-form.tsx +++ b/src/components/form/profile-form.tsx @@ -1,12 +1,14 @@ -import { authClient, useSession } from '@/lib/auth-client'; +import { authClient } from '@/lib/auth-client'; +import { m } from '@/paraglide/messages'; import { uploadProfileImage } from '@/service/profile.api'; import { ProfileInput, profileUpdateSchema } from '@/service/profile.schema'; import { UserCircleIcon } from '@phosphor-icons/react'; import { useForm } from '@tanstack/react-form'; import { useQueryClient } from '@tanstack/react-query'; +import i18next from 'i18next'; import { useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { useAuth } from '../auth/auth-provider'; import AvatarUser from '../avatar/AvatarUser'; import RoleBadge from '../avatar/RoleBadge'; import { Button } from '../ui/button'; @@ -20,9 +22,8 @@ const defaultValues: ProfileInput = { }; const ProfileForm = () => { - const { t } = useTranslation(); const fileInputRef = useRef(null); - const { data: session, isPending } = useSession(); + const { data: session, isPending } = useAuth(); const queryClient = useQueryClient(); const form = useForm({ @@ -61,10 +62,14 @@ const ProfileForm = () => { queryClient.refetchQueries({ queryKey: ['auth', 'session'], }); - toast.success(t('profile.messages.update_success')); + toast.success(m.profile_messages_update_success(), { + richColors: true, + }); }, onError: (ctx) => { - toast.error(t(`backend.${ctx.error.code}` as any)); + toast.error(i18next.t(`backend.${ctx.error.code}` as any), { + richColors: true, + }); }, }, ); @@ -80,7 +85,7 @@ const ProfileForm = () => { - {t('profile.ui.title')} + {m.profile_ui_title()} @@ -130,7 +135,7 @@ const ProfileForm = () => { return ( - {t('profile.form.name')} + {m.profile_form_name()} { }} /> - {t('profile.form.email')} + {m.profile_form_email()} { /> - {t('profile.form.role')} + {m.profile_form_role()}
- + diff --git a/src/components/form/settings-form.tsx b/src/components/form/settings-form.tsx index 09f411e..f88306c 100644 --- a/src/components/form/settings-form.tsx +++ b/src/components/form/settings-form.tsx @@ -1,33 +1,24 @@ +import { m } from '@/paraglide/messages'; import { settingQueries } from '@/service/queries'; import { updateSettings } from '@/service/setting.api'; import { settingSchema, SettingsInput } from '@/service/setting.schema'; import { GearIcon } from '@phosphor-icons/react'; import { useForm } from '@tanstack/react-form'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Field, FieldError, FieldGroup, FieldLabel } from '../ui/field'; import { Input } from '../ui/input'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '../ui/select'; import { Textarea } from '../ui/textarea'; const defaultValues: SettingsInput = { - site_language: '', site_name: '', site_description: '', site_keywords: '', }; const SettingsForm = () => { - const { t } = useTranslation(); const queryClient = useQueryClient(); const { data: settings } = useQuery(settingQueries.list()); @@ -35,8 +26,11 @@ const SettingsForm = () => { const updateMutation = useMutation({ mutationFn: updateSettings, onSuccess: () => { + // setLocale(variables.data.site_language as Locale); queryClient.invalidateQueries({ queryKey: settingQueries.all }); - toast.success(t('settings.messages.update_success')); + toast.success(m.settings_messages_update_success(), { + richColors: true, + }); }, }); @@ -46,7 +40,6 @@ const SettingsForm = () => { site_name: settings?.site_name?.value || '', site_description: settings?.site_description?.value || '', site_keywords: settings?.site_keywords?.value || '', - site_language: settings?.site_language?.value || '', }, validators: { onSubmit: settingSchema, @@ -62,7 +55,7 @@ const SettingsForm = () => { - {t('settings.ui.title')} + {m.settings_ui_title()} @@ -83,7 +76,7 @@ const SettingsForm = () => { return ( - {t('settings.form.name')} + {m.settings_form_name()} { return ( - {t('settings.form.description')} + {m.settings_form_description()}