mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
126f2df21b
* chore: move i18n to shared package * chore: move server translations to shared package and apply linter and prettier on entire shared package
120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
import React, { createContext, useContext, useEffect, useMemo, useState, ReactNode } from 'react'
|
|
import { useSettingsStore } from '../store/settingsStore'
|
|
import en from '@trek/shared/i18n/en'
|
|
import type { SupportedLanguageCode } from '@trek/shared'
|
|
import {
|
|
SUPPORTED_LANGUAGES,
|
|
getLocaleForLanguage,
|
|
getIntlLanguage,
|
|
isRtlLanguage,
|
|
} from '@trek/shared'
|
|
import type { TranslationStrings } from '@trek/shared/i18n'
|
|
|
|
export { SUPPORTED_LANGUAGES }
|
|
|
|
// One explicit dynamic import per locale — Vite code-splits a separate chunk per locale.
|
|
// Only the active locale is fetched; en is always available synchronously as the fallback.
|
|
const localeLoaders: Record<SupportedLanguageCode, () => Promise<{ default: TranslationStrings }>> = {
|
|
en: () => Promise.resolve({ default: en }),
|
|
de: () => import('@trek/shared/i18n/de'),
|
|
es: () => import('@trek/shared/i18n/es'),
|
|
fr: () => import('@trek/shared/i18n/fr'),
|
|
hu: () => import('@trek/shared/i18n/hu'),
|
|
it: () => import('@trek/shared/i18n/it'),
|
|
tr: () => import('@trek/shared/i18n/tr'),
|
|
ru: () => import('@trek/shared/i18n/ru'),
|
|
zh: () => import('@trek/shared/i18n/zh'),
|
|
'zh-TW': () => import('@trek/shared/i18n/zh-TW'),
|
|
nl: () => import('@trek/shared/i18n/nl'),
|
|
id: () => import('@trek/shared/i18n/id'),
|
|
ar: () => import('@trek/shared/i18n/ar'),
|
|
br: () => import('@trek/shared/i18n/br'),
|
|
cs: () => import('@trek/shared/i18n/cs'),
|
|
pl: () => import('@trek/shared/i18n/pl'),
|
|
ja: () => import('@trek/shared/i18n/ja'),
|
|
ko: () => import('@trek/shared/i18n/ko'),
|
|
uk: () => import('@trek/shared/i18n/uk'),
|
|
}
|
|
|
|
// Re-export pure helpers that live in shared so downstream consumers can import them
|
|
// through this module without changing their import path.
|
|
export { getLocaleForLanguage, getIntlLanguage, isRtlLanguage }
|
|
|
|
// Detects the user's preferred language from browser/OS settings.
|
|
// Returns null if no supported language matches.
|
|
export function detectBrowserLanguage(): string | null {
|
|
if (typeof navigator === 'undefined') return null
|
|
const browserLangs = navigator.languages?.length
|
|
? navigator.languages
|
|
: navigator.language ? [navigator.language] : []
|
|
const supported = SUPPORTED_LANGUAGES.map(l => l.value)
|
|
|
|
for (const lang of browserLangs) {
|
|
const exactMatch = supported.find(s => s.toLowerCase() === lang.toLowerCase())
|
|
if (exactMatch) return exactMatch
|
|
|
|
// pt-BR has no exact match (our code is 'br'), so map it explicitly.
|
|
// pt-PT and bare 'pt' are NOT mapped — they fall through to null.
|
|
if (lang.toLowerCase() === 'pt-br') return 'br'
|
|
|
|
const prefix = lang.split('-')[0]?.toLowerCase()
|
|
const prefixMatch = supported.find(s => s.toLowerCase() === prefix)
|
|
if (prefixMatch) return prefixMatch
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
interface TranslationContextValue {
|
|
t: (key: string, params?: Record<string, string | number>) => string
|
|
language: string
|
|
locale: string
|
|
}
|
|
|
|
const TranslationContext = createContext<TranslationContextValue>({ t: (k: string) => k, language: 'en', locale: 'en-US' })
|
|
|
|
interface TranslationProviderProps {
|
|
children: ReactNode
|
|
}
|
|
|
|
export function TranslationProvider({ children }: TranslationProviderProps) {
|
|
const language = useSettingsStore((s) => s.settings.language) || 'en'
|
|
const [strings, setStrings] = useState<TranslationStrings>(en)
|
|
|
|
useEffect(() => {
|
|
document.documentElement.lang = language
|
|
document.documentElement.dir = isRtlLanguage(language) ? 'rtl' : 'ltr'
|
|
}, [language])
|
|
|
|
useEffect(() => {
|
|
const loader = localeLoaders[language as SupportedLanguageCode]
|
|
if (!loader) return
|
|
|
|
let cancelled = false
|
|
loader().then(mod => {
|
|
if (!cancelled) setStrings(mod.default)
|
|
})
|
|
return () => { cancelled = true }
|
|
}, [language])
|
|
|
|
const value = useMemo((): TranslationContextValue => {
|
|
function t(key: string, params?: Record<string, string | number>): string {
|
|
let val: string = (strings[key] ?? en[key] ?? key) as string
|
|
if (params) {
|
|
Object.entries(params).forEach(([k, v]) => {
|
|
val = val.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))
|
|
})
|
|
}
|
|
return val
|
|
}
|
|
|
|
return { t, language, locale: getLocaleForLanguage(language) }
|
|
}, [strings, language])
|
|
|
|
return <TranslationContext.Provider value={value}>{children}</TranslationContext.Provider>
|
|
}
|
|
|
|
export function useTranslation(): TranslationContextValue {
|
|
return useContext(TranslationContext)
|
|
}
|