Skip to content

Commit

Permalink
feat: add multiple language support (#37)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Nov 11, 2024
1 parent ee55c19 commit c5d163d
Show file tree
Hide file tree
Showing 18 changed files with 410 additions and 76 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
63 changes: 63 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions src/components/language-provider.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions src/components/language-switch.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
7 changes: 5 additions & 2 deletions src/components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,6 +90,7 @@ function NavLink({
subLink = false,
}: NavLinkProps) {
const { checkActiveNav } = useCheckActiveNav()
const t = useTranslations()
return (
<Link
to={href}
Expand All @@ -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}
Expand All @@ -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 */
Expand All @@ -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}
Expand Down
4 changes: 3 additions & 1 deletion src/components/search.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
6 changes: 4 additions & 2 deletions src/components/top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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'>
Expand All @@ -34,7 +36,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
to={href}
className={!isActive ? 'text-muted-foreground' : ''}
>
{title}
{t(title)}
</Link>
</DropdownMenuItem>
))}
Expand All @@ -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>
Expand Down
12 changes: 7 additions & 5 deletions src/components/user-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Loading

0 comments on commit c5d163d

Please sign in to comment.