mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-09 17:26:20 +00:00
feat(monitor): add dashboard i18n infrastructure
Add the Monitor dashboard i18n provider, supported language metadata, message catalogs, fallback behavior, and the initial language selector/wiring for the AppImage UI.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"use client"
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
|
||||
import enMessages from "../../messages/en/common.json"
|
||||
import skMessages from "../../messages/sk/common.json"
|
||||
import esMessages from "../../messages/es/common.json"
|
||||
import frMessages from "../../messages/fr/common.json"
|
||||
import deMessages from "../../messages/de/common.json"
|
||||
import itMessages from "../../messages/it/common.json"
|
||||
import ptMessages from "../../messages/pt/common.json"
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
LANGUAGE_STORAGE_KEY,
|
||||
type LanguageCode,
|
||||
SUPPORTED_LANGUAGES,
|
||||
detectBrowserLanguage,
|
||||
isSupportedLanguage,
|
||||
} from "./languages"
|
||||
|
||||
type MessageTree = Record<string, unknown>
|
||||
type TranslationParams = Record<string, string | number>
|
||||
|
||||
const MESSAGE_CATALOG: Record<LanguageCode, MessageTree> = {
|
||||
en: enMessages as MessageTree,
|
||||
sk: skMessages as MessageTree,
|
||||
es: esMessages as MessageTree,
|
||||
fr: frMessages as MessageTree,
|
||||
de: deMessages as MessageTree,
|
||||
it: itMessages as MessageTree,
|
||||
pt: ptMessages as MessageTree,
|
||||
}
|
||||
|
||||
interface I18nContextValue {
|
||||
language: LanguageCode
|
||||
setLanguage: (language: LanguageCode) => void
|
||||
t: (key: string, params?: TranslationParams) => string
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextValue | null>(null)
|
||||
|
||||
function getInitialLanguage(): LanguageCode {
|
||||
if (typeof window === "undefined") return DEFAULT_LANGUAGE
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(LANGUAGE_STORAGE_KEY)
|
||||
if (isSupportedLanguage(stored)) return stored
|
||||
} catch {
|
||||
// localStorage may be unavailable in private browsing.
|
||||
}
|
||||
|
||||
return detectBrowserLanguage()
|
||||
}
|
||||
|
||||
function getMessage(messages: MessageTree, key: string): string | undefined {
|
||||
const value = key.split(".").reduce<unknown>((cursor, segment) => {
|
||||
if (!cursor || typeof cursor !== "object") return undefined
|
||||
return (cursor as Record<string, unknown>)[segment]
|
||||
}, messages)
|
||||
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function interpolate(template: string, params?: TranslationParams): string {
|
||||
if (!params) return template
|
||||
|
||||
return template.replace(/\{(\w+)\}/g, (match, name) => {
|
||||
const value = params[name]
|
||||
return value === undefined ? match : String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
||||
const [language, setLanguageState] = useState<LanguageCode>(DEFAULT_LANGUAGE)
|
||||
const [isHydrated, setIsHydrated] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLanguageState(getInitialLanguage())
|
||||
setIsHydrated(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHydrated) return
|
||||
|
||||
document.documentElement.lang = language
|
||||
try {
|
||||
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language)
|
||||
} catch {
|
||||
// Best-effort; the in-memory language still works for this session.
|
||||
}
|
||||
}, [isHydrated, language])
|
||||
|
||||
useEffect(() => {
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key === LANGUAGE_STORAGE_KEY && isSupportedLanguage(event.newValue)) {
|
||||
setLanguageState(event.newValue)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("storage", onStorage)
|
||||
return () => window.removeEventListener("storage", onStorage)
|
||||
}, [])
|
||||
|
||||
const setLanguage = useCallback((nextLanguage: LanguageCode) => {
|
||||
setLanguageState(nextLanguage)
|
||||
}, [])
|
||||
|
||||
const t = useCallback(
|
||||
(key: string, params?: TranslationParams) => {
|
||||
const localized = getMessage(MESSAGE_CATALOG[language], key)
|
||||
const fallback = getMessage(MESSAGE_CATALOG.en, key)
|
||||
return interpolate(localized ?? fallback ?? key, params)
|
||||
},
|
||||
[language],
|
||||
)
|
||||
|
||||
const value = useMemo<I18nContextValue>(() => ({ language, setLanguage, t }), [language, setLanguage, t])
|
||||
|
||||
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const context = useContext(I18nContext)
|
||||
if (!context) {
|
||||
throw new Error("useI18n must be used within I18nProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function useT() {
|
||||
return useI18n().t
|
||||
}
|
||||
|
||||
export { SUPPORTED_LANGUAGES }
|
||||
Reference in New Issue
Block a user