fix(desktop): translate text typed into components

The header over the pictures in a reply read "1 IMAGE" in every locale.
The text was typed into JSX, and the type of the locale files only
proves that every key has a translation, not that components use one.
A scan of the 246 components found more: Mermaid's error, loading text
and preview hint, "Untitled" in the sidebar, Computer Use's "Failed to
check status.", token counts in /context, labels in the trajectory
view, and screen-reader labels such as the sidebar region and the diff
grid. They now go through t(), reusing a key where one existed and
adding 15 to all five locales. The English text is unchanged.

untranslatedText.test.ts keeps it that way. It reads every component
and fails on JSX text, text-bearing attributes and string literals
rendered as children that skip t(). Key caps, code, icon ligatures and
t() fallbacks are not prose. Product names, units and example values
are listed per file with the reason, and a listed text the source no
longer has fails too. Modal's "Close dialog" stays a known gap: a ui
primitive cannot read the locale, so the label has to come from its
callers.
This commit is contained in:
程序员阿江(Relakkes)
2026-10-04 22:15:29 +08:00
parent bbe39ef32d
commit f238b6965d
19 changed files with 339 additions and 25 deletions
@@ -0,0 +1,223 @@
// @vitest-environment node
import { readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
/**
* Text a user reads has to come from the locale files. A string typed straight
* into JSX renders the same in every locale, and nothing else notices: the
* "1 image" header over the pictures in a reply stayed English in the Chinese UI.
*
* So every component is read here. JSX text, the attributes a user reads or a
* screen reader speaks, and string literals rendered as children all have to go
* through `t()`. Text inside <kbd>, <code>, <pre> or <style>, and icon ligatures,
* is not prose. A literal that only stands in when the translate function is
* missing (`t?.('key') ?? 'Fallback'`) is not what users see either.
*
* What is left reads the same in every language — a product or format name, an
* example value, a key cap — and is listed below, file by file, with no entry
* that the source no longer has.
*/
const srcRoot = path.resolve(import.meta.dirname, '..')
/** Not part of the product: the dev-only component gallery and test support. */
const SKIPPED_DIRECTORIES = new Set([path.join(srcRoot, 'dev'), path.join(srcRoot, 'test')])
/** Attributes whose value a user reads, or a screen reader speaks. */
const TEXT_ATTRIBUTE = /^(?:title|alt|placeholder|label|content|aria-(?:label|description|roledescription|valuetext|placeholder))$|(?:Label|Title|Text|Description|Hint|Message|Tooltip)$/
/** HTML attributes that take a keyword rather than text, though their names look like text. */
const KEYWORD_ATTRIBUTES = new Set(['enterKeyHint'])
/** Elements whose text is code, a key cap or a style sheet rather than prose. */
const NON_PROSE_ELEMENTS = new Set(['kbd', 'Keycap', 'code', 'pre', 'style', 'script'])
/**
* Literals that read the same in every language, by file. Anything else goes
* through `t()`; an entry here says why it does not have to.
*/
const LANGUAGE_NEUTRAL: Record<string, string[]> = {
'components/agentTeams/AgentTeamsCanvas.tsx': ['zZ'], // a sleeping agent's glyph
'components/agentTeams/AgentTeamsWorkbench.tsx': ['Agent Teams ·'], // product name, kept in every locale
'components/chat/MermaidRenderer.tsx': ['Mermaid'], // the diagram language
'components/layout/H5ConnectionView.tsx': ['https://chat.example.com'], // example URL
'components/layout/Sidebar.tsx': ['cc-', 'haha', 'GitHub'], // wordmark, brand
'components/market/FrontmatterPanel.tsx': ['true', 'false'], // YAML values, shown as written
'components/market/InstallConfirmDialog.tsx': ['…/skills/'], // a path
'components/market/MarketSkillDetail.tsx': ['SKILL.md'], // a file name
'components/settings/ProviderImageGenerationFields.tsx': ['sk-...'], // key format
'components/workbench/WorkspaceReviewTab.tsx': ['feature/base', 'HEAD~1'], // example git refs
'components/workspace/WorkspaceDiffSurface.tsx': ['diff --git'], // the git header, read to screen readers
'features/pets/PetSettings.tsx': ['px', 'moon-cat'], // unit, example ID
'pages/Connectors.tsx': ['MCP', 'CLI'], // transport names
'pages/McpSettings.tsx': ['STDIO'], // transport name
'pages/MemorySettings.tsx': ['MARKDOWN'], // file format
'pages/settings/AboutSettings.tsx': [ // product, repository and author; example proxy
'Claude Code Haha', 'GitHub', 'NanmiCoder/cc-haha', '程序员阿江-Relakkes', 'http://127.0.0.1:7890',
],
'pages/settings/ChatAppearanceSettings.tsx': ['px', '${}px'], // unit
'pages/settings/GeneralSettings.tsx': ['http://127.0.0.1:7890', 'tvly-...'], // example proxy, key format
'pages/settings/ProviderSettings.tsx': ['OpenAI Chat', 'OpenAI Responses', 'sk-...'], // API format names, key format
'pages/settings/PublicAccessSettings.tsx': ['/remote'], // a URL path
}
/** Untranslated text that needs a wider change first. Keep this short; each entry says why. */
const KNOWN_GAPS: Record<string, string[]> = {
// A ui primitive cannot read the locale (components/ui/contract.test.ts keeps
// stores out of it), so this label has to come from the Modal's callers: 19 of
// them, and the 34 behind ActionDialog and ConfirmDialog.
'components/ui/Modal.tsx': ['Close dialog'],
}
type Finding = { file: string; line: number; text: string }
function sourceFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(directory, entry.name)
if (entry.isDirectory()) return SKIPPED_DIRECTORIES.has(full) ? [] : sourceFiles(full)
return entry.name.endsWith('.tsx') && !/\.(test|spec)\.tsx$/.test(entry.name) ? [full] : []
})
}
function tagName(element: ts.JsxElement): string {
return element.openingElement.tagName.getText()
}
/** An icon font renders its ligature (`close`, `image`) as a glyph, not as words. */
function isIcon(element: ts.JsxElement): boolean {
return element.openingElement.attributes.properties.some((attribute) => (
ts.isJsxAttribute(attribute)
&& attribute.name.getText() === 'className'
&& attribute.initializer !== undefined
&& /material-symbols/.test(attribute.initializer.getText())
))
}
function isTranslation(expression: ts.Expression): boolean {
return ts.isCallExpression(expression) && expression.expression.getText() === 't'
}
/** String literals an expression renders as written: itself, or a branch of `?:`, `&&`, `||`, `??`. */
function renderedLiterals(expression: ts.Expression): string[] {
if (ts.isParenthesizedExpression(expression)) return renderedLiterals(expression.expression)
if (ts.isConditionalExpression(expression)) {
return [...renderedLiterals(expression.whenTrue), ...renderedLiterals(expression.whenFalse)]
}
if (ts.isBinaryExpression(expression)) {
const operator = expression.operatorToken.kind
if (operator === ts.SyntaxKind.AmpersandAmpersandToken) return renderedLiterals(expression.right)
if (operator === ts.SyntaxKind.BarBarToken || operator === ts.SyntaxKind.QuestionQuestionToken) {
// A fallback for a missing translate function is not what users see.
return isTranslation(expression.left)
? []
: [...renderedLiterals(expression.left), ...renderedLiterals(expression.right)]
}
return []
}
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return [expression.text]
if (ts.isTemplateExpression(expression)) {
return [expression.head.text + expression.templateSpans.map((span) => '${}' + span.literal.text).join('')]
}
return []
}
function findUntranslatedText(file: string, code: string): Finding[] {
const source = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
const found: Finding[] = []
const report = (node: ts.Node, raw: string) => {
const text = raw.replace(/\s+/g, ' ').trim()
if (!/\p{L}{2,}/u.test(text.replaceAll('${}', ' '))) return
found.push({ file, line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1, text })
}
const visit = (node: ts.Node): void => {
if (ts.isJsxElement(node) && (NON_PROSE_ELEMENTS.has(tagName(node)) || isIcon(node))) return
if (ts.isJsxText(node)) {
report(node, node.text)
} else if (
ts.isJsxAttribute(node)
&& TEXT_ATTRIBUTE.test(node.name.getText())
&& !KEYWORD_ATTRIBUTES.has(node.name.getText())
) {
const value = node.initializer
if (value && ts.isStringLiteral(value)) report(node, value.text)
else if (value && ts.isJsxExpression(value) && value.expression) {
for (const text of renderedLiterals(value.expression)) report(node, text)
}
} else if (
ts.isJsxExpression(node)
&& node.expression
&& (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))
) {
for (const text of renderedLiterals(node.expression)) report(node, text)
}
ts.forEachChild(node, visit)
}
visit(source)
return found
}
const files = sourceFiles(srcRoot)
const findings = files.flatMap((file) => findUntranslatedText(path.relative(srcRoot, file), readFileSync(file, 'utf8')))
function isExempt(finding: Finding): boolean {
return [LANGUAGE_NEUTRAL, KNOWN_GAPS].some((exemptions) => exemptions[finding.file]?.includes(finding.text))
}
describe('user-visible text goes through the locale files', () => {
it('flags text typed into JSX and leaves code, key caps, icons and fallbacks alone', () => {
const fixture = [
'export function Gallery({ count, t }) {',
' return (',
' <div title="Gallery" aria-label={`${count} pictures`}>',
" {count === 1 ? '1 image' : `${count} images`}",
' <span>Unable to load image</span>',
' <input enterKeyHint="search" placeholder="Search skills" />',
' <span className="material-symbols-outlined">image</span>',
' <kbd>Esc</kbd>',
' <code>diff --git</code>',
" <span>{t?.('chat.retry') ?? 'Retry'}</span>",
" <span title={t('chat.retry')}>{t('chat.retry')}</span>",
' <span>{count}</span>',
' </div>',
' )',
'}',
].join('\n')
expect(findUntranslatedText('Gallery.tsx', fixture).map((finding) => finding.text)).toEqual([
'Gallery',
'${} pictures',
'1 image',
'${} images',
'Unable to load image',
'Search skills',
])
})
it('reads the source tree it is guarding', () => {
// A scan that found nothing would pass every rule below.
expect(files.length).toBeGreaterThan(200)
expect(files.some((file) => file.endsWith('InlineImageGallery.tsx'))).toBe(true)
})
it('routes every text a user reads through t()', () => {
const untranslated = findings
.filter((finding) => !isExempt(finding))
.map((finding) => `${finding.file}:${finding.line} ${finding.text}`)
// Put the text in all five locale files and render it with t(). Only text that
// reads the same in every language belongs in LANGUAGE_NEUTRAL.
expect(untranslated).toEqual([])
})
it('keeps no exemption for text the source no longer has', () => {
const stale = [LANGUAGE_NEUTRAL, KNOWN_GAPS].flatMap((exemptions) => Object.entries(exemptions).flatMap(
([file, texts]) => texts
.filter((text) => !findings.some((finding) => finding.file === file && finding.text === text))
.map((text) => `${file} ${text}`),
))
expect(stale).toEqual([])
})
})
@@ -28,8 +28,10 @@ vi.mock('../../api/sessions', () => ({ sessionsApi: { getWorkspaceTree } }))
import { InlineImageGallery } from './InlineImageGallery'
import { resetDiskListingCacheForTests } from '../../hooks/useDiskConfirmedTargets'
import { useSettingsStore } from '../../stores/settingsStore'
beforeEach(() => {
useSettingsStore.setState({ locale: 'en' })
resetDiskListingCacheForTests()
getWorkspaceTree.mockReset().mockResolvedValue({ state: 'missing', path: '', entries: [] })
fetchServerImageBlobUrl.mockReset().mockRejectedValue(new ApiError(500, { error: 'Internal error' }))
@@ -55,6 +57,19 @@ describe('InlineImageGallery', () => {
expect(screen.getByRole('button', { name: 'Retry' })).toBeVisible()
})
it.each([
['en', 'See /tmp/chart.png', '1 image'],
['en', 'See /tmp/a.png and /tmp/b.png', '2 images'],
['zh', 'See /tmp/a.png and /tmp/b.png', '2 张图片'],
['jp', 'See /tmp/a.png and /tmp/b.png', '画像 2 枚'],
] as const)('counts the pictures in the reader\'s language (%s: %s)', (locale, text, header) => {
// The header was typed in English and read "1 IMAGE" in every locale.
useSettingsStore.setState({ locale })
render(<InlineImageGallery text={text} />)
expect(screen.getByText(header)).toBeInTheDocument()
})
it('keeps other images usable and tracks failures by source when the list changes', async () => {
const { rerender } = render(<InlineImageGallery text="See /tmp/broken.png and /tmp/chart.png" />)
fireEvent.error(screen.getByRole('img', { name: 'broken.png' }))
@@ -190,7 +190,7 @@ export function InlineImageGallery({ text, sessionId, workDir, changedFiles, sup
<div className="mt-3 space-y-2">
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-outline)]">
<span className="material-symbols-outlined text-[12px]">image</span>
{visibleImages.length === 1 ? '1 image' : `${visibleImages.length} images`}
{t(visibleImages.length === 1 ? 'chat.imageCountOne' : 'chat.imageCountOther', { count: visibleImages.length })}
</div>
<div className={`grid gap-2 ${visibleImages.length === 1 ? 'grid-cols-1' : 'grid-cols-2'}`}>
{visibleImages.map((img, i) => failedSources.has(img.src) ? (
@@ -280,7 +280,7 @@ function isCapacityCategory(category: ContextCategory) {
return category.isDeferred || name.includes('free') || name.includes('autocompact')
}
function ContextStackedBar({ categories, rawMaxTokens }: { categories: ContextCategory[]; rawMaxTokens: number }) {
function ContextStackedBar({ categories, rawMaxTokens, t }: { categories: ContextCategory[]; rawMaxTokens: number; t: Translate }) {
const activeCategories = categories.filter((category) => !isCapacityCategory(category) && category.tokens > 0)
if (activeCategories.length === 0) return null
@@ -290,7 +290,7 @@ function ContextStackedBar({ categories, rawMaxTokens }: { categories: ContextCa
{activeCategories.map((category) => (
<div
key={category.name}
title={`${category.name}: ${formatNumber(category.tokens)} tokens`}
title={`${category.name}: ${t('common.tokens', { count: formatNumber(category.tokens) })}`}
style={{
width: `${Math.max(0.5, (category.tokens / rawMaxTokens) * 100)}%`,
backgroundColor: category.color,
@@ -408,7 +408,7 @@ function MemoryFilesBreakdown({ files, t }: { files: ContextMemoryFile[]; t: Tra
</div>
<div className="shrink-0 text-right font-mono">
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-[var(--color-inspector-muted-strong)]">{file.type}</div>
<div className="mt-0.5 text-[11px] text-[var(--color-inspector-muted)]">{formatNumber(file.tokens)} tokens</div>
<div className="mt-0.5 text-[11px] text-[var(--color-inspector-muted)]">{t('common.tokens', { count: formatNumber(file.tokens) })}</div>
</div>
</div>
))}
@@ -480,7 +480,7 @@ function ContextOverview({ context, categories, t }: { context: SessionContextSn
<span className="ml-3 align-middle text-sm font-normal text-[var(--color-inspector-accent-secondary)]">[{formatPercent(usedPercent)} {t('slash.inspector.context.used')}]</span>
</div>
<div className="mt-7">
<ContextStackedBar categories={categories} rawMaxTokens={context.rawMaxTokens} />
<ContextStackedBar categories={categories} rawMaxTokens={context.rawMaxTokens} t={t} />
</div>
<div className="mt-8 grid grid-cols-2 gap-3 lg:grid-cols-4">
<div className="rounded-md border border-[var(--color-inspector-border)] bg-[var(--color-inspector-surface)] px-4 py-3">
@@ -662,7 +662,7 @@ export function MermaidRenderer({ code }: Props) {
<div className="my-4 overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-error-soft-hover)]">
<div className="flex items-center gap-2 border-b border-[var(--color-error-soft-hover)] bg-[var(--color-error-container)] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--color-on-error-container)]">
<span className="material-symbols-outlined text-[14px]">error</span>
Mermaid Error
{t('mermaid.renderError')}
</div>
<div className="bg-[var(--color-error-soft)] px-3 py-2 font-mono text-[11px] text-[var(--color-on-error-container)]">
{error}
@@ -676,7 +676,7 @@ export function MermaidRenderer({ code }: Props) {
<div className="my-4 flex items-center justify-center rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] py-8">
<div className="flex items-center gap-2 text-[11px] text-[var(--color-text-tertiary)]">
<span className="material-symbols-outlined animate-spin text-[16px]">progress_activity</span>
Rendering diagram...
{t('mermaid.rendering')}
</div>
</div>
)
@@ -716,7 +716,7 @@ export function MermaidRenderer({ code }: Props) {
<div className="mx-auto shrink-0 select-none" style={inlineFrameStyle}>
<div
style={inlineCanvasStyle}
aria-label="Mermaid inline canvas"
aria-label={t('mermaid.inlineCanvas')}
dangerouslySetInnerHTML={{ __html: sanitizedSvg ?? '' }}
/>
</div>
@@ -729,7 +729,7 @@ export function MermaidRenderer({ code }: Props) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--color-text-primary)]">
<span className="material-symbols-outlined text-[18px]">account_tree</span>
Mermaid Diagram
{t('mermaid.previewTitle')}
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1 py-1">
@@ -794,14 +794,14 @@ export function MermaidRenderer({ code }: Props) {
ref={previewContentRef}
style={previewCanvasStyle}
data-dragging={isDraggingPreview ? 'true' : 'false'}
aria-label="Mermaid preview canvas"
aria-label={t('mermaid.previewCanvas')}
dangerouslySetInnerHTML={{ __html: sanitizedSvg ?? '' }}
/>
</div>
</div>
</div>
<div className="text-[11px] text-[var(--color-text-tertiary)]">
Use the zoom controls to enlarge the diagram. Drag inside the preview to pan, or use the trackpad, mouse wheel, and scrollbars. Hold Ctrl/Command while scrolling to zoom.
{t('mermaid.previewHint')}
</div>
</div>
</Modal>
@@ -149,7 +149,7 @@ export function ProjectContextChip({
tabIndex={0}
className="shrink-0 cursor-help rounded-full border border-[var(--color-border)] px-1.5 py-0.5 text-[10px] font-medium uppercase leading-none text-[var(--color-text-tertiary)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface)]"
>
worktree
{t('sidebar.worktree')}
</span>
</Tooltip>
</>
@@ -620,7 +620,7 @@ function AgentCallCard({
<span className="material-symbols-outlined text-[18px] text-[var(--color-outline)]">smart_toy</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[13px] font-semibold text-[var(--color-text-primary)]">Agent</span>
<span className="text-[13px] font-semibold text-[var(--color-text-primary)]">{t('subagentRun.agent')}</span>
{description && (
<span className="truncate text-[12px] text-[var(--color-text-secondary)]">
{description}
+4 -4
View File
@@ -931,7 +931,7 @@ export function Sidebar({
<aside
className="sidebar-panel relative h-full flex flex-col bg-[var(--color-surface-sidebar)] border-r border-[var(--color-border)] select-none"
data-state={expanded ? 'open' : 'closed'}
aria-label="Sidebar"
aria-label={t('sidebar.regionLabel')}
>
<div
data-testid="sidebar-title-region"
@@ -1387,7 +1387,7 @@ export function Sidebar({
}
`}
aria-pressed={isBatchMode ? selectedSessionIds.has(session.id) : undefined}
title={session.title || 'Untitled'}
title={session.title || t('tabs.untitled')}
>
<span className="flex min-w-0 items-center gap-1.5">
{isBatchMode ? (
@@ -1406,7 +1406,7 @@ export function Sidebar({
)}
</span>
) : null}
<span className="min-w-0 flex-1 truncate font-medium tracking-normal">{session.title || 'Untitled'}</span>
<span className="min-w-0 flex-1 truncate font-medium tracking-normal">{session.title || t('tabs.untitled')}</span>
{getSessionWorkspaceState(session) === 'missing' && (
<span
className="flex-shrink-0 text-[10px] text-[var(--color-warning)]"
@@ -1654,7 +1654,7 @@ export function Sidebar({
<ul className="max-h-40 space-y-1 overflow-y-auto rounded-[8px] border border-[var(--color-border)] bg-[var(--color-surface-container-low)] p-2">
{pendingBatchDeleteSessions.slice(0, 5).map((session) => (
<li key={session.id} className="truncate text-xs text-[var(--color-text-secondary)]">
{session.title || 'Untitled'}
{session.title || t('tabs.untitled')}
</li>
))}
{(pendingBatchDeleteSessionIds?.length ?? 0) > 5 && (
@@ -164,9 +164,10 @@ function TextResult({ text }: { text: string }) {
}
function ImageChip({ mediaType }: { mediaType?: string }) {
const t = useTranslation()
return (
<Badge tone="neutral" variant="outline" size="xs" pill={false} mono className="w-fit">
[image]
{t('trace.detail.imageBlock')}
{mediaType ? <span>{mediaType}</span> : null}
</Badge>
)
@@ -229,7 +229,7 @@ function ResponseContent({
{call.error.stack ? (
<details className="mt-2">
<summary className="cursor-pointer font-mono text-[11px] uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]">
stack
{t('trace.detail.stack')}
</summary>
<pre className="mt-1.5 max-h-[240px] overflow-y-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-[1.6] text-[var(--color-text-tertiary)]">
{call.error.stack}
@@ -615,7 +615,7 @@ export function WorkspaceDiffSurface({
data-testid="workspace-code"
data-highlight-engine={highlightResult.engine}
role="grid"
aria-label={`${path} diff`}
aria-label={t('workspace.diffGridLabel', { path })}
className="m-0 min-w-full font-mono text-[13px] leading-5 text-[var(--color-code-fg)]"
style={codeStyle}
>
+15
View File
@@ -707,6 +707,7 @@ export const en = {
'sidebar.timeGroup.older': 'Older',
'sidebar.collapse': 'Collapse sidebar',
'sidebar.expand': 'Expand sidebar',
'sidebar.regionLabel': 'Sidebar',
'sidebar.resize': 'Resize sidebar',
// ─── Title Bar ──────────────────────────────────────
@@ -765,6 +766,7 @@ export const en = {
'workspace.searchResultsTruncated': 'Showing the first {count} results. Refine your search.',
'workspace.reviewWorkspace': 'Review workspace',
'workspace.itemsCount': '{count} items',
'workspace.diffGridLabel': '{path} diff',
'workspace.filteredItemsCount': '{visible} of {total} items',
'workspace.refresh': 'Refresh workspace',
'workspace.resizePanel': 'Resize workspace panel',
@@ -1135,6 +1137,7 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
// Settings > Usage
'settings.activity.title': 'Token usage',
'settings.activity.profileTitle': 'Profile',
'settings.activity.avatarAlt': '{name} avatar',
'settings.activity.profilePrivacy': 'Local only',
'settings.activity.defaultHandle': 'github.com/NanmiCoder/cc-haha',
'settings.activity.editProfile': 'Edit profile',
@@ -1447,6 +1450,7 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
'settings.providers.sonnetModel': 'Sonnet Model',
'settings.providers.opusModel': 'Opus Model',
'settings.providers.model1mSupportShort': '1M',
'settings.providers.model1mSupportAria': '1M support: {slot}',
'settings.providers.model1mSupportHint': 'Only tick “1M” when the endpoint really serves a 1M-token context — ticking it otherwise stops auto-compaction from firing.',
'settings.providers.model1mSupportTooltip': 'Declares that this model endpoint supports a 1M-token context: the model name gets a [1m] suffix and the context window is set to 1,000,000. If the endpoint is actually smaller, auto-compaction never fires and requests hit the provider’s hard context limit. Clearing the box restores the preset window.',
'settings.providers.modelIdPlaceholder': 'e.g. deepseek-v4-flash',
@@ -2172,6 +2176,7 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
'settings.computerUse.title': 'Computer Use',
'settings.computerUse.statusCheckFailed': 'Failed to check status.',
'settings.computerUse.description': 'Allow Claude to take screenshots, click, type, and control your computer. Requires Python 3. On macOS, accessibility permissions are also needed.',
'settings.computerUse.enabledToggle': 'Enabled',
'settings.computerUse.enableRiskTitle': 'Enable Computer Use?',
@@ -2653,6 +2658,8 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
'chat.addSelectionToChat': 'Add to chat',
'chat.imageLoadFailed': 'Unable to load image',
'chat.imageLoadFailedHint': 'The file may be damaged, or the local server did not respond.',
'chat.imageCountOne': '{count} image',
'chat.imageCountOther': '{count} images',
'chat.retryImage': 'Retry image: {name}',
'chat.imageRetrying': 'Retrying…',
'chat.branchFromHere': 'Fork a new conversation',
@@ -3589,6 +3596,8 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
'trace.detail.earlierMessages': 'Show {count} earlier messages',
'trace.detail.chars': '{count} chars',
'trace.detail.thinking': 'thinking',
'trace.detail.imageBlock': '[image]',
'trace.detail.stack': 'stack',
// ─── App Shell ──────────────────────────────────────
'app.serverFailed': 'Local server failed to start',
@@ -3862,6 +3871,12 @@ Row 9, all 8 cells: continuing from straight down, turning left through lower-le
'mermaid.zoomIn': 'Zoom in',
'mermaid.fit': 'Fit',
'mermaid.fitDiagram': 'Fit diagram',
'mermaid.renderError': 'Mermaid Error',
'mermaid.rendering': 'Rendering diagram...',
'mermaid.previewTitle': 'Mermaid Diagram',
'mermaid.previewHint': 'Use the zoom controls to enlarge the diagram. Drag inside the preview to pan, or use the trackpad, mouse wheel, and scrollbars. Hold Ctrl/Command while scrolling to zoom.',
'mermaid.inlineCanvas': 'Mermaid inline canvas',
'mermaid.previewCanvas': 'Mermaid preview canvas',
'codeViewer.collapse': 'Collapse',
'codeViewer.showMoreLines': 'Show {count} more lines',
'common.copy': 'Copy',
+15
View File
@@ -708,6 +708,7 @@ export const jp: Record<TranslationKey, string> = {
'sidebar.timeGroup.older': 'それ以前',
'sidebar.collapse': 'サイドバーを折りたたむ',
'sidebar.expand': 'サイドバーを展開',
'sidebar.regionLabel': 'サイドバー',
'sidebar.resize': 'サイドバーの幅を調整',
// ─── Title Bar ──────────────────────────────────────
@@ -766,6 +767,7 @@ export const jp: Record<TranslationKey, string> = {
'workspace.searchResultsTruncated': '最初の {count} 件を表示しています。検索を絞り込んでください。',
'workspace.reviewWorkspace': 'ワークスペースをレビュー',
'workspace.itemsCount': '{count} 項目',
'workspace.diffGridLabel': '{path} の差分',
'workspace.filteredItemsCount': '{total} 項目中 {visible} 項目',
'workspace.refresh': 'ワークスペースを更新',
'workspace.resizePanel': 'ワークスペースパネルのサイズを変更',
@@ -1136,6 +1138,7 @@ export const jp: Record<TranslationKey, string> = {
// Settings > Usage
'settings.activity.title': 'トークン使用量',
'settings.activity.profileTitle': 'プロフィール',
'settings.activity.avatarAlt': '{name} のアバター',
'settings.activity.profilePrivacy': 'ローカルのみ',
'settings.activity.defaultHandle': 'github.com/NanmiCoder/cc-haha',
'settings.activity.editProfile': 'プロフィールを編集',
@@ -1448,6 +1451,7 @@ export const jp: Record<TranslationKey, string> = {
'settings.providers.sonnetModel': 'Sonnet モデル',
'settings.providers.opusModel': 'Opus モデル',
'settings.providers.model1mSupportShort': '1M',
'settings.providers.model1mSupportAria': '1M コンテキスト対応:{slot}',
'settings.providers.model1mSupportHint': '「1M」は、そのモデルのエンドポイントが実際に 100 万トークンのコンテキストに対応している場合のみチェックしてください。誤ってチェックすると自動圧縮が働かなくなります。',
'settings.providers.model1mSupportTooltip': 'このモデルのエンドポイントが 100 万トークンのコンテキストに対応していることを宣言します。モデル名に [1m] が付き、コンテキストウィンドウは 1,000,000 に設定されます。実際のウィンドウが 1M より小さい場合、自動圧縮は実行されず、リクエストはプロバイダーのコンテキスト上限に達します。チェックを外すとプリセットのウィンドウに戻ります。',
'settings.providers.modelIdPlaceholder': '例: deepseek-v4-flash',
@@ -2173,6 +2177,7 @@ export const jp: Record<TranslationKey, string> = {
// Settings > Computer Use
'settings.tab.computerUse': 'コンピューター操作',
'settings.computerUse.title': 'コンピューター操作',
'settings.computerUse.statusCheckFailed': 'ステータスを確認できませんでした。',
'settings.computerUse.description': 'Claude がスクリーンショットを撮影し、クリック、入力を行い、コンピューターを操作できるようにします。Python 3 が必要です。macOS ではアクセシビリティ権限も必要です。',
'settings.computerUse.enabledToggle': '有効',
'settings.computerUse.enableRiskTitle': 'Computer Use を有効にしますか?',
@@ -2654,6 +2659,8 @@ export const jp: Record<TranslationKey, string> = {
'chat.addSelectionToChat': 'チャットに追加',
'chat.imageLoadFailed': '画像を読み込めません',
'chat.imageLoadFailedHint': 'ファイルが破損しているか、ローカルサーバーが応答しませんでした。',
'chat.imageCountOne': '画像 {count} 枚',
'chat.imageCountOther': '画像 {count} 枚',
'chat.retryImage': '画像を再読み込み: {name}',
'chat.imageRetrying': '再読み込み中…',
'chat.branchFromHere': '新しい会話を分岐',
@@ -3590,6 +3597,8 @@ export const jp: Record<TranslationKey, string> = {
'trace.detail.earlierMessages': '以前の {count} 件のメッセージを表示',
'trace.detail.chars': '{count} 文字',
'trace.detail.thinking': '思考',
'trace.detail.imageBlock': '[画像]',
'trace.detail.stack': 'スタック',
// ─── App Shell ──────────────────────────────────────
'app.serverFailed': 'ローカルサーバーの起動に失敗しました',
@@ -3863,6 +3872,12 @@ export const jp: Record<TranslationKey, string> = {
'mermaid.zoomIn': '拡大',
'mermaid.fit': 'フィット',
'mermaid.fitDiagram': '図を画面に合わせる',
'mermaid.renderError': 'Mermaid の描画エラー',
'mermaid.rendering': '図を描画しています…',
'mermaid.previewTitle': 'Mermaid 図',
'mermaid.previewHint': 'ズームボタンで図を拡大できます。プレビュー内をドラッグして移動するか、トラックパッド、マウスホイール、スクロールバーを使ってください。Ctrl/Command を押しながらスクロールするとズームします。',
'mermaid.inlineCanvas': 'Mermaid インラインキャンバス',
'mermaid.previewCanvas': 'Mermaid プレビューキャンバス',
'codeViewer.collapse': '折りたたむ',
'codeViewer.showMoreLines': '残り {count} 行を表示',
'common.copy': 'コピー',
+15
View File
@@ -710,6 +710,7 @@ export const kr: Record<TranslationKey, string> = {
'sidebar.timeGroup.older': '그 이전',
'sidebar.collapse': '사이드바 접기',
'sidebar.expand': '사이드바 펼치기',
'sidebar.regionLabel': '사이드바',
'sidebar.resize': '사이드바 너비 조절',
// ─── Title Bar ──────────────────────────────────────
@@ -768,6 +769,7 @@ export const kr: Record<TranslationKey, string> = {
'workspace.searchResultsTruncated': '처음 {count}개 결과를 표시합니다. 검색어를 구체화하세요.',
'workspace.reviewWorkspace': '작업 공간 검토',
'workspace.itemsCount': '항목 {count}개',
'workspace.diffGridLabel': '{path} 변경 내용',
'workspace.filteredItemsCount': '항목 {total}개 중 {visible}개',
'workspace.refresh': '작업 공간 새로 고침',
'workspace.resizePanel': '작업 공간 패널 크기 조정',
@@ -1138,6 +1140,7 @@ export const kr: Record<TranslationKey, string> = {
// Settings > Usage
'settings.activity.title': '토큰 사용량',
'settings.activity.profileTitle': '프로필',
'settings.activity.avatarAlt': '{name} 아바타',
'settings.activity.profilePrivacy': '로컬 전용',
'settings.activity.defaultHandle': 'github.com/NanmiCoder/cc-haha',
'settings.activity.editProfile': '프로필 편집',
@@ -1450,6 +1453,7 @@ export const kr: Record<TranslationKey, string> = {
'settings.providers.sonnetModel': 'Sonnet 모델',
'settings.providers.opusModel': 'Opus 모델',
'settings.providers.model1mSupportShort': '1M',
'settings.providers.model1mSupportAria': '1M 컨텍스트 지원: {slot}',
'settings.providers.model1mSupportHint': '해당 모델 엔드포인트가 실제로 100만 토큰 컨텍스트를 지원할 때만 「1M」을 선택하세요. 잘못 선택하면 자동 압축이 작동하지 않습니다.',
'settings.providers.model1mSupportTooltip': '이 모델 엔드포인트가 100만 토큰 컨텍스트를 지원한다고 선언합니다. 모델 이름에 [1m]이 추가되고 컨텍스트 창이 1,000,000으로 설정됩니다. 엔드포인트의 실제 창이 1M보다 작으면 자동 압축이 실행되지 않고 요청이 공급자의 컨텍스트 한도에 도달합니다. 선택을 해제하면 사전 설정 창으로 되돌아갑니다.',
'settings.providers.modelIdPlaceholder': '예: deepseek-v4-flash',
@@ -2175,6 +2179,7 @@ export const kr: Record<TranslationKey, string> = {
// Settings > Computer Use
'settings.tab.computerUse': '컴퓨터 사용',
'settings.computerUse.title': '컴퓨터 사용',
'settings.computerUse.statusCheckFailed': '상태를 확인하지 못했습니다.',
'settings.computerUse.description': 'Claude가 스크린샷을 찍고 클릭, 입력하며 컴퓨터를 제어할 수 있도록 허용합니다. Python 3가 필요합니다. macOS에서는 접근성 권한도 필요합니다.',
'settings.computerUse.enabledToggle': '사용',
'settings.computerUse.enableRiskTitle': 'Computer Use를 활성화할까요?',
@@ -2656,6 +2661,8 @@ export const kr: Record<TranslationKey, string> = {
'chat.addSelectionToChat': '채팅에 추가',
'chat.imageLoadFailed': '이미지를 불러올 수 없습니다',
'chat.imageLoadFailedHint': '파일이 손상되었거나 로컬 서버가 응답하지 않았습니다.',
'chat.imageCountOne': '이미지 {count}개',
'chat.imageCountOther': '이미지 {count}개',
'chat.retryImage': '이미지 다시 시도: {name}',
'chat.imageRetrying': '다시 시도 중…',
'chat.branchFromHere': '새 대화 분기',
@@ -3592,6 +3599,8 @@ export const kr: Record<TranslationKey, string> = {
'trace.detail.earlierMessages': '이전 메시지 {count}개 보기',
'trace.detail.chars': '{count}자',
'trace.detail.thinking': '생각',
'trace.detail.imageBlock': '[이미지]',
'trace.detail.stack': '스택',
// ─── App Shell ──────────────────────────────────────
'app.serverFailed': '로컬 서버를 시작하지 못했습니다',
@@ -3865,6 +3874,12 @@ export const kr: Record<TranslationKey, string> = {
'mermaid.zoomIn': '확대',
'mermaid.fit': '맞춤',
'mermaid.fitDiagram': '다이어그램을 화면에 맞추기',
'mermaid.renderError': 'Mermaid 렌더링 오류',
'mermaid.rendering': '다이어그램을 렌더링하는 중…',
'mermaid.previewTitle': 'Mermaid 다이어그램',
'mermaid.previewHint': '확대/축소 버튼으로 다이어그램을 키울 수 있습니다. 미리 보기 안에서 드래그해 이동하거나 트랙패드, 마우스 휠, 스크롤바를 사용하세요. Ctrl/Command를 누른 채 스크롤하면 확대/축소됩니다.',
'mermaid.inlineCanvas': 'Mermaid 인라인 캔버스',
'mermaid.previewCanvas': 'Mermaid 미리 보기 캔버스',
'codeViewer.collapse': '접기',
'codeViewer.showMoreLines': '{count}줄 더 보기',
'common.copy': '복사',
+15
View File
@@ -708,6 +708,7 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.timeGroup.older': '更早',
'sidebar.collapse': '摺疊側邊欄',
'sidebar.expand': '展開側邊欄',
'sidebar.regionLabel': '側邊欄',
'sidebar.resize': '拖曳調整側邊欄寬度',
// ─── Title Bar ──────────────────────────────────────
@@ -766,6 +767,7 @@ export const zh: Record<TranslationKey, string> = {
'workspace.searchResultsTruncated': '顯示前 {count} 個結果,請縮小搜尋範圍。',
'workspace.reviewWorkspace': '審閱工作區',
'workspace.itemsCount': '{count} 個項目',
'workspace.diffGridLabel': '{path} 的差異',
'workspace.filteredItemsCount': '{visible} / {total} 個項目',
'workspace.refresh': '重新整理工作區',
'workspace.resizePanel': '調整工作區寬度',
@@ -1135,6 +1137,7 @@ export const zh: Record<TranslationKey, string> = {
// Settings > Usage
'settings.activity.title': 'Token 用量',
'settings.activity.profileTitle': '個人資料',
'settings.activity.avatarAlt': '{name} 的頭像',
'settings.activity.profilePrivacy': '僅本地',
'settings.activity.defaultHandle': 'github.com/NanmiCoder/cc-haha',
'settings.activity.editProfile': '編輯個人資料',
@@ -1447,6 +1450,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.providers.sonnetModel': 'Sonnet 模型',
'settings.providers.opusModel': 'Opus 模型',
'settings.providers.model1mSupportShort': '1M',
'settings.providers.model1mSupportAria': '1M 上下文支援:{slot}',
'settings.providers.model1mSupportHint': '「1M」只在該模型端點確實提供 100 萬 token 上下文時勾選,勾錯會讓自動壓縮失效。',
'settings.providers.model1mSupportTooltip': '宣告該模型端點支援 100 萬 token 上下文:模型名稱會附加 [1m],上下文視窗自動填為 1,000,000。若端點實際視窗小於 1M,自動壓縮不會觸發,請求會撞上服務商的上下文硬限制。取消勾選會還原為預設視窗。',
'settings.providers.modelIdPlaceholder': '例如: deepseek-v4-flash',
@@ -2172,6 +2176,7 @@ export const zh: Record<TranslationKey, string> = {
// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
'settings.computerUse.title': 'Computer Use',
'settings.computerUse.statusCheckFailed': '無法檢查狀態。',
'settings.computerUse.description': '允許 Claude 截圖、點選、打字並控制你的電腦。需要 Python 3,macOS 上還需要輔助功能許可權。',
'settings.computerUse.enabledToggle': '啟用',
'settings.computerUse.enableRiskTitle': '開啟 Computer Use?',
@@ -2653,6 +2658,8 @@ export const zh: Record<TranslationKey, string> = {
'chat.addSelectionToChat': '新增到對話',
'chat.imageLoadFailed': '無法載入圖片',
'chat.imageLoadFailedHint': '檔案可能已損壞,或本機服務暫時沒有回應。',
'chat.imageCountOne': '{count} 張圖片',
'chat.imageCountOther': '{count} 張圖片',
'chat.retryImage': '重試圖片:{name}',
'chat.imageRetrying': '正在重試…',
'chat.branchFromHere': 'Fork 一個新對話',
@@ -3589,6 +3596,8 @@ export const zh: Record<TranslationKey, string> = {
'trace.detail.earlierMessages': '展開更早的 {count} 則訊息',
'trace.detail.chars': '{count} 字元',
'trace.detail.thinking': '思考',
'trace.detail.imageBlock': '[圖片]',
'trace.detail.stack': '呼叫堆疊',
// ─── 應用外殼 ──────────────────────────────────────
'app.serverFailed': '本地服務啟動失敗',
@@ -3862,6 +3871,12 @@ export const zh: Record<TranslationKey, string> = {
'mermaid.zoomIn': '放大',
'mermaid.fit': '適應',
'mermaid.fitDiagram': '縮放至適應圖表',
'mermaid.renderError': 'Mermaid 渲染失敗',
'mermaid.rendering': '正在渲染圖表…',
'mermaid.previewTitle': 'Mermaid 圖表',
'mermaid.previewHint': '用縮放按鈕放大圖表。在預覽區內拖曳可平移,也可以用觸控板、滑鼠滾輪或捲軸;捲動時按住 Ctrl/Command 可縮放。',
'mermaid.inlineCanvas': 'Mermaid 內嵌畫布',
'mermaid.previewCanvas': 'Mermaid 預覽畫布',
'codeViewer.collapse': '收合',
'codeViewer.showMoreLines': '顯示其餘 {count} 行',
'common.copy': '複製',
+15
View File
@@ -707,6 +707,7 @@ export const zh: Record<TranslationKey, string> = {
'sidebar.timeGroup.older': '更早',
'sidebar.collapse': '折叠侧边栏',
'sidebar.expand': '展开侧边栏',
'sidebar.regionLabel': '侧边栏',
'sidebar.resize': '拖拽调整侧边栏宽度',
// ─── Title Bar ──────────────────────────────────────
@@ -765,6 +766,7 @@ export const zh: Record<TranslationKey, string> = {
'workspace.searchResultsTruncated': '显示前 {count} 个结果,请缩小搜索范围。',
'workspace.reviewWorkspace': '审阅工作区',
'workspace.itemsCount': '{count} 个项目',
'workspace.diffGridLabel': '{path} 的差异',
'workspace.filteredItemsCount': '{visible} / {total} 个项目',
'workspace.refresh': '刷新工作区',
'workspace.resizePanel': '调整工作区宽度',
@@ -1134,6 +1136,7 @@ export const zh: Record<TranslationKey, string> = {
// Settings > Usage
'settings.activity.title': 'Token 用量',
'settings.activity.profileTitle': '个人资料',
'settings.activity.avatarAlt': '{name} 的头像',
'settings.activity.profilePrivacy': '仅本地',
'settings.activity.defaultHandle': 'github.com/NanmiCoder/cc-haha',
'settings.activity.editProfile': '编辑个人资料',
@@ -1446,6 +1449,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.providers.sonnetModel': 'Sonnet 模型',
'settings.providers.opusModel': 'Opus 模型',
'settings.providers.model1mSupportShort': '1M',
'settings.providers.model1mSupportAria': '1M 上下文支持:{slot}',
'settings.providers.model1mSupportHint': '「1M」只在该模型端点确实提供 100 万 token 上下文时勾选,勾错会让自动压缩失效。',
'settings.providers.model1mSupportTooltip': '声明该模型端点支持 100 万 token 上下文:模型名会追加 [1m],上下文窗口自动填为 1,000,000。若端点实际窗口小于 1M,自动压缩不会触发,请求会撞上服务商的上下文硬限制。取消勾选会恢复预设窗口。',
'settings.providers.modelIdPlaceholder': '例如: deepseek-v4-flash',
@@ -2171,6 +2175,7 @@ export const zh: Record<TranslationKey, string> = {
// Settings > Computer Use
'settings.tab.computerUse': 'Computer Use',
'settings.computerUse.title': 'Computer Use',
'settings.computerUse.statusCheckFailed': '无法检查状态。',
'settings.computerUse.description': '允许 Claude 截屏、点击、打字并控制你的电脑。需要 Python 3,macOS 上还需要辅助功能权限。',
'settings.computerUse.enabledToggle': '启用',
'settings.computerUse.enableRiskTitle': '开启 Computer Use?',
@@ -2652,6 +2657,8 @@ export const zh: Record<TranslationKey, string> = {
'chat.addSelectionToChat': '添加到对话',
'chat.imageLoadFailed': '无法加载图片',
'chat.imageLoadFailedHint': '文件可能已损坏,或本地服务暂时没有响应。',
'chat.imageCountOne': '{count} 张图片',
'chat.imageCountOther': '{count} 张图片',
'chat.retryImage': '重试图片:{name}',
'chat.imageRetrying': '正在重试…',
'chat.branchFromHere': 'Fork 一个新对话',
@@ -3588,6 +3595,8 @@ export const zh: Record<TranslationKey, string> = {
'trace.detail.earlierMessages': '展开更早的 {count} 条消息',
'trace.detail.chars': '{count} 字符',
'trace.detail.thinking': '思考',
'trace.detail.imageBlock': '[图片]',
'trace.detail.stack': '调用栈',
// ─── 应用外壳 ──────────────────────────────────────
'app.serverFailed': '本地服务启动失败',
@@ -3861,6 +3870,12 @@ export const zh: Record<TranslationKey, string> = {
'mermaid.zoomIn': '放大',
'mermaid.fit': '适应',
'mermaid.fitDiagram': '缩放至适应图表',
'mermaid.renderError': 'Mermaid 渲染失败',
'mermaid.rendering': '正在渲染图表…',
'mermaid.previewTitle': 'Mermaid 图表',
'mermaid.previewHint': '用缩放按钮放大图表。在预览区内拖动可平移,也可以用触控板、鼠标滚轮或滚动条;滚动时按住 Ctrl/Command 可缩放。',
'mermaid.inlineCanvas': 'Mermaid 内嵌画布',
'mermaid.previewCanvas': 'Mermaid 预览画布',
'codeViewer.collapse': '收起',
'codeViewer.showMoreLines': '显示其余 {count} 行',
'common.copy': '复制',
+1 -1
View File
@@ -776,7 +776,7 @@ export function ActivitySettings() {
<div className="relative h-16 w-16 overflow-hidden rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] shadow-[var(--shadow-card)]">
<img
src={avatarSrc}
alt={`${profile.displayName} avatar`}
alt={t('settings.activity.avatarAlt', { name: profile.displayName })}
className={avatarClassName}
onError={(event) => {
event.currentTarget.src = DEFAULT_AVATAR_SRC
+3 -3
View File
@@ -284,7 +284,7 @@ export function ComputerUseSettings() {
{checkState === 'error' ? (
<ErrorState
size="lg"
title="Failed to check status."
title={t('settings.computerUse.statusCheckFailed')}
retryLabel={t('common.retry')}
onRetry={fetchStatus}
/>
@@ -428,7 +428,7 @@ export function ComputerUseSettings() {
) : checkState === 'error' ? (
<ErrorState
size="lg"
title="Failed to check status."
title={t('settings.computerUse.statusCheckFailed')}
retryLabel={t('common.retry')}
onRetry={fetchStatus}
/>
@@ -742,7 +742,7 @@ function NativeComputerUse({
{configErrorNotice}
<ErrorState
size="lg"
title="Failed to check status."
title={t('settings.computerUse.statusCheckFailed')}
retryLabel={t('common.retry')}
onRetry={onRecheck}
tone="strong"
@@ -1904,7 +1904,7 @@ function ProviderFormModal({ open, onClose, mode, provider, presets, browserMode
type="checkbox"
checked={model1mSupport[slot]}
onChange={(e) => handleModel1mSupportChange(slot, e.target.checked)}
aria-label={`1M support: ${slot}`}
aria-label={t('settings.providers.model1mSupportAria', { slot })}
className="h-3.5 w-3.5 rounded border-[var(--color-border)] text-[var(--color-brand)] accent-[var(--color-brand)] focus:ring-[var(--color-brand)]"
/>
<span>{t('settings.providers.model1mSupportShort')}</span>