fix(i18n): 崩溃兜底页不再停留旧语言
CI / typecheck + test + build (windows) (push) Canceled after 0s
Deploy website to GitHub Pages / deploy (push) Canceled after 0s

ErrorBoundary / PanelErrorBoundary 是 class 组件(拥有错误生命周期),
不能用 hook 订阅 settings store;根边界还是 App 的父级,语言切换时
App 的重渲染传导不到它——崩溃兜底页因此停留在启动时的语言,恰好是
用户最需要读得懂的一屏。

修法:导出改为一层薄函数包装(订阅 language 传 prop),class 内部经
tFor(language, …) 翻译。错误边界行为不变(边界仍是 class、仍在
children 之上)。使用方(main.tsx / TerminalPanel)无需改动。

ROADMAP 已知待办补录两项 i18n 质量专项:zh-TW 用语统一(命令/指令、
口令/密碼 等)、ja 片假名长音符号统一(フォルダ/フォルダー)。
This commit is contained in:
Bill committed 2026-10-10 15:35:56 +08:00
1 parent b344af4691
commit 7c2044fdf4
2 files changed
+40 -9

No files matched your search

+2
View File
@@ -162,6 +162,8 @@
- [x] ~~布局塌陷~~(v0.2.1 修复:antd `<App>` 包裹层 `.ant-app` 无高度导致百分比高度链断裂)
- [x] ~~关闭标签页整窗崩溃~~(v0.2.1 修复:xterm IDisposable 对象被当函数调用;并新增 ErrorBoundary 兜底 + 渲染层报错转发主进程日志)
- [x] ~~首启动空屏~~(v0.2.1:启动自动打开一个终端)
- [ ] zh-TW 用语统一(i18n 质量专项):`命令/指令` 全文件混用(panels/ssh/settings/terminal,台湾惯用「指令」)、`口令`→`密碼`(ssh.secret.passphrase)、顺带通读 服务器→伺服器 / 默认→預設 / 网络→網路 / 软件→軟體。需要一次全局连贯的专项,零散改会制造新的不一致
- [ ] ja 片假名长音符号统一:`フォルダ`(6) / `フォルダー`(7) 混用,二选一全局统一
- [ ] autoWrap=false 的固定列宽模式(xterm.js 无原生 wrapMode,需自定义列数控制)
- [ ] 终端标题跟随 OSC 序列(窗格标题显示当前目录/命令)
- [ ] 内置等宽字体打包(OFL 许可:JetBrains Mono / Fira Code 等)
+38 -9
View File
@@ -1,6 +1,7 @@
import { Component } from 'react'
import type { ErrorInfo, ReactNode } from 'react'
import { t } from '@shared/i18n'
import { DEFAULT_LANGUAGE, tFor, type Language } from '@shared/i18n'
import { useSettingsStore } from './settings/store'
interface ErrorBoundaryProps {
children: ReactNode
@@ -11,10 +12,29 @@ interface ErrorBoundaryState {
}
/**
* Last-resort boundary: a render/effect crash must never take the whole app
* down to a black window. Shows the error and a reload escape instead.
* Error boundaries must stay class components (they own the error lifecycle),
* and class components cannot subscribe to the settings store the way function
* components do — nor do they re-render when App re-renders on a language
* switch (the root boundary is App's *parent*). So the exported boundaries are
* thin function wrappers that subscribe to the language and hand it down as a
* prop; the classes translate through `tFor(language, …)`. Without this, a
* crashed panel showed its fallback screen in whatever language the app was
* launched in — the one screen where reading it matters most.
*/
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
function useBoundaryLanguage(): Language {
return useSettingsStore((s) => s.settings.system.language ?? DEFAULT_LANGUAGE)
}
/** Last-resort boundary: a render/effect crash must never take the whole app
* down to a black window. Shows the error and a reload escape instead. */
export function ErrorBoundary(props: ErrorBoundaryProps): ReactNode {
const language = useBoundaryLanguage()
return <ErrorBoundaryClass {...props} language={language} />
}
type ErrorBoundaryClassProps = ErrorBoundaryProps & { language: Language }
class ErrorBoundaryClass extends Component<ErrorBoundaryClassProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
@@ -39,7 +59,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
color: '#cccccc'
}}
>
<div>{t('workspace.error.title')}</div>
<div>{tFor(this.props.language, 'workspace.error.title')}</div>
<pre
style={{
maxWidth: 720,
@@ -57,7 +77,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
onClick={() => window.location.reload()}
style={{ padding: '6px 18px', cursor: 'pointer' }}
>
{t('workspace.error.reload')}
{tFor(this.props.language, 'workspace.error.reload')}
</button>
</div>
)
@@ -78,7 +98,14 @@ interface PanelErrorBoundaryProps {
* other live session. A crashed pane is replaced in place and can be remounted
* on retry; the rest of the workspace is untouched.
*/
export class PanelErrorBoundary extends Component<PanelErrorBoundaryProps, ErrorBoundaryState> {
export function PanelErrorBoundary(props: PanelErrorBoundaryProps): ReactNode {
const language = useBoundaryLanguage()
return <PanelErrorBoundaryClass {...props} language={language} />
}
type PanelErrorBoundaryClassProps = PanelErrorBoundaryProps & { language: Language }
class PanelErrorBoundaryClass extends Component<PanelErrorBoundaryClassProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
@@ -98,10 +125,12 @@ export class PanelErrorBoundary extends Component<PanelErrorBoundaryProps, Error
if (!this.state.error) return this.props.children
return (
<div className="panel-error">
<div className="panel-error-title">{this.props.label ?? t('workspace.error.panelTitle')}</div>
<div className="panel-error-title">
{this.props.label ?? tFor(this.props.language, 'workspace.error.panelTitle')}
</div>
<pre className="panel-error-message">{this.state.error.message}</pre>
<button type="button" className="panel-error-retry" onClick={this.retry}>
{t('workspace.error.panelRetry')}
{tFor(this.props.language, 'workspace.error.panelRetry')}
</button>
</div>
)