Просмотр исходного кода

feat: add multiple language support (#37)

* feat: multiple language support

* feat: multiple languagee support

* fix: language setting typo

* fix: remove third language (malay)

* chore: run code formatting

---------

Co-authored-by: Teo Wen Long <teowenlong0316@gmail.com>
Co-authored-by: satnaing <satnaingdev@gmail.com>
Teo 1 год назад
Родитель
Сommit
c5d163d48b

+ 1 - 0
package.json

@@ -44,6 +44,7 @@
     "recharts": "^2.12.5",
     "tailwind-merge": "^2.2.2",
     "tailwindcss-animate": "^1.0.7",
+    "use-intl": "^3.20.0",
     "zod": "^3.22.4"
   },
   "devDependencies": {

+ 63 - 0
pnpm-lock.yaml

@@ -104,6 +104,9 @@ importers:
       tailwindcss-animate:
         specifier: ^1.0.7
         version: 1.0.7(tailwindcss@3.4.3)
+      use-intl:
+        specifier: ^3.20.0
+        version: 3.20.0(react@18.2.0)
       zod:
         specifier: ^3.22.4
         version: 3.22.4
@@ -349,6 +352,21 @@ packages:
   '@floating-ui/utils@0.2.1':
     resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
 
+  '@formatjs/ecma402-abstract@2.0.0':
+    resolution: {integrity: sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==}
+
+  '@formatjs/fast-memoize@2.2.0':
+    resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==}
+
+  '@formatjs/icu-messageformat-parser@2.7.8':
+    resolution: {integrity: sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==}
+
+  '@formatjs/icu-skeleton-parser@1.8.2':
+    resolution: {integrity: sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==}
+
+  '@formatjs/intl-localematcher@0.5.4':
+    resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==}
+
   '@hookform/resolvers@3.3.4':
     resolution: {integrity: sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==}
     peerDependencies:
@@ -1701,6 +1719,9 @@ packages:
     resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
     engines: {node: '>=12'}
 
+  intl-messageformat@10.5.14:
+    resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==}
+
   invariant@2.2.4:
     resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
 
@@ -2331,6 +2352,11 @@ packages:
       '@types/react':
         optional: true
 
