feat(app): initial i18n stubbing
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -33,6 +33,7 @@
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
"@solid-primitives/scroll": "2.1.3",
|
||||
@@ -1634,6 +1635,8 @@
|
||||
|
||||
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
|
||||
|
||||
"@solid-primitives/i18n": ["@solid-primitives/i18n@2.2.1", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-TnTnE2Ku11MGYZ1JzhJ8pYscwg1fr9MteoYxPwsfxWfh9Jp5K7RRJncJn9BhOHvNLwROjqOHZ46PT7sPHqbcXw=="],
|
||||
|
||||
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="],
|
||||
|
||||
"@solid-primitives/map": ["@solid-primitives/map@0.4.13", "", { "dependencies": { "@solid-primitives/trigger": "^1.1.0" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew=="],
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@shikijs/transformers": "3.9.2",
|
||||
"@solid-primitives/active-element": "2.1.3",
|
||||
"@solid-primitives/audio": "1.4.2",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/event-bus": "1.1.2",
|
||||
"@solid-primitives/media": "2.3.3",
|
||||
"@solid-primitives/resize-observer": "2.1.3",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { FileProvider } from "@/context/file"
|
||||
import { NotificationProvider } from "@/context/notification"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { CommandProvider } from "@/context/command"
|
||||
import { LanguageProvider } from "@/context/language"
|
||||
import { Logo } from "@opencode-ai/ui/logo"
|
||||
import Layout from "@/pages/layout"
|
||||
import DirectoryLayout from "@/pages/directory-layout"
|
||||
@@ -84,15 +85,17 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
||||
<Router
|
||||
root={(props) => (
|
||||
<SettingsProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
<LanguageProvider>
|
||||
<PermissionProvider>
|
||||
<LayoutProvider>
|
||||
<NotificationProvider>
|
||||
<CommandProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</CommandProvider>
|
||||
</NotificationProvider>
|
||||
</LayoutProvider>
|
||||
</PermissionProvider>
|
||||
</LanguageProvider>
|
||||
</SettingsProvider>
|
||||
)}
|
||||
>
|
||||
|
||||
77
packages/app/src/context/language.tsx
Normal file
77
packages/app/src/context/language.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as i18n from "@solid-primitives/i18n"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createSimpleContext } from "@opencode-ai/ui/context"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { dict as en } from "@/i18n/en"
|
||||
import { dict as zh } from "@/i18n/zh"
|
||||
|
||||
export type Locale = "en" | "zh"
|
||||
|
||||
type RawDictionary = typeof en
|
||||
type Dictionary = i18n.Flatten<RawDictionary>
|
||||
|
||||
const LOCALES: readonly Locale[] = ["en", "zh"]
|
||||
|
||||
function detectLocale(): Locale {
|
||||
if (typeof navigator !== "object") return "en"
|
||||
|
||||
const languages = navigator.languages?.length ? navigator.languages : [navigator.language]
|
||||
for (const language of languages) {
|
||||
if (!language) continue
|
||||
if (language.toLowerCase().startsWith("zh")) return "zh"
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
export const { use: useLanguage, provider: LanguageProvider } = createSimpleContext({
|
||||
name: "Language",
|
||||
init: () => {
|
||||
const [store, setStore, _, ready] = persisted(
|
||||
Persist.global("language", ["language.v1"]),
|
||||
createStore({
|
||||
locale: detectLocale() as Locale,
|
||||
}),
|
||||
)
|
||||
|
||||
const locale = createMemo<Locale>(() => (store.locale === "zh" ? "zh" : "en"))
|
||||
|
||||
createEffect(() => {
|
||||
const current = locale()
|
||||
if (store.locale === current) return
|
||||
setStore("locale", current)
|
||||
})
|
||||
|
||||
const base = i18n.flatten(en)
|
||||
const dict = createMemo<Dictionary>(() => {
|
||||
if (locale() === "en") return base
|
||||
return { ...base, ...i18n.flatten(zh) }
|
||||
})
|
||||
|
||||
const t = i18n.translator(dict, i18n.resolveTemplate)
|
||||
|
||||
const labelKey: Record<Locale, keyof Dictionary> = {
|
||||
en: "language.en",
|
||||
zh: "language.zh",
|
||||
}
|
||||
|
||||
const label = (value: Locale) => t(labelKey[value])
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof document !== "object") return
|
||||
document.documentElement.lang = locale()
|
||||
})
|
||||
|
||||
return {
|
||||
ready,
|
||||
locale,
|
||||
locales: LOCALES,
|
||||
label,
|
||||
t,
|
||||
setLocale(next: Locale) {
|
||||
setStore("locale", next)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
9
packages/app/src/i18n/en.ts
Normal file
9
packages/app/src/i18n/en.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const dict = {
|
||||
"command.category.language": "Language",
|
||||
"command.language.cycle": "Cycle language",
|
||||
"command.language.set": "Use language: {{language}}",
|
||||
"language.en": "English",
|
||||
"language.zh": "Chinese",
|
||||
"toast.language.title": "Language",
|
||||
"toast.language.description": "Switched to {{language}}",
|
||||
}
|
||||
13
packages/app/src/i18n/zh.ts
Normal file
13
packages/app/src/i18n/zh.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { dict as en } from "./en"
|
||||
|
||||
type Keys = keyof typeof en
|
||||
|
||||
export const dict = {
|
||||
"command.category.language": "\u8bed\u8a00",
|
||||
"command.language.cycle": "\u5207\u6362\u8bed\u8a00",
|
||||
"command.language.set": "\u4f7f\u7528\u8bed\u8a00: {{language}}",
|
||||
"language.en": "\u82f1\u8bed",
|
||||
"language.zh": "\u4e2d\u6587",
|
||||
"toast.language.title": "\u8bed\u8a00",
|
||||
"toast.language.description": "\u5df2\u5207\u6362\u5230{{language}}",
|
||||
} satisfies Partial<Record<Keys, string>>
|
||||
@@ -69,6 +69,7 @@ import { DialogSelectDirectory } from "@/components/dialog-select-directory"
|
||||
import { DialogEditProject } from "@/components/dialog-edit-project"
|
||||
import { Titlebar } from "@/components/titlebar"
|
||||
import { useServer } from "@/context/server"
|
||||
import { useLanguage, type Locale } from "@/context/language"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const [store, setStore, , ready] = persisted(
|
||||
@@ -109,6 +110,7 @@ export default function Layout(props: ParentProps) {
|
||||
const dialog = useDialog()
|
||||
const command = useCommand()
|
||||
const theme = useTheme()
|
||||
const language = useLanguage()
|
||||
const initialDir = params.dir
|
||||
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
|
||||
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
|
||||
@@ -268,6 +270,24 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
}
|
||||
|
||||
function setLocale(next: Locale) {
|
||||
if (next === language.locale()) return
|
||||
language.setLocale(next)
|
||||
showToast({
|
||||
title: language.t("toast.language.title"),
|
||||
description: language.t("toast.language.description", { language: language.label(next) }),
|
||||
})
|
||||
}
|
||||
|
||||
function cycleLanguage(direction = 1) {
|
||||
const locales = language.locales
|
||||
const currentIndex = locales.indexOf(language.locale())
|
||||
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + direction + locales.length) % locales.length
|
||||
const next = locales[nextIndex]
|
||||
if (!next) return
|
||||
setLocale(next)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||
|
||||
@@ -906,6 +926,22 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
}
|
||||
|
||||
commands.push({
|
||||
id: "language.cycle",
|
||||
title: language.t("command.language.cycle"),
|
||||
category: language.t("command.category.language"),
|
||||
onSelect: () => cycleLanguage(1),
|
||||
})
|
||||
|
||||
for (const locale of language.locales) {
|
||||
commands.push({
|
||||
id: `language.set.${locale}`,
|
||||
title: language.t("command.language.set", { language: language.label(locale) }),
|
||||
category: language.t("command.category.language"),
|
||||
onSelect: () => setLocale(locale),
|
||||
})
|
||||
}
|
||||
|
||||
return commands
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user