LocaleContext.tsx 2.28 KB
// vibe_erp SPA locale context.
//
// Provides a `useT()` hook that returns a translation function
// `t('key')` bound to the active locale. The active locale is
// stored in localStorage and defaults to the browser's language.
//
// **Why client-side, not server-driven.** The backend already has
// ICU4J i18n with per-plug-in locale chains. The SPA's UI chrome
// (sidebar, buttons, status labels) is a separate concern — it
// runs entirely in the browser and should not require an API call
// to render a button label. The two i18n systems share the same
// locale code (en-US, zh-CN) so they stay in sync when the user
// picks a language.

import {
  createContext,
  useCallback,
  useContext,
  useState,
  type ReactNode,
} from 'react'
import { en, locales, type LocaleCode, type MessageKey } from './messages'

const STORAGE_KEY = 'vibeerp.locale'

function detectLocale(): LocaleCode {
  const stored = localStorage.getItem(STORAGE_KEY)
  if (stored && stored in locales) return stored as LocaleCode

  const browserLang = navigator.language
  if (browserLang.startsWith('zh')) return 'zh-CN'
  return 'en-US'
}

interface LocaleState {
  locale: LocaleCode
  setLocale: (code: LocaleCode) => void
  t: (key: MessageKey) => string
}

const LocaleContext = createContext<LocaleState | null>(null)

export function LocaleProvider({ children }: { children: ReactNode }) {
  const [locale, setLocaleState] = useState<LocaleCode>(detectLocale)

  const setLocale = useCallback((code: LocaleCode) => {
    localStorage.setItem(STORAGE_KEY, code)
    setLocaleState(code)
  }, [])

  const messages = locales[locale]

  const t = useCallback(
    (key: MessageKey): string => messages[key] ?? en[key] ?? key,
    [messages],
  )

  return (
    <LocaleContext.Provider value={{ locale, setLocale, t }}>
      {children}
    </LocaleContext.Provider>
  )
}

export function useT() {
  const ctx = useContext(LocaleContext)
  if (!ctx) throw new Error('useT must be used inside <LocaleProvider>')
  return ctx.t
}

export function useLocale() {
  const ctx = useContext(LocaleContext)
  if (!ctx) throw new Error('useLocale must be used inside <LocaleProvider>')
  return ctx
}

export const AVAILABLE_LOCALES: { code: LocaleCode; label: string }[] = [
  { code: 'en-US', label: 'English' },
  { code: 'zh-CN', label: '中文' },
]