diff --git a/jest.config.ts b/jest.config.ts index fc0555b..0f05b9c 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -8,7 +8,7 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest const config: Config = { - //coverageProvider: 'v8', + coverageProvider: 'v8', testEnvironment: 'jsdom', // Add more setup options before each test is run setupFilesAfterEnv: ['/setupTests.ts'], diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..a2bcd91 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,119 @@ +{ + "Shared": { + "welcome": "Welcome to", + "logo": "BeeQuant", + "logo-accent": "AI", + "slogan": "Trading smart, trading with BeeQuant AI", + "email": "Email", + "password": "Password", + "signIn": "Sign In", + "login": "Login", + "repeatPassword": "Repeat Password", + "ref": "Reference", + "displayName": "Display Name{optional, select, true {(optional)} other {}}", + "logout": "Logout", + "theme": "Theme", + "lightTheme": "Light Theme", + "darkTheme": "Dark Theme" + }, + + "LoginPage": { + "account-or": "Or Easily Using", + "forget-password": "Forgot a password?", + "remember-me": "Remember Me", + "create-account": "Create Account" + }, + + "RegisterPage": { + "account-have-account": "Already have an account?", + "signUp": "Sign Up", + "haveAccount": "Already have an account?" + }, + + "Sidebar": { + "dashboard": "Dashboard", + "bots": { + "category-name": "Bots", + "dashboard": "Bots Dashboard", + "management": "Bots Management", + "create": "Bot Create", + "details": "Bot Details" + }, + "cryptoEconomy": { + "category-name": "Cryptoeconomy", + "prices": "Prices", + "prices-details": "Price Details", + "exchange": "Exchanges", + "exchange-details": "Exchange Details" + }, + "theme": {}, + "account": { + "category-name": "Account", + "profile": "Profile", + "exchange-management": "Exchange Management", + "app-setting": "App Setting" + }, + "log-out": "Log Out" + }, + + "Notifications": { + "email": { + "required": "Email is required", + "invalid": "Invalid email address" + }, + "displayName": { + "description": "between 4 to 15 characters, only letters, numbers, dashes and underscore are allowed", + "required": "Display Name is required", + "invalid": "Display name must contain only letters, numbers, hyphens and underscores", + "minLength": "Display name must be at least 4 characters", + "maxLength": "Display name must be at most 15 characters" + }, + "password": { + "description": "8 to 32 characters, including letter, number and special character", + "required": "Password is required", + "invalid": "Password must contain at least one letter, one number and one special character", + "minLength": "Password must be at least 8 characters", + "maxLength": "Password must be at most 32 characters", + "notMatch": "Password does not match" + }, + "ref": { + "required": "Referral code is required" + } + }, + + "SettingPage": { + "title": "Settings", + "subtitle": "Update your profile", + "realName": "Real Name", + "mobile": "Mobile", + "submit": "Submit", + "cancel": "Cancel" + }, + + "AppSettingPage": { + "title": "App Settings", + "subtitle": "Change your app settings", + "language": "Language" + }, + + "CryptoSettingPage": { + "title": "Crypto Exchange Management" + }, + + "Dashboard": { + "header": "BeeQuant Dashboard", + "total-profit-earned": "Total Profit Earned", + "total-asset": "Total Asset", + "pending-orders": "Pending Orders", + "recent-transaction": "Recent Transaction" + }, + + "Profile": { + "profile": "Profile", + "wallet": "Wallet", + "settings": "Settings" + }, + + "exchange": {}, + "bot": {} +} diff --git a/messages/zh-cn.json b/messages/zh-cn.json new file mode 100644 index 0000000..2ab4986 --- /dev/null +++ b/messages/zh-cn.json @@ -0,0 +1,117 @@ +{ + "Shared": { + "welcome": "欢迎来到", + "logo": "BeeQuant", + "logo-accent": "AI", + "slogan": "使用BeeQuant AI智能交易", + "email": "邮箱", + "password": "密码", + "signIn": "注册", + "login": "登录", + "repeatPassword": "重复密码", + "ref": "推荐码", + "displayName": "用户名称{optional, select, true {(可选)} other {}}", + "logout": "登出", + "theme": "主题", + "lightTheme": "浅色", + "darkTheme": "暗色" + }, + + "LoginPage": { + "account-or": "或轻松使用", + "forget-password": "忘记密码?", + "remember-me": "记住密码", + "create-account": "创建帐户" + }, + + "RegisterPage": { + "account-have-account": "已有帐户?", + "account-or": "或轻松使用", + "haveAccount": "已经拥有账号?", + "signUp": "注册" + }, + + "Sidebar": { + "dashboard": "中控台", + "bots": { + "category-name": "交易助手", + "dashboard": "控制面板", + "management": "管理助手", + "create": "创建助手", + "details": "详细信息" + }, + "cryptoEconomy": { + "category-name": "加密经济", + "prices": "价格", + "prices-details": "价格信息", + "exchange": "交易", + "exchange-details": "交易信息" + }, + "account": { + "category-name": "帐户", + "profile": "个人资料", + "exchange-management": "交易管理", + "app-setting": "系统设置" + } + }, + + "Notifications": { + "email": { + "required": "请填写邮箱", + "invalid": "邮箱格式不正确" + }, + "displayName": { + "description": "用户名称长度在4到15个字符之间,只允许使用字母、数字、连字符和下划线", + "required": "请输入用户名称", + "invalid": "用户名称只能包含字母、数字、连字符和下划线", + "minLength": "用户名称长度至少为4个字符", + "maxLength": "用户名称长度最多为32个字符" + }, + "password": { + "description": "密码长度在8到32个字符,必须包含至少一个字母、数字或特殊字符", + "required": "请输入密码", + "invalid": "密码必须包含至少一个字母、一个数字和一个特殊字符", + "minLength": "密码长度至少为8个字符", + "maxLength": "密码长度最多为32个字符", + "notMatch": "两次输入的密码不匹配" + }, + "ref": { + "required": "请输入推荐码" + } + }, + + "SettingPage": { + "title": "账户设置", + "subtitle": "更新您的账户信息", + "realName": "真实姓名", + "mobile": "手机号码", + "submit": "提交", + "cancel": "取消" + }, + + "AppSettingPage": { + "title": "系统设置", + "subtitle": "更新您的系统设置", + "language": "语言" + }, + + "CryptoSettingPage": { + "title": "加密货币交易所管理" + }, + + "Dashboard": { + "header": "BeeQuant中控台", + "total-profit-earned": "总收益", + "total-asset": "总资产", + "pending-orders": "待处理订单", + "recent-transaction": "最近交易" + }, + + "Profile": { + "profile": "账户信息", + "wallet": "钱包", + "settings": "账户设置" + }, + "exchange": {}, + "bot": {} +} diff --git a/next.config.mjs b/next.config.mjs index d22cc06..c139812 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,9 +1,13 @@ +import createNextIntPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntPlugin(); + /** * @type {import('next').NextConfig} */ const nextConfig = { /* config options here */ - output: 'export', + output: 'standalone', distDir: './dist', compiler: { styledComponents: true, @@ -13,4 +17,4 @@ const nextConfig = { }, }; -export default nextConfig; +export default withNextIntl(nextConfig); diff --git a/package.json b/package.json index a7b0ea1..6835452 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,11 @@ "final-form": "^4.20.10", "framer-motion": "^10.16.4", "graphql": "^16.8.1", + "i18next": "^23.11.5", + "i18next-resources-to-backend": "^1.2.1", "mdi-react": "^9.3.0", "next": "^14.1.3", + "next-intl": "^3.17.2", "polished": "^4.2.2", "prop-types": "^15.8.1", "rc-notification": "^5.3.0", @@ -47,6 +50,7 @@ "react-dom": "^18.2.0", "react-final-form": "^6.5.9", "react-hook-form": "^7.48.2", + "react-i18next": "^14.1.2", "react-router": "5.2.0", "react-router-dom": "5.2.0", "recharts": "^2.12.6", @@ -91,6 +95,7 @@ "husky": "^8.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-styled-components": "^7.2.0", "jest-transform-stub": "^2.0.0", "jest-transformer-svg": "^2.0.2", "less": "^4.2.0", diff --git a/public/en.json b/public/en.json new file mode 100644 index 0000000..e623f24 --- /dev/null +++ b/public/en.json @@ -0,0 +1,86 @@ +{ + "LoginPage": { + "welcome": "Welcome to" + }, + "Sidebar": { + "route-key-name": { + "dashboard": "Dashboard", + "bots": { + "category-name": "Bots", + "bot-dashboard": "Bots Dashboard", + "bot-management": "Bots Management", + "bot-create": "Bot Create", + "bot-details": "Bot Details" + }, + "cryptoeconomy": { + "category-name": "Cryptoeconomy", + "crypto-prices": "Prices", + "crypto-prices-details": "Price Details", + "crypto-exchange": "Exchanges", + "crypto-exchange-details": "Exchange Details" + }, + "theme": { + "category-name": "Theme", + "light-theme": "Light Theme", + "dark-theme": "Dark Theme" + }, + "account": { + "category-name": "Account", + "account-profile": "Profile", + "exchange-management": "Exchange Management", + "app-setting": "App Setting" + }, + "log-out": "Log Out" + } + }, + + "login": { + "account-title": "Welcome to", + "account-logo": "BeeQuant", + "account-logo-accent": "AI", + "h4-subhead": "Trading smart, trading with BeeQuant AI", + "account-or": "Or Easily Using", + "login-form": { + "form-group-label-email": "Email", + "form-group-label-password": "Password", + "account-forgot-password": "Forgot a password?", + "button-sign-in": "Sign In", + "button-register": "Create Account" + } + }, + "registry": { + "account-title": "Welcome to", + "account-logo": "BeeQuant", + "account-logo-accent": "AI", + "h4-subhead": "Trading smart, trading with BeeQuant AI", + "account-have-account": "Already have an account?", + "link-login": "Login" + }, + "account": { + "profile": {}, + "setting": { + "card-title": "Settings", + "card-subhead": "Update your profile", + "form-group-label-real-name": "Real Name", + "form-group-label-display-name": "Display Name", + "form-group-label-email": "Email", + "form-group-lablel-password": "Password", + "form-group-lablel-mobile": "Mobile", + "form-group-lablel-reference": "Reference", + "button-submit": "Submit", + "button-cancel": "Cancel" + } + }, + "appsetting": { + "card-title": "App Settings", + "card-subhead": "Change your app settings", + "form-group-label-language": "Language", + "form-group-label-theme": "Theme", + "option-light": "Light", + "option-dark": "Dark" + }, + "bot": {}, + "crypto": {}, + "dashboard": {}, + "exchange": {} +} diff --git a/public/zh-cn.json b/public/zh-cn.json new file mode 100644 index 0000000..859f39b --- /dev/null +++ b/public/zh-cn.json @@ -0,0 +1,86 @@ +{ + "LoginPage": { + "welcome": "欢迎来到" + }, + "Sidebar": { + "route-key-name": { + "dashboard": "中控台", + "bots": { + "category-name": "自动交易程序", + "bot-dashboard": "自动交易程序中控台", + "bot-management": "管理", + "bot-create": "创建", + "bot-details": "自动交易程序细节" + }, + "cryptoeconomy": { + "category-name": "加密经济", + "crypto-prices": "价格", + "crypto-prices-details": "价格详细信息", + "crypto-exchange": "交易所", + "crypto-exchange-details": "交易所详细信息" + }, + "theme": { + "category-name": "主题", + "light-theme": "浅色", + "dark-theme": "暗色" + }, + "account": { + "category-name": "帐户", + "account-profile": "个人资料", + "exchange-management": "交易管理", + "app-setting": "应用程序设置" + }, + "log-out": "登出" + } + }, + + "login": { + "account-title": "欢迎来到", + "account-logo": "BeeQuant", + "account-logo-accent": "AI", + "h4-subhead": "使用BeeQuant AI智能交易", + "account-or": "或轻松使用", + "login-form": { + "form-group-label-email": "电邮", + "form-group-label-password": "密码", + "account-forgot-password": "忘记密码?", + "button-sign-in": "登录", + "button-register": "创建帐户" + } + }, + "registry": { + "account-title": "欢迎来到", + "account-logo": "BeeQuant", + "account-logo-accent": "AI", + "h4-subhead": "使用BeeQuant AI智能交易", + "account-have-account": "已有帐户?", + "link-login": "登录" + }, + "account": { + "profile": {}, + "setting": { + "card-title": "设置", + "card-subhead": "更新您的个人资料", + "form-group-label-real-name": "真实姓名", + "form-group-label-display-name": "显示名称", + "form-group-label-email": "电子邮件", + "form-group-lablel-password": "密码", + "form-group-lablel-mobile": "Mobile", + "form-group-lablel-reference": "参考", + "button-submit": "提交", + "button-cancel": "取消" + } + }, + "appsetting": { + "card-title": "系统设置", + "card-subhead": "更新您的系统设置", + "form-group-label-language": "语言", + "form-group-label-theme": "布景主题", + "option-light": "浅色", + "option-dark": "暗色" + }, + "bot": {}, + "crypto": {}, + "dashboard": {}, + "exchange": {} +} diff --git a/src/app/(protected)/dashboard/_components/PendingOrders.tsx b/src/app/(protected)/dashboard/_components/PendingOrders.tsx deleted file mode 100644 index 4dbfc27..0000000 --- a/src/app/(protected)/dashboard/_components/PendingOrders.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Col } from 'react-bootstrap'; -import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; -import ProgressBar from '@/shared/components/ProgressBar'; -import { Card } from '@/shared/components/Card'; -import { - DashboardPortfolioCard, - PortfolioCardDescription, - PortfolioCardTitle, - PortfolioCardWrap, -} from './DashboardCardElements'; - -const BookingCancels = () => ( - - - - - 25 - - - Pending orders - - - - -); - -export default BookingCancels; diff --git a/src/app/(protected)/dashboard/_components/TotalAssets.tsx b/src/app/(protected)/dashboard/_components/TotalAssets.tsx deleted file mode 100644 index 5546858..0000000 --- a/src/app/(protected)/dashboard/_components/TotalAssets.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Col } from 'react-bootstrap'; -import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; -import ProgressBar from '@/shared/components/ProgressBar'; -import { Card } from '@/shared/components/Card'; -import { colorBlue } from '@/styles/palette'; -import { - DashboardPortfolioCard, - PortfolioCardDescription, - PortfolioCardTitle, - PortfolioCardWrap, -} from './DashboardCardElements'; - -const TotalAssets = () => ( - - - - - $ 878 372 - - - Total assets - - - - -); - -export default TotalAssets; diff --git a/src/app/(protected)/dashboard/_components/TotalProfitEarned.tsx b/src/app/(protected)/dashboard/_components/TotalProfitEarned.tsx deleted file mode 100644 index 0ec7eff..0000000 --- a/src/app/(protected)/dashboard/_components/TotalProfitEarned.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Col } from 'react-bootstrap'; -import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; -import ProgressBar from '@/shared/components/ProgressBar'; -import { Card } from '@/shared/components/Card'; -import { colorRed } from '@/styles/palette'; -import { - DashboardPortfolioCard, - PortfolioCardDescription, - PortfolioCardTitle, - PortfolioCardWrap, -} from './DashboardCardElements'; - -const TotalProfitEarned = () => ( - - - - - $ 165 832 - - - Total profit earned - - - - -); - -export default TotalProfitEarned; diff --git a/src/app/(protected)/exchange/_components/ContentCard.tsx b/src/app/(protected)/exchange/_components/ContentCard.tsx deleted file mode 100644 index 89dd3f2..0000000 --- a/src/app/(protected)/exchange/_components/ContentCard.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Col } from 'react-bootstrap'; -import { Card, CardBody, CardTitle, CardTitleWrap, CardSubhead } from '@/shared/components/Card'; - -const ContentCard = () => ( - - - - - title - subtitle - -

content

-
-
- -); - -export default ContentCard; diff --git a/src/app/(auth)/_components/AccountFooter.tsx b/src/app/[locale]/(auth)/_components/AccountFooter.tsx similarity index 83% rename from src/app/(auth)/_components/AccountFooter.tsx rename to src/app/[locale]/(auth)/_components/AccountFooter.tsx index 348e03c..784e875 100644 --- a/src/app/(auth)/_components/AccountFooter.tsx +++ b/src/app/[locale]/(auth)/_components/AccountFooter.tsx @@ -7,6 +7,7 @@ import { } from '@/shared/components/account/AccountElements'; import FacebookIcon from 'mdi-react/FacebookIcon'; import GooglePlusIcon from 'mdi-react/GooglePlusIcon'; +import { useTranslations } from 'next-intl'; import Link from 'next/link'; interface AccountFooterProps { @@ -14,11 +15,12 @@ interface AccountFooterProps { } const AccountFooter = ({ isLogin }: AccountFooterProps) => { + const t = useTranslations(); if (isLogin) { return ( <> -

Or Easily Using

+

{t('LoginPage.account-or')}

{/* @ts-ignore - Ignoring because of complex union types incorrectly inferred */} @@ -38,8 +40,8 @@ const AccountFooter = ({ isLogin }: AccountFooterProps) => { return (

- Already have an account? - Login + {t('RegisterPage.haveAccount')} + {t('Shared.login')}

); diff --git a/src/app/(auth)/_components/AccountHeader.tsx b/src/app/[locale]/(auth)/_components/AccountHeader.tsx similarity index 58% rename from src/app/(auth)/_components/AccountHeader.tsx rename to src/app/[locale]/(auth)/_components/AccountHeader.tsx index a85a92c..7243d07 100644 --- a/src/app/(auth)/_components/AccountHeader.tsx +++ b/src/app/[locale]/(auth)/_components/AccountHeader.tsx @@ -4,19 +4,21 @@ import { AccountLogoAccent, AccountTitle, } from '@/shared/components/account/AccountElements'; +import { useTranslations } from 'next-intl'; const AccountHeader = () => { + const t = useTranslations('Shared'); return ( - Welcome to + {t('welcome')}
- BeeQuant - AI + {t('logo')} + {t('logo-accent')}
-

Trading smart, trading with BeeQuant AI

+

{t('slogan')}

); }; diff --git a/src/app/(auth)/login/page.tsx b/src/app/[locale]/(auth)/login/page.tsx similarity index 100% rename from src/app/(auth)/login/page.tsx rename to src/app/[locale]/(auth)/login/page.tsx diff --git a/src/app/(auth)/register/_components/RegisterForm.test.tsx b/src/app/[locale]/(auth)/register/_components/RegisterForm.test.tsx similarity index 100% rename from src/app/(auth)/register/_components/RegisterForm.test.tsx rename to src/app/[locale]/(auth)/register/_components/RegisterForm.test.tsx diff --git a/src/app/(auth)/register/_components/RegisterForm.tsx b/src/app/[locale]/(auth)/register/_components/RegisterForm.tsx similarity index 81% rename from src/app/(auth)/register/_components/RegisterForm.tsx rename to src/app/[locale]/(auth)/register/_components/RegisterForm.tsx index ae2647a..08b087b 100644 --- a/src/app/(auth)/register/_components/RegisterForm.tsx +++ b/src/app/[locale]/(auth)/register/_components/RegisterForm.tsx @@ -1,3 +1,4 @@ +'client'; import { useState } from 'react'; import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; import { Alert } from 'react-bootstrap'; @@ -20,6 +21,7 @@ import { import { Controller, useForm } from 'react-hook-form'; import FormField from '@/shared/components/form/FormHookField'; import { config } from '@/config/config'; +import { useTranslations } from 'next-intl'; const { referenceName } = config; @@ -38,18 +40,6 @@ type IsFocused = { ref: boolean; }; -// region STYLES -const RegisterButtons = styled(AccountButtons)` - ${marginLeft}: 0!important; - margin-bottom: 20px; - - button { - margin-bottom: 0; - } -`; - -// endregion - const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { const { handleSubmit, @@ -59,6 +49,7 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { } = useForm(); const pwd = watch('password'); + const t = useTranslations(); const prepareFormData = (data: any) => { const { repeatPassword, ...formData } = data; @@ -94,11 +85,8 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { - Display Name (optional) - - {isFocused.displayName && - ': between 4 to 15 characters, only letters, numbers, dashes and underscore are allowed'} - + {t('Shared.displayName', { optional: 'true' })} + {isFocused.displayName && t('Notifications.displayName.description')} @@ -112,12 +100,11 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { rules={{ pattern: { value: displayNamePatten, - message: - 'Display Name must contain 4 to 15 characters, only letters, numbers, dashes and underscore are allowed', + message: t('Notifications.displayName.invalid'), }, }} defaultValue="" - placeholder="Display Name" + placeholder={t('Shared.displayName', { optional: 'false' })} isAboveError onFocus={() => handleFocus('displayName')} onBlur={() => handleBlur('displayName')} @@ -126,7 +113,7 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { - Email + {t('Shared.email')} {isFocused.email && ''} @@ -139,14 +126,14 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { component="input" errors={errors} rules={{ - required: 'This is a required field', + required: t('Notifications.email.required'), pattern: { value: emailPattern, - message: 'Entered value does not match email format', + message: t('Notifications.email.invalid'), }, }} defaultValue="" - placeholder="Email" + placeholder={t('Shared.email')} isAboveError onFocus={() => handleFocus('email')} onBlur={() => handleBlur('email')} @@ -155,11 +142,8 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { - Password - - {isFocused.password && - ': 8 to 32 characters, including letter, number and special character'} - + {t('Shared.password')} + {isFocused.password && t('Notifications.password.description')} { touched: !!fieldState.error, error: fieldState.error?.message, }} - placeholder="Password" + placeholder={t('Shared.password')} keyIcon isAboveError onFocus={() => handleFocus(field.name)} /> )} rules={{ - required: 'This is a required field', + required: t('Notifications.password.required'), pattern: { value: passwordPatten, - message: - 'must contain 8 to 32 characters, including letter, number and special character', + message: t('Notifications.password.invalid'), }, }} defaultValue="" @@ -192,7 +175,7 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { - Repeat Password + {t('Shared.repeatPassword')} {isFocused.repeatPassword && ''} @@ -206,21 +189,21 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { touched: !!fieldState.error, error: fieldState.error?.message, }} - placeholder="Repeat Password" + placeholder={t('Shared.repeatPassword')} keyIcon isAboveError /> )} rules={{ - required: 'This is a required field', - validate: (value) => value === pwd || 'The passwords do not match', + required: t('Notifications.password.required'), + validate: (value) => value === pwd || t('Notifications.password.notMatch'), }} defaultValue="" /> - Reference + {t('Shared.ref')} @@ -231,10 +214,10 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { component="input" errors={errors} rules={{ - required: 'This is a required field', + required: t('Notifications.ref.required'), }} defaultValue={defaultReferenceName} - placeholder="Reference" + placeholder={t('Shared.ref')} isAboveError disabled /> @@ -243,7 +226,7 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - Sign Up + {t('RegisterPage.signUp')} @@ -251,3 +234,15 @@ const RegisterForm = ({ onSubmit, error = '' }: RegisterFormProps) => { }; export default RegisterForm; + +// region STYLES +const RegisterButtons = styled(AccountButtons)` + ${marginLeft}: 0!important; + margin-bottom: 20px; + + button { + margin-bottom: 0; + } +`; + +// endregion diff --git a/src/app/(auth)/register/_components/RegisterSuccess.test.tsx b/src/app/[locale]/(auth)/register/_components/RegisterSuccess.test.tsx similarity index 100% rename from src/app/(auth)/register/_components/RegisterSuccess.test.tsx rename to src/app/[locale]/(auth)/register/_components/RegisterSuccess.test.tsx diff --git a/src/app/(auth)/register/_components/RegisterSuccess.tsx b/src/app/[locale]/(auth)/register/_components/RegisterSuccess.tsx similarity index 100% rename from src/app/(auth)/register/_components/RegisterSuccess.tsx rename to src/app/[locale]/(auth)/register/_components/RegisterSuccess.tsx diff --git a/src/app/(auth)/register/page.tsx b/src/app/[locale]/(auth)/register/page.tsx similarity index 100% rename from src/app/(auth)/register/page.tsx rename to src/app/[locale]/(auth)/register/page.tsx diff --git a/src/app/(protected)/account/profile/_components/ProfileBasicComponents.tsx b/src/app/[locale]/(protected)/account/profile/_components/ProfileBasicComponents.tsx similarity index 100% rename from src/app/(protected)/account/profile/_components/ProfileBasicComponents.tsx rename to src/app/[locale]/(protected)/account/profile/_components/ProfileBasicComponents.tsx diff --git a/src/app/(protected)/account/profile/_components/ProfileMain.tsx b/src/app/[locale]/(protected)/account/profile/_components/ProfileMain.tsx similarity index 100% rename from src/app/(protected)/account/profile/_components/ProfileMain.tsx rename to src/app/[locale]/(protected)/account/profile/_components/ProfileMain.tsx diff --git a/src/app/(protected)/account/profile/page.tsx b/src/app/[locale]/(protected)/account/profile/page.tsx similarity index 100% rename from src/app/(protected)/account/profile/page.tsx rename to src/app/[locale]/(protected)/account/profile/page.tsx diff --git a/src/app/(protected)/account/settings/page.tsx b/src/app/[locale]/(protected)/account/settings/page.tsx similarity index 100% rename from src/app/(protected)/account/settings/page.tsx rename to src/app/[locale]/(protected)/account/settings/page.tsx diff --git a/src/app/[locale]/(protected)/appsetting/page.tsx b/src/app/[locale]/(protected)/appsetting/page.tsx new file mode 100644 index 0000000..1e056cb --- /dev/null +++ b/src/app/[locale]/(protected)/appsetting/page.tsx @@ -0,0 +1,8 @@ +import AppSettingPage from 'module/app-setting/AppSettingPage'; +import React from 'react'; + +function page() { + return ; +} + +export default page; diff --git a/src/app/(protected)/bot/create/page.tsx b/src/app/[locale]/(protected)/bot/create/page.tsx similarity index 100% rename from src/app/(protected)/bot/create/page.tsx rename to src/app/[locale]/(protected)/bot/create/page.tsx diff --git a/src/app/(protected)/bot/dashboard/page.tsx b/src/app/[locale]/(protected)/bot/dashboard/page.tsx similarity index 100% rename from src/app/(protected)/bot/dashboard/page.tsx rename to src/app/[locale]/(protected)/bot/dashboard/page.tsx diff --git a/src/app/(protected)/bot/details/page.tsx b/src/app/[locale]/(protected)/bot/details/page.tsx similarity index 100% rename from src/app/(protected)/bot/details/page.tsx rename to src/app/[locale]/(protected)/bot/details/page.tsx diff --git a/src/app/(protected)/bot/management/page.tsx b/src/app/[locale]/(protected)/bot/management/page.tsx similarity index 100% rename from src/app/(protected)/bot/management/page.tsx rename to src/app/[locale]/(protected)/bot/management/page.tsx diff --git a/src/app/(protected)/crypto/exchange/details/page.tsx b/src/app/[locale]/(protected)/crypto/exchange/details/page.tsx similarity index 100% rename from src/app/(protected)/crypto/exchange/details/page.tsx rename to src/app/[locale]/(protected)/crypto/exchange/details/page.tsx diff --git a/src/app/(protected)/crypto/exchange/page.tsx b/src/app/[locale]/(protected)/crypto/exchange/page.tsx similarity index 100% rename from src/app/(protected)/crypto/exchange/page.tsx rename to src/app/[locale]/(protected)/crypto/exchange/page.tsx diff --git a/src/app/(protected)/crypto/price/details/page.tsx b/src/app/[locale]/(protected)/crypto/price/details/page.tsx similarity index 100% rename from src/app/(protected)/crypto/price/details/page.tsx rename to src/app/[locale]/(protected)/crypto/price/details/page.tsx diff --git a/src/app/(protected)/crypto/price/page.tsx b/src/app/[locale]/(protected)/crypto/price/page.tsx similarity index 100% rename from src/app/(protected)/crypto/price/page.tsx rename to src/app/[locale]/(protected)/crypto/price/page.tsx diff --git a/src/app/(protected)/dashboard/_components/DashboardCardElements.tsx b/src/app/[locale]/(protected)/dashboard/_components/DashboardCardElements.tsx similarity index 100% rename from src/app/(protected)/dashboard/_components/DashboardCardElements.tsx rename to src/app/[locale]/(protected)/dashboard/_components/DashboardCardElements.tsx diff --git a/src/app/[locale]/(protected)/dashboard/_components/PendingOrders.tsx b/src/app/[locale]/(protected)/dashboard/_components/PendingOrders.tsx new file mode 100644 index 0000000..a326b76 --- /dev/null +++ b/src/app/[locale]/(protected)/dashboard/_components/PendingOrders.tsx @@ -0,0 +1,30 @@ +import { Col } from 'react-bootstrap'; +import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; +import ProgressBar from '@/shared/components/ProgressBar'; +import { Card } from '@/shared/components/Card'; +import { + DashboardPortfolioCard, + PortfolioCardDescription, + PortfolioCardTitle, + PortfolioCardWrap, +} from './DashboardCardElements'; +import { useTranslations } from 'next-intl'; + +const BookingCancels = () => { + const t = useTranslations('Dashboard'); + return ( + + + + + 25 + + + {t('pending-orders')} + + + + + ); +}; +export default BookingCancels; diff --git a/src/app/(protected)/dashboard/_components/RecentTransactions.tsx b/src/app/[locale]/(protected)/dashboard/_components/RecentTransactions.tsx similarity index 92% rename from src/app/(protected)/dashboard/_components/RecentTransactions.tsx rename to src/app/[locale]/(protected)/dashboard/_components/RecentTransactions.tsx index 9c583a7..7f025ff 100644 --- a/src/app/(protected)/dashboard/_components/RecentTransactions.tsx +++ b/src/app/[locale]/(protected)/dashboard/_components/RecentTransactions.tsx @@ -8,6 +8,7 @@ import { WidgetCardTotalLarge, WidgetCardWrap, } from './DashboardCardElements'; +import { useTranslations } from 'next-intl'; const data = [ { id: 0, name: 'Page A', pv: 255 }, @@ -28,12 +29,13 @@ const RecentTransactions = () => { setActiveIndex(index); }; + const t = useTranslations('Dashboard'); return ( - Recent transactions + {t('recent-transaction')} {activeItem.pv} diff --git a/src/app/[locale]/(protected)/dashboard/_components/TotalAssets.tsx b/src/app/[locale]/(protected)/dashboard/_components/TotalAssets.tsx new file mode 100644 index 0000000..64a627e --- /dev/null +++ b/src/app/[locale]/(protected)/dashboard/_components/TotalAssets.tsx @@ -0,0 +1,32 @@ +import { Col } from 'react-bootstrap'; +import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; +import ProgressBar from '@/shared/components/ProgressBar'; +import { Card } from '@/shared/components/Card'; +import { colorBlue } from '@/styles/palette'; +import { + DashboardPortfolioCard, + PortfolioCardDescription, + PortfolioCardTitle, + PortfolioCardWrap, +} from './DashboardCardElements'; +import { useTranslations } from 'next-intl'; + +const TotalAssets = () => { + const t = useTranslations('Dashboard'); + return ( + + + + + $ 878 372 + + + {t('total-asset')} + + + + + ); +}; + +export default TotalAssets; diff --git a/src/app/[locale]/(protected)/dashboard/_components/TotalProfitEarned.tsx b/src/app/[locale]/(protected)/dashboard/_components/TotalProfitEarned.tsx new file mode 100644 index 0000000..a4296cf --- /dev/null +++ b/src/app/[locale]/(protected)/dashboard/_components/TotalProfitEarned.tsx @@ -0,0 +1,32 @@ +import { Col } from 'react-bootstrap'; +import TrendingUpIcon from 'mdi-react/TrendingUpIcon'; +import ProgressBar from '@/shared/components/ProgressBar'; +import { Card } from '@/shared/components/Card'; +import { colorRed } from '@/styles/palette'; +import { + DashboardPortfolioCard, + PortfolioCardDescription, + PortfolioCardTitle, + PortfolioCardWrap, +} from './DashboardCardElements'; +import { useTranslations } from 'next-intl'; + +const TotalProfitEarned = () => { + const t = useTranslations('Dashboard'); + return ( + + + + + $ 165 832 + + + {t('total-profit-earned')} + + + + + ); +}; + +export default TotalProfitEarned; diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/[locale]/(protected)/dashboard/page.tsx similarity index 82% rename from src/app/(protected)/dashboard/page.tsx rename to src/app/[locale]/(protected)/dashboard/page.tsx index 6f24d93..0c02e87 100644 --- a/src/app/(protected)/dashboard/page.tsx +++ b/src/app/[locale]/(protected)/dashboard/page.tsx @@ -6,15 +6,17 @@ import TotalProfitEarned from './_components/TotalProfitEarned'; import RecentTransactions from './_components/RecentTransactions'; import TotalAssets from './_components/TotalAssets'; import PendingOrders from './_components/PendingOrders'; +import { useTranslations } from 'next-intl'; const Dashboard = () => { useTitle('Dashboard - BeeQuant'); + const t = useTranslations('Dashboard'); return ( -

BeeQuant Dashboard

+

{t('header')}

diff --git a/src/app/[locale]/(protected)/exchange/page.tsx b/src/app/[locale]/(protected)/exchange/page.tsx new file mode 100644 index 0000000..8de3ab8 --- /dev/null +++ b/src/app/[locale]/(protected)/exchange/page.tsx @@ -0,0 +1,7 @@ +import ExchangePage from 'module/exchange/ExchangePage'; + +const ExchangeManagement = () => { + return ; +}; + +export default ExchangeManagement; diff --git a/src/app/(protected)/layout.tsx b/src/app/[locale]/(protected)/layout.tsx similarity index 100% rename from src/app/(protected)/layout.tsx rename to src/app/[locale]/(protected)/layout.tsx diff --git a/src/app/_lib/registry.tsx b/src/app/[locale]/_lib/registry.tsx similarity index 100% rename from src/app/_lib/registry.tsx rename to src/app/[locale]/_lib/registry.tsx diff --git a/src/app/global.css b/src/app/[locale]/global.css similarity index 100% rename from src/app/global.css rename to src/app/[locale]/global.css diff --git a/src/app/home/page.tsx b/src/app/[locale]/home/page.tsx similarity index 100% rename from src/app/home/page.tsx rename to src/app/[locale]/home/page.tsx diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..f44dd53 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,42 @@ +import Providers from './provider'; +import './global.css'; +import 'bootstrap/dist/css/bootstrap.css'; +import ThemeProvider from './themeProvider'; +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; + +export const metadata = { + title: 'BeeQuant Platform', + description: 'Generated AI with Quantitative trading platform', +}; + +export async function generateStaticParams() { + return ['en', 'zh-cn'].map((locale) => ({ locale })); +} + +export default async function RootLayout({ + children, + params: { locale }, +}: { + children: React.ReactNode; + params: { locale: string }; +}) { + const messages = await getMessages({ locale }); + return ( + + + + + + + + + +
{children}
+
+
+
+ + + ); +} diff --git a/src/app/not-found.tsx b/src/app/[locale]/not-found.tsx similarity index 100% rename from src/app/not-found.tsx rename to src/app/[locale]/not-found.tsx diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 100% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx diff --git a/src/app/provider.tsx b/src/app/[locale]/provider.tsx similarity index 91% rename from src/app/provider.tsx rename to src/app/[locale]/provider.tsx index db6010f..c0237a3 100644 --- a/src/app/provider.tsx +++ b/src/app/[locale]/provider.tsx @@ -4,7 +4,7 @@ import { ApolloProvider } from '@apollo/client'; import { client } from 'boot/apollo'; import { ReactNode } from 'react'; import ScrollToTop from '@/styles/ScrollToTop'; -import LoadUser from '../shared/components/LoadUser'; +import LoadUser from '../../shared/components/LoadUser'; import StyledComponentsRegistry from './_lib/registry'; interface Props { diff --git a/src/app/themeProvider.tsx b/src/app/[locale]/themeProvider.tsx similarity index 100% rename from src/app/themeProvider.tsx rename to src/app/[locale]/themeProvider.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index b67e8cb..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ReactNode } from 'react'; -import Providers from './provider'; -import './global.css'; -import 'bootstrap/dist/css/bootstrap.css'; -import ThemeProvider from './themeProvider'; - -export const metadata = { - title: 'BeeQuant Platform', - description: 'Generated AI with Quantitative trading platform', -}; - -export default function RootLayout({ children }: { children: ReactNode }) { - return ( - - - - - - - - -
{children}
-
-
- - - ); -} diff --git a/src/config/config.ts b/src/config/config.ts index d54a530..d6403c0 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,3 +1,6 @@ export const config = { referenceName: process.env.VITE_APP_REFERENCE_NAME || 'Default_Reference_Name', }; + +export const defaultLocale = 'en' as const; +export const locales = ['en', 'zh-cn'] as const; diff --git a/src/config/navigation.ts b/src/config/navigation.ts new file mode 100644 index 0000000..19d487e --- /dev/null +++ b/src/config/navigation.ts @@ -0,0 +1,6 @@ +import { createSharedPathnamesNavigation } from 'next-intl/navigation'; +import { locales } from './config'; + +export const { Link, redirect, usePathname, useRouter } = createSharedPathnamesNavigation({ + locales, +}); diff --git a/src/containers/App/MainWrapper.tsx b/src/containers/App/MainWrapper.tsx new file mode 100644 index 0000000..e914e2e --- /dev/null +++ b/src/containers/App/MainWrapper.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from 'react'; + +export type MainWrapperProps = { + children: ReactNode; +}; + +const MainWrapper = ({ children }: MainWrapperProps) =>
{children}
; + +export default MainWrapper; diff --git a/src/containers/Layout/sidebar/Sidebar.tsx b/src/containers/Layout/sidebar/Sidebar.tsx index 6798389..2cfd35a 100644 --- a/src/containers/Layout/sidebar/Sidebar.tsx +++ b/src/containers/Layout/sidebar/Sidebar.tsx @@ -76,7 +76,7 @@ const SidebarWrap = styled.div<{ $show?: boolean; $collapse?: boolean; topNaviga padding-top: 0; z-index: 101; display: none; - + ${(props) => props.$show && ` @@ -107,10 +107,10 @@ const SidebarWrap = styled.div<{ $show?: boolean; $collapse?: boolean; topNaviga ${marginRight(props)}: 188px; } } - `}; + `} + } @media screen and (min-width: 1300px) { - ${(props) => props.topNavigation && ` diff --git a/src/containers/Layout/sidebar/SidebarContent.tsx b/src/containers/Layout/sidebar/SidebarContent.tsx index 410e1fb..2e23cc5 100644 --- a/src/containers/Layout/sidebar/SidebarContent.tsx +++ b/src/containers/Layout/sidebar/SidebarContent.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { AUTH_TOKEN, THEME } from '@/shared/constants/storage'; import { useUserContext } from '@/hooks/userHooks'; import { ROUTE_KEY, getPublicRouteByKey, getRouteByKey } from '@/routes/routeConfig'; +import { useTranslations } from 'next-intl'; import SidebarCategory from './SidebarCategory'; import SidebarLink, { SidebarLinkTitle, SidebarNavLink } from './SidebarLink'; @@ -16,6 +17,7 @@ type SidebarContentProps = { const SidebarContent = ({ onClick, $collapse }: SidebarContentProps) => { const { store, setStore } = useUserContext(); + const t = useTranslations(); const logout = () => { sessionStorage.setItem(AUTH_TOKEN, ''); localStorage.setItem(AUTH_TOKEN, ''); @@ -28,92 +30,108 @@ const SidebarContent = ({ onClick, $collapse }: SidebarContentProps) => { }); localStorage.setItem(THEME, color); }; - return ( - + {}} /> {}} /> {}} /> {}} /> - + {}} /> {}} /> {}} /> {}} /> - + {/* eslint-disable-next-line max-len */} {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} changeTheme('light')}> - Light Theme + {t('Shared.lightTheme')} {/* eslint-disable-next-line max-len */} {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} changeTheme('dark')}> - Dark Theme + {t('Shared.darkTheme')} - + + { const { store } = useUserContext(); const [isCollapsed, setIsCollapsed] = useState(false); - + const t = useTranslations(); const toggleCollapse = () => { setIsCollapsed(!isCollapsed); }; @@ -53,25 +54,25 @@ const TopbarProfile = () => { - + diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..8cc36b9 --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,17 @@ +import { notFound } from 'next/navigation'; +import { getRequestConfig } from 'next-intl/server'; + +const locales = ['en', 'zh-cn']; +export default getRequestConfig(async ({ locale }) => { + // Validate that the incoming `locale` parameter is valid + if (!locales.includes(locale as any)) notFound(); + + return { + messages: ( + await (locale === 'en' + ? // When using Turbopack, this will enable HMR for `en` + import('../messages/en.json') + : import('../messages/zh-cn.json')) + ).default, + }; +}); diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..43f897a --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,14 @@ +import createMiddleware from 'next-intl/middleware'; + +export default createMiddleware({ + // A list of all locales that are supported + locales: ['en', 'zh-cn'], + + // Used when no locale matches + defaultLocale: 'en', +}); + +export const config = { + // Match only internationalized pathnames + matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'], +}; diff --git a/src/module/account/settings/SettingForm.tsx b/src/module/account/settings/SettingForm.tsx index 24c3204..bd34f98 100644 --- a/src/module/account/settings/SettingForm.tsx +++ b/src/module/account/settings/SettingForm.tsx @@ -4,23 +4,28 @@ import React from 'react'; import { Col, Container, Row } from 'react-bootstrap'; import { Card, CardBody, CardTitleWrap, CardTitle, CardSubhead } from '@/shared/components/Card'; import SettingFormLayout from './SettingFormLayout'; +import { useTranslations } from 'next-intl'; -const SettingForm = () => ( - - - - - - - Settings - Update your profile - - - - - - - -); +const SettingForm = () => { + const t = useTranslations('SettingPage'); + + return ( + + + + + + + {t('title')} + {t('subtitle')} + + + + + + + + ); +}; export default SettingForm; diff --git a/src/module/account/settings/SettingFormLayout.tsx b/src/module/account/settings/SettingFormLayout.tsx index f6ff6fb..8f291f4 100644 --- a/src/module/account/settings/SettingFormLayout.tsx +++ b/src/module/account/settings/SettingFormLayout.tsx @@ -5,26 +5,28 @@ import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { Button } from '@/shared/components/Button'; import * as z from 'zod'; -import { DisplayErrorMsgs, EmailErrorMsgs, RefErrorMsgs } from '@/shared/utils/helpers'; import FormInput from './_component/FormInput/FormInput'; - -const formSchema = z.object({ - displayName: z - .string() - .min(1, { message: DisplayErrorMsgs.Required }) - .min(4, { message: DisplayErrorMsgs.MinLength }) - .max(15, { message: DisplayErrorMsgs.MaxLength }) - .regex(/^[a-zA-Z0-9-_]+$/, { - message: DisplayErrorMsgs.Invalid, - }), - email: z - .string() - .min(1, { message: EmailErrorMsgs.Required }) - .email({ message: EmailErrorMsgs.Invalid }), - ref: z.string().min(1, { message: RefErrorMsgs.Required }), -}); +import { useTranslations } from 'next-intl'; function SettingFormLayout() { + const t = useTranslations(); + const formSchema = z.object({ + displayName: z + .string() + .min(1, { message: t('Notifications.displayName.required') }) + .min(4, { message: t('Notifications.displayName.minLength') }) + .max(15, { message: t('Notifications.displayName.maxLength') }) + .regex(/^[a-zA-Z0-9-_]+$/, { + message: t('Notifications.displayName.invalid'), + }), + email: z + .string() + .min(1, { message: t('Notifications.email.required') }) + .email({ message: t('Notifications.email.invalid') }), + ref: z.string().min(1, { message: t('Notifications.ref.required') }), + }); + + // const t = useTranslations('SettingPage'); const { handleSubmit, reset, control } = useForm({ defaultValues: { realName: '', @@ -47,41 +49,56 @@ function SettingFormLayout() { name="realName" control={control} render={({ field, formState: { errors } }) => ( - + )} /> ( - + )} /> ( - + )} /> ( - + )} /> ( - + )} /> {/* @ts-ignore - Ignoring because of complex union types incorrectly inferred */} diff --git a/src/module/app-setting/AppSettingFormLayout.test.tsx b/src/module/app-setting/AppSettingFormLayout.test.tsx new file mode 100644 index 0000000..c8272af --- /dev/null +++ b/src/module/app-setting/AppSettingFormLayout.test.tsx @@ -0,0 +1,326 @@ +import { waitFor } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { useUserContext } from '@/hooks/userHooks'; +import { render, screen, within } from '@/shared/utils/mockThemeProvider'; +import { hexToRgb } from '@/shared/utils/hexToRGB'; +import 'jest-styled-components'; +import { AppSettingFormLayout } from './AppSettingFormLayout'; +import { NextIntlClientProvider } from 'next-intl'; + +// Mock 404.tsx to get rid of +// 'Expression produces a union type that is too complex to represent' error from its