diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..3b44906 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,145 @@ +{ + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "back": "Back", + "next": "Next", + "loading": "Loading...", + "submit": "Submit", + "retry": "Try again", + "ok": "OK" + }, + "appName": "wacrm", + "appTagline": "CRM Template for WhatsApp", + "nav": { + "dashboard": "Dashboard", + "inbox": "Inbox", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "flows": "Flows", + "settings": "Settings", + "beta": "Beta", + "betaFeatureAria": "Beta feature", + "unreadAria": "{count, plural, =1 {# unread conversation} other {# unread conversations}}", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "primaryAria": "Primary", + "openAccountMenu": "Open account menu", + "avatarAlt": "Avatar", + "userFallback": "User", + "profile": "Profile", + "signOut": "Sign out" + }, + "roles": { + "owner": "Owner", + "admin": "Admin", + "agent": "Agent", + "viewer": "Viewer" + }, + "auth": { + "login": { + "title": "Welcome back", + "titleInvite": "Sign in to accept", + "subtitle": "Sign in to your account", + "subtitleInvite": "Sign in and we'll take you to the invitation.", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter your password", + "forgotPassword": "Forgot password?", + "submit": "Sign in", + "submitting": "Signing in...", + "noAccount": "Don't have an account?", + "createAccount": "Create account" + }, + "signup": { + "title": "Create account", + "titleInvite": "Create account & join", + "subtitle": "Get started with CRM Template for WhatsApp", + "subtitleInvite": "Verify your email, then accept the invitation to join your team.", + "fullNameLabel": "Full name", + "fullNamePlaceholder": "John Doe", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "At least 6 characters", + "confirmPasswordLabel": "Confirm password", + "confirmPasswordPlaceholder": "Repeat your password", + "submit": "Create account", + "submitting": "Creating account...", + "haveAccount": "Already have an account?", + "signIn": "Sign in", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a confirmation link to {email}. Please check your inbox and click the link to verify your account.", + "backToSignIn": "Back to sign in" + }, + "forgotPassword": { + "title": "Reset password", + "subtitle": "Enter your email and we'll send you a reset link", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "submit": "Send reset link", + "submitting": "Sending...", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a password reset link to {email}. Please check your inbox.", + "backToSignIn": "Back to sign in" + } + }, + "join": { + "verifying": "Verifying invitation…", + "invitedToTitle": "You're invited to {accountName}", + "invitedAs": "You'll join as {role}. Link valid until {date, date, medium}.", + "acceptInvitation": "Accept invitation", + "accepting": "Accepting…", + "acceptFooter": "Accepting moves your login into {accountName}. Your empty personal account from signup will be cleaned up.", + "createAccountAndJoin": "Create account & join", + "alreadyHaveAccount": "I already have an account", + "conflictTitle": "Can't join {accountName} with this account", + "conflictBody": "To join {accountName}, sign out and sign up again with a different email address. The invite link stays valid as long as it hasn't expired.", + "staySignedIn": "Stay signed in", + "signOutAndRetry": "Sign out & use a different email", + "signingOut": "Signing out…", + "createNewAccountInstead": "Create a new account instead", + "signIn": "Sign in", + "tryAgain": "Try again", + "redeemConflictFallback": "You are already in another account. Sign in with a different email to join this one.", + "redeemFailedFallback": "Failed to accept invitation", + "redeemNetworkError": "Could not reach the server", + "signOutError": "Could not sign out. Try refreshing the page.", + "welcomeToast": "Welcome to the team", + "fail": { + "notFound": { + "title": "Invite not found", + "body": "This link doesn't match a valid invitation. Double-check the URL or ask the person who invited you to send a new one." + }, + "used": { + "title": "Invite already used", + "body": "This invitation has already been accepted. If that wasn't you, ask the account admin to send a fresh link." + }, + "expired": { + "title": "Invite expired", + "body": "This invitation has expired. Ask the account admin to send a new one — they take a few seconds to generate." + }, + "serverError": { + "title": "Something went wrong", + "body": "We couldn't verify this invitation right now. Try refreshing the page in a moment." + } + } + }, + "localeSwitcher": { + "label": "Language", + "ariaLabel": "Change language", + "locales": { + "en": "English", + "es": "Español", + "lt": "Lietuvių" + } + } +} diff --git a/messages/es.json b/messages/es.json new file mode 100644 index 0000000..3b44906 --- /dev/null +++ b/messages/es.json @@ -0,0 +1,145 @@ +{ + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "back": "Back", + "next": "Next", + "loading": "Loading...", + "submit": "Submit", + "retry": "Try again", + "ok": "OK" + }, + "appName": "wacrm", + "appTagline": "CRM Template for WhatsApp", + "nav": { + "dashboard": "Dashboard", + "inbox": "Inbox", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "flows": "Flows", + "settings": "Settings", + "beta": "Beta", + "betaFeatureAria": "Beta feature", + "unreadAria": "{count, plural, =1 {# unread conversation} other {# unread conversations}}", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "primaryAria": "Primary", + "openAccountMenu": "Open account menu", + "avatarAlt": "Avatar", + "userFallback": "User", + "profile": "Profile", + "signOut": "Sign out" + }, + "roles": { + "owner": "Owner", + "admin": "Admin", + "agent": "Agent", + "viewer": "Viewer" + }, + "auth": { + "login": { + "title": "Welcome back", + "titleInvite": "Sign in to accept", + "subtitle": "Sign in to your account", + "subtitleInvite": "Sign in and we'll take you to the invitation.", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter your password", + "forgotPassword": "Forgot password?", + "submit": "Sign in", + "submitting": "Signing in...", + "noAccount": "Don't have an account?", + "createAccount": "Create account" + }, + "signup": { + "title": "Create account", + "titleInvite": "Create account & join", + "subtitle": "Get started with CRM Template for WhatsApp", + "subtitleInvite": "Verify your email, then accept the invitation to join your team.", + "fullNameLabel": "Full name", + "fullNamePlaceholder": "John Doe", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "At least 6 characters", + "confirmPasswordLabel": "Confirm password", + "confirmPasswordPlaceholder": "Repeat your password", + "submit": "Create account", + "submitting": "Creating account...", + "haveAccount": "Already have an account?", + "signIn": "Sign in", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a confirmation link to {email}. Please check your inbox and click the link to verify your account.", + "backToSignIn": "Back to sign in" + }, + "forgotPassword": { + "title": "Reset password", + "subtitle": "Enter your email and we'll send you a reset link", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "submit": "Send reset link", + "submitting": "Sending...", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a password reset link to {email}. Please check your inbox.", + "backToSignIn": "Back to sign in" + } + }, + "join": { + "verifying": "Verifying invitation…", + "invitedToTitle": "You're invited to {accountName}", + "invitedAs": "You'll join as {role}. Link valid until {date, date, medium}.", + "acceptInvitation": "Accept invitation", + "accepting": "Accepting…", + "acceptFooter": "Accepting moves your login into {accountName}. Your empty personal account from signup will be cleaned up.", + "createAccountAndJoin": "Create account & join", + "alreadyHaveAccount": "I already have an account", + "conflictTitle": "Can't join {accountName} with this account", + "conflictBody": "To join {accountName}, sign out and sign up again with a different email address. The invite link stays valid as long as it hasn't expired.", + "staySignedIn": "Stay signed in", + "signOutAndRetry": "Sign out & use a different email", + "signingOut": "Signing out…", + "createNewAccountInstead": "Create a new account instead", + "signIn": "Sign in", + "tryAgain": "Try again", + "redeemConflictFallback": "You are already in another account. Sign in with a different email to join this one.", + "redeemFailedFallback": "Failed to accept invitation", + "redeemNetworkError": "Could not reach the server", + "signOutError": "Could not sign out. Try refreshing the page.", + "welcomeToast": "Welcome to the team", + "fail": { + "notFound": { + "title": "Invite not found", + "body": "This link doesn't match a valid invitation. Double-check the URL or ask the person who invited you to send a new one." + }, + "used": { + "title": "Invite already used", + "body": "This invitation has already been accepted. If that wasn't you, ask the account admin to send a fresh link." + }, + "expired": { + "title": "Invite expired", + "body": "This invitation has expired. Ask the account admin to send a new one — they take a few seconds to generate." + }, + "serverError": { + "title": "Something went wrong", + "body": "We couldn't verify this invitation right now. Try refreshing the page in a moment." + } + } + }, + "localeSwitcher": { + "label": "Language", + "ariaLabel": "Change language", + "locales": { + "en": "English", + "es": "Español", + "lt": "Lietuvių" + } + } +} diff --git a/messages/lt.json b/messages/lt.json new file mode 100644 index 0000000..3b44906 --- /dev/null +++ b/messages/lt.json @@ -0,0 +1,145 @@ +{ + "common": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "back": "Back", + "next": "Next", + "loading": "Loading...", + "submit": "Submit", + "retry": "Try again", + "ok": "OK" + }, + "appName": "wacrm", + "appTagline": "CRM Template for WhatsApp", + "nav": { + "dashboard": "Dashboard", + "inbox": "Inbox", + "contacts": "Contacts", + "pipelines": "Pipelines", + "broadcasts": "Broadcasts", + "automations": "Automations", + "flows": "Flows", + "settings": "Settings", + "beta": "Beta", + "betaFeatureAria": "Beta feature", + "unreadAria": "{count, plural, =1 {# unread conversation} other {# unread conversations}}", + "openMenu": "Open menu", + "closeMenu": "Close menu", + "primaryAria": "Primary", + "openAccountMenu": "Open account menu", + "avatarAlt": "Avatar", + "userFallback": "User", + "profile": "Profile", + "signOut": "Sign out" + }, + "roles": { + "owner": "Owner", + "admin": "Admin", + "agent": "Agent", + "viewer": "Viewer" + }, + "auth": { + "login": { + "title": "Welcome back", + "titleInvite": "Sign in to accept", + "subtitle": "Sign in to your account", + "subtitleInvite": "Sign in and we'll take you to the invitation.", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter your password", + "forgotPassword": "Forgot password?", + "submit": "Sign in", + "submitting": "Signing in...", + "noAccount": "Don't have an account?", + "createAccount": "Create account" + }, + "signup": { + "title": "Create account", + "titleInvite": "Create account & join", + "subtitle": "Get started with CRM Template for WhatsApp", + "subtitleInvite": "Verify your email, then accept the invitation to join your team.", + "fullNameLabel": "Full name", + "fullNamePlaceholder": "John Doe", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "At least 6 characters", + "confirmPasswordLabel": "Confirm password", + "confirmPasswordPlaceholder": "Repeat your password", + "submit": "Create account", + "submitting": "Creating account...", + "haveAccount": "Already have an account?", + "signIn": "Sign in", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a confirmation link to {email}. Please check your inbox and click the link to verify your account.", + "backToSignIn": "Back to sign in" + }, + "forgotPassword": { + "title": "Reset password", + "subtitle": "Enter your email and we'll send you a reset link", + "emailLabel": "Email", + "emailPlaceholder": "you@example.com", + "submit": "Send reset link", + "submitting": "Sending...", + "checkEmailTitle": "Check your email", + "checkEmailBody": "We've sent a password reset link to {email}. Please check your inbox.", + "backToSignIn": "Back to sign in" + } + }, + "join": { + "verifying": "Verifying invitation…", + "invitedToTitle": "You're invited to {accountName}", + "invitedAs": "You'll join as {role}. Link valid until {date, date, medium}.", + "acceptInvitation": "Accept invitation", + "accepting": "Accepting…", + "acceptFooter": "Accepting moves your login into {accountName}. Your empty personal account from signup will be cleaned up.", + "createAccountAndJoin": "Create account & join", + "alreadyHaveAccount": "I already have an account", + "conflictTitle": "Can't join {accountName} with this account", + "conflictBody": "To join {accountName}, sign out and sign up again with a different email address. The invite link stays valid as long as it hasn't expired.", + "staySignedIn": "Stay signed in", + "signOutAndRetry": "Sign out & use a different email", + "signingOut": "Signing out…", + "createNewAccountInstead": "Create a new account instead", + "signIn": "Sign in", + "tryAgain": "Try again", + "redeemConflictFallback": "You are already in another account. Sign in with a different email to join this one.", + "redeemFailedFallback": "Failed to accept invitation", + "redeemNetworkError": "Could not reach the server", + "signOutError": "Could not sign out. Try refreshing the page.", + "welcomeToast": "Welcome to the team", + "fail": { + "notFound": { + "title": "Invite not found", + "body": "This link doesn't match a valid invitation. Double-check the URL or ask the person who invited you to send a new one." + }, + "used": { + "title": "Invite already used", + "body": "This invitation has already been accepted. If that wasn't you, ask the account admin to send a fresh link." + }, + "expired": { + "title": "Invite expired", + "body": "This invitation has expired. Ask the account admin to send a new one — they take a few seconds to generate." + }, + "serverError": { + "title": "Something went wrong", + "body": "We couldn't verify this invitation right now. Try refreshing the page in a moment." + } + } + }, + "localeSwitcher": { + "label": "Language", + "ariaLabel": "Change language", + "locales": { + "en": "English", + "es": "Español", + "lt": "Lietuvių" + } + } +} diff --git a/next.config.ts b/next.config.ts index 02bd16f..a4e23e3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,5 +1,8 @@ +import createNextIntlPlugin from "next-intl/plugin"; import type { NextConfig } from "next"; +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); + /** * Baseline security headers applied to every response. * @@ -127,4 +130,4 @@ const nextConfig: NextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package-lock.json b/package-lock.json index fc3eb84..74f0eaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "date-fns": "^4.3.0", "lucide-react": "^1.16.0", "next": "16.2.6", + "next-intl": "^4.13.0", "react": "19.2.4", "react-dom": "19.2.4", "recharts": "^3.8.1", @@ -1015,6 +1016,36 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz", + "integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.10.tgz", + "integrity": "sha512-XeJihYLy1lCe19xfK1KWKG/betBOK2rB0luL8lSkjfvJj0zP+LTJvkC+RKd0jsFI8mWxN71LrarHSrEXE8xxOQ==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.9" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.9.tgz", + "integrity": "sha512-rsxswgHMfU1zUgB2byc08fesf83wLGjFnzLCEtuf00mx2doiqc6pYrf67raI37XqdRcGUviQepk2UKGqpng74Q==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.9.tgz", + "integrity": "sha512-GmB0F/gYh4Hdl4rLWjgDsgT+x4pB54fkJeRh8kAZ4XFzKeCK8dGs+SBJWXO42QZtOUni+IDWKNuCw6wiL4lTvw==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.5" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -2025,6 +2056,313 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", @@ -2351,6 +2689,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -2490,6 +2834,204 @@ "node": ">=20.0.0" } }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz", + "integrity": "sha512-PaYyclfmQ++77D8ityYvmmVzHv9aG8ROwt2GfG6/ccloy4Hgf80qtOnzb9VYvPsUT7Ty1uhuDRhv3XYpf62qhQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.40.tgz", + "integrity": "sha512-HbbPzvfLBUXjIB1Ezks+//lNUjmLjfyd63XSwprJgrZaXYdm70kohXPJUWdqKZozolFxbPaO+xtBaiUp6BoueA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.40.tgz", + "integrity": "sha512-SlRZsCjOCPR2LvFs0Ri/Xrx/5o5TCt8vl4gW6mX1hEZOG0a625RxzRHpHdAQNGykmAN/7IeaFAJG+QnNmxlHcA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.40.tgz", + "integrity": "sha512-Q8byxJt2fh8CR3EUX6snBpy47AoBVm+In/+Z3rjDHMjC38ZvR9/gtUUNCT0tfrn4EdVsO8/QPi59nxrxvqxvBQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.40.tgz", + "integrity": "sha512-4z0MgHU+7M0pZDqBN1El7mFXDI1SBwinfcUkAyA4v8QrhOIUOZltySt2aStQLZGrdXVXM4Y4ylfiTC04ED+MoQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.40.tgz", + "integrity": "sha512-fLI4iUgeSZu0eRWUXwe6YzPFx9gHbFiPkl8Rp3mJfP8OpNR3nTQCGPvHdDh9xniW7mVvgMY4ni7A4VzqI1KrpA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.40.tgz", + "integrity": "sha512-YqeKMAb7d4nQSGMJQ454IlaCENpzcDqhvBE9+CPfdnYpnUXxd+BSrB6Xk0YjW8UyoEhUj4p6quATCxbsp6J3jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.40.tgz", + "integrity": "sha512-7HOuS1iGcme/j/TuL1TfmmLGiMQrjv/GmjyZeydl00FKPtpGXEldwqfI56xgd1YzrzoB2svWjxbGGyQ0TEASxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.40.tgz", + "integrity": "sha512-h4kZYHc7dpc9P9u4brRJaS8Pl7tPVHAeiLSzw7T5RfIJgAoSdaCMKzI/2Uay9gFhaw8uyCDl0L5q37r0EpAfIA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.40.tgz", + "integrity": "sha512-+mQgKZXSj6mV38Zh05QaxSjUDmGP/R2JWlXZTDLSPkDzHU6p3GxN9eeSf5dfyDVU86946fmCvSzyl/ucImx8+A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.40.tgz", + "integrity": "sha512-yvwdPLGd25mcj/mNatjNQ0lZujtQD6psH3v9PNmMb+fSzjbNG8KIDxjFWrcV+fsFVLOkyOmdJsFmX7NAFjVyPw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.40.tgz", + "integrity": "sha512-OXtKsLU1bVtInzzDEAY2sYiF/rl4tvAnLLLpuMp3HzAOQZ5A+i69AKDhA1YLQTaMAqO3vzyYNVAYVRMPtSYD4w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2499,6 +3041,15 @@ "tslib": "^2.8.0" } }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -5133,7 +5684,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -6760,6 +7310,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/icu-minify": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.0.tgz", + "integrity": "sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6835,6 +7400,16 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.7.tgz", + "integrity": "sha512-+q6Ktg119nULZEpZ8YTuGOst9MyEzFtjD63FTGBlN1mLz0Z/MOUYDIvnpVKwq17eezIEh+cfJIebfJoCetpiNw==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.5", + "@formatjs/icu-messageformat-parser": "3.5.10" + } + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -8302,6 +8877,89 @@ } } }, + "node_modules/next-intl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.0.tgz", + "integrity": "sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.13.0", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.13.0", + "po-parser": "^2.1.1", + "use-intl": "^4.13.0" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.0.tgz", + "integrity": "sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.40", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.40.tgz", + "integrity": "sha512-2kwzJikRvgtNAG7MwVZY2vEzZjTxKIq5jXOihuSV/8U+Hej8Va22t65aKnJZs3P+NwojZvR8Mf8kyM7O+V8sQg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.40", + "@swc/core-darwin-x64": "1.15.40", + "@swc/core-linux-arm-gnueabihf": "1.15.40", + "@swc/core-linux-arm64-gnu": "1.15.40", + "@swc/core-linux-arm64-musl": "1.15.40", + "@swc/core-linux-ppc64-gnu": "1.15.40", + "@swc/core-linux-s390x-gnu": "1.15.40", + "@swc/core-linux-x64-gnu": "1.15.40", + "@swc/core-linux-x64-musl": "1.15.40", + "@swc/core-win32-arm64-msvc": "1.15.40", + "@swc/core-win32-ia32-msvc": "1.15.40", + "@swc/core-win32-x64-msvc": "1.15.40" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -8822,6 +9480,12 @@ "node": ">=16.20.0" } }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -10802,6 +11466,27 @@ "punycode": "^2.1.0" } }, + "node_modules/use-intl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.0.tgz", + "integrity": "sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.13.0", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", diff --git a/package.json b/package.json index a0d863b..5200c25 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "date-fns": "^4.3.0", "lucide-react": "^1.16.0", "next": "16.2.6", + "next-intl": "^4.13.0", "react": "19.2.4", "react-dom": "19.2.4", "recharts": "^3.8.1", diff --git a/src/app/(auth)/forgot-password/page.tsx b/src/app/[locale]/(auth)/forgot-password/page.tsx similarity index 85% rename from src/app/(auth)/forgot-password/page.tsx rename to src/app/[locale]/(auth)/forgot-password/page.tsx index 02ff209..b0c10f6 100644 --- a/src/app/(auth)/forgot-password/page.tsx +++ b/src/app/[locale]/(auth)/forgot-password/page.tsx @@ -1,7 +1,8 @@ "use client"; import { useState } from "react"; -import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; import { createClient } from "@/lib/supabase/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -16,6 +17,7 @@ import { import { MessageSquare, CheckCircle, ArrowLeft } from "lucide-react"; export default function ForgotPasswordPage() { + const t = useTranslations("auth.forgotPassword"); const [email, setEmail] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); @@ -50,12 +52,15 @@ export default function ForgotPasswordPage() { - Check your email + {t("checkEmailTitle")} - We've sent a password reset link to{" "} - {email}. Please check your - inbox. + {t.rich("checkEmailBody", { + email, + highlight: (chunks) => ( + {chunks} + ), + })} @@ -64,7 +69,7 @@ export default function ForgotPasswordPage() { variant="outline" className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" > - Back to sign in + {t("backToSignIn")} @@ -80,9 +85,9 @@ export default function ForgotPasswordPage() {
- Reset password + {t("title")} - Enter your email and we'll send you a reset link + {t("subtitle")} @@ -95,12 +100,12 @@ export default function ForgotPasswordPage() {
setEmail(e.target.value)} required @@ -113,7 +118,7 @@ export default function ForgotPasswordPage() { disabled={loading} className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50" > - {loading ? "Sending..." : "Send reset link"} + {loading ? t("submitting") : t("submit")} @@ -122,7 +127,7 @@ export default function ForgotPasswordPage() { className="mt-6 flex items-center justify-center gap-2 text-sm text-slate-400 hover:text-slate-300" > - Back to sign in + {t("backToSignIn")} diff --git a/src/app/(auth)/layout.tsx b/src/app/[locale]/(auth)/layout.tsx similarity index 100% rename from src/app/(auth)/layout.tsx rename to src/app/[locale]/(auth)/layout.tsx diff --git a/src/app/(auth)/login/page.tsx b/src/app/[locale]/(auth)/login/page.tsx similarity index 88% rename from src/app/(auth)/login/page.tsx rename to src/app/[locale]/(auth)/login/page.tsx index 1a53226..ac25695 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/[locale]/(auth)/login/page.tsx @@ -1,8 +1,9 @@ "use client"; import { Suspense, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { Link, useRouter } from "@/i18n/navigation"; import { createClient } from "@/lib/supabase/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -30,6 +31,7 @@ export default function LoginPage() { } function LoginPageInner() { + const t = useTranslations("auth.login"); const searchParams = useSearchParams(); // Forwarded from `/join/` when the visitor already has an // account. After a successful sign-in we send them to the join @@ -78,12 +80,10 @@ function LoginPageInner() { )}
- {inviteToken ? "Sign in to accept" : "Welcome back"} + {inviteToken ? t("titleInvite") : t("title")} - {inviteToken - ? "Sign in and we'll take you to the invitation." - : "Sign in to your account"} + {inviteToken ? t("subtitleInvite") : t("subtitle")} @@ -96,12 +96,12 @@ function LoginPageInner() {
setEmail(e.target.value)} required @@ -112,19 +112,19 @@ function LoginPageInner() {
- Forgot password? + {t("forgotPassword")}
setPassword(e.target.value)} required @@ -137,12 +137,12 @@ function LoginPageInner() { disabled={loading} className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50" > - {loading ? "Signing in..." : "Sign in"} + {loading ? t("submitting") : t("submit")}

- Don't have an account?{" "} + {t("noAccount")}{" "} - Create account + {t("createAccount")}

diff --git a/src/app/(auth)/signup/page.tsx b/src/app/[locale]/(auth)/signup/page.tsx similarity index 87% rename from src/app/(auth)/signup/page.tsx rename to src/app/[locale]/(auth)/signup/page.tsx index c431fe9..875ab4a 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/[locale]/(auth)/signup/page.tsx @@ -1,8 +1,9 @@ "use client"; import { Suspense, useState } from "react"; -import Link from "next/link"; import { useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { Link } from "@/i18n/navigation"; import { createClient } from "@/lib/supabase/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -27,6 +28,7 @@ export default function SignupPage() { } function SignupPageInner() { + const t = useTranslations("auth.signup"); const searchParams = useSearchParams(); // When the user lands here from `/join/` we carry the // invite token in the query so it survives the signup → email @@ -49,12 +51,12 @@ function SignupPageInner() { setError(null); if (password !== confirmPassword) { - setError("Passwords do not match"); + setError(t("passwordMismatch")); return; } if (password.length < 6) { - setError("Password must be at least 6 characters"); + setError(t("passwordTooShort")); return; } @@ -98,12 +100,15 @@ function SignupPageInner() {
- Check your email + {t("checkEmailTitle")} - We've sent a confirmation link to{" "} - {email}. Please check your - inbox and click the link to verify your account. + {t.rich("checkEmailBody", { + email, + highlight: (chunks) => ( + {chunks} + ), + })} @@ -118,7 +123,7 @@ function SignupPageInner() { variant="outline" className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" > - Back to sign in + {t("backToSignIn")} @@ -139,12 +144,10 @@ function SignupPageInner() { )}
- {inviteToken ? "Create account & join" : "Create account"} + {inviteToken ? t("titleInvite") : t("title")} - {inviteToken - ? "Verify your email, then accept the invitation to join your team." - : "Get started with CRM Template for WhatsApp"} + {inviteToken ? t("subtitleInvite") : t("subtitle")} @@ -157,12 +160,12 @@ function SignupPageInner() {
setFullName(e.target.value)} required @@ -172,12 +175,12 @@ function SignupPageInner() {
setEmail(e.target.value)} required @@ -187,12 +190,12 @@ function SignupPageInner() {
setPassword(e.target.value)} required @@ -202,12 +205,12 @@ function SignupPageInner() {
setConfirmPassword(e.target.value)} required @@ -220,12 +223,12 @@ function SignupPageInner() { disabled={loading} className="mt-2 h-10 w-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50" > - {loading ? "Creating account..." : "Create account"} + {loading ? t("submitting") : t("submit")}

- Already have an account?{" "} + {t("haveAccount")}{" "} - Sign in + {t("signIn")}

diff --git a/src/app/(dashboard)/automations/[id]/edit/page.tsx b/src/app/[locale]/(dashboard)/automations/[id]/edit/page.tsx similarity index 100% rename from src/app/(dashboard)/automations/[id]/edit/page.tsx rename to src/app/[locale]/(dashboard)/automations/[id]/edit/page.tsx diff --git a/src/app/(dashboard)/automations/[id]/logs/page.tsx b/src/app/[locale]/(dashboard)/automations/[id]/logs/page.tsx similarity index 100% rename from src/app/(dashboard)/automations/[id]/logs/page.tsx rename to src/app/[locale]/(dashboard)/automations/[id]/logs/page.tsx diff --git a/src/app/(dashboard)/automations/new/page.tsx b/src/app/[locale]/(dashboard)/automations/new/page.tsx similarity index 100% rename from src/app/(dashboard)/automations/new/page.tsx rename to src/app/[locale]/(dashboard)/automations/new/page.tsx diff --git a/src/app/(dashboard)/automations/page.tsx b/src/app/[locale]/(dashboard)/automations/page.tsx similarity index 100% rename from src/app/(dashboard)/automations/page.tsx rename to src/app/[locale]/(dashboard)/automations/page.tsx diff --git a/src/app/(dashboard)/broadcasts/[id]/page.tsx b/src/app/[locale]/(dashboard)/broadcasts/[id]/page.tsx similarity index 100% rename from src/app/(dashboard)/broadcasts/[id]/page.tsx rename to src/app/[locale]/(dashboard)/broadcasts/[id]/page.tsx diff --git a/src/app/(dashboard)/broadcasts/new/page.tsx b/src/app/[locale]/(dashboard)/broadcasts/new/page.tsx similarity index 100% rename from src/app/(dashboard)/broadcasts/new/page.tsx rename to src/app/[locale]/(dashboard)/broadcasts/new/page.tsx diff --git a/src/app/(dashboard)/broadcasts/page.tsx b/src/app/[locale]/(dashboard)/broadcasts/page.tsx similarity index 100% rename from src/app/(dashboard)/broadcasts/page.tsx rename to src/app/[locale]/(dashboard)/broadcasts/page.tsx diff --git a/src/app/(dashboard)/contacts/page.tsx b/src/app/[locale]/(dashboard)/contacts/page.tsx similarity index 100% rename from src/app/(dashboard)/contacts/page.tsx rename to src/app/[locale]/(dashboard)/contacts/page.tsx diff --git a/src/app/(dashboard)/dashboard-shell.tsx b/src/app/[locale]/(dashboard)/dashboard-shell.tsx similarity index 100% rename from src/app/(dashboard)/dashboard-shell.tsx rename to src/app/[locale]/(dashboard)/dashboard-shell.tsx diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/[locale]/(dashboard)/dashboard/page.tsx similarity index 100% rename from src/app/(dashboard)/dashboard/page.tsx rename to src/app/[locale]/(dashboard)/dashboard/page.tsx diff --git a/src/app/(dashboard)/flows/[id]/page.tsx b/src/app/[locale]/(dashboard)/flows/[id]/page.tsx similarity index 100% rename from src/app/(dashboard)/flows/[id]/page.tsx rename to src/app/[locale]/(dashboard)/flows/[id]/page.tsx diff --git a/src/app/(dashboard)/flows/[id]/runs/page.tsx b/src/app/[locale]/(dashboard)/flows/[id]/runs/page.tsx similarity index 100% rename from src/app/(dashboard)/flows/[id]/runs/page.tsx rename to src/app/[locale]/(dashboard)/flows/[id]/runs/page.tsx diff --git a/src/app/(dashboard)/flows/page.tsx b/src/app/[locale]/(dashboard)/flows/page.tsx similarity index 100% rename from src/app/(dashboard)/flows/page.tsx rename to src/app/[locale]/(dashboard)/flows/page.tsx diff --git a/src/app/(dashboard)/inbox/page.tsx b/src/app/[locale]/(dashboard)/inbox/page.tsx similarity index 100% rename from src/app/(dashboard)/inbox/page.tsx rename to src/app/[locale]/(dashboard)/inbox/page.tsx diff --git a/src/app/(dashboard)/layout.tsx b/src/app/[locale]/(dashboard)/layout.tsx similarity index 100% rename from src/app/(dashboard)/layout.tsx rename to src/app/[locale]/(dashboard)/layout.tsx diff --git a/src/app/(dashboard)/pipelines/page.tsx b/src/app/[locale]/(dashboard)/pipelines/page.tsx similarity index 100% rename from src/app/(dashboard)/pipelines/page.tsx rename to src/app/[locale]/(dashboard)/pipelines/page.tsx diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/[locale]/(dashboard)/settings/page.tsx similarity index 100% rename from src/app/(dashboard)/settings/page.tsx rename to src/app/[locale]/(dashboard)/settings/page.tsx diff --git a/src/app/join/[token]/page.tsx b/src/app/[locale]/join/[token]/page.tsx similarity index 81% rename from src/app/join/[token]/page.tsx rename to src/app/[locale]/join/[token]/page.tsx index 4767505..5fdc4ee 100644 --- a/src/app/join/[token]/page.tsx +++ b/src/app/[locale]/join/[token]/page.tsx @@ -23,8 +23,9 @@ // ============================================================ import { useCallback, useEffect, useState } from 'react'; -import Link from 'next/link'; import { useParams } from 'next/navigation'; +import { useTranslations } from 'next-intl'; +import { Link } from '@/i18n/navigation'; import { toast } from 'sonner'; import { AlertTriangle, @@ -65,32 +66,16 @@ interface PeekFail { } type PeekResult = PeekOk | PeekFail; -const ROLE_LABEL: Record = { - admin: 'Admin', - agent: 'Agent', - viewer: 'Viewer', -}; - -const FAIL_COPY: Record = { - not_found: { - title: 'Invite not found', - body: 'This link doesn’t match a valid invitation. Double-check the URL or ask the person who invited you to send a new one.', - }, - used: { - title: 'Invite already used', - body: 'This invitation has already been accepted. If that wasn’t you, ask the account admin to send a fresh link.', - }, - expired: { - title: 'Invite expired', - body: 'This invitation has expired. Ask the account admin to send a new one — they take a few seconds to generate.', - }, - server_error: { - title: 'Something went wrong', - body: 'We couldn’t verify this invitation right now. Try refreshing the page in a moment.', - }, +const FAIL_REASON_KEY: Record = { + not_found: 'notFound', + used: 'used', + expired: 'expired', + server_error: 'serverError', }; export default function JoinPage() { + const t = useTranslations('join'); + const tRoles = useTranslations('roles'); const params = useParams<{ token: string }>(); const token = params?.token; @@ -182,25 +167,24 @@ export default function JoinPage() { // rather than a 3-second toast. if (res.status === 409) { setConflictMessage( - payload.error || - 'You are already in another account. Sign in with a different email to join this one.', + payload.error || t('redeemConflictFallback'), ); } else { - toast.error(payload.error || 'Failed to accept invitation'); + toast.error(payload.error || t('redeemFailedFallback')); } setAccepting(false); return; } - toast.success('Welcome to the team'); + toast.success(t('welcomeToast')); // Full reload (not router.push) so AuthProvider re-fetches // the profile with the new account_id and account_role. window.location.href = '/dashboard'; } catch (err) { console.error('[join] redeem error:', err); - toast.error('Could not reach the server'); + toast.error(t('redeemNetworkError')); setAccepting(false); } - }, [token]); + }, [token, t]); const handleSignOutAndRetry = useCallback(async () => { setSigningOut(true); @@ -212,10 +196,10 @@ export default function JoinPage() { window.location.reload(); } catch (err) { console.error('[join] sign-out error:', err); - toast.error('Could not sign out. Try refreshing the page.'); + toast.error(t('signOutError')); setSigningOut(false); } - }, []); + }, [t]); // ----- Loading state (peek pending OR auth not yet resolved) ----- if (peek === null || authedUserId === undefined) { @@ -223,7 +207,7 @@ export default function JoinPage() { -

Verifying invitation…

+

{t('verifying')}

); @@ -231,16 +215,18 @@ export default function JoinPage() { // ----- Peek failed ----- if (!peek.ok) { - const copy = FAIL_COPY[peek.reason]; + const reasonKey = FAIL_REASON_KEY[peek.reason]; return (
- {copy.title} + + {t(`fail.${reasonKey}.title`)} + - {copy.body} + {t(`fail.${reasonKey}.body`)}
@@ -257,14 +243,14 @@ export default function JoinPage() { onClick={loadPeekAndAuth} className="w-full bg-primary text-primary-foreground hover:bg-primary/90" > - Try again + {t('tryAgain')} @@ -272,7 +258,7 @@ export default function JoinPage() { <> @@ -280,7 +266,7 @@ export default function JoinPage() { variant="outline" className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" > - Sign in + {t('signIn')} @@ -297,22 +283,21 @@ export default function JoinPage() {
- You're invited to{' '} - {peek.account_name} + {t.rich('invitedToTitle', { + accountName: peek.account_name, + highlight: (chunks) => {chunks}, + })} - You'll join as{' '} - - - {ROLE_LABEL[peek.role]} - - . Link valid until{' '} - {new Date(peek.expires_at).toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', + {t.rich('invitedAs', { + role: () => ( + + + {tRoles(peek.role)} + + ), + date: new Date(peek.expires_at), })} - . ); @@ -332,19 +317,22 @@ export default function JoinPage() { {accepting ? ( <> - Accepting… + {t('accepting')} ) : ( <> - Accept invitation + {t('acceptInvitation')} )}

- Accepting moves your login into{' '} - {peek.account_name}. Your - empty personal account from signup will be cleaned up. + {t.rich('acceptFooter', { + accountName: peek.account_name, + highlight: (chunks) => ( + {chunks} + ), + })}

@@ -363,7 +351,7 @@ export default function JoinPage() { - Can't join {peek.account_name} with this account + {t('conflictTitle', { accountName: peek.account_name })} {conflictMessage} @@ -371,11 +359,12 @@ export default function JoinPage() {

- To join{' '} - {peek.account_name}, - sign out and sign up again with a different email address. - The invite link stays valid as long as it hasn't - expired. + {t.rich('conflictBody', { + accountName: peek.account_name, + highlight: (chunks) => ( + {chunks} + ), + })}

@@ -384,7 +373,7 @@ export default function JoinPage() { onClick={() => setConflictMessage(null)} className="border-slate-700 text-slate-300 hover:bg-slate-800" > - Stay signed in + {t('staySignedIn')} @@ -414,7 +403,7 @@ export default function JoinPage() { @@ -422,7 +411,7 @@ export default function JoinPage() { variant="outline" className="w-full border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-white" > - I already have an account + {t('alreadyHaveAccount')} diff --git a/src/app/join/layout.tsx b/src/app/[locale]/join/layout.tsx similarity index 100% rename from src/app/join/layout.tsx rename to src/app/[locale]/join/layout.tsx diff --git a/src/app/layout.tsx b/src/app/[locale]/layout.tsx similarity index 65% rename from src/app/layout.tsx rename to src/app/[locale]/layout.tsx index 0956aa5..8121180 100644 --- a/src/app/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,9 +1,13 @@ import type { Metadata, Viewport } from "next"; +import { hasLocale, NextIntlClientProvider } from "next-intl"; +import { setRequestLocale } from "next-intl/server"; import { Inter } from "next/font/google"; +import { notFound } from "next/navigation"; import Script from "next/script"; import { Toaster } from "sonner"; -import "./globals.css"; +import "../globals.css"; import { ThemeProvider } from "@/hooks/use-theme"; +import { routing } from "@/i18n/routing"; import { DEFAULT_THEME, STORAGE_KEY, THEME_IDS } from "@/lib/themes"; const inter = Inter({ @@ -40,11 +44,6 @@ export const viewport: Viewport = { // chosen theme is on the element before first paint. Without // this every page load flashes the default Violet for a frame before // the React tree mounts and applies the picked theme. -// -// Kept dependency-free (no imports, no JSX) — must be a string the -// browser can run as a single