wip(app): i18n
This commit is contained in:
@@ -44,6 +44,7 @@ export function AppBaseProviders(props: ParentProps) {
|
|||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
<Font />
|
<Font />
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
<LanguageProvider>
|
||||||
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<MarkedProvider>
|
<MarkedProvider>
|
||||||
@@ -53,6 +54,7 @@ export function AppBaseProviders(props: ParentProps) {
|
|||||||
</MarkedProvider>
|
</MarkedProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
</LanguageProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MetaProvider>
|
</MetaProvider>
|
||||||
)
|
)
|
||||||
@@ -85,7 +87,6 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
|||||||
<Router
|
<Router
|
||||||
root={(props) => (
|
root={(props) => (
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<LanguageProvider>
|
|
||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<LayoutProvider>
|
<LayoutProvider>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
@@ -95,7 +96,6 @@ export function AppInterface(props: { defaultUrl?: string }) {
|
|||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</LayoutProvider>
|
</LayoutProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
</LanguageProvider>
|
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ export const DialogFork: Component = () => {
|
|||||||
if (!sessionID) return
|
if (!sessionID) return
|
||||||
|
|
||||||
const parts = sync.data.part[item.id] ?? []
|
const parts = sync.data.part[item.id] ?? []
|
||||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
const restored = extractPromptFromParts(parts, {
|
||||||
|
directory: sdk.directory,
|
||||||
|
attachmentName: language.t("common.attachment"),
|
||||||
|
})
|
||||||
|
|
||||||
dialog.close()
|
dialog.close()
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ const ModelList: Component<{
|
|||||||
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
sortBy={(a, b) => a.name.localeCompare(b.name)}
|
||||||
groupBy={(x) => x.provider.name}
|
groupBy={(x) => x.provider.name}
|
||||||
sortGroupsBy={(a, b) => {
|
sortGroupsBy={(a, b) => {
|
||||||
if (a.category === "Recent" && b.category !== "Recent") return -1
|
|
||||||
if (b.category === "Recent" && a.category !== "Recent") return 1
|
|
||||||
const aProvider = a.items[0].provider.id
|
const aProvider = a.items[0].provider.id
|
||||||
const bProvider = b.items[0].provider.id
|
const bProvider = b.items[0].provider.id
|
||||||
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
if (popularProviders.includes(aProvider) && !popularProviders.includes(bProvider)) return -1
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createMemo, Show } from "solid-js"
|
import { createMemo, Show } from "solid-js"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
|
|
||||||
export function SessionLspIndicator() {
|
export function SessionLspIndicator() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
const lspStats = createMemo(() => {
|
const lspStats = createMemo(() => {
|
||||||
const lsp = sync.data.lsp ?? []
|
const lsp = sync.data.lsp ?? []
|
||||||
@@ -15,7 +17,7 @@ export function SessionLspIndicator() {
|
|||||||
|
|
||||||
const tooltipContent = createMemo(() => {
|
const tooltipContent = createMemo(() => {
|
||||||
const lsp = sync.data.lsp ?? []
|
const lsp = sync.data.lsp ?? []
|
||||||
if (lsp.length === 0) return "No LSP servers"
|
if (lsp.length === 0) return language.t("lsp.tooltip.none")
|
||||||
return lsp.map((s) => s.name).join(", ")
|
return lsp.map((s) => s.name).join(", ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,7 +32,9 @@ export function SessionLspIndicator() {
|
|||||||
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
"bg-icon-success-base": !lspStats().hasError && lspStats().connected > 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span class="text-12-regular text-text-weak">{lspStats().connected} LSP</span>
|
<span class="text-12-regular text-text-weak">
|
||||||
|
{language.t("lsp.label.connected", { count: lspStats().connected })}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Tooltip } from "@opencode-ai/ui/tooltip"
|
|||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
export function FileVisual(props: { path: string; active?: boolean }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +26,7 @@ export function FileVisual(props: { path: string; active?: boolean }): JSX.Eleme
|
|||||||
|
|
||||||
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
export function SortableTab(props: { tab: string; onTabClose: (tab: string) => void }): JSX.Element {
|
||||||
const file = useFile()
|
const file = useFile()
|
||||||
|
const language = useLanguage()
|
||||||
const sortable = createSortable(props.tab)
|
const sortable = createSortable(props.tab)
|
||||||
const path = createMemo(() => file.pathFromTab(props.tab))
|
const path = createMemo(() => file.pathFromTab(props.tab))
|
||||||
return (
|
return (
|
||||||
@@ -34,7 +36,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v
|
|||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value={props.tab}
|
value={props.tab}
|
||||||
closeButton={
|
closeButton={
|
||||||
<Tooltip value="Close tab" placement="bottom">
|
<Tooltip value={language.t("common.closeTab")} placement="bottom">
|
||||||
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
<IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { useTheme } from "@opencode-ai/ui/theme"
|
|||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
import { useCommand } from "@/context/command"
|
import { useCommand } from "@/context/command"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export function Titlebar() {
|
export function Titlebar() {
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
|
const language = useLanguage()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
|
||||||
@@ -93,7 +95,7 @@ export function Titlebar() {
|
|||||||
<TooltipKeybind
|
<TooltipKeybind
|
||||||
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
class={web() ? "hidden xl:flex shrink-0 ml-14" : "hidden xl:flex shrink-0"}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
title="Toggle sidebar"
|
title={language.t("command.sidebar.toggle")}
|
||||||
keybind={command.keybind("sidebar.toggle")}
|
keybind={command.keybind("sidebar.toggle")}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useParams } from "@solidjs/router"
|
|||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { useSDK } from "./sdk"
|
import { useSDK } from "./sdk"
|
||||||
import { useSync } from "./sync"
|
import { useSync } from "./sync"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
||||||
export type FileSelection = {
|
export type FileSelection = {
|
||||||
@@ -186,6 +187,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
const directory = createMemo(() => sync.data.path.directory)
|
const directory = createMemo(() => sync.data.path.directory)
|
||||||
|
|
||||||
@@ -323,7 +325,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: "Failed to load file",
|
title: language.t("toast.file.loadFailed.title"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { getFilename } from "@opencode-ai/util/path"
|
import { getFilename } from "@opencode-ai/util/path"
|
||||||
import { usePlatform } from "./platform"
|
import { usePlatform } from "./platform"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -95,6 +96,7 @@ type ChildOptions = {
|
|||||||
function createGlobalSync() {
|
function createGlobalSync() {
|
||||||
const globalSDK = useGlobalSDK()
|
const globalSDK = useGlobalSDK()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
|
const language = useLanguage()
|
||||||
const owner = getOwner()
|
const owner = getOwner()
|
||||||
if (!owner) throw new Error("GlobalSync must be created within owner")
|
if (!owner) throw new Error("GlobalSync must be created within owner")
|
||||||
const vcsCache = new Map<string, VcsCache>()
|
const vcsCache = new Map<string, VcsCache>()
|
||||||
@@ -232,7 +234,7 @@ function createGlobalSync() {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to load sessions", err)
|
console.error("Failed to load sessions", err)
|
||||||
const project = getFilename(directory)
|
const project = getFilename(directory)
|
||||||
showToast({ title: `Failed to load sessions for ${project}`, description: err.message })
|
showToast({ title: language.t("toast.session.listFailed.title", { project }), description: err.message })
|
||||||
})
|
})
|
||||||
|
|
||||||
sessionLoads.set(directory, promise)
|
sessionLoads.set(directory, promise)
|
||||||
@@ -658,7 +660,7 @@ function createGlobalSync() {
|
|||||||
if (!health?.healthy) {
|
if (!health?.healthy) {
|
||||||
setGlobalStore(
|
setGlobalStore(
|
||||||
"error",
|
"error",
|
||||||
new Error(`Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`),
|
new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useProviders } from "@/hooks/use-providers"
|
|||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { Persist, persisted } from "@/utils/persist"
|
import { Persist, persisted } from "@/utils/persist"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
|
||||||
export type LocalFile = FileNode &
|
export type LocalFile = FileNode &
|
||||||
Partial<{
|
Partial<{
|
||||||
@@ -42,6 +43,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
function isModelValid(model: ModelKey) {
|
function isModelValid(model: ModelKey) {
|
||||||
const provider = providers.all().find((x) => x.id === model.providerID)
|
const provider = providers.all().find((x) => x.id === model.providerID)
|
||||||
@@ -409,7 +411,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
showToast({
|
showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: "Failed to load file",
|
title: language.t("toast.file.loadFailed.title"),
|
||||||
description: e.message,
|
description: e.message,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createSimpleContext } from "@opencode-ai/ui/context"
|
|||||||
import { useGlobalSDK } from "./global-sdk"
|
import { useGlobalSDK } from "./global-sdk"
|
||||||
import { useGlobalSync } from "./global-sync"
|
import { useGlobalSync } from "./global-sync"
|
||||||
import { usePlatform } from "@/context/platform"
|
import { usePlatform } from "@/context/platform"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { base64Encode } from "@opencode-ai/util/encode"
|
import { base64Encode } from "@opencode-ai/util/encode"
|
||||||
@@ -47,6 +48,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
const globalSync = useGlobalSync()
|
const globalSync = useGlobalSync()
|
||||||
const platform = usePlatform()
|
const platform = usePlatform()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const language = useLanguage()
|
||||||
|
|
||||||
const [store, setStore, _, ready] = persisted(
|
const [store, setStore, _, ready] = persisted(
|
||||||
Persist.global("notification", ["notification.v1"]),
|
Persist.global("notification", ["notification.v1"]),
|
||||||
@@ -94,9 +96,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
|
|
||||||
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
const href = `/${base64Encode(directory)}/session/${sessionID}`
|
||||||
if (settings.notifications.agent()) {
|
if (settings.notifications.agent()) {
|
||||||
void platform.notify("Response ready", session?.title ?? sessionID, href)
|
void platform.notify(language.t("notification.session.responseReady.title"), session?.title ?? sessionID, href)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "session.error": {
|
case "session.error": {
|
||||||
@@ -115,13 +116,12 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
|
|||||||
session: sessionID ?? "global",
|
session: sessionID ?? "global",
|
||||||
error,
|
error,
|
||||||
})
|
})
|
||||||
|
const description =
|
||||||
const description = session?.title ?? (typeof error === "string" ? error : "An error occurred")
|
session?.title ?? (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription"))
|
||||||
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}`
|
||||||
if (settings.notifications.errors()) {
|
if (settings.notifications.errors()) {
|
||||||
void platform.notify("Session error", description, href)
|
void platform.notify(language.t("notification.session.error.title"), description, href)
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export const dict = {
|
|||||||
"common.save": "Save",
|
"common.save": "Save",
|
||||||
"common.saving": "Saving...",
|
"common.saving": "Saving...",
|
||||||
"common.default": "Default",
|
"common.default": "Default",
|
||||||
|
"common.attachment": "attachment",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "Enter shell command...",
|
"prompt.placeholder.shell": "Enter shell command...",
|
||||||
"prompt.placeholder.normal": "Ask anything... \"{{example}}\"",
|
"prompt.placeholder.normal": "Ask anything... \"{{example}}\"",
|
||||||
@@ -278,6 +279,8 @@ export const dict = {
|
|||||||
"toast.model.none.title": "No model selected",
|
"toast.model.none.title": "No model selected",
|
||||||
"toast.model.none.description": "Connect a provider to summarize this session",
|
"toast.model.none.description": "Connect a provider to summarize this session",
|
||||||
|
|
||||||
|
"toast.file.loadFailed.title": "Failed to load file",
|
||||||
|
|
||||||
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
"toast.session.share.copyFailed.title": "Failed to copy URL to clipboard",
|
||||||
"toast.session.share.success.title": "Session shared",
|
"toast.session.share.success.title": "Session shared",
|
||||||
"toast.session.share.success.description": "Share URL copied to clipboard!",
|
"toast.session.share.success.description": "Share URL copied to clipboard!",
|
||||||
@@ -289,6 +292,8 @@ export const dict = {
|
|||||||
"toast.session.unshare.failed.title": "Failed to unshare session",
|
"toast.session.unshare.failed.title": "Failed to unshare session",
|
||||||
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
|
||||||
|
|
||||||
|
"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
|
||||||
|
|
||||||
"toast.update.title": "Update available",
|
"toast.update.title": "Update available",
|
||||||
"toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.",
|
"toast.update.description": "A new version of OpenCode ({{version}}) is now available to install.",
|
||||||
"toast.update.action.installRestart": "Install and restart",
|
"toast.update.action.installRestart": "Install and restart",
|
||||||
@@ -305,6 +310,8 @@ export const dict = {
|
|||||||
"error.page.report.discord": "on Discord",
|
"error.page.report.discord": "on Discord",
|
||||||
"error.page.version": "Version: {{version}}",
|
"error.page.version": "Version: {{version}}",
|
||||||
|
|
||||||
|
"error.globalSync.connectFailed": "Could not connect to server. Is there a server running at `{{url}}`?",
|
||||||
|
|
||||||
"error.chain.unknown": "Unknown error",
|
"error.chain.unknown": "Unknown error",
|
||||||
"error.chain.causedBy": "Caused by:",
|
"error.chain.causedBy": "Caused by:",
|
||||||
"error.chain.apiError": "API error",
|
"error.chain.apiError": "API error",
|
||||||
@@ -332,6 +339,10 @@ export const dict = {
|
|||||||
"notification.question.description": "{{sessionTitle}} in {{projectName}} has a question",
|
"notification.question.description": "{{sessionTitle}} in {{projectName}} has a question",
|
||||||
"notification.action.goToSession": "Go to session",
|
"notification.action.goToSession": "Go to session",
|
||||||
|
|
||||||
|
"notification.session.responseReady.title": "Response ready",
|
||||||
|
"notification.session.error.title": "Session error",
|
||||||
|
"notification.session.error.fallbackDescription": "An error occurred",
|
||||||
|
|
||||||
"home.recentProjects": "Recent projects",
|
"home.recentProjects": "Recent projects",
|
||||||
"home.empty.title": "No recent projects",
|
"home.empty.title": "No recent projects",
|
||||||
"home.empty.description": "Get started by opening a local project",
|
"home.empty.description": "Get started by opening a local project",
|
||||||
@@ -368,6 +379,9 @@ export const dict = {
|
|||||||
"session.share.copy.copied": "Copied",
|
"session.share.copy.copied": "Copied",
|
||||||
"session.share.copy.copyLink": "Copy link",
|
"session.share.copy.copyLink": "Copy link",
|
||||||
|
|
||||||
|
"lsp.tooltip.none": "No LSP servers",
|
||||||
|
"lsp.label.connected": "{{count}} LSP",
|
||||||
|
|
||||||
"prompt.loading": "Loading prompt...",
|
"prompt.loading": "Loading prompt...",
|
||||||
"terminal.loading": "Loading terminal...",
|
"terminal.loading": "Loading terminal...",
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export const dict = {
|
|||||||
"common.save": "保存",
|
"common.save": "保存",
|
||||||
"common.saving": "保存中...",
|
"common.saving": "保存中...",
|
||||||
"common.default": "默认",
|
"common.default": "默认",
|
||||||
|
"common.attachment": "附件",
|
||||||
|
|
||||||
"prompt.placeholder.shell": "输入 shell 命令...",
|
"prompt.placeholder.shell": "输入 shell 命令...",
|
||||||
"prompt.placeholder.normal": "随便问点什么... \"{{example}}\"",
|
"prompt.placeholder.normal": "随便问点什么... \"{{example}}\"",
|
||||||
@@ -277,6 +278,8 @@ export const dict = {
|
|||||||
"toast.model.none.title": "未选择模型",
|
"toast.model.none.title": "未选择模型",
|
||||||
"toast.model.none.description": "请先连接提供商以总结此会话",
|
"toast.model.none.description": "请先连接提供商以总结此会话",
|
||||||
|
|
||||||
|
"toast.file.loadFailed.title": "加载文件失败",
|
||||||
|
|
||||||
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
|
"toast.session.share.copyFailed.title": "无法复制链接到剪贴板",
|
||||||
"toast.session.share.success.title": "会话已分享",
|
"toast.session.share.success.title": "会话已分享",
|
||||||
"toast.session.share.success.description": "分享链接已复制到剪贴板",
|
"toast.session.share.success.description": "分享链接已复制到剪贴板",
|
||||||
@@ -288,6 +291,8 @@ export const dict = {
|
|||||||
"toast.session.unshare.failed.title": "取消分享失败",
|
"toast.session.unshare.failed.title": "取消分享失败",
|
||||||
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
|
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
|
||||||
|
|
||||||
|
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
|
||||||
|
|
||||||
"toast.update.title": "有可用更新",
|
"toast.update.title": "有可用更新",
|
||||||
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
|
"toast.update.description": "OpenCode 有新版本 ({{version}}) 可安装。",
|
||||||
"toast.update.action.installRestart": "安装并重启",
|
"toast.update.action.installRestart": "安装并重启",
|
||||||
@@ -304,6 +309,8 @@ export const dict = {
|
|||||||
"error.page.report.discord": "在 Discord 上",
|
"error.page.report.discord": "在 Discord 上",
|
||||||
"error.page.version": "版本: {{version}}",
|
"error.page.version": "版本: {{version}}",
|
||||||
|
|
||||||
|
"error.globalSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?",
|
||||||
|
|
||||||
"error.chain.unknown": "未知错误",
|
"error.chain.unknown": "未知错误",
|
||||||
"error.chain.causedBy": "原因:",
|
"error.chain.causedBy": "原因:",
|
||||||
"error.chain.apiError": "API 错误",
|
"error.chain.apiError": "API 错误",
|
||||||
@@ -329,6 +336,10 @@ export const dict = {
|
|||||||
"notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题",
|
"notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题",
|
||||||
"notification.action.goToSession": "前往会话",
|
"notification.action.goToSession": "前往会话",
|
||||||
|
|
||||||
|
"notification.session.responseReady.title": "回复已就绪",
|
||||||
|
"notification.session.error.title": "会话错误",
|
||||||
|
"notification.session.error.fallbackDescription": "发生错误",
|
||||||
|
|
||||||
"home.recentProjects": "最近项目",
|
"home.recentProjects": "最近项目",
|
||||||
"home.empty.title": "没有最近项目",
|
"home.empty.title": "没有最近项目",
|
||||||
"home.empty.description": "通过打开本地项目开始使用",
|
"home.empty.description": "通过打开本地项目开始使用",
|
||||||
@@ -365,6 +376,9 @@ export const dict = {
|
|||||||
"session.share.copy.copied": "已复制",
|
"session.share.copy.copied": "已复制",
|
||||||
"session.share.copy.copyLink": "复制链接",
|
"session.share.copy.copyLink": "复制链接",
|
||||||
|
|
||||||
|
"lsp.tooltip.none": "没有 LSP 服务器",
|
||||||
|
"lsp.label.connected": "{{count}} LSP",
|
||||||
|
|
||||||
"prompt.loading": "正在加载提示...",
|
"prompt.loading": "正在加载提示...",
|
||||||
"terminal.loading": "正在加载终端...",
|
"terminal.loading": "正在加载终端...",
|
||||||
|
|
||||||
|
|||||||
@@ -597,7 +597,10 @@ export default function Page() {
|
|||||||
// Restore the prompt from the reverted message
|
// Restore the prompt from the reverted message
|
||||||
const parts = sync.data.part[message.id]
|
const parts = sync.data.part[message.id]
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
const restored = extractPromptFromParts(parts, {
|
||||||
|
directory: sdk.directory,
|
||||||
|
attachmentName: language.t("common.attachment"),
|
||||||
|
})
|
||||||
prompt.set(restored)
|
prompt.set(restored)
|
||||||
}
|
}
|
||||||
// Navigate to the message before the reverted one (which will be the new last visible message)
|
// Navigate to the message before the reverted one (which will be the new last visible message)
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ function textPartValue(parts: Part[]) {
|
|||||||
* Extract prompt content from message parts for restoring into the prompt input.
|
* Extract prompt content from message parts for restoring into the prompt input.
|
||||||
* This is used by undo to restore the original user prompt.
|
* This is used by undo to restore the original user prompt.
|
||||||
*/
|
*/
|
||||||
export function extractPromptFromParts(parts: Part[], opts?: { directory?: string }): Prompt {
|
export function extractPromptFromParts(parts: Part[], opts?: { directory?: string; attachmentName?: string }): Prompt {
|
||||||
const textPart = textPartValue(parts)
|
const textPart = textPartValue(parts)
|
||||||
const text = textPart?.text ?? ""
|
const text = textPart?.text ?? ""
|
||||||
const directory = opts?.directory
|
const directory = opts?.directory
|
||||||
|
const attachmentName = opts?.attachmentName ?? "attachment"
|
||||||
|
|
||||||
const toRelative = (path: string) => {
|
const toRelative = (path: string) => {
|
||||||
if (!directory) return path
|
if (!directory) return path
|
||||||
@@ -104,7 +105,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin
|
|||||||
images.push({
|
images.push({
|
||||||
type: "image",
|
type: "image",
|
||||||
id: filePart.id,
|
id: filePart.id,
|
||||||
filename: filePart.filename ?? "attachment",
|
filename: filePart.filename ?? attachmentName,
|
||||||
mime: filePart.mime,
|
mime: filePart.mime,
|
||||||
dataUrl: filePart.url,
|
dataUrl: filePart.url,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ This report documents the remaining user-facing strings in `packages/app/src` th
|
|||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
- The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
|
- The app uses `useLanguage().t("...")` with dictionaries in `packages/app/src/i18n/en.ts` and `packages/app/src/i18n/zh.ts`.
|
||||||
- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/app/src/components/dialog-connect-provider.tsx`, `packages/app/src/components/session/session-header.tsx`, `packages/app/src/pages/error.tsx`, `packages/app/src/components/session/session-new-view.tsx`, `packages/app/src/components/session-context-usage.tsx`, `packages/app/src/components/session/session-context-tab.tsx` (plus new keys added in both dictionaries).
|
- Recent progress (already translated): `packages/app/src/pages/home.tsx`, `packages/app/src/pages/layout.tsx`, `packages/app/src/pages/session.tsx`, `packages/app/src/components/prompt-input.tsx`, `packages/app/src/components/dialog-connect-provider.tsx`, `packages/app/src/components/session/session-header.tsx`, `packages/app/src/pages/error.tsx`, `packages/app/src/components/session/session-new-view.tsx`, `packages/app/src/components/session-context-usage.tsx`, `packages/app/src/components/session/session-context-tab.tsx`, `packages/app/src/components/session-lsp-indicator.tsx`, `packages/app/src/components/session/session-sortable-tab.tsx`, `packages/app/src/components/titlebar.tsx`, `packages/app/src/components/dialog-select-model.tsx`, `packages/app/src/context/notification.tsx`, `packages/app/src/context/global-sync.tsx`, `packages/app/src/context/file.tsx`, `packages/app/src/context/local.tsx`, `packages/app/src/utils/prompt.ts` (plus new keys added in both dictionaries).
|
||||||
- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (362 keys each; no missing or extra keys).
|
- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (371 keys each; no missing or extra keys).
|
||||||
|
|
||||||
## Methodology
|
## Methodology
|
||||||
|
|
||||||
@@ -105,36 +105,33 @@ Completed (2026-01-20):
|
|||||||
|
|
||||||
File: `packages/app/src/components/session-lsp-indicator.tsx`
|
File: `packages/app/src/components/session-lsp-indicator.tsx`
|
||||||
|
|
||||||
**Untranslated strings**
|
Completed (2026-01-20):
|
||||||
- Tooltip: "No LSP servers"
|
|
||||||
- Label suffix: "{connected} LSP" (acronym likely fine; the framing text should be localized)
|
- Localized tooltip/label framing via `lsp.*` keys (kept the acronym itself).
|
||||||
|
|
||||||
### 9) Session Tab Close Tooltip
|
### 9) Session Tab Close Tooltip
|
||||||
|
|
||||||
File: `packages/app/src/components/session/session-sortable-tab.tsx`
|
File: `packages/app/src/components/session/session-sortable-tab.tsx`
|
||||||
|
|
||||||
**Untranslated strings**
|
Completed (2026-01-20):
|
||||||
- Tooltip: "Close tab"
|
|
||||||
|
|
||||||
Note: you already have `common.closeTab`.
|
- Reused `common.closeTab` for the close tooltip.
|
||||||
|
|
||||||
### 10) Titlebar Tooltip
|
### 10) Titlebar Tooltip
|
||||||
|
|
||||||
File: `packages/app/src/components/titlebar.tsx`
|
File: `packages/app/src/components/titlebar.tsx`
|
||||||
|
|
||||||
**Untranslated strings**
|
Completed (2026-01-20):
|
||||||
- "Toggle sidebar"
|
|
||||||
|
|
||||||
Note: can likely reuse `command.sidebar.toggle`.
|
- Reused `command.sidebar.toggle` for the tooltip title.
|
||||||
|
|
||||||
### 11) Model Selection "Recent" Group
|
### 11) Model Selection "Recent" Group
|
||||||
|
|
||||||
File: `packages/app/src/components/dialog-select-model.tsx`
|
File: `packages/app/src/components/dialog-select-model.tsx`
|
||||||
|
|
||||||
**Untranslated / fragile string**
|
Completed (2026-01-20):
|
||||||
- Hardcoded category name comparisons against "Recent".
|
|
||||||
|
|
||||||
Recommendation: introduce a key (e.g. `model.group.recent`) and ensure both the grouping label and the comparator use the localized label, or replace the comparator with an internal enum.
|
- Removed the unused hardcoded "Recent" group comparisons to avoid locale-coupled sorting.
|
||||||
|
|
||||||
### 12) Select Server Dialog Placeholder (Optional)
|
### 12) Select Server Dialog Placeholder (Optional)
|
||||||
|
|
||||||
@@ -150,22 +147,18 @@ This is an example URL; you may choose to keep it as-is even after translating s
|
|||||||
|
|
||||||
File: `packages/app/src/context/notification.tsx`
|
File: `packages/app/src/context/notification.tsx`
|
||||||
|
|
||||||
**Untranslated notification titles / fallback copy**
|
Completed (2026-01-20):
|
||||||
- "Response ready"
|
|
||||||
- "Session error"
|
|
||||||
- Fallback description: "An error occurred"
|
|
||||||
|
|
||||||
Recommendation: `notification.session.*` namespace (separate from the permission/question notifications already added).
|
- Localized OS notification titles/fallback copy via `notification.session.*` keys.
|
||||||
|
|
||||||
### 14) Global Sync (Bootstrap Errors + Toast)
|
### 14) Global Sync (Bootstrap Errors + Toast)
|
||||||
|
|
||||||
File: `packages/app/src/context/global-sync.tsx`
|
File: `packages/app/src/context/global-sync.tsx`
|
||||||
|
|
||||||
**Untranslated toast title**
|
Completed (2026-01-20):
|
||||||
- `Failed to load sessions for ${project}`
|
|
||||||
|
|
||||||
**Untranslated fatal init error**
|
- Localized the sessions list failure toast via `toast.session.listFailed.title`.
|
||||||
- `Could not connect to server. Is there a server running at \`${globalSDK.url}\`?`
|
- Localized the bootstrap connection error via `error.globalSync.connectFailed`.
|
||||||
|
|
||||||
### 15) File Load Failure Toast (Duplicate)
|
### 15) File Load Failure Toast (Duplicate)
|
||||||
|
|
||||||
@@ -173,10 +166,9 @@ Files:
|
|||||||
- `packages/app/src/context/file.tsx`
|
- `packages/app/src/context/file.tsx`
|
||||||
- `packages/app/src/context/local.tsx`
|
- `packages/app/src/context/local.tsx`
|
||||||
|
|
||||||
**Untranslated toast title**
|
Completed (2026-01-20):
|
||||||
- "Failed to load file"
|
|
||||||
|
|
||||||
Recommendation: create one shared key (e.g. `toast.file.loadFailed.title`) and reuse it in both contexts.
|
- Introduced `toast.file.loadFailed.title` and reused it in both contexts.
|
||||||
|
|
||||||
### 16) Terminal Naming (Tricky)
|
### 16) Terminal Naming (Tricky)
|
||||||
|
|
||||||
@@ -195,9 +187,9 @@ Recommendation:
|
|||||||
|
|
||||||
File: `packages/app/src/utils/prompt.ts`
|
File: `packages/app/src/utils/prompt.ts`
|
||||||
|
|
||||||
- Default filename fallback: "attachment"
|
Completed (2026-01-20):
|
||||||
|
|
||||||
Recommendation: `common.attachment` or `prompt.attachment.defaultFilename`.
|
- Added `common.attachment` and plumbed it into `extractPromptFromParts(...)` as `opts.attachmentName`.
|
||||||
|
|
||||||
### 18) Dev-only Root Mount Error
|
### 18) Dev-only Root Mount Error
|
||||||
|
|
||||||
@@ -209,18 +201,9 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
|
|||||||
|
|
||||||
## Prioritized Implementation Plan
|
## Prioritized Implementation Plan
|
||||||
|
|
||||||
1. Small stragglers:
|
1. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
|
||||||
- `packages/app/src/components/session-lsp-indicator.tsx`
|
2. Optional: `packages/app/src/components/dialog-select-server.tsx` placeholder example URL.
|
||||||
- `packages/app/src/components/session/session-sortable-tab.tsx`
|
3. Optional: `packages/app/src/entry.tsx` dev-only root mount error.
|
||||||
- `packages/app/src/components/titlebar.tsx`
|
|
||||||
- `packages/app/src/components/dialog-select-model.tsx`
|
|
||||||
- `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
|
|
||||||
2. Context modules:
|
|
||||||
- `packages/app/src/context/notification.tsx`
|
|
||||||
- `packages/app/src/context/global-sync.tsx`
|
|
||||||
- `packages/app/src/context/file.tsx` + `packages/app/src/context/local.tsx`
|
|
||||||
- `packages/app/src/utils/prompt.ts`
|
|
||||||
3. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
|
|
||||||
|
|
||||||
## Suggested Key Naming Conventions
|
## Suggested Key Naming Conventions
|
||||||
|
|
||||||
@@ -243,19 +226,10 @@ Pages:
|
|||||||
- (none)
|
- (none)
|
||||||
|
|
||||||
Components:
|
Components:
|
||||||
- `packages/app/src/components/session-lsp-indicator.tsx`
|
|
||||||
- `packages/app/src/components/session/session-sortable-tab.tsx`
|
|
||||||
- `packages/app/src/components/titlebar.tsx`
|
|
||||||
- `packages/app/src/components/dialog-select-model.tsx`
|
|
||||||
- `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
|
- `packages/app/src/components/dialog-select-server.tsx` (optional URL placeholder)
|
||||||
|
|
||||||
Context:
|
Context:
|
||||||
- `packages/app/src/context/notification.tsx`
|
|
||||||
- `packages/app/src/context/global-sync.tsx`
|
|
||||||
- `packages/app/src/context/file.tsx`
|
|
||||||
- `packages/app/src/context/local.tsx`
|
|
||||||
- `packages/app/src/context/terminal.tsx` (naming)
|
- `packages/app/src/context/terminal.tsx` (naming)
|
||||||
|
|
||||||
Utils:
|
Utils:
|
||||||
- `packages/app/src/utils/prompt.ts`
|
|
||||||
- `packages/app/src/entry.tsx` (dev-only)
|
- `packages/app/src/entry.tsx` (dev-only)
|
||||||
|
|||||||
Reference in New Issue
Block a user