Skip to content

Commit 11aef5d

Browse files
committed
feat(i18n): expand Explorer to 17 languages
1 parent 297f9e6 commit 11aef5d

22 files changed

Lines changed: 9824 additions & 19 deletions

src/components/detail/relative-time.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { formatDistanceToNow, type Locale as DateFnsLocale } from 'date-fns'
2-
import { enUS, es, fr, de, ru, zhCN, ja, ko } from 'date-fns/locale'
2+
import { enUS, es, fr, de, ru, zhCN, ja, ko, ca, hi, ar, bn, pt, id, tr, vi } from 'date-fns/locale'
33
import { useLocale, type Locale } from '@/lib/i18n'
44
import { cn } from '@/lib/utils'
55

@@ -18,6 +18,15 @@ const DATE_FNS_LOCALES: Record<Locale, DateFnsLocale> = {
1818
zh: zhCN,
1919
ja,
2020
ko,
21+
ca,
22+
hi,
23+
ar,
24+
bn,
25+
pt,
26+
id,
27+
ur: ar,
28+
tr,
29+
vi,
2130
}
2231

2332
/**

src/components/home/github-card.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Github, Star, Download, Tag } from 'lucide-react'
22
import { formatDistanceToNow } from 'date-fns'
3-
import { enUS, es, fr, de, ru, zhCN, ja, ko } from 'date-fns/locale'
3+
import { enUS, es, fr, de, ru, zhCN, ja, ko, ca, hi, ar, bn, pt, id, tr, vi } from 'date-fns/locale'
44
import { useLocale, useTranslations, type Locale } from '@/lib/i18n'
55
import { useGithubStats, type GithubReleaseAsset } from '@/hooks/use-github-stats'
66
import { ModuleCard } from '@/components/home/module-card'
@@ -17,6 +17,15 @@ const DATE_FNS_LOCALES: Record<Locale, typeof enUS> = {
1717
zh: zhCN,
1818
ja,
1919
ko,
20+
ca,
21+
hi,
22+
ar,
23+
bn,
24+
pt,
25+
id,
26+
ur: ar,
27+
tr,
28+
vi,
2029
}
2130

2231
const REPO_URL = 'https://github.com/FairCoinOfficial/FairCoin'

src/components/language-selector.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLocale, setLocale, SUPPORTED_LOCALES } from '@/lib/i18n'
1+
import { useLocale, useTranslations, setLocale, SUPPORTED_LOCALES } from '@/lib/i18n'
22
import { Globe } from 'lucide-react'
33
import {
44
DropdownMenu,
@@ -15,6 +15,7 @@ interface LanguageSelectorProps {
1515

1616
export function LanguageSelector({ collapsed }: LanguageSelectorProps) {
1717
const locale = useLocale()
18+
const t = useTranslations('common')
1819
const current = SUPPORTED_LOCALES.find((l) => l.code === locale)
1920

2021
return (
@@ -40,7 +41,7 @@ export function LanguageSelector({ collapsed }: LanguageSelectorProps) {
4041
key={loc.code}
4142
onClick={() => {
4243
setLocale(loc.code)
43-
toast.success(`Language changed to ${loc.name}`)
44+
toast.success(t('languageChanged', { language: loc.nativeName }))
4445
}}
4546
className={cn(
4647
'cursor-pointer',

src/lib/i18n.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111
import { describe, it, expect, beforeEach } from 'vitest'
1212
import { renderHook, act } from '@testing-library/react'
13-
import { getLocale, setLocale, useTranslations } from './i18n'
13+
import { getLocale, setLocale, SUPPORTED_LOCALES, useTranslations } from './i18n'
1414

1515
describe('useTranslations', () => {
1616
beforeEach(() => {
@@ -68,6 +68,11 @@ describe('useTranslations', () => {
6868
})
6969

7070
describe('setLocale / getLocale', () => {
71+
it('offers at least 15 languages including Catalan', () => {
72+
expect(SUPPORTED_LOCALES.length).toBeGreaterThanOrEqual(15)
73+
expect(SUPPORTED_LOCALES.some(({ code }) => code === 'ca')).toBe(true)
74+
})
75+
7176
it('updates the active locale and propagates to <html lang>', () => {
7277
act(() => setLocale('fr'))
7378
expect(getLocale()).toBe('fr')
@@ -80,4 +85,11 @@ describe('setLocale / getLocale', () => {
8085
setLocale('en')
8186
expect(document.documentElement.lang).toBe(before)
8287
})
88+
89+
it('sets right-to-left direction for Arabic and Urdu', () => {
90+
setLocale('ar')
91+
expect(document.documentElement.dir).toBe('rtl')
92+
setLocale('ca')
93+
expect(document.documentElement.dir).toBe('ltr')
94+
})
8395
})

src/lib/i18n.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,17 @@ import ru from '@/messages/ru.json'
88
import zh from '@/messages/zh.json'
99
import ja from '@/messages/ja.json'
1010
import ko from '@/messages/ko.json'
11-
12-
export type Locale = 'en' | 'es' | 'fr' | 'de' | 'ru' | 'zh' | 'ja' | 'ko'
11+
import ca from '@/messages/ca.json'
12+
import hi from '@/messages/hi.json'
13+
import ar from '@/messages/ar.json'
14+
import bn from '@/messages/bn.json'
15+
import pt from '@/messages/pt.json'
16+
import id from '@/messages/id.json'
17+
import ur from '@/messages/ur.json'
18+
import tr from '@/messages/tr.json'
19+
import vi from '@/messages/vi.json'
20+
21+
export type Locale = 'en' | 'es' | 'fr' | 'de' | 'ru' | 'zh' | 'ja' | 'ko' | 'ca' | 'hi' | 'ar' | 'bn' | 'pt' | 'id' | 'ur' | 'tr' | 'vi'
1322

1423
export interface LocaleConfig {
1524
code: Locale
@@ -26,6 +35,15 @@ export const SUPPORTED_LOCALES: LocaleConfig[] = [
2635
{ code: 'zh', name: 'Chinese', nativeName: '中文' },
2736
{ code: 'ja', name: 'Japanese', nativeName: '日本語' },
2837
{ code: 'ko', name: 'Korean', nativeName: '한국어' },
38+
{ code: 'ca', name: 'Catalan', nativeName: 'Català' },
39+
{ code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' },
40+
{ code: 'ar', name: 'Arabic', nativeName: 'العربية' },
41+
{ code: 'bn', name: 'Bengali', nativeName: 'বাংলা' },
42+
{ code: 'pt', name: 'Portuguese', nativeName: 'Português' },
43+
{ code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia' },
44+
{ code: 'ur', name: 'Urdu', nativeName: 'اردو' },
45+
{ code: 'tr', name: 'Turkish', nativeName: 'Türkçe' },
46+
{ code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' },
2947
]
3048

3149
const STORAGE_KEY = 'faircoin-locale'
@@ -42,6 +60,22 @@ const messagesByLocale: Record<Locale, Messages> = {
4260
zh: zh as Messages,
4361
ja: ja as Messages,
4462
ko: ko as Messages,
63+
ca: ca as Messages,
64+
hi: hi as Messages,
65+
ar: ar as Messages,
66+
bn: bn as Messages,
67+
pt: pt as Messages,
68+
id: id as Messages,
69+
ur: ur as Messages,
70+
tr: tr as Messages,
71+
vi: vi as Messages,
72+
}
73+
74+
const RTL_LOCALES = new Set<Locale>(['ar', 'ur'])
75+
76+
function syncDocumentLanguage(locale: Locale): void {
77+
document.documentElement.lang = locale
78+
document.documentElement.dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'
4579
}
4680

4781
// ── External store for locale (allows all components to react to changes) ──
@@ -55,7 +89,7 @@ let currentLocale: Locale = (() => {
5589

5690
// Keep <html lang> in sync with the active locale (a11y + SEO).
5791
if (typeof document !== 'undefined') {
58-
document.documentElement.lang = currentLocale
92+
syncDocumentLanguage(currentLocale)
5993
}
6094

6195
const listeners = new Set<() => void>()
@@ -79,7 +113,7 @@ export function setLocale(locale: Locale): void {
79113
if (locale === currentLocale) return
80114
currentLocale = locale
81115
localStorage.setItem(STORAGE_KEY, locale)
82-
document.documentElement.lang = locale
116+
syncDocumentLanguage(locale)
83117
emitChange()
84118
}
85119

0 commit comments

Comments
 (0)