Files
qwen-code/packages/web-shell/client/components/RootErrorFallback.tsx
4ekuct25andСергей Панченко 68f181b885 feat(web-shell): add Russian locale for the Goal card and Goals dialog (#13685)
* feat(web-shell): add Russian locale for the Goal card and Goals dialog

The web-shell i18n shipped only en and zh-CN and collapsed every other
browser locale (e.g. ru-RU) to en in normalizeLanguage, so a Russian
user saw the Goal card and the Goals dialog in English around a
Russian objective.

Add a partial ru catalog for the goal.*/goals.* keys with Russian
pluralisation (ход/хода/ходов, токен/токена/токенов,
активная цель/активные цели/активных целей). getTranslator already
falls back per key to EN (messages[key] ?? EN[key] ?? key), and the
partial catalog relies on that for every other key — the behaviour this
locale has been running with as an installer patch.

- WEB_SHELL_LANGUAGES / LANGUAGE_LABELS: add 'ru' / 'Русский [ru]' to
  the settings picker
- normalizeLanguage: accept ru / ru-ru / russian / русский
- languageSettingToWebShellLanguage: accept the same spellings for the
  daemon's general.language setting
- WebShellProps.language / WebShellTranscriptProps.language: accept the
  'ru' and 'ru-ru' raw values
- surfaces with their own WebShellLanguage-keyed copy tables
  (StandaloneAuth, RootErrorFallback, conversation search, desktop
  relay) reuse the EN copy for ru, same partial-catalog policy
- re-measure the transcript entry size budget for the catalog
  (+4,649 to 1,325,015; ceiling raised to 1,328,000)

Part of #13391

* fix(web-shell): goals.dropped count and ru_* language normalization

goals.dropped now renders the session count with the correct Russian
plural form (it previously dropped the ${count} interpolation and
showed a bare phrase). The ru language normalizers match the ru prefix
instead of a fixed spelling list, so region-qualified values
(ru-RU, ru-UA, ru-KZ, …) and the underscore form resolve to the
partial ru catalog instead of falling through to en. Add count and
plural pins for goals.dropped and region-form pins for both
normalizers.

* chore: empty commit to retrigger CI and review

* feat(core): Goal verifier verdict inherits the session output language

The verifier's 'Last check:' reason is user-visible prose (#13391 part 2), so it should honor the session output-language preference. It was the only goal-related side query still passing skipOutputLanguagePreference: true; removing the flag makes runSideQuery append the preference. The decision enum stays schema-bound - only the reason prose localizes. Also re-measures the merged web-shell transcript entry (1,334,649 chars) and sets its size ceiling to 1_337_000 with the matching budget narrative.

* fix(web-shell): drop stray conflict marker left by the main merge

* fix(web-shell): accept /language ui ru and derive language listings from WEB_SHELL_LANGUAGES

* fix(web-shell): render goals.dropped with the count and the dialog's session term

* fix(web-shell): make the desktop relay copy table total over WebShellLanguage

* fix(vscode-ide-companion): let the web shell resolve its own UI language

* docs(web-shell): document ru in the language prop unions

* test(web-shell): pin tokensOfBudget pluralising on the budget

* revert(core): move the Goal verifier language change to companion PR #15806

The output-language change for the Goal verifier verdict is split out to
its own PR (QwenLM/qwen-code#15806) so the core change gets dedicated
review under the small-core-change rule. This PR is now web-shell only.

* test(web-shell): pin the /language error and usage copy to the shipped set

* fix(web-shell): restore the DesktopRelayControl header truncated in 705a3b799

The staged hunk for the relay copy-table change cut off the top of the
file: the license header, all imports and the DesktopRelayPhase/
DesktopRelayBlocker types were missing from the committed file, leaving
it uncompilable (tsc: 63 errors). Restore the header verbatim from the
parent commit (c5ff652b5), re-indent the extracted EN_COPY table, and
fold the ternary-free useCallback call to the single line prettier wants.
The semantic change of 705a3b799 (en table extracted to EN_COPY,
explicit ru: EN_COPY entry, ternary-free COPY[language][key] lookup)
is preserved unchanged.

* style(web-shell): prettier 3.6.1 formatting for the /language gate and README language row

---------

Co-authored-by: Сергей Панченко <[email protected]>
2026-10-29 12:49:34 +00:00

120 lines
3.6 KiB
TypeScript

import type { CSSProperties } from 'react';
import type { WebShellLanguage } from '../i18n';
interface RootErrorFallbackProps {
error: Error;
onRetry: () => void;
/** Selects the fallback copy. Defaults to English when omitted. */
language?: WebShellLanguage;
/**
* What the retry button does, so its label stays honest. Defaults to
* 'reset' (an in-place boundary retry); 'reload' is for callers whose
* retry navigates (e.g. the standalone root boundary reloads the page).
*/
retryMode?: 'reset' | 'reload';
}
interface FallbackCopy {
title: string;
body: string;
retry: string;
reload: string;
}
// This surface renders OUTSIDE the in-app I18nProvider (the boundary wraps the
// whole App, which owns that provider), so it cannot call useI18n. It carries
// its own minimal copy instead of pulling the full translation table.
const EN_COPY: FallbackCopy = {
title: 'Something went wrong',
body: 'An unexpected error occurred and this content could not be displayed.',
retry: 'Try again',
reload: 'Reload page',
};
const COPY: Record<WebShellLanguage, FallbackCopy> = {
en: EN_COPY,
'zh-CN': {
title: '出了点问题',
body: '发生意外错误,无法显示此内容。',
retry: '重试',
reload: '重新加载',
},
// Not translated yet — same partial-catalog policy as the ru table in
// i18n.tsx (#13391): ru reuses the EN copy.
ru: EN_COPY,
};
// The boundary wraps the whole App, so this surface renders when the themed
// App container (which defines --text-primary etc.) never mounted. It therefore
// cannot use app theme tokens. Inheriting the host's text color keeps it
// readable on both light and dark hosts; opacity carries the hierarchy and a
// neutral gray border reads acceptably on either background.
const containerStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.75rem',
padding: '2rem',
minHeight: '8rem',
height: '100%',
textAlign: 'center',
color: 'inherit',
};
const titleStyle: CSSProperties = {
margin: 0,
fontSize: '1rem',
fontWeight: 600,
};
const bodyStyle: CSSProperties = {
margin: 0,
fontSize: '0.85rem',
opacity: 0.7,
maxWidth: '32rem',
};
const buttonStyle: CSSProperties = {
appearance: 'none',
cursor: 'pointer',
padding: '0.4rem 1rem',
fontSize: '0.85rem',
borderRadius: '6px',
border: '1px solid rgba(128, 128, 128, 0.4)',
background: 'transparent',
color: 'inherit',
};
/**
* Last-resort surface for the top-level boundary. Self-contained (no provider,
* theme-token, or i18n dependency) so it survives even when the whole App tree
* fails to mount. Offers a retry instead of forcing a host-page reload, which
* would be hostile in embedded integrations.
*/
export function RootErrorFallback({
error,
onRetry,
language = 'en',
retryMode = 'reset',
}: RootErrorFallbackProps) {
const copy = COPY[language] ?? COPY.en;
return (
<div role="alert" style={containerStyle} data-web-shell-error>
<p style={titleStyle}>{copy.title}</p>
<p style={bodyStyle}>{copy.body}</p>
{/* Dev-only: end users (and embedded hosts) shouldn't see raw internal
error text — it stays in console.error for everyone. In the published
lib build DEV is statically false, so this never ships to consumers. */}
{import.meta.env.DEV && error.message && (
<p style={{ ...bodyStyle, fontFamily: 'monospace', opacity: 0.55 }}>
{error.message}
</p>
)}
<button type="button" style={buttonStyle} onClick={onRetry}>
{retryMode === 'reload' ? copy.reload : copy.retry}
</button>
</div>
);
}