Skip to content

Commit c5d163d

Browse files
WenLonG12345Teo Wen Longsatnaing
authored
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 <[email protected]> Co-authored-by: satnaing <[email protected]>
1 parent ee55c19 commit c5d163d

File tree

18 files changed

+410
-76
lines changed

18 files changed

+410
-76
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"recharts": "^2.12.5",
4545
"tailwind-merge": "^2.2.2",
4646
"tailwindcss-animate": "^1.0.7",
47+
"use-intl": "^3.20.0",
4748
"zod": "^3.22.4"
4849
},
4950
"devDependencies": {

pnpm-lock.yaml

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { createContext, useContext, useState } from 'react'
2+
import { IntlProvider } from 'use-intl'
3+
import translations from '../translations'
4+
5+
export type Language = 'en' | 'zh'
6+
7+
type LanguageProviderProps = {
8+
children: React.ReactNode
9+
defaultLanguage?: Language
10+
storageKey?: string
11+
}
12+
13+
type LanguageProviderState = {
14+
language: Language
15+
setLanguage: (lang: Language) => void
16+
}
17+
18+
const initialState: LanguageProviderState = {
19+
language: 'en',
20+
setLanguage: () => null,
21+
}
22+
23+
const LanguageProviderContext =
24+
createContext<LanguageProviderState>(initialState)
25+
26+
export function LanguageProvider({
27+
children,
28+
defaultLanguage = 'en',
29+
storageKey = 'vite-ui-language',
30+
...props
31+
}: LanguageProviderProps) {
32+
const [language, setLanguage] = useState<Language>(
33+
() => (localStorage.getItem(storageKey) as Language) || defaultLanguage
34+
)
35+
36+
const value = {
37+
language,
38+
setLanguage: (lang: Language) => {
39+
localStorage.setItem(storageKey, lang)
40+
setLanguage(lang)
41+
},
42+
}
43+
44+
return (
45+
<LanguageProviderContext.Provider {...props} value={value}>
46+
<IntlProvider locale={language} messages={translations[language]}>
47+
{children}
48+
</IntlProvider>
49+
</LanguageProviderContext.Provider>
50+
)
51+
}
52+
53+
// eslint-disable-next-line react-refresh/only-export-components
54+
export const useLanguage = () => {
55+
const context = useContext(LanguageProviderContext)
56+
57+
if (context === undefined)
58+
throw new Error('useLanguage must be used within a LanguageProvider')
59+
60+
return context
61+
}

src/components/language-switch.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Language, useLanguage } from './language-provider'
2+
import { IconCheck } from '@tabler/icons-react'
3+
import { cn } from '@/lib/utils'
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuTrigger,
9+
} from '@/components/ui/dropdown-menu'
10+
import { Button } from './custom/button'
11+
12+
export default function LanguageSwitch() {
13+
const { language, setLanguage } = useLanguage()
14+
15+
const languageText = new Map<string, string>([
16+
['en', 'English'],
17+
['zh', '简体中文'],
18+
])
19+
20+
const renderDropdownItem = () => {
21+
return Array.from(languageText).map(([key, value]) => (
22+
<DropdownMenuItem key={key} onClick={() => setLanguage(key as Language)}>
23+
{value}{' '}
24+
<IconCheck
25+
size={14}
26+
className={cn('ml-auto', language !== key && 'hidden')}
27+
/>
28+
</DropdownMenuItem>
29+
))
30+
}
31+
32+
return (
33+
<DropdownMenu>
34+
<DropdownMenuTrigger asChild>
35+
<Button
36+
variant='outline'
37+
size='default'
38+
className='scale-95 rounded-full'
39+
>
40+
{languageText.get(language)}
41+
<span className='sr-only'>Toggle language</span>
42+
</Button>
43+
</DropdownMenuTrigger>
44+
<DropdownMenuContent align='end'>
45+
{renderDropdownItem()}
46+
</DropdownMenuContent>
47+
</DropdownMenu>
48+
)
49+
}

