diff --git a/package.json b/package.json index 37025473dd..fb531bfc11 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,14 @@ "clsx": "^2.1.1", "cmdk": "1.1.1", "date-fns": "^4.1.0", + "i18next": "^26.2.0", "input-otp": "^1.4.2", "lucide-react": "^0.545.0", "react": "^19.2.0", "react-day-picker": "9.11.1", "react-dom": "^19.2.0", "react-hook-form": "^7.64.0", + "react-i18next": "^17.0.8", "react-top-loading-bar": "^3.0.2", "recharts": "^3.2.1", "sonner": "^2.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70a4d3991a..0ef6322696 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + i18next: + specifier: ^26.2.0 + version: 26.2.0(typescript@5.9.3) input-otp: specifier: ^1.4.2 version: 1.4.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -113,6 +116,9 @@ importers: react-hook-form: specifier: ^7.64.0 version: 7.64.0(react@19.2.0) + react-i18next: + specifier: ^17.0.8 + version: 17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3) react-top-loading-bar: specifier: ^3.0.2 version: 3.0.2(react@19.2.0) @@ -136,7 +142,7 @@ importers: version: 4.1.12 zustand: specifier: ^5.0.8 - version: 5.0.8(@types/react@19.2.2)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.5.0(react@19.2.0)) + version: 5.0.8(@types/react@19.2.2)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: '@eslint/js': specifier: ^9.37.0 @@ -334,6 +340,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -2257,6 +2267,17 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + i18next@26.2.0: + resolution: {integrity: sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2644,6 +2665,22 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@17.0.8: + resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + peerDependencies: + i18next: '>= 26.2.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 || ^6 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2922,6 +2959,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + victory-vendor@37.3.6: resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} @@ -2965,6 +3007,10 @@ packages: yaml: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + walk-up-path@4.0.0: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} @@ -3204,6 +3250,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.7': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -5089,6 +5137,14 @@ snapshots: dependencies: hermes-estree: 0.25.1 + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + i18next@26.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -5372,6 +5428,17 @@ snapshots: dependencies: react: 19.2.0 + react-i18next@17.0.8(i18next@26.2.0(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.7 + html-parse-stringify: 3.0.1 + i18next: 26.2.0(typescript@5.9.3) + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + react-dom: 19.2.0(react@19.2.0) + typescript: 5.9.3 + react-is@18.3.1: {} react-redux@9.2.0(@types/react@19.2.2)(react@19.2.0)(redux@5.0.1): @@ -5641,6 +5708,10 @@ snapshots: dependencies: react: 19.2.0 + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.1 @@ -5673,6 +5744,8 @@ snapshots: lightningcss: 1.30.1 tsx: 4.20.3 + void-elements@3.1.0: {} + walk-up-path@4.0.0: {} webpack-virtual-modules@0.6.2: {} @@ -5697,9 +5770,9 @@ snapshots: zod@4.1.12: {} - zustand@5.0.8(@types/react@19.2.2)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.5.0(react@19.2.0)): + zustand@5.0.8(@types/react@19.2.2)(immer@10.1.1)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): optionalDependencies: '@types/react': 19.2.2 immer: 10.1.1 react: 19.2.0 - use-sync-external-store: 1.5.0(react@19.2.0) + use-sync-external-store: 1.6.0(react@19.2.0) diff --git a/src/components/coming-soon.tsx b/src/components/coming-soon.tsx index 262c08bc98..8c33f53b25 100644 --- a/src/components/coming-soon.tsx +++ b/src/components/coming-soon.tsx @@ -1,14 +1,16 @@ import { Telescope } from 'lucide-react' +import { useTranslation } from 'react-i18next' export function ComingSoon() { + const { t } = useTranslation() return (
-

Coming Soon!

+

{t('comingSoon')}

- This page has not been created yet.
- Stay tuned though! + {t('comingSoonDesc')}
+ {t('comingSoonStayTuned')}

