chore(app): i18n sync (#17283)

This commit is contained in:
Adam
2026-03-13 06:48:38 -05:00
committed by GitHub
parent 270cb0b8b4
commit 05cb3c87ca
65 changed files with 1776 additions and 156 deletions

View File

@@ -2,6 +2,7 @@ import { createEffect, createMemo, onCleanup, onMount, type Accessor } from "sol
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { dict as en } from "@/i18n/en"
import { useLanguage } from "@/context/language"
import { useSettings } from "@/context/settings"
import { Persist, persisted } from "@/utils/persist"
@@ -13,6 +14,27 @@ const DEFAULT_PALETTE_KEYBIND = "mod+shift+p"
const SUGGESTED_PREFIX = "suggested."
const EDITABLE_KEYBIND_IDS = new Set(["terminal.toggle", "terminal.new", "file.attach"])
type KeyLabel =
| "common.key.ctrl"
| "common.key.alt"
| "common.key.shift"
| "common.key.meta"
| "common.key.space"
| "common.key.backspace"
| "common.key.enter"
| "common.key.tab"
| "common.key.delete"
| "common.key.home"
| "common.key.end"
| "common.key.pageUp"
| "common.key.pageDown"
| "common.key.insert"
| "common.key.esc"
function keyText(key: KeyLabel, t?: (key: KeyLabel) => string) {
return t ? t(key) : en[key]
}
function actionId(id: string) {
if (!id.startsWith(SUGGESTED_PREFIX)) return id
return id.slice(SUGGESTED_PREFIX.length)
@@ -145,7 +167,7 @@ export function matchKeybind(keybinds: Keybind[], event: KeyboardEvent): boolean
return false
}
export function formatKeybind(config: string): string {
export function formatKeybind(config: string, t?: (key: KeyLabel) => string): string {
if (!config || config === "none") return ""
const keybinds = parseKeybind(config)
@@ -154,10 +176,10 @@ export function formatKeybind(config: string): string {
const kb = keybinds[0]
const parts: string[] = []
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : "Ctrl")
if (kb.alt) parts.push(IS_MAC ? "⌥" : "Alt")
if (kb.shift) parts.push(IS_MAC ? "⇧" : "Shift")
if (kb.meta) parts.push(IS_MAC ? "⌘" : "Meta")
if (kb.ctrl) parts.push(IS_MAC ? "⌃" : keyText("common.key.ctrl", t))
if (kb.alt) parts.push(IS_MAC ? "⌥" : keyText("common.key.alt", t))
if (kb.shift) parts.push(IS_MAC ? "⇧" : keyText("common.key.shift", t))
if (kb.meta) parts.push(IS_MAC ? "⌘" : keyText("common.key.meta", t))
if (kb.key) {
const keys: Record<string, string> = {
@@ -167,10 +189,29 @@ export function formatKeybind(config: string): string {
arrowright: "→",
comma: ",",
plus: "+",
space: "Space",
}
const named: Record<string, KeyLabel> = {
backspace: "common.key.backspace",
delete: "common.key.delete",
end: "common.key.end",
enter: "common.key.enter",
esc: "common.key.esc",
escape: "common.key.esc",
home: "common.key.home",
insert: "common.key.insert",
pagedown: "common.key.pageDown",
pageup: "common.key.pageUp",
space: "common.key.space",
tab: "common.key.tab",
}
const key = kb.key.toLowerCase()
const displayKey = keys[key] ?? (key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1))
const displayKey =
keys[key] ??
(named[key]
? keyText(named[key], t)
: key.length === 1
? key.toUpperCase()
: key.charAt(0).toUpperCase() + key.slice(1))
parts.push(displayKey)
}
@@ -364,17 +405,17 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
},
keybind(id: string) {
if (id === PALETTE_ID) {
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
return formatKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND, language.t)
}
const base = actionId(id)
const option = options().find((x) => actionId(x.id) === base)
if (option?.keybind) return formatKeybind(option.keybind)
if (option?.keybind) return formatKeybind(option.keybind, language.t)
const meta = catalog[base]
const config = bind(base, meta?.keybind)
if (!config) return ""
return formatKeybind(config)
return formatKeybind(config, language.t)
},
show: showPalette,
keybinds(enabled: boolean) {

View File

@@ -43,10 +43,10 @@ export {
touchFileContent,
}
function errorMessage(error: unknown) {
function errorMessage(error: unknown, fallback: string) {
if (error instanceof Error && error.message) return error.message
if (typeof error === "string" && error) return error
return "Unknown error"
return fallback
}
export const { use: useFile, provider: FileProvider } = createSimpleContext({
@@ -184,7 +184,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
})
.catch((e) => {
if (scope() !== directory) return
setLoadError(file, errorMessage(e))
setLoadError(file, errorMessage(e, language.t("error.chain.unknown")))
})
.finally(() => {
inflight.delete(key)

View File

@@ -4,6 +4,7 @@ import { createGlobalEmitter } from "@solid-primitives/event-bus"
import { batch, onCleanup } from "solid-js"
import z from "zod"
import { createSdkForServer } from "@/utils/server"
import { useLanguage } from "./language"
import { usePlatform } from "./platform"
import { useServer } from "./server"
@@ -14,6 +15,7 @@ const abortError = z.object({
export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleContext({
name: "GlobalSDK",
init: () => {
const language = useLanguage()
const server = useServer()
const platform = usePlatform()
const abort = new AbortController()
@@ -30,7 +32,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
})()
const currentServer = server.current
if (!currentServer) throw new Error("No server available")
if (!currentServer) throw new Error(language.t("error.globalSDK.noServerAvailable"))
const eventSdk = createSdkForServer({
signal: abort.signal,
@@ -218,7 +220,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
event: emitter,
createClient(opts: Omit<Parameters<typeof createSdkForServer>[0], "server" | "fetch">) {
const s = server.current
if (!s) throw new Error("Server not available")
if (!s) throw new Error(language.t("error.globalSDK.serverNotAvailable"))
return createSdkForServer({
server: s.http,
fetch: platform.fetch,

View File

@@ -164,6 +164,7 @@ function createGlobalSync() {
sdkCache.delete(directory)
clearSessionPrefetchDirectory(directory)
},
translate: language.t,
})
const sdkFor = (directory: string) => {

View File

@@ -139,7 +139,7 @@ export async function bootstrapDirectory(input: {
const project = getFilename(input.directory)
showToast({
variant: "error",
title: `Failed to reload ${project}`,
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
input.setStore("status", "partial")

View File

@@ -21,6 +21,7 @@ describe("createChildStoreManager", () => {
isLoadingSessions: () => false,
onBootstrap() {},
onDispose() {},
translate: (key) => key,
})
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {

View File

@@ -21,6 +21,7 @@ export function createChildStoreManager(input: {
isLoadingSessions: (directory: string) => boolean
onBootstrap: (directory: string) => void
onDispose: (directory: string) => void
translate: (key: string, vars?: Record<string, string | number>) => string
}) {
const children: Record<string, [Store<State>, SetStoreFunction<State>]> = {}
const vcsCache = new Map<string, VcsCache>()
@@ -129,7 +130,7 @@ export function createChildStoreManager(input: {
createStore({ value: undefined as VcsInfo | undefined }),
),
)
if (!vcs) throw new Error("Failed to create persisted cache")
if (!vcs) throw new Error(input.translate("error.childStore.persistedCacheCreateFailed"))
const vcsStore = vcs[0]
vcsCache.set(directory, { store: vcsStore, setStore: vcs[1], ready: vcs[3] })
@@ -139,7 +140,7 @@ export function createChildStoreManager(input: {
createStore({ value: undefined as ProjectMeta | undefined }),
),
)
if (!meta) throw new Error("Failed to create persisted project metadata")
if (!meta) throw new Error(input.translate("error.childStore.persistedProjectMetadataCreateFailed"))
metaCache.set(directory, { store: meta[0], setStore: meta[1], ready: meta[3] })
const icon = runWithOwner(input.owner, () =>
@@ -148,7 +149,7 @@ export function createChildStoreManager(input: {
createStore({ value: undefined as string | undefined }),
),
)
if (!icon) throw new Error("Failed to create persisted project icon")
if (!icon) throw new Error(input.translate("error.childStore.persistedProjectIconCreateFailed"))
iconCache.set(directory, { store: icon[0], setStore: icon[1], ready: icon[3] })
const init = () =>
@@ -211,7 +212,7 @@ export function createChildStoreManager(input: {
}
mark(directory)
const childStore = children[directory]
if (!childStore) throw new Error("Failed to create store")
if (!childStore) throw new Error(input.translate("error.childStore.storeCreateFailed"))
return childStore
}

View File

@@ -0,0 +1,51 @@
import { dict as ar } from "@/i18n/ar"
import { dict as br } from "@/i18n/br"
import { dict as bs } from "@/i18n/bs"
import { dict as da } from "@/i18n/da"
import { dict as de } from "@/i18n/de"
import { dict as en } from "@/i18n/en"
import { dict as es } from "@/i18n/es"
import { dict as fr } from "@/i18n/fr"
import { dict as ja } from "@/i18n/ja"
import { dict as ko } from "@/i18n/ko"
import { dict as no } from "@/i18n/no"
import { dict as pl } from "@/i18n/pl"
import { dict as ru } from "@/i18n/ru"
import { dict as th } from "@/i18n/th"
import { dict as tr } from "@/i18n/tr"
import { dict as zh } from "@/i18n/zh"
import { dict as zht } from "@/i18n/zht"
const numbered = Array.from(
new Set([
en["terminal.title.numbered"],
ar["terminal.title.numbered"],
br["terminal.title.numbered"],
bs["terminal.title.numbered"],
da["terminal.title.numbered"],
de["terminal.title.numbered"],
es["terminal.title.numbered"],
fr["terminal.title.numbered"],
ja["terminal.title.numbered"],
ko["terminal.title.numbered"],
no["terminal.title.numbered"],
pl["terminal.title.numbered"],
ru["terminal.title.numbered"],
th["terminal.title.numbered"],
tr["terminal.title.numbered"],
zh["terminal.title.numbered"],
zht["terminal.title.numbered"],
]),
)
export function defaultTitle(number: number) {
return en["terminal.title.numbered"].replace("{{number}}", String(number))
}
export function isDefaultTitle(title: string, number: number) {
return numbered.some((text) => title === text.replace("{{number}}", String(number)))
}
export function titleNumber(title: string, max: number) {
return Array.from({ length: max }, (_, idx) => idx + 1).find((number) => isDefaultTitle(title, number))
}

View File

@@ -4,6 +4,7 @@ import { batch, createEffect, createMemo, createRoot, on, onCleanup } from "soli
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import type { Platform } from "./platform"
import { defaultTitle, titleNumber } from "./terminal-title"
import { Persist, persisted, removePersisted } from "@/utils/persist"
export type LocalPTY = {
@@ -33,11 +34,7 @@ function num(value: unknown) {
}
function numberFromTitle(title: string) {
const match = title.match(/^Terminal (\d+)$/)
if (!match) return
const value = Number(match[1])
if (!Number.isFinite(value) || value <= 0) return
return value
return titleNumber(title, MAX_TERMINAL_SESSIONS)
}
function pty(value: unknown): LocalPTY | undefined {
@@ -202,13 +199,13 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
const nextNumber = pickNextTerminalNumber()
sdk.client.pty
.create({ title: `Terminal ${nextNumber}` })
.create({ title: defaultTitle(nextNumber) })
.then((pty: { data?: { id?: string; title?: string } }) => {
const id = pty.data?.id
if (!id) return
const newTerminal = {
id,
title: pty.data?.title ?? "Terminal",
title: pty.data?.title ?? defaultTitle(nextNumber),
titleNumber: nextNumber,
}
setStore("all", store.all.length, newTerminal)