app.tsx 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. import { Router } from "@solidjs/router"
  2. import { FileRoutes } from "@solidjs/start/router"
  3. import { Font } from "@opencode-ai/ui/font"
  4. import { MetaProvider } from "@solidjs/meta"
  5. import { MarkedProvider } from "@opencode-ai/ui/context/marked"
  6. import { DialogProvider } from "@opencode-ai/ui/context/dialog"
  7. import { I18nProvider, type UiI18nParams } from "@opencode-ai/ui/context"
  8. import { dict as uiEn } from "@opencode-ai/ui/i18n/en"
  9. import { dict as uiZh } from "@opencode-ai/ui/i18n/zh"
  10. import { createEffect, createMemo, Suspense, type ParentProps } from "solid-js"
  11. import { getRequestEvent } from "solid-js/web"
  12. import "./app.css"
  13. import { Favicon } from "@opencode-ai/ui/favicon"
  14. function resolveTemplate(text: string, params?: UiI18nParams) {
  15. if (!params) return text
  16. return text.replace(/{{\s*([^}]+?)\s*}}/g, (_, rawKey) => {
  17. const key = String(rawKey)
  18. const value = params[key]
  19. return value === undefined ? "" : String(value)
  20. })
  21. }
  22. function detectLocaleFromHeader(header: string | null | undefined) {
  23. if (!header) return
  24. for (const item of header.split(",")) {
  25. const value = item.trim().split(";")[0]?.toLowerCase()
  26. if (!value) continue
  27. if (value.startsWith("zh")) return "zh" as const
  28. if (value.startsWith("en")) return "en" as const
  29. }
  30. }
  31. function detectLocale() {
  32. const event = getRequestEvent()
  33. const header = event?.request.headers.get("accept-language")
  34. const headerLocale = detectLocaleFromHeader(header)
  35. if (headerLocale) return headerLocale
  36. if (typeof document === "object") {
  37. const value = document.documentElement.lang?.toLowerCase() ?? ""
  38. if (value.startsWith("zh")) return "zh" as const
  39. if (value.startsWith("en")) return "en" as const
  40. }
  41. if (typeof navigator === "object") {
  42. const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
  43. for (const language of languages) {
  44. if (!language) continue
  45. if (language.toLowerCase().startsWith("zh")) return "zh" as const
  46. }
  47. }
  48. return "en" as const
  49. }
  50. function UiI18nBridge(props: ParentProps) {
  51. const locale = createMemo(() => detectLocale())
  52. const zh = uiZh as Partial<Record<string, string>>
  53. const t = (key: keyof typeof uiEn, params?: UiI18nParams) => {
  54. const value = locale() === "zh" ? (zh[key] ?? uiEn[key]) : uiEn[key]
  55. const text = value ?? String(key)
  56. return resolveTemplate(text, params)
  57. }
  58. createEffect(() => {
  59. if (typeof document !== "object") return
  60. document.documentElement.lang = locale()
  61. })
  62. return <I18nProvider value={{ locale, t }}>{props.children}</I18nProvider>
  63. }
  64. export default function App() {
  65. return (
  66. <Router
  67. root={(props) => (
  68. <MetaProvider>
  69. <DialogProvider>
  70. <MarkedProvider>
  71. <Favicon />
  72. <Font />
  73. <UiI18nBridge>
  74. <Suspense>{props.children}</Suspense>
  75. </UiI18nBridge>
  76. </MarkedProvider>
  77. </DialogProvider>
  78. </MetaProvider>
  79. )}
  80. >
  81. <FileRoutes />
  82. </Router>
  83. )
  84. }