diff --git a/src/components/command-menu.tsx b/src/components/command-menu.tsx index 2d281c7263..f5540134ad 100644 --- a/src/components/command-menu.tsx +++ b/src/components/command-menu.tsx @@ -1,6 +1,7 @@ import React from 'react' import { useNavigate } from '@tanstack/react-router' import { ArrowRight, ChevronRight, Laptop, Moon, Sun } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { useSearch } from '@/context/search-provider' import { useTheme } from '@/context/theme-provider' import { @@ -12,13 +13,15 @@ import { CommandList, CommandSeparator, } from '@/components/ui/command' -import { sidebarData } from './layout/data/sidebar-data' +import { useSidebarData } from './layout/data/use-sidebar-data' import { ScrollArea } from './ui/scroll-area' export function CommandMenu() { const navigate = useNavigate() const { setTheme } = useTheme() const { open, setOpen } = useSearch() + const { t } = useTranslation() + const sidebarData = useSidebarData() const runCommand = React.useCallback( (command: () => unknown) => { @@ -30,10 +33,10 @@ export function CommandMenu() { return ( - + - No results found. + {t('noResults')} {sidebarData.navGroups.map((group) => ( {group.items.map((navItem, i) => { @@ -71,17 +74,17 @@ export function CommandMenu() { ))} - + runCommand(() => setTheme('light'))}> - Light + {t('light')} runCommand(() => setTheme('dark'))}> - Dark + {t('dark')} runCommand(() => setTheme('system'))}> - System + {t('system')} diff --git a/src/components/config-drawer.tsx b/src/components/config-drawer.tsx index 9da855fcf3..f92ca4c9e8 100644 --- a/src/components/config-drawer.tsx +++ b/src/components/config-drawer.tsx @@ -1,5 +1,6 @@ import { type SVGProps } from 'react' import { Root as Radio, Item } from '@radix-ui/react-radio-group' +import { useTranslation } from 'react-i18next' import { CircleCheck, RotateCcw, Settings } from 'lucide-react' import { IconDir } from '@/assets/custom/icon-dir' import { IconLayoutCompact } from '@/assets/custom/icon-layout-compact' @@ -28,6 +29,7 @@ import { import { useSidebar } from './ui/sidebar' export function ConfigDrawer() { + const { t } = useTranslation() const { setOpen } = useSidebar() const { resetDir } = useDirection() const { resetTheme } = useTheme() @@ -46,7 +48,7 @@ export function ConfigDrawer() { @@ -124,11 +126,12 @@ function RadioGroupItem({ } isTheme?: boolean }) { + const { t } = useTranslation() return (
setTheme(defaultTheme)} /> @@ -181,23 +185,23 @@ function ThemeConfig() { value={theme} onValueChange={setTheme} className='grid w-full max-w-md grid-cols-3 gap-4' - aria-label='Select theme preference' + aria-label={t('themeSection')} aria-describedby='theme-description' > {[ { value: 'system', - label: 'System', + label: t('system'), icon: IconThemeSystem, }, { value: 'light', - label: 'Light', + label: t('light'), icon: IconThemeLight, }, { value: 'dark', - label: 'Dark', + label: t('dark'), icon: IconThemeDark, }, ].map((item) => ( @@ -205,18 +209,19 @@ function ThemeConfig() { ))}
- Choose between system preference, light mode, or dark mode + {t('themeDescription')}
) } function SidebarConfig() { + const { t } = useTranslation() const { defaultVariant, variant, setVariant } = useLayout() return (
setVariant(defaultVariant)} /> @@ -224,23 +229,23 @@ function SidebarConfig() { value={variant} onValueChange={setVariant} className='grid w-full max-w-md grid-cols-3 gap-4' - aria-label='Select sidebar style' + aria-label={t('sidebarSection')} aria-describedby='sidebar-description' > {[ { value: 'inset', - label: 'Inset', + label: t('inset'), icon: IconSidebarInset, }, { value: 'floating', - label: 'Floating', + label: t('floating'), icon: IconSidebarFloating, }, { value: 'sidebar', - label: 'Sidebar', + label: t('sidebarSection'), icon: IconSidebarSidebar, }, ].map((item) => ( @@ -248,13 +253,14 @@ function SidebarConfig() { ))}
) } function LayoutConfig() { + const { t } = useTranslation() const { open, setOpen } = useSidebar() const { defaultCollapsible, collapsible, setCollapsible } = useLayout() @@ -263,7 +269,7 @@ function LayoutConfig() { return (
{ setOpen(true) @@ -281,23 +287,23 @@ function LayoutConfig() { setCollapsible(v as Collapsible) }} className='grid w-full max-w-md grid-cols-3 gap-4' - aria-label='Select layout style' + aria-label={t('layoutSection')} aria-describedby='layout-description' > {[ { value: 'default', - label: 'Default', + label: t('default'), icon: IconLayoutDefault, }, { value: 'icon', - label: 'Compact', + label: t('compact'), icon: IconLayoutCompact, }, { value: 'offcanvas', - label: 'Full layout', + label: t('fullLayout'), icon: IconLayoutFull, }, ].map((item) => ( @@ -305,18 +311,19 @@ function LayoutConfig() { ))}
- Choose between default expanded, compact icon-only, or full layout mode + {t('layoutDescription')}
) } function DirConfig() { + const { t } = useTranslation() const { defaultDir, dir, setDir } = useDirection() return (
setDir(defaultDir)} /> @@ -324,20 +331,20 @@ function DirConfig() { value={dir} onValueChange={setDir} className='grid w-full max-w-md grid-cols-3 gap-4' - aria-label='Select site direction' + aria-label={t('directionSection')} aria-describedby='direction-description' > {[ { value: 'ltr', - label: 'Left to Right', + label: t('ltr'), icon: (props: SVGProps) => ( ), }, { value: 'rtl', - label: 'Right to Left', + label: t('rtl'), icon: (props: SVGProps) => ( ), @@ -347,7 +354,7 @@ function DirConfig() { ))}
- Choose between left-to-right or right-to-left site direction + {t('directionDescription')}
) diff --git a/src/components/confirm-dialog.tsx b/src/components/confirm-dialog.tsx index 2db6b25529..1826764cb0 100644 --- a/src/components/confirm-dialog.tsx +++ b/src/components/confirm-dialog.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { AlertDialog, @@ -26,6 +27,7 @@ type ConfirmDialogProps = { } export function ConfirmDialog(props: ConfirmDialogProps) { + const { t } = useTranslation() const { title, desc, @@ -51,14 +53,14 @@ export function ConfirmDialog(props: ConfirmDialogProps) { {children} - {cancelBtnText ?? 'Cancel'} + {cancelBtnText ?? t('cancel')} diff --git a/src/components/language-switch.tsx b/src/components/language-switch.tsx new file mode 100644 index 0000000000..2667b2912d --- /dev/null +++ b/src/components/language-switch.tsx @@ -0,0 +1,43 @@ +import { Check, Languages } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { cn } from '@/lib/utils' +import { useLanguage } from '@/context/language-provider' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +export function LanguageSwitch() { + const { language, setLanguage } = useLanguage() + const { t } = useTranslation() + + return ( + + + + + + setLanguage('en')}> + {t('english')}{' '} + + + setLanguage('zh')}> + {t('chinese')}{' '} + + + + + ) +} diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index 32d944327f..e2bcff55e3 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -7,13 +7,14 @@ import { SidebarRail, } from '@/components/ui/sidebar' // import { AppTitle } from './app-title' -import { sidebarData } from './data/sidebar-data' +import { useSidebarData } from './data/use-sidebar-data' import { NavGroup } from './nav-group' import { NavUser } from './nav-user' import { TeamSwitcher } from './team-switcher' export function AppSidebar() { const { collapsible, variant } = useLayout() + const sidebarData = useSidebarData() return ( diff --git a/src/components/layout/data/use-sidebar-data.ts b/src/components/layout/data/use-sidebar-data.ts new file mode 100644 index 0000000000..7e81ed82a9 --- /dev/null +++ b/src/components/layout/data/use-sidebar-data.ts @@ -0,0 +1,210 @@ +import { + Construction, + LayoutDashboard, + Monitor, + Bug, + ListTodo, + FileX, + HelpCircle, + Lock, + Bell, + Package, + Palette, + ServerOff, + Settings, + Wrench, + UserCog, + UserX, + Users, + MessagesSquare, + ShieldCheck, + AudioWaveform, + Command, + GalleryVerticalEnd, +} from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { ClerkLogo } from '@/assets/clerk-logo' +import { type SidebarData } from '../types' + +export function useSidebarData(): SidebarData { + const { t } = useTranslation('sidebar') + + return { + user: { + name: 'satnaing', + email: 'satnaingdev@gmail.com', + avatar: '/avatars/shadcn.jpg', + }, + teams: [ + { + name: 'Shadcn Admin', + logo: Command, + plan: 'Vite + ShadcnUI', + }, + { + name: 'Acme Inc', + logo: GalleryVerticalEnd, + plan: 'Enterprise', + }, + { + name: 'Acme Corp.', + logo: AudioWaveform, + plan: 'Startup', + }, + ], + navGroups: [ + { + title: t('general'), + items: [ + { + title: t('dashboard'), + url: '/', + icon: LayoutDashboard, + }, + { + title: t('tasks'), + url: '/tasks', + icon: ListTodo, + }, + { + title: t('apps'), + url: '/apps', + icon: Package, + }, + { + title: t('chats'), + url: '/chats', + badge: '3', + icon: MessagesSquare, + }, + { + title: t('users'), + url: '/users', + icon: Users, + }, + { + title: t('securedByClerk'), + icon: ClerkLogo, + items: [ + { + title: t('signIn'), + url: '/clerk/sign-in', + }, + { + title: t('signUp'), + url: '/clerk/sign-up', + }, + { + title: t('userManagement'), + url: '/clerk/user-management', + }, + ], + }, + ], + }, + { + title: t('pages'), + items: [ + { + title: t('auth'), + icon: ShieldCheck, + items: [ + { + title: t('signIn'), + url: '/sign-in', + }, + { + title: t('signIn2Col'), + url: '/sign-in-2', + }, + { + title: t('signUp'), + url: '/sign-up', + }, + { + title: t('forgotPassword'), + url: '/forgot-password', + }, + { + title: t('otp'), + url: '/otp', + }, + ], + }, + { + title: t('errors'), + icon: Bug, + items: [ + { + title: t('unauthorized'), + url: '/errors/unauthorized', + icon: Lock, + }, + { + title: t('forbidden'), + url: '/errors/forbidden', + icon: UserX, + }, + { + title: t('notFound'), + url: '/errors/not-found', + icon: FileX, + }, + { + title: t('internalServerError'), + url: '/errors/internal-server-error', + icon: ServerOff, + }, + { + title: t('maintenanceError'), + url: '/errors/maintenance-error', + icon: Construction, + }, + ], + }, + ], + }, + { + title: t('other'), + items: [ + { + title: t('settings'), + icon: Settings, + items: [ + { + title: t('profile'), + url: '/settings', + icon: UserCog, + }, + { + title: t('account'), + url: '/settings/account', + icon: Wrench, + }, + { + title: t('appearance'), + url: '/settings/appearance', + icon: Palette, + }, + { + title: t('notifications'), + url: '/settings/notifications', + icon: Bell, + }, + { + title: t('display'), + url: '/settings/display', + icon: Monitor, + }, + ], + }, + { + title: t('helpCenter'), + url: '/help-center', + icon: HelpCircle, + }, + ], + }, + ], + } +} diff --git a/src/components/layout/nav-user.tsx b/src/components/layout/nav-user.tsx index daa75da381..0ce874bfc1 100644 --- a/src/components/layout/nav-user.tsx +++ b/src/components/layout/nav-user.tsx @@ -1,4 +1,5 @@ import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import { BadgeCheck, Bell, @@ -35,6 +36,7 @@ type NavUserProps = { } export function NavUser({ user }: NavUserProps) { + const { t } = useTranslation() const { isMobile } = useSidebar() const [open, setOpen] = useDialogState() @@ -81,7 +83,7 @@ export function NavUser({ user }: NavUserProps) { - Upgrade to Pro + {t('upgradeToPro')} @@ -89,19 +91,19 @@ export function NavUser({ user }: NavUserProps) { - Account + {t('account')} - Billing + {t('billing')} - Notifications + {t('notifications')} @@ -111,7 +113,7 @@ export function NavUser({ user }: NavUserProps) { onClick={() => setOpen(true)} > - Sign out + {t('signOut')} diff --git a/src/components/layout/team-switcher.tsx b/src/components/layout/team-switcher.tsx index 0c2322566a..0a8aeebabe 100644 --- a/src/components/layout/team-switcher.tsx +++ b/src/components/layout/team-switcher.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { useTranslation } from 'react-i18next' import { ChevronsUpDown, Plus } from 'lucide-react' import { DropdownMenu, @@ -25,6 +26,7 @@ type TeamSwitcherProps = { } export function TeamSwitcher({ teams }: TeamSwitcherProps) { + const { t } = useTranslation() const { isMobile } = useSidebar() const [activeTeam, setActiveTeam] = React.useState(teams[0]) @@ -56,7 +58,7 @@ export function TeamSwitcher({ teams }: TeamSwitcherProps) { sideOffset={4} > - Teams + {t('teams')} {teams.map((team, index) => ( -
Add team
+
{t('addTeam')}
diff --git a/src/components/profile-dropdown.tsx b/src/components/profile-dropdown.tsx index 2e819f3aff..80fd2592a8 100644 --- a/src/components/profile-dropdown.tsx +++ b/src/components/profile-dropdown.tsx @@ -1,4 +1,5 @@ import { Link } from '@tanstack/react-router' +import { useTranslation } from 'react-i18next' import useDialogState from '@/hooks/use-dialog-state' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' @@ -15,6 +16,7 @@ import { import { SignOutDialog } from '@/components/sign-out-dialog' export function ProfileDropdown() { + const { t } = useTranslation() const [open, setOpen] = useDialogState() return ( @@ -41,27 +43,27 @@ export function ProfileDropdown() { - Profile + {t('profile')} ⇧⌘P - Billing + {t('billing')} ⌘B - Settings + {t('settings')} ⌘S - New Team + {t('newTeam')} setOpen(true)}> - Sign out + {t('signOut')} ⇧⌘Q diff --git a/src/components/search.tsx b/src/components/search.tsx index 525f61e041..e9078a102f 100644 --- a/src/components/search.tsx +++ b/src/components/search.tsx @@ -1,4 +1,5 @@ import { SearchIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' import { cn } from '@/lib/utils' import { useSearch } from '@/context/search-provider' import { Button } from './ui/button' @@ -11,9 +12,11 @@ type SearchProps = { export function Search({ className = '', - placeholder = 'Search', + placeholder, }: SearchProps) { + const { t } = useTranslation() const { setOpen } = useSearch() + const displayPlaceholder = placeholder ?? t('search') return (
diff --git a/src/features/auth/forgot-password/components/forgot-password-form.tsx b/src/features/auth/forgot-password/components/forgot-password-form.tsx index 00f37ac3d0..a541c168b8 100644 --- a/src/features/auth/forgot-password/components/forgot-password-form.tsx +++ b/src/features/auth/forgot-password/components/forgot-password-form.tsx @@ -1,3 +1,5 @@ +import i18n from '@/i18n' +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' @@ -19,7 +21,7 @@ import { Input } from '@/components/ui/input' const formSchema = z.object({ email: z.email({ - error: (iss) => (iss.input === '' ? 'Please enter your email' : undefined), + error: (iss) => (iss.input === '' ? i18n.t('auth:emailRequired') : undefined), }), }) @@ -27,6 +29,7 @@ export function ForgotPasswordForm({ className, ...props }: React.HTMLAttributes) { + const { t } = useTranslation('auth') const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) @@ -41,14 +44,14 @@ export function ForgotPasswordForm({ console.log(data) toast.promise(sleep(2000), { - loading: 'Sending email...', + loading: t('sendingEmail'), success: () => { setIsLoading(false) form.reset() navigate({ to: '/otp' }) - return `Email sent to ${data.email}` + return t('emailSent', { email: data.email }) }, - error: 'Error', + error: t('error'), }) } @@ -64,16 +67,16 @@ export function ForgotPasswordForm({ name='email' render={({ field }) => ( - Email + {t('emailLabel')} - + )} /> diff --git a/src/features/auth/forgot-password/index.tsx b/src/features/auth/forgot-password/index.tsx index d72ad38138..e5080a898b 100644 --- a/src/features/auth/forgot-password/index.tsx +++ b/src/features/auth/forgot-password/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Link } from '@tanstack/react-router' import { Card, @@ -11,16 +12,16 @@ import { AuthLayout } from '../auth-layout' import { ForgotPasswordForm } from './components/forgot-password-form' export function ForgotPassword() { + const { t } = useTranslation('auth') return ( - Forgot Password + {t('forgotPasswordTitle')} - Enter your registered email and
we will send you a link to - reset your password. + {t('forgotPasswordDesc1')}
{t('forgotPasswordDesc2')}
@@ -28,12 +29,12 @@ export function ForgotPassword() {

- Don't have an account?{' '} + {t('dontHaveAccount')}{' '} - Sign up + {t('signUpLink')} .

diff --git a/src/features/auth/otp/components/otp-form.tsx b/src/features/auth/otp/components/otp-form.tsx index 7c8ce9867c..7282639b62 100644 --- a/src/features/auth/otp/components/otp-form.tsx +++ b/src/features/auth/otp/components/otp-form.tsx @@ -1,3 +1,5 @@ +import i18n from '@/i18n' +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' @@ -24,13 +26,14 @@ import { const formSchema = z.object({ otp: z .string() - .min(6, 'Please enter the 6-digit code.') - .max(6, 'Please enter the 6-digit code.'), + .min(6, i18n.t('auth:otpRequired')) + .max(6, i18n.t('auth:otpRequired')), }) type OtpFormProps = React.HTMLAttributes export function OtpForm({ className, ...props }: OtpFormProps) { + const { t } = useTranslation('auth') const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) @@ -63,7 +66,7 @@ export function OtpForm({ className, ...props }: OtpFormProps) { name='otp' render={({ field }) => ( - One-Time Password + {t('otpLabel')} diff --git a/src/features/auth/otp/index.tsx b/src/features/auth/otp/index.tsx index df5754bfc0..1a4965be94 100644 --- a/src/features/auth/otp/index.tsx +++ b/src/features/auth/otp/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Link } from '@tanstack/react-router' import { Card, @@ -11,16 +12,16 @@ import { AuthLayout } from '../auth-layout' import { OtpForm } from './components/otp-form' export function Otp() { + const { t } = useTranslation('auth') return ( - Two-factor Authentication + {t('otpTitle')} - Please enter the authentication code.
We have sent the - authentication code to your email. + {t('otpDesc1')}
{t('otpDesc2')}
@@ -28,12 +29,12 @@ export function Otp() {

- Haven't received it?{' '} + {t('havenReceived')}{' '} - Resend a new code. + {t('resendCode')} .

diff --git a/src/features/auth/sign-in/components/user-auth-form.tsx b/src/features/auth/sign-in/components/user-auth-form.tsx index 0c34069116..e4755f6fec 100644 --- a/src/features/auth/sign-in/components/user-auth-form.tsx +++ b/src/features/auth/sign-in/components/user-auth-form.tsx @@ -1,3 +1,5 @@ +import i18n from '@/i18n' +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' @@ -22,12 +24,12 @@ import { PasswordInput } from '@/components/password-input' const formSchema = z.object({ email: z.email({ - error: (iss) => (iss.input === '' ? 'Please enter your email' : undefined), + error: (iss) => (iss.input === '' ? i18n.t('auth:emailRequired') : undefined), }), password: z .string() - .min(1, 'Please enter your password') - .min(7, 'Password must be at least 7 characters long'), + .min(1, i18n.t('auth:passwordRequired')) + .min(7, i18n.t('auth:passwordMinLength')), }) interface UserAuthFormProps extends React.HTMLAttributes { @@ -39,6 +41,7 @@ export function UserAuthForm({ redirectTo, ...props }: UserAuthFormProps) { + const { t } = useTranslation('auth') const [isLoading, setIsLoading] = useState(false) const navigate = useNavigate() const { auth } = useAuthStore() @@ -55,7 +58,7 @@ export function UserAuthForm({ setIsLoading(true) toast.promise(sleep(2000), { - loading: 'Signing in...', + loading: t('signingIn'), success: () => { setIsLoading(false) @@ -75,9 +78,9 @@ export function UserAuthForm({ const targetPath = redirectTo || '/' navigate({ to: targetPath, replace: true }) - return `Welcome back, ${data.email}!` + return t('welcomeBack', { email: data.email }) }, - error: 'Error', + error: t('error'), }) } @@ -93,9 +96,9 @@ export function UserAuthForm({ name='email' render={({ field }) => ( - Email + {t('emailLabel')} - + @@ -106,23 +109,23 @@ export function UserAuthForm({ name='password' render={({ field }) => ( - Password + {t('passwordLabel')} - + - Forgot password? + {t('forgotPasswordLink')} )} />
@@ -131,7 +134,7 @@ export function UserAuthForm({
- Or continue with + {t('orContinueWith')}
diff --git a/src/features/auth/sign-in/index.tsx b/src/features/auth/sign-in/index.tsx index 099d9de122..e8409fbbc4 100644 --- a/src/features/auth/sign-in/index.tsx +++ b/src/features/auth/sign-in/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { useSearch } from '@tanstack/react-router' import { Card, @@ -11,16 +12,17 @@ import { AuthLayout } from '../auth-layout' import { UserAuthForm } from './components/user-auth-form' export function SignIn() { + const { t } = useTranslation('auth') const { redirect } = useSearch({ from: '/(auth)/sign-in' }) return ( - Sign in + {t('signInTitle')} - Enter your email and password below to
- log into your account + {t('signInDesc1')}
+ {t('signInDesc2')}
@@ -28,19 +30,19 @@ export function SignIn() {

- By clicking sign in, you agree to our{' '} + {t('signInAgreement')}{' '} - Terms of Service + {t('termsOfService')} {' '} - and{' '} + {t('and')}{' '} - Privacy Policy + {t('privacyPolicy')} .

diff --git a/src/features/auth/sign-in/sign-in-2.tsx b/src/features/auth/sign-in/sign-in-2.tsx index 839d975b2d..b816f0579f 100644 --- a/src/features/auth/sign-in/sign-in-2.tsx +++ b/src/features/auth/sign-in/sign-in-2.tsx @@ -1,10 +1,15 @@ +import { useTranslation } from 'react-i18next' import { Logo } from '@/assets/logo' +import { LanguageSwitch } from '@/components/language-switch' +import { ThemeSwitch } from '@/components/theme-switch' import { cn } from '@/lib/utils' import dashboardDark from './assets/dashboard-dark.png' import dashboardLight from './assets/dashboard-light.png' import { UserAuthForm } from './components/user-auth-form' export function SignIn2() { + const { t } = useTranslation('auth') + return (
@@ -12,31 +17,35 @@ export function SignIn2() {

Shadcn Admin

+
+ + +
-

Sign in

+

{t('signInTitle')}

- Enter your email and password below
- to log into your account + {t('signInDesc1')}
+ {t('signInDesc2')}

- By clicking sign in, you agree to our{' '} + {t('signInAgreement')}{' '} - Terms of Service + {t('termsOfService')} {' '} - and{' '} + {t('and')}{' '} - Privacy Policy + {t('privacyPolicy')} .

diff --git a/src/features/auth/sign-up/components/sign-up-form.tsx b/src/features/auth/sign-up/components/sign-up-form.tsx index b5ffd20484..7700c9b006 100644 --- a/src/features/auth/sign-up/components/sign-up-form.tsx +++ b/src/features/auth/sign-up/components/sign-up-form.tsx @@ -1,3 +1,5 @@ +import i18n from '@/i18n' +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' @@ -20,16 +22,16 @@ const formSchema = z .object({ email: z.email({ error: (iss) => - iss.input === '' ? 'Please enter your email' : undefined, + iss.input === '' ? i18n.t('auth:emailRequired') : undefined, }), password: z .string() - .min(1, 'Please enter your password') - .min(7, 'Password must be at least 7 characters long'), - confirmPassword: z.string().min(1, 'Please confirm your password'), + .min(1, i18n.t('auth:passwordRequired')) + .min(7, i18n.t('auth:passwordMinLength')), + confirmPassword: z.string().min(1, i18n.t('auth:confirmPasswordRequired')), }) .refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match.", + message: i18n.t('auth:passwordsDontMatch'), path: ['confirmPassword'], }) @@ -37,6 +39,7 @@ export function SignUpForm({ className, ...props }: React.HTMLAttributes) { + const { t } = useTranslation('auth') const [isLoading, setIsLoading] = useState(false) const form = useForm>({ @@ -70,9 +73,9 @@ export function SignUpForm({ name='email' render={({ field }) => ( - Email + {t('emailLabel')} - + @@ -83,9 +86,9 @@ export function SignUpForm({ name='password' render={({ field }) => ( - Password + {t('passwordLabel')} - + @@ -96,16 +99,16 @@ export function SignUpForm({ name='confirmPassword' render={({ field }) => ( - Confirm Password + {t('confirmPasswordLabel')} - + )} />
@@ -114,7 +117,7 @@ export function SignUpForm({
- Or continue with + {t('orContinueWith')}
diff --git a/src/features/auth/sign-up/index.tsx b/src/features/auth/sign-up/index.tsx index 3ae6983197..318d8003ed 100644 --- a/src/features/auth/sign-up/index.tsx +++ b/src/features/auth/sign-up/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Link } from '@tanstack/react-router' import { Card, @@ -11,21 +12,22 @@ import { AuthLayout } from '../auth-layout' import { SignUpForm } from './components/sign-up-form' export function SignUp() { + const { t } = useTranslation('auth') return ( - Create an account + {t('createAccountTitle')} - Enter your email and password to create an account.
- Already have an account?{' '} + {t('createAccountDesc')}
+ {t('alreadyHaveAccount')}{' '} - Sign In + {t('signInLink')}
@@ -34,19 +36,19 @@ export function SignUp() {

- By creating an account, you agree to our{' '} + {t('createAccountAgreement')}{' '} - Terms of Service + {t('termsOfService')} {' '} - and{' '} + {t('and')}{' '} - Privacy Policy + {t('privacyPolicy')} .

diff --git a/src/features/chats/components/new-chat.tsx b/src/features/chats/components/new-chat.tsx index 0e8a71af57..1fa3853a50 100644 --- a/src/features/chats/components/new-chat.tsx +++ b/src/features/chats/components/new-chat.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { Check, X } from 'lucide-react' import { showSubmittedData } from '@/lib/show-submitted-data' @@ -27,6 +28,7 @@ type NewChatProps = { onOpenChange: (open: boolean) => void } export function NewChat({ users, onOpenChange, open }: NewChatProps) { + const { t } = useTranslation('chats') const [selectedUsers, setSelectedUsers] = useState([]) const handleSelectUser = (user: User) => { @@ -53,11 +55,11 @@ export function NewChat({ users, onOpenChange, open }: NewChatProps) { - New message + {t('newMessage')}
- To: + {t('to')} {selectedUsers.map((user) => ( {user.fullName} @@ -77,11 +79,11 @@ export function NewChat({ users, onOpenChange, open }: NewChatProps) {
- No people found. + {t('noPeopleFound')} {users.map((user) => ( showSubmittedData(selectedUsers)} disabled={selectedUsers.length === 0} > - Chat + {t('chatButton')}
diff --git a/src/features/chats/index.tsx b/src/features/chats/index.tsx index f5225305ff..5e9d36209b 100644 --- a/src/features/chats/index.tsx +++ b/src/features/chats/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { useState } from 'react' import { Fragment } from 'react/jsx-runtime' import { format } from 'date-fns' @@ -24,6 +25,7 @@ import { Header } from '@/components/layout/header' import { Main } from '@/components/layout/main' import { ProfileDropdown } from '@/components/profile-dropdown' import { Search } from '@/components/search' +import { LanguageSwitch } from '@/components/language-switch' import { ThemeSwitch } from '@/components/theme-switch' import { NewChat } from './components/new-chat' import { type ChatUser, type Convo } from './data/chat-types' @@ -31,6 +33,7 @@ import { type ChatUser, type Convo } from './data/chat-types' import { conversations } from './data/convo.json' export function Chats() { + const { t } = useTranslation('chats') const [search, setSearch] = useState('') const [selectedUser, setSelectedUser] = useState(null) const [mobileSelectedUser, setMobileSelectedUser] = useState( @@ -70,6 +73,7 @@ export function Chats() {
+
@@ -82,7 +86,7 @@ export function Chats() {
-

Inbox

+

{t('title')}

@@ -103,11 +107,11 @@ export function Chats() { )} > - Search + {t('searchLabel')} setSearch(e.target.value)} /> @@ -120,7 +124,7 @@ export function Chats() { const lastConvo = messages[0] const lastMsg = lastConvo.sender === 'You' - ? `You: ${lastConvo.message}` + ? `${t('youPrefix')}${lastConvo.message}` : lastConvo.message return ( @@ -294,10 +298,10 @@ export function Chats() {
@@ -310,7 +314,7 @@ export function Chats() {
@@ -326,13 +330,13 @@ export function Chats() {
-

Your messages

+

{t('emptyTitle')}

- Send a message to start a chat. + {t('emptyDesc')}

diff --git a/src/features/dashboard/components/analytics.tsx b/src/features/dashboard/components/analytics.tsx index 87a680a64b..b7076fabc7 100644 --- a/src/features/dashboard/components/analytics.tsx +++ b/src/features/dashboard/components/analytics.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Card, CardContent, @@ -8,12 +9,14 @@ import { import { AnalyticsChart } from './analytics-chart' export function Analytics() { + const { t } = useTranslation('dashboard') + return (
- Traffic Overview - Weekly clicks and unique visitors + {t('trafficOverview')} + {t('trafficOverviewDesc')} @@ -22,7 +25,7 @@ export function Analytics() {
- Total Clicks + {t('totalClicks')}
1,248
-

+12.4% vs last week

+

{t('totalClicksDelta')}

- Unique Visitors + {t('uniqueVisitors')}
832
-

+5.8% vs last week

+

{t('uniqueVisitorsDelta')}

- Bounce Rate + {t('bounceRate')}
42%
-

-3.2% vs last week

+

{t('bounceRateDelta')}

- Avg. Session + {t('avgSession')}
3m 24s
-

+18s vs last week

+

{t('avgSessionDelta')}

- Referrers - Top sources driving traffic + {t('referrers')} + {t('referrersDesc')} - Devices - How users access your app + {t('devices')} + {t('devicesDesc')} `${n}%`} diff --git a/src/features/dashboard/components/overview.tsx b/src/features/dashboard/components/overview.tsx index befcd3b789..4b45cacae3 100644 --- a/src/features/dashboard/components/overview.tsx +++ b/src/features/dashboard/components/overview.tsx @@ -1,57 +1,19 @@ +import { useTranslation } from 'react-i18next' import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts' -const data = [ - { - name: 'Jan', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Feb', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Mar', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Apr', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'May', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Jun', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Jul', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Aug', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Sep', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Oct', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Nov', - total: Math.floor(Math.random() * 5000) + 1000, - }, - { - name: 'Dec', - total: Math.floor(Math.random() * 5000) + 1000, - }, -] +const months = [ + 'jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', +] as const export function Overview() { + const { t } = useTranslation('dashboard') + + const data = months.map((m) => ({ + name: t(m), + total: Math.floor(Math.random() * 5000) + 1000, + })) + return ( diff --git a/src/features/dashboard/index.tsx b/src/features/dashboard/index.tsx index 760569f2d9..c3fea50b29 100644 --- a/src/features/dashboard/index.tsx +++ b/src/features/dashboard/index.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Card, @@ -13,12 +14,42 @@ import { Main } from '@/components/layout/main' import { TopNav } from '@/components/layout/top-nav' import { ProfileDropdown } from '@/components/profile-dropdown' import { Search } from '@/components/search' +import { LanguageSwitch } from '@/components/language-switch' import { ThemeSwitch } from '@/components/theme-switch' import { Analytics } from './components/analytics' import { Overview } from './components/overview' import { RecentSales } from './components/recent-sales' export function Dashboard() { + const { t } = useTranslation('dashboard') + + const topNav = [ + { + title: t('topNavOverview'), + href: 'dashboard/overview', + isActive: true, + disabled: false, + }, + { + title: t('topNavCustomers'), + href: 'dashboard/customers', + isActive: false, + disabled: true, + }, + { + title: t('topNavProducts'), + href: 'dashboard/products', + isActive: false, + disabled: true, + }, + { + title: t('topNavSettings'), + href: 'dashboard/settings', + isActive: false, + disabled: true, + }, + ] + return ( <> {/* ===== Top Heading ===== */} @@ -27,6 +58,7 @@ export function Dashboard() {
+
@@ -35,9 +67,9 @@ export function Dashboard() { {/* ===== Main ===== */}
-

Dashboard

+

{t('title')}

- +
- Overview - Analytics + {t('overview')} + {t('analytics')} - Reports + {t('reports')} - Notifications + {t('notifications')}
@@ -62,7 +94,7 @@ export function Dashboard() { - Total Revenue + {t('totalRevenue')}
$45,231.89

- +20.1% from last month + {t('totalRevenueDesc')}

- Subscriptions + {t('subscriptions')}
+2350

- +180.1% from last month + {t('subscriptionsDesc')}

- Sales + {t('sales')}
+12,234

- +19% from last month + {t('salesDesc')}

- Active Now + {t('activeNow')}
+573

- +201 since last hour + {t('activeNowDesc')}

@@ -164,7 +196,7 @@ export function Dashboard() {
- Overview + {t('overview')} @@ -172,9 +204,9 @@ export function Dashboard() { - Recent Sales + {t('recentSales')} - You made 265 sales this month. + {t('recentSalesDesc')} @@ -191,30 +223,3 @@ export function Dashboard() { ) } - -const topNav = [ - { - title: 'Overview', - href: 'dashboard/overview', - isActive: true, - disabled: false, - }, - { - title: 'Customers', - href: 'dashboard/customers', - isActive: false, - disabled: true, - }, - { - title: 'Products', - href: 'dashboard/products', - isActive: false, - disabled: true, - }, - { - title: 'Settings', - href: 'dashboard/settings', - isActive: false, - disabled: true, - }, -] diff --git a/src/features/errors/forbidden.tsx b/src/features/errors/forbidden.tsx index 106550c106..026c4e612d 100644 --- a/src/features/errors/forbidden.tsx +++ b/src/features/errors/forbidden.tsx @@ -1,23 +1,26 @@ +import { useTranslation } from 'react-i18next' import { useNavigate, useRouter } from '@tanstack/react-router' import { Button } from '@/components/ui/button' export function ForbiddenError() { + const { t } = useTranslation('errors') + const { t: tc } = useTranslation('common') const navigate = useNavigate() const { history } = useRouter() return (

403

- Access Forbidden + {t('forbiddenTitle')}

- You don't have necessary permission
- to view this resource. + {t('forbiddenDesc1')}
+ {t('forbiddenDesc2')}

- +
diff --git a/src/features/errors/general-error.tsx b/src/features/errors/general-error.tsx index db98cae613..c211bfbaec 100644 --- a/src/features/errors/general-error.tsx +++ b/src/features/errors/general-error.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { useNavigate, useRouter } from '@tanstack/react-router' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' @@ -10,6 +11,8 @@ export function GeneralError({ className, minimal = false, }: GeneralErrorProps) { + const { t } = useTranslation('errors') + const { t: tc } = useTranslation('common') const navigate = useNavigate() const { history } = useRouter() return ( @@ -18,16 +21,16 @@ export function GeneralError({ {!minimal && (

500

)} - Oops! Something went wrong {`:')`} + {t('serverErrorTitle')}

- We apologize for the inconvenience.
Please try again later. + {t('serverErrorDesc1')}
{t('serverErrorDesc2')}

{!minimal && (
- +
)}
diff --git a/src/features/errors/maintenance-error.tsx b/src/features/errors/maintenance-error.tsx index f53fae3034..390e014917 100644 --- a/src/features/errors/maintenance-error.tsx +++ b/src/features/errors/maintenance-error.tsx @@ -1,17 +1,20 @@ +import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' export function MaintenanceError() { + const { t } = useTranslation('errors') + const { t: tc } = useTranslation('common') return (

503

- Website is under maintenance! + {t('maintenanceTitle')}

- The site is not available at the moment.
- We'll be back online shortly. + {t('maintenanceDesc1')}
+ {t('maintenanceDesc2')}

- +
diff --git a/src/features/errors/not-found-error.tsx b/src/features/errors/not-found-error.tsx index 42a8f8be5a..cc517b3cb2 100644 --- a/src/features/errors/not-found-error.tsx +++ b/src/features/errors/not-found-error.tsx @@ -1,23 +1,26 @@ +import { useTranslation } from 'react-i18next' import { useNavigate, useRouter } from '@tanstack/react-router' import { Button } from '@/components/ui/button' export function NotFoundError() { + const { t } = useTranslation('errors') + const { t: tc } = useTranslation('common') const navigate = useNavigate() const { history } = useRouter() return (

404

- Oops! Page Not Found! + {t('notFoundTitle')}

- It seems like the page you're looking for
- does not exist or might have been removed. + {t('notFoundDesc1')}
+ {t('notFoundDesc2')}

- +
diff --git a/src/features/errors/unauthorized-error.tsx b/src/features/errors/unauthorized-error.tsx index bd29d12547..5e51a73437 100644 --- a/src/features/errors/unauthorized-error.tsx +++ b/src/features/errors/unauthorized-error.tsx @@ -1,23 +1,25 @@ +import { useTranslation } from 'react-i18next' import { useNavigate, useRouter } from '@tanstack/react-router' import { Button } from '@/components/ui/button' export function UnauthorisedError() { + const { t } = useTranslation('errors') + const { t: tc } = useTranslation('common') const navigate = useNavigate() const { history } = useRouter() return (

401

- Unauthorized Access + {t('unauthorizedTitle')}

- Please log in with the appropriate credentials
to access this - resource. + {t('unauthorizedDesc1')}
{t('unauthorizedDesc2')}

- +
diff --git a/src/features/settings/account/account-form.tsx b/src/features/settings/account/account-form.tsx index fcbc803610..4f0e79dc64 100644 --- a/src/features/settings/account/account-form.tsx +++ b/src/features/settings/account/account-form.tsx @@ -1,3 +1,5 @@ +import i18n from '@/i18n' +import { useTranslation } from 'react-i18next' import { z } from 'zod' import { useForm } from 'react-hook-form' import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons' @@ -44,12 +46,11 @@ const languages = [ const accountFormSchema = z.object({ name: z - .string() - .min(1, 'Please enter your name.') - .min(2, 'Name must be at least 2 characters.') - .max(30, 'Name must not be longer than 30 characters.'), - dob: z.date('Please select your date of birth.'), - language: z.string('Please select a language.'), + .string(i18n.t('settings:nameRequired')) + .min(2, i18n.t('settings:nameMinLength')) + .max(30, i18n.t('settings:nameMaxLength')), + dob: z.date(i18n.t('settings:dobRequired')), + language: z.string(i18n.t('settings:languageRequired')), }) type AccountFormValues = z.infer @@ -60,6 +61,7 @@ const defaultValues: Partial = { } export function AccountForm() { + const { t } = useTranslation('settings') const form = useForm({ resolver: zodResolver(accountFormSchema), defaultValues, @@ -77,13 +79,12 @@ export function AccountForm() { name='name' render={({ field }) => ( - Name + {t('name')} - + - This is the name that will be displayed on your profile and in - emails. + {t('nameDesc')} @@ -94,10 +95,10 @@ export function AccountForm() { name='dob' render={({ field }) => ( - Date of birth + {t('dateOfBirth')} - Your date of birth is used to calculate your age. + {t('dobDesc')} @@ -108,7 +109,7 @@ export function AccountForm() { name='language' render={({ field }) => ( - Language + {t('language')} @@ -124,15 +125,15 @@ export function AccountForm() { ? languages.find( (language) => language.value === field.value )?.label - : 'Select language'} + : t('languagePlaceholder')} - - No language found. + + {t('languageRequired')} {languages.map((language) => ( diff --git a/src/features/settings/appearance/appearance-form.tsx b/src/features/settings/appearance/appearance-form.tsx index 550c446f7c..24fab5f2a4 100644 --- a/src/features/settings/appearance/appearance-form.tsx +++ b/src/features/settings/appearance/appearance-form.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import { z } from 'zod' import { useForm } from 'react-hook-form' import { ChevronDownIcon } from '@radix-ui/react-icons' @@ -27,6 +28,7 @@ const appearanceFormSchema = z.object({ type AppearanceFormValues = z.infer export function AppearanceForm() { + const { t } = useTranslation('settings') const { font, setFont } = useFont() const { theme, setTheme } = useTheme() @@ -56,7 +58,7 @@ export function AppearanceForm() { name='font' render={({ field }) => ( - Font + {t('font')}
+ - This is your public display name. It can be your real name or a - pseudonym. You can only change this once every 30 days. + {t('usernameDesc')} @@ -96,11 +101,11 @@ export function ProfileForm() { name='email' render={({ field }) => ( - Email + {t('email')} - You can manage verified email addresses in your{' '} - email settings. + {t('emailSettingsDesc')}{' '} + {t('emailSettingsLink')}. @@ -122,17 +127,16 @@ export function ProfileForm() { name='bio' render={({ field }) => ( - Bio + {t('bio')}