app.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  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 t = (key: keyof typeof uiEn, params?: UiI18nParams) => {
  53. const value = locale() === "zh" ? (uiZh[key] ?? uiEn[key]) : uiEn[key]
  54. const text = value ?? String(key)
  55. return resolveTemplate(text, params)
  56. }
  57. createEffect(() => {
  58. if (typeof document !== "object") return
  59. document.documentElement.lang = locale()
  60. })
  61. return <I18nProvider value={{ locale, t }}>{props.children}</I18nProvider>
  62. }
  63. export default function App() {
  64. return (
  65. <Router
  66. root={(props) => (
  67. <MetaProvider>
  68. <DialogProvider>
  69. <MarkedProvider>
  70. <Favicon />
  71. <Font />
  72. <UiI18nBridge>
  73. <Suspense>{props.children}</Suspense>
  74. </UiI18nBridge>
  75. </MarkedProvider>
  76. </DialogProvider>
  77. </MetaProvider>
  78. )}
  79. >
  80. <FileRoutes />
  81. </Router>
  82. )
  83. }