Adam 2 месяцев назад
Родитель
Сommit
7e8e4d9938

+ 24 - 12
packages/app/src/components/session/session-header.tsx

@@ -4,6 +4,7 @@ import { Portal } from "solid-js/web"
 import { useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
+import { useLanguage } from "@/context/language"
 // import { useServer } from "@/context/server"
 // import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { usePlatform } from "@/context/platform"
@@ -29,6 +30,7 @@ export function SessionHeader() {
   // const dialog = useDialog()
   const sync = useSync()
   const platform = usePlatform()
+  const language = useLanguage()
 
   const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
   const project = createMemo(() => {
@@ -138,7 +140,7 @@ export function SessionHeader() {
               <div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible">
                 <Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" />
                 <span class="flex-1 min-w-0 text-14-regular text-text-weak truncate h-4.5 flex items-center">
-                  Search {name()}
+                  {language.t("session.header.search.placeholder", { project: name() })}
                 </span>
               </div>
 
@@ -181,7 +183,7 @@ export function SessionHeader() {
                   }}
                   aria-hidden={!showReview()}
                 >
-                  <TooltipKeybind title="Toggle review" keybind={command.keybind("review.toggle")}>
+                  <TooltipKeybind title={language.t("command.review.toggle")} keybind={command.keybind("review.toggle")}>
                     <Button
                       variant="ghost"
                       class="group/review-toggle size-6 p-0"
@@ -209,7 +211,7 @@ export function SessionHeader() {
                 </div>
                 <TooltipKeybind
                   class="hidden md:block shrink-0"
-                  title="Toggle terminal"
+                  title={language.t("command.terminal.toggle")}
                   keybind={command.keybind("terminal.toggle")}
                 >
                   <Button
@@ -245,20 +247,20 @@ export function SessionHeader() {
                 aria-hidden={!showShare()}
               >
                 <Popover
-                  title="Publish on web"
+                  title={language.t("session.share.popover.title")}
                   description={
                     shareUrl()
-                      ? "This session is public on the web. It is accessible to anyone with the link."
-                      : "Share session publicly on the web. It will be accessible to anyone with the link."
+                      ? language.t("session.share.popover.description.shared")
+                      : language.t("session.share.popover.description.unshared")
                   }
                   trigger={
-                    <Tooltip class="shrink-0" value="Share session">
+                    <Tooltip class="shrink-0" value={language.t("command.session.share")}>
                       <Button
                         variant="secondary"
                         classList={{ "rounded-r-none": shareUrl() !== undefined }}
                         style={{ scale: 1 }}
                       >
-                        Share
+                        {language.t("session.share.action.share")}
                       </Button>
                     </Tooltip>
                   }
@@ -275,7 +277,9 @@ export function SessionHeader() {
                             onClick={shareSession}
                             disabled={state.share}
                           >
-                            {state.share ? "Publishing..." : "Publish"}
+                            {state.share
+                              ? language.t("session.share.action.publishing")
+                              : language.t("session.share.action.publish")}
                           </Button>
                         </div>
                       }
@@ -290,7 +294,9 @@ export function SessionHeader() {
                             onClick={unshareSession}
                             disabled={state.unshare}
                           >
-                            {state.unshare ? "Unpublishing..." : "Unpublish"}
+                            {state.unshare
+                              ? language.t("session.share.action.unpublishing")
+                              : language.t("session.share.action.unpublish")}
                           </Button>
                           <Button
                             size="large"
@@ -299,7 +305,7 @@ export function SessionHeader() {
                             onClick={viewShare}
                             disabled={state.unshare}
                           >
-                            View
+                            {language.t("session.share.action.view")}
                           </Button>
                         </div>
                       </div>
@@ -307,7 +313,13 @@ export function SessionHeader() {
                   </div>
                 </Popover>
                 <Show when={shareUrl()} fallback={<div class="size-6" aria-hidden="true" />}>
-                  <Tooltip value={state.copied ? "Copied" : "Copy link"} placement="top" gutter={8}>
+                  <Tooltip
+                    value={
+                      state.copied ? language.t("session.share.copy.copied") : language.t("session.share.copy.copyLink")
+                    }
+                    placement="top"
+                    gutter={8}
+                  >
                     <IconButton
                       icon={state.copied ? "check" : "copy"}
                       variant="secondary"

+ 46 - 0
packages/app/src/i18n/en.ts

@@ -289,6 +289,38 @@ export const dict = {
   "toast.update.action.installRestart": "Install and restart",
   "toast.update.action.notYet": "Not yet",
 
+  "error.page.title": "Something went wrong",
+  "error.page.description": "An error occurred while loading the application.",
+  "error.page.details.label": "Error Details",
+  "error.page.action.restart": "Restart",
+  "error.page.action.checking": "Checking...",
+  "error.page.action.checkUpdates": "Check for updates",
+  "error.page.action.updateTo": "Update to {{version}}",
+  "error.page.report.prefix": "Please report this error to the OpenCode team",
+  "error.page.report.discord": "on Discord",
+  "error.page.version": "Version: {{version}}",
+
+  "error.chain.unknown": "Unknown error",
+  "error.chain.causedBy": "Caused by:",
+  "error.chain.apiError": "API error",
+  "error.chain.status": "Status: {{status}}",
+  "error.chain.retryable": "Retryable: {{retryable}}",
+  "error.chain.responseBody": "Response body:\n{{body}}",
+  "error.chain.didYouMean": "Did you mean: {{suggestions}}",
+  "error.chain.modelNotFound": "Model not found: {{provider}}/{{model}}",
+  "error.chain.checkConfig": "Check your config (opencode.json) provider/model names",
+  "error.chain.mcpFailed":
+    "MCP server \"{{name}}\" failed. Note, OpenCode does not support MCP authentication yet.",
+  "error.chain.providerAuthFailed": "Provider authentication failed ({{provider}}): {{message}}",
+  "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.configJsonInvalidWithMessage": "Config file at {{path}} is not valid JSON(C): {{message}}",
+  "error.chain.configDirectoryTypo":
+    "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.configInvalid": "Config file at {{path}} is invalid",
+  "error.chain.configInvalidWithMessage": "Config file at {{path}} is invalid: {{message}}",
+
   "notification.permission.title": "Permission required",
   "notification.permission.description": "{{sessionTitle}} in {{projectName}} needs permission",
   "notification.question.title": "Question",
@@ -312,6 +344,20 @@ export const dict = {
 
   "session.context.addToContext": "Add {{selection}} to context",
 
+  "session.header.search.placeholder": "Search {{project}}",
+
+  "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.unshared": "Share session publicly on the web. It will be accessible to anyone with the link.",
+  "session.share.action.share": "Share",
+  "session.share.action.publish": "Publish",
+  "session.share.action.publishing": "Publishing...",
+  "session.share.action.unpublish": "Unpublish",
+  "session.share.action.unpublishing": "Unpublishing...",
+  "session.share.action.view": "View",
+  "session.share.copy.copied": "Copied",
+  "session.share.copy.copyLink": "Copy link",
+
   "prompt.loading": "Loading prompt...",
   "terminal.loading": "Loading terminal...",
 

+ 44 - 0
packages/app/src/i18n/zh.ts

@@ -288,6 +288,36 @@ export const dict = {
   "toast.update.action.installRestart": "安装并重启",
   "toast.update.action.notYet": "稍后",
 
+  "error.page.title": "出了点问题",
+  "error.page.description": "加载应用程序时发生错误。",
+  "error.page.details.label": "错误详情",
+  "error.page.action.restart": "重启",
+  "error.page.action.checking": "检查中...",
+  "error.page.action.checkUpdates": "检查更新",
+  "error.page.action.updateTo": "更新到 {{version}}",
+  "error.page.report.prefix": "请将此错误报告给 OpenCode 团队",
+  "error.page.report.discord": "在 Discord 上",
+  "error.page.version": "版本: {{version}}",
+
+  "error.chain.unknown": "未知错误",
+  "error.chain.causedBy": "原因:",
+  "error.chain.apiError": "API 错误",
+  "error.chain.status": "状态: {{status}}",
+  "error.chain.retryable": "可重试: {{retryable}}",
+  "error.chain.responseBody": "响应内容:\n{{body}}",
+  "error.chain.didYouMean": "你是不是想输入: {{suggestions}}",
+  "error.chain.modelNotFound": "未找到模型: {{provider}}/{{model}}",
+  "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称",
+  "error.chain.mcpFailed": "MCP 服务器 \"{{name}}\" 启动失败。注意: OpenCode 暂不支持 MCP 认证。",
+  "error.chain.providerAuthFailed": "提供商认证失败 ({{provider}}): {{message}}",
+  "error.chain.providerInitFailed": "无法初始化提供商 \"{{provider}}\"。请检查凭据和配置。",
+  "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)",
+  "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C): {{message}}",
+  "error.chain.configDirectoryTypo": "{{path}} 中的目录 \"{{dir}}\" 无效。请将目录重命名为 \"{{suggestion}}\" 或移除它。这是一个常见拼写错误。",
+  "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}",
+  "error.chain.configInvalid": "配置文件 {{path}} 无效",
+  "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效: {{message}}",
+
   "notification.permission.title": "需要权限",
   "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限",
   "notification.question.title": "问题",
@@ -311,6 +341,20 @@ export const dict = {
 
   "session.context.addToContext": "将 {{selection}} 添加到上下文",
 
+  "session.header.search.placeholder": "搜索 {{project}}",
+
+  "session.share.popover.title": "发布到网页",
+  "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
+  "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。",
+  "session.share.action.share": "分享",
+  "session.share.action.publish": "发布",
+  "session.share.action.publishing": "正在发布...",
+  "session.share.action.unpublish": "取消发布",
+  "session.share.action.unpublishing": "正在取消发布...",
+  "session.share.action.view": "查看",
+  "session.share.copy.copied": "已复制",
+  "session.share.copy.copyLink": "复制链接",
+
   "prompt.loading": "正在加载提示...",
   "terminal.loading": "正在加载终端...",
 

+ 66 - 38
packages/app/src/pages/error.tsx

@@ -4,6 +4,7 @@ import { Button } from "@opencode-ai/ui/button"
 import { Component, Show } from "solid-js"
 import { createStore } from "solid-js/store"
 import { usePlatform } from "@/context/platform"
+import { useLanguage } from "@/context/language"
 import { Icon } from "@opencode-ai/ui/icon"
 
 export type InitError = {
@@ -11,6 +12,8 @@ export type InitError = {
   data: Record<string, unknown>
 }
 
+type Translator = ReturnType<typeof useLanguage>["t"]
+
 function isInitError(error: unknown): error is InitError {
   return (
     typeof error === "object" &&
@@ -38,30 +41,32 @@ function safeJson(value: unknown): string {
   return json ?? String(value)
 }
 
-function formatInitError(error: InitError): string {
+function formatInitError(error: InitError, t: Translator): string {
   const data = error.data
   switch (error.name) {
-    case "MCPFailed":
-      return `MCP server "${data.name}" failed. Note, opencode does not support MCP authentication yet.`
+    case "MCPFailed": {
+      const name = typeof data.name === "string" ? data.name : ""
+      return t("error.chain.mcpFailed", { name })
+    }
     case "ProviderAuthError": {
       const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
       const message = typeof data.message === "string" ? data.message : safeJson(data.message)
-      return `Provider authentication failed (${providerID}): ${message}`
+      return t("error.chain.providerAuthFailed", { provider: providerID, message })
     }
     case "APIError": {
-      const message = typeof data.message === "string" ? data.message : "API error"
+      const message = typeof data.message === "string" ? data.message : t("error.chain.apiError")
       const lines: string[] = [message]
 
       if (typeof data.statusCode === "number") {
-        lines.push(`Status: ${data.statusCode}`)
+        lines.push(t("error.chain.status", { status: data.statusCode }))
       }
 
       if (typeof data.isRetryable === "boolean") {
-        lines.push(`Retryable: ${data.isRetryable}`)
+        lines.push(t("error.chain.retryable", { retryable: data.isRetryable }))
       }
 
       if (typeof data.responseBody === "string" && data.responseBody) {
-        lines.push(`Response body:\n${data.responseBody}`)
+        lines.push(t("error.chain.responseBody", { body: data.responseBody }))
       }
 
       return lines.join("\n")
@@ -72,24 +77,38 @@ function formatInitError(error: InitError): string {
         modelID: string
         suggestions?: string[]
       }
+
+      const suggestionsLine = Array.isArray(suggestions) && suggestions.length
+        ? [t("error.chain.didYouMean", { suggestions: suggestions.join(", ") })]
+        : []
+
       return [
-        `Model not found: ${providerID}/${modelID}`,
-        ...(Array.isArray(suggestions) && suggestions.length ? ["Did you mean: " + suggestions.join(", ")] : []),
-        `Check your config (opencode.json) provider/model names`,
+        t("error.chain.modelNotFound", { provider: providerID, model: modelID }),
+        ...suggestionsLine,
+        t("error.chain.checkConfig"),
       ].join("\n")
     }
     case "ProviderInitError": {
       const providerID = typeof data.providerID === "string" ? data.providerID : "unknown"
-      return `Failed to initialize provider "${providerID}". Check credentials and configuration.`
+      return t("error.chain.providerInitFailed", { provider: providerID })
     }
     case "ConfigJsonError": {
+      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
       const message = typeof data.message === "string" ? data.message : ""
-      return `Config file at ${data.path} is not valid JSON(C)` + (message ? `: ${message}` : "")
+      if (message) return t("error.chain.configJsonInvalidWithMessage", { path, message })
+      return t("error.chain.configJsonInvalid", { path })
+    }
+    case "ConfigDirectoryTypoError": {
+      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
+      const dir = typeof data.dir === "string" ? data.dir : safeJson(data.dir)
+      const suggestion = typeof data.suggestion === "string" ? data.suggestion : safeJson(data.suggestion)
+      return t("error.chain.configDirectoryTypo", { dir, path, suggestion })
+    }
+    case "ConfigFrontmatterError": {
+      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
+      const message = typeof data.message === "string" ? data.message : safeJson(data.message)
+      return t("error.chain.configFrontmatterError", { path, message })
     }
-    case "ConfigDirectoryTypoError":
-      return `Directory "${data.dir}" in ${data.path} is not valid. Rename the directory to "${data.suggestion}" or remove it. This is a common typo.`
-    case "ConfigFrontmatterError":
-      return `Failed to parse frontmatter in ${data.path}:\n${data.message}`
     case "ConfigInvalidError": {
       const issues = Array.isArray(data.issues)
         ? data.issues.map(
@@ -97,7 +116,13 @@ function formatInitError(error: InitError): string {
           )
         : []
       const message = typeof data.message === "string" ? data.message : ""
-      return [`Config file at ${data.path} is invalid` + (message ? `: ${message}` : ""), ...issues].join("\n")
+      const path = typeof data.path === "string" ? data.path : safeJson(data.path)
+
+      const line = message
+        ? t("error.chain.configInvalidWithMessage", { path, message })
+        : t("error.chain.configInvalid", { path })
+
+      return [line, ...issues].join("\n")
     }
     case "UnknownError":
       return typeof data.message === "string" ? data.message : safeJson(data)
@@ -107,20 +132,20 @@ function formatInitError(error: InitError): string {
   }
 }
 
-function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): string {
-  if (!error) return "Unknown error"
+function formatErrorChain(error: unknown, t: Translator, depth = 0, parentMessage?: string): string {
+  if (!error) return t("error.chain.unknown")
 
   if (isInitError(error)) {
-    const message = formatInitError(error)
+    const message = formatInitError(error, t)
     if (depth > 0 && parentMessage === message) return ""
-    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
     return indent + `${error.name}\n${message}`
   }
 
   if (error instanceof Error) {
     const isDuplicate = depth > 0 && parentMessage === error.message
     const parts: string[] = []
-    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
 
     const header = `${error.name}${error.message ? `: ${error.message}` : ""}`
     const stack = error.stack?.trim()
@@ -153,7 +178,7 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
     }
 
     if (error.cause) {
-      const causeResult = formatErrorChain(error.cause, depth + 1, error.message)
+      const causeResult = formatErrorChain(error.cause, t, depth + 1, error.message)
       if (causeResult) {
         parts.push(causeResult)
       }
@@ -164,16 +189,16 @@ function formatErrorChain(error: unknown, depth = 0, parentMessage?: string): st
 
   if (typeof error === "string") {
     if (depth > 0 && parentMessage === error) return ""
-    const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+    const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
     return indent + error
   }
 
-  const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+  const indent = depth > 0 ? `\n${"─".repeat(40)}\n${t("error.chain.causedBy")}\n` : ""
   return indent + safeJson(error)
 }
 
-function formatError(error: unknown): string {
-  return formatErrorChain(error, 0)
+function formatError(error: unknown, t: Translator): string {
+  return formatErrorChain(error, t, 0)
 }
 
 interface ErrorPageProps {
@@ -182,6 +207,7 @@ interface ErrorPageProps {
 
 export const ErrorPage: Component<ErrorPageProps> = (props) => {
   const platform = usePlatform()
+  const language = useLanguage()
   const [store, setStore] = createStore({
     checking: false,
     version: undefined as string | undefined,
@@ -206,51 +232,53 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
       <div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
         <Logo class="w-58.5 opacity-12 shrink-0" />
         <div class="flex flex-col items-center gap-2 text-center">
-          <h1 class="text-lg font-medium text-text-strong">Something went wrong</h1>
-          <p class="text-sm text-text-weak">An error occurred while loading the application.</p>
+          <h1 class="text-lg font-medium text-text-strong">{language.t("error.page.title")}</h1>
+          <p class="text-sm text-text-weak">{language.t("error.page.description")}</p>
         </div>
         <TextField
-          value={formatError(props.error)}
+          value={formatError(props.error, language.t)}
           readOnly
           copyable
           multiline
           class="max-h-96 w-full font-mono text-xs no-scrollbar"
-          label="Error Details"
+          label={language.t("error.page.details.label")}
           hideLabel
         />
         <div class="flex items-center gap-3">
           <Button size="large" onClick={platform.restart}>
-            Restart
+            {language.t("error.page.action.restart")}
           </Button>
           <Show when={platform.checkUpdate}>
             <Show
               when={store.version}
               fallback={
                 <Button size="large" variant="ghost" onClick={checkForUpdates} disabled={store.checking}>
-                  {store.checking ? "Checking..." : "Check for updates"}
+                  {store.checking ? language.t("error.page.action.checking") : language.t("error.page.action.checkUpdates")}
                 </Button>
               }
             >
               <Button size="large" onClick={installUpdate}>
-                Update to {store.version}
+                {language.t("error.page.action.updateTo", { version: store.version ?? "" })}
               </Button>
             </Show>
           </Show>
         </div>
         <div class="flex flex-col items-center gap-2">
           <div class="flex items-center justify-center gap-1">
-            Please report this error to the OpenCode team
+            {language.t("error.page.report.prefix")}
             <button
               type="button"
               class="flex items-center text-text-interactive-base gap-1"
               onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
             >
-              <div>on Discord</div>
+              <div>{language.t("error.page.report.discord")}</div>
               <Icon name="discord" class="text-text-interactive-base" />
             </button>
           </div>
           <Show when={platform.version}>
-            <p class="text-xs text-text-weak">Version: {platform.version}</p>
+            {(version) => (
+              <p class="text-xs text-text-weak">{language.t("error.page.version", { version: version() })}</p>
+            )}
           </Show>
         </div>
       </div>

+ 17 - 46
specs/06-app-i18n-audit.md

@@ -9,8 +9,8 @@ This report documents the remaining user-facing strings in `packages/app/src` th
 ## Current State
 
 - 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` (plus new keys added in both dictionaries).
-- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (314 keys each; no missing or extra keys).
+- 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` (plus new keys added in both dictionaries).
+- Dictionary parity check: `en.ts` and `zh.ts` currently contain the same key set (354 keys each; no missing or extra keys).
 
 ## Methodology
 
@@ -30,28 +30,11 @@ This report documents the remaining user-facing strings in `packages/app/src` th
 
 File: `packages/app/src/pages/error.tsx`
 
-This is the largest remaining untranslated surface and is user-visible during app failures.
-
-**Page UI copy** (headings/buttons/labels):
-- "Something went wrong"
-- "An error occurred while loading the application."
-- Text field label: "Error Details"
-- Buttons: "Restart", "Checking...", "Check for updates", "Update to {version}"
-- Reporting help: "Please report this error..." and the link caption "on Discord"
-- Version display prefix: "Version: {platform.version}"
-
-**Error chain / formatting strings** (shown inside the details field):
-- "Unknown error"
-- "Caused by:"
-- "Status:", "Retryable:", "Response body:"
-- Generic API fallback: "API error"
-- Suggestion prefix: "Did you mean: ..."
-
-**Recommendation:**
-- Translate all framing/UI labels and action buttons.
-- Decide whether to translate highly technical diagnostics. A good compromise is:
-  - Translate the labels ("Caused by", "Unknown error", "Status")
-  - Keep raw messages from servers/providers as-is
+Completed (2026-01-20):
+
+- Localized page UI copy via `error.page.*` keys (title, description, buttons, report text, version label).
+- Localized error chain framing and common init error templates via `error.chain.*` keys.
+- Kept raw server/provider error messages as-is when provided (only localizing labels and structure).
 
 ## Highest Priority: Components
 
@@ -81,20 +64,11 @@ Completed (2026-01-20):
 
 File: `packages/app/src/components/session/session-header.tsx`
 
-**Representative untranslated strings**
-- Search placeholder: "Search {projectName}"
-- Tooltips: "Toggle review", "Toggle terminal", "Share session"
-- Share/publish popover:
-  - "Publish on web"
-  - "This session is public on the web..."
-  - "Share session publicly on the web..."
-  - Button states: "Publishing..." / "Publish", "Unpublishing..." / "Unpublish"
-  - Buttons: "Share", "View"
-- Copy tooltip: "Copied" / "Copy link"
-
-**Recommendation:**
-- Most of these should become `session.share.*` keys.
-- Reuse command keys where appropriate (e.g. `command.review.toggle`, `command.terminal.toggle`) instead of introducing new duplicates.
+Completed (2026-01-20):
+
+- Localized search placeholder via `session.header.search.placeholder`.
+- Localized share/publish UI via `session.share.*` keys (popover title/description, button states, copy tooltip).
+- Reused existing command keys for toggle/share tooltips (`command.review.toggle`, `command.terminal.toggle`, `command.session.share`).
 
 ## Medium Priority: Components
 
@@ -237,11 +211,9 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
 
 ## Prioritized Implementation Plan
 
-1. `packages/app/src/components/session/session-header.tsx`
-2. `packages/app/src/pages/error.tsx`
-3. `packages/app/src/components/session/session-new-view.tsx`
-4. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`)
-5. Small stragglers:
+1. `packages/app/src/components/session/session-new-view.tsx`
+2. `packages/app/src/components/session-context-usage.tsx` + locale formatting improvements (also `packages/app/src/components/session/session-context-tab.tsx`)
+3. Small stragglers:
    - `packages/app/src/components/session-lsp-indicator.tsx`
    - `packages/app/src/components/session/session-sortable-tab.tsx`
    - `packages/app/src/components/titlebar.tsx`
@@ -250,7 +222,7 @@ This is only thrown in DEV and is more of a developer diagnostic. Optional to tr
    - `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`
-6. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
+4. Decide on the terminal naming approach (`packages/app/src/context/terminal.tsx`).
 
 ## Suggested Key Naming Conventions
 
@@ -270,10 +242,9 @@ Also reuse existing command keys for tooltip titles whenever possible (e.g. `com
 ## Appendix: Remaining Files At-a-Glance
 
 Pages:
-- `packages/app/src/pages/error.tsx`
+- (none)
 
 Components:
-- `packages/app/src/components/session/session-header.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` (formatting locale)