+  use-intl@3.20.0:
+    resolution: {integrity: sha512-5WQs6yZVWI9K7vw3134P0bhKNp4mi8NbmqKOCuhD9nQUMTKdmpBXwjk62+axwvEbj4XrZxj4X93mQMLXU5ZsCg==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   use-sidecar@1.1.2:
     resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
     engines: {node: '>=10'}
@@ -2532,6 +2558,30 @@ snapshots:
 
   '@floating-ui/utils@0.2.1': {}
 
+  '@formatjs/ecma402-abstract@2.0.0':
+    dependencies:
+      '@formatjs/intl-localematcher': 0.5.4
+      tslib: 2.6.2
+
+  '@formatjs/fast-memoize@2.2.0':
+    dependencies:
+      tslib: 2.6.2
+
+  '@formatjs/icu-messageformat-parser@2.7.8':
+    dependencies:
+      '@formatjs/ecma402-abstract': 2.0.0
+      '@formatjs/icu-skeleton-parser': 1.8.2
+      tslib: 2.6.2
+
+  '@formatjs/icu-skeleton-parser@1.8.2':
+    dependencies:
+      '@formatjs/ecma402-abstract': 2.0.0
+      tslib: 2.6.2
+
+  '@formatjs/intl-localematcher@0.5.4':
+    dependencies:
+      tslib: 2.6.2
+
   '@hookform/resolvers@3.3.4(react-hook-form@7.51.3(react@18.2.0))':
     dependencies:
       react-hook-form: 7.51.3(react@18.2.0)
@@ -3965,6 +4015,13 @@ snapshots:
 
   internmap@2.0.3: {}
 
+  intl-messageformat@10.5.14:
+    dependencies:
+      '@formatjs/ecma402-abstract': 2.0.0
+      '@formatjs/fast-memoize': 2.2.0
+      '@formatjs/icu-messageformat-parser': 2.7.8
+      tslib: 2.6.2
+
   invariant@2.2.4:
     dependencies:
       loose-envify: 1.4.0
@@ -4536,6 +4593,12 @@ snapshots:
     optionalDependencies:
       '@types/react': 18.2.79
 
+  use-intl@3.20.0(react@18.2.0):
+    dependencies:
+      '@formatjs/fast-memoize': 2.2.0
+      intl-messageformat: 10.5.14
+      react: 18.2.0
+
   use-sidecar@1.1.2(@types/react@18.2.79)(react@18.2.0):
     dependencies:
       detect-node-es: 1.1.0

+ 61 - 0
src/components/language-provider.tsx

@@ -0,0 +1,61 @@
+import { createContext, useContext, useState } from 'react'
+import { IntlProvider } from 'use-intl'
+import translations from '../translations'
+
+export type Language = 'en' | 'zh'
+
+type LanguageProviderProps = {
+  children: React.ReactNode
+  defaultLanguage?: Language
+  storageKey?: string
+}
+
+type LanguageProviderState = {
+  language: Language
+  setLanguage: (lang: Language) => void
+}
+
+const initialState: LanguageProviderState = {
+  language: 'en',
+  setLanguage: () => null,
+}
+
+const LanguageProviderContext =
+  createContext<LanguageProviderState>(initialState)
+
+export function LanguageProvider({
+  children,
+  defaultLanguage = 'en',
+  storageKey = 'vite-ui-language',
+  ...props
+}: LanguageProviderProps) {
+  const [language, setLanguage] = useState<Language>(
+    () => (localStorage.getItem(storageKey) as Language) || defaultLanguage
+  )
+
+  const value = {
+    language,
+    setLanguage: (lang: Language) => {
+      localStorage.setItem(storageKey, lang)
+      setLanguage(lang)
+    },
+  }
+
+  return (
+    <LanguageProviderContext.Provider {...props} value={value}>
+      <IntlProvider locale={language} messages={translations[language]}>
+        {children}
+      </IntlProvider>
+    </LanguageProviderContext.Provider>
+  )
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useLanguage = () => {
+  const context = useContext(LanguageProviderContext)
+
+  if (context === undefined)
+    throw new Error('useLanguage must be used within a LanguageProvider')
+
+  return context
+}

+ 49 - 0
src/components/language-switch.tsx

@@ -0,0 +1,49 @@
+import { Language, useLanguage } from './language-provider'
+import { IconCheck } from '@tabler/icons-react'
+import { cn } from '@/lib/utils'
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+import { Button } from './custom/button'
+
+export default function LanguageSwitch() {
+  const { language, setLanguage } = useLanguage()
+
+  const languageText = new Map<string, string>([
+    ['en', 'English'],
+    ['zh', '简体中文'],
+  ])
+
+  const renderDropdownItem = () => {
+    return Array.from(languageText).map(([key, value]) => (
+      <DropdownMenuItem key={key} onClick={() => setLanguage(key as Language)}>
+        {value}{' '}
+        <IconCheck
+          size={14}
+          className={cn('ml-auto', language !== key && 'hidden')}
+        />
+      </DropdownMenuItem>
+    ))
+  }
+
+  return (
+    <DropdownMenu>
+      <DropdownMenuTrigger asChild>
+        <Button
+          variant='outline'
+          size='default'
+          className='scale-95 rounded-full'
+        >
+          {languageText.get(language)}
+          <span className='sr-only'>Toggle language</span>
+        </Button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent align='end'>
+        {renderDropdownItem()}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  )
+}

+ 5 - 2
src/components/nav.tsx

@@ -23,6 +23,7 @@ import {
 import { cn } from '@/lib/utils'
 import useCheckActiveNav from '@/hooks/use-check-active-nav'
 import { SideLink } from '@/data/sidelinks'
+import { useTranslations } from 'use-intl'
 
 interface NavProps extends React.HTMLAttributes<HTMLDivElement> {
   isCollapsed: boolean
@@ -89,6 +90,7 @@ function NavLink({
   subLink = false,
 }: NavLinkProps) {
   const { checkActiveNav } = useCheckActiveNav()
+  const t = useTranslations()
   return (
     <Link
       to={href}
@@ -104,7 +106,7 @@ function NavLink({
       aria-current={checkActiveNav(href) ? 'page' : undefined}
     >
       <div className='mr-2'>{icon}</div>
-      {title}
+      {t(title)}
       {label && (
         <div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
           {label}
@@ -116,6 +118,7 @@ function NavLink({
 
 function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
   const { checkActiveNav } = useCheckActiveNav()
+  const t = useTranslations()
 
   /* Open collapsible by default
    * if one of child element is active */
@@ -130,7 +133,7 @@ function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
         )}
       >
         <div className='mr-2'>{icon}</div>
-        {title}
+        {t(title)}
         {label && (
           <div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
             {label}

+ 3 - 1
src/components/search.tsx

@@ -1,11 +1,13 @@
 import { Input } from '@/components/ui/input'
+import { useTranslations } from 'use-intl'
 
 export function Search() {
+  const t = useTranslations('dashboard')
   return (
     <div>
       <Input
         type='search'
-        placeholder='Search...'
+        placeholder={t('search')}
         className='md:w-[100px] lg:w-[300px]'
       />
     </div>

+ 4 - 2
src/components/top-nav.tsx

@@ -8,6 +8,7 @@ import {
 } from '@/components/ui/dropdown-menu'
 import { Button } from './custom/button'
 import { IconMenu } from '@tabler/icons-react'
+import { useTranslations } from 'use-intl'
 
 interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
   links: {
@@ -18,6 +19,7 @@ interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
 }
 
 export function TopNav({ className, links, ...props }: TopNavProps) {
+  const t = useTranslations()
   return (
     <>
       <div className='md:hidden'>
@@ -34,7 +36,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
                   to={href}
                   className={!isActive ? 'text-muted-foreground' : ''}
                 >
-                  {title}
+                  {t(title)}
                 </Link>
               </DropdownMenuItem>
             ))}
@@ -55,7 +57,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
             to={href}
             className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
           >
-            {title}
+            {t(title)}
           </Link>
         ))}
       </nav>

+ 7 - 5
src/components/user-nav.tsx

@@ -10,8 +10,10 @@ import {
   DropdownMenuShortcut,
   DropdownMenuTrigger,
 } from '@/components/ui/dropdown-menu'
+import { useTranslations } from 'use-intl'
 
 export function UserNav() {
+  const t = useTranslations('userNav')
   return (
     <DropdownMenu>
       <DropdownMenuTrigger asChild>
@@ -34,22 +36,22 @@ export function UserNav() {
         <DropdownMenuSeparator />
         <DropdownMenuGroup>
           <DropdownMenuItem>
-            Profile
+            {t('profile')}
             <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
           </DropdownMenuItem>
           <DropdownMenuItem>
-            Billing
+            {t('billing')}
             <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
           </DropdownMenuItem>
           <DropdownMenuItem>
-            Settings
+            {t('settings')}
             <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
           </DropdownMenuItem>
-          <DropdownMenuItem>New Team</DropdownMenuItem>
+          <DropdownMenuItem>{t('new_team')}</DropdownMenuItem>
         </DropdownMenuGroup>
         <DropdownMenuSeparator />
         <DropdownMenuItem>
-          Log out
+          {t('log_out')}
           <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
         </DropdownMenuItem>
       </DropdownMenuContent>

+ 22 - 22
src/data/sidelinks.tsx

@@ -36,61 +36,61 @@ export interface SideLink extends NavLink {
 
 export const sidelinks: SideLink[] = [
   {
-    title: 'Dashboard',
+    title: 'sidebar.dashboard',
     label: '',
     href: '/',
     icon: <IconLayoutDashboard size={18} />,
   },
   {
-    title: 'Tasks',
+    title: 'sidebar.tasks',
     label: '3',
     href: '/tasks',
     icon: <IconChecklist size={18} />,
   },
   {
-    title: 'Chats',
+    title: 'sidebar.chats',
     label: '9',
     href: '/chats',
     icon: <IconMessages size={18} />,
   },
   {
-    title: 'Apps',
+    title: 'sidebar.apps',
     label: '',
     href: '/apps',
     icon: <IconApps size={18} />,
   },
   {
-    title: 'Authentication',
+    title: 'sidebar.authentication',
     label: '',
     href: '',
     icon: <IconUserShield size={18} />,
     sub: [
       {
-        title: 'Sign In (email + password)',
+        title: 'sidebar.auth_sign_in_email_password',
         label: '',
         href: '/sign-in',
         icon: <IconHexagonNumber1 size={18} />,
       },
       {
-        title: 'Sign In (Box)',
+        title: 'sidebar.auth_sign_in_box',
         label: '',
         href: '/sign-in-2',
         icon: <IconHexagonNumber2 size={18} />,
       },
       {
-        title: 'Sign Up',
+        title: 'sidebar.auth_sign_up',
         label: '',
         href: '/sign-up',
         icon: <IconHexagonNumber3 size={18} />,
       },
       {
-        title: 'Forgot Password',
+        title: 'sidebar.auth_forgot_password',
         label: '',
         href: '/forgot-password',
         icon: <IconHexagonNumber4 size={18} />,
       },
       {
-        title: 'OTP',
+        title: 'sidebar.auth_otp',
         label: '',
         href: '/otp',
         icon: <IconHexagonNumber5 size={18} />,
@@ -98,25 +98,25 @@ export const sidelinks: SideLink[] = [
     ],
   },
   {
-    title: 'Users',
+    title: 'sidebar.users',
     label: '',
     href: '/users',
     icon: <IconUsers size={18} />,
   },
   {
-    title: 'Requests',
+    title: 'sidebar.requests',
     label: '10',
     href: '/requests',
     icon: <IconRouteAltLeft size={18} />,
     sub: [
       {
-        title: 'Trucks',
+        title: 'sidebar.requests_trucks',
         label: '9',
         href: '/trucks',
         icon: <IconTruck size={18} />,
       },
       {
-        title: 'Cargos',
+        title: 'sidebar.requests_cargos',
         label: '',
         href: '/cargos',
         icon: <IconBoxSeam size={18} />,
@@ -124,43 +124,43 @@ export const sidelinks: SideLink[] = [
     ],
   },
   {
-    title: 'Analysis',
+    title: 'sidebar.analytics',
     label: '',
     href: '/analysis',
     icon: <IconChartHistogram size={18} />,
   },
   {
-    title: 'Extra Components',
+    title: 'sidebar.extra_components',
     label: '',
     href: '/extra-components',
     icon: <IconComponents size={18} />,
   },
   {
-    title: 'Error Pages',
+    title: 'sidebar.error_pages',
     label: '',
     href: '',
     icon: <IconExclamationCircle size={18} />,
     sub: [
       {
-        title: 'Not Found',
+        title: 'sidebar.error_not_found',
         label: '',
         href: '/404',
         icon: <IconError404 size={18} />,
       },
       {
-        title: 'Internal Server Error',
+        title: 'sidebar.error_internal_server',
         label: '',
         href: '/500',
         icon: <IconServerOff size={18} />,
       },
       {
-        title: 'Maintenance Error',
+        title: 'sidebar.error_maintenance',
         label: '',
         href: '/503',
         icon: <IconBarrierBlock size={18} />,
       },
       {
-        title: 'Unauthorised Error',
+        title: 'sidebar.error_unauthorised',
         label: '',
         href: '/401',
         icon: <IconLock size={18} />,
@@ -168,7 +168,7 @@ export const sidelinks: SideLink[] = [
     ],
   },
   {
-    title: 'Settings',
+    title: 'sidebar.settings',
     label: '',
     href: '/settings',
     icon: <IconSettings size={18} />,

+ 5 - 2
src/main.tsx

@@ -3,14 +3,17 @@ import ReactDOM from 'react-dom/client'
 import { RouterProvider } from 'react-router-dom'
 import { Toaster } from '@/components/ui/toaster'
 import { ThemeProvider } from '@/components/theme-provider'
+import { LanguageProvider } from '@/components/language-provider'
 import router from '@/router'
 import '@/index.css'
 
 ReactDOM.createRoot(document.getElementById('root')!).render(
   <React.StrictMode>
     <ThemeProvider defaultTheme='dark' storageKey='vite-ui-theme'>
-      <RouterProvider router={router} />
-      <Toaster />
+      <LanguageProvider defaultLanguage='en' storageKey='vite-ui-language'>
+        <RouterProvider router={router} />
+        <Toaster />
+      </LanguageProvider>
     </ThemeProvider>
   </React.StrictMode>
 )

+ 31 - 21
src/pages/dashboard/index.tsx

@@ -14,8 +14,11 @@ import { TopNav } from '@/components/top-nav'
 import { UserNav } from '@/components/user-nav'
 import { RecentSales } from './components/recent-sales'
 import { Overview } from './components/overview'
+import { useTranslations } from 'use-intl'
+import LanguageSwitch from '@/components/language-switch'
 
 export default function Dashboard() {
+  const t = useTranslations('dashboard')
   return (
     <Layout>
       {/* ===== Top Heading ===== */}
@@ -24,6 +27,7 @@ export default function Dashboard() {
         <div className='ml-auto flex items-center space-x-4'>
           <Search />
           <ThemeSwitch />
+          <LanguageSwitch />
           <UserNav />
         </div>
       </Layout.Header>
@@ -31,9 +35,11 @@ export default function Dashboard() {
       {/* ===== Main ===== */}
       <Layout.Body>
         <div className='mb-2 flex items-center justify-between space-y-2'>
-          <h1 className='text-2xl font-bold tracking-tight'>Dashboard</h1>
+          <h1 className='text-2xl font-bold tracking-tight'>
+            {t('dashboard')}
+          </h1>
           <div className='flex items-center space-x-2'>
-            <Button>Download</Button>
+            <Button>{t('download')}</Button>
           </div>
         </div>
         <Tabs
@@ -43,10 +49,12 @@ export default function Dashboard() {
         >
           <div className='w-full overflow-x-auto pb-2'>
             <TabsList>
-              <TabsTrigger value='overview'>Overview</TabsTrigger>
-              <TabsTrigger value='analytics'>Analytics</TabsTrigger>
-              <TabsTrigger value='reports'>Reports</TabsTrigger>
-              <TabsTrigger value='notifications'>Notifications</TabsTrigger>
+              <TabsTrigger value='overview'>{t('overview')}</TabsTrigger>
+              <TabsTrigger value='analytics'>{t('analytics')}</TabsTrigger>
+              <TabsTrigger value='reports'>{t('reports')}</TabsTrigger>
+              <TabsTrigger value='notifications'>
+                {t('notifications')}
+              </TabsTrigger>
             </TabsList>
           </div>
           <TabsContent value='overview' className='space-y-4'>
@@ -54,7 +62,7 @@ export default function Dashboard() {
               <Card>
                 <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
                   <CardTitle className='text-sm font-medium'>
-                    Total Revenue
+                    {t('total_revenue')}
                   </CardTitle>
                   <svg
                     xmlns='http://www.w3.org/2000/svg'
@@ -72,14 +80,14 @@ export default function Dashboard() {
                 <CardContent>
                   <div className='text-2xl font-bold'>$45,231.89</div>
                   <p className='text-xs text-muted-foreground'>
-                    +20.1% from last month
+                    {t('from_last_month', { amount: '+20.1%' })}
                   </p>
                 </CardContent>
               </Card>
               <Card>
                 <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
                   <CardTitle className='text-sm font-medium'>
-                    Subscriptions
+                    {t('subscriptions')}
                   </CardTitle>
                   <svg
                     xmlns='http://www.w3.org/2000/svg'
@@ -99,13 +107,15 @@ export default function Dashboard() {
                 <CardContent>
                   <div className='text-2xl font-bold'>+2350</div>
                   <p className='text-xs text-muted-foreground'>
-                    +180.1% from last month
+                    {t('from_last_month', { amount: '+180.1%' })}
                   </p>
                 </CardContent>
               </Card>
               <Card>
                 <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
-                  <CardTitle className='text-sm font-medium'>Sales</CardTitle>
+                  <CardTitle className='text-sm font-medium'>
+                    {t('sales')}
+                  </CardTitle>
                   <svg
                     xmlns='http://www.w3.org/2000/svg'
                     viewBox='0 0 24 24'
@@ -123,14 +133,14 @@ export default function Dashboard() {
                 <CardContent>
                   <div className='text-2xl font-bold'>+12,234</div>
                   <p className='text-xs text-muted-foreground'>
-                    +19% from last month
+                    {t('from_last_month', { amount: '+19%' })}
                   </p>
                 </CardContent>
               </Card>
               <Card>
                 <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
                   <CardTitle className='text-sm font-medium'>
-                    Active Now
+                    {t('active_now')}
                   </CardTitle>
                   <svg
                     xmlns='http://www.w3.org/2000/svg'
@@ -148,7 +158,7 @@ export default function Dashboard() {
                 <CardContent>
                   <div className='text-2xl font-bold'>+573</div>
                   <p className='text-xs text-muted-foreground'>
-                    +201 since last hour
+                    {t('since_last_hour', { amount: '+201' })}
                   </p>
                 </CardContent>
               </Card>
@@ -156,7 +166,7 @@ export default function Dashboard() {
             <div className='grid grid-cols-1 gap-4 lg:grid-cols-7'>
               <Card className='col-span-1 lg:col-span-4'>
                 <CardHeader>
-                  <CardTitle>Overview</CardTitle>
+                  <CardTitle>{t('overview')}</CardTitle>
                 </CardHeader>
                 <CardContent className='pl-2'>
                   <Overview />
@@ -164,9 +174,9 @@ export default function Dashboard() {
               </Card>
               <Card className='col-span-1 lg:col-span-3'>
                 <CardHeader>
-                  <CardTitle>Recent Sales</CardTitle>
+                  <CardTitle>{t('recent_sales')}</CardTitle>
                   <CardDescription>
-                    You made 265 sales this month.
+                    {t('recent_sales_desc', { amount: '265' })}
                   </CardDescription>
                 </CardHeader>
                 <CardContent>
@@ -183,22 +193,22 @@ export default function Dashboard() {
 
 const topNav = [
   {
-    title: 'Overview',
+    title: 'dashboard.overview',
     href: 'dashboard/overview',
     isActive: true,
   },
   {
-    title: 'Customers',
+    title: 'dashboard.customers',
     href: 'dashboard/customers',
     isActive: false,
   },
   {
-    title: 'Products',
+    title: 'dashboard.products',
     href: 'dashboard/products',
     isActive: false,
   },
   {
-    title: 'Settings',
+    title: 'dashboard.settings',
     href: 'dashboard/settings',
     isActive: false,
   },

+ 6 - 4
src/pages/errors/general-error.tsx

@@ -1,6 +1,7 @@
 import { useNavigate } from 'react-router-dom'
 import { Button } from '@/components/custom/button'
 import { cn } from '@/lib/utils'
+import { useTranslations } from 'use-intl'
 
 interface GeneralErrorProps extends React.HTMLAttributes<HTMLDivElement> {
   minimal?: boolean
@@ -11,22 +12,23 @@ export default function GeneralError({
   minimal = false,
 }: GeneralErrorProps) {
   const navigate = useNavigate()
+  const t = useTranslations('error')
   return (
     <div className={cn('h-svh w-full', className)}>
       <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
         {!minimal && (
           <h1 className='text-[7rem] font-bold leading-tight'>500</h1>
         )}
-        <span className='font-medium'>Oops! Something went wrong {`:')`}</span>
+        <span className='font-medium'>{t('500_title')}</span>
         <p className='text-center text-muted-foreground'>
-          We apologize for the inconvenience. <br /> Please try again later.
+          {t.rich('500_desc', { br: () => <br /> })}
         </p>
         {!minimal && (
           <div className='mt-6 flex gap-4'>
             <Button variant='outline' onClick={() => navigate(-1)}>
-              Go Back
+              {t('go_back')}
             </Button>
-            <Button onClick={() => navigate('/')}>Back to Home</Button>
+            <Button onClick={() => navigate('/')}>{t('back_to_home')}</Button>
           </div>
         )}
       </div>

+ 5 - 4
src/pages/errors/maintenance-error.tsx

@@ -1,17 +1,18 @@
 import { Button } from '@/components/custom/button'
+import { useTranslations } from 'use-intl'
 
 export default function MaintenanceError() {
+  const t = useTranslations('error')
   return (
     <div className='h-svh'>
       <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
         <h1 className='text-[7rem] font-bold leading-tight'>503</h1>
-        <span className='font-medium'>Website is under maintenance!</span>
+        <span className='font-medium'>{t('maintenance_title')}</span>
         <p className='text-center text-muted-foreground'>
-          The site is not available at the moment. <br />
-          We'll be back online shortly.
+          {t.rich('maintenance_desc', { br: () => <br /> })}
         </p>
         <div className='mt-6 flex gap-4'>
-          <Button variant='outline'>Learn more</Button>
+          <Button variant='outline'>{t('learn_more')}</Button>
         </div>
       </div>
     </div>

+ 6 - 5
src/pages/errors/not-found-error.tsx

@@ -1,22 +1,23 @@
 import { useNavigate } from 'react-router-dom'
 import { Button } from '@/components/custom/button'
+import { useTranslations } from 'use-intl'
 
 export default function NotFoundError() {
   const navigate = useNavigate()
+  const t = useTranslations('error')
   return (
     <div className='h-svh'>
       <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
         <h1 className='text-[7rem] font-bold leading-tight'>404</h1>
-        <span className='font-medium'>Oops! Page Not Found!</span>
+        <span className='font-medium'>{t('404_title')}</span>
         <p className='text-center text-muted-foreground'>
-          It seems like the page you're looking for <br />
-          does not exist or might have been removed.
+          {t.rich('404_desc', { br: () => <br /> })}
         </p>
         <div className='mt-6 flex gap-4'>
           <Button variant='outline' onClick={() => navigate(-1)}>
-            Go Back
+            {t('go_back')}
           </Button>
-          <Button onClick={() => navigate('/')}>Back to Home</Button>
+          <Button onClick={() => navigate('/')}>{t('back_to_home')}</Button>
         </div>
       </div>
     </div>

+ 6 - 8
src/pages/errors/unauthorised-error.tsx

@@ -1,25 +1,23 @@
 import { useNavigate } from 'react-router-dom'
 import { Button } from '@/components/custom/button'
+import { useTranslations } from 'use-intl'
 
 export default function UnauthorisedError() {
   const navigate = useNavigate()
+  const t = useTranslations('error')
   return (
     <div className='h-svh'>
       <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
         <h1 className='text-[7rem] font-bold leading-tight'>401</h1>
-        <span className='font-medium'>
-          Oops! You don't have permission to access this page.
-        </span>
+        <span className='font-medium'>{t('unauthorised_title')}</span>
         <p className='text-center text-muted-foreground'>
-          It looks like you tried to access a resource that requires proper
-          authentication. <br />
-          Please log in with the appropriate credentials.
+          {t.rich('unauthorised_desc', { br: () => <br /> })}
         </p>
         <div className='mt-6 flex gap-4'>
           <Button variant='outline' onClick={() => navigate(-1)}>
-            Go Back
+            {t('go_back')}
           </Button>
-          <Button onClick={() => navigate('/')}>Back to Home</Button>
+          <Button onClick={() => navigate('/')}>{t('back_to_home')}</Button>
         </div>
       </div>
     </div>

+ 66 - 0
src/translations/en.json

@@ -0,0 +1,66 @@
+{
+  "sidebar": {
+    "dashboard": "Dashboard",
+    "tasks": "Tasks",
+    "chats": "Chats",
+    "apps": "Apps",
+    "authentication": "Authentication",
+    "auth_sign_in_email_password": "Sign In (email + password)",
+    "auth_sign_in_box": "Sign In (box)",
+    "auth_sign_up": "Sign Up",
+    "auth_forgot_password": "Forgot Password",
+    "auth_otp": "OTP",
+    "users": "Users",
+    "requests": "Requests",
+    "requests_trucks": "Trucks",
+    "requests_cargos": "Cargos",
+    "analytics": "Analytics",
+    "extra_components": "Extra Components",
+    "error_pages": "Error Pages",
+    "error_not_found": "Not Found",
+    "error_internal_server": "Internal Server Error",
+    "error_maintenance": "Maintenance Error",
+    "error_unauthorised": "Unauthorised Error",
+    "settings": "Settings"
+  },
+  "dashboard": {
+    "overview": "Overview",
+    "analytics": "Analytics",
+    "reports": "Reports",
+    "notifications": "Notifications",
+    "customers": "Customers",
+    "products": "Products",
+    "settings": "Settings",
+    "search": "Search...",
+    "dashboard": "Dashboard",
+    "download": "Download",
+    "total_revenue": "Total Revenue",
+    "from_last_month": "{amount} from last month",
+    "since_last_hour": "{amount} since last hour",
+    "subscriptions": "Subscriptions",
+    "sales": "Sales",
+    "active_now": "Active Now",
+    "recent_sales": "Recent Sales",
+    "recent_sales_desc": "You made {amount} sales this month."
+  },
+  "error": {
+    "404_title": "Oops! Page Not Found!",
+    "404_desc": "It seems like the page you're looking for <br></br> does not exist or might have been removed.",
+    "go_back": "Go Back",
+    "back_to_home": "Back to Home",
+    "500_title": "Oops! Something went wrong :')",
+    "500_desc": "We apologize for the inconvenience. <br></br>  Please try again later.",
+    "maintenance_title": "Website is under maintenance!",
+    "maintenance_desc": "The site is not available at the moment. <br></br> We'll be back online shortly.",
+    "learn_more": "Learn More",
+    "unauthorised_title": "Oops! You don't have permission to access this page.",
+    "unauthorised_desc": " It looks like you tried to access a resource that requires proper authentication. <br></br> Please log in with the appropriate credentials."
+  },
+  "userNav": {
+    "profile": "Profile",
+    "billing": "Billing",
+    "settings": "Settings",
+    "new_team": "New Team",
+    "log_out": "Log out"
+  }
+}

+ 4 - 0
src/translations/index.ts

@@ -0,0 +1,4 @@
+import * as en from './en.json'
+import * as zh from './zh.json'
+
+export default { en, zh }

+ 66 - 0
src/translations/zh.json

@@ -0,0 +1,66 @@
+{
+  "sidebar": {
+    "dashboard": "仪表板",
+    "tasks": "任务",
+    "chats": "聊天",
+    "apps": "应用",
+    "authentication": "验证",
+    "auth_sign_in_email_password": "登录(邮箱 + 密码)",
+    "auth_sign_in_box": "登录(盒子)",
+    "auth_sign_up": "注册",
+    "auth_forgot_password": "忘记密码",
+    "auth_otp": "一次性密码",
+    "users": "用户",
+    "requests": "请求",
+    "requests_trucks": "卡车",
+    "requests_cargos": "货物",
+    "analytics": "分析",
+    "extra_components": "额外组件",
+    "error_pages": "错误页面",
+    "error_not_found": "未找到",
+    "error_internal_server": "内部服务器错误",
+    "error_maintenance": "维护错误",
+    "error_unauthorised": "未授权错误",
+    "settings": "设置"
+  },
+  "dashboard": {
+    "overview": "概览",
+    "analytics": "分析",
+    "reports": "报告",
+    "notifications": "通知",
+    "customers": "客户",
+    "products": "产品",
+    "settings": "设置",
+    "search": "搜索...",
+    "dashboard": "仪表板",
+    "download": "下载",
+    "total_revenue": "总收入",
+    "from_last_month": "对比上个月 {amount}",
+    "since_last_hour": "过去一小时 {amount}",
+    "subscriptions": "订阅",
+    "sales": "销售",
+    "active_now": "现在活跃",
+    "recent_sales": "最近销售",
+    "recent_sales_desc": "你本月进行了 {amount} 笔销售。"
+  },
+  "error": {
+    "404_title": "哎呀!页面未找到!",
+    "404_desc": "看起来您要找的页面 <br></br> 不存在或可能已被删除。",
+    "go_back": "返回",
+    "back_to_home": "返回首页",
+    "500_title": "哎呀!出了点问题 :')",
+    "500_desc": "我们为给您带来的不便表示歉意。<br></br> 请稍后再试。",
+    "maintenance_title": "网站正在维护中!",
+    "maintenance_desc": "该网站目前不可用。<br></br> 我们很快就会恢复上线。",
+    "learn_more": "了解更多",
+    "unauthorised_title": "哎呀!您没有权限访问此页面。",
+    "unauthorised_desc": "看起来您尝试访问需要适当身份验证的资源。<br></br> 请使用适当的凭据登录。"
+  },
+  "userNav": {
+    "profile": "个人资料",
+    "billing": "账单",
+    "settings": "设置",
+    "new_team": "新团队",
+    "log_out": "登出"
+  }
+}