src/components/nav.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { cn } from '@/lib/utils'
2424
import useCheckActiveNav from '@/hooks/use-check-active-nav'
2525
import { SideLink } from '@/data/sidelinks'
26+
import { useTranslations } from 'use-intl'
2627

2728
interface NavProps extends React.HTMLAttributes<HTMLDivElement> {
2829
isCollapsed: boolean
@@ -89,6 +90,7 @@ function NavLink({
8990
subLink = false,
9091
}: NavLinkProps) {
9192
const { checkActiveNav } = useCheckActiveNav()
93+
const t = useTranslations()
9294
return (
9395
<Link
9496
to={href}
@@ -104,7 +106,7 @@ function NavLink({
104106
aria-current={checkActiveNav(href) ? 'page' : undefined}
105107
>
106108
<div className='mr-2'>{icon}</div>
107-
{title}
109+
{t(title)}
108110
{label && (
109111
<div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
110112
{label}
@@ -116,6 +118,7 @@ function NavLink({
116118

117119
function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
118120
const { checkActiveNav } = useCheckActiveNav()
121+
const t = useTranslations()
119122

120123
/* Open collapsible by default
121124
* if one of child element is active */
@@ -130,7 +133,7 @@ function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
130133
)}
131134
>
132135
<div className='mr-2'>{icon}</div>
133-
{title}
136+
{t(title)}
134137
{label && (
135138
<div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
136139
{label}

src/components/search.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Input } from '@/components/ui/input'
2+
import { useTranslations } from 'use-intl'
23

34
export function Search() {
5+
const t = useTranslations('dashboard')
46
return (
57
<div>
68
<Input
79
type='search'
8-
placeholder='Search...'
10+
placeholder={t('search')}
911
className='md:w-[100px] lg:w-[300px]'
1012
/>
1113
</div>

src/components/top-nav.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@/components/ui/dropdown-menu'
99
import { Button } from './custom/button'
1010
import { IconMenu } from '@tabler/icons-react'
11+
import { useTranslations } from 'use-intl'
1112

1213
interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
1314
links: {
@@ -18,6 +19,7 @@ interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
1819
}
1920

2021
export function TopNav({ className, links, ...props }: TopNavProps) {
22+
const t = useTranslations()
2123
return (
2224
<>
2325
<div className='md:hidden'>
@@ -34,7 +36,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
3436
to={href}
3537
className={!isActive ? 'text-muted-foreground' : ''}
3638
>
37-
{title}
39+
{t(title)}
3840
</Link>
3941
</DropdownMenuItem>
4042
))}
@@ -55,7 +57,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
5557
to={href}
5658
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
5759
>
58-
{title}
60+
{t(title)}
5961
</Link>
6062
))}
6163
</nav>

src/components/user-nav.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
DropdownMenuShortcut,
1111
DropdownMenuTrigger,
1212
} from '@/components/ui/dropdown-menu'
13+
import { useTranslations } from 'use-intl'
1314

1415
export function UserNav() {
16+
const t = useTranslations('userNav')
1517
return (
1618
<DropdownMenu>
1719
<DropdownMenuTrigger asChild>
@@ -34,22 +36,22 @@ export function UserNav() {
3436
<DropdownMenuSeparator />
3537
<DropdownMenuGroup>
3638
<DropdownMenuItem>
37-
Profile
39+
{t('profile')}
3840
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
3941
</DropdownMenuItem>
4042
<DropdownMenuItem>
41-
Billing
43+
{t('billing')}
4244
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
4345
</DropdownMenuItem>
4446
<DropdownMenuItem>
45-
Settings
47+
{t('settings')}
4648
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
4749
</DropdownMenuItem>
48-
<DropdownMenuItem>New Team</DropdownMenuItem>
50+
<DropdownMenuItem>{t('new_team')}</DropdownMenuItem>
4951
</DropdownMenuGroup>
5052
<DropdownMenuSeparator />
5153
<DropdownMenuItem>
52-
Log out
54+
{t('log_out')}
5355
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
5456
</DropdownMenuItem>
5557
</DropdownMenuContent>

0 commit comments

Comments
 (0)