+ {/* TODO i18n: missing key codex.providerBridgeReference */}
Provider / bridge reference
@@ -131,6 +142,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
@@ -139,34 +151,39 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<>
+ {/* TODO i18n: missing key codex.saveProviderNamedCliproxy */}
Save a provider named cliproxy with the base URL and env key above.
+ {/* TODO i18n: missing key codex.inTopLevelSetDefault */}
In Top-level settings , set Default provider to{' '}
cliproxy.
+ {/* TODO i18n: missing key codex.exportCliproxyApiKey */}
Export CLIPROXY_API_KEY in your shell before launching native
Codex.
@@ -174,6 +191,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
>
) : (
+ {/* TODO i18n: missing key codex.noConfigOverrides */}
This Codex build can still use the native path, but CCS-backed Codex routing via{' '}
ccsxp or ccs codex --target codex stays unavailable until
the detected Codex binary exposes --config overrides.
@@ -186,30 +204,34 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.configFile */}
Config file
- User config
+ {t('codex.userConfig')}
{diagnostics.file.exists ? (
) : (
)}
+ {/* TODO i18n: missing keys codex.path / codex.resolved / codex.size / codex.lastModified */}
{diagnostics.file.parseError && (
+ {/* TODO i18n: missing key codex.tomlWarning */}
TOML warning: {diagnostics.file.parseError}
)}
{diagnostics.file.readError && (
+ {/* TODO i18n: missing key codex.readWarning */}
Read warning: {diagnostics.file.readError}
)}
@@ -221,34 +243,46 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.currentUserLayerSummary */}
Current user-layer summary
-
+ {/* TODO i18n: missing key codex.notSet */}
+
-
+
+ {/* TODO i18n: missing keys codex.providersCount / codex.profilesCount / codex.enabledFeaturesCount / codex.mcpServersCount */}
providers: {diagnostics.config.modelProviderCount}
@@ -265,6 +299,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
{diagnostics.config.topLevelKeys.length > 0 && (
+ {/* TODO i18n: missing key codex.userLayerKeysPresent */}
User-layer keys present
@@ -282,28 +317,31 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.runtimeVsProvider */}
Runtime vs provider
-
Native Codex runtime
+
{t('codex.nativeCodexRuntime')}
ccs-codex
@@ -334,11 +373,12 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.honorsSavedNativeConfig */}
Honors saved native user config
-
CCS Codex provider / bridge
+
{t('codex.ccsCodexProvider')}
{supportsManagedRouting ? (
<>
@@ -350,11 +390,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.usesTransientOverrides */}
Uses transient overrides
>
) : (
+ {/* TODO i18n: missing key codex.unavailableNoConfig */}
Unavailable (Codex build lacks --config support).
)}
@@ -364,15 +406,15 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
- Supported flows
+ {t('codex.supportedFlows')}
- Flow
- Status
- Notes
+ {t('codex.flow')}
+ {t('codex.status')}
+ {t('codex.notes')}
@@ -381,6 +423,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
{entry.label}
+ {/* TODO i18n: missing keys common.yes / common.no */}
{entry.supported ? 'Yes' : 'No'}
@@ -397,6 +440,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
+ {/* TODO i18n: missing key codex.warnings */}
Warnings
diff --git a/ui/src/components/compatible-cli/codex-profiles-card.tsx b/ui/src/components/compatible-cli/codex-profiles-card.tsx
index f2e002a7..82c63de0 100644
--- a/ui/src/components/compatible-cli/codex-profiles-card.tsx
+++ b/ui/src/components/compatible-cli/codex-profiles-card.tsx
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import { Layers3, Loader2, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
@@ -62,6 +63,7 @@ function ProfileEditor({
onDelete,
onSetActive,
}: ProfileEditorProps) {
+ const { t } = useTranslation();
const [nameDraft, setNameDraft] = useState(initialName);
const [modelDraft, setModelDraft] = useState(initialModel);
const [providerDraft, setProviderDraft] = useState(initialProvider);
@@ -88,10 +90,10 @@ function ProfileEditor({
disabled={disabled}
>
-
+
- Use global provider
+ {t('codex.useGlobalProvider')}
{providerNames.map((name) => (
{name}
@@ -105,10 +107,10 @@ function ProfileEditor({
disabled={disabled}
>
-
+
- Use global effort
+ {t('codex.useGlobalEffort')}
{['minimal', 'low', 'medium', 'high', 'xhigh'].map((value) => (
{value}
@@ -126,6 +128,7 @@ function ProfileEditor({
disabled={disabled || saving || !selectedEntryName}
>
+ {/* TODO i18n: missing key common.delete */}
Delete
+ {/* TODO i18n: missing key codex.setActive */}
Set active
@@ -155,6 +159,7 @@ function ProfileEditor({
disabled={disabled || saving || nameDraft.trim().length === 0}
>
{saving ? : null}
+ {/* TODO i18n: missing key codex.saveProfile */}
Save profile
{saving ? : null}
+ {/* TODO i18n: missing key codex.saveAndActivate */}
Save + activate
@@ -191,6 +197,8 @@ export function CodexProfilesCard({
onDelete,
onSetActive,
}: CodexProfilesCardProps) {
+ const { t } = useTranslation();
+
const [selectedName, setSelectedName] = useState('new');
const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null,
@@ -200,21 +208,24 @@ export function CodexProfilesCard({
return (
}
+ // TODO i18n: missing key codex.profilesDesc
description="Create reusable Codex overlays and set the active default profile."
disabledReason={disabledReason}
>
+ {/* TODO i18n: missing key codex.selectProfile */}
- Create new profile
+ {t('codex.createNewProfile')}
{entries.map((entry) => (
{entry.name}
+ {/* TODO i18n: missing key codex.activeSuffix */}
{entry.name === activeProfile ? ' (active)' : ''}
))}
diff --git a/ui/src/components/compatible-cli/codex-project-trust-card.tsx b/ui/src/components/compatible-cli/codex-project-trust-card.tsx
index 7d265080..f712d180 100644
--- a/ui/src/components/compatible-cli/codex-project-trust-card.tsx
+++ b/ui/src/components/compatible-cli/codex-project-trust-card.tsx
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { FolderCheck, Loader2, Trash2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
@@ -34,6 +35,7 @@ function ProjectTrustComposer({
saving,
onSave,
}: ProjectTrustComposerProps) {
+ const { t } = useTranslation();
const [pathDraft, setPathDraft] = useState(workspacePath);
const [trustLevel, setTrustLevel] = useState('trusted');
@@ -50,12 +52,13 @@ function ProjectTrustComposer({
- trusted
- untrusted
+ {t('codex.trusted')}
+ {t('codex.untrusted')}
onSave(pathDraft, trustLevel)} disabled={disabled || saving}>
{saving ? : null}
+ {/* TODO i18n: missing key codex.saveTrust */}
Save trust
@@ -70,15 +73,20 @@ export function CodexProjectTrustCard({
saving = false,
onSave,
}: CodexProjectTrustCardProps) {
+ const { t } = useTranslation();
+
return (
}
+ // TODO i18n: missing key codex.projectTrustDesc
description="Trust current workspaces or remove stale trust entries without opening raw TOML."
disabledReason={disabledReason}
>
+ {/* TODO i18n: missing key codex.trustPathsHint */}
Paths must be absolute or start with ~/. Relative paths are rejected so CCS
does not trust the wrong folder.
@@ -96,12 +104,13 @@ export function CodexProjectTrustCard({
onClick={() => onSave(workspacePath, 'trusted')}
disabled={disabled || saving}
>
+ {/* TODO i18n: missing key codex.trustCurrentWorkspace */}
Trust current workspace
{entries.length === 0 ? (
-
No explicit project trust entries saved.
+
{t('codex.noProjectTrustEntries')}
) : (
entries.map((entry) => (
+ {/* TODO i18n: missing key codex.toggle */}
Toggle
(initialValues);
const reasoningOptions = withCurrentValue(
['minimal', 'low', 'medium', 'high', 'xhigh'],
@@ -121,7 +124,7 @@ function TopLevelControlsForm({
<>
-
Model
+
{t('codex.model')}
@@ -133,7 +136,7 @@ function TopLevelControlsForm({
-
Reasoning effort
+
{t('codex.reasoningEffort')}
@@ -145,10 +148,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use default
+ {t('codex.useDefault')}
{reasoningOptions.map((value) => (
{value}
@@ -159,7 +162,7 @@ function TopLevelControlsForm({
-
Default provider
+
{t('codex.defaultProvider')}
@@ -168,10 +171,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use Codex default
+ {t('codex.useCodexDefault')}
{providerOptions.map((name) => (
{name}
@@ -182,7 +185,7 @@ function TopLevelControlsForm({
-
Approval policy
+
{t('codex.approvalPolicy')}
@@ -191,10 +194,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use default
+ {t('codex.useDefault')}
{approvalOptions.map((value) => (
{value}
@@ -205,7 +208,7 @@ function TopLevelControlsForm({
-
Sandbox mode
+
{t('codex.sandboxMode')}
@@ -214,10 +217,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use default
+ {t('codex.useDefault')}
{sandboxOptions.map((value) => (
{value}
@@ -228,7 +231,7 @@ function TopLevelControlsForm({
-
Web search
+
{t('codex.webSearch')}
@@ -237,10 +240,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use default
+ {t('codex.useDefault')}
{webSearchOptions.map((value) => (
{value}
@@ -251,7 +254,7 @@ function TopLevelControlsForm({
-
Tool output token limit
+
{t('codex.toolOutputTokenLimit')}
-
Personality
+
{t('codex.personality')}
@@ -277,10 +280,10 @@ function TopLevelControlsForm({
disabled={disabled}
>
-
+
- Use default
+ {t('codex.useDefault')}
{personalityOptions.map((value) => (
{value}
@@ -296,21 +299,24 @@ function TopLevelControlsForm({
-
Long context override
+
{t('codex.longContextOverride')}
+ {/* TODO i18n: missing key codex.manualOptInOnly */}
Manual opt-in only
+ {/* TODO i18n: missing keys codex.gpt54Selected / codex.gpt54Reference */}
{isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'}
+ {/* TODO i18n: missing key codex.draftValuesOnly */}
Draft values only. Nothing applies until Save.
@@ -329,6 +335,7 @@ function TopLevelControlsForm({
}))
}
>
+ {/* TODO i18n: missing key codex.fillCautiousPair */}
Fill cautious pair
+ {/* TODO i18n: missing key codex.setOfficialMaxWindow */}
Set official max window
+ {/* TODO i18n: missing key codex.clear */}
Clear
@@ -366,26 +375,31 @@ function TopLevelControlsForm({
+ {/* TODO i18n: missing key codex.officialMax */}
Official max
1.05M / 1M
-
GPT-5.4 context cap
+
{t('codex.gptContextCap')}
+ {/* TODO i18n: missing key codex.standardWindow */}
Standard window
{formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)}
-
Normal usage window
+
{t('codex.normalUsageWindow')}
+ {/* TODO i18n: missing key codex.above272k */}
Above 272K
-
Counts 2x
-
Usage-limit cost above 272K
+
+ {t('codex.counts2x')}
+
+
{t('codex.usageLimitCost')}
@@ -393,12 +407,15 @@ function TopLevelControlsForm({
+ {/* TODO i18n: missing key codex.oneCautiousPair */}
One cautious pair
+ {/* TODO i18n: missing key codex.context */}
Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)}
+ {/* TODO i18n: missing key codex.autoCompact */}
Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)}
@@ -407,20 +424,23 @@ function TopLevelControlsForm({
variant="outline"
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
>
+ {/* TODO i18n: missing key codex.notOfficial */}
Not official
+ {/* TODO i18n: missing key codex.draftOnly */}
Draft only
-
Quick-fill only. Review before saving.
+
{t('codex.quickFillWarning')}
{!isGpt54Selected && draft.model ? (
+ {/* TODO i18n: missing key codex.shouldBeCheckedSeparately */}
{draft.model} should be checked separately.
) : null}
@@ -429,9 +449,9 @@ function TopLevelControlsForm({
-
Model context window
+
{t('codex.modelContextWindow')}
+ {/* TODO i18n: missing key codex.writesModelContextWindow */}
Writes model_context_window. Leave unset to keep Codex defaults.
-
Auto-compact token limit
+
{t('codex.autoCompactTokenLimit')}
+ {/* TODO i18n: missing key codex.writesAutoCompactTokenLimit */}
Writes model_auto_compact_token_limit. Leave unset to keep model
defaults.
@@ -473,6 +497,7 @@ function TopLevelControlsForm({
@@ -504,6 +532,7 @@ function TopLevelControlsForm({
onSave(patch)} disabled={disabled || saving || !hasChanges}>
{saving ? : null}
+ {/* TODO i18n: missing key codex.saveTopLevelSettings */}
Save top-level settings
@@ -521,9 +550,11 @@ export function CodexTopLevelControlsCard({
}: CodexTopLevelControlsCardProps) {
return (
}
+ // TODO i18n: missing key codex.topLevelControlsDesc
description="Structured controls for the stable top-level Codex settings users touch most often. Unsupported upstream shapes stay untouched and should be edited in raw TOML."
disabledReason={disabledReason}
>
diff --git a/ui/src/components/compatible-cli/droid-byok-reasoning-controls-card.tsx b/ui/src/components/compatible-cli/droid-byok-reasoning-controls-card.tsx
index c65b9390..82768200 100644
--- a/ui/src/components/compatible-cli/droid-byok-reasoning-controls-card.tsx
+++ b/ui/src/components/compatible-cli/droid-byok-reasoning-controls-card.tsx
@@ -1,4 +1,5 @@
import { BrainCircuit } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -39,12 +40,14 @@ export function DroidByokReasoningControlsCard({
onEffortChange,
onAnthropicBudgetChange,
}: DroidByokReasoningControlsCardProps) {
+ const { t } = useTranslation();
+
return (
- BYOK Reasoning / Thinking
+ {t('droidSettings.reasoningControls')}
customModels
@@ -55,6 +58,7 @@ export function DroidByokReasoningControlsCard({
{models.length === 0 ? (
+ {/* TODO i18n: missing key droidSettings.noByokModels */}
No BYOK custom models found in settings.json (`customModels` or `custom_models`).
) : (
@@ -75,7 +79,9 @@ export function DroidByokReasoningControlsCard({
-
Reasoning Effort
+
+ {t('codex.reasoningEffortCapitalized')}
+
@@ -84,9 +90,11 @@ export function DroidByokReasoningControlsCard({
disabled={disabled}
>
+ {/* TODO i18n: missing key droidSettings.useProviderDefault */}
+ {/* TODO i18n: missing key droidSettings.useProviderDefault */}
Use provider default
{DROID_REASONING_EFFORT_OPTIONS.map((option) => (
@@ -99,7 +107,7 @@ export function DroidByokReasoningControlsCard({
{model.providerKind === 'anthropic' && (
-
Thinking Budget Tokens
+
{t('codex.thinkingBudgetTokens')}
- Quick Settings
+ {t('droidSettings.quickControls')}
settings.json
@@ -140,6 +143,7 @@ export function DroidSettingsQuickControlsCard({
{enumFieldConfig.map((field) => (
+ {/* TODO i18n: missing keys for droidSettings enum/boolean/number field labels */}
{field.label}
-
+
- Use default
+ {t('codex.useDefault')}
{field.options.map((option) => (
{option.label}
@@ -166,6 +170,7 @@ export function DroidSettingsQuickControlsCard({
{numberFieldConfig.map((field) => (
+ {/* TODO i18n: missing keys for droidSettings number field labels */}
{field.label}
(
+ {/* TODO i18n: missing keys for droidSettings boolean field labels */}
{field.label}
-
+
- Use default
+ {t('codex.useDefault')}
true
false
diff --git a/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx b/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx
index 7852d5b1..62dd91f6 100644
--- a/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx
+++ b/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx
@@ -67,6 +67,7 @@ export function RawConfigEditorPanel({
{title}
{dirty && (
+ {/* TODO i18n: missing key rawJsonSettingsEditor.unsaved */}
Unsaved
)}
@@ -80,15 +81,18 @@ export function RawConfigEditorPanel({
) : (
)}
+ {/* TODO i18n: missing key rawJsonSettingsEditor.save */}
Save
{onDiscard ? (
+ {/* TODO i18n: missing key rawJsonSettingsEditor.discard */}
Discard
) : null}
+ {/* TODO i18n: missing keys rawJsonSettingsEditor.copied / rawJsonSettingsEditor.copy */}
{copied ? 'Copied' : 'Copy'}
@@ -113,6 +117,7 @@ export function RawConfigEditorPanel({
)}
{readWarning && (
+ {/* TODO i18n: missing key rawJsonSettingsEditor.readOnly */}
Read-only: {readWarning}
)}
diff --git a/ui/src/components/copilot/config-form/header-section.tsx b/ui/src/components/copilot/config-form/header-section.tsx
index f276cff5..e5d748c6 100644
--- a/ui/src/components/copilot/config-form/header-section.tsx
+++ b/ui/src/components/copilot/config-form/header-section.tsx
@@ -6,6 +6,7 @@
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Loader2, Save, RefreshCw } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
interface RawSettings {
path: string;
@@ -35,12 +36,14 @@ export function HeaderSection({
onRefresh,
onSave,
}: HeaderSectionProps) {
+ const { t } = useTranslation();
+
return (
-
Copilot Configuration
+ {t('copilotConfigForm.copilotConfiguration')}
{rawSettings && (
copilot.settings.json
@@ -49,7 +52,8 @@ export function HeaderSection({
{rawSettings && (
- Last modified:{' '}
+ {/* TODO i18n: missing key copilotConfigForm.lastModified */}
+ Last modified: {/* TODO i18n: missing key copilotConfigForm.neverSaved */}
{rawSettings.exists ? new Date(rawSettings.mtime).toLocaleString() : 'Never saved'}
)}
@@ -67,11 +71,13 @@ export function HeaderSection({
{isUpdating || isSavingRawSettings ? (
<>
+ {/* TODO i18n: missing key copilotConfigForm.saving */}
Saving...
>
) : (
<>
+ {/* TODO i18n: missing key copilotConfigForm.save */}
Save
>
)}
diff --git a/ui/src/components/copilot/config-form/index.tsx b/ui/src/components/copilot/config-form/index.tsx
index 0acec851..61bc7033 100644
--- a/ui/src/components/copilot/config-form/index.tsx
+++ b/ui/src/components/copilot/config-form/index.tsx
@@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AlertTriangle, Code2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
import { HeaderSection } from './header-section';
@@ -22,6 +23,7 @@ import { RawEditorSection } from './raw-editor-section';
import { useCopilotConfigForm } from './use-copilot-config-form';
export function CopilotConfigForm() {
+ const { t } = useTranslation();
const {
configLoading,
rawSettingsLoading,
@@ -83,9 +85,10 @@ export function CopilotConfigForm() {
- Deprecated Copilot models detected
+ {t('copilotConfigForm.deprecatedModels')}
+ {/* TODO i18n: missing key copilotConfigForm.deprecatedModelsDesc */}
Loading this page did not rewrite your files. Save the Copilot configuration to
persist these replacements.
@@ -108,12 +111,15 @@ export function CopilotConfigForm() {
+ {/* TODO i18n: missing key copilotConfigForm.modelConfig */}
Model Config
+ {/* TODO i18n: missing key copilotConfigForm.settings */}
Settings
+ {/* TODO i18n: missing key copilotConfigForm.info */}
Info
diff --git a/ui/src/components/copilot/config-form/info-tab.tsx b/ui/src/components/copilot/config-form/info-tab.tsx
index 02973d7b..9e0d3b92 100644
--- a/ui/src/components/copilot/config-form/info-tab.tsx
+++ b/ui/src/components/copilot/config-form/info-tab.tsx
@@ -8,6 +8,7 @@ import { CopyButton } from '@/components/ui/copy-button';
import { Badge } from '@/components/ui/badge';
import { Info } from 'lucide-react';
import { TabsContent } from '@/components/ui/tabs';
+import { useTranslation } from 'react-i18next';
import { UsageCommand } from './usage-command';
interface RawSettings {
@@ -22,6 +23,7 @@ interface InfoTabProps {
}
export function InfoTab({ rawSettings }: InfoTabProps) {
+ const { t } = useTranslation();
return (
@@ -29,17 +31,22 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
+ {/* TODO i18n: missing key for 'Configuration Info' */}
Configuration Info
- Provider
+
+ {t('copilotConfigForm.provider')}
+
GitHub Copilot
{rawSettings && (
<>
-
File Path
+
+ {t('copilotConfigForm.filePath')}
+
{rawSettings.path}
@@ -48,7 +55,9 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
- Status
+
+ {t('copilotConfigForm.status')}
+
+ {/* TODO i18n: missing key for 'File exists' / 'Using defaults' */}
{rawSettings.exists ? 'File exists' : 'Using defaults'}
@@ -66,8 +76,9 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
-
Quick Usage
+
{t('copilotConfigForm.quickUsage')}
+ {/* TODO i18n: missing keys for usage command labels */}
diff --git a/ui/src/components/copilot/config-form/model-config-tab.tsx b/ui/src/components/copilot/config-form/model-config-tab.tsx
index e21e0e1b..b4148ac5 100644
--- a/ui/src/components/copilot/config-form/model-config-tab.tsx
+++ b/ui/src/components/copilot/config-form/model-config-tab.tsx
@@ -9,6 +9,7 @@ import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import { Sparkles, Zap } from 'lucide-react';
import { TabsContent } from '@/components/ui/tabs';
+import { useTranslation } from 'react-i18next';
import type { CopilotModel } from '@/hooks/use-copilot';
import { FREE_PRESETS, PAID_PRESETS } from './presets';
import { FlexibleModelSelector } from './model-selector';
@@ -70,6 +71,7 @@ export function ModelConfigTab({
onUpdateSonnetModel,
onUpdateHaikuModel,
}: ModelConfigTabProps) {
+ const { t } = useTranslation();
const mappedModelLimits = [
{ label: 'Default', id: currentModel },
{ label: 'Opus', id: opusModel || currentModel },
@@ -94,10 +96,10 @@ export function ModelConfigTab({
- Presets
+ {t('providerEditor.presets')}
- Apply pre-configured model mappings
+ {t('copilotConfigForm.modelMapping')}
{/* Free Tier Presets */}
@@ -107,9 +109,12 @@ export function ModelConfigTab({
variant="outline"
className="text-[10px] bg-green-100 text-green-700 border-green-200"
>
+ {/* TODO i18n: missing key for 'Free Tier' */}
Free Tier
-
No premium usage count
+
+ {t('copilotConfigForm.noPremiumUsage')}
+
{FREE_PRESETS.map((preset) => (
@@ -135,9 +140,11 @@ export function ModelConfigTab({
variant="outline"
className="text-[10px] bg-blue-100 text-blue-700 border-blue-200"
>
+ {/* TODO i18n: missing key for 'Pro+ Required' */}
Pro+ Required
+ {/* TODO i18n: missing key for 'Uses premium request quota' */}
Uses premium request quota
@@ -163,13 +170,15 @@ export function ModelConfigTab({
{/* Model Mapping */}
-
Model Mapping
+
{t('copilotConfigForm.modelMapping')}
+ {/* TODO i18n: missing key for model mapping description */}
Configure which models to use for each tier
-
GitHub Copilot controls prompt/context limits upstream.
+
{t('copilotConfigForm.githubCopilotControls')}
+ {/* TODO i18n: missing key for 'CCS can switch Copilot models...' */}
CCS can switch Copilot models, but it cannot increase the provider's max prompt
or context window.
@@ -183,6 +192,7 @@ export function ModelConfigTab({
) : (
+ {/* TODO i18n: missing key for 'Start the daemon to inspect...' */}
Start the daemon to inspect live model limits from GitHub Copilot metadata.
)}
diff --git a/ui/src/components/copilot/config-form/presets.ts b/ui/src/components/copilot/config-form/presets.ts
index f882f932..3b05abdc 100644
--- a/ui/src/components/copilot/config-form/presets.ts
+++ b/ui/src/components/copilot/config-form/presets.ts
@@ -5,6 +5,7 @@
import type { ModelPreset } from './types';
+// TODO i18n: missing keys for preset descriptions ("Free tier - no premium usage", etc.)
// Note: ALL Claude models require paid Copilot subscription
export const FREE_PRESETS: ModelPreset[] = [
{
@@ -25,6 +26,7 @@ export const FREE_PRESETS: ModelPreset[] = [
},
];
+// TODO i18n: missing keys for paid preset descriptions
export const PAID_PRESETS: ModelPreset[] = [
{
name: 'Claude Opus 4.5',
diff --git a/ui/src/components/copilot/config-form/raw-editor-section.tsx b/ui/src/components/copilot/config-form/raw-editor-section.tsx
index b1369951..7714692c 100644
--- a/ui/src/components/copilot/config-form/raw-editor-section.tsx
+++ b/ui/src/components/copilot/config-form/raw-editor-section.tsx
@@ -5,6 +5,7 @@
import { Suspense, lazy } from 'react';
import { Loader2, X, AlertTriangle } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
// Lazy load CodeEditor
@@ -29,6 +30,7 @@ export function RawEditorSection({
onChange,
missingRequiredFields = [],
}: RawEditorSectionProps) {
+ const { t } = useTranslation();
const hasMissingFields = missingRequiredFields.length > 0;
return (
@@ -36,7 +38,7 @@ export function RawEditorSection({
fallback={
- Loading editor...
+ {t('providerEditor.loadingEditor')}
}
>
@@ -44,6 +46,7 @@ export function RawEditorSection({
{!isRawJsonValid && rawJsonEdits !== null && (
+ {/* TODO i18n: missing key for 'Invalid JSON syntax' */}
Invalid JSON syntax
)}
@@ -52,12 +55,14 @@ export function RawEditorSection({
+ {/* TODO i18n: missing key for 'Missing required fields:' */}
Missing required fields:
{' '}
{missingRequiredFields.join(', ')}
+ {/* TODO i18n: missing key for 'These fields will use default values at runtime.' */}
These fields will use default values at runtime.
diff --git a/ui/src/components/copilot/config-form/use-copilot-config-form.ts b/ui/src/components/copilot/config-form/use-copilot-config-form.ts
index 32b5ccfd..30af409e 100644
--- a/ui/src/components/copilot/config-form/use-copilot-config-form.ts
+++ b/ui/src/components/copilot/config-form/use-copilot-config-form.ts
@@ -8,6 +8,7 @@ import { useCopilot } from '@/hooks/use-copilot';
import type { CopilotNormalizationWarning } from '@/hooks/use-copilot';
import { isApiConflictError } from '@/lib/api-client';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import type { ModelPreset } from './types';
/** Required env vars for Copilot settings (informational only - runtime fills defaults) */
@@ -31,6 +32,7 @@ function dedupeWarnings(
}
export function useCopilotConfigForm() {
+ const { t } = useTranslation();
const {
config,
configLoading,
@@ -91,7 +93,7 @@ export function useCopilotConfigForm() {
sonnetModel: preset.sonnet,
haikuModel: preset.haiku,
}));
- toast.success(`Applied "${preset.name}" preset`);
+ toast.success(t('toasts.presetApplied', { name: preset.name }));
};
// Raw JSON content
@@ -191,15 +193,15 @@ export function useCopilotConfigForm() {
}
if (uniqueWarnings.length > 0) {
- toast.warning('Copilot configuration saved with model adjustments', {
+ toast.warning(t('toasts.settingsSavedWithAdjustments'), {
description: descriptions.join(' '),
});
} else if (descriptions.length > 0) {
- toast.success('Copilot configuration saved', {
+ toast.success(t('toasts.settingsSaved'), {
description: descriptions.join(' '),
});
} else {
- toast.success('Copilot configuration saved');
+ toast.success(t('toasts.settingsSaved'));
}
// Clear local state
@@ -209,7 +211,7 @@ export function useCopilotConfigForm() {
if (isApiConflictError(error)) {
setConflictDialog(true);
} else {
- toast.error('Failed to save settings');
+ toast.error(t('toasts.failedSaveSettings'));
}
}
};
diff --git a/ui/src/components/copilot/config-form/utils.ts b/ui/src/components/copilot/config-form/utils.ts
index 7fb71aad..2c004ddc 100644
--- a/ui/src/components/copilot/config-form/utils.ts
+++ b/ui/src/components/copilot/config-form/utils.ts
@@ -25,6 +25,7 @@ export function getPlanBadgeStyle(plan?: CopilotPlanTier): string {
/** Get multiplier display */
export function getMultiplierDisplay(multiplier?: number): string | null {
if (multiplier === undefined || multiplier === null) return null;
+ // TODO i18n: missing key for 'Free' multiplier label
if (multiplier === 0) return 'Free';
if (multiplier < 1) return `${multiplier}x`;
if (multiplier === 1) return '1x';
diff --git a/ui/src/components/copilot/copilot-status-card.tsx b/ui/src/components/copilot/copilot-status-card.tsx
index a877d3d8..19e9c840 100644
--- a/ui/src/components/copilot/copilot-status-card.tsx
+++ b/ui/src/components/copilot/copilot-status-card.tsx
@@ -9,8 +9,10 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useCopilot } from '@/hooks/use-copilot';
import { CheckCircle2, XCircle, AlertTriangle, Loader2, Download } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
export function CopilotStatusCard() {
+ const { t } = useTranslation();
const {
status,
statusLoading,
@@ -28,7 +30,7 @@ export function CopilotStatusCard() {
return (
- GitHub Copilot Status
+ {t('copilotPage.status')}
@@ -41,10 +43,10 @@ export function CopilotStatusCard() {
return (
- GitHub Copilot Status
+ {t('copilotPage.status')}
- Failed to load status
+ {t('copilotConfigForm.failedLoadStatus')}
);
@@ -54,21 +56,21 @@ export function CopilotStatusCard() {
- GitHub Copilot Status
+ {t('copilotPage.status')}
{status.enabled ? (
- Enabled
+ {t('copilotPage.enabled')}
) : (
- Disabled
+ {t('copilotPage.disabled')}
)}
- Use your GitHub Copilot subscription with Claude Code
+ {t('copilotConfigForm.useWithClaudeCode')}
{/* Warning Banner */}
- This uses a reverse-engineered API. Excessive usage may trigger GitHub abuse detection.
+ {t('copilotPage.unofficialItem2')}
@@ -82,7 +84,7 @@ export function CopilotStatusCard() {
)}
- copilot-api {status.installed ? `v${status.version}` : 'Not Installed'}
+ copilot-api {status.installed ? `v${status.version}` : t('copilotPage.missing')}
@@ -94,7 +96,7 @@ export function CopilotStatusCard() {
)}
- {status.authenticated ? 'Authenticated' : 'Not Authenticated'}
+ {status.authenticated ? t('copilotPage.connected') : t('copilotPage.notConnected')}
@@ -105,15 +107,25 @@ export function CopilotStatusCard() {
) : (
)}
-
Daemon {status.daemon_running ? 'Running' : 'Stopped'}
+
+ {t('copilotPage.daemon')}{' '}
+ {status.daemon_running ? t('copilotPage.running') : t('copilotPage.stopped')}
+
{/* Quick Info */}
- Port: {status.port}
- Model: {status.model}
- Auto-start: {status.auto_start ? 'Yes' : 'No'}
+
+ {t('copilotPage.port')}: {status.port}
+
+
+ {t('providerEditor.modelMapping')}: {status.model}
+
+
+ {/* TODO i18n: missing key for 'Auto-start' */}
+ Auto-start: {status.auto_start ? t('copilotPage.yes') : t('copilotPage.no')}
+
{/* Actions */}
@@ -123,12 +135,12 @@ export function CopilotStatusCard() {
{isInstalling ? (
<>
- Installing...
+ {t('copilotPage.installing')}
>
) : (
<>
- Install copilot-api
+ {t('copilotPage.installCopilotApi')}
>
)}
@@ -143,10 +155,10 @@ export function CopilotStatusCard() {
{isAuthenticating ? (
<>
- Authenticating...
+ {t('copilotPage.authenticating')}
>
) : (
- 'Authenticate with GitHub'
+ t('copilotPage.authenticate')
)}
)}
@@ -161,10 +173,10 @@ export function CopilotStatusCard() {
{isStoppingDaemon ? (
<>
- Stopping...
+ {t('copilotPage.stopping')}
>
) : (
- 'Stop Daemon'
+ t('copilotPage.stop')
)}
) : (
@@ -177,10 +189,10 @@ export function CopilotStatusCard() {
{isStartingDaemon ? (
<>
- Starting...
+ {t('copilotPage.starting')}
>
) : (
- 'Start Daemon'
+ t('copilotPage.start')
)}
)}
diff --git a/ui/src/components/health/health-card.tsx b/ui/src/components/health/health-card.tsx
index c3097596..71a74f6a 100644
--- a/ui/src/components/health/health-card.tsx
+++ b/ui/src/components/health/health-card.tsx
@@ -2,6 +2,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { CheckCircle, AlertTriangle, XCircle, Wrench } from 'lucide-react';
import { useFixHealth } from '@/hooks/use-health';
+import { useTranslation } from 'react-i18next';
interface HealthCheck {
id: string;
@@ -35,6 +36,7 @@ const statusConfig = {
export function HealthCard({ check }: { check: HealthCheck }) {
const fixMutation = useFixHealth();
+ const { t } = useTranslation();
const config = statusConfig[check.status];
const Icon = config.icon;
@@ -54,7 +56,7 @@ export function HealthCard({ check }: { check: HealthCheck }) {
disabled={fixMutation.isPending}
>
- Fix
+ {t('health.fix')}
)}
diff --git a/ui/src/components/layout/hero-section.tsx b/ui/src/components/layout/hero-section.tsx
index 40ea83d9..eb7556ab 100644
--- a/ui/src/components/layout/hero-section.tsx
+++ b/ui/src/components/layout/hero-section.tsx
@@ -1,24 +1,27 @@
import { Badge } from '@/components/ui/badge';
import { CcsLogo } from '@/components/shared/ccs-logo';
+import { useTranslation } from 'react-i18next';
interface HeroSectionProps {
version?: string;
}
export function HeroSection({ version }: HeroSectionProps) {
+ const { t } = useTranslation();
+
return (
-
CCS Config
+ {t('heroSection.title')}
{version && (
v{version}
)}
-
Claude Code Switch Dashboard
+
{t('heroSection.subtitle')}
);
diff --git a/ui/src/components/layout/hub-footer.tsx b/ui/src/components/layout/hub-footer.tsx
index a81539f8..1d6fb820 100644
--- a/ui/src/components/layout/hub-footer.tsx
+++ b/ui/src/components/layout/hub-footer.tsx
@@ -1,26 +1,28 @@
import { Separator } from '@/components/ui/separator';
import { Button } from '@/components/ui/button';
import { FileTextIcon, SettingsIcon, GithubIcon, ExternalLinkIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
export function HubFooter() {
+ const { t } = useTranslation();
const currentYear = new Date().getFullYear();
const footerLinks = [
{
icon: ,
- label: 'Logs',
+ label: t('hubFooter.logs'),
href: '#logs',
onClick: () => console.log('Navigate to Logs'),
},
{
icon: ,
- label: 'Settings',
+ label: t('hubFooter.settings'),
href: '#settings',
onClick: () => console.log('Navigate to Settings'),
},
{
icon: ,
- label: 'GitHub',
+ label: t('hubFooter.github'),
href: 'https://github.com/kaitranntt/ccs',
external: true,
},
@@ -32,7 +34,7 @@ export function HubFooter() {
CCS v0.0.0
- © {currentYear} kaitranntt
+ {t('hubFooter.copyright', { year: currentYear })}
diff --git a/ui/src/components/layout/theme-toggle.tsx b/ui/src/components/layout/theme-toggle.tsx
index 619b3a11..c1fb7a2e 100644
--- a/ui/src/components/layout/theme-toggle.tsx
+++ b/ui/src/components/layout/theme-toggle.tsx
@@ -1,9 +1,11 @@
import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useTheme } from '@/hooks/use-theme';
+import { useTranslation } from 'react-i18next';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
+ const { t } = useTranslation();
return (
- Toggle theme
+ {t('themeToggle.srLabel')}
);
}
diff --git a/ui/src/components/logs/logs-config-card.tsx b/ui/src/components/logs/logs-config-card.tsx
index 4fe573a3..22893bdb 100644
--- a/ui/src/components/logs/logs-config-card.tsx
+++ b/ui/src/components/logs/logs-config-card.tsx
@@ -13,6 +13,7 @@ import {
import { Switch } from '@/components/ui/switch';
import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client';
import { cn } from '@/lib/utils';
+// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
function parseInteger(value: string, fallback: number) {
const parsed = Number.parseInt(value, 10);
@@ -32,6 +33,8 @@ export function LogsConfigCard({
onSave: (payload: UpdateLogsConfigPayload) => void;
isPending: boolean;
}) {
+ // TODO i18n: uncomment when keys for Commit Changes, Rollback Draft, etc. are added
+ // const { t } = useTranslation();
const [draft, setDraft] = useState(config);
useEffect(() => {
@@ -247,6 +250,7 @@ export function LogsConfigCard({
className="h-10 w-full gap-2 rounded-xl bg-primary text-[11px] font-semibold uppercase tracking-[0.14em] shadow-lg shadow-primary/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
>
+ {/* TODO i18n: missing key for "Commit Changes" */}
Commit Changes
+ {/* TODO i18n: missing key for "Rollback Draft" */}
Rollback Draft
diff --git a/ui/src/components/logs/logs-detail-panel.tsx b/ui/src/components/logs/logs-detail-panel.tsx
index f2b219c5..65839b89 100644
--- a/ui/src/components/logs/logs-detail-panel.tsx
+++ b/ui/src/components/logs/logs-detail-panel.tsx
@@ -15,6 +15,7 @@ import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge';
import { formatJson } from './utils';
+import { useTranslation } from 'react-i18next';
function MetaRow({
label,
@@ -52,6 +53,8 @@ export function LogsDetailPanel({
entry: LogsEntry | null;
sourceLabel?: string;
}) {
+ const { t } = useTranslation();
+
if (!entry) {
return (
@@ -63,6 +66,7 @@ export function LogsDetailPanel({
+ {/* TODO i18n: missing key for "Inspector Standby" */}
Inspector Standby
@@ -134,13 +138,14 @@ export function LogsDetailPanel({
className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm"
>
- Details
+ {t('logsDetailPanel.details')}
+ {/* TODO i18n: missing key for "Raw Context" */}
Raw Context
diff --git a/ui/src/components/logs/logs-entry-list.tsx b/ui/src/components/logs/logs-entry-list.tsx
index dc205f6a..d54ce5e9 100644
--- a/ui/src/components/logs/logs-entry-list.tsx
+++ b/ui/src/components/logs/logs-entry-list.tsx
@@ -3,6 +3,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge';
+import { useTranslation } from 'react-i18next';
export function LogsEntryList({
entries,
@@ -19,6 +20,8 @@ export function LogsEntryList({
isLoading: boolean;
isFetching: boolean;
}) {
+ const { t } = useTranslation();
+
return (
@@ -53,13 +56,23 @@ export function LogsEntryList({
-
Time
-
Lvl
-
Source
-
Message
-
Proc
-
Run
-
Open
+
{t('logsConfig.time')}
+
+ {t('logsConfig.level')}
+
+
+ {t('logsConfig.source')}
+
+
{t('logsConfig.message')}
+
+ {t('logsConfig.proc')}
+
+
+ {t('logsConfig.run')}
+
+
+ {t('logsConfig.open')}
+
diff --git a/ui/src/components/logs/logs-filters.tsx b/ui/src/components/logs/logs-filters.tsx
index f212890d..d3b73b3d 100644
--- a/ui/src/components/logs/logs-filters.tsx
+++ b/ui/src/components/logs/logs-filters.tsx
@@ -6,6 +6,7 @@ import type { LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs';
import { getLogLevelOptions } from '@/hooks/use-logs';
+import { useTranslation } from 'react-i18next';
export function LogsFilters({
sources,
@@ -32,6 +33,7 @@ export function LogsFilters({
onRefresh: () => void;
isRefreshing: boolean;
}) {
+ const { t } = useTranslation();
const levels = getLogLevelOptions();
const limits = [50, 100, 150, 250];
@@ -188,7 +190,7 @@ export function LogsFilters({
- Refresh Entries
+ {t('logsConfig.refreshEntries')}
diff --git a/ui/src/components/logs/logs-overview-cards.tsx b/ui/src/components/logs/logs-overview-cards.tsx
index 678f8548..8cdea69b 100644
--- a/ui/src/components/logs/logs-overview-cards.tsx
+++ b/ui/src/components/logs/logs-overview-cards.tsx
@@ -2,6 +2,7 @@ import { Activity, Archive, Database, RadioTower } from 'lucide-react';
import type { LogsConfig, LogsEntry, LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { formatCount, formatLogTimestamp, formatRelativeLogTime } from './utils';
+// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
function MetricCard({
label,
@@ -43,12 +44,15 @@ export function LogsOverviewCards({
entries: LogsEntry[];
latestTimestamp: string | null;
}) {
+ // TODO i18n: uncomment when keys for Pipeline/Retention/Coverage/Visible Entries are added
+ // const { t } = useTranslation();
const nativeSources = sources.filter((source) => source.kind === 'native').length;
const legacySources = sources.length - nativeSources;
const errorCount = entries.filter((entry) => entry.level === 'error').length;
return (
+ {/* TODO i18n: missing keys for Pipeline/Retention/Coverage/Visible Entries labels and detail strings */}
+
diff --git a/ui/src/components/logs/utils.ts b/ui/src/components/logs/utils.ts
index 16d0cff8..90de8d1b 100644
--- a/ui/src/components/logs/utils.ts
+++ b/ui/src/components/logs/utils.ts
@@ -1,4 +1,9 @@
import type { LogsLevel } from '@/lib/api-client';
+// NOTE: This module contains utility functions that are not directly i18n-aware.
+// String literals here ("No activity yet", "Error", etc.) are used as fallbacks
+// and defaults in non-component contexts. Components consuming these values
+// should wrap them with t() calls when rendering.
+// TODO i18n: Consider making formatRelativeLogTime/formatLogTimestamp i18n-aware
export function formatLogTimestamp(timestamp: string | null | undefined) {
if (!timestamp) {
diff --git a/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx
index bd60db8f..77c6de94 100644
--- a/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx
+++ b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx
@@ -3,6 +3,7 @@
*/
import { CheckCircle2, XCircle } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
interface InlineStatsBadgeProps {
success: number;
@@ -10,8 +11,13 @@ interface InlineStatsBadgeProps {
}
export function InlineStatsBadge({ success, failure }: InlineStatsBadgeProps) {
+ const { t } = useTranslation();
if (success === 0 && failure === 0) {
- return no activity ;
+ return (
+
+ {t('authMonitorLive.noActivity')}
+
+ );
}
return (
diff --git a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx
index dc0ea26e..3d04b8c6 100644
--- a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx
+++ b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx
@@ -12,6 +12,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import type { ProviderStats } from '../types';
import { getSuccessRate } from '../utils';
import { InlineStatsBadge } from './inline-stats-badge';
+import { useTranslation } from 'react-i18next';
interface ProviderCardProps {
stats: ProviderStats;
@@ -30,6 +31,7 @@ export function ProviderCard({
onMouseEnter,
onMouseLeave,
}: ProviderCardProps) {
+ const { t } = useTranslation();
const successRate = getSuccessRate(stats.successCount, stats.failureCount);
const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280';
@@ -59,6 +61,7 @@ export function ProviderCard({
{stats.displayName}
+ {/* TODO i18n: missing key for account count */}
{stats.accountCount} account{stats.accountCount !== 1 ? 's' : ''}
@@ -73,11 +76,11 @@ export function ProviderCard({
{/* Inline success/failure stats - immediately visible */}
- Stats
+ {t('authMonitorLive.stats')}
-
Success Rate
+
{t('authMonitorLive.successRate')}
- Missing Project ID - re-add account to fix
+ {/* TODO i18n: missing key */}Missing Project ID - re-add account to fix
diff --git a/ui/src/components/monitoring/auth-monitor/index.tsx b/ui/src/components/monitoring/auth-monitor/index.tsx
index 615410dd..a521a162 100644
--- a/ui/src/components/monitoring/auth-monitor/index.tsx
+++ b/ui/src/components/monitoring/auth-monitor/index.tsx
@@ -146,17 +146,27 @@ export function AuthMonitor() {
- LIVE
- Account Monitor
+
+ {t('authMonitorLive.live')}
+
+
+ {t('authMonitorLive.accountMonitor')}
+
- Updated {timeSinceUpdate || 'now'}
+
+ {timeSinceUpdate
+ ? t('authMonitorLive.updated', { time: timeSinceUpdate })
+ : t('authMonitorLive.updatedNow')}
+
|
{t('authMonitor.accountsCount', { count: displayedAccountCount })}
-
{displayedTotalRequests.toLocaleString()} req
+
+ {displayedTotalRequests.toLocaleString()} {t('authMonitorLive.requestsLabel')}
+
@@ -210,6 +220,7 @@ export function AuthMonitor() {
/>
) : (
+ {/* TODO i18n: missing key for "Request Distribution by Provider" */}
Request Distribution by Provider
diff --git a/ui/src/components/monitoring/error-logs/log-content-panel.tsx b/ui/src/components/monitoring/error-logs/log-content-panel.tsx
index 46fc5a72..356de298 100644
--- a/ui/src/components/monitoring/error-logs/log-content-panel.tsx
+++ b/ui/src/components/monitoring/error-logs/log-content-panel.tsx
@@ -133,8 +133,12 @@ export function LogContentPanel({ name, absolutePath }: LogContentPanelProps) {
{activeTab === 'overview' && }
{activeTab === 'headers' && }
- {activeTab === 'request' && }
- {activeTab === 'response' && }
+ {activeTab === 'request' && (
+
+ )}
+ {activeTab === 'response' && (
+
+ )}
{activeTab === 'raw' && }
diff --git a/ui/src/components/monitoring/error-logs/tab-components.tsx b/ui/src/components/monitoring/error-logs/tab-components.tsx
index da2374b8..48fcae80 100644
--- a/ui/src/components/monitoring/error-logs/tab-components.tsx
+++ b/ui/src/components/monitoring/error-logs/tab-components.tsx
@@ -144,9 +144,10 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
/** Headers tab content */
export function HeadersTab({ headers }: { headers: Record }) {
+ const { t } = useTranslation();
const entries = Object.entries(headers);
if (entries.length === 0) {
- return No headers available
;
+ return {t('errorLogs.noHeaders')}
;
}
return (
diff --git a/ui/src/components/profiles/editor/friendly-ui-section.tsx b/ui/src/components/profiles/editor/friendly-ui-section.tsx
index 10cec625..dc132aaa 100644
--- a/ui/src/components/profiles/editor/friendly-ui-section.tsx
+++ b/ui/src/components/profiles/editor/friendly-ui-section.tsx
@@ -20,6 +20,7 @@ import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './uti
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import i18n from '@/lib/i18n';
+import { useTranslation } from 'react-i18next';
import type { Settings, SettingsResponse } from './types';
import type { CliTarget } from '@/lib/api-client';
@@ -50,6 +51,7 @@ export function FriendlyUISection({
onAddEnvVar,
onEnvBulkChange,
}: FriendlyUISectionProps) {
+ const { t } = useTranslation();
const isOpenRouter = isOpenRouterProfile(currentSettings);
const settingsEnv = currentSettings?.env;
@@ -124,11 +126,13 @@ export function FriendlyUISection({
+ {/* TODO i18n: missing key for "Configuration" tab label */}
- {isOpenRouter ? 'Configuration' : 'Environment Variables'}
+ {isOpenRouter ? 'Configuration' : t('settingsDialog.envTab')}
+ {/* TODO i18n: missing key for "Info & Usage" tab label */}
- Info & Usage
+ Info & Usage
@@ -145,6 +149,7 @@ export function FriendlyUISection({
{/* Model Selection - Primary Focus */}
+ {/* TODO i18n: missing key for "Model Selection" label */}
Model Selection
- API Key
+ {t('profileDialog.apiKey')}
onEnvValueChange('ANTHROPIC_AUTH_TOKEN', e.target.value)}
@@ -193,6 +198,7 @@ export function FriendlyUISection({
)}
/>
+ {/* TODO i18n: missing key for "Additional Variables" label */}
Additional Variables
({unmanagedEnvVars.length})
@@ -220,18 +226,18 @@ export function FriendlyUISection({
{/* Fixed Add Variable Input at Bottom */}
- Add Environment Variable
+ {t('envEditor.addVariable')}
onNewEnvKeyChange(e.target.value.toUpperCase())}
className="font-mono text-sm h-8 w-2/5"
onKeyDown={(e) => e.key === 'Enter' && newEnvKey.trim() && onAddEnvVar()}
/>
onNewEnvValueChange(e.target.value)}
className="font-mono text-sm h-8 flex-1"
diff --git a/ui/src/components/profiles/editor/header-section.tsx b/ui/src/components/profiles/editor/header-section.tsx
index 0ed87e02..098391a4 100644
--- a/ui/src/components/profiles/editor/header-section.tsx
+++ b/ui/src/components/profiles/editor/header-section.tsx
@@ -13,6 +13,7 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Save, Loader2, Trash2, RefreshCw } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { OpenRouterBadge } from '@/components/profiles/openrouter-badge';
import { isOpenRouterProfile } from './utils';
import type { Settings } from './types';
@@ -49,6 +50,7 @@ export function HeaderSection({
onDelete,
onSave,
}: HeaderSectionProps) {
+ const { t } = useTranslation();
const isMutating = isSaving || isTargetSaving;
const disableHeaderActions = isLoading || isMutating;
@@ -66,11 +68,11 @@ export function HeaderSection({
{data && (
- Last modified: {new Date(data.mtime).toLocaleString()}
+ {t('profileEditor.lastModified')}: {new Date(data.mtime).toLocaleString()}
)}
-
Default target:
+
{t('profileEditor.defaultTarget')}:
{
@@ -83,9 +85,9 @@ export function HeaderSection({
- Claude Code
- Factory Droid
- Codex CLI
+ {t('profileEditor.targetClaude')}
+ {t('profileEditor.targetDroid')}
+ {t('profileEditor.targetCodex')}
{isTargetSaving &&
}
@@ -104,12 +106,12 @@ export function HeaderSection({
{isSaving ? (
<>
- Saving...
+ {t('profileEditor.saving')}
>
) : (
<>
- Save
+ {t('settingsAuth.save')}
>
)}
diff --git a/ui/src/components/profiles/editor/image-analysis-status-section.tsx b/ui/src/components/profiles/editor/image-analysis-status-section.tsx
index c1100487..f5fca3c2 100644
--- a/ui/src/components/profiles/editor/image-analysis-status-section.tsx
+++ b/ui/src/components/profiles/editor/image-analysis-status-section.tsx
@@ -1,5 +1,6 @@
import { ArrowUpRight, Image as ImageIcon } from 'lucide-react';
import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
@@ -15,33 +16,38 @@ interface ImageAnalysisStatusSectionProps {
onToggleNativeRead?: (enabled: boolean) => void;
}
-const TARGET_LABELS: Record
= {
- claude: 'Claude Code',
- droid: 'Factory Droid',
- codex: 'Codex CLI',
-};
-
function getPreviewLabel(
+ t: (key: string) => string,
source: 'saved' | 'editor',
previewState: ImageAnalysisStatusSectionProps['previewState']
) {
- if (previewState === 'refreshing') return 'Refreshing preview';
- if (previewState === 'invalid') return 'Saved status';
- return source === 'editor' ? 'Live preview' : 'Saved status';
+ if (previewState === 'refreshing') return t('imageAnalysisStatus.refreshingPreview');
+ if (previewState === 'invalid') return t('imageAnalysisStatus.savedStatus');
+ return source === 'editor'
+ ? t('imageAnalysisStatus.livePreview')
+ : t('imageAnalysisStatus.savedStatus');
}
-function getHeaderLabel(status: ImageAnalysisStatus, target: CliTarget): string {
- if (status.status === 'disabled') return 'Disabled globally';
- if (target !== 'claude') return `${TARGET_LABELS[target]} bypasses the hook`;
- if (status.nativeReadPreference) return 'Native image reading';
- if (status.status === 'hook-missing') return 'Setup needed';
- if (status.authReadiness === 'missing') return 'Needs auth';
- if (status.proxyReadiness === 'unavailable') return 'Needs proxy';
- if (status.effectiveRuntimeMode === 'native-read') return 'Native fallback';
- return 'Transformer ready';
+function getHeaderLabel(
+ t: (key: string, options?: Record) => string,
+ status: ImageAnalysisStatus,
+ target: CliTarget
+): string {
+ if (status.status === 'disabled') return t('imageAnalysisStatus.disabledGlobally');
+ if (target !== 'claude')
+ return t('imageAnalysisStatus.targetBypassesHook', {
+ target: t(`imageAnalysisStatus.targetLabel.${target}`),
+ });
+ if (status.nativeReadPreference) return t('imageAnalysisStatus.nativeImageReading');
+ if (status.status === 'hook-missing') return t('imageAnalysisStatus.setupNeeded');
+ if (status.authReadiness === 'missing') return t('imageAnalysisStatus.needsAuth');
+ if (status.proxyReadiness === 'unavailable') return t('imageAnalysisStatus.needsProxy');
+ if (status.effectiveRuntimeMode === 'native-read') return t('imageAnalysisStatus.nativeFallback');
+ return t('imageAnalysisStatus.transformerReady');
}
function getHeaderBadge(
+ t: (key: string) => string,
status: ImageAnalysisStatus,
target: CliTarget
): {
@@ -50,75 +56,93 @@ function getHeaderBadge(
} {
if (status.status === 'disabled') {
return {
- label: 'Disabled',
+ label: t('imageAnalysisStatus.badgeDisabled'),
className: 'border-border/80 bg-background/85 text-muted-foreground',
};
}
if (target !== 'claude') {
return {
- label: 'Bypassed',
+ label: t('imageAnalysisStatus.badgeBypassed'),
className: 'border-sky-500/25 bg-sky-500/10 text-sky-800 dark:text-sky-200',
};
}
if (status.nativeReadPreference) {
return {
- label: 'Native',
+ label: t('imageAnalysisStatus.badgeNative'),
className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200',
};
}
if (status.status === 'hook-missing' || status.authReadiness === 'missing') {
return {
- label: status.status === 'hook-missing' ? 'Setup' : 'Auth',
+ label:
+ status.status === 'hook-missing'
+ ? t('imageAnalysisStatus.badgeSetup')
+ : t('imageAnalysisStatus.badgeAuth'),
className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200',
};
}
if (status.proxyReadiness === 'unavailable') {
return {
- label: 'Proxy',
+ label: t('imageAnalysisStatus.badgeProxy'),
className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200',
};
}
return {
- label: 'Ready',
+ label: t('imageAnalysisStatus.badgeReady'),
className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200',
};
}
-function getToggleSummary(status: ImageAnalysisStatus, target: CliTarget): string {
+function getToggleSummary(
+ t: (key: string, options?: Record) => string,
+ status: ImageAnalysisStatus,
+ target: CliTarget
+): string {
if (status.nativeReadPreference) {
if (status.profileModel && status.nativeImageCapable) {
- return `${status.profileModel} looks image-ready. CCS will bypass the transformer here.`;
+ return t('imageAnalysisStatus.toggleSummaryNativeCapable', { model: status.profileModel });
}
if (status.profileModel) {
- return `CCS will prefer native reading for ${status.profileModel}.`;
+ return t('imageAnalysisStatus.toggleSummaryNativeModel', { model: status.profileModel });
}
- return 'CCS will prefer native image reading for this profile.';
+ return t('imageAnalysisStatus.toggleSummaryNativeDefault');
}
if (!status.backendDisplayName && target === 'claude') {
- return 'This profile currently stays on native file access.';
+ return t('imageAnalysisStatus.toggleSummaryNativeFileAccess');
}
if (!status.backendDisplayName) {
- return `Saved Claude-side image routing is inactive while ${TARGET_LABELS[target]} is selected.`;
+ return t('imageAnalysisStatus.toggleSummaryInactiveTarget', {
+ target: t(`imageAnalysisStatus.targetLabel.${target}`),
+ });
}
const modelSuffix = status.model ? ` · ${status.model}` : '';
- return `Transformer route: ${status.backendDisplayName}${modelSuffix}.`;
+ return t('imageAnalysisStatus.toggleSummaryTransformerRoute', {
+ backend: status.backendDisplayName,
+ modelSuffix,
+ });
}
-function getExceptionalNote(status: ImageAnalysisStatus, target: CliTarget): string | null {
+function getExceptionalNote(
+ t: (key: string, options?: Record) => string,
+ status: ImageAnalysisStatus,
+ target: CliTarget
+): string | null {
if (status.status === 'disabled') {
- return 'Image is disabled globally in CCS settings.';
+ return t('imageAnalysisStatus.noteDisabledGlobally');
}
if (target !== 'claude') {
- return `Current target ${TARGET_LABELS[target]} bypasses the Claude Read hook.`;
+ return t('imageAnalysisStatus.noteTargetBypassesHook', {
+ target: t(`imageAnalysisStatus.targetLabel.${target}`),
+ });
}
if (status.nativeReadPreference) {
return status.nativeImageCapable === true ? null : status.nativeImageReason;
}
if (status.status === 'hook-missing') {
- return 'Persist the profile hook before transformer routing can run here.';
+ return t('imageAnalysisStatus.notePersistHook');
}
if (status.authReadiness === 'missing') {
return status.authReason;
@@ -137,6 +161,8 @@ export function ImageAnalysisStatusSection({
nativeReadPreferenceOverride,
onToggleNativeRead,
}: ImageAnalysisStatusSectionProps) {
+ const { t } = useTranslation();
+
if (!status) {
return (
@@ -148,12 +174,12 @@ export function ImageAnalysisStatusSection({
const nativeReadChecked = nativeReadPreferenceOverride ?? status.nativeReadPreference;
const effectiveStatus = { ...status, nativeReadPreference: nativeReadChecked };
- const headerBadge = getHeaderBadge(effectiveStatus, target);
- const note = getExceptionalNote(effectiveStatus, target);
+ const headerBadge = getHeaderBadge(t, effectiveStatus, target);
+ const note = getExceptionalNote(t, effectiveStatus, target);
const capabilityLabel = status.nativeImageCapable
- ? 'Verified'
+ ? t('imageAnalysisStatus.capabilityVerified')
: status.profileModel
- ? 'Unknown'
+ ? t('imageAnalysisStatus.capabilityUnknown')
: null;
return (
@@ -166,13 +192,14 @@ export function ImageAnalysisStatusSection({
-
Image
+ {t('imageAnalysisStatus.sectionTitle')}
{headerBadge.label}
- {getPreviewLabel(source, previewState)} · {getHeaderLabel(effectiveStatus, target)}
+ {getPreviewLabel(t, source, previewState)} ·{' '}
+ {getHeaderLabel(t, effectiveStatus, target)}
@@ -180,7 +207,7 @@ export function ImageAnalysisStatusSection({
- Open Settings
+ {t('imageAnalysisStatus.openSettings')}
@@ -190,7 +217,9 @@ export function ImageAnalysisStatusSection({
-
Use native image reading
+
+ {t('imageAnalysisStatus.useNativeImageReading')}
+
{capabilityLabel && (
{capabilityLabel}
@@ -198,7 +227,7 @@ export function ImageAnalysisStatusSection({
)}
- {getToggleSummary(effectiveStatus, target)}
+ {getToggleSummary(t, effectiveStatus, target)}
@@ -206,7 +235,7 @@ export function ImageAnalysisStatusSection({
checked={nativeReadChecked}
onCheckedChange={onToggleNativeRead}
disabled={!onToggleNativeRead}
- aria-label="Use native image reading"
+ aria-label={t('imageAnalysisStatus.useNativeImageReading')}
/>
diff --git a/ui/src/components/profiles/editor/index.tsx b/ui/src/components/profiles/editor/index.tsx
index a7daa66a..12006fe8 100644
--- a/ui/src/components/profiles/editor/index.tsx
+++ b/ui/src/components/profiles/editor/index.tsx
@@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/shared/confirm-dialog';
import { Loader2, Code2, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import { HeaderSection } from './header-section';
import { FriendlyUISection } from './friendly-ui-section';
@@ -24,6 +25,7 @@ export function ProfileEditor({
onDelete,
onHasChangesUpdate,
}: ProfileEditorProps) {
+ const { t } = useTranslation();
const [localEdits, setLocalEdits] = useState>({});
const [conflictDialog, setConflictDialog] = useState(false);
const [rawJsonEdits, setRawJsonEdits] = useState(null);
@@ -299,15 +301,15 @@ export function ProfileEditor({
{isLoading ? (
- Loading settings...
+ {t('settingsDialog.loadingSettings')}
) : isError ? (
-
Failed to load settings.
+
{t('settingsPage.failedLoad')}
refetch()}>
- Retry
+ {t('apiProfiles.retry')}
@@ -332,7 +334,7 @@ export function ProfileEditor({
- Raw Configuration (JSON)
+ {t('rawEditorSection.rawConfig')}
handleConflictResolve(true)}
onCancel={() => handleConflictResolve(false)}
diff --git a/ui/src/components/profiles/editor/raw-editor-section.tsx b/ui/src/components/profiles/editor/raw-editor-section.tsx
index 924e187b..79cf6ea1 100644
--- a/ui/src/components/profiles/editor/raw-editor-section.tsx
+++ b/ui/src/components/profiles/editor/raw-editor-section.tsx
@@ -5,6 +5,7 @@
import { Suspense, lazy } from 'react';
import { Loader2, X, AlertTriangle } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
import { ImageAnalysisStatusSection } from './image-analysis-status-section';
import type { Settings } from './types';
@@ -44,6 +45,7 @@ export function RawEditorSection({
onChange,
missingRequiredFields = [],
}: RawEditorSectionProps) {
+ const { t } = useTranslation();
const hasMissingFields = missingRequiredFields.length > 0;
return (
@@ -51,7 +53,9 @@ export function RawEditorSection({
fallback={
- Loading editor...
+
+ {t('profileEditorSections.loadingEditor')}
+
}
>
@@ -59,7 +63,7 @@ export function RawEditorSection({
{!isRawJsonValid && rawJsonEdits !== null && (
- Invalid JSON syntax
+ {t('profileEditor.invalidJson')}
)}
{isRawJsonValid && hasMissingFields && (
@@ -67,13 +71,13 @@ export function RawEditorSection({
- Missing required fields:
+ {t('profileEditor.missingFields')}:
{' '}
{missingRequiredFields.join(', ')}
- These fields will use default values at runtime.
+ {t('profileEditor.missingFieldsHint')}
diff --git a/ui/src/components/profiles/editor/use-profile-editor.ts b/ui/src/components/profiles/editor/use-profile-editor.ts
index 07202c58..462eaa5e 100644
--- a/ui/src/components/profiles/editor/use-profile-editor.ts
+++ b/ui/src/components/profiles/editor/use-profile-editor.ts
@@ -6,6 +6,7 @@
import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import type { Settings, SettingsResponse } from './types';
/** Required env vars for profiles to function (informational only - runtime fills defaults) */
@@ -34,6 +35,7 @@ export function useProfileEditor({
onSuccess,
onConflict,
}: UseProfileEditorOptions) {
+ const { t } = useTranslation();
const queryClient = useQueryClient();
// Fetch settings for selected profile
@@ -132,11 +134,11 @@ export function useProfileEditor({
onSuccess();
// Show warning if fields missing (runtime uses defaults)
if (data?.warning) {
- toast.success('Settings saved', {
+ toast.success(t('commonToast.settingsSaved'), {
description: data.warning,
});
} else {
- toast.success('Settings saved');
+ toast.success(t('commonToast.settingsSaved'));
}
},
onError: (error: Error) => {
diff --git a/ui/src/components/profiles/openrouter-badge.tsx b/ui/src/components/profiles/openrouter-badge.tsx
index 6f11e3b8..02ad445d 100644
--- a/ui/src/components/profiles/openrouter-badge.tsx
+++ b/ui/src/components/profiles/openrouter-badge.tsx
@@ -3,6 +3,7 @@
* Visual indicator for OpenRouter-configured profiles
*/
+import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
@@ -13,6 +14,8 @@ interface OpenRouterBadgeProps {
}
export function OpenRouterBadge({ className, showTooltip = true }: OpenRouterBadgeProps) {
+ const { t } = useTranslation();
+
const badge = (
{badge}
- Access 349+ models via OpenRouter
+ {t('openrouterBadge.integration')}
);
diff --git a/ui/src/components/profiles/openrouter-banner.tsx b/ui/src/components/profiles/openrouter-banner.tsx
index ac213f5b..6ecdd029 100644
--- a/ui/src/components/profiles/openrouter-banner.tsx
+++ b/ui/src/components/profiles/openrouter-banner.tsx
@@ -5,6 +5,7 @@
/* eslint-disable react-hooks/set-state-in-effect */
import { useState, useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
import { X, Sparkles, ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
@@ -16,6 +17,7 @@ interface OpenRouterBannerProps {
}
export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
+ const { t } = useTranslation();
const [dismissed, setDismissed] = useState(true); // Start hidden to avoid flash
const { modelCount, isLoading } = useOpenRouterReady();
@@ -40,10 +42,13 @@ export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
-
NEW: OpenRouter Integration
+
+ {t('openrouterBadge.new')}: {t('openrouterBadge.integration')}
+
- Browse {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
- Meta and more.
+ {t('openrouterBanner.accessModels', {
+ count: isLoading ? 300 : modelCount,
+ })}
@@ -56,7 +61,7 @@ export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
onClick={onCreateClick}
className="bg-white text-accent hover:bg-white/90 h-8"
>
- Try it now
+ {t('openrouterBanner.add')}
)}
(null);
@@ -105,7 +107,7 @@ export function OpenRouterModelPicker({
setSearch(e.target.value)}
- placeholder={placeholder}
+ placeholder={placeholder ?? t('openrouterModelPicker.searchModels')}
className="pl-9"
/>
@@ -180,7 +182,7 @@ export function OpenRouterModelPicker({
- Newest Models
+ {t('openrouterModelPicker.newestModels')}
{newestModels.map((model) => (
diff --git a/ui/src/components/profiles/openrouter-promo-card.tsx b/ui/src/components/profiles/openrouter-promo-card.tsx
index ef3119d3..461a5779 100644
--- a/ui/src/components/profiles/openrouter-promo-card.tsx
+++ b/ui/src/components/profiles/openrouter-promo-card.tsx
@@ -3,6 +3,7 @@
* Permanent promotional card for OpenRouter - always visible in sidebar footer
*/
+import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
import { Zap } from 'lucide-react';
@@ -12,7 +13,8 @@ interface OpenRouterPromoCardProps {
}
export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps) {
- const { modelCount, isLoading } = useOpenRouterReady();
+ const { t } = useTranslation();
+ useOpenRouterReady();
return (
@@ -21,9 +23,11 @@ export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps)
-
OpenRouter
+
+ {t('openrouterPromoCard.title')}
+
- {isLoading ? '300+' : `${modelCount}+`} models available
+ {t('openrouterPromoCard.description')}
- Add
+ {t('openrouterBanner.add')}
diff --git a/ui/src/components/profiles/profile-card.tsx b/ui/src/components/profiles/profile-card.tsx
index ff18790c..50832636 100644
--- a/ui/src/components/profiles/profile-card.tsx
+++ b/ui/src/components/profiles/profile-card.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -23,6 +24,7 @@ interface ProfileCardProps {
}
export function ProfileCard({ profile, settings, onSwitch, onConfig, onTest }: ProfileCardProps) {
+ const { t } = useTranslation();
const showOpenRouterIcon = isOpenRouterProfile(settings);
return (
@@ -36,7 +38,7 @@ export function ProfileCard({ profile, settings, onSwitch, onConfig, onTest }: P
- OpenRouter profile
+ {t('profileCard.openRouter')}
)}
{profile.isActive && (
diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx
index 1ae37568..1cb81e9b 100644
--- a/ui/src/components/profiles/profile-create-dialog.tsx
+++ b/ui/src/components/profiles/profile-create-dialog.tsx
@@ -230,7 +230,7 @@ export function ProfileCreateDialog({
setValue('haikuModel', model.id);
setModelSearch(model.name);
// Show feedback that model was applied to all tiers
- toast.success(`Applied "${model.name}" to all model tiers`, {
+ toast.success(t('profileCreateDialog.appliedModelToTiers', { model: model.name }), {
duration: 2000,
});
};
@@ -268,11 +268,11 @@ export function ProfileCreateDialog({
};
try {
await createMutation.mutateAsync(finalData);
- toast.success(`Profile "${finalData.name}" created`);
+ toast.success(t('profileCreateDialog.profileCreated', { name: finalData.name }));
onSuccess(finalData.name);
onOpenChange(false);
} catch (error) {
- toast.error((error as Error).message || 'Failed to create profile');
+ toast.error((error as Error).message || t('profileCreateDialog.failedCreate'));
}
};
@@ -289,11 +289,9 @@ export function ProfileCreateDialog({
- Create API Profile
+ {t('profileCreateDialog.createProfile')}
-
- Choose a provider preset or configure a custom API endpoint.
-
+ {t('profileCreateDialog.chooseProviderHint')}
@@ -180,18 +184,18 @@ export function ProjectSelectionDialog({
handleSubmit(true)} disabled={isSubmitting}>
- Use Default
+ {t('projectSelectionDialog.useDefault')}
handleSubmit(false)} disabled={isSubmitting}>
{isSubmitting ? (
<>
- Selecting...
+ {t('projectSelectionDialog.selecting')}
>
) : (
<>
- Confirm Selection
+ {t('projectSelectionDialog.confirmSelection')}
>
)}
diff --git a/ui/src/components/shared/quick-commands.tsx b/ui/src/components/shared/quick-commands.tsx
index 6ab4d6f8..a8ee6e62 100644
--- a/ui/src/components/shared/quick-commands.tsx
+++ b/ui/src/components/shared/quick-commands.tsx
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
import { Copy, Check, Terminal } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
+import { useTranslation } from 'react-i18next';
interface CommandSnippet {
label: string;
@@ -39,6 +40,7 @@ interface QuickCommandsProps {
export function QuickCommands({ snippets = defaultSnippets }: QuickCommandsProps) {
const [copiedIndex, setCopiedIndex] = useState
(null);
+ const { t } = useTranslation();
const copyToClipboard = async (text: string, index: number) => {
await navigator.clipboard.writeText(text);
@@ -51,7 +53,7 @@ export function QuickCommands({ snippets = defaultSnippets }: QuickCommandsProps
- Quick Commands
+ {t('quickCommands.title')}
diff --git a/ui/src/components/shared/quota-tooltip-content.tsx b/ui/src/components/shared/quota-tooltip-content.tsx
index b653dde7..0dba6fb7 100644
--- a/ui/src/components/shared/quota-tooltip-content.tsx
+++ b/ui/src/components/shared/quota-tooltip-content.tsx
@@ -22,6 +22,7 @@ import {
type UnifiedQuotaResult,
} from '@/lib/utils';
import type { ProviderEntitlementEvidence } from '@/lib/api-client';
+import { useTranslation } from 'react-i18next';
interface QuotaTooltipContentProps {
quota: UnifiedQuotaResult | null | undefined;
@@ -57,22 +58,26 @@ function formatAbsoluteResetTime(resetTime: string | null): string | null {
}
}
-function getClaudeWindowDisplayLabel(rateLimitType: string, fallback: string): string {
+function getClaudeWindowDisplayLabel(
+ rateLimitType: string,
+ fallback: string,
+ t: (key: string) => string
+): string {
switch (rateLimitType) {
case 'five_hour':
- return '5h usage limit';
+ return t('quotaTooltip.fiveHourLimit');
case 'seven_day':
- return 'Weekly usage limit';
+ return t('quotaTooltip.weeklyLimit');
case 'seven_day_opus':
- return 'Weekly usage (Opus)';
+ return t('quotaTooltip.weeklyOpus');
case 'seven_day_sonnet':
- return 'Weekly usage (Sonnet)';
+ return t('quotaTooltip.weeklySonnet');
case 'seven_day_oauth_apps':
- return 'Weekly usage (OAuth apps)';
+ return t('quotaTooltip.weeklyOAuthApps');
case 'seven_day_cowork':
- return 'Weekly usage (Cowork)';
+ return t('quotaTooltip.weeklyCowork');
case 'overage':
- return 'Extra usage';
+ return t('quotaTooltip.extraUsage');
default:
return fallback;
}
@@ -118,38 +123,42 @@ function formatGeminiBucketModels(modelIds: string[] | undefined): string | null
function formatGeminiRemainingAmount(
remainingAmount: number | null | undefined,
- tokenType: string | null | undefined
+ tokenType: string | null | undefined,
+ t: (key: string, options?: Record) => string
): string | null {
if (remainingAmount === null || remainingAmount === undefined) return null;
const formattedAmount = remainingAmount.toLocaleString();
switch (tokenType?.trim().toLowerCase()) {
case 'requests':
- return `${formattedAmount} requests remaining`;
+ return t('quotaTooltip.requestsRemaining', { count: formattedAmount });
case 'input':
- return `${formattedAmount} input tokens remaining`;
+ return t('quotaTooltip.inputTokensRemaining', { count: formattedAmount });
case 'output':
- return `${formattedAmount} output tokens remaining`;
+ return t('quotaTooltip.outputTokensRemaining', { count: formattedAmount });
default:
- return `${formattedAmount} remaining`;
+ return t('quotaTooltip.amountRemaining', { count: formattedAmount });
}
}
-function renderEntitlementRows(entitlement: ProviderEntitlementEvidence | undefined) {
+function renderEntitlementRows(
+ entitlement: ProviderEntitlementEvidence | undefined,
+ t: (key: string) => string
+) {
if (!entitlement) return null;
const rows: Array<{ label: string; value: string | null }> = [];
if (entitlement.rawTierLabel) {
- rows.push({ label: 'Tier', value: entitlement.rawTierLabel });
+ rows.push({ label: t('quotaTooltip.tier'), value: entitlement.rawTierLabel });
} else if (entitlement.normalizedTier !== 'unknown') {
- rows.push({ label: 'Tier', value: entitlement.normalizedTier });
+ rows.push({ label: t('quotaTooltip.tier'), value: entitlement.normalizedTier });
}
if (entitlement.rawTierId) {
- rows.push({ label: 'Tier ID', value: entitlement.rawTierId });
+ rows.push({ label: t('quotaTooltip.tierId'), value: entitlement.rawTierId });
}
if (entitlement.accessState !== 'entitled' || entitlement.capacityState !== 'available') {
rows.push({
- label: 'State',
+ label: t('quotaTooltip.state'),
value: `${entitlement.accessState.replaceAll('_', ' ')} / ${entitlement.capacityState.replaceAll('_', ' ')}`,
});
}
@@ -169,8 +178,10 @@ function renderEntitlementRows(entitlement: ProviderEntitlementEvidence | undefi
* Uses type guards for proper TypeScript narrowing
*/
export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentProps) {
+ const { t } = useTranslation();
+
if (!quota) {
- return Loading quota...
;
+ return {t('quotaTooltip.loadingQuota')}
;
}
if (!quota.success) {
@@ -186,7 +197,7 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
- {failureInfo?.label || quota.error || 'Failed to load quota'}
+ {failureInfo?.label || quota.error || t('quotaTooltip.failedLoadQuota')}
{failureInfo?.summary || quota.error}
@@ -219,8 +230,8 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
return (
- {renderEntitlementRows(quota.entitlement)}
-
Model Quotas:
+ {renderEntitlementRows(quota.entitlement, t)}
+
{t('quotaTooltip.modelQuotas')}
{tierOrder.map((tier, idx) => {
const models = groups.get(tier);
if (!models || models.length === 0) return null;
@@ -263,8 +274,12 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
return (
-
Rate Limits:
- {quota.planType &&
Plan: {quota.planType}
}
+
{t('quotaTooltip.rateLimits')}
+ {quota.planType && (
+
+ {t('quotaTooltip.plan', { plan: quota.planType })}
+
+ )}
{orderedWindows.map((w, index) => (
-
Rate Limits:
+
{t('quotaTooltip.rateLimits')}
{orderedWindows.map((window, index) => (
- {getClaudeWindowDisplayLabel(window.rateLimitType, window.label)}
+ {getClaudeWindowDisplayLabel(window.rateLimitType, window.label, t)}
{window.remainingPercent}%
@@ -369,24 +384,24 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
return (
- {renderEntitlementRows(quota.entitlement)}
+ {renderEntitlementRows(quota.entitlement, t)}
{!hasEntitlementTier && quota.tierLabel && (
- Tier
+ {t('quotaTooltip.tier')}
{quota.tierLabel}
)}
{quota.creditBalance !== null && quota.creditBalance !== undefined && (
- Credits
+ {t('quotaTooltip.credits')}
{quota.creditBalance.toLocaleString()}
)}
-
Model quotas:
+
{t('quotaTooltip.modelQuotasLower')}
{sharedTokenType && (
- All buckets report {sharedTokenType}
+ {t('quotaTooltip.allBucketsReport', { tokenType: sharedTokenType })}
)}
@@ -395,7 +410,8 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
const bucketModels = formatGeminiBucketModels(bucket.modelIds);
const remainingAmountLabel = formatGeminiRemainingAmount(
bucket.remainingAmount,
- bucket.tokenType
+ bucket.tokenType,
+ t
);
return (
@@ -437,17 +453,22 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
// GitHub Copilot (ghcp) provider tooltip
if (isGhcpQuotaResult(quota)) {
const snapshotRows = [
- { label: 'Premium Interactions', snapshot: quota.snapshots.premiumInteractions },
- { label: 'Chat', snapshot: quota.snapshots.chat },
- { label: 'Completions', snapshot: quota.snapshots.completions },
+ {
+ label: t('quotaTooltip.premiumInteractions'),
+ snapshot: quota.snapshots.premiumInteractions,
+ },
+ { label: t('quotaTooltip.chat'), snapshot: quota.snapshots.chat },
+ { label: t('quotaTooltip.completions'), snapshot: quota.snapshots.completions },
];
const effectiveResetTime = quota.quotaResetDate ?? resetTime;
const planLabel = formatPlanLabel(quota.planType);
return (
-
Quota Snapshots:
- {planLabel &&
Plan: {planLabel}
}
+
{t('quotaTooltip.quotaSnapshots')}
+ {planLabel && (
+
{t('quotaTooltip.plan', { plan: planLabel })}
+ )}
{snapshotRows.map(({ label, snapshot }) => {
const isLow = snapshot.percentRemaining < 20;
return (
@@ -456,13 +477,16 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
{label}
{snapshot.unlimited
- ? 'Unlimited'
+ ? t('quotaTooltip.unlimited')
: `${formatQuotaPercent(snapshot.percentRemaining)}%`}
{!snapshot.unlimited && (
- {snapshot.remaining}/{snapshot.entitlement} remaining
+ {t('quotaTooltip.remaining', {
+ remaining: snapshot.remaining,
+ entitlement: snapshot.entitlement,
+ })}
)}
@@ -480,13 +504,15 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
* Reset time indicator shown at bottom of tooltip
*/
function ResetTimeIndicator({ resetTime }: { resetTime: string | null }) {
+ const { t } = useTranslation();
+
if (!resetTime) return null;
return (
- Resets {formatResetTime(resetTime)}
+ {t('quotaTooltip.resets', { time: formatResetTime(resetTime) })}
);
@@ -501,6 +527,8 @@ function CodexResetIndicators({
weeklyResetTime: string | null;
fallbackResetTime: string | null;
}) {
+ const { t } = useTranslation();
+
const hasSpecificReset = !!fiveHourResetTime || !!weeklyResetTime;
if (!hasSpecificReset && !fallbackResetTime) return null;
@@ -510,7 +538,7 @@ function CodexResetIndicators({
- 5h resets {formatResetTime(fiveHourResetTime)}
+ {t('quotaTooltip.fiveHourResets', { time: formatResetTime(fiveHourResetTime) })}
)}
@@ -518,7 +546,7 @@ function CodexResetIndicators({
- Weekly resets {formatResetTime(weeklyResetTime)}
+ {t('quotaTooltip.weeklyResets', { time: formatResetTime(weeklyResetTime) })}
)}
diff --git a/ui/src/components/shared/settings-dialog.tsx b/ui/src/components/shared/settings-dialog.tsx
index dac1321d..3f79d35d 100644
--- a/ui/src/components/shared/settings-dialog.tsx
+++ b/ui/src/components/shared/settings-dialog.tsx
@@ -207,16 +207,16 @@ function SettingsDialogContent({
return (
<>
- Edit Profile: {profileName}
-
- Configure environment variables and settings for this profile.
-
+ {i18n.t('settingsDialog.editProfile', { name: profileName })}
+ {i18n.t('settingsDialog.description')}
{isLoading ? (
- Loading settings...
+
+ {i18n.t('settingsDialog.loadingSettings')}
+
) : (
@@ -230,20 +230,20 @@ function SettingsDialogContent({
value="env"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
>
- Environment
+ {i18n.t('settingsDialog.envTab')}
- Raw JSON
+ {i18n.t('settingsDialog.rawJsonTab')}
- General
+ {i18n.t('settingsDialog.generalTab')}
@@ -272,8 +272,8 @@ function SettingsDialogContent({
) : (
-
No environment variables configured.
-
Add variables in your settings.json file.
+
{i18n.t('settingsDialog.noEnvVars')}
+
{i18n.t('settingsDialog.noEnvVarsHint')}
)}
@@ -284,7 +284,9 @@ function SettingsDialogContent({
fallback={
- Loading editor...
+
+ {i18n.t('settingsDialog.loadingEditor')}
+
}
>
@@ -301,20 +303,26 @@ function SettingsDialogContent({
- Profile Information
- Details about this configuration file.
+
+ {i18n.t('settingsDialog.profileInfo')}
+
+ {i18n.t('settingsDialog.profileInfoDesc')}
{data && (
<>
- Path
+
+ {i18n.t('settingsDialog.path')}
+
{data.path}
- Last Modified
+
+ {i18n.t('settingsDialog.lastModified')}
+
{new Date(data.mtime).toLocaleString()}
>
@@ -326,7 +334,7 @@ function SettingsDialogContent({
- Cancel
+ {i18n.t('settingsDialog.cancel')}
{saveMutation.isPending ? (
<>
- Saving...
+ {' '}
+ {i18n.t('settingsDialog.saving')}
>
) : (
<>
- Save Changes
+ {i18n.t('settingsDialog.saveChanges')}
>
)}
@@ -348,9 +357,9 @@ function SettingsDialogContent({
handleConflictResolve(true)}
onCancel={() => handleConflictResolve(false)}
diff --git a/ui/src/components/shared/sponsor-button.tsx b/ui/src/components/shared/sponsor-button.tsx
index 382d9afb..ac4a453e 100644
--- a/ui/src/components/shared/sponsor-button.tsx
+++ b/ui/src/components/shared/sponsor-button.tsx
@@ -7,10 +7,13 @@
import { Heart } from 'lucide-react';
import { cn } from '@/lib/utils';
+import { useTranslation } from 'react-i18next';
const SPONSOR_URL = 'https://github.com/sponsors/kaitranntt';
export function SponsorButton() {
+ const { t } = useTranslation();
+
return (
- Sponsor
+ {t('sponsorButton.sponsor')}
);
diff --git a/ui/src/components/shared/value-metrics.tsx b/ui/src/components/shared/value-metrics.tsx
index 9975a7b9..e7ffed46 100644
--- a/ui/src/components/shared/value-metrics.tsx
+++ b/ui/src/components/shared/value-metrics.tsx
@@ -1,6 +1,7 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { TrendingUpIcon, TrendingDownIcon, DollarSignIcon, ZapIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
interface MetricCardProps {
title: string;
@@ -39,37 +40,39 @@ function MetricCard({ title, value, change, changeLabel, icon, trend }: MetricCa
}
export function ValueMetrics() {
+ const { t } = useTranslation();
+
// Mock data for demonstration
const metrics = [
{
- title: 'API Cost Saved',
+ title: t('valueMetrics.apiCostSaved'),
value: '$127.50',
change: 23,
- changeLabel: 'vs last month',
+ changeLabel: t('valueMetrics.vsLastMonth'),
icon: ,
trend: 'up' as const,
},
{
- title: 'Tokens Saved',
+ title: t('valueMetrics.tokensSaved'),
value: '2.4M',
change: 18,
- changeLabel: 'through caching',
+ changeLabel: t('valueMetrics.throughCaching'),
icon: ,
trend: 'up' as const,
},
{
- title: 'Queries Faster',
+ title: t('valueMetrics.queriesFaster'),
value: '43%',
change: 12,
- changeLabel: 'average speedup',
+ changeLabel: t('valueMetrics.averageSpeedup'),
icon: ,
trend: 'up' as const,
},
{
- title: 'Errors Reduced',
+ title: t('valueMetrics.errorsReduced'),
value: '-67%',
change: 67,
- changeLabel: 'with retry logic',
+ changeLabel: t('valueMetrics.withRetryLogic'),
icon: ,
trend: 'down' as const,
},
@@ -77,7 +80,7 @@ export function ValueMetrics() {
return (
-
Performance Metrics
+
{t('valueMetrics.performanceMetrics')}
{metrics.map((metric, index) => (
@@ -86,25 +89,29 @@ export function ValueMetrics() {
- Monthly Summary
+ {t('valueMetrics.monthlySummary')}
$342.10
-
Total Saved
+
{t('valueMetrics.totalSaved')}
8.7M
-
Tokens Processed
+
+ {t('valueMetrics.tokensProcessed')}
+
1,247
-
Queries Handled
+
+ {t('valueMetrics.queriesHandled')}
+
99.8%
-
Uptime
+
{t('valueMetrics.uptime')}
diff --git a/ui/src/components/updates/support-entry-card.tsx b/ui/src/components/updates/support-entry-card.tsx
index f3cc70b7..d9ab873e 100644
--- a/ui/src/components/updates/support-entry-card.tsx
+++ b/ui/src/components/updates/support-entry-card.tsx
@@ -24,9 +24,9 @@ const SCOPE_STYLES: Record = {
export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) {
const { t } = useTranslation();
const pillarLabels: { key: keyof CliSupportEntry['pillars']; label: string }[] = [
- { key: 'baseUrl', label: 'Base URL' },
- { key: 'auth', label: 'Auth' },
- { key: 'model', label: 'Model' },
+ { key: 'baseUrl', label: t('profileDialog.baseUrl') },
+ { key: 'auth', label: t('copilotPage.auth') },
+ { key: 'model', label: t('cliproxyTable.model') },
];
return (
diff --git a/ui/src/components/updates/updates-spotlight.tsx b/ui/src/components/updates/updates-spotlight.tsx
index e9bea171..b53ff587 100644
--- a/ui/src/components/updates/updates-spotlight.tsx
+++ b/ui/src/components/updates/updates-spotlight.tsx
@@ -3,6 +3,7 @@ import { BellRing, ExternalLink } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { formatCatalogDate, getLatestSupportNotice } from '@/lib/support-updates-catalog';
import { cn } from '@/lib/utils';
+import { useTranslation } from 'react-i18next';
export function UpdatesSpotlight({
className,
@@ -12,6 +13,7 @@ export function UpdatesSpotlight({
compact?: boolean;
}) {
const latest = getLatestSupportNotice();
+ const { t } = useTranslation();
if (!latest) {
return null;
}
@@ -35,7 +37,7 @@ export function UpdatesSpotlight({
to="/updates"
className="inline-flex items-center gap-1 font-medium text-blue-700 hover:underline dark:text-blue-300"
>
- Open Updates Center
+ {t('updatesSpotlight.openUpdatesCenter')}
diff --git a/ui/src/hooks/use-accounts.ts b/ui/src/hooks/use-accounts.ts
index 83193022..d1461b67 100644
--- a/ui/src/hooks/use-accounts.ts
+++ b/ui/src/hooks/use-accounts.ts
@@ -12,6 +12,7 @@ import {
type SharedGroupSummary,
} from '@/lib/account-continuity';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
export interface AuthAccountsView {
accounts: AuthAccountRow[];
@@ -70,12 +71,13 @@ export function useAccounts() {
export function useSetDefaultAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (name: string) => api.accounts.setDefault(name),
onSuccess: (_data, name) => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.success(`Default account set to "${name}"`);
+ toast.success(t('toasts.defaultAccountSet', { name }));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -85,12 +87,13 @@ export function useSetDefaultAccount() {
export function useResetDefaultAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: () => api.accounts.resetDefault(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.success('Default account reset to CCS');
+ toast.success(t('toasts.defaultAccountReset'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -100,12 +103,13 @@ export function useResetDefaultAccount() {
export function useDeleteAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (name: string) => api.accounts.delete(name),
onSuccess: (_data, name) => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.success(`Account "${name}" deleted`);
+ toast.success(t('toasts.accountDeleted', { name }));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -115,6 +119,7 @@ export function useDeleteAccount() {
export function useUpdateAccountContext() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({
@@ -136,7 +141,7 @@ export function useUpdateAccountContext() {
? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, deeper continuity)`
: `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, standard)`
: 'isolated';
- toast.success(`Updated "${vars.name}" context to ${contextSummary}`);
+ toast.success(t('toasts.contextUpdated', { name: vars.name, summary: contextSummary }));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -146,6 +151,7 @@ export function useUpdateAccountContext() {
export function useConfirmLegacyAccountPolicies() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: async (accounts: Account[]) => {
@@ -174,6 +180,7 @@ export function useConfirmLegacyAccountPolicies() {
onSuccess: ({ updatedCount, failedCount }) => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
if (failedCount > 0 && updatedCount > 0) {
+ // TODO i18n: missing key for partial legacy confirmation with failures
toast.error(
`Confirmed ${updatedCount} legacy account${updatedCount > 1 ? 's' : ''}, but ${failedCount} update${failedCount > 1 ? 's' : ''} failed. Refreshed account state.`
);
@@ -181,6 +188,7 @@ export function useConfirmLegacyAccountPolicies() {
}
if (failedCount > 0) {
+ // TODO i18n: missing key for all legacy confirmations failed
toast.error(
`Failed to confirm ${failedCount} legacy account${failedCount > 1 ? 's' : ''}. Refreshed account state.`
);
@@ -188,13 +196,11 @@ export function useConfirmLegacyAccountPolicies() {
}
if (updatedCount > 0) {
- toast.success(
- `Confirmed explicit sync mode for ${updatedCount} legacy account${updatedCount > 1 ? 's' : ''}`
- );
+ toast.success(t('toasts.legacyConfirmSuccess', { count: updatedCount }));
return;
}
- toast.info('No legacy accounts need confirmation');
+ toast.info(t('toasts.noLegacyAccounts'));
},
onError: (error: Error) => {
queryClient.invalidateQueries({ queryKey: ['accounts'] });
diff --git a/ui/src/hooks/use-claude-extension.ts b/ui/src/hooks/use-claude-extension.ts
index 058e2e0b..d2df822c 100644
--- a/ui/src/hooks/use-claude-extension.ts
+++ b/ui/src/hooks/use-claude-extension.ts
@@ -1,5 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import { withApiBase } from '@/lib/api-client';
export interface ClaudeExtensionProfileOption {
@@ -149,6 +150,7 @@ export function useCreateClaudeExtensionBinding() {
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bindingsQueryKey });
+ // TODO i18n: missing key for 'Binding created'
toast.success('Binding created');
},
onError: (error: Error) => toast.error(error.message),
@@ -157,6 +159,7 @@ export function useCreateClaudeExtensionBinding() {
export function useUpdateClaudeExtensionBinding() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ id, binding }: { id: string; binding: ClaudeExtensionBindingInput }) =>
@@ -172,7 +175,7 @@ export function useUpdateClaudeExtensionBinding() {
queryClient.invalidateQueries({
queryKey: ['claude-extension-binding-status', variables.id],
});
- toast.success('Binding saved');
+ toast.success(t('claudeExtensionPage.bindingSaved'));
},
onError: (error: Error) => toast.error(error.message),
});
@@ -180,6 +183,7 @@ export function useUpdateClaudeExtensionBinding() {
export function useDeleteClaudeExtensionBinding() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (id: string) =>
@@ -188,7 +192,7 @@ export function useDeleteClaudeExtensionBinding() {
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bindingsQueryKey });
- toast.success('Binding deleted');
+ toast.success(t('claudeExtensionPage.bindingDeleted'));
},
onError: (error: Error) => toast.error(error.message),
});
@@ -216,9 +220,11 @@ function useClaudeExtensionActionMutation(action: 'apply' | 'reset', successMess
}
export function useApplyClaudeExtensionBinding() {
- return useClaudeExtensionActionMutation('apply', 'Binding applied');
+ const { t } = useTranslation();
+ return useClaudeExtensionActionMutation('apply', t('claudeExtensionPage.bindingApplied'));
}
export function useResetClaudeExtensionBinding() {
- return useClaudeExtensionActionMutation('reset', 'Managed values removed');
+ const { t } = useTranslation();
+ return useClaudeExtensionActionMutation('reset', t('claudeExtensionPage.managedValuesRemoved'));
}
diff --git a/ui/src/hooks/use-cliproxy-ai-providers.ts b/ui/src/hooks/use-cliproxy-ai-providers.ts
index 8493493c..6824859b 100644
--- a/ui/src/hooks/use-cliproxy-ai-providers.ts
+++ b/ui/src/hooks/use-cliproxy-ai-providers.ts
@@ -17,6 +17,7 @@ export function useCliproxyAiProviders() {
export function useCreateCliproxyAiProviderEntry() {
const queryClient = useQueryClient();
+ // TODO i18n: missing key for 'Provider entry created'
return useMutation({
mutationFn: ({
@@ -38,6 +39,7 @@ export function useCreateCliproxyAiProviderEntry() {
export function useUpdateCliproxyAiProviderEntry() {
const queryClient = useQueryClient();
+ // TODO i18n: missing key for 'Provider entry updated'
return useMutation({
mutationFn: ({
@@ -61,6 +63,7 @@ export function useUpdateCliproxyAiProviderEntry() {
export function useDeleteCliproxyAiProviderEntry() {
const queryClient = useQueryClient();
+ // TODO i18n: missing key for 'Provider entry removed'
return useMutation({
mutationFn: ({ family, entryId }: { family: AiProviderFamilyId; entryId: string }) =>
diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts
index a49d0c25..3f41a1e3 100644
--- a/ui/src/hooks/use-cliproxy-auth-flow.ts
+++ b/ui/src/hooks/use-cliproxy-auth-flow.ts
@@ -6,6 +6,7 @@
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import { api } from '@/lib/api-client';
import { isValidProvider, isDeviceCodeProvider } from '@/lib/provider-config';
@@ -77,6 +78,7 @@ const INITIAL_STATE: AuthFlowState = {
};
export function useCliproxyAuthFlow() {
+ const { t } = useTranslation();
const [state, setState] = useState(INITIAL_STATE);
const stateRef = useRef(INITIAL_STATE);
@@ -129,6 +131,7 @@ export function useCliproxyAuthFlow() {
setState((prev) => ({
...prev,
isAuthenticating: false,
+ // TODO i18n: missing key for 'Authentication timed out. Please try again.'
error: 'Authentication timed out. Please try again.',
}));
}
@@ -158,6 +161,7 @@ export function useCliproxyAuthFlow() {
const hasAccount = typeof data.account === 'object' && data.account !== null;
if (!hasAccount) {
stopPolling();
+ // TODO i18n: missing key for 'Authenticated account could not be registered'
const errorMsg = 'Authenticated account could not be registered';
toast.error(errorMsg);
setState((prev) => ({
@@ -173,7 +177,7 @@ export function useCliproxyAuthFlow() {
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
invalidateCliproxyRoutingData(queryClient);
- toast.success(`${provider} authentication successful`);
+ toast.success(t('toasts.providerAuthSuccess', { provider }));
openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
} else if (data.status === 'auth_url') {
@@ -194,7 +198,7 @@ export function useCliproxyAuthFlow() {
data.user_code && data.verification_url
? `Open ${data.verification_url} and enter code: ${data.user_code}`
: 'Switch to Device Code method and try again.';
- toast.error('Provider returned Device Code flow in callback mode');
+ toast.error(t('toasts.providerDeviceCodeInCallback'));
setState((prev) => ({
...prev,
isAuthenticating: false,
@@ -222,6 +226,7 @@ export function useCliproxyAuthFlow() {
}
stopPolling();
+ // TODO i18n: missing key for 'Lost contact with the auth status endpoint'
const message =
error instanceof Error && error.message.trim().length > 0
? error.message
@@ -234,12 +239,13 @@ export function useCliproxyAuthFlow() {
}));
}
},
- [isActiveAttempt, queryClient, stopPolling]
+ [isActiveAttempt, queryClient, stopPolling, t]
);
const startAuth = useCallback(
async (provider: string, options?: StartAuthOptions) => {
if (!isValidProvider(provider)) {
+ // TODO i18n: missing key for 'Unknown provider: {{provider}}'
setState({
...INITIAL_STATE,
error: `Unknown provider: ${provider}`,
@@ -313,6 +319,7 @@ export function useCliproxyAuthFlow() {
openedAuthUrlRef.current = false;
setState(INITIAL_STATE);
} else {
+ // TODO i18n: missing key for 'Authenticated account could not be registered' (start endpoint)
const errorMsg =
typeof data.error === 'string'
? data.error
@@ -363,6 +370,7 @@ export function useCliproxyAuthFlow() {
const success = data.success === true;
if (!response.ok || !success) {
+ // TODO i18n: missing key for 'Failed to start OAuth'
const errorMsg = typeof data.error === 'string' ? data.error : 'Failed to start OAuth';
throw new Error(errorMsg);
}
@@ -475,9 +483,10 @@ export function useCliproxyAuthFlow() {
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
queryClient.invalidateQueries({ queryKey: ['account-quota'] });
invalidateCliproxyRoutingData(queryClient);
- toast.success(`${currentProvider} authentication successful`);
+ toast.success(t('toasts.providerAuthSuccess', { provider: currentProvider }));
setState(INITIAL_STATE);
} else {
+ // TODO i18n: missing key for 'Callback submission failed'
const errorMsg =
typeof data.error === 'string'
? data.error
@@ -490,12 +499,13 @@ export function useCliproxyAuthFlow() {
if (!isActiveAttempt(attemptId)) {
return;
}
+ // TODO i18n: missing key for 'Failed to submit callback'
const message = error instanceof Error ? error.message : 'Failed to submit callback';
toast.error(message);
setState((prev) => ({ ...prev, isSubmittingCallback: false, error: message }));
}
},
- [isActiveAttempt, state.provider, queryClient, stopPolling]
+ [isActiveAttempt, state.provider, queryClient, stopPolling, t]
);
return useMemo(
diff --git a/ui/src/hooks/use-cliproxy-config.ts b/ui/src/hooks/use-cliproxy-config.ts
index 23795ced..6dc25288 100644
--- a/ui/src/hooks/use-cliproxy-config.ts
+++ b/ui/src/hooks/use-cliproxy-config.ts
@@ -11,6 +11,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { parse as parseYaml } from 'yaml';
import { api } from '@/lib/api-client';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
interface ValidationResult {
valid: boolean;
@@ -70,6 +71,7 @@ function validateYaml(code: string): ValidationResult {
export function useCliproxyConfig() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
// Fetch config.yaml - server state
const configQuery = useQuery({
@@ -121,21 +123,21 @@ export function useCliproxyConfig() {
dispatch({ type: 'SAVE_SUCCESS', content: variables });
queryClient.invalidateQueries({ queryKey: ['cliproxy-config-yaml'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
- toast.success('Configuration saved successfully');
+ toast.success(t('toasts.configSaved'));
},
onError: (error: Error) => {
- toast.error(`Failed to save: ${error.message}`);
+ toast.error(t('toasts.configSaveFailed', { error: error.message }));
},
});
// Save handler
const saveContent = useCallback(() => {
if (!validation.valid) {
- toast.error('Cannot save invalid YAML');
+ toast.error(t('toasts.invalidYaml'));
return;
}
saveMutation.mutate(content);
- }, [content, validation.valid, saveMutation]);
+ }, [content, validation.valid, saveMutation, t]);
return {
// State
diff --git a/ui/src/hooks/use-cliproxy-sync.ts b/ui/src/hooks/use-cliproxy-sync.ts
index e468c7b5..7baa5510 100644
--- a/ui/src/hooks/use-cliproxy-sync.ts
+++ b/ui/src/hooks/use-cliproxy-sync.ts
@@ -4,6 +4,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
/** Sync status response */
export interface SyncStatus {
@@ -151,6 +152,7 @@ export function useSyncPreview() {
*/
export function useExecuteSync() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: executeSync,
@@ -161,15 +163,16 @@ export function useExecuteSync() {
// Show success toast with synced count
if (data.syncedCount === 0) {
- toast.info('No profiles to sync');
+ toast.info(t('toasts.noProfilesToSync'));
} else {
+ // TODO i18n: missing key for 'Synced {{count}} profile(s) to CLIProxy'
toast.success(
`Synced ${data.syncedCount} profile${data.syncedCount === 1 ? '' : 's'} to CLIProxy`
);
}
},
onError: (error: Error) => {
- toast.error(`Sync failed: ${error.message}`);
+ toast.error(t('toasts.syncFailed', { error: error.message }));
},
});
}
diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts
index 46d48a4e..2574833d 100644
--- a/ui/src/hooks/use-cliproxy.ts
+++ b/ui/src/hooks/use-cliproxy.ts
@@ -13,6 +13,7 @@ import {
type RoutingStrategy,
} from '@/lib/api-client';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
function invalidateCliproxyRoutingQueries(queryClient: ReturnType): void {
queryClient.invalidateQueries({ queryKey: ['cliproxy-catalog'] });
@@ -57,12 +58,15 @@ export function useCliproxyRoutingStrategy() {
export function useUpdateCliproxyRoutingStrategy() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (strategy: RoutingStrategy) => api.cliproxy.updateRoutingStrategy(strategy),
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['cliproxy-routing'] });
- toast.success(result.message || `Routing strategy set to ${result.strategy}`);
+ toast.success(
+ result.message || t('toasts.routingStrategySet', { strategy: result.strategy })
+ );
},
onError: (error: Error) => {
toast.error(error.message);
@@ -72,12 +76,13 @@ export function useUpdateCliproxyRoutingStrategy() {
export function useCreateVariant() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (data: CreateVariant) => api.cliproxy.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
- toast.success('Variant created successfully');
+ toast.success(t('toasts.variantCreated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -87,13 +92,14 @@ export function useCreateVariant() {
export function useUpdateVariant() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ name, data }: { name: string; data: UpdateVariant }) =>
api.cliproxy.update(name, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
- toast.success('Variant updated successfully');
+ toast.success(t('toasts.variantUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -103,12 +109,13 @@ export function useUpdateVariant() {
export function useDeleteVariant() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (name: string) => api.cliproxy.delete(name),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
- toast.success('Variant deleted successfully');
+ toast.success(t('toasts.variantDeleted'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -134,13 +141,14 @@ export function useProviderAccounts(provider: string) {
export function useSetDefaultAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
api.cliproxy.accounts.setDefault(provider, accountId),
onSuccess: () => {
invalidateCliproxyAccountQueries(queryClient);
- toast.success('Default account updated');
+ toast.success(t('toasts.defaultAccountUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -150,13 +158,14 @@ export function useSetDefaultAccount() {
export function useRemoveAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
api.cliproxy.accounts.remove(provider, accountId),
onSuccess: () => {
invalidateCliproxyAccountQueries(queryClient);
- toast.success('Account removed');
+ toast.success(t('toasts.accountRemoved'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -166,6 +175,7 @@ export function useRemoveAccount() {
export function usePauseAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
@@ -173,7 +183,7 @@ export function usePauseAccount() {
onSuccess: () => {
invalidateCliproxyAccountQueries(queryClient);
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
- toast.success('Account paused');
+ toast.success(t('toasts.accountPaused'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -183,6 +193,7 @@ export function usePauseAccount() {
export function useResumeAccount() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, accountId }: { provider: string; accountId: string }) =>
@@ -190,7 +201,7 @@ export function useResumeAccount() {
onSuccess: () => {
invalidateCliproxyAccountQueries(queryClient);
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
- toast.success('Account resumed');
+ toast.success(t('toasts.accountResumed'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -208,6 +219,7 @@ export function useSoloAccount() {
invalidateCliproxyAccountQueries(queryClient);
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
const pausedCount = data.paused.length;
+ // TODO i18n: missing key for 'Solo mode: paused {{count}} other account(s)'
toast.success(
`Solo mode: paused ${pausedCount} other account${pausedCount !== 1 ? 's' : ''}`
);
@@ -227,10 +239,12 @@ export function useBulkPauseAccounts() {
onSuccess: (data) => {
invalidateCliproxyAccountQueries(queryClient);
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
+ // TODO i18n: missing key for 'Paused {{count}} account(s)'
toast.success(
`Paused ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
);
if (data.failed.length > 0) {
+ // TODO i18n: missing key for '{{count}} account(s) failed to pause'
toast.warning(
`${data.failed.length} account${data.failed.length !== 1 ? 's' : ''} failed to pause`
);
@@ -251,10 +265,12 @@ export function useBulkResumeAccounts() {
onSuccess: (data) => {
invalidateCliproxyAccountQueries(queryClient);
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
+ // TODO i18n: missing key for 'Resumed {{count}} account(s)'
toast.success(
`Resumed ${data.succeeded.length} account${data.succeeded.length !== 1 ? 's' : ''}`
);
if (data.failed.length > 0) {
+ // TODO i18n: missing key for '{{count}} account(s) failed to resume'
toast.warning(
`${data.failed.length} account${data.failed.length !== 1 ? 's' : ''} failed to resume`
);
@@ -269,13 +285,14 @@ export function useBulkResumeAccounts() {
// OAuth flow hook
export function useStartAuth() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, nickname }: { provider: string; nickname?: string }) =>
api.cliproxy.auth.start(provider, nickname),
onSuccess: (_data, variables) => {
invalidateCliproxyAccountQueries(queryClient);
- toast.success(`Account added for ${variables.provider}`);
+ toast.success(t('toasts.accountAdded', { provider: variables.provider }));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -296,15 +313,16 @@ export function useCancelAuth() {
// Kiro IDE import hook (alternative auth path when OAuth callback fails)
export function useKiroImport() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: () => api.cliproxy.auth.kiroImport(),
onSuccess: (data) => {
invalidateCliproxyAccountQueries(queryClient);
if (data.account) {
- toast.success(`Imported Kiro account: ${data.account.email || data.account.id}`);
+ toast.success(t('toasts.kiroImported', { name: data.account.email || data.account.id }));
} else {
- toast.success('Kiro token imported');
+ toast.success(t('toasts.kiroTokenImported'));
}
},
onError: (error: Error) => {
@@ -331,13 +349,14 @@ export function useCliproxyModels() {
export function useUpdateModel() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ provider, model }: { provider: string; model: string }) =>
api.cliproxy.updateModel(provider, model),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cliproxy-models'] });
- toast.success('Model updated');
+ toast.success(t('toasts.modelUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -357,13 +376,14 @@ export function usePresets(profile: string) {
export function useCreatePreset() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ profile, data }: { profile: string; data: CreatePreset }) =>
api.presets.create(profile, data),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['presets', variables.profile] });
- toast.success(`Preset "${variables.data.name}" saved`);
+ toast.success(t('toasts.presetSaved', { name: variables.data.name }));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -373,13 +393,14 @@ export function useCreatePreset() {
export function useDeletePreset() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ profile, name }: { profile: string; name: string }) =>
api.presets.delete(profile, name),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['presets', variables.profile] });
- toast.success('Preset deleted');
+ toast.success(t('toasts.presetDeleted'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -399,17 +420,18 @@ export function useProxyStatus() {
export function useStartProxy() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: () => api.cliproxy.proxyStart(),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
if (data.alreadyRunning) {
- toast.info('CLIProxy was already running');
+ toast.info(t('toasts.cliproxyAlreadyRunning'));
} else if (data.started) {
- toast.success('CLIProxy started successfully');
+ toast.success(t('toasts.cliproxyStarted'));
} else {
- toast.error(data.error || 'Failed to start CLIProxy');
+ toast.error(data.error || t('toasts.cliproxyStartFailed'));
}
},
onError: (error: Error) => {
@@ -420,17 +442,19 @@ export function useStartProxy() {
export function useStopProxy() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: () => api.cliproxy.proxyStop(),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
if (data.stopped) {
+ // TODO i18n: missing key for 'CLIProxy stopped ({{count}} session(s) disconnected)'
toast.success(
`CLIProxy stopped${data.sessionCount ? ` (${data.sessionCount} session(s) disconnected)` : ''}`
);
} else {
- toast.error(data.error || 'Failed to stop CLIProxy');
+ toast.error(data.error || t('toasts.cliproxyStopFailed'));
}
},
onError: (error: Error) => {
@@ -472,10 +496,11 @@ export function useUpdateBackend() {
queryClient.invalidateQueries({ queryKey: ['cliproxy-server-config'], refetchType: 'all' });
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
+ // TODO i18n: missing key for 'Backend updated'
toast.success('Backend updated');
},
onError: (error: Error) => {
- // Handle 409 conflict (proxy running)
+ // TODO i18n: missing key for 'Stop the proxy first to change backend'
if (error.message.includes('Proxy is running')) {
toast.error('Stop the proxy first to change backend');
} else {
@@ -511,8 +536,10 @@ export function useInstallVersion() {
queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'] });
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
if (data.success) {
+ // TODO i18n: missing key for 'Installed v{{version}}'
toast.success(data.message || `Installed v${data.version}`);
} else {
+ // TODO i18n: missing key for 'Installation failed'
toast.error(data.error || 'Installation failed');
}
},
@@ -530,8 +557,10 @@ export function useRestartProxy() {
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
if (data.success) {
+ // TODO i18n: missing key for 'Proxy restarted on port {{port}}'
toast.success(`Proxy restarted on port ${data.port}`);
} else {
+ // TODO i18n: missing key for 'Restart failed'
toast.error(data.error || 'Restart failed');
}
},
diff --git a/ui/src/hooks/use-device-code.ts b/ui/src/hooks/use-device-code.ts
index 81c18a44..2011b6aa 100644
--- a/ui/src/hooks/use-device-code.ts
+++ b/ui/src/hooks/use-device-code.ts
@@ -8,6 +8,7 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import { getDeviceCodeProviderDisplayName } from '@/lib/provider-config';
export interface DeviceCodePrompt {
@@ -33,6 +34,7 @@ function coerceProvider(value: unknown): string {
}
export function useDeviceCode() {
+ const { t } = useTranslation();
const [state, setState] = useState({
isOpen: false,
prompt: null,
@@ -48,7 +50,7 @@ export function useDeviceCode() {
console.log('[DeviceCode] Received prompt:', data.sessionId);
const provider = coerceProvider(data.provider);
const displayName = getDeviceCodeProviderDisplayName(provider);
- toast.info(`${displayName} authorization required`);
+ toast.info(t('toasts.authRequired', { provider: displayName }));
setState({
isOpen: true,
@@ -66,7 +68,7 @@ export function useDeviceCode() {
setState((prev) => {
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
const displayName = getDeviceCodeProviderDisplayName(prev.prompt.provider);
- toast.success(`${displayName} authentication successful!`);
+ toast.success(t('toasts.authSuccess', { provider: displayName }));
return { isOpen: false, prompt: null, error: null };
}
return prev;
@@ -76,7 +78,7 @@ export function useDeviceCode() {
setState((prev) => {
if (prev.prompt && prev.prompt.sessionId === data.sessionId) {
const displayName = getDeviceCodeProviderDisplayName(prev.prompt.provider);
- toast.error(`${displayName} authentication failed`);
+ toast.error(t('toasts.authFailed', { provider: displayName }));
return { isOpen: false, prompt: null, error: data.error as string };
}
return prev;
@@ -85,7 +87,7 @@ export function useDeviceCode() {
console.log('[DeviceCode] Code expired:', data.sessionId);
setState((prev) => {
if (prev.prompt?.sessionId === data.sessionId) {
- toast.error('Device code expired. Please try again.');
+ toast.error(t('toasts.deviceCodeExpired'));
return { isOpen: false, prompt: null, error: 'Device code expired' };
}
return prev;
@@ -99,7 +101,7 @@ export function useDeviceCode() {
return () => {
window.removeEventListener('ws-message', handleMessage as EventListener);
};
- }, []);
+ }, [t]);
const handleClose = useCallback(() => {
setState({ isOpen: false, prompt: null, error: null });
@@ -115,12 +117,12 @@ export function useDeviceCode() {
if (state.prompt?.userCode) {
try {
await navigator.clipboard.writeText(state.prompt.userCode);
- toast.success('Code copied to clipboard');
+ toast.success(t('toasts.codeCopied'));
} catch {
- toast.error('Failed to copy code');
+ toast.error(t('toasts.failedCopy'));
}
}
- }, [state.prompt]);
+ }, [state.prompt, t]);
return useMemo(
() => ({
diff --git a/ui/src/hooks/use-logs.ts b/ui/src/hooks/use-logs.ts
index 2f63f4fa..356ebb3a 100644
--- a/ui/src/hooks/use-logs.ts
+++ b/ui/src/hooks/use-logs.ts
@@ -1,6 +1,7 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDeferredValue, useMemo, useState } from 'react';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
import {
api,
type LogsEntry,
@@ -100,6 +101,7 @@ export function useLogsWorkspace() {
export function useUpdateLogsConfig() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (payload: UpdateLogsConfigPayload) => api.logs.updateConfig(payload),
@@ -109,10 +111,10 @@ export function useUpdateLogsConfig() {
queryClient.invalidateQueries({ queryKey: SOURCES_QUERY_KEY }),
queryClient.invalidateQueries({ queryKey: ['logs', 'entries'] }),
]);
- toast.success('Logging configuration saved.');
+ toast.success(t('toasts.loggingConfigSaved'));
},
onError: (error: Error) => {
- toast.error(error.message || 'Failed to save logging configuration.');
+ toast.error(error.message || t('toasts.loggingConfigSaveFailed'));
},
});
}
diff --git a/ui/src/hooks/use-profiles.ts b/ui/src/hooks/use-profiles.ts
index 5e46dcd3..971bab53 100644
--- a/ui/src/hooks/use-profiles.ts
+++ b/ui/src/hooks/use-profiles.ts
@@ -13,6 +13,7 @@ import {
type ImportProfileRequest,
} from '@/lib/api-client';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
export function useProfiles() {
return useQuery({
@@ -23,12 +24,13 @@ export function useProfiles() {
export function useCreateProfile() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (data: CreateProfile) => api.profiles.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Profile created successfully');
+ toast.success(t('toasts.profileCreated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -38,13 +40,14 @@ export function useCreateProfile() {
export function useUpdateProfile() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ name, data }: { name: string; data: UpdateProfile }) =>
api.profiles.update(name, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Profile updated successfully');
+ toast.success(t('toasts.profileUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -54,12 +57,13 @@ export function useUpdateProfile() {
export function useDeleteProfile() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (name: string) => api.profiles.delete(name),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Profile deleted successfully');
+ toast.success(t('toasts.profileDeleted'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -75,25 +79,27 @@ export function useDiscoverProfileOrphans() {
export function useRegisterProfileOrphans() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (data: RegisterProfileOrphansRequest) => api.profiles.registerOrphans(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Orphan profiles registration complete');
+ toast.success(t('toasts.orphanProfilesComplete'));
},
});
}
export function useCopyProfile() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: ({ name, data }: { name: string; data: CopyProfileRequest }) =>
api.profiles.copy(name, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Profile copied successfully');
+ toast.success(t('toasts.profileCopied'));
},
});
}
@@ -107,12 +113,13 @@ export function useExportProfile() {
export function useImportProfile() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (data: ImportProfileRequest) => api.profiles.import(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.success('Profile imported successfully');
+ toast.success(t('toasts.profileImported'));
},
});
}
diff --git a/ui/src/hooks/use-unified-config.ts b/ui/src/hooks/use-unified-config.ts
index e0d1d021..871e1033 100644
--- a/ui/src/hooks/use-unified-config.ts
+++ b/ui/src/hooks/use-unified-config.ts
@@ -6,6 +6,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api-client';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
/**
* Get current config format and migration status
@@ -33,13 +34,14 @@ export function useUnifiedConfig() {
*/
export function useUpdateConfig() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (config: Record) => api.config.update(config),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['unified-config'] });
queryClient.invalidateQueries({ queryKey: ['config-format'] });
- toast.success('Configuration updated successfully');
+ toast.success(t('toasts.unifiedConfigUpdated'));
},
onError: (error: Error) => {
toast.error(error.message);
@@ -52,20 +54,21 @@ export function useUpdateConfig() {
*/
export function useMigration() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (dryRun: boolean) => api.config.migrate(dryRun),
onSuccess: (result, dryRun) => {
if (dryRun) {
- toast.info('Migration preview completed');
+ toast.info(t('toasts.migrationPreviewComplete'));
} else if (result.success) {
queryClient.invalidateQueries({ queryKey: ['config-format'] });
queryClient.invalidateQueries({ queryKey: ['unified-config'] });
queryClient.invalidateQueries({ queryKey: ['profiles'] });
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.success('Migration completed successfully');
+ toast.success(t('toasts.migrationComplete'));
} else {
- toast.error(result.error ?? 'Migration failed');
+ toast.error(result.error ?? t('toasts.migrationFailed'));
}
},
onError: (error: Error) => {
@@ -79,6 +82,7 @@ export function useMigration() {
*/
export function useRollback() {
const queryClient = useQueryClient();
+ const { t } = useTranslation();
return useMutation({
mutationFn: (backupPath: string) => api.config.rollback(backupPath),
@@ -88,9 +92,9 @@ export function useRollback() {
queryClient.invalidateQueries({ queryKey: ['unified-config'] });
queryClient.invalidateQueries({ queryKey: ['profiles'] });
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.success('Rollback completed successfully');
+ toast.success(t('toasts.rollbackComplete'));
} else {
- toast.error('Rollback failed');
+ toast.error(t('toasts.rollbackFailed'));
}
},
onError: (error: Error) => {
diff --git a/ui/src/hooks/use-websocket.ts b/ui/src/hooks/use-websocket.ts
index aab8a6e1..aa2ced99 100644
--- a/ui/src/hooks/use-websocket.ts
+++ b/ui/src/hooks/use-websocket.ts
@@ -7,6 +7,7 @@
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useTranslation } from 'react-i18next';
interface WSMessage {
type: string;
@@ -17,6 +18,7 @@ interface WSMessage {
type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
export function useWebSocket() {
+ const { t } = useTranslation();
const [status, setStatus] = useState('disconnected');
const [isReconnecting, setIsReconnecting] = useState(false);
const wsRef = useRef(null);
@@ -36,17 +38,17 @@ export function useWebSocket() {
case 'config-changed':
queryClient.invalidateQueries({ queryKey: ['profiles'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy'] });
- toast.info('Configuration updated externally');
+ toast.info(t('toasts.configUpdatedExternally'));
break;
case 'settings-changed':
queryClient.invalidateQueries({ queryKey: ['profiles'] });
- toast.info('Settings file updated');
+ toast.info(t('toasts.settingsFileUpdated'));
break;
case 'profiles-changed':
queryClient.invalidateQueries({ queryKey: ['accounts'] });
- toast.info('Accounts updated');
+ toast.info(t('toasts.accountsUpdated'));
break;
case 'proxy-status-changed':
@@ -61,7 +63,7 @@ export function useWebSocket() {
console.log(`[WS] Unknown message: ${message.type}`);
}
},
- [queryClient]
+ [queryClient, t]
);
const connect = useCallback(() => {
diff --git a/ui/src/lib/account-identity.ts b/ui/src/lib/account-identity.ts
index eac2beda..b001fd02 100644
--- a/ui/src/lib/account-identity.ts
+++ b/ui/src/lib/account-identity.ts
@@ -32,13 +32,13 @@ function formatVariantPart(part: string): string {
switch (normalized) {
case 'team':
- return 'Team';
+ return 'Team'; // TODO i18n: missing key for account variant team
case 'free':
- return 'Free';
+ return 'Free'; // TODO i18n: missing key for account variant free
case 'plus':
- return 'Plus';
+ return 'Plus'; // TODO i18n: missing key for account variant plus
case 'pro':
- return 'Pro';
+ return 'Pro'; // TODO i18n: missing key for account variant pro
default:
return /^[a-f0-9]{8}$/i.test(normalized)
? normalized
@@ -98,14 +98,14 @@ function formatWorkspaceLabel(parts: string[]): {
const workspaceId = parts.find((part) => /^[a-f0-9]{8}$/i.test(part));
if (workspaceId) {
return {
- detailLabel: `Workspace ${workspaceId.toLowerCase()}`,
+ detailLabel: `Workspace ${workspaceId.toLowerCase()}`, // TODO i18n: missing key for workspace label
compactDetailLabel: workspaceId.toLowerCase(),
};
}
const extraLabel = parts.map(formatVariantPart).filter(Boolean).join(' · ');
return {
- detailLabel: extraLabel || 'Team',
+ detailLabel: extraLabel || 'Team', // TODO i18n: missing key for team fallback
compactDetailLabel: extraLabel || 'Team',
};
}
@@ -155,7 +155,7 @@ export function getAccountIdentityPresentation(
const suffix = parts[parts.length - 1]?.toLowerCase();
if (suffix && BUSINESS_PLAN_PARTS.has(suffix)) {
const workspace = formatWorkspaceLabel(parts.slice(0, -1));
- const inlineLabel = ['Business', workspace.detailLabel].filter(Boolean).join(' · ');
+ const inlineLabel = ['Business', workspace.detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing keys for Business/Personal audience labels
return {
email: resolvedEmail,
audience: 'business',
@@ -171,7 +171,7 @@ export function getAccountIdentityPresentation(
.filter(Boolean)
.join(' · ');
const detailLabel = detailParts || formatVariantPart(suffix);
- const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · ');
+ const inlineLabel = ['Personal', detailLabel].filter(Boolean).join(' · '); // TODO i18n: missing key for Personal
return {
email: resolvedEmail,
audience: 'personal',
diff --git a/ui/src/lib/codex-config.ts b/ui/src/lib/codex-config.ts
index cfae6619..35f57b14 100644
--- a/ui/src/lib/codex-config.ts
+++ b/ui/src/lib/codex-config.ts
@@ -61,41 +61,41 @@ wire_api = "responses"`;
export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
{
name: 'multi_agent',
- label: 'Multi-agent',
- description: 'Enable subagent collaboration tools.',
+ label: 'Multi-agent', // TODO i18n: missing key for codex feature
+ description: 'Enable subagent collaboration tools.', // TODO i18n: missing key
},
{
name: 'unified_exec',
- label: 'Unified exec',
- description: 'Use the PTY-backed unified exec tool.',
+ label: 'Unified exec', // TODO i18n: missing key for codex feature
+ description: 'Use the PTY-backed unified exec tool.', // TODO i18n: missing key
},
{
name: 'shell_snapshot',
- label: 'Shell snapshot',
- description: 'Reuse shell environment snapshots.',
+ label: 'Shell snapshot', // TODO i18n: missing key for codex feature
+ description: 'Reuse shell environment snapshots.', // TODO i18n: missing key
},
{
name: 'apply_patch_freeform',
- label: 'Apply patch',
- description: 'Enable freeform apply_patch edits.',
+ label: 'Apply patch', // TODO i18n: missing key for codex feature
+ description: 'Enable freeform apply_patch edits.', // TODO i18n: missing key
},
- { name: 'js_repl', label: 'JS REPL', description: 'Enable the Node-backed JavaScript REPL.' },
+ { name: 'js_repl', label: 'JS REPL', description: 'Enable the Node-backed JavaScript REPL.' }, // TODO i18n: missing keys
{
name: 'runtime_metrics',
- label: 'Runtime metrics',
- description: 'Collect Codex runtime metrics.',
+ label: 'Runtime metrics', // TODO i18n: missing key for codex feature
+ description: 'Collect Codex runtime metrics.', // TODO i18n: missing key
},
{
name: 'prevent_idle_sleep',
- label: 'Prevent idle sleep',
- description: 'Keep the machine awake while active.',
+ label: 'Prevent idle sleep', // TODO i18n: missing key for codex feature
+ description: 'Keep the machine awake while active.', // TODO i18n: missing key
},
- { name: 'fast_mode', label: 'Fast mode', description: 'Allow the fast service tier path.' },
- { name: 'apps', label: 'Apps', description: 'Enable ChatGPT Apps and connectors support.' },
+ { name: 'fast_mode', label: 'Fast mode', description: 'Allow the fast service tier path.' }, // TODO i18n: missing keys
+ { name: 'apps', label: 'Apps', description: 'Enable ChatGPT Apps and connectors support.' }, // TODO i18n: missing keys
{
name: 'smart_approvals',
- label: 'Smart approvals',
- description: 'Route eligible approvals through the guardian flow.',
+ label: 'Smart approvals', // TODO i18n: missing key for codex feature
+ description: 'Route eligible approvals through the guardian flow.', // TODO i18n: missing key
},
];
diff --git a/ui/src/lib/codex-effort.ts b/ui/src/lib/codex-effort.ts
index ff5e8bfb..9f3b26f2 100644
--- a/ui/src/lib/codex-effort.ts
+++ b/ui/src/lib/codex-effort.ts
@@ -10,12 +10,16 @@ export function parseCodexEffort(modelId: string | undefined): CodexEffort | und
}
export function getCodexEffortDisplay(
- modelId: string | undefined
+ modelId: string | undefined,
+ effortLabels?: { pinned: (effort: string) => string; auto: string }
): { label: string; explicit: boolean } | null {
if (!modelId) return null;
const effort = parseCodexEffort(modelId);
if (effort) {
- return { label: `Pinned ${effort}`, explicit: true };
+ return {
+ label: effortLabels?.pinned(effort) ?? `Pinned ${effort}`,
+ explicit: true,
+ };
}
- return { label: 'Auto effort', explicit: false };
+ return { label: effortLabels?.auto ?? 'Auto effort', explicit: false };
}
diff --git a/ui/src/lib/droid-byok-custom-models.ts b/ui/src/lib/droid-byok-custom-models.ts
index c454dcdb..30f7e628 100644
--- a/ui/src/lib/droid-byok-custom-models.ts
+++ b/ui/src/lib/droid-byok-custom-models.ts
@@ -267,7 +267,7 @@ export function extractDroidByokModels(settings: Record): Droid
const displayName =
asNonEmptyString(entry.displayName) ??
asNonEmptyString(entry.model_display_name) ??
- 'Unnamed model';
+ 'Unnamed model'; // TODO i18n: missing key for unnamed model
const model = asNonEmptyString(entry.model) ?? '';
const provider = asNonEmptyString(entry.provider) ?? 'unknown';
const providerKind = normalizeProviderKind(provider);
diff --git a/ui/src/lib/error-log-parser.ts b/ui/src/lib/error-log-parser.ts
index 66b4207d..ff61c6c0 100644
--- a/ui/src/lib/error-log-parser.ts
+++ b/ui/src/lib/error-log-parser.ts
@@ -398,8 +398,6 @@ function getStatusText(code: number): string {
*/
export function formatRelativeTime(modifiedSeconds: number, locale?: string): string {
const formatLocale = getFormattingLocale(locale);
- const isZh = formatLocale === 'zh-CN';
- const isVi = formatLocale === 'vi';
const now = Date.now();
const modified = modifiedSeconds * 1000; // Convert to milliseconds
const diff = now - modified;
@@ -410,21 +408,33 @@ export function formatRelativeTime(modifiedSeconds: number, locale?: string): st
const days = Math.floor(hours / 24);
if (seconds < 60) {
+ // TODO i18n: missing key for relative time "just now"
+ const isZh = formatLocale === 'zh-CN';
+ const isVi = formatLocale === 'vi';
if (isZh) return '刚刚';
if (isVi) return 'vừa xong';
return 'just now';
}
if (minutes < 60) {
+ // TODO i18n: missing key for relative time minutes
+ const isZh = formatLocale === 'zh-CN';
+ const isVi = formatLocale === 'vi';
if (isZh) return `${minutes} 分钟前`;
if (isVi) return `${minutes} phút trước`;
return `${minutes}m ago`;
}
if (hours < 24) {
+ // TODO i18n: missing key for relative time hours
+ const isZh = formatLocale === 'zh-CN';
+ const isVi = formatLocale === 'vi';
if (isZh) return `${hours} 小时前`;
if (isVi) return `${hours} giờ trước`;
return `${hours}h ago`;
}
if (days < 7) {
+ // TODO i18n: missing key for relative time days
+ const isZh = formatLocale === 'zh-CN';
+ const isVi = formatLocale === 'vi';
if (isZh) return `${days} 天前`;
if (isVi) return `${days} ngày trước`;
return `${days}d ago`;
@@ -458,6 +468,7 @@ export function getStatusColor(code: number): string {
* Get error type label
*/
export function getErrorTypeLabel(type: ParsedErrorLog['errorType'], locale?: string): string {
+ // TODO i18n: missing keys for error type labels (rate_limit, auth, not_found, server, timeout, unknown)
const formatLocale = getFormattingLocale(locale);
if (formatLocale === 'zh-CN') {
const labels: Record = {
@@ -520,6 +531,7 @@ export function formatQuotaResetTimestamp(
const formatLocale = getFormattingLocale(locale);
const isZh = formatLocale === 'zh-CN';
const isVi = formatLocale === 'vi';
+ // TODO i18n: missing keys for quota reset timestamp formatting
try {
const resetDate = new Date(timestamp);
const now = new Date();
diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts
index 67e00c03..99c7133d 100644
--- a/ui/src/lib/i18n.ts
+++ b/ui/src/lib/i18n.ts
@@ -171,6 +171,7 @@ const resources = {
cancel: 'Cancel',
savePreset: 'Save Preset',
applyPreset: 'Apply Preset',
+ deletePreset: 'Delete Preset',
},
componentModelSelector: {
selectModel: 'Select model',
@@ -212,6 +213,14 @@ const resources = {
recommended: 'Recommended',
allModelsCount: 'All Models ({{count}})',
noModelsAvailable: 'No models available',
+ shadowed: 'Shadowed',
+ prefixOnly: 'Prefix only',
+ current: 'Current',
+ currentValue: 'Current value',
+ preferredPinnedModel: 'Preferred pinned model:',
+ pinnedRouteStatus: 'Pinned route status:',
+ pinnedModelNotAdvertised:
+ 'Pinned model is not currently advertised by the proxy: {{model}}',
},
createAuthProfileDialog: {
title: 'Create New Account',
@@ -849,6 +858,8 @@ const resources = {
},
settingsTabs: {
web: 'Web',
+ image: 'Image',
+ channels: 'Channels',
env: 'Env',
think: 'Think',
proxy: 'Proxy',
@@ -1426,6 +1437,886 @@ const resources = {
retryContent: 'Retry content',
noMarkdown: 'No markdown content available.',
},
+
+ // ========================================
+ // Domain 1: Navigation / Layout / Shared
+ // ========================================
+ heroSection: {
+ title: 'CCS Config',
+ subtitle: 'Claude Code Switch Dashboard',
+ },
+ hubFooter: {
+ logs: 'Logs',
+ settings: 'Settings',
+ github: 'GitHub',
+ copyright: '© {{year}} kaitranntt',
+ },
+ themeToggle: {
+ srLabel: 'Toggle theme',
+ },
+ ccsLogo: {
+ alt: 'CCS Logo',
+ text: 'CCS Config',
+ },
+ claudekitBadge: {
+ title: 'Powered by ClaudeKit Framework',
+ alt: 'ClaudeKit',
+ poweredBy: 'Powered by',
+ claudekit: 'ClaudeKit',
+ },
+ codeEditor: {
+ revealSensitive: 'Reveal sensitive values',
+ maskSensitive: 'Mask sensitive values',
+ valid: 'Valid {{language}}',
+ readOnly: '(Read-only)',
+ },
+ commandBuilder: {
+ title: 'Command Builder',
+ searchPlaceholder: 'Type or select a command...',
+ copy: 'Copy',
+ run: 'Run',
+ cmdConfig: 'Open configuration interface',
+ cmdCreateProfile: 'Create a new profile',
+ cmdSwitchProfile: 'Switch to a profile',
+ cmdDoctor: 'Check system health',
+ cmdListProviders: 'List available CLIProxy providers',
+ cmdAddProvider: 'Add CLIProxy provider',
+ },
+ confirmDialog: {
+ confirm: 'Confirm',
+ cancel: 'Cancel',
+ },
+ connectionIndicator: {
+ connected: 'Connected',
+ connecting: 'Connecting...',
+ disconnected: 'Disconnected',
+ reconnecting: 'Reconnecting...',
+ },
+ docsLink: {
+ title: 'View documentation',
+ },
+ githubLink: {
+ title: 'Report an issue on GitHub',
+ },
+ globalEnvIndicator: {
+ injectedCount_one: '{{count}} global env var will be injected at runtime',
+ injectedCount_other: '{{count}} global env vars will be injected at runtime',
+ overriddenCount: '({{count}} overridden by profile)',
+ skippedLabel: 'Skipped (profile already defines):',
+ configureInSettings: 'Configure in Settings',
+ },
+ localhostDisclaimer: {
+ remoteReadonlyAuthDisabledLong:
+ 'Remote dashboard access is read-only because dashboard auth is currently disabled on the host. Re-enable dashboard auth on the host to unlock remote changes.',
+ remoteReadonlyAuthDisabledShort:
+ 'Remote dashboard is read-only until dashboard auth is re-enabled on the host.',
+ remoteReadonlySetupLong:
+ 'Remote dashboard access is read-only until you run ccs config auth setup on the host.',
+ remoteReadonlySetupShort: 'Remote dashboard is read-only until host auth is configured.',
+ localLong: 'This dashboard runs locally. All data stays on your machine.',
+ localShort: 'Local dashboard - data stays on your device.',
+ dismiss: 'Dismiss disclaimer',
+ },
+ privacyToggle: {
+ modeOn: 'Privacy mode ON - Click to show data',
+ modeOff: 'Privacy mode OFF - Click to hide data',
+ },
+ projectSelectionDialog: {
+ title: 'Select Google Cloud Project',
+ description: 'Choose which project to use for {{provider}} authentication.',
+ autoSelectCountdown: '(Auto-selecting default in {{count}}s)',
+ default: 'Default',
+ allProjects: 'All Projects',
+ allProjectsDescription: 'Onboard all {{count}} listed projects',
+ useDefault: 'Use Default',
+ selecting: 'Selecting...',
+ confirmSelection: 'Confirm Selection',
+ codeCopied: 'Code copied',
+ copyVerificationCode: 'Copy verification code',
+ },
+ quickCommands: {
+ title: 'Quick Commands',
+ startDefault: 'Start Default',
+ startDefaultDesc: 'Launch Claude with default profile',
+ glmProfile: 'GLM Profile',
+ glmProfileDesc: 'Switch to GLM model',
+ healthCheck: 'Health Check',
+ healthCheckDesc: 'Run system diagnostics',
+ delegateTask: 'Delegate Task',
+ delegateTaskDesc: 'Delegate to GLM profile',
+ },
+ quotaTooltip: {
+ loadingQuota: 'Loading quota...',
+ failedLoadQuota: 'Failed to load quota',
+ modelQuotas: 'Model Quotas:',
+ rateLimits: 'Rate Limits:',
+ plan: 'Plan: {{plan}}',
+ quotaSnapshots: 'Quota Snapshots:',
+ unlimited: 'Unlimited',
+ remaining: '{{remaining}}/{{entitlement}} remaining',
+ tier: 'Tier',
+ tierId: 'Tier ID',
+ state: 'State',
+ credits: 'Credits',
+ modelQuotasLower: 'Model quotas:',
+ allBucketsReport: 'All buckets report {{tokenType}}',
+ requestsRemaining: '{{count}} requests remaining',
+ inputTokensRemaining: '{{count}} input tokens remaining',
+ outputTokensRemaining: '{{count}} output tokens remaining',
+ amountRemaining: '{{count}} remaining',
+ fiveHourLimit: '5h usage limit',
+ weeklyLimit: 'Weekly usage limit',
+ weeklyOpus: 'Weekly usage (Opus)',
+ weeklySonnet: 'Weekly usage (Sonnet)',
+ weeklyOAuthApps: 'Weekly usage (OAuth apps)',
+ weeklyCowork: 'Weekly usage (Cowork)',
+ extraUsage: 'Extra usage',
+ premiumInteractions: 'Premium Interactions',
+ chat: 'Chat',
+ completions: 'Completions',
+ resets: 'Resets {{time}}',
+ fiveHourResets: '5h resets {{time}}',
+ weeklyResets: 'Weekly resets {{time}}',
+ },
+ sponsorButton: {
+ title: 'Sponsor this project on GitHub',
+ sponsor: 'Sponsor',
+ },
+ valueMetrics: {
+ apiCostSaved: 'API Cost Saved',
+ tokensSaved: 'Tokens Saved',
+ queriesFaster: 'Queries Faster',
+ errorsReduced: 'Errors Reduced',
+ vsLastMonth: 'vs last month',
+ throughCaching: 'through caching',
+ averageSpeedup: 'average speedup',
+ withRetryLogic: 'with retry logic',
+ performanceMetrics: 'Performance Metrics',
+ monthlySummary: 'Monthly Summary',
+ totalSaved: 'Total Saved',
+ tokensProcessed: 'Tokens Processed',
+ queriesHandled: 'Queries Handled',
+ uptime: 'Uptime',
+ },
+ updatesSpotlight: {
+ openUpdatesCenter: 'Open Updates Center',
+ },
+ deviceCodeDialog: {
+ authorize: 'Authorize {{provider}}',
+ enterCodeAtPage: 'Enter the code below at the authorization page.',
+ expiresIn: '(Expires in {{time}})',
+ codeExpired: '(Code expired)',
+ copied: 'Copied!',
+ copyCode: 'Copy Code',
+ waitingForAuth: 'Waiting for authorization...',
+ openVerificationPage: 'Open verification page',
+ openProviderPage: 'Open {{provider}}',
+ copyCodeAria: 'Copy verification code',
+ codeCopiedAria: 'Code copied',
+ },
+ settingsDialog: {
+ editProfile: 'Edit Profile: {{name}}',
+ description: 'Configure environment variables and settings for this profile.',
+ loadingSettings: 'Loading settings...',
+ envTab: 'Environment',
+ rawJsonTab: 'Raw JSON',
+ generalTab: 'General',
+ noEnvVars: 'No environment variables configured.',
+ noEnvVarsHint: 'Add variables in your settings.json file.',
+ loadingEditor: 'Loading editor...',
+ profileInfo: 'Profile Information',
+ profileInfoDesc: 'Details about this configuration file.',
+ path: 'Path',
+ lastModified: 'Last Modified',
+ cancel: 'Cancel',
+ saving: 'Saving...',
+ saveChanges: 'Save Changes',
+ conflictTitle: 'File Modified Externally',
+ conflictDesc:
+ 'This settings file was modified by another process. Overwrite with your changes or discard?',
+ overwrite: 'Overwrite',
+ },
+
+ // ========================================
+ // Domain 2: Accounts / Auth
+ // ========================================
+ setupWizard: {
+ title: 'Quick Setup Wizard',
+ stepProviderDesc: 'Select a provider to get started',
+ stepAuthDesc: 'Authenticate with your provider',
+ stepAccountDesc: 'Select which account to use',
+ stepVariantDesc: 'Create your custom variant',
+ stepSuccessDesc: 'Setup complete!',
+ authStep: {
+ authenticateWith: 'Authenticate with {{provider}} to add an account',
+ authenticating: 'Authenticating...',
+ authenticateInBrowser: 'Authenticate in Browser',
+ completeOAuth: 'Complete the OAuth flow in your browser...',
+ orUseTerminal: 'Or use terminal',
+ runCommandHint: 'Run this command in your terminal:',
+ back: 'Back',
+ checking: 'Checking...',
+ refreshStatus: 'Refresh Status',
+ },
+ accountStep: {
+ selectAccount: 'Select an account ({{count}})',
+ defaultAccount: 'Default account',
+ or: 'Or',
+ addNewAccount: 'Add new account',
+ addNewAccountDesc: 'Authenticate with a different account',
+ back: 'Back',
+ },
+ variantStep: {
+ back: 'Back',
+ skip: 'Skip',
+ },
+ successStep: {
+ title: 'Variant Created!',
+ subtitle: 'Your custom variant is ready to use',
+ usage: 'Usage:',
+ done: 'Done',
+ },
+ },
+ accountSurfaceCard: {
+ business: 'Biz',
+ personal: 'Pers',
+ variant: 'Variant',
+ },
+ accountCardStats: {
+ notUsedYet: 'Not used yet',
+ },
+ accountQuotaPanel: {
+ weekly: 'Weekly',
+ loadingQuota: 'Loading quota...',
+ },
+ userMenu: {
+ signedInAs: 'Signed in as {{username}}',
+ },
+ authMonitorLive: {
+ live: 'LIVE',
+ accountMonitor: 'Account Monitor',
+ updated: 'Updated {{time}}',
+ updatedNow: 'Updated now',
+ requestsLabel: 'req',
+ stats: 'Stats',
+ successRate: 'Success Rate',
+ missingProjectId: 'Missing Project ID',
+ noActivity: 'no activity',
+ },
+ providerCard: {
+ missingProjectIdAria: 'Missing Project ID',
+ },
+ loginPage: {
+ showPassword: 'Show password',
+ hidePassword: 'Hide password',
+ },
+
+ // ========================================
+ // Domain 3: CLIProxy / Provider Editor
+ // ========================================
+ cliproxyStatsOverview: {
+ sessionStatistics: 'Session Statistics',
+ realTimeMetrics: 'Real-time usage metrics from {{backend}}',
+ offline: 'Offline',
+ running: 'Running',
+ noActiveSession: 'No Active Session',
+ noActiveSessionHint:
+ 'Start a CLIProxy session using ccs gemini, ccs codex, or ccs agy to view real-time statistics.',
+ failedLoadStats: 'Failed to Load Statistics',
+ totalRequests: 'Total Requests',
+ successCount: '{{count}} success',
+ successRate: 'Success Rate',
+ totalTokens: 'Total Tokens',
+ estimatedCost: '~${{cost}} estimated',
+ modelsUsed: 'Models Used',
+ modelUsageDistribution: 'Model Usage Distribution',
+ requestCount: '{{count}} requests',
+ },
+ cliproxyTable: {
+ name: 'Name',
+ provider: 'Provider',
+ model: 'Model',
+ account: 'Account',
+ status: 'Status',
+ default: 'Default',
+ actions: 'Actions',
+ },
+ cliproxyTabs: {
+ overview: 'Overview',
+ variants: 'Variants',
+ aiProviders: 'AI Providers',
+ controlPanel: 'Control Panel',
+ },
+ cliproxyHeader: {
+ ccsLevelAccountManagement: 'CCS-level account management',
+ cliproxyNotAvailable: 'CLIProxy Not Available',
+ cliproxyControlPanel: 'CLIProxy Control Panel',
+ noVariants: 'No CLIProxy variants found.',
+ addAccountToStart: 'Add an account to get started',
+ },
+ routingGuidance: {
+ roundRobin: 'Round robin spreads usage.',
+ fillFirst: 'Fill first keeps backup accounts cold until they are needed.',
+ routingStrategy: 'Routing strategy',
+ optionalRouting: 'Optional routing',
+ },
+ extendedContext: {
+ extendedContext: 'Extended Context',
+ },
+ cliproxyConfig: {
+ unsavedChanges: 'Unsaved changes',
+ original: 'Original',
+ modified: 'Modified',
+ reviewChanges: 'Review Changes',
+ loadingEditor: 'Loading editor...',
+ },
+ providerEditor: {
+ provider: 'Provider',
+ filePath: 'File Path',
+ lastModified: 'Last Modified',
+ defaultTarget: 'Default Target',
+ quickUsage: 'Quick Usage',
+ modelMapping: 'Model Mapping',
+ status: 'Status',
+ loadingSettings: 'Loading settings...',
+ loadingEditor: 'Loading editor...',
+ noAccountsConnected: 'No accounts connected',
+ addAccountToStart: 'Add an account to get started',
+ gcpProjectIdReadonly: 'GCP Project ID (read-only)',
+ projectIdNA: 'Project ID: N/A',
+ missingProjectId: 'Missing Project ID',
+ missingProjectIdHint:
+ 'This may cause errors. Remove the account and re-add it to fetch the project ID.',
+ useIncognito: 'Use incognito',
+ aliases: 'Aliases',
+ current: 'Current',
+ currentValue: 'Current value',
+ composite: 'composite',
+ defaultLabel: 'default',
+ requiredSetup: 'Required setup',
+ connectorName: 'Connector Name',
+ proxyUrl: 'Proxy URL',
+ proxyUrlSet: 'Proxy URL set',
+ excludedModels: 'Excluded Models',
+ headers: 'Headers',
+ secret: 'Secret',
+ prefix: 'Prefix',
+ modelMappings: 'Model Mappings',
+ baseUri: 'Base URL',
+ apiKeys: 'API Keys',
+ presets: 'Apply pre-configured model mappings',
+ createVariant: 'Create CLIProxy Variant',
+ agyDenylist: 'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.',
+ },
+ providerEditorAccountItem: {
+ modelsUsed: 'Models Used',
+ },
+ bulkActionBar: {
+ applyPreset: 'Apply preset',
+ },
+ modelConfigSection: {
+ defaultModel: 'Default Model',
+ },
+ rawEditorSection: {
+ rawConfig: 'Raw Configuration',
+ },
+ providerEditorHeader: {
+ connectorName: 'Connector Name',
+ },
+ aiProvidersFamilyRail: {
+ current: 'Current',
+ },
+ aiProvidersEntryCard: {
+ apiKeys: 'API Keys',
+ },
+ aiProvidersEntryDialog: {
+ connectorName: 'Connector Name',
+ baseUri: 'Base URL',
+ proxyUrl: 'Proxy URL',
+ secret: 'Secret',
+ prefix: 'Prefix',
+ excludedModels: 'Excluded Models',
+ headers: 'Headers',
+ modelMappings: 'Model Mappings',
+ requiredSetup: 'Required setup',
+ optionalRouting: 'Optional routing',
+ },
+
+ // ========================================
+ // Domain 4: Compatible CLI Tabs
+ // ========================================
+ codex: {
+ controlCenter: 'Control Center',
+ overview: 'Overview',
+ docs: 'Docs',
+ nativeCodexRuntime: 'Native Codex Runtime',
+ ccsCodexProvider: 'CCS Codex provider / bridge',
+ codexDocs: 'Codex docs',
+ supportedFlows: 'Supported flows',
+ twoSupportedPaths: 'Two supported paths:',
+ nativeLabel: 'Native:',
+ nativeDesc: 'Codex is a first-class, runtime-only target in CCS v1.',
+ ccsBridge: 'CCS Bridge',
+ apiProfilesDefault: 'API profiles continue to default to Claude or Droid.',
+ recommendedSetupFlow: 'Recommended setup flow',
+ fastestPath: 'Fastest path',
+ officialChannels: 'Official Channels',
+ codexCli: 'Codex CLI',
+ openNativeCodex: 'Open native Codex',
+ runBuiltInCodex: 'Run built-in Codex on Codex',
+ runBuiltInCodexExplicit: 'Run built-in Codex on Codex (explicit)',
+ openCodexDashboard: 'Open Codex dashboard',
+ status: 'Status',
+ profiles: 'Profiles',
+ createNewProfile: 'Create new profile',
+ createNewProvider: 'Create new provider',
+ createNewMcpServer: 'Create new MCP server',
+ defaultProvider: 'Default provider',
+ useDefault: 'Use default',
+ useGlobalProvider: 'Use global provider',
+ useProviderDefault: 'Use provider default',
+ quickFillWarning: 'Quick-fill only. Review before saving.',
+ thisFileUpstreamOwned: 'This file is upstream-owned by Codex CLI.',
+ notes: 'Notes',
+ approvalPolicy: 'Approval policy',
+ sandboxMode: 'Sandbox mode',
+ reasoningEffort: 'Reasoning effort',
+ useGlobalEffort: 'Use global effort',
+ reasoningEffortCapitalized: 'Reasoning Effort',
+ thinkingBudgetTokens: 'Thinking Budget Tokens',
+ modelContextWindow: 'Model context window',
+ autoCompactTokenLimit: 'Auto-compact token limit',
+ toolOutputTokenLimit: 'Tool output token limit',
+ webSearch: 'Web search',
+ personality: 'Personality',
+ model: 'Model',
+ rawOnly: 'Raw only',
+ trusted: 'trusted',
+ untrusted: 'untrusted',
+ noProjectTrustEntries: 'No explicit project trust entries saved.',
+ codexNativeRecipe: 'Saved native Codex recipe',
+ gptContextCap: 'GPT-5.4 context cap',
+ usageLimitCost: 'Usage-limit cost above 272K',
+ longContextOverride: 'Long context override',
+ counts2x: 'Counts 2x',
+ normalUsageWindow: 'Normal usage window',
+ useCodexDefault: 'Use Codex default',
+ stdio: 'stdio',
+ streamableHttp: 'streamable-http',
+ responses: 'responses',
+ defaultTargetCli: 'Default Target CLI',
+ executionChain: 'Execution chain',
+ targetPath: 'Current target path',
+ userConfig: 'User config',
+ configYaml: 'config.yaml',
+ flow: 'Flow',
+ docsTab: 'Docs',
+ },
+ droidSettings: {
+ quickControls: 'Quick Controls',
+ reasoningControls: 'Reasoning Controls',
+ thinkingBudget: 'Thinking Budget',
+ anthropicOnly: 'Anthropic models only',
+ byokCustomModels: 'BYOK Custom Models',
+ },
+ rawJsonSettingsEditor: {
+ title: 'Raw Settings Editor',
+ },
+ copilotConfigForm: {
+ copilotConfiguration: 'Copilot Configuration',
+ deprecatedModels: 'Deprecated Copilot models detected',
+ failedLoadStatus: 'Failed to load status',
+ useWithClaudeCode: 'Use your GitHub Copilot subscription with Claude Code',
+ githubCopilotControls: 'GitHub Copilot controls prompt/context limits upstream.',
+ provider: 'Provider',
+ filePath: 'File Path',
+ status: 'Status',
+ enabled: 'Enabled',
+ disabled: 'Disabled',
+ loadingEditor: 'Loading editor...',
+ modelMapping: 'Model Mapping',
+ quickUsage: 'Quick Usage',
+ noPremiumUsage: 'No premium usage count',
+ },
+ copilotPresets: {
+ gpt5Codex: 'GPT-5.3 Codex',
+ claude46: 'Claude 4.6',
+ gemini3: 'Gemini 3',
+ },
+
+ // ========================================
+ // Domain 5: Logs / Monitoring / Health / Analytics
+ // ========================================
+ healthCard: {
+ allSystemsNominal: 'All Systems Nominal',
+ machineChecks: 'Machine checks',
+ },
+ analyticsCards: {
+ cacheCost: 'Cache Cost',
+ hitRate: 'Hit Rate',
+ inputOutputRatio: 'Input/Output Ratio',
+ noCacheData: 'No cache data available',
+ noModelData: 'No model data available',
+ noSessionData: 'No session data available',
+ noTokenData: 'No token data available',
+ totalCost: 'Total Cost',
+ totalTokens: 'Total Tokens',
+ usageInsights: 'Usage Insights',
+ },
+ dateRangeFilter: {
+ pickADate: 'Pick a date',
+ },
+ logsConfig: {
+ level: 'Lvl',
+ message: 'Message',
+ source: 'Source',
+ time: 'Time',
+ proc: 'Proc',
+ open: 'Open',
+ run: 'Run',
+ refreshEntries: 'Refresh Entries',
+ },
+ logsDetailPanel: {
+ details: 'Details',
+ },
+ logsFilters: {
+ filters: 'Filters',
+ },
+ logsOverviewCards: {
+ overview: 'Overview',
+ },
+ logsPageSkeleton: {
+ loadingLogs: 'Loading logs...',
+ },
+ monitoringErrorLogs: {
+ logContent: 'Log Content',
+ },
+ analyticsPages: {
+ chartsGrid: 'Charts',
+ costByModel: 'Cost by Model',
+ },
+
+ // ========================================
+ // Domain 6: Hooks / Toasts / Error Transport
+ // ========================================
+ toasts: {
+ profileCreated: 'Profile created successfully',
+ profileUpdated: 'Profile updated successfully',
+ profileDeleted: 'Profile deleted successfully',
+ orphanProfilesComplete: 'Orphan profiles registration complete',
+ profileCopied: 'Profile copied successfully',
+ profileImported: 'Profile imported successfully',
+ authRequired: '{{provider}} authorization required',
+ authSuccess: '{{provider}} authentication successful!',
+ authFailed: '{{provider}} authentication failed',
+ deviceCodeExpired: 'Device code expired. Please try again.',
+ codeCopied: 'Code copied to clipboard',
+ failedCopy: 'Failed to copy code',
+ configSaved: 'Configuration saved successfully',
+ configSaveFailed: 'Failed to save: {{error}}',
+ invalidYaml: 'Cannot save invalid YAML',
+ configUpdatedExternally: 'Configuration updated externally',
+ settingsFileUpdated: 'Settings file updated',
+ accountsUpdated: 'Accounts updated',
+ noProfilesToSync: 'No profiles to sync',
+ syncFailed: 'Sync failed: {{error}}',
+ providerAuthSuccess: '{{provider}} authentication successful',
+ providerDeviceCodeInCallback: 'Provider returned Device Code flow in callback mode',
+ loggingConfigSaved: 'Logging configuration saved.',
+ loggingConfigSaveFailed: 'Failed to save logging configuration.',
+ unifiedConfigUpdated: 'Configuration updated successfully',
+ migrationPreviewComplete: 'Migration preview completed',
+ migrationComplete: 'Migration completed successfully',
+ migrationFailed: 'Migration failed',
+ rollbackComplete: 'Rollback completed successfully',
+ rollbackFailed: 'Rollback failed',
+ defaultAccountSet: 'Default account set to "{{name}}"',
+ defaultAccountReset: 'Default account reset to CCS',
+ accountDeleted: 'Account "{{name}}" deleted',
+ contextUpdated: 'Updated "{{name}}" context to {{summary}}',
+ legacyConfirmError:
+ 'Account "{{name}}" needs explicit confirmation. Use Edit History Sync on this account.',
+ legacyConfirmFailed: 'Legacy account "{{name}}" failed confirmation: {{error}}',
+ legacyConfirmSuccess_one: '{{count}} legacy account confirmed',
+ legacyConfirmSuccess_other: '{{count}} legacy accounts confirmed',
+ noLegacyAccounts: 'No legacy accounts need confirmation',
+ routingStrategySet: 'Routing strategy set to {{strategy}}',
+ variantCreated: 'Variant created successfully',
+ variantUpdated: 'Variant updated successfully',
+ variantDeleted: 'Variant deleted successfully',
+ defaultAccountUpdated: 'Default account updated',
+ accountRemoved: 'Account removed',
+ accountPaused: 'Account paused',
+ accountResumed: 'Account resumed',
+ accountAdded: 'Account added for {{provider}}',
+ kiroImported: 'Imported Kiro account: {{name}}',
+ kiroTokenImported: 'Kiro token imported',
+ modelUpdated: 'Model updated',
+ presetSaved: 'Preset "{{name}}" saved',
+ presetDeleted: 'Preset deleted',
+ cliproxyAlreadyRunning: 'CLIProxy was already running',
+ cliproxyStarted: 'CLIProxy started successfully',
+ cliproxyStartFailed: 'Failed to start CLIProxy',
+ cliproxyStopped: 'CLIProxy stopped',
+ cliproxyStopFailed: 'Failed to stop CLIProxy',
+ presetApplied: 'Applied "{{name}}" preset',
+ presetAppliedCustom: 'Applied custom preset',
+ settingsSavedWithAdjustments: 'Settings saved with model adjustments',
+ settingsSaved: 'Settings saved',
+ failedSaveSettings: 'Failed to save settings',
+ codexRefreshFailed: 'Failed to refresh Codex snapshot. Raw edits were kept.',
+ codexRefreshError: 'Failed to refresh Codex snapshot.',
+ codexFixToml: 'Fix TOML before saving.',
+ codexSaved: 'Saved Codex config.toml.',
+ codexChangedExternally: 'config.toml changed externally. Refresh and retry.',
+ codexSaveFailed: 'Failed to save Codex config.toml.',
+ codexUpdateFailed: 'Failed to update Codex config.',
+ noOrphanProfiles: 'No orphan profile settings found',
+ profilesRegistered: 'Registered {{count}} profile(s){{skipped}}',
+ destinationEmpty: 'Destination profile name cannot be empty',
+ profileExportDownloaded: 'Profile export downloaded',
+ profileImportFailed: 'Failed to import profile bundle',
+ },
+
+ // ========================================
+ // Domain 7: Profiles / Settings / Pages
+ // ========================================
+ profileEditorSections: {
+ imageAnalysis: 'Image Analysis',
+ loadingImageSettings: 'Loading image settings...',
+ skipPermissionPrompts: 'Skip permission prompts on launch',
+ useNativeImageReading: 'Use native image reading',
+ skipTransformer: 'Skip transformer',
+ friendlyUi: 'Friendly UI',
+ info: 'Info',
+ },
+ imageAnalysisStatus: {
+ sectionTitle: 'Image',
+ openSettings: 'Open Settings',
+ useNativeImageReading: 'Use native image reading',
+ refreshingPreview: 'Refreshing preview',
+ savedStatus: 'Saved status',
+ livePreview: 'Live preview',
+ disabledGlobally: 'Disabled globally',
+ targetBypassesHook: '{{target}} bypasses the hook',
+ nativeImageReading: 'Native image reading',
+ setupNeeded: 'Setup needed',
+ needsAuth: 'Needs auth',
+ needsProxy: 'Needs proxy',
+ nativeFallback: 'Native fallback',
+ transformerReady: 'Transformer ready',
+ badgeDisabled: 'Disabled',
+ badgeBypassed: 'Bypassed',
+ badgeNative: 'Native',
+ badgeSetup: 'Setup',
+ badgeAuth: 'Auth',
+ badgeProxy: 'Proxy',
+ badgeReady: 'Ready',
+ capabilityVerified: 'Verified',
+ capabilityUnknown: 'Unknown',
+ toggleSummaryNativeCapable:
+ '{{model}} looks image-ready. CCS will bypass the transformer here.',
+ toggleSummaryNativeModel: 'CCS will prefer native reading for {{model}}.',
+ toggleSummaryNativeDefault: 'CCS will prefer native image reading for this profile.',
+ toggleSummaryNativeFileAccess: 'This profile currently stays on native file access.',
+ toggleSummaryInactiveTarget:
+ 'Saved Claude-side image routing is inactive while {{target}} is selected.',
+ toggleSummaryTransformerRoute: 'Transformer route: {{backend}}{{modelSuffix}}.',
+ noteDisabledGlobally: 'Image is disabled globally in CCS settings.',
+ noteTargetBypassesHook: 'Current target {{target}} bypasses the Claude Read hook.',
+ notePersistHook: 'Persist the profile hook before transformer routing can run here.',
+ targetLabel: {
+ claude: 'Claude Code',
+ droid: 'Factory Droid',
+ codex: 'Codex CLI',
+ },
+ },
+ openrouterBadge: {
+ new: 'NEW',
+ integration: 'OpenRouter Integration',
+ },
+ openrouterBanner: {
+ accessModels: 'Access {{count}}+ models via OpenRouter',
+ add: 'Add',
+ },
+ openrouterModelPicker: {
+ searchModels: 'Search Models',
+ newestModels: 'Newest Models',
+ },
+ openrouterPromoCard: {
+ title: 'OpenRouter',
+ description: 'Access hundreds of models from one API endpoint.',
+ },
+ profileCard: {
+ profile: 'Profile',
+ openRouter: 'OpenRouter profile',
+ claudeCode: 'Claude Code',
+ claudeCodeDefault: 'Claude Code (default)',
+ factoryDroid: 'Factory Droid',
+ codexCli: 'Codex CLI',
+ ccsProfile: 'CCS profile',
+ },
+ profileDeck: {
+ profiles: 'Profiles',
+ failedToLoad: 'Failed to load profiles: {{message}}',
+ noProfiles: 'No profiles configured. Create your first profile to get started.',
+ },
+ profilesTable: {
+ name: 'Name',
+ provider: 'Provider',
+ model: 'Model',
+ target: 'Target',
+ lastModified: 'Last Modified',
+ actions: 'Actions',
+ edit: 'Edit',
+ },
+ profileCreateDialog: {
+ createProfile: 'Create Profile',
+ appliedModelToTiers: 'Applied "{{model}}" to all model tiers',
+ profileCreated: 'Profile "{{name}}" created',
+ failedCreate: 'Failed to create profile',
+ chooseProviderHint: 'Choose a provider preset or configure a custom API endpoint.',
+ basicInformation: 'Basic Information',
+ modelConfiguration: 'Model Configuration',
+ usedInCli: 'Used in CLI:',
+ apiBaseUrl: 'API Base URL',
+ baseUrlPlaceholder: 'https://api.example.com/v1',
+ prefilledFromPreset: 'Pre-filled from {{name}}. You can customize if needed.',
+ optionalForPreset: 'Optional for {{name}}. Leave blank to use native Anthropic auth.',
+ endpointHint: 'The endpoint that accepts OpenAI-compatible and Anthropic requests',
+ optional: '(optional)',
+ apiKeyOptionalPlaceholder: 'Optional - only if auth is enabled',
+ apiKeyPlaceholder: 'sk-...',
+ apiKeyOptionalHint: 'Only needed if your local endpoint has authentication enabled',
+ defaultTargetCli: 'Default Target CLI',
+ modelMapping: 'Model Mapping',
+ modelMappingDesc:
+ 'Map Claude Code tiers (Opus/Sonnet/Haiku) to models supported by your provider.',
+ searchModelsPlaceholder: 'Type to search (e.g., opus, sonnet, gpt-4o)...',
+ noModelsFound: 'No models found for "{{query}}"',
+ loadingModels: 'Loading models...',
+ defaultModel: 'Default Model',
+ sonnetMapping: 'Sonnet Mapping',
+ opusMapping: 'Opus Mapping',
+ haikuMapping: 'Haiku Mapping',
+ sonnetMappingPlaceholder: 'e.g. gpt-4o, claude-sonnet-4',
+ opusMappingPlaceholder: 'e.g. o1, claude-opus-4.5',
+ haikuMappingPlaceholder: 'e.g. gpt-4o-mini, claude-3.5-haiku',
+ free: 'Free',
+ },
+ profileDialogLegacy: {
+ editProfile: 'Edit Profile',
+ },
+ supportEntryCard: {
+ actionRequired: 'Action Required',
+ },
+ settingsPage: {
+ title: 'Settings',
+ loading: 'Loading...',
+ failedLoad: 'Failed to load settings.',
+ tabs: {
+ web: 'Web',
+ env: 'Env',
+ think: 'Think',
+ proxy: 'Proxy',
+ auth: 'Auth',
+ backup: 'Backup',
+ channels: 'Channels',
+ imageAnalysis: 'Image',
+ },
+ websearchSection: {
+ title: 'Web Search',
+ description: 'CLI-based web search configuration.',
+ },
+ thinkingSection: {
+ title: 'Thinking',
+ description: 'Configure extended thinking/reasoning for supported models.',
+ directOverride: 'Direct override',
+ youType: 'You type:',
+ ccsAdds: 'CCS adds:',
+ executionChain: 'Execution chain',
+ primaryBackends: 'Primary backends',
+ legacyCliFallbacks: 'Legacy CLI fallbacks',
+ managedPayload: 'Managed payload',
+ sharedTargetMetadata: 'Shared target metadata',
+ ideTargetMetadata: 'IDE target metadata',
+ ideSettingsPath: 'IDE settings path',
+ ideHost: 'IDE host',
+ resolvedBinding: 'Resolved binding',
+ bindingName: 'Binding name',
+ inSync: 'In sync',
+ currentTargetPath: 'Current target path',
+ warnings: 'Warnings',
+ notes: 'Notes',
+ workspacePresets: 'Workspace presets',
+ draft: 'Draft',
+ advanced: 'Advanced',
+ recommended: 'Recommended setup flow',
+ configureModelFirst: 'Configure a model first',
+ },
+ proxySection: {
+ title: 'Proxy',
+ loadingImageSettings: 'Loading image settings...',
+ },
+ channelsSection: {
+ title: 'Official Channels',
+ description: 'View and manage official release channels.',
+ },
+ imageAnalysisSection: {
+ title: 'Image Analysis',
+ description: 'Configure image analysis settings.',
+ loading: 'Loading image settings...',
+ },
+ },
+ codexPage: {
+ title: 'Codex',
+ controlCenter: 'Control Center',
+ overview: 'Overview',
+ docs: 'Docs',
+ nativeRuntime: 'Native Runtime',
+ ccsProvider: 'CCS Provider',
+ setup: 'Setup',
+ },
+ apiPage: {
+ title: 'API Profiles',
+ subtitle: 'Manage your API profiles and endpoints.',
+ },
+ claudeExtensionPage: {
+ title: 'Claude Extension',
+ subtitle: 'Claude Extension integration settings.',
+ claudeAuth: 'Claude auth',
+ status: 'Status',
+ targetMetadata: 'IDE target metadata',
+ },
+ sharedPageV2: {
+ title: 'Shared',
+ subtitle: 'Shared data management.',
+ },
+ homePageV2: {
+ title: 'Home',
+ logsMoved: 'Logs moved to a dedicated workspace',
+ profiles: 'Profiles',
+ cliproxy: 'CLIProxy',
+ accounts: 'Accounts',
+ health: 'Health',
+ },
+ analyticsPageV2: {
+ title: 'Analytics',
+ subtitle: 'Usage analytics and insights.',
+ },
+ logsPageV2: {
+ title: 'Logs',
+ subtitle: 'View and manage system logs.',
+ },
+ healthPageV2: {
+ title: 'Health',
+ subtitle: 'System health monitoring.',
+ },
+ aiProvidersPage: {
+ title: 'AI Providers',
+ subtitle: 'Manage AI provider configurations.',
+ unableToLoad: 'Unable to load AI Providers',
+ },
},
},
'zh-CN': {
@@ -1458,6 +2349,7 @@ const resources = {
factoryDroid: 'Factory Droid',
system: '系统',
health: '健康',
+ logs: '日志',
settings: '设置',
openrouterTooltip: '精选:OpenRouter + Alibaba Coding Plan + Ollama',
},
@@ -1587,6 +2479,7 @@ const resources = {
cancel: '取消',
savePreset: '保存预设',
applyPreset: '应用预设',
+ deletePreset: '删除预设',
},
componentModelSelector: {
selectModel: '选择模型',
@@ -1628,6 +2521,13 @@ const resources = {
recommended: '推荐',
allModelsCount: '全部模型({{count}})',
noModelsAvailable: '暂无可用模型',
+ shadowed: '已遮蔽',
+ prefixOnly: '仅前缀',
+ current: '当前',
+ currentValue: '当前值',
+ preferredPinnedModel: '偏好的固定模型:',
+ pinnedRouteStatus: '固定路由状态:',
+ pinnedModelNotAdvertised: '固定模型当前未被代理广播:{{model}}',
},
createAuthProfileDialog: {
title: '创建新账号',
@@ -2213,6 +3113,8 @@ const resources = {
},
settingsTabs: {
web: '网页',
+ image: '图片',
+ channels: '频道',
env: '环境',
think: '思考',
proxy: '代理',
@@ -2775,6 +3677,850 @@ const resources = {
retryContent: '重试内容加载',
noMarkdown: '暂无 Markdown 内容。',
},
+ heroSection: {
+ title: 'CCS Config',
+ subtitle: 'Claude Code Switch Dashboard',
+ },
+ hubFooter: {
+ logs: '日志',
+ settings: '设置',
+ github: 'GitHub',
+ copyright: '© {{year}} kaitranntt',
+ },
+ themeToggle: {
+ srLabel: '切换主题',
+ },
+ ccsLogo: {
+ alt: 'CCS Logo',
+ text: 'CCS Config',
+ },
+ claudekitBadge: {
+ title: 'Powered by ClaudeKit Framework',
+ alt: 'ClaudeKit',
+ poweredBy: 'Powered by',
+ claudekit: 'ClaudeKit',
+ },
+ codeEditor: {
+ revealSensitive: '显示敏感值',
+ maskSensitive: '隐藏敏感值',
+ valid: '有效的 {{language}}',
+ readOnly: '(只读)',
+ },
+ commandBuilder: {
+ title: '命令构建器',
+ searchPlaceholder: '输入或选择命令...',
+ copy: '复制',
+ run: '运行',
+ cmdConfig: '打开配置界面',
+ cmdCreateProfile: '创建新配置',
+ cmdSwitchProfile: '切换到指定配置',
+ cmdDoctor: '检查系统健康',
+ cmdListProviders: '列出可用 CLIProxy 提供商',
+ cmdAddProvider: '添加 CLIProxy 提供商',
+ },
+ confirmDialog: {
+ confirm: '确认',
+ cancel: '取消',
+ },
+ connectionIndicator: {
+ connected: '已连接',
+ connecting: '连接中...',
+ disconnected: '已断开',
+ reconnecting: '重新连接中...',
+ },
+ docsLink: {
+ title: '查看文档',
+ },
+ githubLink: {
+ title: '在 GitHub 上报告问题',
+ },
+ globalEnvIndicator: {
+ injectedCount_one: '{{count}} 个全局环境变量将在运行时注入',
+ injectedCount_other: '{{count}} 个全局环境变量将在运行时注入',
+ overriddenCount: '({{count}} 个已被配置覆盖)',
+ skippedLabel: '已跳过(配置中已定义):',
+ configureInSettings: '在设置中配置',
+ },
+ localhostDisclaimer: {
+ remoteReadonlyAuthDisabledLong:
+ '远程控制台当前为只读,因为主机未启用控制台认证。请在主机上重新启用控制台认证以解锁远程编辑。',
+ remoteReadonlyAuthDisabledShort: '远程控制台为只读,直到主机重新启用控制台认证。',
+ remoteReadonlySetupLong: '远程控制台为只读,直到在主机上运行 ccs config auth setup。',
+ remoteReadonlySetupShort: '远程控制台为只读,直到完成主机认证配置。',
+ localLong: '本控制台在本地运行,所有数据保留在本机。',
+ localShort: '本地控制台 - 数据保留在本机。',
+ dismiss: '关闭提示',
+ },
+ privacyToggle: {
+ modeOn: '隐私模式已开启 - 点击显示数据',
+ modeOff: '隐私模式已关闭 - 点击隐藏数据',
+ },
+ projectSelectionDialog: {
+ title: '选择 Google Cloud 项目',
+ description: '选择用于 {{provider}} 认证的项目。',
+ autoSelectCountdown: '({{count}} 秒后自动选择默认值)',
+ default: '默认',
+ allProjects: '全部项目',
+ allProjectsDescription: '接入全部 {{count}} 个已列出的项目',
+ useDefault: '使用默认值',
+ selecting: '选择中...',
+ confirmSelection: '确认选择',
+ codeCopied: '验证码已复制',
+ copyVerificationCode: '复制验证码',
+ },
+ quickCommands: {
+ title: '快捷命令',
+ startDefault: '启动默认',
+ startDefaultDesc: '使用默认配置启动 Claude',
+ glmProfile: 'GLM 配置',
+ glmProfileDesc: '切换到 GLM 模型',
+ healthCheck: '健康检查',
+ healthCheckDesc: '运行系统诊断',
+ delegateTask: '委托任务',
+ delegateTaskDesc: '委托到 GLM 配置',
+ },
+ quotaTooltip: {
+ loadingQuota: '加载配额中...',
+ failedLoadQuota: '加载配额失败',
+ modelQuotas: '模型配额:',
+ rateLimits: '速率限制:',
+ plan: '计划:{{plan}}',
+ quotaSnapshots: '配额快照:',
+ unlimited: '无限制',
+ remaining: '剩余 {{remaining}}/{{entitlement}}',
+ tier: '档位',
+ tierId: '档位 ID',
+ state: '状态',
+ credits: '额度',
+ modelQuotasLower: '模型配额:',
+ allBucketsReport: '所有桶报告 {{tokenType}}',
+ requestsRemaining: '剩余 {{count}} 次请求',
+ inputTokensRemaining: '剩余 {{count}} 个输入 token',
+ outputTokensRemaining: '剩余 {{count}} 个输出 token',
+ amountRemaining: '剩余 {{count}}',
+ fiveHourLimit: '5 小时用量限制',
+ weeklyLimit: '每周用量限制',
+ weeklyOpus: '每周用量(Opus)',
+ weeklySonnet: '每周用量(Sonnet)',
+ weeklyOAuthApps: '每周用量(OAuth 应用)',
+ weeklyCowork: '每周用量(Cowork)',
+ extraUsage: '额外用量',
+ premiumInteractions: '高级交互次数',
+ chat: '对话',
+ completions: '补全',
+ resets: '重置于 {{time}}',
+ fiveHourResets: '5 小时重置于 {{time}}',
+ weeklyResets: '每周重置于 {{time}}',
+ },
+ sponsorButton: {
+ title: '在 GitHub 上赞助此项目',
+ sponsor: '赞助',
+ },
+ valueMetrics: {
+ apiCostSaved: 'API 成本节省',
+ tokensSaved: 'Token 节省',
+ queriesFaster: '查询加速',
+ errorsReduced: '错误减少',
+ vsLastMonth: '对比上月',
+ throughCaching: '通过缓存',
+ averageSpeedup: '平均加速',
+ withRetryLogic: '通过重试逻辑',
+ performanceMetrics: '性能指标',
+ monthlySummary: '月度概览',
+ totalSaved: '总节省',
+ tokensProcessed: '处理 Token 数',
+ queriesHandled: '处理请求数',
+ uptime: '正常运行时间',
+ },
+ updatesSpotlight: {
+ openUpdatesCenter: '打开更新中心',
+ },
+ deviceCodeDialog: {
+ authorize: '授权 {{provider}}',
+ enterCodeAtPage: '在授权页面输入下方验证码。',
+ expiresIn: '({{time}} 后过期)',
+ codeExpired: '(验证码已过期)',
+ copied: '已复制!',
+ copyCode: '复制验证码',
+ waitingForAuth: '等待授权中...',
+ openVerificationPage: '打开验证页面',
+ openProviderPage: '打开 {{provider}}',
+ copyCodeAria: '复制验证码',
+ codeCopiedAria: '验证码已复制',
+ },
+ settingsDialog: {
+ editProfile: '编辑配置:{{name}}',
+ description: '为此配置设置环境变量和其他设置。',
+ loadingSettings: '加载设置中...',
+ envTab: '环境变量',
+ rawJsonTab: '原始 JSON',
+ generalTab: '常规',
+ noEnvVars: '尚未配置环境变量。',
+ noEnvVarsHint: '在 settings.json 文件中添加变量。',
+ loadingEditor: '加载编辑器中...',
+ profileInfo: '配置信息',
+ profileInfoDesc: '此配置文件的详细信息。',
+ path: '路径',
+ lastModified: '最后修改',
+ cancel: '取消',
+ saving: '保存中...',
+ saveChanges: '保存更改',
+ conflictTitle: '文件被外部修改',
+ conflictDesc: '此设置文件已被其他进程修改。用你的更改覆盖还是丢弃?',
+ overwrite: '覆盖',
+ },
+ setupWizard: {
+ title: '快速设置向导',
+ stepProviderDesc: '选择一个提供商开始',
+ stepAuthDesc: '完成提供商认证',
+ stepAccountDesc: '选择要使用的账号',
+ stepVariantDesc: '创建自定义变体',
+ stepSuccessDesc: '设置完成!',
+ authStep: {
+ authenticateWith: '通过 {{provider}} 认证以添加账号',
+ authenticating: '认证中...',
+ authenticateInBrowser: '在浏览器中认证',
+ completeOAuth: '在浏览器中完成 OAuth 流程...',
+ orUseTerminal: '或使用终端',
+ runCommandHint: '在终端中运行此命令:',
+ back: '返回',
+ checking: '检查中...',
+ refreshStatus: '刷新状态',
+ },
+ accountStep: {
+ selectAccount: '选择账号({{count}})',
+ defaultAccount: '默认账号',
+ or: '或',
+ addNewAccount: '添加新账号',
+ addNewAccountDesc: '使用其他账号认证',
+ back: '返回',
+ },
+ variantStep: {
+ back: '返回',
+ skip: '跳过',
+ },
+ successStep: {
+ title: '变体已创建!',
+ subtitle: '你的自定义变体已准备就绪',
+ usage: '用法:',
+ done: '完成',
+ },
+ },
+ accountSurfaceCard: {
+ business: '企业',
+ personal: '个人',
+ variant: '变体',
+ },
+ accountCardStats: {
+ notUsedYet: '尚未使用',
+ },
+ accountQuotaPanel: {
+ weekly: '每周',
+ loadingQuota: '加载配额中...',
+ },
+ userMenu: {
+ signedInAs: '已登录为 {{username}}',
+ },
+ authMonitorLive: {
+ live: '实时',
+ accountMonitor: '账号监控',
+ updated: '更新于 {{time}}',
+ updatedNow: '刚刚更新',
+ requestsLabel: '请求',
+ stats: '统计',
+ successRate: '成功率',
+ missingProjectId: '缺少项目 ID',
+ noActivity: '无活动',
+ },
+ providerCard: {
+ missingProjectIdAria: '缺少项目 ID',
+ },
+ loginPage: {
+ showPassword: '显示密码',
+ hidePassword: '隐藏密码',
+ },
+ cliproxyStatsOverview: {
+ sessionStatistics: '会话统计',
+ realTimeMetrics: '来自 {{backend}} 的实时使用指标',
+ offline: '离线',
+ running: '运行中',
+ noActiveSession: '无活跃会话',
+ noActiveSessionHint:
+ '使用 ccs gemini、ccs codex 或 ccs agy 启动 CLIProxy 会话后即可查看实时统计。',
+ failedLoadStats: '加载统计数据失败',
+ totalRequests: '总请求数',
+ successCount: '{{count}} 次成功',
+ successRate: '成功率',
+ totalTokens: '总 Token 数',
+ estimatedCost: '预估 ${{cost}}',
+ modelsUsed: '使用模型数',
+ modelUsageDistribution: '模型使用分布',
+ requestCount: '{{count}} 次请求',
+ },
+ cliproxyTable: {
+ name: '名称',
+ provider: '提供商',
+ model: '模型',
+ account: '账号',
+ status: '状态',
+ default: '默认',
+ actions: '操作',
+ },
+ cliproxyTabs: {
+ overview: '概览',
+ variants: '变体',
+ aiProviders: 'AI 提供商',
+ controlPanel: '控制面板',
+ },
+ cliproxyHeader: {
+ ccsLevelAccountManagement: 'CCS 级账号管理',
+ cliproxyNotAvailable: 'CLIProxy 不可用',
+ cliproxyControlPanel: 'CLIProxy 控制面板',
+ noVariants: '未找到 CLIProxy 变体。',
+ addAccountToStart: '添加账号以开始',
+ },
+ routingGuidance: {
+ roundRobin: '轮询模式均匀分配用量。',
+ fillFirst: '优先填满模式让备用账号保持冷启动直到需要时。',
+ routingStrategy: '路由策略',
+ optionalRouting: '可选路由',
+ },
+ extendedContext: {
+ extendedContext: '扩展上下文',
+ },
+ cliproxyConfig: {
+ unsavedChanges: '未保存的更改',
+ original: '原始值',
+ modified: '已修改',
+ reviewChanges: '查看更改',
+ loadingEditor: '加载编辑器中...',
+ },
+ providerEditor: {
+ provider: '提供商',
+ filePath: '文件路径',
+ lastModified: '最后修改',
+ defaultTarget: '默认目标',
+ quickUsage: '快速使用',
+ modelMapping: '模型映射',
+ status: '状态',
+ loadingSettings: '加载设置中...',
+ loadingEditor: '加载编辑器中...',
+ noAccountsConnected: '未连接账号',
+ addAccountToStart: '添加账号以开始',
+ gcpProjectIdReadonly: 'GCP 项目 ID(只读)',
+ projectIdNA: '项目 ID:无',
+ missingProjectId: '缺少项目 ID',
+ missingProjectIdHint: '可能导致错误。请移除该账号并重新添加以获取项目 ID。',
+ useIncognito: '使用隐身模式',
+ aliases: '别名',
+ current: '当前',
+ currentValue: '当前值',
+ composite: '复合',
+ defaultLabel: '默认',
+ requiredSetup: '必要设置',
+ connectorName: '连接器名称',
+ proxyUrl: '代理 URL',
+ proxyUrlSet: '代理 URL 已设置',
+ excludedModels: '排除模型',
+ headers: '请求头',
+ secret: '密钥',
+ prefix: '前缀',
+ modelMappings: '模型映射',
+ baseUri: 'Base URL',
+ apiKeys: 'API Key',
+ presets: '应用预设模型映射',
+ createVariant: '创建 CLIProxy 变体',
+ agyDenylist: 'Antigravity 禁用列表:Claude Opus 4.5 和 Claude Sonnet 4.5 已弃用。',
+ },
+ providerEditorAccountItem: {
+ modelsUsed: '使用模型数',
+ },
+ bulkActionBar: {
+ applyPreset: '应用预设',
+ },
+ modelConfigSection: {
+ defaultModel: '默认模型',
+ },
+ rawEditorSection: {
+ rawConfig: '原始配置',
+ },
+ providerEditorHeader: {
+ connectorName: '连接器名称',
+ },
+ aiProvidersFamilyRail: {
+ current: '当前',
+ },
+ aiProvidersEntryCard: {
+ apiKeys: 'API Key',
+ },
+ aiProvidersEntryDialog: {
+ connectorName: '连接器名称',
+ baseUri: 'Base URL',
+ proxyUrl: '代理 URL',
+ secret: '密钥',
+ prefix: '前缀',
+ excludedModels: '排除模型',
+ headers: '请求头',
+ modelMappings: '模型映射',
+ requiredSetup: '必要设置',
+ optionalRouting: '可选路由',
+ },
+ codex: {
+ controlCenter: '控制中心',
+ overview: '概览',
+ docs: '文档',
+ nativeCodexRuntime: '原生 Codex 运行时',
+ ccsCodexProvider: 'CCS Codex 提供商 / 桥接',
+ codexDocs: 'Codex 文档',
+ supportedFlows: '支持的工作流',
+ twoSupportedPaths: '两种支持路径:',
+ nativeLabel: '原生:',
+ nativeDesc: 'Codex 是 CCS v1 的一等公民运行时目标。',
+ ccsBridge: 'CCS 桥接',
+ apiProfilesDefault: 'API 配置默认仍使用 Claude 或 Droid。',
+ recommendedSetupFlow: '推荐设置流程',
+ fastestPath: '最快路径',
+ officialChannels: '官方渠道',
+ codexCli: 'Codex CLI',
+ openNativeCodex: '打开原生 Codex',
+ runBuiltInCodex: '在 Codex 上运行内置命令',
+ runBuiltInCodexExplicit: '在 Codex 上运行内置命令(显式)',
+ openCodexDashboard: '打开 Codex Dashboard',
+ status: '状态',
+ profiles: '配置',
+ createNewProfile: '创建新配置',
+ createNewProvider: '创建新提供商',
+ createNewMcpServer: '创建新 MCP 服务器',
+ defaultProvider: '默认提供商',
+ useDefault: '使用默认值',
+ useGlobalProvider: '使用全局提供商',
+ useProviderDefault: '使用提供商默认值',
+ quickFillWarning: '快速填充内容仅供参考,保存前请仔细检查。',
+ thisFileUpstreamOwned: '此文件由 Codex CLI 上游维护。',
+ notes: '备注',
+ approvalPolicy: '审批策略',
+ sandboxMode: '沙盒模式',
+ reasoningEffort: '推理强度',
+ useGlobalEffort: '使用全局强度',
+ reasoningEffortCapitalized: '推理强度',
+ thinkingBudgetTokens: '思考预算 Token',
+ modelContextWindow: '模型上下文窗口',
+ autoCompactTokenLimit: '自动压缩 Token 上限',
+ toolOutputTokenLimit: '工具输出 Token 上限',
+ webSearch: '网页搜索',
+ personality: '人格设定',
+ model: '模型',
+ rawOnly: '仅原始',
+ trusted: '受信任',
+ untrusted: '不受信任',
+ noProjectTrustEntries: '没有已保存的项目信任条目。',
+ codexNativeRecipe: '已保存原生 Codex 配方',
+ gptContextCap: 'GPT-5.4 上下文上限',
+ usageLimitCost: '用量上限成本超过 272K',
+ longContextOverride: '长上下文覆盖',
+ counts2x: '计数 2 倍',
+ normalUsageWindow: '常规使用窗口',
+ useCodexDefault: '使用 Codex 默认值',
+ stdio: 'stdio',
+ streamableHttp: 'streamable-http',
+ responses: 'responses',
+ defaultTargetCli: '默认目标 CLI',
+ executionChain: '执行链',
+ targetPath: '当前目标路径',
+ userConfig: '用户配置',
+ configYaml: 'config.yaml',
+ flow: '工作流',
+ docsTab: '文档',
+ },
+ droidSettings: {
+ quickControls: '快捷控制',
+ reasoningControls: '推理控制',
+ thinkingBudget: '思考预算',
+ anthropicOnly: '仅 Anthropic 模型',
+ byokCustomModels: 'BYOK 自定义模型',
+ },
+ rawJsonSettingsEditor: {
+ title: '原始设置编辑器',
+ },
+ copilotConfigForm: {
+ copilotConfiguration: 'Copilot 配置',
+ deprecatedModels: '检测到已弃用的 Copilot 模型',
+ failedLoadStatus: '加载状态失败',
+ useWithClaudeCode: '通过 Claude Code 使用你的 GitHub Copilot 订阅',
+ githubCopilotControls: 'GitHub Copilot 在上游控制提示词/上下文限制。',
+ provider: '提供商',
+ filePath: '文件路径',
+ status: '状态',
+ enabled: '已启用',
+ disabled: '已禁用',
+ loadingEditor: '加载编辑器中...',
+ modelMapping: '模型映射',
+ quickUsage: '快速使用',
+ noPremiumUsage: '无高级用量',
+ },
+ copilotPresets: {
+ gpt5Codex: 'GPT-5.3 Codex',
+ claude46: 'Claude 4.6',
+ gemini3: 'Gemini 3',
+ },
+ healthCard: {
+ allSystemsNominal: '所有系统正常',
+ machineChecks: '机器检查',
+ },
+ analyticsCards: {
+ cacheCost: '缓存成本',
+ hitRate: '命中率',
+ inputOutputRatio: '输入/输出比',
+ noCacheData: '暂无缓存数据',
+ noModelData: '暂无模型数据',
+ noSessionData: '暂无会话数据',
+ noTokenData: '暂无 Token 数据',
+ totalCost: '总成本',
+ totalTokens: '总 Token 数',
+ usageInsights: '使用洞察',
+ },
+ dateRangeFilter: {
+ pickADate: '选择日期',
+ },
+ logsConfig: {
+ level: '级别',
+ message: '消息',
+ source: '来源',
+ time: '时间',
+ proc: '进程',
+ open: '打开',
+ run: '运行',
+ refreshEntries: '刷新条目',
+ },
+ logsDetailPanel: {
+ details: '详情',
+ },
+ logsFilters: {
+ filters: '筛选',
+ },
+ logsOverviewCards: {
+ overview: '概览',
+ },
+ logsPageSkeleton: {
+ loadingLogs: '加载日志中...',
+ },
+ monitoringErrorLogs: {
+ logContent: '日志内容',
+ },
+ analyticsPages: {
+ chartsGrid: '图表',
+ costByModel: '按模型成本',
+ },
+ toasts: {
+ profileCreated: '配置创建成功',
+ profileUpdated: '配置更新成功',
+ profileDeleted: '配置删除成功',
+ orphanProfilesComplete: '孤立配置注册完成',
+ profileCopied: '配置复制成功',
+ profileImported: '配置导入成功',
+ authRequired: '{{provider}} 授权需要认证',
+ authSuccess: '{{provider}} 认证成功!',
+ authFailed: '{{provider}} 认证失败',
+ deviceCodeExpired: '验证码已过期,请重试。',
+ codeCopied: '验证码已复制到剪贴板',
+ failedCopy: '复制验证码失败',
+ configSaved: '配置保存成功',
+ configSaveFailed: '保存失败:{{error}}',
+ invalidYaml: 'YAML 无效,无法保存',
+ configUpdatedExternally: '配置已被外部更新',
+ settingsFileUpdated: '设置文件已更新',
+ accountsUpdated: '账号已更新',
+ noProfilesToSync: '没有可同步的配置',
+ syncFailed: '同步失败:{{error}}',
+ providerAuthSuccess: '{{provider}} 认证成功',
+ providerDeviceCodeInCallback: '提供商在回调模式中返回了设备码流程',
+ loggingConfigSaved: '日志配置已保存。',
+ loggingConfigSaveFailed: '保存日志配置失败。',
+ unifiedConfigUpdated: '配置更新成功',
+ migrationPreviewComplete: '迁移预览完成',
+ migrationComplete: '迁移成功',
+ migrationFailed: '迁移失败',
+ rollbackComplete: '回滚成功',
+ rollbackFailed: '回滚失败',
+ defaultAccountSet: '默认账号已设为「{{name}}」',
+ defaultAccountReset: '默认账号已重置为 CCS',
+ accountDeleted: '账号「{{name}}」已删除',
+ contextUpdated: '已将「{{name}}」的上下文更新为 {{summary}}',
+ legacyConfirmError: '账号「{{name}}」需要显式确认。请在该账号上使用"编辑历史同步"。',
+ legacyConfirmFailed: '旧版账号「{{name}}」确认失败:{{error}}',
+ legacyConfirmSuccess_one: '{{count}} 个旧版账号已确认',
+ legacyConfirmSuccess_other: '{{count}} 个旧版账号已确认',
+ noLegacyAccounts: '没有需要确认的旧版账号',
+ routingStrategySet: '路由策略已设为 {{strategy}}',
+ variantCreated: '变体创建成功',
+ variantUpdated: '变体更新成功',
+ variantDeleted: '变体删除成功',
+ defaultAccountUpdated: '默认账号已更新',
+ accountRemoved: '账号已移除',
+ accountPaused: '账号已暂停',
+ accountResumed: '账号已恢复',
+ accountAdded: '已为 {{provider}} 添加账号',
+ kiroImported: '已导入 Kiro 账号:{{name}}',
+ kiroTokenImported: 'Kiro token 已导入',
+ modelUpdated: '模型已更新',
+ presetSaved: '预设「{{name}}」已保存',
+ presetDeleted: '预设已删除',
+ cliproxyAlreadyRunning: 'CLIProxy 已在运行',
+ cliproxyStarted: 'CLIProxy 启动成功',
+ cliproxyStartFailed: 'CLIProxy 启动失败',
+ cliproxyStopped: 'CLIProxy 已停止',
+ cliproxyStopFailed: 'CLIProxy 停止失败',
+ presetApplied: '已应用「{{name}}」预设',
+ presetAppliedCustom: '已应用自定义预设',
+ settingsSavedWithAdjustments: '设置已保存(含模型调整)',
+ settingsSaved: '设置已保存',
+ failedSaveSettings: '保存设置失败',
+ codexRefreshFailed: '刷新 Codex 快照失败。原始编辑已保留。',
+ codexRefreshError: '刷新 Codex 快照失败。',
+ codexFixToml: '请先修复 TOML 再保存。',
+ codexSaved: '已保存 Codex config.toml。',
+ codexChangedExternally: 'config.toml 已被外部修改,请刷新后重试。',
+ codexSaveFailed: '保存 Codex config.toml 失败。',
+ codexUpdateFailed: '更新 Codex 配置失败。',
+ noOrphanProfiles: '未发现孤立配置',
+ profilesRegistered: '已注册 {{count}} 个配置{{skipped}}',
+ destinationEmpty: '目标配置名称不能为空',
+ profileExportDownloaded: '配置导出已下载',
+ profileImportFailed: '导入配置包失败',
+ },
+ profileEditorSections: {
+ imageAnalysis: '图片分析',
+ loadingImageSettings: '加载图片设置中...',
+ skipPermissionPrompts: '启动时跳过权限提示',
+ useNativeImageReading: '使用原生图片读取',
+ skipTransformer: '跳过转换器',
+ friendlyUi: '友好界面',
+ info: '信息',
+ },
+ imageAnalysisStatus: {
+ sectionTitle: '图片',
+ openSettings: '打开设置',
+ useNativeImageReading: '使用原生图片读取',
+ refreshingPreview: '刷新预览中',
+ savedStatus: '已保存状态',
+ livePreview: '实时预览',
+ disabledGlobally: '全局已禁用',
+ targetBypassesHook: '{{target}} 绕过了 hook',
+ nativeImageReading: '原生图片读取',
+ setupNeeded: '需要设置',
+ needsAuth: '需要认证',
+ needsProxy: '需要代理',
+ nativeFallback: '原生回退',
+ transformerReady: '转换器就绪',
+ badgeDisabled: '已禁用',
+ badgeBypassed: '已绕过',
+ badgeNative: '原生',
+ badgeSetup: '需设置',
+ badgeAuth: '认证',
+ badgeProxy: '代理',
+ badgeReady: '就绪',
+ capabilityVerified: '已验证',
+ capabilityUnknown: '未知',
+ toggleSummaryNativeCapable: '{{model}} 支持图片,CCS 将在此跳过转换器。',
+ toggleSummaryNativeModel: 'CCS 将对 {{model}} 优先使用原生读取。',
+ toggleSummaryNativeDefault: 'CCS 将对此配置优先使用原生图片读取。',
+ toggleSummaryNativeFileAccess: '此配置当前仍使用原生文件访问。',
+ toggleSummaryInactiveTarget: '当前选择 {{target}} 时,已保存的 Claude 端图片路由不生效。',
+ toggleSummaryTransformerRoute: '转换器路由:{{backend}}{{modelSuffix}}。',
+ noteDisabledGlobally: '图片功能在 CCS 设置中被全局禁用。',
+ noteTargetBypassesHook: '当前目标 {{target}} 绕过了 Claude Read hook。',
+ notePersistHook: '请先持久化配置 hook,然后才能在此使用转换器路由。',
+ targetLabel: {
+ claude: 'Claude Code',
+ droid: 'Factory Droid',
+ codex: 'Codex CLI',
+ },
+ },
+ openrouterBadge: {
+ new: '新',
+ integration: 'OpenRouter 集成',
+ },
+ openrouterBanner: {
+ accessModels: '通过 OpenRouter 访问 {{count}}+ 模型',
+ add: '添加',
+ },
+ openrouterModelPicker: {
+ searchModels: '搜索模型',
+ newestModels: '最新模型',
+ },
+ openrouterPromoCard: {
+ title: 'OpenRouter',
+ description: '通过一个 API 端点访问数百个模型。',
+ },
+ profileCard: {
+ profile: '配置',
+ openRouter: 'OpenRouter 配置',
+ claudeCode: 'Claude Code',
+ claudeCodeDefault: 'Claude Code(默认)',
+ factoryDroid: 'Factory Droid',
+ codexCli: 'Codex CLI',
+ ccsProfile: 'CCS 配置',
+ },
+ profileDeck: {
+ profiles: '配置',
+ failedToLoad: '加载配置失败:{{message}}',
+ noProfiles: '尚未配置。创建你的第一个配置以开始。',
+ },
+ profilesTable: {
+ name: '名称',
+ provider: '提供商',
+ model: '模型',
+ target: '目标',
+ lastModified: '最后修改',
+ actions: '操作',
+ edit: '编辑',
+ },
+ profileCreateDialog: {
+ createProfile: '创建配置',
+ appliedModelToTiers: '已将「{{model}}」应用到所有模型档位',
+ profileCreated: '配置「{{name}}」已创建',
+ failedCreate: '创建配置失败',
+ chooseProviderHint: '选择提供商预设,或配置自定义 API 端点。',
+ basicInformation: '基本信息',
+ modelConfiguration: '模型配置',
+ usedInCli: 'CLI 中使用:',
+ apiBaseUrl: 'API Base URL',
+ baseUrlPlaceholder: 'https://api.example.com/v1',
+ prefilledFromPreset: '从 {{name}} 预填。可按需调整。',
+ optionalForPreset: '{{name}} 的可选项。留空使用原生 Anthropic 认证。',
+ endpointHint: '接受 OpenAI 兼容和 Anthropic 请求的端点',
+ optional: '(可选)',
+ apiKeyOptionalPlaceholder: '可选 - 仅在启用认证时需要',
+ apiKeyPlaceholder: 'sk-...',
+ apiKeyOptionalHint: '仅在本地端点启用了认证时才需要',
+ defaultTargetCli: '默认目标 CLI',
+ modelMapping: '模型映射',
+ modelMappingDesc: '将 Claude Code 档位(Opus/Sonnet/Haiku)映射到提供商支持的模型。',
+ searchModelsPlaceholder: '输入搜索(例如:opus、sonnet、gpt-4o)...',
+ noModelsFound: '未找到匹配「{{query}}」的模型',
+ loadingModels: '加载模型中...',
+ defaultModel: '默认模型',
+ sonnetMapping: 'Sonnet 映射',
+ opusMapping: 'Opus 映射',
+ haikuMapping: 'Haiku 映射',
+ sonnetMappingPlaceholder: '例如:gpt-4o、claude-sonnet-4',
+ opusMappingPlaceholder: '例如:o1、claude-opus-4.5',
+ haikuMappingPlaceholder: '例如:gpt-4o-mini、claude-3.5-haiku',
+ free: '免费',
+ },
+ profileDialogLegacy: {
+ editProfile: '编辑配置',
+ },
+ supportEntryCard: {
+ actionRequired: '待处理',
+ },
+ settingsPage: {
+ title: '设置',
+ loading: '加载中...',
+ failedLoad: '加载设置失败。',
+ tabs: {
+ web: '网页',
+ env: '环境',
+ think: '思考',
+ proxy: '代理',
+ auth: '认证',
+ backup: '备份',
+ channels: '频道',
+ imageAnalysis: '图片',
+ },
+ websearchSection: {
+ title: '网页搜索',
+ description: 'CLI 网页搜索配置。',
+ },
+ thinkingSection: {
+ title: '思考',
+ description: '为支持的模型配置扩展思考/推理。',
+ directOverride: '直接覆盖',
+ youType: '你输入:',
+ ccsAdds: 'CCS 添加:',
+ executionChain: '执行链',
+ primaryBackends: '主要后端',
+ legacyCliFallbacks: '旧版 CLI 回退',
+ managedPayload: '托管载荷',
+ sharedTargetMetadata: '共享目标元数据',
+ ideTargetMetadata: 'IDE 目标元数据',
+ ideSettingsPath: 'IDE 设置路径',
+ ideHost: 'IDE 主机',
+ resolvedBinding: '已解析绑定',
+ bindingName: '绑定名称',
+ inSync: '已同步',
+ currentTargetPath: '当前目标路径',
+ warnings: '告警',
+ notes: '备注',
+ workspacePresets: '工作区预设',
+ draft: '草稿',
+ advanced: '高级',
+ recommended: '推荐设置流程',
+ configureModelFirst: '请先配置模型',
+ },
+ proxySection: {
+ title: '代理',
+ loadingImageSettings: '加载图片设置中...',
+ },
+ channelsSection: {
+ title: '官方渠道',
+ description: '查看和管理官方发布渠道。',
+ },
+ imageAnalysisSection: {
+ title: '图片分析',
+ description: '配置图片分析设置。',
+ loading: '加载图片设置中...',
+ },
+ },
+ codexPage: {
+ title: 'Codex',
+ controlCenter: '控制中心',
+ overview: '概览',
+ docs: '文档',
+ nativeRuntime: '原生运行时',
+ ccsProvider: 'CCS 提供商',
+ setup: '安装',
+ },
+ apiPage: {
+ title: 'API 配置',
+ subtitle: '管理你的 API 配置和端点。',
+ },
+ claudeExtensionPage: {
+ title: 'Claude Extension',
+ subtitle: 'Claude Extension 集成设置。',
+ claudeAuth: 'Claude 认证',
+ status: '状态',
+ targetMetadata: 'IDE 目标元数据',
+ },
+ sharedPageV2: {
+ title: '共享',
+ subtitle: '共享数据管理。',
+ },
+ homePageV2: {
+ title: '首页',
+ logsMoved: '日志已移至专用工作区',
+ profiles: '配置',
+ cliproxy: 'CLIProxy',
+ accounts: '账号',
+ health: '健康',
+ },
+ analyticsPageV2: {
+ title: '分析',
+ subtitle: '使用分析与洞察。',
+ },
+ logsPageV2: {
+ title: '日志',
+ subtitle: '查看和管理系统日志。',
+ },
+ healthPageV2: {
+ title: '健康',
+ subtitle: '系统健康监控。',
+ },
+ aiProvidersPage: {
+ title: 'AI 提供商',
+ subtitle: '管理 AI 提供商配置。',
+ unableToLoad: '无法加载 AI 提供商',
+ },
},
},
vi: {
@@ -2809,6 +4555,7 @@ const resources = {
health: 'Sức khỏe',
settings: 'Cài đặt',
openrouterTooltip: 'Nổi bật: OpenRouter + Alibaba Coding Plan + Ollama',
+ logs: 'Nhật ký',
},
home: {
profiles: 'Hồ sơ',
@@ -2946,6 +4693,7 @@ const resources = {
cancel: 'Hủy bỏ',
savePreset: 'Lưu preset',
applyPreset: 'Áp dụng cài sẵn',
+ deletePreset: 'Xóa cài sẵn',
},
componentModelSelector: {
selectModel: 'Chọn mô hình',
@@ -2987,6 +4735,13 @@ const resources = {
recommended: 'Đề xuất',
allModelsCount: 'Tất cả mô hình ({{count}})',
noModelsAvailable: 'Không có mô hình khả dụng',
+ shadowed: 'Bị che khuất',
+ prefixOnly: 'Chỉ tiền tố',
+ current: 'Hiện tại',
+ currentValue: 'Giá trị hiện tại',
+ preferredPinnedModel: 'Mô hình ghim ưu tiên:',
+ pinnedRouteStatus: 'Trạng thái tuyến ghim:',
+ pinnedModelNotAdvertised: 'Mô hình ghim hiện không được proxy quảng bá: {{model}}',
},
createAuthProfileDialog: {
title: 'Tạo tài khoản mới',
@@ -3636,6 +5391,8 @@ const resources = {
},
settingsTabs: {
web: 'Web',
+ image: 'Hình ảnh',
+ channels: 'Kênh',
env: 'Env',
think: 'Tư duy',
proxy: 'Proxy',
@@ -4221,6 +5978,860 @@ const resources = {
retryContent: 'Thử lại nội dung',
noMarkdown: 'Không có nội dung Markdown khả dụng.',
},
+ heroSection: {
+ title: 'CCS Config',
+ subtitle: 'Bảng điều khiển Claude Code Switch',
+ },
+ hubFooter: {
+ logs: 'Nhật ký',
+ settings: 'Cài đặt',
+ github: 'GitHub',
+ copyright: '© {{year}} kaitranntt',
+ },
+ themeToggle: {
+ srLabel: 'Chuyển đổi giao diện',
+ },
+ ccsLogo: {
+ alt: 'Logo CCS',
+ text: 'CCS Config',
+ },
+ claudekitBadge: {
+ title: 'Được vận hành bởi ClaudeKit Framework',
+ alt: 'ClaudeKit',
+ poweredBy: 'Được vận hành bởi',
+ claudekit: 'ClaudeKit',
+ },
+ codeEditor: {
+ revealSensitive: 'Hiện giá trị nhạy cảm',
+ maskSensitive: 'Ẩn giá trị nhạy cảm',
+ valid: '{{language}} hợp lệ',
+ readOnly: '(Chỉ đọc)',
+ },
+ commandBuilder: {
+ title: 'Trình tạo lệnh',
+ searchPlaceholder: 'Gõ hoặc chọn lệnh...',
+ copy: 'Sao chép',
+ run: 'Chạy',
+ cmdConfig: 'Mở giao diện cấu hình',
+ cmdCreateProfile: 'Tạo hồ sơ mới',
+ cmdSwitchProfile: 'Chuyển sang hồ sơ',
+ cmdDoctor: 'Kiểm tra sức khỏe hệ thống',
+ cmdListProviders: 'Liệt kê nhà cung cấp CLIProxy',
+ cmdAddProvider: 'Thêm nhà cung cấp CLIProxy',
+ },
+ confirmDialog: {
+ confirm: 'Xác nhận',
+ cancel: 'Hủy',
+ },
+ connectionIndicator: {
+ connected: 'Đã kết nối',
+ connecting: 'Đang kết nối...',
+ disconnected: 'Đã ngắt kết nối',
+ reconnecting: 'Đang kết nối lại...',
+ },
+ docsLink: {
+ title: 'Xem tài liệu',
+ },
+ githubLink: {
+ title: 'Báo cáo vấn đề trên GitHub',
+ },
+ globalEnvIndicator: {
+ injectedCount_one: '{{count}} biến env toàn cục sẽ được áp dụng khi chạy',
+ injectedCount_other: '{{count}} biến env toàn cục sẽ được áp dụng khi chạy',
+ overriddenCount: '({{count}} bị ghi đè bởi hồ sơ)',
+ skippedLabel: 'Đã bỏ qua (hồ sơ đã định nghĩa):',
+ configureInSettings: 'Cấu hình trong Cài đặt',
+ },
+ localhostDisclaimer: {
+ remoteReadonlyAuthDisabledLong:
+ 'Dashboard từ xa ở chế độ chỉ đọc vì dashboard auth đang bị tắt trên máy host. Bật lại dashboard auth trên máy host để mở khóa thay đổi từ xa.',
+ remoteReadonlyAuthDisabledShort:
+ 'Dashboard từ xa chỉ đọc cho đến khi dashboard auth được bật lại trên máy host.',
+ remoteReadonlySetupLong:
+ 'Dashboard từ xa chỉ đọc cho đến khi bạn chạy ccs config auth setup trên máy host.',
+ remoteReadonlySetupShort:
+ 'Dashboard từ xa chỉ đọc cho đến khi auth được cấu hình trên máy host.',
+ localLong: 'Dashboard này chạy cục bộ. Toàn bộ dữ liệu nằm trên máy của bạn.',
+ localShort: 'Dashboard cục bộ - dữ liệu nằm trên thiết bị của bạn.',
+ dismiss: 'Bỏ qua thông báo',
+ },
+ privacyToggle: {
+ modeOn: 'Chế độ riêng tư BẬT - Nhấp để hiện dữ liệu',
+ modeOff: 'Chế độ riêng tư TẮT - Nhấp để ẩn dữ liệu',
+ },
+ projectSelectionDialog: {
+ title: 'Chọn dự án Google Cloud',
+ description: 'Chọn dự án để dùng cho xác thực {{provider}}.',
+ autoSelectCountdown: '(Tự động chọn mặc định sau {{count}}s)',
+ default: 'Mặc định',
+ allProjects: 'Tất cả dự án',
+ allProjectsDescription: 'Thêm tất cả {{count}} dự án đã liệt kê',
+ useDefault: 'Dùng mặc định',
+ selecting: 'Đang chọn...',
+ confirmSelection: 'Xác nhận lựa chọn',
+ codeCopied: 'Đã sao chép mã',
+ copyVerificationCode: 'Sao chép mã xác minh',
+ },
+ quickCommands: {
+ title: 'Lệnh nhanh',
+ startDefault: 'Khởi chạy mặc định',
+ startDefaultDesc: 'Chạy Claude với hồ sơ mặc định',
+ glmProfile: 'Hồ sơ GLM',
+ glmProfileDesc: 'Chuyển sang mô hình GLM',
+ healthCheck: 'Kiểm tra sức khỏe',
+ healthCheckDesc: 'Chạy chẩn đoán hệ thống',
+ delegateTask: 'Giao việc',
+ delegateTaskDesc: 'Giao việc cho hồ sơ GLM',
+ },
+ quotaTooltip: {
+ loadingQuota: 'Đang tải quota...',
+ failedLoadQuota: 'Không tải được quota',
+ modelQuotas: 'Hạn ngạch mô hình:',
+ rateLimits: 'Giới hạn tốc độ:',
+ plan: 'Gói: {{plan}}',
+ quotaSnapshots: 'Ảnh chụp hạn ngạch:',
+ unlimited: 'Không giới hạn',
+ remaining: 'Còn {{remaining}}/{{entitlement}}',
+ tier: 'Tier',
+ tierId: 'Tier ID',
+ state: 'Trạng thái',
+ credits: 'Credits',
+ modelQuotasLower: 'Hạn ngạch mô hình:',
+ allBucketsReport: 'Tất cả bucket báo cáo {{tokenType}}',
+ requestsRemaining: 'Còn {{count}} yêu cầu',
+ inputTokensRemaining: 'Còn {{count}} token đầu vào',
+ outputTokensRemaining: 'Còn {{count}} token đầu ra',
+ amountRemaining: 'Còn {{count}}',
+ fiveHourLimit: 'Giới hạn dùng 5 giờ',
+ weeklyLimit: 'Giới hạn dùng hàng tuần',
+ weeklyOpus: 'Dùng hàng tuần (Opus)',
+ weeklySonnet: 'Dùng hàng tuần (Sonnet)',
+ weeklyOAuthApps: 'Dùng hàng tuần (OAuth apps)',
+ weeklyCowork: 'Dùng hàng tuần (Cowork)',
+ extraUsage: 'Lượt dùng thêm',
+ premiumInteractions: 'Tương tác Premium',
+ chat: 'Chat',
+ completions: 'Hoàn thành',
+ resets: 'Reset lúc {{time}}',
+ fiveHourResets: 'Reset 5h lúc {{time}}',
+ weeklyResets: 'Reset hàng tuần lúc {{time}}',
+ },
+ sponsorButton: {
+ title: 'Tài trợ dự án này trên GitHub',
+ sponsor: 'Tài trợ',
+ },
+ valueMetrics: {
+ apiCostSaved: 'Chi phí API tiết kiệm',
+ tokensSaved: 'Token tiết kiệm',
+ queriesFaster: 'Truy vấn nhanh hơn',
+ errorsReduced: 'Lỗi giảm',
+ vsLastMonth: 'so với tháng trước',
+ throughCaching: 'thông qua cache',
+ averageSpeedup: 'tăng tốc trung bình',
+ withRetryLogic: 'với logic thử lại',
+ performanceMetrics: 'Chỉ số hiệu suất',
+ monthlySummary: 'Tổng kết hàng tháng',
+ totalSaved: 'Tổng tiết kiệm',
+ tokensProcessed: 'Token đã xử lý',
+ queriesHandled: 'Truy vấn đã xử lý',
+ uptime: 'Thời gian hoạt động',
+ },
+ updatesSpotlight: {
+ openUpdatesCenter: 'Mở Trung tâm cập nhật',
+ },
+ deviceCodeDialog: {
+ authorize: 'Xác thực {{provider}}',
+ enterCodeAtPage: 'Nhập mã bên dưới tại trang xác thực.',
+ expiresIn: '(Hết hạn sau {{time}})',
+ codeExpired: '(Mã đã hết hạn)',
+ copied: 'Đã sao chép!',
+ copyCode: 'Sao chép mã',
+ waitingForAuth: 'Đang chờ xác thực...',
+ openVerificationPage: 'Mở trang xác minh',
+ openProviderPage: 'Mở {{provider}}',
+ copyCodeAria: 'Sao chép mã xác minh',
+ codeCopiedAria: 'Đã sao chép mã',
+ },
+ settingsDialog: {
+ editProfile: 'Chỉnh sửa hồ sơ: {{name}}',
+ description: 'Cấu hình biến môi trường và cài đặt cho hồ sơ này.',
+ loadingSettings: 'Đang tải cài đặt...',
+ envTab: 'Môi trường',
+ rawJsonTab: 'JSON thô',
+ generalTab: 'Chung',
+ noEnvVars: 'Không có biến môi trường nào được cấu hình.',
+ noEnvVarsHint: 'Thêm biến trong tệp settings.json của bạn.',
+ loadingEditor: 'Đang tải trình soạn thảo...',
+ profileInfo: 'Thông tin hồ sơ',
+ profileInfoDesc: 'Chi tiết về tệp cấu hình này.',
+ path: 'Đường dẫn',
+ lastModified: 'Sửa đổi lần cuối',
+ cancel: 'Hủy',
+ saving: 'Đang lưu...',
+ saveChanges: 'Lưu thay đổi',
+ conflictTitle: 'Tệp đã bị thay đổi bên ngoài',
+ conflictDesc:
+ 'Tệp cài đặt này đã bị thay đổi bởi một tiến trình khác. Ghi đè thay đổi của bạn hay hủy?',
+ overwrite: 'Ghi đè',
+ },
+ setupWizard: {
+ title: 'Trình thiết lập nhanh',
+ stepProviderDesc: 'Chọn nhà cung cấp để bắt đầu',
+ stepAuthDesc: 'Xác thực với nhà cung cấp',
+ stepAccountDesc: 'Chọn tài khoản để sử dụng',
+ stepVariantDesc: 'Tạo biến thể tùy chỉnh',
+ stepSuccessDesc: 'Thiết lập hoàn tất!',
+ authStep: {
+ authenticateWith: 'Xác thực với {{provider}} để thêm tài khoản',
+ authenticating: 'Đang xác thực...',
+ authenticateInBrowser: 'Xác thực trong trình duyệt',
+ completeOAuth: 'Hoàn tất luồng OAuth trong trình duyệt...',
+ orUseTerminal: 'Hoặc dùng terminal',
+ runCommandHint: 'Chạy lệnh này trong terminal:',
+ back: 'Quay lại',
+ checking: 'Đang kiểm tra...',
+ refreshStatus: 'Làm mới trạng thái',
+ },
+ accountStep: {
+ selectAccount: 'Chọn tài khoản ({{count}})',
+ defaultAccount: 'Tài khoản mặc định',
+ or: 'Hoặc',
+ addNewAccount: 'Thêm tài khoản mới',
+ addNewAccountDesc: 'Xác thực với một tài khoản khác',
+ back: 'Quay lại',
+ },
+ variantStep: {
+ back: 'Quay lại',
+ skip: 'Bỏ qua',
+ },
+ successStep: {
+ title: 'Biến thể đã được tạo!',
+ subtitle: 'Biến thể tùy chỉnh đã sẵn sàng để sử dụng',
+ usage: 'Cách dùng:',
+ done: 'Xong',
+ },
+ },
+ accountSurfaceCard: {
+ business: 'Biz',
+ personal: 'Cá nhân',
+ variant: 'Biến thể',
+ },
+ accountCardStats: {
+ notUsedYet: 'Chưa sử dụng',
+ },
+ accountQuotaPanel: {
+ weekly: 'Hàng tuần',
+ loadingQuota: 'Đang tải quota...',
+ },
+ userMenu: {
+ signedInAs: 'Đăng nhập sebagai {{username}}',
+ },
+ authMonitorLive: {
+ live: 'TRỰC TIẾP',
+ accountMonitor: 'Theo dõi tài khoản',
+ updated: 'Cập nhật lúc {{time}}',
+ updatedNow: 'Vừa cập nhật',
+ requestsLabel: 'req',
+ stats: 'Thống kê',
+ successRate: 'Tỷ lệ thành công',
+ missingProjectId: 'Thiếu Project ID',
+ noActivity: 'không hoạt động',
+ },
+ providerCard: {
+ missingProjectIdAria: 'Thiếu Project ID',
+ },
+ loginPage: {
+ showPassword: 'Hiện mật khẩu',
+ hidePassword: 'Ẩn mật khẩu',
+ },
+ cliproxyStatsOverview: {
+ sessionStatistics: 'Thống kê phiên',
+ realTimeMetrics: 'Chỉ số thời gian thực từ {{backend}}',
+ offline: 'Ngoại tuyến',
+ running: 'Đang chạy',
+ noActiveSession: 'Không có phiên hoạt động',
+ noActiveSessionHint:
+ 'Bắt đầu phiên CLIProxy bằng ccs gemini, ccs codex hoặc ccs agy để xem thống kê thời gian thực.',
+ failedLoadStats: 'Không tải được thống kê',
+ totalRequests: 'Tổng yêu cầu',
+ successCount: '{{count}} thành công',
+ successRate: 'Tỷ lệ thành công',
+ totalTokens: 'Tổng token',
+ estimatedCost: '~${{cost}} ước tính',
+ modelsUsed: 'Mô hình đã dùng',
+ modelUsageDistribution: 'Phân bố sử dụng mô hình',
+ requestCount: '{{count}} yêu cầu',
+ },
+ cliproxyTable: {
+ name: 'Tên',
+ provider: 'Nhà cung cấp',
+ model: 'Mô hình',
+ account: 'Tài khoản',
+ status: 'Trạng thái',
+ default: 'Mặc định',
+ actions: 'Hành động',
+ },
+ cliproxyTabs: {
+ overview: 'Tổng quan',
+ variants: 'Biến thể',
+ aiProviders: 'AI Providers',
+ controlPanel: 'Bảng điều khiển',
+ },
+ cliproxyHeader: {
+ ccsLevelAccountManagement: 'Quản lý tài khoản cấp CCS',
+ cliproxyNotAvailable: 'CLIProxy không khả dụng',
+ cliproxyControlPanel: 'Bảng điều khiển CLIProxy',
+ noVariants: 'Không tìm thấy biến thể CLIProxy nào.',
+ addAccountToStart: 'Thêm tài khoản để bắt đầu',
+ },
+ routingGuidance: {
+ roundRobin: 'Round-robin phân bổ đều lượt dùng.',
+ fillFirst: 'Fill-first giữ tài khoản dự phòng cho đến khi cần thiết.',
+ routingStrategy: 'Chiến lược định tuyến',
+ optionalRouting: 'Định tuyến tùy chọn',
+ },
+ extendedContext: {
+ extendedContext: 'Ngữ cảnh mở rộng',
+ },
+ cliproxyConfig: {
+ unsavedChanges: 'Thay đổi chưa lưu',
+ original: 'Gốc',
+ modified: 'Đã sửa đổi',
+ reviewChanges: 'Xem lại thay đổi',
+ loadingEditor: 'Đang tải trình soạn thảo...',
+ },
+ providerEditor: {
+ provider: 'Nhà cung cấp',
+ filePath: 'Đường dẫn tệp',
+ lastModified: 'Sửa đổi lần cuối',
+ defaultTarget: 'Mục tiêu mặc định',
+ quickUsage: 'Sử dụng nhanh',
+ modelMapping: 'Ánh xạ mô hình',
+ status: 'Trạng thái',
+ loadingSettings: 'Đang tải cài đặt...',
+ loadingEditor: 'Đang tải trình soạn thảo...',
+ noAccountsConnected: 'Chưa kết nối tài khoản nào',
+ addAccountToStart: 'Thêm tài khoản để bắt đầu',
+ gcpProjectIdReadonly: 'GCP Project ID (chỉ đọc)',
+ projectIdNA: 'Project ID: N/A',
+ missingProjectId: 'Thiếu Project ID',
+ missingProjectIdHint:
+ 'Điều này có thể gây lỗi. Xóa tài khoản và thêm lại để lấy Project ID.',
+ useIncognito: 'Dùng ẩn danh',
+ aliases: 'Bí danh',
+ current: 'Hiện tại',
+ currentValue: 'Giá trị hiện tại',
+ composite: 'tổng hợp',
+ defaultLabel: 'mặc định',
+ requiredSetup: 'Cài đặt cần thiết',
+ connectorName: 'Tên connector',
+ proxyUrl: 'URL proxy',
+ proxyUrlSet: 'Đã đặt URL proxy',
+ excludedModels: 'Mô hình loại trừ',
+ headers: 'Headers',
+ secret: 'Secret',
+ prefix: 'Tiền tố',
+ modelMappings: 'Ánh xạ mô hình',
+ baseUri: 'URL cơ sở',
+ apiKeys: 'Khóa API',
+ presets: 'Áp dụng ánh xạ mô hình đã cấu hình sẵn',
+ createVariant: 'Tạo biến thể CLIProxy',
+ agyDenylist:
+ 'Danh sách loại trừ Antigravity: Claude Opus 4.5 và Claude Sonnet 4.5 đã bị loại bỏ.',
+ },
+ providerEditorAccountItem: {
+ modelsUsed: 'Mô hình đã dùng',
+ },
+ bulkActionBar: {
+ applyPreset: 'Áp dụng preset',
+ },
+ modelConfigSection: {
+ defaultModel: 'Mô hình mặc định',
+ },
+ rawEditorSection: {
+ rawConfig: 'Cấu hình thô',
+ },
+ providerEditorHeader: {
+ connectorName: 'Tên connector',
+ },
+ aiProvidersFamilyRail: {
+ current: 'Hiện tại',
+ },
+ aiProvidersEntryCard: {
+ apiKeys: 'Khóa API',
+ },
+ aiProvidersEntryDialog: {
+ connectorName: 'Tên connector',
+ baseUri: 'URL cơ sở',
+ proxyUrl: 'URL proxy',
+ secret: 'Secret',
+ prefix: 'Tiền tố',
+ excludedModels: 'Mô hình loại trừ',
+ headers: 'Headers',
+ modelMappings: 'Ánh xạ mô hình',
+ requiredSetup: 'Cài đặt cần thiết',
+ optionalRouting: 'Định tuyến tùy chọn',
+ },
+ codex: {
+ controlCenter: 'Trung tâm điều khiển',
+ overview: 'Tổng quan',
+ docs: 'Tài liệu',
+ nativeCodexRuntime: 'Codex Runtime gốc',
+ ccsCodexProvider: 'CCS Codex provider / bridge',
+ codexDocs: 'Tài liệu Codex',
+ supportedFlows: 'Các luồng được hỗ trợ',
+ twoSupportedPaths: 'Hai đường dẫn được hỗ trợ:',
+ nativeLabel: 'Gốc:',
+ nativeDesc: 'Codex là target runtime hạng nhất trong CCS v1.',
+ ccsBridge: 'CCS Bridge',
+ apiProfilesDefault: 'API profiles mặc định dùng Claude hoặc Droid.',
+ recommendedSetupFlow: 'Luồng thiết lập khuyến nghị',
+ fastestPath: 'Đường dẫn nhanh nhất',
+ officialChannels: 'Kênh chính thức',
+ codexCli: 'Codex CLI',
+ openNativeCodex: 'Mở Codex gốc',
+ runBuiltInCodex: 'Chạy Codex tích hợp trên Codex',
+ runBuiltInCodexExplicit: 'Chạy Codex tích hợp trên Codex (rõ ràng)',
+ openCodexDashboard: 'Mở dashboard Codex',
+ status: 'Trạng thái',
+ profiles: 'Hồ sơ',
+ createNewProfile: 'Tạo hồ sơ mới',
+ createNewProvider: 'Tạo nhà cung cấp mới',
+ createNewMcpServer: 'Tạo MCP server mới',
+ defaultProvider: 'Nhà cung cấp mặc định',
+ useDefault: 'Dùng mặc định',
+ useGlobalProvider: 'Dùng nhà cung cấp toàn cục',
+ useProviderDefault: 'Dùng mặc định của nhà cung cấp',
+ quickFillWarning: 'Chỉ điền nhanh. Hãy xem lại trước khi lưu.',
+ thisFileUpstreamOwned: 'Tệp này thuộc sở hữu của Codex CLI upstream.',
+ notes: 'Ghi chú',
+ approvalPolicy: 'Chính sách phê duyệt',
+ sandboxMode: 'Chế độ sandbox',
+ reasoningEffort: 'Mức nỗ lực lý luận',
+ useGlobalEffort: 'Dùng mức toàn cục',
+ reasoningEffortCapitalized: 'Mức nỗ lực lý luận',
+ thinkingBudgetTokens: 'Token ngân sách tư duy',
+ modelContextWindow: 'Cửa sổ ngữ cảnh mô hình',
+ autoCompactTokenLimit: 'Giới hạn token tự động nén',
+ toolOutputTokenLimit: 'Giới hạn token đầu ra tool',
+ webSearch: 'Tìm kiếm web',
+ personality: 'Tính cách',
+ model: 'Mô hình',
+ rawOnly: 'Chỉ thô',
+ trusted: 'đã tin cậy',
+ untrusted: 'chưa tin cậy',
+ noProjectTrustEntries: 'Không có mục tin cậy dự án nào được lưu.',
+ codexNativeRecipe: 'Đã lưu công thức Codex gốc',
+ gptContextCap: 'Giới hạn ngữ cảnh GPT-5.4',
+ usageLimitCost: 'Chi phí vượt mức 272K',
+ longContextOverride: 'Ghi đè ngữ cảnh dài',
+ counts2x: 'Tính 2x',
+ normalUsageWindow: 'Cửa sổ sử dụng bình thường',
+ useCodexDefault: 'Dùng mặc định Codex',
+ stdio: 'stdio',
+ streamableHttp: 'streamable-http',
+ responses: 'phản hồi',
+ defaultTargetCli: 'CLI mục tiêu mặc định',
+ executionChain: 'Chuỗi thực thi',
+ targetPath: 'Đường dẫn mục tiêu hiện tại',
+ userConfig: 'Cấu hình người dùng',
+ configYaml: 'config.yaml',
+ flow: 'Luồng',
+ docsTab: 'Tài liệu',
+ },
+ droidSettings: {
+ quickControls: 'Điều khiển nhanh',
+ reasoningControls: 'Điều khiển lý luận',
+ thinkingBudget: 'Ngân sách tư duy',
+ anthropicOnly: 'Chỉ mô hình Anthropic',
+ byokCustomModels: 'Mô hình tùy chỉnh BYOK',
+ },
+ rawJsonSettingsEditor: {
+ title: 'Trình soạn thảo cài đặt thô',
+ },
+ copilotConfigForm: {
+ copilotConfiguration: 'Cấu hình Copilot',
+ deprecatedModels: 'Phát hiện mô hình Copilot đã lỗi thời',
+ failedLoadStatus: 'Không tải được trạng thái',
+ useWithClaudeCode: 'Dùng đăng ký GitHub Copilot với Claude Code',
+ githubCopilotControls: 'GitHub Copilot kiểm soát giới hạn prompt/ngữ cảnh ở upstream.',
+ provider: 'Nhà cung cấp',
+ filePath: 'Đường dẫn tệp',
+ status: 'Trạng thái',
+ enabled: 'Đã bật',
+ disabled: 'Đã tắt',
+ loadingEditor: 'Đang tải trình soạn thảo...',
+ modelMapping: 'Ánh xạ mô hình',
+ quickUsage: 'Sử dụng nhanh',
+ noPremiumUsage: 'Không có số liệu sử dụng premium',
+ },
+ copilotPresets: {
+ gpt5Codex: 'GPT-5.3 Codex',
+ claude46: 'Claude 4.6',
+ gemini3: 'Gemini 3',
+ },
+ healthCard: {
+ allSystemsNominal: 'Tất cả hệ thống hoạt động bình thường',
+ machineChecks: 'Kiểm tra máy',
+ },
+ analyticsCards: {
+ cacheCost: 'Chi phí cache',
+ hitRate: 'Tỷ lệ trúng',
+ inputOutputRatio: 'Tỷ lệ Đầu vào/Đầu ra',
+ noCacheData: 'Không có dữ liệu cache',
+ noModelData: 'Không có dữ liệu mô hình',
+ noSessionData: 'Không có dữ liệu phiên',
+ noTokenData: 'Không có dữ liệu token',
+ totalCost: 'Tổng chi phí',
+ totalTokens: 'Tổng token',
+ usageInsights: 'Phân tích sử dụng',
+ },
+ dateRangeFilter: {
+ pickADate: 'Chọn ngày',
+ },
+ logsConfig: {
+ level: 'Mức',
+ message: 'Thông điệp',
+ source: 'Nguồn',
+ time: 'Thời gian',
+ proc: 'Tiến trình',
+ open: 'Mở',
+ run: 'Chạy',
+ refreshEntries: 'Làm mới mục',
+ },
+ logsDetailPanel: {
+ details: 'Chi tiết',
+ },
+ logsFilters: {
+ filters: 'Bộ lọc',
+ },
+ logsOverviewCards: {
+ overview: 'Tổng quan',
+ },
+ logsPageSkeleton: {
+ loadingLogs: 'Đang tải nhật ký...',
+ },
+ monitoringErrorLogs: {
+ logContent: 'Nội dung nhật ký',
+ },
+ analyticsPages: {
+ chartsGrid: 'Biểu đồ',
+ costByModel: 'Chi phí theo mô hình',
+ },
+ toasts: {
+ profileCreated: 'Đã tạo hồ sơ thành công',
+ profileUpdated: 'Đã cập nhật hồ sơ thành công',
+ profileDeleted: 'Đã xóa hồ sơ thành công',
+ orphanProfilesComplete: 'Hoàn tất đăng ký hồ sơ mồ côi',
+ profileCopied: 'Đã sao chép hồ sơ thành công',
+ profileImported: 'Đã nhập hồ sơ thành công',
+ authRequired: 'Yêu cầu xác thực {{provider}}',
+ authSuccess: 'Xác thực {{provider}} thành công!',
+ authFailed: 'Xác thực {{provider}} thất bại',
+ deviceCodeExpired: 'Mã thiết bị đã hết hạn. Vui lòng thử lại.',
+ codeCopied: 'Đã sao chép mã vào bảng nhớ tạm',
+ failedCopy: 'Không sao chép được mã',
+ configSaved: 'Đã lưu cấu hình thành công',
+ configSaveFailed: 'Lưu thất bại: {{error}}',
+ invalidYaml: 'Không thể lưu YAML không hợp lệ',
+ configUpdatedExternally: 'Cấu hình đã được cập nhật bên ngoài',
+ settingsFileUpdated: 'Tệp cài đặt đã được cập nhật',
+ accountsUpdated: 'Tài khoản đã được cập nhật',
+ noProfilesToSync: 'Không có hồ sơ để đồng bộ',
+ syncFailed: 'Đồng bộ thất bại: {{error}}',
+ providerAuthSuccess: 'Xác thực {{provider}} thành công',
+ providerDeviceCodeInCallback: 'Nhà cung cấp trả về Device Code flow trong chế độ callback',
+ loggingConfigSaved: 'Đã lưu cấu hình ghi nhật ký.',
+ loggingConfigSaveFailed: 'Không lưu được cấu hình ghi nhật ký.',
+ unifiedConfigUpdated: 'Đã cập nhật cấu hình thành công',
+ migrationPreviewComplete: 'Xem trước di chuyển hoàn tất',
+ migrationComplete: 'Di chuyển hoàn tất thành công',
+ migrationFailed: 'Di chuyển thất bại',
+ rollbackComplete: 'Khôi phục hoàn tất thành công',
+ rollbackFailed: 'Khôi phục thất bại',
+ defaultAccountSet: 'Tài khoản mặc định đã đặt thành "{{name}}"',
+ defaultAccountReset: 'Tài khoản mặc định đã đặt lại về CCS',
+ accountDeleted: 'Đã xóa tài khoản "{{name}}"',
+ contextUpdated: 'Đã cập nhật ngữ cảnh "{{name}}" thành {{summary}}',
+ legacyConfirmError:
+ 'Tài khoản "{{name}}" cần xác nhận rõ ràng. Dùng Chỉnh sửa Đồng bộ lịch sử trên tài khoản này.',
+ legacyConfirmFailed: 'Tài khoản cũ "{{name}}" xác nhận thất bại: {{error}}',
+ legacyConfirmSuccess_one: 'Đã xác nhận {{count}} tài khoản cũ',
+ legacyConfirmSuccess_other: 'Đã xác nhận {{count}} tài khoản cũ',
+ noLegacyAccounts: 'Không có tài khoản cũ nào cần xác nhận',
+ routingStrategySet: 'Chiến lược định tuyến đã đặt thành {{strategy}}',
+ variantCreated: 'Đã tạo biến thể thành công',
+ variantUpdated: 'Đã cập nhật biến thể thành công',
+ variantDeleted: 'Đã xóa biến thể thành công',
+ defaultAccountUpdated: 'Đã cập nhật tài khoản mặc định',
+ accountRemoved: 'Đã xóa tài khoản',
+ accountPaused: 'Đã tạm dừng tài khoản',
+ accountResumed: 'Đã tiếp tục tài khoản',
+ accountAdded: 'Đã thêm tài khoản cho {{provider}}',
+ kiroImported: 'Đã nhập tài khoản Kiro: {{name}}',
+ kiroTokenImported: 'Đã nhập token Kiro',
+ modelUpdated: 'Đã cập nhật mô hình',
+ presetSaved: 'Đã lưu preset "{{name}}"',
+ presetDeleted: 'Đã xóa preset',
+ cliproxyAlreadyRunning: 'CLIProxy đã đang chạy',
+ cliproxyStarted: 'CLIProxy đã khởi động thành công',
+ cliproxyStartFailed: 'Không khởi động được CLIProxy',
+ cliproxyStopped: 'Đã dừng CLIProxy',
+ cliproxyStopFailed: 'Không dừng được CLIProxy',
+ presetApplied: 'Đã áp dụng preset "{{name}}"',
+ presetAppliedCustom: 'Đã áp dụng preset tùy chỉnh',
+ settingsSavedWithAdjustments: 'Cài đặt đã lưu với điều chỉnh mô hình',
+ settingsSaved: 'Đã lưu cài đặt',
+ failedSaveSettings: 'Không lưu được cài đặt',
+ codexRefreshFailed: 'Không làm mới được snapshot Codex. Các chỉnh sửa thô đã được giữ.',
+ codexRefreshError: 'Không làm mới được snapshot Codex.',
+ codexFixToml: 'Sửa TOML trước khi lưu.',
+ codexSaved: 'Đã lưu Codex config.toml.',
+ codexChangedExternally: 'config.toml đã thay đổi bên ngoài. Làm mới và thử lại.',
+ codexSaveFailed: 'Không lưu được Codex config.toml.',
+ codexUpdateFailed: 'Không cập nhật được cấu hình Codex.',
+ noOrphanProfiles: 'Không tìm thấy cài đặt hồ sơ mồ côi',
+ profilesRegistered: 'Đã đăng ký {{count}} hồ sơ{{skipped}}',
+ destinationEmpty: 'Tên hồ sơ đích không được để trống',
+ profileExportDownloaded: 'Đã tải xuống xuất hồ sơ',
+ profileImportFailed: 'Không nhập được gói hồ sơ',
+ },
+ profileEditorSections: {
+ imageAnalysis: 'Phân tích hình ảnh',
+ loadingImageSettings: 'Đang tải cài đặt hình ảnh...',
+ skipPermissionPrompts: 'Bỏ qua nhắc quyền khi khởi chạy',
+ useNativeImageReading: 'Dùng đọc hình ảnh gốc',
+ skipTransformer: 'Bỏ qua transformer',
+ friendlyUi: 'Giao diện thân thiện',
+ info: 'Thông tin',
+ },
+ imageAnalysisStatus: {
+ sectionTitle: 'Hình ảnh',
+ openSettings: 'Mở Cài đặt',
+ useNativeImageReading: 'Dùng đọc hình ảnh gốc',
+ refreshingPreview: 'Đang làm mới xem trước',
+ savedStatus: 'Trạng thái đã lưu',
+ livePreview: 'Xem trước trực tiếp',
+ disabledGlobally: 'Đã tắt toàn cục',
+ targetBypassesHook: '{{target}} bỏ qua hook',
+ nativeImageReading: 'Đọc hình ảnh gốc',
+ setupNeeded: 'Cần thiết lập',
+ needsAuth: 'Cần xác thực',
+ needsProxy: 'Cần proxy',
+ nativeFallback: 'Dự phòng gốc',
+ transformerReady: 'Transformer sẵn sàng',
+ badgeDisabled: 'Đã tắt',
+ badgeBypassed: 'Đã bỏ qua',
+ badgeNative: 'Gốc',
+ badgeSetup: 'Thiết lập',
+ badgeAuth: 'Xác thực',
+ badgeProxy: 'Proxy',
+ badgeReady: 'Sẵn sàng',
+ capabilityVerified: 'Đã xác minh',
+ capabilityUnknown: 'Không xác định',
+ toggleSummaryNativeCapable:
+ '{{model}} có vẻ hỗ trợ hình ảnh. CCS sẽ bỏ qua transformer ở đây.',
+ toggleSummaryNativeModel: 'CCS sẽ ưu tiên đọc gốc cho {{model}}.',
+ toggleSummaryNativeDefault: 'CCS sẽ ưu tiên đọc hình ảnh gốc cho hồ sơ này.',
+ toggleSummaryNativeFileAccess: 'Hồ sơ này hiện đang dùng truy cập tệp gốc.',
+ toggleSummaryInactiveTarget:
+ 'Định tuyến hình ảnh phía Claude đã lưu không hoạt động khi {{target}} đang được chọn.',
+ toggleSummaryTransformerRoute: 'Tuyến transformer: {{backend}}{{modelSuffix}}.',
+ noteDisabledGlobally: 'Hình ảnh đã bị tắt toàn cục trong cài đặt CCS.',
+ noteTargetBypassesHook: 'Mục tiêu hiện tại {{target}} bỏ qua Claude Read hook.',
+ notePersistHook: 'Persist hook hồ sơ trước khi tuyến transformer có thể chạy ở đây.',
+ targetLabel: {
+ claude: 'Claude Code',
+ droid: 'Factory Droid',
+ codex: 'Codex CLI',
+ },
+ },
+ openrouterBadge: {
+ new: 'MỚI',
+ integration: 'Tích hợp OpenRouter',
+ },
+ openrouterBanner: {
+ accessModels: 'Truy cập {{count}}+ mô hình qua OpenRouter',
+ add: 'Thêm',
+ },
+ openrouterModelPicker: {
+ searchModels: 'Tìm kiếm mô hình',
+ newestModels: 'Mô hình mới nhất',
+ },
+ openrouterPromoCard: {
+ title: 'OpenRouter',
+ description: 'Truy cập hàng trăm mô hình qua một API endpoint.',
+ },
+ profileCard: {
+ profile: 'Hồ sơ',
+ openRouter: 'Hồ sơ OpenRouter',
+ claudeCode: 'Claude Code',
+ claudeCodeDefault: 'Claude Code (mặc định)',
+ factoryDroid: 'Factory Droid',
+ codexCli: 'Codex CLI',
+ ccsProfile: 'Hồ sơ CCS',
+ },
+ profileDeck: {
+ profiles: 'Hồ sơ',
+ failedToLoad: 'Không tải được hồ sơ: {{message}}',
+ noProfiles: 'Chưa có hồ sơ nào. Tạo hồ sơ đầu tiên để bắt đầu.',
+ },
+ profilesTable: {
+ name: 'Tên',
+ provider: 'Nhà cung cấp',
+ model: 'Mô hình',
+ target: 'Mục tiêu',
+ lastModified: 'Sửa đổi lần cuối',
+ actions: 'Hành động',
+ edit: 'Chỉnh sửa',
+ },
+ profileCreateDialog: {
+ createProfile: 'Tạo hồ sơ',
+ appliedModelToTiers: 'Đã áp dụng "{{model}}" cho tất cả tier mô hình',
+ profileCreated: 'Đã tạo hồ sơ "{{name}}"',
+ failedCreate: 'Không tạo được hồ sơ',
+ chooseProviderHint: 'Chọn preset nhà cung cấp hoặc cấu hình API endpoint tùy chỉnh.',
+ basicInformation: 'Thông tin cơ bản',
+ modelConfiguration: 'Cấu hình mô hình',
+ usedInCli: 'Dùng trong CLI:',
+ apiBaseUrl: 'URL cơ sở API',
+ baseUrlPlaceholder: 'https://api.example.com/v1',
+ prefilledFromPreset: 'Điền sẵn từ {{name}}. Bạn có thể tùy chỉnh nếu cần.',
+ optionalForPreset: 'Tùy chọn cho {{name}}. Để trống để dùng xác thực Anthropic gốc.',
+ endpointHint: 'Endpoint chấp nhận yêu cầu tương thích OpenAI và Anthropic',
+ optional: '(tùy chọn)',
+ apiKeyOptionalPlaceholder: 'Tùy chọn - chỉ khi bật xác thực',
+ apiKeyPlaceholder: 'sk-...',
+ apiKeyOptionalHint: 'Chỉ cần khi endpoint cục bộ bật xác thực',
+ defaultTargetCli: 'CLI mục tiêu mặc định',
+ modelMapping: 'Ánh xạ mô hình',
+ modelMappingDesc:
+ 'Ánh xạ các tier Claude Code (Opus/Sonnet/Haiku) sang mô hình được nhà cung cấp hỗ trợ.',
+ searchModelsPlaceholder: 'Gõ để tìm (vd: opus, sonnet, gpt-4o)...',
+ noModelsFound: 'Không tìm thấy mô hình cho "{{query}}"',
+ loadingModels: 'Đang tải mô hình...',
+ defaultModel: 'Mô hình mặc định',
+ sonnetMapping: 'Ánh xạ Sonnet',
+ opusMapping: 'Ánh xạ Opus',
+ haikuMapping: 'Ánh xạ Haiku',
+ sonnetMappingPlaceholder: 'vd: gpt-4o, claude-sonnet-4',
+ opusMappingPlaceholder: 'vd: o1, claude-opus-4.5',
+ haikuMappingPlaceholder: 'vd: gpt-4o-mini, claude-3.5-haiku',
+ free: 'Miễn phí',
+ },
+ profileDialogLegacy: {
+ editProfile: 'Chỉnh sửa hồ sơ',
+ },
+ supportEntryCard: {
+ actionRequired: 'Cần hành động',
+ },
+ settingsPage: {
+ title: 'Cài đặt',
+ loading: 'Đang tải...',
+ failedLoad: 'Không tải được cài đặt.',
+ tabs: {
+ web: 'Web',
+ env: 'Env',
+ think: 'Tư duy',
+ proxy: 'Proxy',
+ auth: 'Xác thực',
+ backup: 'Sao lưu',
+ channels: 'Kênh',
+ imageAnalysis: 'Hình ảnh',
+ },
+ websearchSection: {
+ title: 'Tìm kiếm Web',
+ description: 'Cấu hình tìm kiếm web dựa trên CLI.',
+ },
+ thinkingSection: {
+ title: 'Tư duy',
+ description: 'Cấu hình tư duy/lý luận mở rộng cho các mô hình được hỗ trợ.',
+ directOverride: 'Ghi đè trực tiếp',
+ youType: 'Bạn gõ:',
+ ccsAdds: 'CCS thêm:',
+ executionChain: 'Chuỗi thực thi',
+ primaryBackends: 'Backend chính',
+ legacyCliFallbacks: 'Fallback CLI cũ',
+ managedPayload: 'Payload được quản lý',
+ sharedTargetMetadata: 'Metadata mục tiêu dùng chung',
+ ideTargetMetadata: 'Metadata mục tiêu IDE',
+ ideSettingsPath: 'Đường dẫn cài đặt IDE',
+ ideHost: 'Host IDE',
+ resolvedBinding: 'Binding đã giải quyết',
+ bindingName: 'Tên binding',
+ inSync: 'Đồng bộ',
+ currentTargetPath: 'Đường dẫn mục tiêu hiện tại',
+ warnings: 'Cảnh báo',
+ notes: 'Ghi chú',
+ workspacePresets: 'Preset workspace',
+ draft: 'Bản nháp',
+ advanced: 'Nâng cao',
+ recommended: 'Luồng thiết lập khuyến nghị',
+ configureModelFirst: 'Cấu hình mô hình trước',
+ },
+ proxySection: {
+ title: 'Proxy',
+ loadingImageSettings: 'Đang tải cài đặt hình ảnh...',
+ },
+ channelsSection: {
+ title: 'Kênh chính thức',
+ description: 'Xem và quản lý các kênh phát hành chính thức.',
+ },
+ imageAnalysisSection: {
+ title: 'Phân tích hình ảnh',
+ description: 'Cấu hình cài đặt phân tích hình ảnh.',
+ loading: 'Đang tải cài đặt hình ảnh...',
+ },
+ },
+ codexPage: {
+ title: 'Codex',
+ controlCenter: 'Trung tâm điều khiển',
+ overview: 'Tổng quan',
+ docs: 'Tài liệu',
+ nativeRuntime: 'Runtime gốc',
+ ccsProvider: 'CCS Provider',
+ setup: 'Thiết lập',
+ },
+ apiPage: {
+ title: 'Hồ sơ API',
+ subtitle: 'Quản lý hồ sơ API và các endpoint.',
+ },
+ claudeExtensionPage: {
+ title: 'Claude Extension',
+ subtitle: 'Cài đặt tích hợp Claude Extension.',
+ claudeAuth: 'Xác thực Claude',
+ status: 'Trạng thái',
+ targetMetadata: 'Metadata mục tiêu IDE',
+ },
+ sharedPageV2: {
+ title: 'Dùng chung',
+ subtitle: 'Quản lý dữ liệu dùng chung.',
+ },
+ homePageV2: {
+ title: 'Trang chủ',
+ logsMoved: 'Nhật ký đã chuyển sang workspace riêng',
+ profiles: 'Hồ sơ',
+ cliproxy: 'CLIProxy',
+ accounts: 'Tài khoản',
+ health: 'Sức khỏe',
+ },
+ analyticsPageV2: {
+ title: 'Phân tích',
+ subtitle: 'Phân tích sử dụng và thông tin chi tiết.',
+ },
+ logsPageV2: {
+ title: 'Nhật ký',
+ subtitle: 'Xem và quản lý nhật ký hệ thống.',
+ },
+ healthPageV2: {
+ title: 'Sức khỏe',
+ subtitle: 'Giám sát sức khỏe hệ thống.',
+ },
+ aiProvidersPage: {
+ title: 'AI Providers',
+ subtitle: 'Quản lý cấu hình AI providers.',
+ unableToLoad: 'Không thể tải AI Providers',
+ },
},
},
ja: {
@@ -4253,6 +6864,7 @@ const resources = {
factoryDroid: 'Factory Droid',
system: 'システム',
health: 'ヘルス',
+ logs: 'ログ',
settings: '設定',
openrouterTooltip: '注目: OpenRouter + Alibaba Coding Plan + Ollama',
},
@@ -4391,6 +7003,7 @@ const resources = {
cancel: 'キャンセル',
savePreset: 'プリセットを保存',
applyPreset: 'プリセットを適用',
+ deletePreset: 'プリセットを削除',
},
componentModelSelector: {
selectModel: 'モデルを選択',
@@ -4432,6 +7045,13 @@ const resources = {
recommended: '推奨',
allModelsCount: 'すべてのモデル ({{count}})',
noModelsAvailable: '利用可能なモデルはありません',
+ shadowed: 'シャドウ済み',
+ prefixOnly: 'プレフィックスのみ',
+ current: '現在',
+ currentValue: '現在の値',
+ preferredPinnedModel: '優先ピンモデル:',
+ pinnedRouteStatus: 'ピンルートの状態:',
+ pinnedModelNotAdvertised: 'ピンモデルは現在プロキシから通知されていません: {{model}}',
},
createAuthProfileDialog: {
title: '新しいアカウントを作成',
@@ -5081,6 +7701,8 @@ const resources = {
},
settingsTabs: {
web: 'Web検索',
+ image: '画像',
+ channels: 'チャンネル',
env: '環境変数',
think: '思考',
proxy: 'プロキシ',
@@ -5672,6 +8294,870 @@ const resources = {
retryContent: '再読み込み',
noMarkdown: 'Markdownコンテンツはありません。',
},
+
+ accountCardStats: {
+ notUsedYet: '未使用',
+ },
+ accountQuotaPanel: {
+ weekly: '週間',
+ loadingQuota: 'クォータを読み込み中...',
+ },
+ accountSurfaceCard: {
+ business: 'ビジネス',
+ personal: '個人',
+ variant: 'バリアント',
+ },
+ aiProvidersEntryCard: {
+ apiKeys: 'API Keys',
+ },
+ aiProvidersEntryDialog: {
+ connectorName: 'コネクタ名',
+ baseUri: 'ベース URL',
+ proxyUrl: 'プロキシ URL',
+ secret: 'シークレット',
+ prefix: 'プレフィックス',
+ excludedModels: '除外モデル',
+ headers: 'ヘッダー',
+ modelMappings: 'モデルマッピング',
+ requiredSetup: '必要なセットアップ',
+ optionalRouting: 'オプションのルーティング',
+ },
+ aiProvidersFamilyRail: {
+ current: '現在',
+ },
+ aiProvidersPage: {
+ title: 'AI プロバイダー',
+ subtitle: 'AI プロバイダーの設定を管理します。',
+ unableToLoad: 'AI プロバイダーを読み込めませんでした',
+ },
+ analyticsCards: {
+ cacheCost: 'キャッシュコスト',
+ hitRate: 'ヒット率',
+ inputOutputRatio: '入力/出力比',
+ noCacheData: 'キャッシュデータがありません',
+ noModelData: 'モデルデータがありません',
+ noSessionData: 'セッションデータがありません',
+ noTokenData: 'トークンデータがありません',
+ totalCost: '合計コスト',
+ totalTokens: '合計トークン',
+ usageInsights: '利用インサイト',
+ },
+ analyticsPageV2: {
+ title: '分析',
+ subtitle: '利用分析とインサイト。',
+ },
+ analyticsPages: {
+ chartsGrid: 'チャート',
+ costByModel: 'モデル別コスト',
+ },
+ apiPage: {
+ title: 'API プロファイル',
+ subtitle: 'API プロファイルとエンドポイントを管理します。',
+ },
+ authMonitorLive: {
+ live: 'ライブ',
+ accountMonitor: 'アカウントモニター',
+ updated: '{{time}} に更新',
+ updatedNow: 'たった今更新',
+ requestsLabel: '件',
+ stats: '統計',
+ successRate: '成功率',
+ missingProjectId: 'プロジェクト ID がありません',
+ noActivity: 'アクティビティなし',
+ },
+ bulkActionBar: {
+ applyPreset: 'プリセットを適用',
+ },
+ ccsLogo: {
+ alt: 'CCS ロゴ',
+ text: 'CCS Config',
+ },
+ claudeExtensionPage: {
+ title: 'Claude Extension',
+ subtitle: 'Claude Extension の連携設定。',
+ claudeAuth: 'Claude 認証',
+ status: 'ステータス',
+ targetMetadata: 'IDE ターゲットメタデータ',
+ },
+ claudekitBadge: {
+ title: 'Powered by ClaudeKit Framework',
+ alt: 'ClaudeKit',
+ poweredBy: 'Powered by',
+ claudekit: 'ClaudeKit',
+ },
+ cliproxyConfig: {
+ unsavedChanges: '未保存の変更',
+ original: '変更前',
+ modified: '変更後',
+ reviewChanges: '変更を確認',
+ loadingEditor: 'エディターを読み込み中...',
+ },
+ cliproxyHeader: {
+ ccsLevelAccountManagement: 'CCS レベルのアカウント管理',
+ cliproxyNotAvailable: 'CLIProxy は利用できません',
+ cliproxyControlPanel: 'CLIProxy コントロールパネル',
+ noVariants: 'CLIProxy バリアントが見つかりません。',
+ addAccountToStart: 'アカウントを追加して開始',
+ },
+ cliproxyStatsOverview: {
+ sessionStatistics: 'セッション統計',
+ realTimeMetrics: '{{backend}} からのリアルタイム利用指標',
+ offline: 'オフライン',
+ running: '稼働中',
+ noActiveSession: 'アクティブなセッションなし',
+ noActiveSessionHint:
+ 'リアルタイム統計を表示するには、ccs gemini、ccs codex、または ccs agy で CLIProxy セッションを開始してください。',
+ failedLoadStats: '統計の読み込みに失敗しました',
+ totalRequests: '総リクエスト数',
+ successCount: '{{count}} 件成功',
+ successRate: '成功率',
+ totalTokens: '総トークン数',
+ estimatedCost: '推定約 ${{cost}}',
+ modelsUsed: '使用モデル',
+ modelUsageDistribution: 'モデル別利用分布',
+ requestCount: '{{count}} リクエスト',
+ },
+ cliproxyTable: {
+ name: '名前',
+ provider: 'プロバイダー',
+ model: 'モデル',
+ account: 'アカウント',
+ status: 'ステータス',
+ default: 'デフォルト',
+ actions: '操作',
+ },
+ cliproxyTabs: {
+ overview: '概要',
+ variants: 'バリアント',
+ aiProviders: 'AI プロバイダー',
+ controlPanel: 'コントロールパネル',
+ },
+ codeEditor: {
+ revealSensitive: '機密値を表示',
+ maskSensitive: '機密値を隠す',
+ valid: '有効な {{language}}',
+ readOnly: '(読み取り専用)',
+ },
+ codex: {
+ controlCenter: 'コントロールセンター',
+ overview: '概要',
+ docs: 'ドキュメント',
+ nativeCodexRuntime: 'ネイティブ Codex ランタイム',
+ ccsCodexProvider: 'CCS Codex プロバイダー / ブリッジ',
+ codexDocs: 'Codex ドキュメント',
+ supportedFlows: '対応フロー',
+ twoSupportedPaths: '2つのサポートパス:',
+ nativeLabel: 'ネイティブ:',
+ nativeDesc: 'Codex は CCS v1 でファーストクラスのランタイム専用ターゲットです。',
+ ccsBridge: 'CCS Bridge',
+ apiProfilesDefault: 'API プロファイルのデフォルトは引き続き Claude または Droid です。',
+ recommendedSetupFlow: '推奨セットアップフロー',
+ fastestPath: '最速パス',
+ officialChannels: '公式チャンネル',
+ codexCli: 'Codex CLI',
+ openNativeCodex: 'ネイティブ Codex を開く',
+ runBuiltInCodex: '内蔵 Codex で Codex を実行',
+ runBuiltInCodexExplicit: '内蔵 Codex で Codex を実行(明示)',
+ openCodexDashboard: 'Codex ダッシュボードを開く',
+ status: 'ステータス',
+ profiles: 'プロファイル',
+ createNewProfile: '新しいプロファイルを作成',
+ createNewProvider: '新しいプロバイダーを作成',
+ createNewMcpServer: '新しい MCP サーバーを作成',
+ defaultProvider: 'デフォルトプロバイダー',
+ useDefault: 'デフォルトを使用',
+ useGlobalProvider: 'グローバルプロバイダーを使用',
+ useProviderDefault: 'プロバイダーのデフォルトを使用',
+ quickFillWarning: 'クイック入力のみです。保存前に確認してください。',
+ thisFileUpstreamOwned: 'このファイルは Codex CLI の上流管理対象です。',
+ notes: 'メモ',
+ approvalPolicy: '承認ポリシー',
+ sandboxMode: 'サンドボックスモード',
+ reasoningEffort: '推論強度',
+ useGlobalEffort: 'グローバル設定を使用',
+ reasoningEffortCapitalized: '推論強度',
+ thinkingBudgetTokens: '思考予算トークン',
+ modelContextWindow: 'モデルコンテキストウィンドウ',
+ autoCompactTokenLimit: '自動圧縮トークン上限',
+ toolOutputTokenLimit: 'ツール出力トークン上限',
+ webSearch: 'Web 検索',
+ personality: 'パーソナリティ',
+ model: 'モデル',
+ rawOnly: 'Raw のみ',
+ trusted: '信頼済み',
+ untrusted: '未信頼',
+ noProjectTrustEntries: '明示的なプロジェクト信頼エントリはありません。',
+ codexNativeRecipe: 'ネイティブ Codex レシピを保存しました',
+ gptContextCap: 'GPT-5.4 コンテキスト上限',
+ usageLimitCost: '272K 超過時の利用制限コスト',
+ longContextOverride: '長文コンテキストオーバーライド',
+ counts2x: '2倍カウント',
+ normalUsageWindow: '通常利用ウィンドウ',
+ useCodexDefault: 'Codex のデフォルトを使用',
+ stdio: 'stdio',
+ streamableHttp: 'streamable-http',
+ responses: 'responses',
+ defaultTargetCli: 'デフォルトターゲット CLI',
+ executionChain: '実行チェーン',
+ targetPath: '現在のターゲットパス',
+ userConfig: 'ユーザー設定',
+ configYaml: 'config.yaml',
+ flow: 'フロー',
+ docsTab: 'ドキュメント',
+ },
+ codexPage: {
+ title: 'Codex',
+ controlCenter: 'コントロールセンター',
+ overview: '概要',
+ docs: 'ドキュメント',
+ nativeRuntime: 'ネイティブランタイム',
+ ccsProvider: 'CCS プロバイダー',
+ setup: 'セットアップ',
+ },
+ commandBuilder: {
+ title: 'コマンドビルダー',
+ searchPlaceholder: 'コマンドを入力または選択...',
+ copy: 'コピー',
+ run: '実行',
+ cmdConfig: '設定画面を開く',
+ cmdCreateProfile: '新しいプロファイルを作成',
+ cmdSwitchProfile: 'プロファイルを切り替え',
+ cmdDoctor: 'システムヘルスチェック',
+ cmdListProviders: '利用可能な CLIProxy プロバイダーを一覧表示',
+ cmdAddProvider: 'CLIProxy プロバイダーを追加',
+ },
+ confirmDialog: {
+ confirm: '確認',
+ cancel: 'キャンセル',
+ },
+ connectionIndicator: {
+ connected: '接続済み',
+ connecting: '接続中...',
+ disconnected: '切断',
+ reconnecting: '再接続中...',
+ },
+ copilotConfigForm: {
+ copilotConfiguration: 'Copilot 設定',
+ deprecatedModels: '非推奨の Copilot モデルが検出されました',
+ failedLoadStatus: 'ステータスの読み込みに失敗しました',
+ useWithClaudeCode: 'GitHub Copilot サブスクリプションを Claude Code で利用する',
+ githubCopilotControls:
+ 'GitHub Copilot はプロンプト/コンテキストの制限を上流で管理しています。',
+ provider: 'プロバイダー',
+ filePath: 'ファイルパス',
+ status: 'ステータス',
+ enabled: '有効',
+ disabled: '無効',
+ loadingEditor: 'エディターを読み込み中...',
+ modelMapping: 'モデルマッピング',
+ quickUsage: 'クイック実行',
+ noPremiumUsage: 'プレミアム利用回数なし',
+ },
+ copilotPresets: {
+ gpt5Codex: 'GPT-5.3 Codex',
+ claude46: 'Claude 4.6',
+ gemini3: 'Gemini 3',
+ },
+ dateRangeFilter: {
+ pickADate: '日付を選択',
+ },
+ deviceCodeDialog: {
+ authorize: '{{provider}} を認証',
+ enterCodeAtPage: '認証ページで下のコードを入力してください。',
+ expiresIn: '({{time}} 後に期限切れ)',
+ codeExpired: '(コードの有効期限が切れました)',
+ copied: 'コピーしました!',
+ copyCode: 'コードをコピー',
+ waitingForAuth: '認証を待機中...',
+ openVerificationPage: '認証ページを開く',
+ openProviderPage: '{{provider}} を開く',
+ copyCodeAria: '確認コードをコピー',
+ codeCopiedAria: 'コードをコピーしました',
+ },
+ docsLink: {
+ title: 'ドキュメントを表示',
+ },
+ droidSettings: {
+ quickControls: 'クイックコントロール',
+ reasoningControls: '推論コントロール',
+ thinkingBudget: '思考予算',
+ anthropicOnly: 'Anthropic モデルのみ',
+ byokCustomModels: 'BYOK カスタムモデル',
+ },
+ extendedContext: {
+ extendedContext: '拡張コンテキスト',
+ },
+ githubLink: {
+ title: 'GitHub で問題を報告',
+ },
+ globalEnvIndicator: {
+ injectedCount_one: '{{count}} 件のグローバル環境変数が実行時に注入されます',
+ injectedCount_other: '{{count}} 件のグローバル環境変数が実行時に注入されます',
+ overriddenCount: '({{count}} 件はプロファイルで上書き)',
+ skippedLabel: 'スキップ(プロファイルで既に定義済み):',
+ configureInSettings: '設定で構成',
+ },
+ healthCard: {
+ allSystemsNominal: 'すべてのシステムが正常です',
+ machineChecks: 'マシンチェック',
+ },
+ healthPageV2: {
+ title: 'ヘルス',
+ subtitle: 'システムヘルスの監視。',
+ },
+ heroSection: {
+ title: 'CCS Config',
+ subtitle: 'Claude Code Switch Dashboard',
+ },
+ homePageV2: {
+ title: 'ホーム',
+ logsMoved: 'ログは専用ワークスペースに移動しました',
+ profiles: 'プロファイル',
+ cliproxy: 'CLIProxy',
+ accounts: 'アカウント',
+ health: 'ヘルス',
+ },
+ hubFooter: {
+ logs: 'ログ',
+ settings: '設定',
+ github: 'GitHub',
+ copyright: '\u00a9 {{year}} kaitranntt',
+ },
+ imageAnalysisStatus: {
+ sectionTitle: '画像',
+ openSettings: '設定を開く',
+ useNativeImageReading: 'ネイティブ画像読み取りを使用',
+ refreshingPreview: 'プレビューを更新中',
+ savedStatus: '保存済みステータス',
+ livePreview: 'ライブプレビュー',
+ disabledGlobally: 'グローバルで無効',
+ targetBypassesHook: '{{target}} はフックをバイパスします',
+ nativeImageReading: 'ネイティブ画像読み取り',
+ setupNeeded: 'セットアップが必要',
+ needsAuth: '認証が必要',
+ needsProxy: 'プロキシが必要',
+ nativeFallback: 'ネイティブフォールバック',
+ transformerReady: 'トランスフォーマー準備完了',
+ badgeDisabled: '無効',
+ badgeBypassed: 'バイパス中',
+ badgeNative: 'ネイティブ',
+ badgeSetup: 'セットアップ',
+ badgeAuth: '認証',
+ badgeProxy: 'プロキシ',
+ badgeReady: '準備完了',
+ capabilityVerified: '検証済み',
+ capabilityUnknown: '不明',
+ toggleSummaryNativeCapable:
+ '{{model}} は画像対応のようです。CCS はここではトランスフォーマーをバイパスします。',
+ toggleSummaryNativeModel: 'CCS は {{model}} でネイティブ読み取りを優先します。',
+ toggleSummaryNativeDefault: 'CCS はこのプロファイルでネイティブ画像読み取りを優先します。',
+ toggleSummaryNativeFileAccess:
+ 'このプロファイルは現在ネイティブファイルアクセスのままです。',
+ toggleSummaryInactiveTarget:
+ '{{target}} が選択されている間は、保存済みの Claude 側画像ルーティングは無効です。',
+ toggleSummaryTransformerRoute: 'トランスフォーマールート: {{backend}}{{modelSuffix}}。',
+ noteDisabledGlobally: '画像は CCS 設定でグローバルに無効になっています。',
+ noteTargetBypassesHook:
+ '現在のターゲット {{target}} は Claude Read フックをバイパスします。',
+ notePersistHook:
+ 'トランスフォーマールーティングをここで有効にする前に、プロファイルフックを保存してください。',
+ targetLabel: {
+ claude: 'Claude Code',
+ droid: 'Factory Droid',
+ codex: 'Codex CLI',
+ },
+ },
+ localhostDisclaimer: {
+ remoteReadonlyAuthDisabledLong:
+ 'ホストでダッシュボード認証が無効になっているため、リモートダッシュボードは読み取り専用です。リモートでの変更を有効にするには、ホスト側でダッシュボード認証を再度有効にしてください。',
+ remoteReadonlyAuthDisabledShort:
+ 'ホストでダッシュボード認証が再有効化されるまで、リモートダッシュボードは読み取り専用です。',
+ remoteReadonlySetupLong:
+ 'ホストで ccs config auth setup を実行するまで、リモートダッシュボードは読み取り専用です。',
+ remoteReadonlySetupShort:
+ 'ホストの認証が設定されるまで、リモートダッシュボードは読み取り専用です。',
+ localLong:
+ 'このダッシュボードはローカルで動作しています。データはすべてお使いのマシンに残ります。',
+ localShort: 'ローカルダッシュボード - データはお使いのデバイスに保存されます。',
+ dismiss: '免責事項を閉じる',
+ },
+ loginPage: {
+ showPassword: 'パスワードを表示',
+ hidePassword: 'パスワードを隠す',
+ },
+ logsConfig: {
+ level: 'レベル',
+ message: 'メッセージ',
+ source: 'ソース',
+ time: '時刻',
+ proc: 'プロセス',
+ open: '開く',
+ run: '実行',
+ refreshEntries: 'エントリを更新',
+ },
+ logsDetailPanel: {
+ details: '詳細',
+ },
+ logsFilters: {
+ filters: 'フィルター',
+ },
+ logsOverviewCards: {
+ overview: '概要',
+ },
+ logsPageSkeleton: {
+ loadingLogs: 'ログを読み込み中...',
+ },
+ logsPageV2: {
+ title: 'ログ',
+ subtitle: 'システムログの表示と管理。',
+ },
+ modelConfigSection: {
+ defaultModel: 'デフォルトモデル',
+ },
+ monitoringErrorLogs: {
+ logContent: 'ログ内容',
+ },
+ openrouterBadge: {
+ new: 'NEW',
+ integration: 'OpenRouter 連携',
+ },
+ openrouterBanner: {
+ accessModels: 'OpenRouter で {{count}}+ のモデルにアクセス',
+ add: '追加',
+ },
+ openrouterModelPicker: {
+ searchModels: 'モデルを検索',
+ newestModels: '最新モデル',
+ },
+ openrouterPromoCard: {
+ title: 'OpenRouter',
+ description: '1つの API エンドポイントで数百のモデルにアクセス。',
+ },
+ privacyToggle: {
+ modeOn: 'プライバシーモード ON - クリックしてデータを表示',
+ modeOff: 'プライバシーモード OFF - クリックしてデータを隠す',
+ },
+ profileCard: {
+ profile: 'プロファイル',
+ openRouter: 'OpenRouter プロファイル',
+ claudeCode: 'Claude Code',
+ claudeCodeDefault: 'Claude Code(デフォルト)',
+ factoryDroid: 'Factory Droid',
+ codexCli: 'Codex CLI',
+ ccsProfile: 'CCS プロファイル',
+ },
+ profileCreateDialog: {
+ createProfile: 'プロファイルを作成',
+ appliedModelToTiers: 'すべてのモデルティアに「{{model}}」を適用しました',
+ profileCreated: 'プロファイル「{{name}}」を作成しました',
+ failedCreate: 'プロファイルの作成に失敗しました',
+ chooseProviderHint:
+ 'プロバイダーのプリセットを選ぶか、カスタム API エンドポイントを設定してください。',
+ basicInformation: '基本情報',
+ modelConfiguration: 'モデル設定',
+ usedInCli: 'CLI での使用:',
+ apiBaseUrl: 'API ベース URL',
+ baseUrlPlaceholder: 'https://api.example.com/v1',
+ prefilledFromPreset: '{{name}} から自動入力されています。必要に応じて変更できます。',
+ optionalForPreset:
+ '{{name}} では任意です。空欄の場合はネイティブの Anthropic 認証を使用します。',
+ endpointHint: 'OpenAI 互換および Anthropic リクエストを受け付けるエンドポイント',
+ optional: '(任意)',
+ apiKeyOptionalPlaceholder: '任意 - 認証が有効な場合のみ',
+ apiKeyPlaceholder: 'sk-...',
+ apiKeyOptionalHint: 'ローカルエンドポイントで認証が有効な場合のみ必要です',
+ defaultTargetCli: 'デフォルトターゲット CLI',
+ modelMapping: 'モデルマッピング',
+ modelMappingDesc:
+ 'Claude Code のティア(Opus/Sonnet/Haiku)をプロバイダー対応モデルにマッピングします。',
+ searchModelsPlaceholder: '検索(例: opus, sonnet, gpt-4o)...',
+ noModelsFound: '「{{query}}」に一致するモデルが見つかりません',
+ loadingModels: 'モデルを読み込み中...',
+ defaultModel: 'デフォルトモデル',
+ sonnetMapping: 'Sonnet マッピング',
+ opusMapping: 'Opus マッピング',
+ haikuMapping: 'Haiku マッピング',
+ sonnetMappingPlaceholder: '例: gpt-4o, claude-sonnet-4',
+ opusMappingPlaceholder: '例: o1, claude-opus-4.5',
+ haikuMappingPlaceholder: '例: gpt-4o-mini, claude-3.5-haiku',
+ free: '無料',
+ },
+ profileDeck: {
+ profiles: 'プロファイル',
+ failedToLoad: 'プロファイルの読み込みに失敗しました: {{message}}',
+ noProfiles: 'プロファイルが設定されていません。最初のプロファイルを作成して始めましょう。',
+ },
+ profileDialogLegacy: {
+ editProfile: 'プロファイルを編集',
+ },
+ profileEditorSections: {
+ imageAnalysis: '画像分析',
+ loadingImageSettings: '画像設定を読み込み中...',
+ skipPermissionPrompts: '起動時に権限プロンプトをスキップ',
+ useNativeImageReading: 'ネイティブ画像読み取りを使用',
+ skipTransformer: 'トランスフォーマーをスキップ',
+ friendlyUi: 'フレンドリー UI',
+ info: '情報',
+ },
+ profilesTable: {
+ name: '名前',
+ provider: 'プロバイダー',
+ model: 'モデル',
+ target: 'ターゲット',
+ lastModified: '最終更新',
+ actions: '操作',
+ edit: '編集',
+ },
+ projectSelectionDialog: {
+ title: 'Google Cloud プロジェクトを選択',
+ description: '{{provider}} の認証に使用するプロジェクトを選択してください。',
+ autoSelectCountdown: '({{count}}秒後にデフォルトを自動選択)',
+ default: 'デフォルト',
+ allProjects: '全プロジェクト',
+ allProjectsDescription: 'リスト内の {{count}} プロジェクトをすべてオンボード',
+ useDefault: 'デフォルトを使用',
+ selecting: '選択中...',
+ confirmSelection: '選択を確認',
+ codeCopied: 'コードをコピーしました',
+ copyVerificationCode: '確認コードをコピー',
+ },
+ providerCard: {
+ missingProjectIdAria: 'プロジェクト ID がありません',
+ },
+ providerEditor: {
+ provider: 'プロバイダー',
+ filePath: 'ファイルパス',
+ lastModified: '最終更新',
+ defaultTarget: 'デフォルトターゲット',
+ quickUsage: 'クイック実行',
+ modelMapping: 'モデルマッピング',
+ status: 'ステータス',
+ loadingSettings: '設定を読み込み中...',
+ loadingEditor: 'エディターを読み込み中...',
+ noAccountsConnected: '接続されたアカウントなし',
+ addAccountToStart: 'アカウントを追加して開始',
+ gcpProjectIdReadonly: 'GCP プロジェクト ID(読み取り専用)',
+ projectIdNA: 'プロジェクト ID: N/A',
+ missingProjectId: 'プロジェクト ID がありません',
+ missingProjectIdHint:
+ 'エラーの原因になる可能性があります。アカウントを削除して再追加し、プロジェクト ID を取得してください。',
+ useIncognito: 'シークレットモードを使用',
+ aliases: 'エイリアス',
+ current: '現在',
+ currentValue: '現在の値',
+ composite: '複合',
+ defaultLabel: 'デフォルト',
+ requiredSetup: '必要なセットアップ',
+ connectorName: 'コネクタ名',
+ proxyUrl: 'プロキシ URL',
+ proxyUrlSet: 'プロキシ URL 設定済み',
+ excludedModels: '除外モデル',
+ headers: 'ヘッダー',
+ secret: 'シークレット',
+ prefix: 'プレフィックス',
+ modelMappings: 'モデルマッピング',
+ baseUri: 'ベース URL',
+ apiKeys: 'API Keys',
+ presets: '事前設定済みのモデルマッピングを適用',
+ createVariant: 'CLIProxy バリアントを作成',
+ agyDenylist:
+ 'Antigravity ブロックリスト: Claude Opus 4.5 と Claude Sonnet 4.5 は非推奨です。',
+ },
+ providerEditorAccountItem: {
+ modelsUsed: '使用モデル',
+ },
+ providerEditorHeader: {
+ connectorName: 'コネクタ名',
+ },
+ quickCommands: {
+ title: 'クイックコマンド',
+ startDefault: 'デフォルトで起動',
+ startDefaultDesc: 'デフォルトプロファイルで Claude を起動',
+ glmProfile: 'GLM プロファイル',
+ glmProfileDesc: 'GLM モデルに切り替え',
+ healthCheck: 'ヘルスチェック',
+ healthCheckDesc: 'システム診断を実行',
+ delegateTask: 'タスクを委譲',
+ delegateTaskDesc: 'GLM プロファイルに委譲',
+ },
+ quotaTooltip: {
+ loadingQuota: 'クォータを読み込み中...',
+ failedLoadQuota: 'クォータの読み込みに失敗しました',
+ modelQuotas: 'モデルクォータ:',
+ rateLimits: 'レート制限:',
+ plan: 'プラン: {{plan}}',
+ quotaSnapshots: 'クォータスナップショット:',
+ unlimited: '無制限',
+ remaining: '残り {{remaining}}/{{entitlement}}',
+ tier: 'ティア',
+ tierId: 'ティア ID',
+ state: '状態',
+ credits: 'クレジット',
+ modelQuotasLower: 'モデルクォータ:',
+ allBucketsReport: '全バケットが {{tokenType}} を報告',
+ requestsRemaining: '残り {{count}} リクエスト',
+ inputTokensRemaining: '残り {{count}} 入力トークン',
+ outputTokensRemaining: '残り {{count}} 出力トークン',
+ amountRemaining: '残り {{count}}',
+ fiveHourLimit: '5時間利用制限',
+ weeklyLimit: '週間利用制限',
+ weeklyOpus: '週間利用(Opus)',
+ weeklySonnet: '週間利用(Sonnet)',
+ weeklyOAuthApps: '週間利用(OAuth アプリ)',
+ weeklyCowork: '週間利用(Cowork)',
+ extraUsage: '追加利用',
+ premiumInteractions: 'プレミアムインタラクション',
+ chat: 'チャット',
+ completions: '補完',
+ resets: '{{time}} にリセット',
+ fiveHourResets: '5時間リセット {{time}}',
+ weeklyResets: '週間リセット {{time}}',
+ },
+ rawEditorSection: {
+ rawConfig: 'Raw 設定',
+ },
+ rawJsonSettingsEditor: {
+ title: 'Raw 設定エディター',
+ },
+ routingGuidance: {
+ roundRobin: 'ラウンドロビンで利用を分散します。',
+ fillFirst: 'Fill first は、バックアップアカウントが必要になるまで待機させます。',
+ routingStrategy: 'ルーティング戦略',
+ optionalRouting: 'オプションのルーティング',
+ },
+ settingsDialog: {
+ editProfile: 'プロファイルを編集: {{name}}',
+ description: 'このプロファイルの環境変数と設定を構成します。',
+ loadingSettings: '設定を読み込み中...',
+ envTab: '環境変数',
+ rawJsonTab: 'Raw JSON',
+ generalTab: '全般',
+ noEnvVars: '環境変数は設定されていません。',
+ noEnvVarsHint: 'settings.json ファイルで変数を追加してください。',
+ loadingEditor: 'エディターを読み込み中...',
+ profileInfo: 'プロファイル情報',
+ profileInfoDesc: 'この設定ファイルの詳細情報。',
+ path: 'パス',
+ lastModified: '最終更新',
+ cancel: 'キャンセル',
+ saving: '保存中...',
+ saveChanges: '変更を保存',
+ conflictTitle: 'ファイルが外部で変更されました',
+ conflictDesc:
+ 'この設定ファイルは別のプロセスで変更されました。変更で上書きしますか?それとも破棄しますか?',
+ overwrite: '上書き',
+ },
+ settingsPage: {
+ title: '設定',
+ loading: '読み込み中...',
+ failedLoad: '設定の読み込みに失敗しました。',
+ tabs: {
+ web: 'Web',
+ env: '環境変数',
+ think: '思考',
+ proxy: 'プロキシ',
+ auth: '認証',
+ backup: 'バックアップ',
+ channels: 'チャンネル',
+ imageAnalysis: '画像',
+ },
+ websearchSection: {
+ title: 'Web 検索',
+ description: 'CLI ベースの Web 検索設定。',
+ },
+ thinkingSection: {
+ title: '思考',
+ description: '対応モデルの高度な思考 / 推論設定。',
+ directOverride: '直接上書き',
+ youType: '入力:',
+ ccsAdds: 'CCS が追加:',
+ executionChain: '実行チェーン',
+ primaryBackends: 'プライマリバックエンド',
+ legacyCliFallbacks: 'レガシー CLI フォールバック',
+ managedPayload: '管理対象ペイロード',
+ sharedTargetMetadata: '共有ターゲットメタデータ',
+ ideTargetMetadata: 'IDE ターゲットメタデータ',
+ ideSettingsPath: 'IDE 設定パス',
+ ideHost: 'IDE ホスト',
+ resolvedBinding: '解決済みバインディング',
+ bindingName: 'バインディング名',
+ inSync: '同期済み',
+ currentTargetPath: '現在のターゲットパス',
+ warnings: '警告',
+ notes: 'メモ',
+ workspacePresets: 'ワークスペースプリセット',
+ draft: '下書き',
+ advanced: '詳細設定',
+ recommended: '推奨セットアップフロー',
+ configureModelFirst: '先にモデルを設定してください',
+ },
+ proxySection: {
+ title: 'プロキシ',
+ loadingImageSettings: '画像設定を読み込み中...',
+ },
+ channelsSection: {
+ title: '公式チャンネル',
+ description: '公式リリースチャンネルを表示・管理。',
+ },
+ imageAnalysisSection: {
+ title: '画像分析',
+ description: '画像分析の設定。',
+ loading: '画像設定を読み込み中...',
+ },
+ },
+ setupWizard: {
+ title: 'クイックセットアップウィザード',
+ stepProviderDesc: 'プロバイダーを選択して開始',
+ stepAuthDesc: 'プロバイダーで認証',
+ stepAccountDesc: '使用するアカウントを選択',
+ stepVariantDesc: 'カスタムバリアントを作成',
+ stepSuccessDesc: 'セットアップ完了!',
+ authStep: {
+ authenticateWith: '{{provider}} で認証してアカウントを追加',
+ authenticating: '認証中...',
+ authenticateInBrowser: 'ブラウザーで認証',
+ completeOAuth: 'ブラウザーで OAuth フローを完了してください...',
+ orUseTerminal: 'またはターミナルを使用',
+ runCommandHint: 'ターミナルで次のコマンドを実行:',
+ back: '戻る',
+ checking: '確認中...',
+ refreshStatus: 'ステータスを更新',
+ },
+ accountStep: {
+ selectAccount: 'アカウントを選択({{count}})',
+ defaultAccount: 'デフォルトアカウント',
+ or: 'または',
+ addNewAccount: '新しいアカウントを追加',
+ addNewAccountDesc: '別のアカウントで認証',
+ back: '戻る',
+ },
+ variantStep: {
+ back: '戻る',
+ skip: 'スキップ',
+ },
+ successStep: {
+ title: 'バリアントを作成しました!',
+ subtitle: 'カスタムバリアントが使用可能です',
+ usage: '使用方法:',
+ done: '完了',
+ },
+ },
+ sharedPageV2: {
+ title: '共有',
+ subtitle: '共有データ管理。',
+ },
+ sponsorButton: {
+ title: 'GitHub でこのプロジェクトをスポンサー',
+ sponsor: 'スポンサー',
+ },
+ supportEntryCard: {
+ actionRequired: '対応が必要',
+ },
+ themeToggle: {
+ srLabel: 'テーマを切り替え',
+ },
+ toasts: {
+ profileCreated: 'プロファイルを作成しました',
+ profileUpdated: 'プロファイルを更新しました',
+ profileDeleted: 'プロファイルを削除しました',
+ orphanProfilesComplete: '孤立プロファイルの登録が完了しました',
+ profileCopied: 'プロファイルをコピーしました',
+ profileImported: 'プロファイルをインポートしました',
+ authRequired: '{{provider}} の認証が必要です',
+ authSuccess: '{{provider}} の認証に成功しました!',
+ authFailed: '{{provider}} の認証に失敗しました',
+ deviceCodeExpired: 'デバイスコードの有効期限が切れました。もう一度お試しください。',
+ codeCopied: 'コードをクリップボードにコピーしました',
+ failedCopy: 'コードのコピーに失敗しました',
+ configSaved: '設定を保存しました',
+ configSaveFailed: '保存に失敗しました: {{error}}',
+ invalidYaml: '無効な YAML は保存できません',
+ configUpdatedExternally: '設定が外部で更新されました',
+ settingsFileUpdated: '設定ファイルが更新されました',
+ accountsUpdated: 'アカウントを更新しました',
+ noProfilesToSync: '同期するプロファイルがありません',
+ syncFailed: '同期に失敗しました: {{error}}',
+ providerAuthSuccess: '{{provider}} の認証に成功しました',
+ providerDeviceCodeInCallback:
+ 'コールバックモードでプロバイダーがデバイスコードフローを返しました',
+ loggingConfigSaved: 'ログ設定を保存しました。',
+ loggingConfigSaveFailed: 'ログ設定の保存に失敗しました。',
+ unifiedConfigUpdated: '設定を更新しました',
+ migrationPreviewComplete: '移行プレビューが完了しました',
+ migrationComplete: '移行が完了しました',
+ migrationFailed: '移行に失敗しました',
+ rollbackComplete: 'ロールバックが完了しました',
+ rollbackFailed: 'ロールバックに失敗しました',
+ defaultAccountSet: 'デフォルトアカウントを「{{name}}」に設定しました',
+ defaultAccountReset: 'デフォルトアカウントを CCS に戻しました',
+ accountDeleted: 'アカウント「{{name}}」を削除しました',
+ contextUpdated: '「{{name}}」のコンテキストを {{summary}} に更新しました',
+ legacyConfirmError:
+ 'アカウント「{{name}}」は明示的な確認が必要です。このアカウントの「履歴同期を編集」を使用してください。',
+ legacyConfirmFailed: 'レガシーアカウント「{{name}}」の確認に失敗しました: {{error}}',
+ legacyConfirmSuccess_one: '{{count}} 件のレガシーアカウントを確認しました',
+ legacyConfirmSuccess_other: '{{count}} 件のレガシーアカウントを確認しました',
+ noLegacyAccounts: '確認が必要なレガシーアカウントはありません',
+ routingStrategySet: 'ルーティング戦略を {{strategy}} に設定しました',
+ variantCreated: 'バリアントを作成しました',
+ variantUpdated: 'バリアントを更新しました',
+ variantDeleted: 'バリアントを削除しました',
+ defaultAccountUpdated: 'デフォルトアカウントを更新しました',
+ accountRemoved: 'アカウントを削除しました',
+ accountPaused: 'アカウントを一時停止しました',
+ accountResumed: 'アカウントを再開しました',
+ accountAdded: '{{provider}} のアカウントを追加しました',
+ kiroImported: 'Kiro アカウントをインポートしました: {{name}}',
+ kiroTokenImported: 'Kiro トークンをインポートしました',
+ modelUpdated: 'モデルを更新しました',
+ presetSaved: 'プリセット「{{name}}」を保存しました',
+ presetDeleted: 'プリセットを削除しました',
+ cliproxyAlreadyRunning: 'CLIProxy はすでに稼働していました',
+ cliproxyStarted: 'CLIProxy を起動しました',
+ cliproxyStartFailed: 'CLIProxy の起動に失敗しました',
+ cliproxyStopped: 'CLIProxy を停止しました',
+ cliproxyStopFailed: 'CLIProxy の停止に失敗しました',
+ presetApplied: 'プリセット「{{name}}」を適用しました',
+ presetAppliedCustom: 'カスタムプリセットを適用しました',
+ settingsSavedWithAdjustments: '設定を保存しました(モデル調整あり)',
+ settingsSaved: '設定を保存しました',
+ failedSaveSettings: '設定の保存に失敗しました',
+ codexRefreshFailed:
+ 'Codex スナップショットの更新に失敗しました。Raw 編集は保持されました。',
+ codexRefreshError: 'Codex スナップショットの更新に失敗しました。',
+ codexFixToml: '保存する前に TOML を修正してください。',
+ codexSaved: 'Codex config.toml を保存しました。',
+ codexChangedExternally: 'config.toml が外部で変更されました。更新して再試行してください。',
+ codexSaveFailed: 'Codex config.toml の保存に失敗しました。',
+ codexUpdateFailed: 'Codex 設定の更新に失敗しました。',
+ noOrphanProfiles: '孤立プロファイル設定は見つかりませんでした',
+ profilesRegistered: '{{count}} 件のプロファイルを登録しました{{skipped}}',
+ destinationEmpty: '送信先プロファイル名は空にできません',
+ profileExportDownloaded: 'プロファイルエクスポートをダウンロードしました',
+ profileImportFailed: 'プロファイルバンドルのインポートに失敗しました',
+ },
+ updatesSpotlight: {
+ openUpdatesCenter: '更新センターを開く',
+ },
+ userMenu: {
+ signedInAs: '{{username}} としてサインイン中',
+ },
+ valueMetrics: {
+ apiCostSaved: 'API コスト削減',
+ tokensSaved: '節約トークン',
+ queriesFaster: '高速化されたクエリ',
+ errorsReduced: '削減されたエラー',
+ vsLastMonth: '前月比',
+ throughCaching: 'キャッシュによる',
+ averageSpeedup: '平均高速化',
+ withRetryLogic: 'リトライロジックによる',
+ performanceMetrics: 'パフォーマンス指標',
+ monthlySummary: '月間サマリー',
+ totalSaved: '合計節約額',
+ tokensProcessed: '処理トークン数',
+ queriesHandled: '処理クエリ数',
+ uptime: '稼働時間',
+ },
},
},
} as const;
diff --git a/ui/src/lib/model-catalogs.ts b/ui/src/lib/model-catalogs.ts
index a37ba349..025ddc01 100644
--- a/ui/src/lib/model-catalogs.ts
+++ b/ui/src/lib/model-catalogs.ts
@@ -139,6 +139,7 @@ function resolveGeminiPreviewModelId(
}
/** Model catalog data - mirrors src/cliproxy/model-catalog.ts */
+// TODO i18n: missing keys for MODEL_CATALOGS displayNames, model names, and descriptions
export const MODEL_CATALOGS: Record = {
agy: {
provider: 'agy',
diff --git a/ui/src/lib/openrouter-utils.ts b/ui/src/lib/openrouter-utils.ts
index d651b62c..84d0e16a 100644
--- a/ui/src/lib/openrouter-utils.ts
+++ b/ui/src/lib/openrouter-utils.ts
@@ -4,6 +4,7 @@
*/
import type { OpenRouterModel, CategorizedModel, ModelCategory } from './openrouter-types';
+import i18n from './i18n';
const CACHE_KEY = 'ccs:openrouter-models';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -19,8 +20,8 @@ export function pricePerMillion(perToken: string): number {
/** Format price for display */
export function formatPrice(perToken: string): string {
const perMillion = pricePerMillion(perToken);
- if (perMillion === 0) return 'Free';
- if (perMillion < 0.01) return '<$0.01';
+ if (perMillion === 0) return i18n.t('openrouterUtils.priceFree');
+ if (perMillion < 0.01) return i18n.t('openrouterUtils.priceLessThanCent');
if (perMillion < 1) return `$${perMillion.toFixed(2)}`;
return `$${perMillion.toFixed(perMillion < 10 ? 2 : 0)}`;
}
@@ -29,7 +30,11 @@ export function formatPrice(perToken: string): string {
export function formatPricingPair(pricing: { prompt: string; completion: string }): string {
const promptPrice = formatPrice(pricing.prompt);
const completionPrice = formatPrice(pricing.completion);
- if (promptPrice === 'Free' && completionPrice === 'Free') return 'Free';
+ if (
+ promptPrice === i18n.t('openrouterUtils.priceFree') &&
+ completionPrice === i18n.t('openrouterUtils.priceFree')
+ )
+ return i18n.t('openrouterUtils.priceFree');
return `${promptPrice}/${completionPrice}`;
}
@@ -248,10 +253,13 @@ export function formatModelAge(created: number): string {
const now = Date.now() / 1000; // Convert to seconds
const diff = now - created;
- if (diff < 86400) return 'Today';
- if (diff < 172800) return 'Yesterday';
- if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
- if (diff < 2592000) return `${Math.floor(diff / 604800)}w ago`;
- if (diff < 31536000) return `${Math.floor(diff / 2592000)}mo ago`;
- return `${Math.floor(diff / 31536000)}y ago`;
+ if (diff < 86400) return i18n.t('openrouterUtils.ageToday');
+ if (diff < 172800) return i18n.t('openrouterUtils.ageYesterday');
+ if (diff < 604800)
+ return i18n.t('openrouterUtils.ageDaysAgo', { count: Math.floor(diff / 86400) });
+ if (diff < 2592000)
+ return i18n.t('openrouterUtils.ageWeeksAgo', { count: Math.floor(diff / 604800) });
+ if (diff < 31536000)
+ return i18n.t('openrouterUtils.ageMonthsAgo', { count: Math.floor(diff / 2592000) });
+ return i18n.t('openrouterUtils.ageYearsAgo', { count: Math.floor(diff / 31536000) });
}
diff --git a/ui/src/lib/provider-config.ts b/ui/src/lib/provider-config.ts
index 35e9822e..7b26a31e 100644
--- a/ui/src/lib/provider-config.ts
+++ b/ui/src/lib/provider-config.ts
@@ -239,7 +239,7 @@ const PROVIDER_NAMES: Record = {
export function getProviderDisplayName(provider: unknown): string {
const normalized = normalizeProviderInput(provider);
if (!normalized) {
- return 'Unknown provider';
+ return 'Unknown provider'; // TODO i18n: missing key
}
return PROVIDER_NAMES[normalized] || String(provider);
}
@@ -283,7 +283,7 @@ export function isDeviceCodeProvider(provider: unknown): boolean {
export function getDeviceCodeProviderDisplayName(provider: unknown): string {
const normalized = normalizeProviderInput(provider);
if (!normalized) {
- return 'Unknown provider';
+ return 'Unknown provider'; // TODO i18n: missing key
}
if (isValidProvider(normalized)) {
return DEVICE_CODE_PROVIDER_DISPLAY_NAMES[normalized] || getProviderDisplayName(normalized);
@@ -296,10 +296,10 @@ export function getDeviceCodeProviderInstruction(provider: unknown): string {
const normalized = normalizeProviderInput(provider);
if (isValidProvider(normalized)) {
return (
- DEVICE_CODE_PROVIDER_INSTRUCTIONS[normalized] || 'Complete the authorization in your browser.'
+ DEVICE_CODE_PROVIDER_INSTRUCTIONS[normalized] || 'Complete the authorization in your browser.' // TODO i18n: missing key
);
}
- return 'Complete the authorization in your browser.';
+ return 'Complete the authorization in your browser.'; // TODO i18n: missing key
}
/** Kiro auth methods exposed in CCS UI (aligned with CLIProxyAPIPlus support). */
@@ -326,36 +326,36 @@ export const DEFAULT_KIRO_AUTH_METHOD: KiroAuthMethod = 'aws';
export const KIRO_AUTH_METHOD_OPTIONS: readonly KiroAuthMethodOption[] = [
{
id: 'aws',
- label: 'AWS Builder ID (Recommended)',
- description: 'Device code flow for AWS organizations and Builder ID accounts.',
+ label: 'AWS Builder ID (Recommended)', // TODO i18n: missing key for kiro auth method aws
+ description: 'Device code flow for AWS organizations and Builder ID accounts.', // TODO i18n: missing key
flowType: 'device_code',
startEndpoint: 'start',
},
{
id: 'aws-authcode',
- label: 'AWS Builder ID (Auth Code)',
- description: 'Authorization code flow via CLI binary.',
+ label: 'AWS Builder ID (Auth Code)', // TODO i18n: missing key
+ description: 'Authorization code flow via CLI binary.', // TODO i18n: missing key
flowType: 'authorization_code',
startEndpoint: 'start',
},
{
id: 'google',
- label: 'Google OAuth',
- description: 'Social OAuth flow with callback URL support.',
+ label: 'Google OAuth', // TODO i18n: missing key
+ description: 'Social OAuth flow with callback URL support.', // TODO i18n: missing key
flowType: 'authorization_code',
startEndpoint: 'start-url',
},
{
id: 'github',
- label: 'GitHub OAuth',
- description: 'Social OAuth flow via management API callback.',
+ label: 'GitHub OAuth', // TODO i18n: missing key
+ description: 'Social OAuth flow via management API callback.', // TODO i18n: missing key
flowType: 'authorization_code',
startEndpoint: 'start-url',
},
{
id: 'idc',
- label: 'AWS Identity Center (IDC)',
- description: 'Use your organization start URL with auth code or device flow.',
+ label: 'AWS Identity Center (IDC)', // TODO i18n: missing key
+ description: 'Use your organization start URL with auth code or device flow.', // TODO i18n: missing key
flowType: 'authorization_code',
startEndpoint: 'start',
},
diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts
index 8d3cbd61..ac248b36 100644
--- a/ui/src/lib/support-updates-catalog.ts
+++ b/ui/src/lib/support-updates-catalog.ts
@@ -48,10 +48,10 @@ export interface CliSupportEntry {
}
export const SUPPORT_SCOPE_LABELS: Record = {
- target: 'Target CLI',
- cliproxy: 'CLIProxy Provider',
- 'api-profiles': 'API Profile',
- websearch: 'WebSearch',
+ target: 'Target CLI', // TODO i18n: missing key for support scope target
+ cliproxy: 'CLIProxy Provider', // TODO i18n: missing key for support scope cliproxy
+ 'api-profiles': 'API Profile', // TODO i18n: missing key for support scope api-profiles
+ websearch: 'WebSearch', // TODO i18n: missing key for support scope websearch
};
export const SUPPORT_NOTICES: SupportNotice[] = [
diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts
index 0105d1e1..e50d0c5b 100644
--- a/ui/src/lib/utils.ts
+++ b/ui/src/lib/utils.ts
@@ -246,13 +246,13 @@ export interface TieredModel {
export function getTierLabel(tier: ModelTier): string {
switch (tier) {
case 'primary':
- return 'Claude & GPT';
+ return i18n.t('utils.tierPrimary');
case 'gemini-3':
- return 'Gemini 3';
+ return i18n.t('utils.tierGemini3');
case 'gemini-2':
- return 'Gemini 2.5';
+ return i18n.t('utils.tierGemini2');
case 'other':
- return 'Other';
+ return i18n.t('utils.tierOther');
}
}
@@ -405,16 +405,16 @@ export function getCodexWindowDisplayLabel(
switch (getCodexWindowKind(label)) {
case 'usage-5h':
- return '5h usage limit';
+ return i18n.t('quotaTooltip.fiveHourLimit');
case 'usage-weekly':
- return 'Weekly usage limit';
+ return i18n.t('quotaTooltip.weeklyLimit');
case 'code-review-5h':
case 'code-review-weekly':
case 'code-review': {
const inferred = inferCodeReviewCadence(currentWindow, context);
- if (inferred === '5h') return 'Code review (5h)';
- if (inferred === 'weekly') return 'Code review (weekly)';
- return 'Code review';
+ if (inferred === '5h') return i18n.t('utils.codeReview5h');
+ if (inferred === 'weekly') return i18n.t('utils.codeReviewWeekly');
+ return i18n.t('utils.codeReview');
}
case 'unknown':
return label;
diff --git a/ui/src/pages/analytics/components/charts-grid.tsx b/ui/src/pages/analytics/components/charts-grid.tsx
index 4b32135b..6995373f 100644
--- a/ui/src/pages/analytics/components/charts-grid.tsx
+++ b/ui/src/pages/analytics/components/charts-grid.tsx
@@ -13,6 +13,7 @@ import { TrendingUp, PieChart } from 'lucide-react';
import { usePrivacy } from '@/contexts/privacy-context';
import { CostByModelCard } from './cost-by-model-card';
import type { ModelUsage, PaginatedSessions, DailyUsage, HourlyUsage } from '@/hooks/use-usage';
+// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
interface ChartsGridProps {
viewMode: 'daily' | 'hourly';
@@ -42,6 +43,8 @@ export function ChartsGrid({
onModelClick,
}: ChartsGridProps) {
const { privacyMode } = usePrivacy();
+ // TODO i18n: uncomment when keys for "Last 24 Hours" / "Usage Trends" / "Model Usage" are added
+ // const { t } = useTranslation();
return (
@@ -50,6 +53,7 @@ export function ChartsGrid({
+ {/* TODO i18n: missing keys for "Last 24 Hours" / "Usage Trends" */}
{viewMode === 'hourly' ? 'Last 24 Hours' : 'Usage Trends'}
@@ -77,6 +81,7 @@ export function ChartsGrid({
+ {/* TODO i18n: missing key for "Model Usage" */}
Model Usage
diff --git a/ui/src/pages/analytics/components/cost-by-model-card.tsx b/ui/src/pages/analytics/components/cost-by-model-card.tsx
index 13693ab4..60eb621d 100644
--- a/ui/src/pages/analytics/components/cost-by-model-card.tsx
+++ b/ui/src/pages/analytics/components/cost-by-model-card.tsx
@@ -11,6 +11,7 @@ import { getModelColor, cn } from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { formatTokens } from '../utils';
import type { ModelUsage } from '@/hooks/use-usage';
+import { useTranslation } from 'react-i18next';
interface CostByModelCardProps {
models: ModelUsage[] | undefined;
@@ -25,12 +26,14 @@ export function CostByModelCard({
onModelClick,
privacyMode,
}: CostByModelCardProps) {
+ const { t } = useTranslation();
+
return (
- Cost by Model
+ {t('analyticsPages.costByModel')}
diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx
index 425b1316..74cb842b 100644
--- a/ui/src/pages/api.tsx
+++ b/ui/src/pages/api.tsx
@@ -38,6 +38,10 @@ import type { ProviderPreset } from '@/lib/provider-presets';
import { cn } from '@/lib/utils';
import { CopyButton } from '@/components/ui/copy-button';
import { useTranslation } from 'react-i18next';
+// TODO i18n: missing keys for apiProfiles: noOrphansFound, confirmRegisterOrphans,
+// registeredWithSkipped, registeredProfiles, copyPrompt, destinationEmpty,
+// exportRedacted, exportDownloaded, importFailed, sidebarTitle, sidebarSubtitle,
+// discoverOrphans, importProfileBundle
import { toast } from 'sonner';
import { useNavigate } from 'react-router-dom';
@@ -116,21 +120,29 @@ export function ApiPage() {
try {
const result = await discoverOrphansMutation.mutateAsync();
if (result.orphans.length === 0) {
- toast.success('No orphan profile settings found');
+ toast.success(t('apiProfiles.noOrphansFound'));
return;
}
const validCount = result.orphans.filter((orphan) => orphan.validation.valid).length;
const shouldRegister = window.confirm(
- `Found ${result.orphans.length} orphan settings file(s). Register ${validCount} valid profile(s) now?`
+ t('apiProfiles.confirmRegisterOrphans', {
+ total: result.orphans.length,
+ valid: validCount,
+ })
);
if (!shouldRegister) return;
const registration = await registerOrphansMutation.mutateAsync({});
const skippedMessage =
- registration.skipped.length > 0 ? `, skipped ${registration.skipped.length}` : '';
- toast.success(`Registered ${registration.registered.length} profile(s)${skippedMessage}`);
+ registration.skipped.length > 0
+ ? t('apiProfiles.registeredWithSkipped', { count: registration.skipped.length })
+ : '';
+ toast.success(
+ t('apiProfiles.registeredProfiles', { count: registration.registered.length }) +
+ skippedMessage
+ );
} catch (error) {
toast.error((error as Error).message);
}
@@ -139,13 +151,13 @@ export function ApiPage() {
const handleCopySelectedProfile = async () => {
if (!selectedProfileData) return;
const destinationInput = window.prompt(
- `Copy profile "${selectedProfileData.name}" to new profile name:`,
+ t('apiProfiles.copyPrompt', { name: selectedProfileData.name }),
`${selectedProfileData.name}-copy`
);
if (!destinationInput) return;
const destination = destinationInput.trim();
if (!destination) {
- toast.error('Destination profile name cannot be empty');
+ toast.error(t('apiProfiles.destinationEmpty'));
return;
}
@@ -169,11 +181,9 @@ export function ApiPage() {
const result = await exportProfileMutation.mutateAsync({ name: selectedProfileData.name });
triggerDownload(`${selectedProfileData.name}.ccs-profile.json`, result.bundle);
if (result.redacted) {
- toast.info(
- 'Export created with redacted token. Use include-secrets flow in CLI if needed.'
- );
+ toast.info(t('apiProfiles.exportRedacted'));
} else {
- toast.success('Profile export downloaded');
+ toast.success(t('apiProfiles.exportDownloaded'));
}
} catch (error) {
toast.error((error as Error).message);
@@ -200,7 +210,7 @@ export function ApiPage() {
toast.info(result.warnings.join('\n'));
}
} catch (error) {
- toast.error((error as Error).message || 'Failed to import profile bundle');
+ toast.error((error as Error).message || t('apiProfiles.importFailed'));
}
};
@@ -214,7 +224,7 @@ export function ApiPage() {
-
Profiles
+ {t('apiProfiles.sidebarTitle')}
@@ -223,8 +233,8 @@ export function ApiPage() {
variant="outline"
onClick={() => void handleDiscoverOrphans()}
disabled={discoverOrphansMutation.isPending || registerOrphansMutation.isPending}
- aria-label="Discover orphan profiles"
- title="Discover orphan profiles"
+ aria-label={t('apiProfiles.discoverOrphans')}
+ title={t('apiProfiles.discoverOrphans')}
>
@@ -253,7 +263,7 @@ export function ApiPage() {
- Premium APIs, local runtimes, custom endpoints
+ {t('apiProfiles.sidebarSubtitle')}
diff --git a/ui/src/pages/claude-extension.tsx b/ui/src/pages/claude-extension.tsx
index c17057ac..9e20d896 100644
--- a/ui/src/pages/claude-extension.tsx
+++ b/ui/src/pages/claude-extension.tsx
@@ -10,6 +10,7 @@ import {
Sparkles,
Trash2,
} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -284,6 +285,7 @@ function BindingListItem({
}
export function ClaudeExtensionPage() {
+ const { t } = useTranslation();
const optionsQuery = useClaudeExtensionOptions();
const bindingsQuery = useClaudeExtensionBindings();
const createBinding = useCreateClaudeExtensionBinding();
@@ -415,8 +417,9 @@ export function ClaudeExtensionPage() {
-
Claude Extension
+
{t('claudeExtensionPage.title')}
+ {/* TODO i18n: missing key for subtitle */}
Saved IDE bindings for CCS profiles
@@ -428,6 +431,7 @@ export function ClaudeExtensionPage() {
+ {/* TODO i18n: missing key for "New" */}
New
@@ -438,30 +442,34 @@ export function ClaudeExtensionPage() {
+ {/* TODO i18n: missing key for "Create binding"/"Binding editor" */}
{creating ? 'Create binding' : 'Binding editor'}
+ {/* TODO i18n: missing key for binding editor description */}
Save a profile + IDE path once, then apply or reset it from the dashboard.
+ {/* TODO i18n: missing key for "Binding name" */}
Binding name
updateDraft('name', event.target.value)}
- placeholder="VS Code · work profile"
+ placeholder="VS Code · work profile" /* TODO i18n: missing key */
/>
+ {/* TODO i18n: missing key for "CCS profile" */}
CCS profile
updateDraft('profile', value)}
>
-
+
{profiles.map((profile) => (
@@ -478,13 +486,15 @@ export function ClaudeExtensionPage() {
-
IDE host
+
+ {t('settingsPage.thinkingSection.ideHost')}
+
updateDraft('host', value as BindingDraft['host'])}
>
-
+
{hosts.map((host) => (
@@ -497,13 +507,15 @@ export function ClaudeExtensionPage() {
-
IDE settings path
+
+ {t('settingsPage.thinkingSection.ideSettingsPath')}
+
updateDraft('ideSettingsPath', event.target.value)}
placeholder={
selectedHost?.defaultSettingsPath ||
- 'Leave blank for the default user settings path'
+ 'Leave blank for the default user settings path' /* TODO i18n: missing key */
}
/>
@@ -513,11 +525,12 @@ export function ClaudeExtensionPage() {
+ {/* TODO i18n: missing key for "Notes" */}
Notes
updateDraft('notes', event.target.value)}
- placeholder="Optional reminder for this machine or workspace"
+ placeholder="Optional reminder for this machine or workspace" /* TODO i18n: missing key */
/>
@@ -532,9 +545,11 @@ export function ClaudeExtensionPage() {
) : (
)}
+ {/* TODO i18n: missing key for "Create"/"Save" */}
{creating ? 'Create' : 'Save'}
+ {/* TODO i18n: missing key for "Reset form" */}
Reset form
@@ -547,6 +562,7 @@ export function ClaudeExtensionPage() {
disabled={deleteBinding.isPending}
>
+ {/* TODO i18n: missing key for "Delete binding" */}
Delete binding
) : null}
@@ -555,6 +571,7 @@ export function ClaudeExtensionPage() {
+ {/* TODO i18n: missing key for "Saved bindings" */}
Saved bindings
@@ -574,6 +591,7 @@ export function ClaudeExtensionPage() {
) : (
+ {/* TODO i18n: missing key for empty bindings text */}
No saved bindings yet. Create one to manage apply, reset, and drift checks
from the dashboard.
@@ -595,17 +613,23 @@ export function ClaudeExtensionPage() {
{selectedProfile.label}
) : null}
{selectedHost ? {selectedHost.label} : null}
- {creating ? Draft : null}
+ {creating ? (
+ {t('settingsPage.thinkingSection.draft')}
+ ) : null}
{status?.sharedSettings &&
isPlainStatusActive(status.sharedSettings) &&
isPlainStatusActive(status.ideSettings) ? (
- In sync
+
+ {t('settingsPage.thinkingSection.inSync')}
+
) : null}
+ {/* TODO i18n: missing key for default binding name */}
{selectedBinding?.name || 'Claude extension binding'}
+ {/* TODO i18n: missing key for binding description */}
Manage the shared Claude settings file and the IDE-local settings file as two
scoped targets.
@@ -624,6 +648,7 @@ export function ClaudeExtensionPage() {
) : (
)}
+ {/* TODO i18n: missing key for "Verify" */}
Verify
{setup ? (
@@ -644,8 +669,11 @@ export function ClaudeExtensionPage() {
{!activeError ? (
- Overview
- Advanced
+ Overview {' '}
+ {/* TODO i18n: missing key */}
+
+ {t('settingsPage.thinkingSection.advanced')}
+
@@ -654,8 +682,8 @@ export function ClaudeExtensionPage() {
title="Shared Claude settings"
description="Writes the managed env block inside ~/.claude/settings.json so CLI and IDE behavior stay aligned."
status={status?.sharedSettings}
- applyLabel="Apply shared"
- resetLabel="Reset shared"
+ applyLabel="Apply shared" /* TODO i18n: missing key */
+ resetLabel="Reset shared" /* TODO i18n: missing key */
onApply={() => runBindingAction('shared', 'apply')}
onReset={() => runBindingAction('shared', 'reset')}
disabled={creating}
@@ -665,8 +693,8 @@ export function ClaudeExtensionPage() {
title={`${selectedHost?.label || 'IDE'} settings.json`}
description="Writes only the Anthropic extension keys so unrelated editor preferences stay untouched."
status={status?.ideSettings}
- applyLabel="Apply IDE"
- resetLabel="Reset IDE"
+ applyLabel="Apply IDE" /* TODO i18n: missing key */
+ resetLabel="Reset IDE" /* TODO i18n: missing key */
onApply={() => runBindingAction('ide', 'apply')}
onReset={() => runBindingAction('ide', 'reset')}
disabled={creating}
@@ -677,7 +705,10 @@ export function ClaudeExtensionPage() {
- Resolved binding
+
+ {t('settingsPage.thinkingSection.resolvedBinding')}
+
+ {/* TODO i18n: missing key for resolved binding description */}
The binding uses the same profile resolution as `ccs persist` and `ccs
env`.
@@ -729,7 +760,10 @@ export function ClaudeExtensionPage() {
- Managed payload
+
+ {t('settingsPage.thinkingSection.managedPayload')}
+
+ {/* TODO i18n: missing key for managed payload description */}
Keep the main view short. The full JSON stays in the Advanced tab.
@@ -780,6 +814,7 @@ export function ClaudeExtensionPage() {
applyBinding.variables?.target === 'all' ? (
) : null}
+ {/* TODO i18n: missing key for "Apply both targets" */}
Apply both targets
runBindingAction('all', 'reset')}
disabled={resetBinding.isPending}
>
+ {/* TODO i18n: missing key for "Reset both targets" */}
Reset both targets
) : (
+ {/* TODO i18n: missing key for "Save this draft..." */}
Save this draft to unlock apply, reset, and verify actions.
)}
@@ -804,7 +841,9 @@ export function ClaudeExtensionPage() {
- Warnings
+
+ {t('settingsPage.thinkingSection.warnings')}
+
Operational details that can break the binding even when JSON is
correct.
@@ -831,7 +870,9 @@ export function ClaudeExtensionPage() {
- Notes
+
+ {t('settingsPage.thinkingSection.notes')}
+
Short context from CCS about account continuity and host-specific
behavior.
diff --git a/ui/src/pages/cliproxy-ai-providers.tsx b/ui/src/pages/cliproxy-ai-providers.tsx
index 0d60c6bd..73f60c6f 100644
--- a/ui/src/pages/cliproxy-ai-providers.tsx
+++ b/ui/src/pages/cliproxy-ai-providers.tsx
@@ -52,6 +52,7 @@ import {
Workflow,
Zap,
} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
function SummaryCard({ label, value, hint }: { label: string; value: string; hint?: string }) {
return (
@@ -1403,6 +1404,7 @@ function EmptyEntryWorkspace({
export function CliproxyAiProvidersPage() {
const location = useLocation();
const navigate = useNavigate();
+ const { t } = useTranslation();
const { data, error, isLoading, isFetching, refetch } = useCliproxyAiProviders();
const createMutation = useCreateCliproxyAiProviderEntry();
const updateMutation = useUpdateCliproxyAiProviderEntry();
@@ -1475,7 +1477,7 @@ export function CliproxyAiProvidersPage() {
-
Unable to load AI Providers
+
{t('aiProvidersPage.unableToLoad')}
{message}
void refetch()}>
diff --git a/ui/src/pages/codex.tsx b/ui/src/pages/codex.tsx
index 07756061..bd34659e 100644
--- a/ui/src/pages/codex.tsx
+++ b/ui/src/pages/codex.tsx
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { GripVertical, Loader2 } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { CodexControlCenterTab } from '@/components/compatible-cli/codex-control-center-tab';
import { CodexDocsTab } from '@/components/compatible-cli/codex-docs-tab';
import { useCodex } from '@/hooks/use-codex';
@@ -21,6 +22,7 @@ import {
import { safeParseTomlObject } from '@shared/toml-object';
export function CodexPage() {
+ const { t } = useTranslation();
const {
diagnostics,
diagnosticsLoading,
@@ -52,15 +54,15 @@ export function CodexPage() {
rawConfig?.parseError !== null ||
rawConfig?.readError !== null;
const controlsDisabledReason = rawConfigError
- ? 'Structured controls unavailable: failed to load the current config.toml.'
+ ? /* TODO i18n: missing key for "Structured controls unavailable: failed to load the current config.toml." */ 'Structured controls unavailable: failed to load the current config.toml.'
: rawConfig?.readError
- ? `Structured controls unavailable: ${rawConfig.readError}`
+ ? /* TODO i18n: missing key for controls unavailable with read error */ `Structured controls unavailable: ${rawConfig.readError}`
: rawConfigDirty
? rawEditorValidation.valid
- ? 'Save or discard raw TOML edits before using structured controls.'
- : 'Fix or discard raw TOML edits before using structured controls.'
+ ? /* TODO i18n: missing key for "Save or discard raw TOML edits before using structured controls." */ 'Save or discard raw TOML edits before using structured controls.'
+ : /* TODO i18n: missing key for "Fix or discard raw TOML edits before using structured controls." */ 'Fix or discard raw TOML edits before using structured controls.'
: rawConfig?.parseError
- ? `Structured controls disabled: ${rawConfig.parseError}`
+ ? /* TODO i18n: missing key for controls disabled with parse error */ `Structured controls disabled: ${rawConfig.parseError}`
: null;
const topLevelSettings = useMemo(
@@ -95,19 +97,19 @@ export function CodexPage() {
);
if (refreshFailed) {
- toast.error('Failed to refresh Codex snapshot. Raw edits were kept.');
+ toast.error(t('toasts.codexRefreshFailed'));
return;
}
setRawDraftText(null);
} catch (error) {
- toast.error((error as Error).message || 'Failed to refresh Codex snapshot.');
+ toast.error((error as Error).message || t('toasts.codexRefreshError'));
}
};
const handleSaveRawConfig = async () => {
if (!rawEditorValidation.valid) {
- toast.error('Fix TOML before saving.');
+ toast.error(t('toasts.codexFixToml'));
return;
}
@@ -117,13 +119,13 @@ export function CodexPage() {
expectedMtime: rawConfig?.exists ? rawConfig.mtime : undefined,
});
setRawDraftText(null);
- toast.success('Saved Codex config.toml.');
+ toast.success(t('toasts.codexSaved'));
await refetchDiagnostics();
} catch (error) {
if (isApiConflictError(error)) {
- toast.error('config.toml changed externally. Refresh and retry.');
+ toast.error(t('toasts.codexChangedExternally'));
} else {
- toast.error((error as Error).message || 'Failed to save Codex config.toml.');
+ toast.error((error as Error).message || t('toasts.codexSaveFailed'));
}
}
};
@@ -141,9 +143,9 @@ export function CodexPage() {
toast.success(successMessage);
} catch (error) {
if (isApiConflictError(error)) {
- toast.error('config.toml changed externally. Refresh and retry.');
+ toast.error(t('toasts.codexChangedExternally'));
} else {
- toast.error((error as Error).message || 'Failed to update Codex config.');
+ toast.error((error as Error).message || t('toasts.codexUpdateFailed'));
}
}
};
@@ -155,7 +157,9 @@ export function CodexPage() {
return (
- Loading Codex diagnostics...
+ {
+ /* TODO i18n: missing key for "Loading Codex diagnostics..." */ 'Loading Codex diagnostics...'
+ }
);
}
@@ -163,7 +167,9 @@ export function CodexPage() {
if (diagnosticsError || !diagnostics) {
return (
- Failed to load Codex diagnostics.
+ {
+ /* TODO i18n: missing key for "Failed to load Codex diagnostics." */ 'Failed to load Codex diagnostics.'
+ }
);
}
@@ -172,9 +178,9 @@ export function CodexPage() {
- Overview
- Control Center
- Docs
+ {t('codexPage.overview')}
+ {t('codexPage.controlCenter')}
+ {t('codexPage.docs')}
@@ -220,6 +226,7 @@ export function CodexPage() {
setRawDraftText(null)}
language="toml"
+ /* TODO i18n: missing key for "Loading config.toml..." */
loadingLabel="Loading config.toml..."
+ /* TODO i18n: missing key for "TOML warning" */
parseWarningLabel="TOML warning"
ownershipNotice={
+ {/* TODO i18n: missing keys for ownership notice paragraphs */}
This file is upstream-owned by Codex CLI.
CCS does not keep ~/.codex/config.toml in sync for you.
diff --git a/ui/src/pages/cursor.tsx b/ui/src/pages/cursor.tsx
index 54d9ae86..dffa5262 100644
--- a/ui/src/pages/cursor.tsx
+++ b/ui/src/pages/cursor.tsx
@@ -142,6 +142,7 @@ function parseRawSettings(value: string): RawSettingsParseResult {
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return {
isValid: false,
+ /* TODO i18n: missing key for "Raw settings must be a JSON object" */
error: 'Raw settings must be a JSON object',
};
}
@@ -153,6 +154,7 @@ function parseRawSettings(value: string): RawSettingsParseResult {
} catch (error) {
return {
isValid: false,
+ /* TODO i18n: missing key for "Invalid JSON" */
error: (error as Error).message || 'Invalid JSON',
};
}
@@ -1075,6 +1077,7 @@ export function CursorPage() {
className="text-xs h-7 gap-1"
onClick={() => applyPreset('codex53')}
disabled={modelsLoading || models.length === 0}
+ /* TODO i18n: missing key for "OpenAI-only mapping: GPT-5.3 Codex / Codex Max / GPT-5 Mini" */
title="OpenAI-only mapping: GPT-5.3 Codex / Codex Max / GPT-5 Mini"
>
@@ -1086,6 +1089,7 @@ export function CursorPage() {
className="text-xs h-7 gap-1"
onClick={() => applyPreset('claude46')}
disabled={modelsLoading || models.length === 0}
+ /* TODO i18n: missing key for "Claude-first mapping: Opus 4.6 / Sonnet 4.5 / Haiku 4.5" */
title="Claude-first mapping: Opus 4.6 / Sonnet 4.5 / Haiku 4.5"
>
@@ -1097,6 +1101,7 @@ export function CursorPage() {
className="text-xs h-7 gap-1"
onClick={() => applyPreset('gemini3')}
disabled={modelsLoading || models.length === 0}
+ /* TODO i18n: missing key for "Gemini-first mapping: Gemini 3 Pro + Gemini 3 Flash" */
title="Gemini-first mapping: Gemini 3 Pro + Gemini 3 Flash"
>
@@ -1177,7 +1182,7 @@ export function CursorPage() {
- Port
+ {t('cursorPage.port')}
+ {/* TODO i18n: missing key for model mapping env var info paragraph */}
Model mapping writes `ANTHROPIC_MODEL`,
`ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and
diff --git a/ui/src/pages/droid.tsx b/ui/src/pages/droid.tsx
index 0166ab23..563ff2ae 100644
--- a/ui/src/pages/droid.tsx
+++ b/ui/src/pages/droid.tsx
@@ -34,6 +34,7 @@ import {
extractDroidByokModels,
} from '@/lib/droid-byok-custom-models';
+// TODO i18n: missing keys for DEFAULT_DROID_FACTORY_DOC_LINKS labels and descriptions
const DEFAULT_DROID_FACTORY_DOC_LINKS = [
{
id: 'droid-cli-overview',
@@ -55,6 +56,7 @@ const DEFAULT_DROID_FACTORY_DOC_LINKS = [
},
];
+// TODO i18n: missing keys for DEFAULT_DROID_PROVIDER_DOC_LINKS labels and apiFormat
const DEFAULT_DROID_PROVIDER_DOC_LINKS = [
{
provider: 'anthropic',
@@ -112,7 +114,7 @@ function renderTextWithLinks(text: string): ReactNode[] {
}
function formatTimestamp(value: number | null | undefined): string {
- if (!value || !Number.isFinite(value)) return 'N/A';
+ if (!value || !Number.isFinite(value)) return /* TODO i18n: missing key for "N/A" */ 'N/A';
return new Date(value).toLocaleString();
}
@@ -129,7 +131,11 @@ function parseJsonObjectText(
try {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
- return { valid: false, error: 'JSON root must be an object.' };
+ return {
+ valid: false,
+ /* TODO i18n: missing key for "JSON root must be an object." */ error:
+ 'JSON root must be an object.',
+ };
}
return { valid: true, value: parsed as Record };
} catch (error) {
@@ -354,17 +360,26 @@ export function DroidPage() {
/>
@@ -443,7 +458,7 @@ export function DroidPage() {
disabledReason={
rawEditorParsed.valid
? null
- : `Quick settings disabled: ${rawEditorParsed.error}`
+ : /* TODO i18n: missing key for "Quick settings disabled: " */ `Quick settings disabled: ${rawEditorParsed.error}`
}
onEnumSettingChange={(key, value) => {
updateSettingsField(key, value);
@@ -512,7 +527,10 @@ export function DroidPage() {
{model.provider}
- {model.apiKeyPreview || 'no-key'}
+ {model.apiKeyPreview ||
+ /* TODO i18n: missing key for "no-key" */ 'no-key'}
diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx
index 42f2f231..50b879ad 100644
--- a/ui/src/pages/home.tsx
+++ b/ui/src/pages/home.tsx
@@ -187,7 +187,7 @@ export function HomePage() {
-
Logs moved to a dedicated workspace
+
{t('homePageV2.logsMoved')}
Use the unified logs page for source-level filtering, structured entry inspection,
and retention policy edits without crowding the home dashboard.
@@ -195,6 +195,7 @@ export function HomePage() {
navigate('/logs')}>
+ {/* TODO i18n: missing key for "Open logs" */}
Open logs
diff --git a/ui/src/pages/logs.tsx b/ui/src/pages/logs.tsx
index e96d1ab9..ffc755fe 100644
--- a/ui/src/pages/logs.tsx
+++ b/ui/src/pages/logs.tsx
@@ -20,6 +20,7 @@ import { LogsEntryList } from '@/components/logs/logs-entry-list';
import { LogsFilters } from '@/components/logs/logs-filters';
import { LogsPageSkeleton } from '@/components/logs/logs-page-skeleton';
import { getSourceLabelMap, useLogsWorkspace, useUpdateLogsConfig } from '@/hooks/use-logs';
+// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
const DESKTOP_LOGS_BREAKPOINT = 1200;
const LEFT_PANEL_WIDTH = 336;
@@ -66,6 +67,8 @@ function CollapsedPaneToggle({
}
export function LogsPage() {
+ // TODO i18n: uncomment when keys for Syncing/Refresh and other strings are added
+ // const { t } = useTranslation();
const workspace = useLogsWorkspace();
const updateConfig = useUpdateLogsConfig();
const sourceLabels = getSourceLabelMap(workspace.sourcesQuery.data ?? []);
@@ -179,8 +182,8 @@ export function LogsPage() {
)}
{workspace.entriesQuery.isFetching || workspace.sourcesQuery.isFetching
- ? 'Syncing'
- : 'Refresh'}
+ ? /* TODO i18n: missing key for "Syncing" */ 'Syncing'
+ : /* TODO i18n: missing key for "Refresh" */ 'Refresh'}
+ {/* TODO i18n: missing key for "config.yaml" header */}
config.yaml
+ {/* TODO i18n: missing key for "~/.ccs/config.yaml" path */}
~/.ccs/config.yaml
diff --git a/ui/src/pages/settings/sections/channels.tsx b/ui/src/pages/settings/sections/channels.tsx
index 36d1a001..cf5ecc8e 100644
--- a/ui/src/pages/settings/sections/channels.tsx
+++ b/ui/src/pages/settings/sections/channels.tsx
@@ -15,6 +15,7 @@ import {
ShieldAlert,
Trash2,
} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { useOfficialChannelsConfig } from '../hooks/use-official-channels-config';
import { useRawConfig } from '../hooks';
import type { OfficialChannelId } from '../types';
@@ -84,6 +85,7 @@ function getSelectedChannelLabel(
}
export default function ChannelsSection() {
+ const { t } = useTranslation();
const {
config,
status,
@@ -148,7 +150,7 @@ export default function ChannelsSection() {
- Loading
+ {t('settings.loading')}
);
@@ -182,11 +184,13 @@ export default function ChannelsSection() {
-
Official Channels
+
{t('settingsPage.channelsSection.title')}
+ {/* TODO i18n: missing key for channels description paragraphs */}
Configure official Claude channels here, then run ccs normally on a
supported native Claude session.
+ {/* TODO i18n: missing key for channels storage description */}
CCS stores only channel selection in config.yaml. Claude keeps the
machine-level channel state under ~/.claude/channels/.
@@ -208,10 +212,12 @@ export default function ChannelsSection() {
{status.summary.nextStep}
+ {/* TODO i18n: missing key for "Machine checks" */}
Machine checks
Bun
+ {/* TODO i18n: missing key for "Installed"/"Missing" */}
{status.bunInstalled ? 'Installed' : 'Missing'}
@@ -224,6 +230,7 @@ export default function ChannelsSection() {
Claude auth
+ {/* TODO i18n: missing key for "Unknown" */}
{status.auth.authMethod ?? 'Unknown'}
@@ -241,6 +248,7 @@ export default function ChannelsSection() {
{status && (
+ {/* TODO i18n: missing key for "Fastest path" and step descriptions */}
Fastest path
1. Turn on the channels you want below.
@@ -252,6 +260,7 @@ export default function ChannelsSection() {
{status.supportMessage}
+ {/* TODO i18n: missing key for "Advanced notes and scope" */}
Advanced notes and scope
@@ -267,6 +276,7 @@ export default function ChannelsSection() {
+ {/* TODO i18n: missing key for "If you run ccs now" */}
If you run ccs now
@@ -278,10 +288,12 @@ export default function ChannelsSection() {
+ {/* TODO i18n: missing key for "You type:" */}
You type: {' '}
{status.launchPreview.command}
+ {/* TODO i18n: missing key for "CCS adds:" */}
CCS adds: {' '}
{status.launchPreview.appendedArgs.length > 0
? status.launchPreview.appendedArgs.join(' ')
@@ -391,6 +403,7 @@ export default function ChannelsSection() {
disabled={saving || !tokenDraft.trim()}
>
+ {/* TODO i18n: missing key for "Save Token" */}
Save Token
+ {/* TODO i18n: missing key for "Clear Saved Token" */}
Clear Saved Token
@@ -406,6 +420,7 @@ export default function ChannelsSection() {
)}
+ {/* TODO i18n: missing key for "Claude-side setup commands" */}
Claude-side setup commands
@@ -426,6 +441,7 @@ export default function ChannelsSection() {
+ {/* TODO i18n: missing key for channels injection disclaimer */}
CCS injects --channels only for the current Claude session. Telegram,
Discord, and iMessage stop receiving messages when that Claude session exits.
@@ -437,7 +453,10 @@ export default function ChannelsSection() {
-
Skip permission prompts on launch
+
+ {t('profileEditorSections.skipPermissionPrompts')}
+
+ {/* TODO i18n: missing key for skip permission description */}
Optional advanced behavior. CCS adds --dangerously-skip-permissions{' '}
only when at least one selected channel is being auto-enabled and you did not
@@ -466,7 +485,7 @@ export default function ChannelsSection() {
void refreshAll()} disabled={saving}>
- Refresh
+ {t('settings.refresh')}
diff --git a/ui/src/pages/settings/sections/image-analysis/index.tsx b/ui/src/pages/settings/sections/image-analysis/index.tsx
index a644dea9..a4fb1545 100644
--- a/ui/src/pages/settings/sections/image-analysis/index.tsx
+++ b/ui/src/pages/settings/sections/image-analysis/index.tsx
@@ -26,6 +26,7 @@ import {
Sparkles,
Trash2,
} from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { api, type ImageAnalysisDashboardData } from '@/lib/api-client';
import { cn } from '@/lib/utils';
import { useRawConfig } from '../../hooks';
@@ -115,6 +116,7 @@ function backendStateClass(state: ImageAnalysisDashboardData['backends'][number]
}
}
+// TODO i18n: missing keys for currentTargetModeLabel values
function currentTargetModeLabel(
mode: ImageAnalysisDashboardData['profiles'][number]['currentTargetMode']
): string {
@@ -155,6 +157,7 @@ function currentTargetModeClass(
}
}
+// TODO i18n: missing keys for backendStateLabel values
function backendStateLabel(state: ImageBackend['state']): string {
switch (state) {
case 'starts_on_launch':
@@ -170,6 +173,7 @@ function backendStateLabel(state: ImageBackend['state']): string {
}
}
+// TODO i18n: missing keys for backendStatusNote values
function backendStatusNote(backend: ImageBackend | undefined): string | null {
if (!backend) {
return 'No model configured.';
@@ -189,6 +193,7 @@ function backendStatusNote(backend: ImageBackend | undefined): string | null {
}
}
+// TODO i18n: missing keys for routeSourceLabel values
function routeSourceLabel(source: ImageProfile['resolutionSource']): string {
switch (source) {
case 'profile-backend':
@@ -254,6 +259,7 @@ function getCoverageRowClass(index: number, profile: ImageProfile): string {
return index % 2 === 0 ? 'bg-background/75' : 'bg-muted/18';
}
+// TODO i18n: missing keys for summaryCompactDetail format strings
function summaryCompactDetail(summary: ImageAnalysisDashboardData['summary']): string {
const parts = [`${summary.activeProfileCount} routed`, `${summary.nativeProfileCount} native`];
@@ -361,6 +367,7 @@ function ImageSectionPanel({
}
export default function ImageAnalysisSection() {
+ const { t } = useTranslation();
const { fetchRawConfig } = useRawConfig();
const [data, setData] = useState
(null);
const [loading, setLoading] = useState(true);
@@ -530,7 +537,7 @@ export default function ImageAnalysisSection() {
});
setData(payload);
hydrateDraft(payload);
- setSuccess('Image settings saved.');
+ setSuccess(t('commonToast.settingsSaved'));
await fetchRawConfig();
return true;
} catch (err) {
@@ -549,6 +556,7 @@ export default function ImageAnalysisSection() {
hydrateDraft,
mappingDrafts,
providerModels,
+ t,
timeout,
]
);
@@ -638,7 +646,7 @@ export default function ImageAnalysisSection() {
- Loading image settings...
+ {t('settingsPage.imageAnalysisSection.loading')}
);
@@ -649,12 +657,14 @@ export default function ImageAnalysisSection() {
- {error ?? 'Failed to load image settings.'}
+
+ {error ?? t('settingsPage.imageAnalysisSection.description')}
+
- Retry
+ {t('sharedPage.retry')}
@@ -702,7 +712,9 @@ export default function ImageAnalysisSection() {
- Image
+
+ {t('settingsPage.imageAnalysisSection.title')}
+
@@ -750,7 +762,7 @@ export default function ImageAnalysisSection() {
tone="amber"
eyebrow="Control deck"
title="Core setup"
- description="Global toggle, timeout, and fallback."
+ description={t('settingsPage.imageAnalysisSection.description')}
icon={ }
meta={
@@ -837,11 +849,15 @@ export default function ImageAnalysisSection() {
disabled={saving}
>
-
+
{configuredBackendIds.length === 0 ? (
- Configure a model first
+
+ {t('settingsPage.thinkingSection.configureModelFirst')}
+
) : (
configuredBackendIds.map((backendId) => (
@@ -954,7 +970,7 @@ export default function ImageAnalysisSection() {
@@ -983,6 +999,7 @@ export default function ImageAnalysisSection() {
void commitProviderModel(backendId, '');
}}
>
+ {/* TODO i18n: missing key for "Clear" */}
Clear
)}
@@ -1084,6 +1101,7 @@ export default function ImageAnalysisSection() {
) : (
)}
+ {/* TODO i18n: missing key for "Hide"/"Show" */}
{showProfileRouting ? 'Hide' : 'Show'}
{showProfileRouting && (
@@ -1104,6 +1122,7 @@ export default function ImageAnalysisSection() {
disabled={configuredBackendIds.length === 0 || saving}
>
+ {/* TODO i18n: missing key for "Add mapping" */}
Add mapping
)}
@@ -1157,6 +1176,7 @@ export default function ImageAnalysisSection() {
}}
>
+ {/* TODO i18n: missing key for "Remove" */}
Remove
@@ -1166,7 +1186,7 @@ export default function ImageAnalysisSection() {
value={row.profileName}
list="image-profile-suggestions"
disabled={saving}
- placeholder="Profile or variant name"
+ placeholder="Profile or variant name" /* TODO i18n: missing key */
className="h-10 border-slate-400/15 bg-background/88 text-base"
onChange={(event) => {
updateMappingRow(row.id, { profileName: event.target.value });
@@ -1198,11 +1218,15 @@ export default function ImageAnalysisSection() {
}}
>
-
+
{configuredBackendIds.length === 0 ? (
- Configure a model first
+
+ {t('settingsPage.thinkingSection.configureModelFirst')}
+
) : (
configuredBackendIds.map((backendId) => (
diff --git a/ui/src/pages/settings/sections/thinking/index.tsx b/ui/src/pages/settings/sections/thinking/index.tsx
index bfe4f13e..d4c57e74 100644
--- a/ui/src/pages/settings/sections/thinking/index.tsx
+++ b/ui/src/pages/settings/sections/thinking/index.tsx
@@ -22,6 +22,10 @@ import { useThinkingConfig } from '../../hooks';
import type { ThinkingMode } from '../../types';
import { useTranslation } from 'react-i18next';
+// Thinking level labels are technical descriptors with token counts that stay
+// consistent across locales. If locale-specific labels are needed later, add
+// i18n keys and replace these with t() calls.
+// TODO i18n: missing key for thinking level labels
const THINKING_LEVELS = [
{ value: 'minimal', label: 'Minimal (512 tokens)' },
{ value: 'low', label: 'Low (1K tokens)' },
@@ -31,6 +35,7 @@ const THINKING_LEVELS = [
{ value: 'auto', label: 'Auto (dynamic)' },
];
+// TODO i18n: missing key for override level labels
const OVERRIDE_LEVELS = [
{ value: '__none__', label: 'None (use CLI flags only)' },
...THINKING_LEVELS,
@@ -334,6 +339,7 @@ export default function ThinkingSection() {
{t('settingsThinking.apply')}
+ {/* TODO i18n: missing key for budget range text */}
Range: {THINKING_BUDGET_MIN} to {THINKING_BUDGET_MAX}
@@ -446,6 +452,7 @@ export default function ThinkingSection() {
{/* Info Box */}
{t('settingsThinking.cliEnvOverride')}
+ {/* TODO i18n: missing key for CLI/env override info text */}
Override per session with flags or{' '}
CCS_THINKING env var.
diff --git a/ui/src/pages/settings/sections/websearch/index.tsx b/ui/src/pages/settings/sections/websearch/index.tsx
index b7a19a13..4365cc4d 100644
--- a/ui/src/pages/settings/sections/websearch/index.tsx
+++ b/ui/src/pages/settings/sections/websearch/index.tsx
@@ -62,6 +62,7 @@ interface ProviderDefinition {
fields?: ProviderFieldDefinition[];
}
+// TODO i18n: missing keys for CHAIN_STEPS titles
const CHAIN_STEPS = [
{ id: 'exa', title: 'Exa', defaultEnabled: false },
{ id: 'tavily', title: 'Tavily', defaultEnabled: false },
@@ -71,6 +72,7 @@ const CHAIN_STEPS = [
{ id: 'legacy', title: 'Legacy CLI', defaultEnabled: false },
] as const;
+// TODO i18n: missing keys for BACKEND_PROVIDERS titles, descriptions, badges, footerNotes, field labels, helpTexts, placeholders
const BACKEND_PROVIDERS: ProviderDefinition[] = [
{
id: 'exa',
@@ -194,6 +196,7 @@ const BACKEND_PROVIDERS: ProviderDefinition[] = [
},
];
+// TODO i18n: missing keys for LEGACY_PROVIDERS titles, descriptions, badges, footerNotes, field labels, helpTexts, placeholders
const LEGACY_PROVIDERS: ProviderDefinition[] = [
{
id: 'gemini',
@@ -292,6 +295,7 @@ function getStatusTone(
return 'idle';
}
+// TODO i18n: missing keys for getStatusLabel return values ("Ready", "Needs setup", "Disabled")
function getStatusLabel(provider: CliStatus | undefined, enabled: boolean): string {
if (enabled && provider?.available) {
return 'Ready';
@@ -375,6 +379,7 @@ function isApiKeyProvider(providerId: ProviderId): providerId is WebSearchApiKey
return providerId === 'exa' || providerId === 'tavily' || providerId === 'brave';
}
+// TODO i18n: missing keys for getApiKeySummary return values
function getApiKeySummary(apiKeyState: WebSearchApiKeyState | undefined): string {
if (!apiKeyState?.configured) {
return 'Not stored';
@@ -437,6 +442,7 @@ export default function WebSearchSection() {
const legacyReady = legacyEnabled.some((provider) => providerStatus.get(provider.id)?.available);
const legacySummary =
+ // TODO i18n: missing keys for legacy summary format strings ("Off", "X enabled", "N enabled")
legacyEnabled.length === 0
? 'Off'
: legacyEnabled.length === 1
@@ -624,6 +630,7 @@ export default function WebSearchSection() {
+ {/* TODO i18n: missing key for "Execution chain" */}
Execution chain
@@ -686,6 +693,7 @@ export default function WebSearchSection() {
+ {/* TODO i18n: missing key for "Primary backends" */}
Primary backends
Real backends run top-down before any legacy CLI fallback.
@@ -739,6 +747,7 @@ export default function WebSearchSection() {
+ {/* TODO i18n: missing key for "API Key" */}
API Key
@@ -747,12 +756,12 @@ export default function WebSearchSection() {
{config?.apiKeys?.[apiKeyProviderId]?.maskedValue
? `${config.apiKeys[apiKeyProviderId]?.envVar} ${config.apiKeys[apiKeyProviderId]?.maskedValue}`
- : `Store ${provider.badge} here so the backend is ready immediately after you enable it.`}
+ : /* TODO i18n: missing key for "Store X here..." */ `Store ${provider.badge} here so the backend is ready immediately after you enable it.`}
{savedApiKeyProvider === provider.id && (
- Saved
+ {/* TODO i18n: missing key for "Saved" */ 'Saved'}
)}
@@ -768,8 +777,8 @@ export default function WebSearchSection() {
}
placeholder={
config?.apiKeys?.[apiKeyProviderId]?.configured
- ? 'Enter a new key to rotate the stored secret'
- : `Paste ${provider.badge}`
+ ? /* TODO i18n: missing key for "Enter a new key to rotate the stored secret" */ 'Enter a new key to rotate the stored secret'
+ : /* TODO i18n: missing key for "Paste X" */ `Paste ${provider.badge}`
}
className="bg-background/80 font-mono text-sm"
disabled={saving}
@@ -786,8 +795,8 @@ export default function WebSearchSection() {
}
>
{config?.apiKeys?.[apiKeyProviderId]?.configured
- ? 'Update key'
- : 'Save key'}
+ ? /* TODO i18n: missing key for "Update key" */ 'Update key'
+ : /* TODO i18n: missing key for "Save key" */ 'Save key'}
{(config?.apiKeys?.[apiKeyProviderId]?.source === 'global_env' ||
@@ -800,7 +809,9 @@ export default function WebSearchSection() {
}}
disabled={saving}
>
- Remove stored key
+ {
+ /* TODO i18n: missing key for "Remove stored key" */ 'Remove stored key'
+ }
)}
@@ -828,6 +839,7 @@ export default function WebSearchSection() {
+ {/* TODO i18n: missing key for "Legacy CLI fallbacks" */}
Legacy CLI fallbacks
Runs only after every enabled real backend fails.
diff --git a/ui/src/pages/settings/sections/websearch/provider-card.tsx b/ui/src/pages/settings/sections/websearch/provider-card.tsx
index 9f144a33..40caaab0 100644
--- a/ui/src/pages/settings/sections/websearch/provider-card.tsx
+++ b/ui/src/pages/settings/sections/websearch/provider-card.tsx
@@ -1,5 +1,6 @@
import type { KeyboardEvent, ReactNode } from 'react';
import { ExternalLink } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
@@ -116,6 +117,7 @@ export function ProviderCard({
footerNote,
children,
}: ProviderCardProps) {
+ const { t } = useTranslation();
const tone = PROVIDER_TONE_STYLES[badgeTone];
const status = getStatusToneStyles(statusTone);
@@ -195,7 +197,7 @@ export function ProviderCard({
{field.saved && (
- Saved
+ {t('settings.saved')}
)}
@@ -241,7 +243,7 @@ export function ProviderCard({
className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
>
- View docs
+ {t('settingsWebsearch.viewDocs')}
)}
diff --git a/ui/src/pages/shared.tsx b/ui/src/pages/shared.tsx
index 8a848519..5b632569 100644
--- a/ui/src/pages/shared.tsx
+++ b/ui/src/pages/shared.tsx
@@ -89,6 +89,7 @@ export function SharedPage() {
const hasNoItems = !isLoading && !isError && allItems.length === 0;
const hasNoMatches = !isLoading && !isError && allItems.length > 0 && filteredItems.length === 0;
+ // TODO i18n: missing key for "Shared item totals could not be loaded. Listing still works."
const summaryErrorMessage = getSharedErrorMessage(
summaryError,
'Shared item totals could not be loaded. Listing still works.'
@@ -155,6 +156,7 @@ export function SharedPage() {
{t('sharedPage.configurationRequired')}
+ {/* TODO i18n: missing key for "Run `ccs sync` to configure." */}
{summary.symlinkStatus.message}. Run `ccs sync` to configure.
@@ -399,6 +401,7 @@ function getSharedErrorMessage(error: unknown, fallbackMessage: string): string
const normalized = error.message.toLowerCase();
if (normalized.includes('failed to fetch') || normalized.includes('network')) {
+ // TODO i18n: missing key for connection lost message
return 'Connection to dashboard server lost or restarting. Keep `ccs config` running, then retry.';
}