All checks were successful
Beta Release / beta (push) Successful in 36s
Add full internationalization system with React context, French/English translations, and AZERTY/QWERTY keyboard layout support. Dashboard now uses a tabbed layout (Tools, Notifications, Workflows). Config page exposes language and keyboard preferences persisted via new /api/preferences endpoint. 💕 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
102 lines
2.9 KiB
JavaScript
102 lines
2.9 KiB
JavaScript
import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
import en from './en'
|
|
import fr from './fr'
|
|
import { getLayout, getLayoutList } from './keyboards'
|
|
import api from '../api/client'
|
|
|
|
const translations = { en, fr }
|
|
|
|
const STORAGE_KEY_LANG = 'muyue-language'
|
|
const STORAGE_KEY_KBD = 'muyue-keyboard'
|
|
|
|
const I18nContext = createContext(null)
|
|
|
|
function resolveLocale(layout) {
|
|
const l = getLayout(layout)
|
|
return l.locale
|
|
}
|
|
|
|
export function I18nProvider({ children }) {
|
|
const [language, setLanguageState] = useState(() => localStorage.getItem(STORAGE_KEY_LANG) || 'fr')
|
|
const [keyboard, setKeyboardState] = useState(() => localStorage.getItem(STORAGE_KEY_KBD) || 'azerty')
|
|
const [loaded, setLoaded] = useState(false)
|
|
const pendingSave = useRef(null)
|
|
|
|
useEffect(() => {
|
|
api.getConfig()
|
|
.then(d => {
|
|
const prefs = d.profile?.preferences
|
|
if (prefs?.language) setLanguageState(prefs.language)
|
|
if (prefs?.keyboard_layout) setKeyboardState(prefs.keyboard_layout)
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => setLoaded(true))
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (!loaded) return
|
|
if (pendingSave.current) clearTimeout(pendingSave.current)
|
|
pendingSave.current = setTimeout(() => {
|
|
api.savePreferences({ language, keyboard_layout: keyboard }).catch(() => {})
|
|
}, 500)
|
|
return () => { if (pendingSave.current) clearTimeout(pendingSave.current) }
|
|
}, [language, keyboard, loaded])
|
|
|
|
const setLanguage = useCallback((lang) => {
|
|
setLanguageState(lang)
|
|
localStorage.setItem(STORAGE_KEY_LANG, lang)
|
|
}, [])
|
|
|
|
const setKeyboard = useCallback((kbd) => {
|
|
setKeyboardState(kbd)
|
|
localStorage.setItem(STORAGE_KEY_KBD, kbd)
|
|
}, [])
|
|
|
|
const layout = useMemo(() => getLayout(keyboard), [keyboard])
|
|
|
|
const t = useCallback((key, params) => {
|
|
const dict = translations[language] || translations.fr
|
|
const keys = key.split('.')
|
|
let value = dict
|
|
for (const k of keys) {
|
|
if (value == null) return key
|
|
value = value[k]
|
|
}
|
|
if (typeof value !== 'string') return key
|
|
if (params) {
|
|
return Object.entries(params).reduce((str, [k, v]) => str.replace(`{${k}}`, v), value)
|
|
}
|
|
return value
|
|
}, [language])
|
|
|
|
const clockLocale = useMemo(() => resolveLocale(keyboard), [keyboard])
|
|
|
|
const contextValue = useMemo(() => ({
|
|
language,
|
|
keyboard,
|
|
layout,
|
|
setLanguage,
|
|
setKeyboard,
|
|
t,
|
|
clockLocale,
|
|
layouts: getLayoutList(),
|
|
}), [language, keyboard, layout, t, clockLocale])
|
|
|
|
return (
|
|
<I18nContext.Provider value={contextValue}>
|
|
{children}
|
|
</I18nContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useI18n() {
|
|
const ctx = useContext(I18nContext)
|
|
if (!ctx) throw new Error('useI18n must be used within I18nProvider')
|
|
return ctx
|
|
}
|
|
|
|
export const LANGUAGES = [
|
|
{ id: 'fr', name: 'Fran\u00e7ais' },
|
|
{ id: 'en', name: 'English' },
|
|
]
|