Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/components/detail/relative-time.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { formatDistanceToNow, type Locale as DateFnsLocale } from 'date-fns'
import { enUS, es, fr, de, ru, zhCN, ja, ko } from 'date-fns/locale'
import { enUS, es, fr, de, ru, zhCN, ja, ko, ca, hi, ar, bn, pt, id, tr, vi } from 'date-fns/locale'
import { useLocale, type Locale } from '@/lib/i18n'
import { cn } from '@/lib/utils'

Expand All @@ -18,6 +18,15 @@ const DATE_FNS_LOCALES: Record<Locale, DateFnsLocale> = {
zh: zhCN,
ja,
ko,
ca,
hi,
ar,
bn,
pt,
id,
ur: ar,
tr,
vi,
}

/**
Expand Down
11 changes: 10 additions & 1 deletion src/components/home/github-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Github, Star, Download, Tag } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import { enUS, es, fr, de, ru, zhCN, ja, ko } from 'date-fns/locale'
import { enUS, es, fr, de, ru, zhCN, ja, ko, ca, hi, ar, bn, pt, id, tr, vi } from 'date-fns/locale'
import { useLocale, useTranslations, type Locale } from '@/lib/i18n'
import { useGithubStats, type GithubReleaseAsset } from '@/hooks/use-github-stats'
import { ModuleCard } from '@/components/home/module-card'
Expand All @@ -17,6 +17,15 @@ const DATE_FNS_LOCALES: Record<Locale, typeof enUS> = {
zh: zhCN,
ja,
ko,
ca,
hi,
ar,
bn,
pt,
id,
ur: ar,
tr,
vi,
}

const REPO_URL = 'https://github.com/FairCoinOfficial/FairCoin'
Expand Down
5 changes: 3 additions & 2 deletions src/components/language-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocale, setLocale, SUPPORTED_LOCALES } from '@/lib/i18n'
import { useLocale, useTranslations, setLocale, SUPPORTED_LOCALES } from '@/lib/i18n'
import { Globe } from 'lucide-react'
import {
DropdownMenu,
Expand All @@ -15,6 +15,7 @@ interface LanguageSelectorProps {

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

return (
Expand All @@ -40,7 +41,7 @@ export function LanguageSelector({ collapsed }: LanguageSelectorProps) {
key={loc.code}
onClick={() => {
setLocale(loc.code)
toast.success(`Language changed to ${loc.name}`)
toast.success(t('languageChanged', { language: loc.nativeName }))
}}
className={cn(
'cursor-pointer',
Expand Down
14 changes: 13 additions & 1 deletion src/lib/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { getLocale, setLocale, useTranslations } from './i18n'
import { getLocale, setLocale, SUPPORTED_LOCALES, useTranslations } from './i18n'

describe('useTranslations', () => {
beforeEach(() => {
Expand Down Expand Up @@ -68,6 +68,11 @@ describe('useTranslations', () => {
})

describe('setLocale / getLocale', () => {
it('offers at least 15 languages including Catalan', () => {
expect(SUPPORTED_LOCALES.length).toBeGreaterThanOrEqual(15)
expect(SUPPORTED_LOCALES.some(({ code }) => code === 'ca')).toBe(true)
})

it('updates the active locale and propagates to <html lang>', () => {
act(() => setLocale('fr'))
expect(getLocale()).toBe('fr')
Expand All @@ -80,4 +85,11 @@ describe('setLocale / getLocale', () => {
setLocale('en')
expect(document.documentElement.lang).toBe(before)
})

it('sets right-to-left direction for Arabic and Urdu', () => {
setLocale('ar')
expect(document.documentElement.dir).toBe('rtl')
setLocale('ca')
expect(document.documentElement.dir).toBe('ltr')
})
})
42 changes: 38 additions & 4 deletions src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import ru from '@/messages/ru.json'
import zh from '@/messages/zh.json'
import ja from '@/messages/ja.json'
import ko from '@/messages/ko.json'

export type Locale = 'en' | 'es' | 'fr' | 'de' | 'ru' | 'zh' | 'ja' | 'ko'
import ca from '@/messages/ca.json'
import hi from '@/messages/hi.json'
import ar from '@/messages/ar.json'
import bn from '@/messages/bn.json'
import pt from '@/messages/pt.json'
import id from '@/messages/id.json'
import ur from '@/messages/ur.json'
import tr from '@/messages/tr.json'
import vi from '@/messages/vi.json'

export type Locale = 'en' | 'es' | 'fr' | 'de' | 'ru' | 'zh' | 'ja' | 'ko' | 'ca' | 'hi' | 'ar' | 'bn' | 'pt' | 'id' | 'ur' | 'tr' | 'vi'

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

const STORAGE_KEY = 'faircoin-locale'
Expand All @@ -42,6 +60,22 @@ const messagesByLocale: Record<Locale, Messages> = {
zh: zh as Messages,
ja: ja as Messages,
ko: ko as Messages,
ca: ca as Messages,
hi: hi as Messages,
ar: ar as Messages,
bn: bn as Messages,
pt: pt as Messages,
id: id as Messages,
ur: ur as Messages,
tr: tr as Messages,
vi: vi as Messages,
}

const RTL_LOCALES = new Set<Locale>(['ar', 'ur'])

function syncDocumentLanguage(locale: Locale): void {
document.documentElement.lang = locale
document.documentElement.dir = RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'
}

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

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

const listeners = new Set<() => void>()
Expand All @@ -79,7 +113,7 @@ export function setLocale(locale: Locale): void {
if (locale === currentLocale) return
currentLocale = locale
localStorage.setItem(STORAGE_KEY, locale)
document.documentElement.lang = locale
syncDocumentLanguage(locale)
emitChange()
}

Expand Down
Loading
Loading