wip(app): i18n

This commit is contained in:
Adam
2026-01-20 10:43:43 -06:00
parent be493e8be0
commit 0f2e8ea2b4
15 changed files with 110 additions and 89 deletions

View File

@@ -44,15 +44,17 @@ export function AppBaseProviders(props: ParentProps) {
<MetaProvider> <MetaProvider>
<Font /> <Font />
<ThemeProvider> <ThemeProvider>
<ErrorBoundary fallback={(error) => <ErrorPage error={error} />}> <LanguageProvider>
<DialogProvider> <ErrorBoundary fallback={(error) => <ErrorPage error={error} />}>
<MarkedProvider> <DialogProvider>
<DiffComponentProvider component={Diff}> <MarkedProvider>
<CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider> <DiffComponentProvider component={Diff}>
</DiffComponentProvider> <CodeComponentProvider component={Code}>{props.children}</CodeComponentProvider>
</MarkedProvider> </DiffComponentProvider>
</DialogProvider> </MarkedProvider>
</ErrorBoundary> </DialogProvider>
</ErrorBoundary>
</LanguageProvider>
</ThemeProvider> </ThemeProvider>
</MetaProvider> </MetaProvider>
) )
@@ -85,17 +87,15 @@ export function AppInterface(props: { defaultUrl?: string }) {
<Router <Router
root={(props) => ( root={(props) => (
<SettingsProvider> <SettingsProvider>
<LanguageProvider> <PermissionProvider>
<PermissionProvider> <LayoutProvider>
<LayoutProvider> <NotificationProvider>
<NotificationProvider> <CommandProvider>
<CommandProvider> <Layout>{props.children}</Layout>
<Layout>{props.children}</Layout> </CommandProvider>
</CommandProvider> </NotificationProvider>
</NotificationProvider> </LayoutProvider>
</LayoutProvider> </PermissionProvider>
</PermissionProvider>
</LanguageProvider>
</SettingsProvider> </SettingsProvider>
)} )}
> >

View File

@@ -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()

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
} }

View File

@@ -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

View File

@@ -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,
}) })
}) })

View File

@@ -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
} }

View File

@@ -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,
}) })
}) })

View File

@@ -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
} }
} }

View File

@@ -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...",

View File

@@ -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": "正在加载终端...",

View File

@@ -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)

View File

@@ -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,
}) })

View File

@@ -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)