chore: generate

This commit is contained in:
GitHub Action 2026-01-20 23:58:59 +00:00
parent 233d003b49
commit bb8bf32abe
20 changed files with 161 additions and 123 deletions

View File

@ -236,8 +236,12 @@ export function DialogConnectProvider(props: { provider: string }) {
<Switch> <Switch>
<Match when={provider().id === "opencode"}> <Match when={provider().id === "opencode"}>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line1")}</div> <div class="text-14-regular text-text-base">
<div class="text-14-regular text-text-base">{language.t("provider.connect.opencodeZen.line2")}</div> {language.t("provider.connect.opencodeZen.line1")}
</div>
<div class="text-14-regular text-text-base">
{language.t("provider.connect.opencodeZen.line2")}
</div>
<div class="text-14-regular text-text-base"> <div class="text-14-regular text-text-base">
{language.t("provider.connect.opencodeZen.visit.prefix")} {language.t("provider.connect.opencodeZen.visit.prefix")}
<Link href="https://opencode.ai/zen" tabIndex={-1}> <Link href="https://opencode.ai/zen" tabIndex={-1}>
@ -317,7 +321,9 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base"> <div class="text-14-regular text-text-base">
{language.t("provider.connect.oauth.code.visit.prefix")} {language.t("provider.connect.oauth.code.visit.prefix")}
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.code.visit.link")}</Link> <Link href={store.authorization!.url}>
{language.t("provider.connect.oauth.code.visit.link")}
</Link>
{language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })} {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
</div> </div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4"> <form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
@ -367,7 +373,9 @@ export function DialogConnectProvider(props: { provider: string }) {
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base"> <div class="text-14-regular text-text-base">
{language.t("provider.connect.oauth.auto.visit.prefix")} {language.t("provider.connect.oauth.auto.visit.prefix")}
<Link href={store.authorization!.url}>{language.t("provider.connect.oauth.auto.visit.link")}</Link> <Link href={store.authorization!.url}>
{language.t("provider.connect.oauth.auto.visit.link")}
</Link>
{language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })} {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
</div> </div>
<TextField <TextField

View File

@ -62,9 +62,9 @@ export const DialogSelectModelUnpaid: Component = () => {
</div> </div>
<div class="px-1.5 pb-1.5"> <div class="px-1.5 pb-1.5">
<div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base"> <div class="w-full rounded-sm border border-border-weak-base bg-surface-raised-base">
<div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4"> <div class="w-full flex flex-col items-start gap-4 px-1.5 pt-4 pb-4">
<div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div> <div class="px-2 text-14-medium text-text-base">{language.t("dialog.model.unpaid.addMore.title")}</div>
<div class="w-full"> <div class="w-full">
<List <List
class="w-full px-0" class="w-full px-0"
key={(x) => x?.id} key={(x) => x?.id}

View File

@ -200,9 +200,7 @@ export function DialogSelectServer() {
<div class="mt-6 px-3 flex flex-col gap-1.5"> <div class="mt-6 px-3 flex flex-col gap-1.5">
<div class="px-3"> <div class="px-3">
<h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3> <h3 class="text-14-regular text-text-weak">{language.t("dialog.server.default.title")}</h3>
<p class="text-12-regular text-text-weak mt-1"> <p class="text-12-regular text-text-weak mt-1">{language.t("dialog.server.default.description")}</p>
{language.t("dialog.server.default.description")}
</p>
</div> </div>
<div class="flex items-center gap-2 px-3 py-2"> <div class="flex items-center gap-2 px-3 py-2">
<Show <Show
@ -210,7 +208,9 @@ export function DialogSelectServer() {
fallback={ fallback={
<Show <Show
when={server.url} when={server.url}
fallback={<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>} fallback={
<span class="text-14-regular text-text-weak">{language.t("dialog.server.default.none")}</span>
}
> >
<Button <Button
variant="secondary" variant="secondary"

View File

@ -261,7 +261,10 @@ export function SessionContextTab(props: SessionContextTabProps) {
value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`, value: `${number(c?.cacheRead)} / ${number(c?.cacheWrite)}`,
}, },
{ label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) }, { label: language.t("context.stats.userMessages"), value: count.user.toLocaleString(language.locale()) },
{ label: language.t("context.stats.assistantMessages"), value: count.assistant.toLocaleString(language.locale()) }, {
label: language.t("context.stats.assistantMessages"),
value: count.assistant.toLocaleString(language.locale()),
},
{ label: language.t("context.stats.totalCost"), value: cost() }, { label: language.t("context.stats.totalCost"), value: cost() },
{ label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) }, { label: language.t("context.stats.sessionCreated"), value: time(props.info()?.time.created) },
{ label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) }, { label: language.t("context.stats.lastActivity"), value: time(c?.message.time.created) },
@ -402,9 +405,7 @@ export function SessionContextTab(props: SessionContextTabProps) {
)} )}
</For> </For>
</div> </div>
<div class="hidden text-11-regular text-text-weaker"> <div class="hidden text-11-regular text-text-weaker">{language.t("context.breakdown.note")}</div>
{language.t("context.breakdown.note")}
</div>
</div> </div>
</Show> </Show>

View File

@ -183,7 +183,10 @@ export function SessionHeader() {
}} }}
aria-hidden={!showReview()} aria-hidden={!showReview()}
> >
<TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}> <TooltipKeybind
title={language.t("command.review.toggle")}
keybind={command.keybind("review.toggle")}
>
<Button <Button
variant="ghost" variant="ghost"
class="group/review-toggle size-6 p-0" class="group/review-toggle size-6 p-0"

View File

@ -15,13 +15,11 @@ export const SettingsGeneral: Component = () => {
Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })), Object.entries(theme.themes()).map(([id, def]) => ({ id, name: def.name ?? id })),
) )
const colorSchemeOptions = createMemo( const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
(): { value: ColorScheme; label: string }[] => [ { value: "system", label: language.t("theme.scheme.system") },
{ value: "system", label: language.t("theme.scheme.system") }, { value: "light", label: language.t("theme.scheme.light") },
{ value: "light", label: language.t("theme.scheme.light") }, { value: "dark", label: language.t("theme.scheme.dark") },
{ value: "dark", label: language.t("theme.scheme.dark") }, ])
],
)
const languageOptions = createMemo(() => const languageOptions = createMemo(() =>
language.locales.map((locale) => ({ language.locales.map((locale) => ({
@ -107,7 +105,7 @@ export const SettingsGeneral: Component = () => {
title={language.t("settings.general.row.theme.title")} title={language.t("settings.general.row.theme.title")}
description={ description={
<> <>
{language.t("settings.general.row.theme.description")} {" "} {language.t("settings.general.row.theme.description")}{" "}
<a href="#" class="text-text-interactive-base"> <a href="#" class="text-text-interactive-base">
{language.t("common.learnMore")} {language.t("common.learnMore")}
</a> </a>

View File

@ -658,10 +658,7 @@ function createGlobalSync() {
.then((x) => x.data) .then((x) => x.data)
.catch(() => undefined) .catch(() => undefined)
if (!health?.healthy) { if (!health?.healthy) {
setGlobalStore( setGlobalStore("error", new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })))
"error",
new Error(language.t("error.globalSync.connectFailed", { url: globalSDK.url })),
)
return return
} }

View File

@ -96,7 +96,11 @@ 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(language.t("notification.session.responseReady.title"), session?.title ?? sessionID, href) void platform.notify(
language.t("notification.session.responseReady.title"),
session?.title ?? sessionID,
href,
)
} }
break break
} }
@ -117,7 +121,8 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
error, error,
}) })
const description = const description =
session?.title ?? (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) 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(language.t("notification.session.error.title"), description, href) void platform.notify(language.t("notification.session.error.title"), description, href)

View File

@ -19,7 +19,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
})() })()
const key = "error.dev.rootNotFound" as const const key = "error.dev.rootNotFound" as const
const message = locale === "zh" ? zh[key] ?? en[key] : en[key] const message = locale === "zh" ? (zh[key] ?? en[key]) : en[key]
throw new Error(message) throw new Error(message)
} }

View File

@ -144,7 +144,7 @@ export const dict = {
"common.attachment": "attachment", "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}}"',
"prompt.mode.shell": "Shell", "prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "esc to exit", "prompt.mode.shell.exit": "esc to exit",
@ -219,7 +219,8 @@ export const dict = {
"dialog.server.add.checking": "Checking...", "dialog.server.add.checking": "Checking...",
"dialog.server.add.button": "Add", "dialog.server.add.button": "Add",
"dialog.server.default.title": "Default server", "dialog.server.default.title": "Default server",
"dialog.server.default.description": "Connect to this server on app launch instead of starting a local server. Requires restart.", "dialog.server.default.description":
"Connect to this server on app launch instead of starting a local server. Requires restart.",
"dialog.server.default.none": "No server selected", "dialog.server.default.none": "No server selected",
"dialog.server.default.set": "Set current server as default", "dialog.server.default.set": "Set current server as default",
"dialog.server.default.clear": "Clear", "dialog.server.default.clear": "Clear",
@ -233,7 +234,7 @@ export const dict = {
"dialog.project.edit.color": "Color", "dialog.project.edit.color": "Color",
"context.breakdown.title": "Context Breakdown", "context.breakdown.title": "Context Breakdown",
"context.breakdown.note": "Approximate breakdown of input tokens. \"Other\" includes tool definitions and overhead.", "context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
"context.breakdown.system": "System", "context.breakdown.system": "System",
"context.breakdown.user": "User", "context.breakdown.user": "User",
"context.breakdown.assistant": "Assistant", "context.breakdown.assistant": "Assistant",
@ -327,14 +328,14 @@ export const dict = {
"error.chain.didYouMean": "Did you mean: {{suggestions}}", "error.chain.didYouMean": "Did you mean: {{suggestions}}",
"error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}", "error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}",
"error.chain.checkConfig": "Check your config (opencode.json) provider/model names", "error.chain.checkConfig": "Check your config (opencode.json) provider/model names",
"error.chain.mcpFailed": "error.chain.mcpFailed": 'MCP server "{{name}}" failed. Note, OpenCode does not support MCP authentication yet.',
"MCP server \"{{name}}\" failed. Note, OpenCode does not support MCP authentication yet.",
"error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}", "error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}",
"error.chain.providerInitFailed": "Failed to initialize provider \"{{provider}}\". Check credentials and configuration.", "error.chain.providerInitFailed":
'Failed to initialize provider "{{provider}}". Check credentials and configuration.',
"error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)", "error.chain.configJsonInvalid": "Config file at {{path}} is not valid JSON(C)",
"error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}", "error.chain.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}",
"error.chain.configDirectoryTypo": "error.chain.configDirectoryTypo":
"Directory \"{{dir}}\" in {{path}} is not valid. Rename the directory to \"{{suggestion}}\" or remove it. This is a common typo.", 'Directory "{{dir}}" in {{path}} is not valid. Rename the directory to "{{suggestion}}" or remove it. This is a common typo.',
"error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}", "error.chain.configFrontmatterError": "Failed to parse frontmatter in {{path}}:\n{{message}}",
"error.chain.configInvalid": "Config file at {{path}} is invalid", "error.chain.configInvalid": "Config file at {{path}} is invalid",
"error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}", "error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}",
@ -374,8 +375,10 @@ export const dict = {
"session.header.search.placeholder": "Search {{project}}", "session.header.search.placeholder": "Search {{project}}",
"session.share.popover.title": "Publish on web", "session.share.popover.title": "Publish on web",
"session.share.popover.description.shared": "This session is public on the web. It is accessible to anyone with the link.", "session.share.popover.description.shared":
"session.share.popover.description.unshared": "Share session publicly on the web. It will be accessible to anyone with the link.", "This session is public on the web. It is accessible to anyone with the link.",
"session.share.popover.description.unshared":
"Share session publicly on the web. It will be accessible to anyone with the link.",
"session.share.action.share": "Share", "session.share.action.share": "Share",
"session.share.action.publish": "Publish", "session.share.action.publish": "Publish",
"session.share.action.publishing": "Publishing...", "session.share.action.publishing": "Publishing...",
@ -433,7 +436,8 @@ export const dict = {
"settings.general.row.font.description": "Customise the mono font used in code blocks", "settings.general.row.font.description": "Customise the mono font used in code blocks",
"settings.general.notifications.agent.title": "Agent", "settings.general.notifications.agent.title": "Agent",
"settings.general.notifications.agent.description": "Show system notification when the agent is complete or needs attention", "settings.general.notifications.agent.description":
"Show system notification when the agent is complete or needs attention",
"settings.general.notifications.permissions.title": "Permissions", "settings.general.notifications.permissions.title": "Permissions",
"settings.general.notifications.permissions.description": "Show system notification when a permission is required", "settings.general.notifications.permissions.description": "Show system notification when a permission is required",
"settings.general.notifications.errors.title": "Errors", "settings.general.notifications.errors.title": "Errors",
@ -530,10 +534,10 @@ export const dict = {
"workspace.status.clean": "No unmerged changes detected.", "workspace.status.clean": "No unmerged changes detected.",
"workspace.status.dirty": "Unmerged changes detected in this workspace.", "workspace.status.dirty": "Unmerged changes detected in this workspace.",
"workspace.delete.title": "Delete workspace", "workspace.delete.title": "Delete workspace",
"workspace.delete.confirm": "Delete workspace \"{{name}}\"?", "workspace.delete.confirm": 'Delete workspace "{{name}}"?',
"workspace.delete.button": "Delete workspace", "workspace.delete.button": "Delete workspace",
"workspace.reset.title": "Reset workspace", "workspace.reset.title": "Reset workspace",
"workspace.reset.confirm": "Reset workspace \"{{name}}\"?", "workspace.reset.confirm": 'Reset workspace "{{name}}"?',
"workspace.reset.button": "Reset workspace", "workspace.reset.button": "Reset workspace",
"workspace.reset.archived.none": "No active sessions will be archived.", "workspace.reset.archived.none": "No active sessions will be archived.",
"workspace.reset.archived.one": "1 session will be archived.", "workspace.reset.archived.one": "1 session will be archived.",

View File

@ -108,7 +108,8 @@ export const dict = {
"provider.connect.status.inProgress": "正在授权...", "provider.connect.status.inProgress": "正在授权...",
"provider.connect.status.waiting": "等待授权...", "provider.connect.status.waiting": "等待授权...",
"provider.connect.status.failed": "授权失败: {{error}}", "provider.connect.status.failed": "授权失败: {{error}}",
"provider.connect.apiKey.description": "输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。", "provider.connect.apiKey.description":
"输入你的 {{provider}} API 密钥以连接帐户,并在 OpenCode 中使用 {{provider}} 模型。",
"provider.connect.apiKey.label": "{{provider}} API 密钥", "provider.connect.apiKey.label": "{{provider}} API 密钥",
"provider.connect.apiKey.placeholder": "API 密钥", "provider.connect.apiKey.placeholder": "API 密钥",
"provider.connect.apiKey.required": "API 密钥为必填项", "provider.connect.apiKey.required": "API 密钥为必填项",
@ -143,7 +144,7 @@ export const dict = {
"common.attachment": "附件", "common.attachment": "附件",
"prompt.placeholder.shell": "输入 shell 命令...", "prompt.placeholder.shell": "输入 shell 命令...",
"prompt.placeholder.normal": "随便问点什么... \"{{example}}\"", "prompt.placeholder.normal": '随便问点什么... "{{example}}"',
"prompt.mode.shell": "Shell", "prompt.mode.shell": "Shell",
"prompt.mode.shell.exit": "按 esc 退出", "prompt.mode.shell.exit": "按 esc 退出",
@ -325,12 +326,13 @@ export const dict = {
"error.chain.didYouMean": "你是不是想输入: {{suggestions}}", "error.chain.didYouMean": "你是不是想输入: {{suggestions}}",
"error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}", "error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}",
"error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
"error.chain.mcpFailed": "MCP 服务器 \"{{name}}\" 启动失败。注意: OpenCode 暂不支持 MCP 认证。", "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意: OpenCode 暂不支持 MCP 认证。',
"error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}", "error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}",
"error.chain.providerInitFailed": "无法初始化提供商 \"{{provider}}\"。请检查凭据和配置。", "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。',
"error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
"error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}", "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}",
"error.chain.configDirectoryTypo": "{{path}} 中的目录 \"{{dir}}\" 无效。请将目录重命名为 \"{{suggestion}}\" 或移除它。这是一个常见拼写错误。", "error.chain.configDirectoryTypo":
'{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。',
"error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
"error.chain.configInvalid": "配置文件 {{path}} 无效", "error.chain.configInvalid": "配置文件 {{path}} 无效",
"error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}", "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}",
@ -526,10 +528,10 @@ export const dict = {
"workspace.status.clean": "未检测到未合并的更改。", "workspace.status.clean": "未检测到未合并的更改。",
"workspace.status.dirty": "检测到未合并的更改。", "workspace.status.dirty": "检测到未合并的更改。",
"workspace.delete.title": "删除工作区", "workspace.delete.title": "删除工作区",
"workspace.delete.confirm": "删除工作区 \"{{name}}\"?", "workspace.delete.confirm": '删除工作区 "{{name}}"?',
"workspace.delete.button": "删除工作区", "workspace.delete.button": "删除工作区",
"workspace.reset.title": "重置工作区", "workspace.reset.title": "重置工作区",
"workspace.reset.confirm": "重置工作区 \"{{name}}\"?", "workspace.reset.confirm": '重置工作区 "{{name}}"?',
"workspace.reset.button": "重置工作区", "workspace.reset.button": "重置工作区",
"workspace.reset.archived.none": "不会归档任何活跃会话。", "workspace.reset.archived.none": "不会归档任何活跃会话。",
"workspace.reset.archived.one": "将归档 1 个会话。", "workspace.reset.archived.one": "将归档 1 个会话。",

View File

@ -78,9 +78,10 @@ function formatInitError(error: InitError, t: Translator): string {
suggestions?: string[] suggestions?: string[]
} }
const suggestionsLine = Array.isArray(suggestions) && suggestions.length const suggestionsLine =
? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })] Array.isArray(suggestions) && suggestions.length
: [] ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })]
: []
return [ return [
t("error.chain.modelNotFound", { provider: providerID, model: modelID }), t("error.chain.modelNotFound", { provider: providerID, model: modelID }),
@ -253,7 +254,9 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
when={store.version} when={store.version}
fallback={ fallback={
<Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}> <Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
{store.checking ? language.t("error.page.action.checking") : language.t("error.page.action.checkUpdates")} {store.checking
? language.t("error.page.action.checking")
: language.t("error.page.action.checkUpdates")}
</Button> </Button>
} }
> >

View File

@ -76,13 +76,13 @@ export default function Home() {
</Button> </Button>
<Switch> <Switch>
<Match when={sync.data.project.length > 0}> <Match when={sync.data.project.length > 0}>
<div class="mt-20 w-full flex flex-col gap-4"> <div class="mt-20 w-full flex flex-col gap-4">
<div class="flex gap-2 items-center justify-between pl-3"> <div class="flex gap-2 items-center justify-between pl-3">
<div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div> <div class="text-14-medium text-text-strong">{language.t("home.recentProjects")}</div>
<Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}> <Button icon="folder-add-left" size="normal" class="pl-2 pr-3" onClick={chooseProject}>
{language.t("command.project.open")} {language.t("command.project.open")}
</Button> </Button>
</div> </div>
<ul class="flex flex-col gap-2"> <ul class="flex flex-col gap-2">
<For <For
each={sync.data.project each={sync.data.project

View File

@ -1909,9 +1909,9 @@ export default function Layout(props: ParentProps) {
trigger={trigger} trigger={trigger}
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<div class="-m-3 p-2 flex flex-col w-72"> <div class="-m-3 p-2 flex flex-col w-72">
<div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div> <div class="px-4 pt-2 pb-1 text-14-medium text-text-strong truncate">{displayName(props.project)}</div>
<div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div> <div class="px-4 pb-2 text-12-medium text-text-weak">{language.t("sidebar.project.recentSessions")}</div>
<div class="px-2 pb-2 flex flex-col gap-2"> <div class="px-2 pb-2 flex flex-col gap-2">
<Show <Show
when={workspaceEnabled()} when={workspaceEnabled()}
@ -2177,22 +2177,22 @@ export default function Layout(props: ParentProps) {
class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active" class="shrink-0 size-6 rounded-md opacity-0 group-hover/project:opacity-100 data-[expanded]:opacity-100 data-[expanded]:bg-surface-base-active"
/> />
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content class="mt-1"> <DropdownMenu.Content class="mt-1">
<DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}> <DropdownMenu.Item onSelect={() => dialog.show(() => <DialogEditProject project={p} />)}>
<DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("common.edit")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}> <DropdownMenu.Item onSelect={() => layout.sidebar.toggleWorkspaces(p.worktree)}>
<DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>
{layout.sidebar.workspaces(p.worktree)() {layout.sidebar.workspaces(p.worktree)()
? language.t("sidebar.workspaces.disable") ? language.t("sidebar.workspaces.disable")
: language.t("sidebar.workspaces.enable")} : language.t("sidebar.workspaces.enable")}
</DropdownMenu.ItemLabel> </DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator /> <DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => closeProject(p.worktree)}> <DropdownMenu.Item onSelect={() => closeProject(p.worktree)}>
<DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel> <DropdownMenu.ItemLabel>{language.t("common.close")}</DropdownMenu.ItemLabel>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</DropdownMenu> </DropdownMenu>
</div> </div>

View File

@ -673,14 +673,14 @@ export default function Page() {
}, },
...(sync.data.config.share !== "disabled" ...(sync.data.config.share !== "disabled"
? [ ? [
{ {
id: "session.share", id: "session.share",
title: language.t("command.session.share"), title: language.t("command.session.share"),
description: language.t("command.session.share.description"), description: language.t("command.session.share.description"),
category: language.t("command.category.session"), category: language.t("command.category.session"),
slash: "share", slash: "share",
disabled: !params.id || !!info()?.share?.url, disabled: !params.id || !!info()?.share?.url,
onSelect: async () => { onSelect: async () => {
if (!params.id) return if (!params.id) return
await sdk.client.session await sdk.client.session
.share({ sessionID: params.id }) .share({ sessionID: params.id })
@ -708,14 +708,14 @@ export default function Page() {
) )
}, },
}, },
{ {
id: "session.unshare", id: "session.unshare",
title: language.t("command.session.unshare"), title: language.t("command.session.unshare"),
description: language.t("command.session.unshare.description"), description: language.t("command.session.unshare.description"),
category: language.t("command.category.session"), category: language.t("command.category.session"),
slash: "unshare", slash: "unshare",
disabled: !params.id || !info()?.share?.url, disabled: !params.id || !info()?.share?.url,
onSelect: async () => { onSelect: async () => {
if (!params.id) return if (!params.id) return
await sdk.client.session await sdk.client.session
.unshare({ sessionID: params.id }) .unshare({ sessionID: params.id })
@ -1262,7 +1262,9 @@ export default function Page() {
<Show <Show
when={diffsReady()} when={diffsReady()}
fallback={ fallback={
<div class="px-4 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div> <div class="px-4 py-4 text-text-weak">
{language.t("session.review.loadingChanges")}
</div>
} }
> >
<SessionReviewTab <SessionReviewTab
@ -1283,13 +1285,15 @@ export default function Page() {
</Show> </Show>
</Match> </Match>
<Match when={true}> <Match when={true}>
<div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6"> <div class="h-full px-4 pb-30 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" /> <Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div> <div class="text-14-regular text-text-weak max-w-56">
{language.t("session.review.empty")}
</div> </div>
</Match> </div>
</Switch> </Match>
</div> </Switch>
</div>
} }
> >
<div class="relative w-full h-full min-w-0"> <div class="relative w-full h-full min-w-0">
@ -1502,11 +1506,11 @@ export default function Page() {
<Show when={diffs()}> <Show when={diffs()}>
<DiffChanges changes={diffs()} variant="bars" /> <DiffChanges changes={diffs()} variant="bars" />
</Show> </Show>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div> <div>{language.t("session.tab.review")}</div>
<Show when={info()?.summary?.files}> <Show when={info()?.summary?.files}>
<div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base"> <div class="text-12-medium text-text-strong h-4 px-2 flex flex-col items-center justify-center rounded-full bg-surface-base">
{info()?.summary?.files ?? 0} {info()?.summary?.files ?? 0}
</div> </div>
</Show> </Show>
</div> </div>
@ -1558,7 +1562,9 @@ export default function Page() {
<Show <Show
when={diffsReady()} when={diffsReady()}
fallback={ fallback={
<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div> <div class="px-6 py-4 text-text-weak">
{language.t("session.review.loadingChanges")}
</div>
} }
> >
<SessionReviewTab <SessionReviewTab
@ -1575,13 +1581,15 @@ export default function Page() {
</Show> </Show>
</Match> </Match>
<Match when={true}> <Match when={true}>
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6"> <div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" /> <Mark class="w-14 opacity-10" />
<div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.empty")}</div> <div class="text-14-regular text-text-weak max-w-56">
{language.t("session.review.empty")}
</div> </div>
</Match> </div>
</Switch> </Match>
</div> </Switch>
</div>
</Show> </Show>
</Tabs.Content> </Tabs.Content>
</Show> </Show>
@ -1871,13 +1879,15 @@ export default function Page() {
</div> </div>
)} )}
</For> </For>
<div class="flex-1" /> <div class="flex-1" />
<div class="text-text-weak pr-2">{language.t("common.loading")}...</div> <div class="text-text-weak pr-2">{language.t("common.loading")}...</div>
</div>
<div class="flex-1 flex items-center justify-center text-text-weak">
{language.t("terminal.loading")}
</div>
</div> </div>
<div class="flex-1 flex items-center justify-center text-text-weak">{language.t("terminal.loading")}</div> }
</div> >
}
>
<DragDropProvider <DragDropProvider
onDragStart={handleTerminalDragStart} onDragStart={handleTerminalDragStart}
onDragEnd={handleTerminalDragEnd} onDragEnd={handleTerminalDragEnd}

View File

@ -57,7 +57,7 @@ function detectLocale() {
function UiI18nBridge(props: ParentProps) { function UiI18nBridge(props: ParentProps) {
const locale = createMemo(() => detectLocale()) const locale = createMemo(() => detectLocale())
const t = (key: keyof typeof uiEn, params?: UiI18nParams) => { const t = (key: keyof typeof uiEn, params?: UiI18nParams) => {
const value = locale() === "zh" ? uiZh[key] ?? uiEn[key] : uiEn[key] const value = locale() === "zh" ? (uiZh[key] ?? uiEn[key]) : uiEn[key]
const text = value ?? String(key) const text = value ?? String(key)
return resolveTemplate(text, params) return resolveTemplate(text, params)
} }

View File

@ -228,9 +228,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
when={flat().length > 0} when={flat().length > 0}
fallback={ fallback={
<div data-slot="list-empty-state"> <div data-slot="list-empty-state">
<div data-slot="list-message"> <div data-slot="list-message">{emptyMessage()}</div>
{emptyMessage()}
</div>
</div> </div>
} }
> >

View File

@ -417,7 +417,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
<Icon name="chevron-down" size="small" /> <Icon name="chevron-down" size="small" />
</button> </button>
<div data-slot="user-message-copy-wrapper"> <div data-slot="user-message-copy-wrapper">
<Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top" gutter={8}> <Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
gutter={8}
>
<IconButton <IconButton
icon={copied() ? "check" : "copy"} icon={copied() ? "check" : "copy"}
variant="secondary" variant="secondary"

View File

@ -163,6 +163,7 @@ Completed (2026-01-20):
### 15) File Load Failure Toast (Duplicate) ### 15) File Load Failure Toast (Duplicate)
Files: 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`
@ -220,13 +221,17 @@ Also reuse existing command keys for tooltip titles whenever possible (e.g. `com
## Appendix: Remaining Files At-a-Glance ## Appendix: Remaining Files At-a-Glance
Pages: Pages:
- (none) - (none)
Components: Components:
- (none) - (none)
Context: Context:
- (none) - (none)
Utils: Utils:
- (none) - (none)

View File

@ -27,7 +27,7 @@ Why this is the best long-term shape:
### Proposed Architecture ### Proposed Architecture
1) **UI provides an i18n context (no persistence)** 1. **UI provides an i18n context (no persistence)**
- Add `packages/ui/src/context/i18n.tsx`: - Add `packages/ui/src/context/i18n.tsx`:
- Exports `I18nProvider` and `useI18n()`. - Exports `I18nProvider` and `useI18n()`.
@ -36,14 +36,14 @@ Why this is the best long-term shape:
- `locale()` accessor for locale-sensitive formatting (Luxon/Intl). - `locale()` accessor for locale-sensitive formatting (Luxon/Intl).
- Context should have a safe default (English) so UI components can render even if a consumer forgets the provider. - Context should have a safe default (English) so UI components can render even if a consumer forgets the provider.
2) **UI owns UI strings (dictionaries live in UI)** 2. **UI owns UI strings (dictionaries live in UI)**
- Add `packages/ui/src/i18n/en.ts` and `packages/ui/src/i18n/zh.ts`. - Add `packages/ui/src/i18n/en.ts` and `packages/ui/src/i18n/zh.ts`.
- Export them from `@opencode-ai/ui` via `packages/ui/package.json` exports (e.g. `"./i18n/*": "./src/i18n/*.ts"`). - Export them from `@opencode-ai/ui` via `packages/ui/package.json` exports (e.g. `"./i18n/*": "./src/i18n/*.ts"`).
- Use a clear namespace prefix for all UI keys to avoid collisions: - Use a clear namespace prefix for all UI keys to avoid collisions:
- Recommended: `ui.*` (e.g. `ui.sessionReview.title`). - Recommended: `ui.*` (e.g. `ui.sessionReview.title`).
3) **Consumers merge dictionaries and provide `t`/`locale` once** 3. **Consumers merge dictionaries and provide `t`/`locale` once**
- `packages/app/`: - `packages/app/`:
- Keep `packages/app/src/context/language.tsx` as the source of truth for locale selection/persistence. - Keep `packages/app/src/context/language.tsx` as the source of truth for locale selection/persistence.