diff --git a/docs/i18n-dashboard.md b/docs/i18n-dashboard.md index 58a0b293..618e18ba 100644 --- a/docs/i18n-dashboard.md +++ b/docs/i18n-dashboard.md @@ -1,6 +1,6 @@ # Dashboard i18n Guide -Last Updated: 2026-03-02 +Last Updated: 2026-04-14 This document describes the internationalization (i18n) architecture used by the CCS Dashboard (`ui/`), how locale selection works, and how to add new languages safely. @@ -14,6 +14,7 @@ Dashboard i18n currently covers UI text rendered by React components. - `en` (English) - `zh-CN` (Simplified Chinese) - `vi` (Vietnamese) + - `ja` (Japanese) - Locale state is persisted in browser localStorage using `ccs-ui-locale`. - Fallback language is `en`. @@ -38,6 +39,7 @@ Out of scope: - `en.translation` - `zh-CN.translation` - `vi.translation` + - `ja.translation` - Uses `initReactI18next` for React integration. ### Locale utilities @@ -54,6 +56,12 @@ Out of scope: - Uses `react-i18next` + shadcn `Select`. - Calls `persistLocale` and `i18n.changeLanguage` on selection. +### Test bootstrap + +- File: `ui/tests/setup/vitest-setup.ts` +- Test setup must import `ui/src/lib/i18n.ts` so direct `useTranslation()` consumers resolve the same singleton instance as the app. +- If a test mocks `useTranslation()`, keep the mocked key surface aligned with the component output or the assertions will drift. + --- ## Key Conventions diff --git a/ui/src/components/compatible-cli/codex-features-card.tsx b/ui/src/components/compatible-cli/codex-features-card.tsx index 386ff6f9..fb6a5345 100644 --- a/ui/src/components/compatible-cli/codex-features-card.tsx +++ b/ui/src/components/compatible-cli/codex-features-card.tsx @@ -30,12 +30,10 @@ export function CodexFeaturesCard({ return ( } - // TODO i18n: missing key codex.featuresDesc - description="Toggle the supported Codex feature flags CCS can safely manage." + description={t('codex.featuresDesc')} disabledReason={disabledReason} >
@@ -81,14 +79,9 @@ export function CodexFeaturesCard({

- {/* TODO i18n: missing key codex.configOnlyFlags */} - Existing config-only flags -

-

- {/* TODO i18n: missing key codex.configOnlyFlagsDesc */} - These feature keys already exist in your `config.toml`, so CCS can surface them - without claiming full catalog coverage. + {t('codex.configOnlyFlags')}

+

{t('codex.configOnlyFlagsDesc')}

{configOnlyFeatures.map(([name, current]) => (

{name}

- {/* TODO i18n: missing key codex.existing */} - existing + {t('codex.existing')}

- {/* TODO i18n: missing keys codex.nonBooleanForm / codex.discoveredFromFile */} - {current === null - ? 'Stored in a non-boolean form. Use raw TOML if you need to edit it.' - : "Discovered from the current file instead of CCS's built-in catalog."} + {current === null ? t('codex.nonBooleanForm') : t('codex.discoveredFromFile')}

{current === null ? ( diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx index 3b45b031..1b66f8b0 100644 --- a/ui/src/components/compatible-cli/codex-overview-tab.tsx +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -74,27 +74,21 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.howCodexWorks */} - How Codex works in CCS + {t('codex.howCodexWorks')}
  • {t('codex.nativeDesc')}
  • - {/* TODO i18n: missing key codex.nativeConfigLabel */} - Native config: ccs-codex and ccsx launch - native Codex using your saved defaults. + {t('codex.nativeConfigLabel')} {t('codex.nativeConfigDesc')}
  • - {/* TODO i18n: missing key codex.transientOverrides */} - Transient overrides: ccsxp (or{' '} - ccs codex --target codex) uses the CCS provider shortcut. + {t('codex.transientOverridesLabel')}{' '} + {t('codex.transientOverridesDesc')}
  • - {/* TODO i18n: missing key codex.cliproxyDefault */} - CLIProxy default: To make plain codex use CLIProxy, - set model_provider = "cliproxy" and add the recipe below. + {t('codex.cliproxyDefaultLabel')} {t('codex.cliproxyDefaultDesc')}
  • {t('codex.apiProfilesDefault')}
@@ -105,34 +99,42 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.runtimeInstall */} - Runtime install + {t('codex.runtimeInstall')}
{t('codex.status')} - {/* TODO i18n: missing keys codex.detected / codex.notFound */} - {diagnostics.binary.installed ? 'Detected' : 'Not found'} + {diagnostics.binary.installed ? t('codex.detected') : t('codex.notFound')}
- {/* TODO i18n: missing keys codex.detectionSource / codex.binaryPath / codex.installDirectory / codex.version / codex.nativeAliases / codex.ccsProviderShortcut / codex.configOverrideSupport */} - - + + - - - + + +
- --config override support + + {t('codex.configOverrideSupport')} + - {/* TODO i18n: missing keys codex.available / codex.missing */} - {diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'} + {diagnostics.binary.supportsConfigOverrides + ? t('codex.available') + : t('codex.missing')}
@@ -142,8 +144,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.cliproxyNativeCodex */} - CLIProxy-backed native Codex + {t('codex.cliproxyNativeCodex')} @@ -155,14 +156,10 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {

  • - {/* TODO i18n: missing key codex.builtInCcsxp */} - Built-in: Use ccsxp for the CCS provider - shortcut. + {t('codex.builtInLabel')} {t('codex.builtInCcsxpDesc')}
  • - {/* TODO i18n: missing key codex.nativeRecipe */} - Native: Configure the recipe below to use CLIProxy directly - with codex. + {t('codex.nativeRecipeLabel')} {t('codex.nativeRecipeDesc')}
@@ -173,29 +170,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
    -
  1. - {/* TODO i18n: missing key codex.saveProviderNamedCliproxy */} - Save a provider named cliproxy with the base URL and env key above. -
  2. -
  3. - {/* TODO i18n: missing key codex.inTopLevelSetDefault */} - In Top-level settings, set Default provider to{' '} - cliproxy. -
  4. -
  5. - {/* TODO i18n: missing key codex.exportCliproxyApiKey */} - Export CLIPROXY_API_KEY in your shell before launching native - Codex. -
  6. +
  7. {t('codex.saveProviderNamedCliproxy')}
  8. +
  9. {t('codex.inTopLevelSetDefault')}
  10. +
  11. {t('codex.exportCliproxyApiKey')}
) : ( -

- {/* 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. -

+

{t('codex.noConfigOverrides')}

)} @@ -204,8 +185,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.configFile */} - Config file + {t('codex.configFile')} @@ -218,21 +198,21 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { )} - {/* 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} + {t('codex.tomlWarning')}: {diagnostics.file.parseError}

)} {diagnostics.file.readError && (

- {/* TODO i18n: missing key codex.readWarning */} - Read warning: {diagnostics.file.readError} + {t('codex.readWarning')}: {diagnostics.file.readError}

)} @@ -243,64 +223,61 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.currentUserLayerSummary */} - Current user-layer summary + {t('codex.currentUserLayerSummary')} - {/* TODO i18n: missing key codex.notSet */}
- {/* TODO i18n: missing keys codex.providersCount / codex.profilesCount / codex.enabledFeaturesCount / codex.mcpServersCount */} - providers: {diagnostics.config.modelProviderCount} + {t('codex.providersCount', { count: diagnostics.config.modelProviderCount })} - profiles: {diagnostics.config.profileCount} + {t('codex.profilesCount', { count: diagnostics.config.profileCount })} - enabled features: {diagnostics.config.enabledFeatures.length} + {t('codex.enabledFeaturesCount', { + count: diagnostics.config.enabledFeatures.length, + })} - MCP servers: {diagnostics.config.mcpServerCount} + {t('codex.mcpServersCount', { count: diagnostics.config.mcpServerCount })}
{diagnostics.config.topLevelKeys.length > 0 && (

- {/* TODO i18n: missing key codex.userLayerKeysPresent */} - User-layer keys present + {t('codex.userLayerKeysPresent')}

{diagnostics.config.topLevelKeys.map((key) => ( @@ -317,22 +294,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.runtimeVsProvider */} - Runtime vs provider + {t('codex.runtimeVsProvider')} @@ -373,8 +346,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.honorsSavedNativeConfig */} - Honors saved native user config + {t('codex.honorsSavedNativeConfig')}
@@ -390,15 +362,11 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.usesTransientOverrides */} - Uses transient overrides + {t('codex.usesTransientOverrides')} ) : ( -

- {/* TODO i18n: missing key codex.unavailableNoConfig */} - Unavailable (Codex build lacks --config support). -

+

{t('codex.unavailableNoConfig')}

)}
@@ -423,8 +391,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { {entry.label} - {/* TODO i18n: missing keys common.yes / common.no */} - {entry.supported ? 'Yes' : 'No'} + {entry.supported ? t('codex.yes') : t('codex.no')} {entry.notes} @@ -440,8 +407,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { - {/* TODO i18n: missing key codex.warnings */} - Warnings + {t('codex.warningsTitle')} diff --git a/ui/src/components/shared/settings-dialog.tsx b/ui/src/components/shared/settings-dialog.tsx index 3f79d35d..e1213183 100644 --- a/ui/src/components/shared/settings-dialog.tsx +++ b/ui/src/components/shared/settings-dialog.tsx @@ -120,7 +120,7 @@ function SettingsDialogContent({ try { settingsToSave = JSON.parse(rawJsonContent); } catch { - throw new Error('Invalid JSON'); + throw new Error(i18n.t('settingsDialog.invalidJson')); } } else { // Use form-based edits @@ -147,7 +147,7 @@ function SettingsDialogContent({ } if (!res.ok) { - throw new Error('Failed to save'); + throw new Error(i18n.t('settingsDialog.failedSave')); } return res.json(); diff --git a/ui/src/components/updates/support-entry-card.tsx b/ui/src/components/updates/support-entry-card.tsx index d9ab873e..847fd554 100644 --- a/ui/src/components/updates/support-entry-card.tsx +++ b/ui/src/components/updates/support-entry-card.tsx @@ -3,7 +3,7 @@ import { ArrowUpRight } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { - SUPPORT_SCOPE_LABELS, + getSupportScopeLabels, type CliSupportEntry, type SupportScope, } from '@/lib/support-updates-catalog'; @@ -23,6 +23,7 @@ const SCOPE_STYLES: Record = { export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) { const { t } = useTranslation(); + const scopeLabels = getSupportScopeLabels(); const pillarLabels: { key: keyof CliSupportEntry['pillars']; label: string }[] = [ { key: 'baseUrl', label: t('profileDialog.baseUrl') }, { key: 'auth', label: t('copilotPage.auth') }, @@ -41,7 +42,7 @@ export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) {
- {SUPPORT_SCOPE_LABELS[entry.scope]} + {scopeLabels[entry.scope]} diff --git a/ui/src/components/updates/updates-details-panel.tsx b/ui/src/components/updates/updates-details-panel.tsx index 566b9b36..f722ee82 100644 --- a/ui/src/components/updates/updates-details-panel.tsx +++ b/ui/src/components/updates/updates-details-panel.tsx @@ -9,7 +9,7 @@ import { SupportStatusBadge } from '@/components/updates/support-status-badge'; import { NoticeProgressBadge } from '@/components/updates/notice-progress-badge'; import { UpdatesNoticeActionRow } from '@/components/updates/updates-notice-action-row'; import { - SUPPORT_SCOPE_LABELS, + getSupportScopeLabels, formatCatalogDate, type CliSupportEntry, type SupportNotice, @@ -31,6 +31,7 @@ export function UpdatesDetailsPanel({ onUpdateProgress: (nextState: UpdatableNoticeProgress) => void; }) { const { t } = useTranslation(); + const scopeLabels = getSupportScopeLabels(); if (!notice) { return (
@@ -109,7 +110,7 @@ export function UpdatesDetailsPanel({

{entry.name}

- {SUPPORT_SCOPE_LABELS[entry.scope]} + {scopeLabels[entry.scope]}
diff --git a/ui/src/hooks/use-accounts.ts b/ui/src/hooks/use-accounts.ts index d1461b67..890ff1ac 100644 --- a/ui/src/hooks/use-accounts.ts +++ b/ui/src/hooks/use-accounts.ts @@ -135,12 +135,16 @@ export function useUpdateAccountContext() { }) => api.accounts.updateContext(name, { context_mode, context_group, continuity_mode }), onSuccess: (_data, vars) => { queryClient.invalidateQueries({ queryKey: ['accounts'] }); + const normalizedGroup = (vars.context_group || 'default') + .trim() + .toLowerCase() + .replace(/\s+/g, '-'); const contextSummary = vars.context_mode === 'shared' ? vars.continuity_mode === 'deeper' - ? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, deeper continuity)` - : `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, standard)` - : 'isolated'; + ? `${t('accountsPage.sharedDeeper')} (${normalizedGroup})` + : `${t('accountsPage.sharedStandard')} (${normalizedGroup})` + : t('accountsPage.isolated'); toast.success(t('toasts.contextUpdated', { name: vars.name, summary: contextSummary })); }, onError: (error: Error) => { @@ -180,18 +184,14 @@ 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.` + t('toasts.legacyConfirmPartial', { updated: updatedCount, failed: failedCount }) ); return; } 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.` - ); + toast.error(t('toasts.legacyConfirmAllFailed', { failed: failedCount })); return; } diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts index 3f41a1e3..ce847823 100644 --- a/ui/src/hooks/use-cliproxy-auth-flow.ts +++ b/ui/src/hooks/use-cliproxy-auth-flow.ts @@ -131,8 +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.', + error: t('toasts.providerAuthTimeout'), })); } return; @@ -161,8 +160,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'; + const errorMsg = t('toasts.providerAccountRegistrationFailed'); toast.error(errorMsg); setState((prev) => ({ ...prev, @@ -226,11 +224,10 @@ 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 - : 'Lost contact with the auth status endpoint'; + : t('toasts.providerLostStatusEndpoint'); toast.error(message); setState((prev) => ({ ...prev, @@ -245,10 +242,9 @@ export function useCliproxyAuthFlow() { 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}`, + error: t('toasts.providerUnknown', { provider }), }); return; } @@ -319,13 +315,12 @@ 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 : success - ? 'Authenticated account could not be registered' - : 'Authentication failed'; + ? t('toasts.providerAccountRegistrationFailed') + : t('auth.loginFailed'); toast.error(errorMsg); setState((prev) => ({ ...prev, @@ -370,8 +365,8 @@ 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'; + const errorMsg = + typeof data.error === 'string' ? data.error : t('toasts.providerStartOAuthFailed'); throw new Error(errorMsg); } @@ -418,7 +413,7 @@ export function useCliproxyAuthFlow() { setState(INITIAL_STATE); return; } - const message = error instanceof Error ? error.message : 'Authentication failed'; + const message = error instanceof Error ? error.message : t('auth.loginFailed'); toast.error(message); setState((prev) => ({ ...prev, @@ -427,7 +422,7 @@ export function useCliproxyAuthFlow() { })); } }, - [isActiveAttempt, pollStatus, stopPolling, queryClient] + [isActiveAttempt, pollStatus, stopPolling, queryClient, t] ); const cancelAuth = useCallback(() => { @@ -486,21 +481,20 @@ export function useCliproxyAuthFlow() { 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 : success - ? 'Authenticated account could not be registered' - : 'Callback submission failed'; + ? t('toasts.providerAccountRegistrationFailed') + : t('toasts.providerCallbackFailed'); throw new Error(errorMsg); } } catch (error) { if (!isActiveAttempt(attemptId)) { return; } - // TODO i18n: missing key for 'Failed to submit callback' - const message = error instanceof Error ? error.message : 'Failed to submit callback'; + const message = + error instanceof Error ? error.message : t('toasts.providerSubmitCallbackFailed'); toast.error(message); setState((prev) => ({ ...prev, isSubmittingCallback: false, error: message })); } diff --git a/ui/src/lib/codex-config.ts b/ui/src/lib/codex-config.ts index 35f57b14..11c4d602 100644 --- a/ui/src/lib/codex-config.ts +++ b/ui/src/lib/codex-config.ts @@ -1,3 +1,5 @@ +import i18n from '@/lib/i18n'; + export interface CodexTopLevelSettingsView { model: string | null; modelReasoningEffort: string | null; @@ -58,46 +60,73 @@ base_url = "http://127.0.0.1:8317/api/provider/codex" env_key = "CLIPROXY_API_KEY" wire_api = "responses"`; -export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [ - { - name: 'multi_agent', - 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', // 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', // TODO i18n: missing key for codex feature - description: 'Reuse shell environment snapshots.', // TODO i18n: missing key - }, - { - name: 'apply_patch_freeform', - 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.' }, // TODO i18n: missing keys - { - name: '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', // 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.' }, // 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', // TODO i18n: missing key for codex feature - description: 'Route eligible approvals through the guardian flow.', // TODO i18n: missing key - }, -]; +export const KNOWN_CODEX_FEATURE_NAMES = [ + 'multi_agent', + 'unified_exec', + 'shell_snapshot', + 'apply_patch_freeform', + 'js_repl', + 'runtime_metrics', + 'prevent_idle_sleep', + 'fast_mode', + 'apps', + 'smart_approvals', +] as const; + +export function getKnownCodexFeatures(): CodexFeatureCatalogEntry[] { + return [ + { + name: 'multi_agent', + label: i18n.t('codex.featureMultiAgentLabel'), + description: i18n.t('codex.featureMultiAgentDesc'), + }, + { + name: 'unified_exec', + label: i18n.t('codex.featureUnifiedExecLabel'), + description: i18n.t('codex.featureUnifiedExecDesc'), + }, + { + name: 'shell_snapshot', + label: i18n.t('codex.featureShellSnapshotLabel'), + description: i18n.t('codex.featureShellSnapshotDesc'), + }, + { + name: 'apply_patch_freeform', + label: i18n.t('codex.featureApplyPatchLabel'), + description: i18n.t('codex.featureApplyPatchDesc'), + }, + { + name: 'js_repl', + label: i18n.t('codex.featureJsReplLabel'), + description: i18n.t('codex.featureJsReplDesc'), + }, + { + name: 'runtime_metrics', + label: i18n.t('codex.featureRuntimeMetricsLabel'), + description: i18n.t('codex.featureRuntimeMetricsDesc'), + }, + { + name: 'prevent_idle_sleep', + label: i18n.t('codex.featurePreventIdleSleepLabel'), + description: i18n.t('codex.featurePreventIdleSleepDesc'), + }, + { + name: 'fast_mode', + label: i18n.t('codex.featureFastModeLabel'), + description: i18n.t('codex.featureFastModeDesc'), + }, + { + name: 'apps', + label: i18n.t('codex.featureAppsLabel'), + description: i18n.t('codex.featureAppsDesc'), + }, + { + name: 'smart_approvals', + label: i18n.t('codex.featureSmartApprovalsLabel'), + description: i18n.t('codex.featureSmartApprovalsDesc'), + }, + ]; +} function asObject(value: unknown): Record | null { return typeof value === 'object' && value !== null && !Array.isArray(value) @@ -222,9 +251,9 @@ export function readCodexFeatureState( const features = asObject(config?.features); const state: Record = {}; - for (const feature of KNOWN_CODEX_FEATURES) { - const value = features?.[feature.name]; - state[feature.name] = typeof value === 'boolean' ? value : null; + for (const featureName of KNOWN_CODEX_FEATURE_NAMES) { + const value = features?.[featureName]; + state[featureName] = typeof value === 'boolean' ? value : null; } if (features) { diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 99c7133d..ce95fc3e 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -1066,6 +1066,8 @@ const resources = { }, apiProfiles: { title: 'Profiles', + sidebarTitle: 'API Profiles', + sidebarSubtitle: 'Premium APIs, local runtimes, custom endpoints', new: 'New', searchPlaceholder: 'Search profiles...', loadingProfiles: 'Loading profiles...', @@ -1087,6 +1089,19 @@ const resources = { unsavedChangesDesc: 'You have unsaved changes in "{{current}}". Discard and switch to "{{next}}"?', discardSwitch: 'Discard and Switch', + noOrphansFound: 'No orphan profile settings found', + confirmRegisterOrphans: + 'Found {{total}} orphan profile setting file(s), with {{valid}} ready to register. Register them now?', + registeredWithSkipped: ' ({{count}} skipped)', + registeredProfiles: 'Registered {{count}} profile(s)', + copyPrompt: 'Copy "{{name}}" to which new profile name?', + destinationEmpty: 'Destination profile name cannot be empty', + exportRedacted: + 'Export created with redacted token. Use include-secrets flow in CLI if needed.', + exportDownloaded: 'Profile export downloaded', + importFailed: 'Failed to import profile bundle', + discoverOrphans: 'Discover orphan profiles', + importProfileBundle: 'Import profile bundle', }, accountsPage: { title: 'Accounts', @@ -1578,6 +1593,11 @@ const resources = { fiveHourResets: '5h resets {{time}}', weeklyResets: 'Weekly resets {{time}}', }, + utils: { + codeReview5h: 'Code review (5h)', + codeReviewWeekly: 'Code review (weekly)', + codeReview: 'Code review', + }, sponsorButton: { title: 'Sponsor this project on GitHub', sponsor: 'Sponsor', @@ -1635,6 +1655,8 @@ const resources = { conflictDesc: 'This settings file was modified by another process. Overwrite with your changes or discard?', overwrite: 'Overwrite', + invalidJson: 'Invalid JSON', + failedSave: 'Failed to save', }, // ======================================== @@ -1852,12 +1874,60 @@ const resources = { nativeCodexRuntime: 'Native Codex Runtime', ccsCodexProvider: 'CCS Codex provider / bridge', codexDocs: 'Codex docs', + howCodexWorks: 'How Codex works in CCS', supportedFlows: 'Supported flows', twoSupportedPaths: 'Two supported paths:', nativeLabel: 'Native:', nativeDesc: 'Codex is a first-class, runtime-only target in CCS v1.', + nativeConfigLabel: 'Native config:', + nativeConfigDesc: 'ccs-codex and ccsx launch native Codex using your saved defaults.', + transientOverridesLabel: 'Transient overrides:', + transientOverridesDesc: + 'ccsxp (or ccs codex --target codex) uses the CCS provider shortcut.', + cliproxyDefaultLabel: 'CLIProxy default:', + cliproxyDefaultDesc: + 'To make plain codex use CLIProxy, set model_provider = "cliproxy" and add the recipe below.', ccsBridge: 'CCS Bridge', apiProfilesDefault: 'API profiles continue to default to Claude or Droid.', + runtimeInstall: 'Runtime install', + detected: 'Detected', + notFound: 'Not found', + detectionSource: 'Detection source', + binaryPath: 'Binary path', + installDirectory: 'Install directory', + versionLabel: 'Version', + nativeAliases: 'Native aliases', + ccsProviderShortcut: 'CCS provider shortcut', + configOverrideSupport: '--config override support', + available: 'Available', + missing: 'Missing', + cliproxyNativeCodex: 'CLIProxy-backed native Codex', + builtInLabel: 'Built-in:', + builtInCcsxpDesc: 'Use ccsxp for the CCS provider shortcut.', + nativeRecipeLabel: 'Native:', + nativeRecipeDesc: 'Configure the recipe below to use CLIProxy directly with codex.', + saveProviderNamedCliproxy: + 'Save a provider named cliproxy with the base URL and env key above.', + inTopLevelSetDefault: 'In Top-level settings, set Default provider to cliproxy.', + exportCliproxyApiKey: + 'Export CLIPROXY_API_KEY in your shell before launching native 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.', + configFile: 'Config file', + path: 'Path', + resolved: 'Resolved', + size: 'Size', + lastModified: 'Last modified', + tomlWarning: 'TOML warning', + readWarning: 'Read warning', + currentUserLayerSummary: 'Current user-layer summary', + activeProfile: 'Active profile', + notSet: 'Not set', + providersCount: 'providers: {{count}}', + profilesCount: 'profiles: {{count}}', + enabledFeaturesCount: 'enabled features: {{count}}', + mcpServersCount: 'mcp servers: {{count}}', + userLayerKeysPresent: 'User-layer keys present', recommendedSetupFlow: 'Recommended setup flow', fastestPath: 'Fastest path', officialChannels: 'Official Channels', @@ -1866,6 +1936,9 @@ const resources = { runBuiltInCodex: 'Run built-in Codex on Codex', runBuiltInCodexExplicit: 'Run built-in Codex on Codex (explicit)', openCodexDashboard: 'Open Codex dashboard', + nativeShortAlias: 'Native short alias', + ccsCodexShortcut: 'CCS Codex shortcut', + explicitProviderRoute: 'Explicit provider route', status: 'Status', profiles: 'Profiles', createNewProfile: 'Create new profile', @@ -1895,6 +1968,10 @@ const resources = { untrusted: 'untrusted', noProjectTrustEntries: 'No explicit project trust entries saved.', codexNativeRecipe: 'Saved native Codex recipe', + runtimeVsProvider: 'Runtime vs provider', + honorsSavedNativeConfig: 'Honors saved native user config', + usesTransientOverrides: 'Uses transient overrides', + unavailableNoConfig: 'Unavailable (Codex build lacks --config support).', gptContextCap: 'GPT-5.4 context cap', usageLimitCost: 'Usage-limit cost above 272K', longContextOverride: 'Long context override', @@ -1911,6 +1988,37 @@ const resources = { configYaml: 'config.yaml', flow: 'Flow', docsTab: 'Docs', + yes: 'Yes', + no: 'No', + warningsTitle: 'Warnings', + features: 'Features', + featuresDesc: 'Toggle the supported Codex feature flags CCS can safely manage.', + configOnlyFlags: 'Existing config-only flags', + configOnlyFlagsDesc: + 'These feature keys already exist in your `config.toml`, so CCS can surface them without claiming full catalog coverage.', + existing: 'existing', + nonBooleanForm: 'Stored in a non-boolean form. Use raw TOML if you need to edit it.', + discoveredFromFile: "Discovered from the current file instead of CCS's built-in catalog.", + featureMultiAgentLabel: 'Multi-agent', + featureMultiAgentDesc: 'Enable subagent collaboration tools.', + featureUnifiedExecLabel: 'Unified exec', + featureUnifiedExecDesc: 'Use the PTY-backed unified exec tool.', + featureShellSnapshotLabel: 'Shell snapshot', + featureShellSnapshotDesc: 'Reuse shell environment snapshots.', + featureApplyPatchLabel: 'Apply patch', + featureApplyPatchDesc: 'Enable freeform apply_patch edits.', + featureJsReplLabel: 'JS REPL', + featureJsReplDesc: 'Enable the Node-backed JavaScript REPL.', + featureRuntimeMetricsLabel: 'Runtime metrics', + featureRuntimeMetricsDesc: 'Collect Codex runtime metrics.', + featurePreventIdleSleepLabel: 'Prevent idle sleep', + featurePreventIdleSleepDesc: 'Keep the machine awake while active.', + featureFastModeLabel: 'Fast mode', + featureFastModeDesc: 'Allow the fast service tier path.', + featureAppsLabel: 'Apps', + featureAppsDesc: 'Enable ChatGPT Apps and connectors support.', + featureSmartApprovalsLabel: 'Smart approvals', + featureSmartApprovalsDesc: 'Route eligible approvals through the guardian flow.', }, droidSettings: { quickControls: 'Quick Controls', @@ -2022,6 +2130,13 @@ const resources = { syncFailed: 'Sync failed: {{error}}', providerAuthSuccess: '{{provider}} authentication successful', providerDeviceCodeInCallback: 'Provider returned Device Code flow in callback mode', + providerAuthTimeout: 'Authentication timed out. Please try again.', + providerAccountRegistrationFailed: 'Authenticated account could not be registered', + providerLostStatusEndpoint: 'Lost contact with the auth status endpoint', + providerUnknown: 'Unknown provider: {{provider}}', + providerStartOAuthFailed: 'Failed to start OAuth', + providerCallbackFailed: 'Callback submission failed', + providerSubmitCallbackFailed: 'Failed to submit callback', loggingConfigSaved: 'Logging configuration saved.', loggingConfigSaveFailed: 'Failed to save logging configuration.', unifiedConfigUpdated: 'Configuration updated successfully', @@ -2039,6 +2154,10 @@ const resources = { legacyConfirmFailed: 'Legacy account "{{name}}" failed confirmation: {{error}}', legacyConfirmSuccess_one: '{{count}} legacy account confirmed', legacyConfirmSuccess_other: '{{count}} legacy accounts confirmed', + legacyConfirmPartial: + 'Confirmed {{updated}} legacy account(s), but {{failed}} update(s) failed. Refreshed account state.', + legacyConfirmAllFailed: + 'Failed to confirm {{failed}} legacy account(s). Refreshed account state.', noLegacyAccounts: 'No legacy accounts need confirmation', routingStrategySet: 'Routing strategy set to {{strategy}}', variantCreated: 'Variant created successfully', @@ -3315,6 +3434,8 @@ const resources = { }, apiProfiles: { title: 'API 配置', + sidebarTitle: 'API 配置', + sidebarSubtitle: '高级 API、本地运行时、自定义端点', new: '新建', searchPlaceholder: '搜索配置...', loadingProfiles: '正在加载配置...', @@ -3334,6 +3455,19 @@ const resources = { unsavedChangesTitle: '未保存的更改', unsavedChangesDesc: '你在“{{current}}”中有未保存内容,是否丢弃并切换到“{{next}}”?', discardSwitch: '丢弃并切换', + noOrphansFound: '未发现孤立配置', + confirmRegisterOrphans: + '发现 {{total}} 个孤立配置文件,其中 {{valid}} 个可直接注册。现在注册吗?', + registeredWithSkipped: '(跳过 {{count}} 个)', + registeredProfiles: '已注册 {{count}} 个配置', + copyPrompt: '将“{{name}}”复制为哪个新配置名称?', + destinationEmpty: '目标配置名称不能为空', + exportRedacted: + '导出内容中的令牌已脱敏。如需包含密钥,请改用 CLI 的 include-secrets 流程。', + exportDownloaded: '配置导出已下载', + importFailed: '导入配置包失败', + discoverOrphans: '发现孤立配置', + importProfileBundle: '导入配置包', }, accountsPage: { title: '账号', @@ -3812,6 +3946,11 @@ const resources = { fiveHourResets: '5 小时重置于 {{time}}', weeklyResets: '每周重置于 {{time}}', }, + utils: { + codeReview5h: '代码审查(5 小时)', + codeReviewWeekly: '代码审查(每周)', + codeReview: '代码审查', + }, sponsorButton: { title: '在 GitHub 上赞助此项目', sponsor: '赞助', @@ -3868,6 +4007,8 @@ const resources = { conflictTitle: '文件被外部修改', conflictDesc: '此设置文件已被其他进程修改。用你的更改覆盖还是丢弃?', overwrite: '覆盖', + invalidJson: 'JSON 无效', + failedSave: '保存失败', }, setupWizard: { title: '快速设置向导', @@ -4131,6 +4272,90 @@ const resources = { configYaml: 'config.yaml', flow: '工作流', docsTab: '文档', + features: '功能', + featuresDesc: '切换 CCS 可安全管理的 Codex 功能开关。', + configOnlyFlags: '仅配置文件中的现有开关', + configOnlyFlagsDesc: + '这些功能键已存在于你的 `config.toml` 中,因此 CCS 可以显示它们,而不必宣称自己覆盖了完整目录。', + existing: '已存在', + nonBooleanForm: '该值以非布尔形式保存。如果需要编辑,请使用原始 TOML。', + discoveredFromFile: '这是从当前文件中发现的,而不是来自 CCS 内置目录。', + featureMultiAgentLabel: 'Multi-agent', + featureMultiAgentDesc: '启用子代理协作工具。', + featureUnifiedExecLabel: 'Unified exec', + featureUnifiedExecDesc: '使用基于 PTY 的统一 exec 工具。', + featureShellSnapshotLabel: 'Shell snapshot', + featureShellSnapshotDesc: '复用 shell 环境快照。', + featureApplyPatchLabel: 'Apply patch', + featureApplyPatchDesc: '启用自由格式 apply_patch 编辑。', + featureJsReplLabel: 'JS REPL', + featureJsReplDesc: '启用基于 Node 的 JavaScript REPL。', + featureRuntimeMetricsLabel: 'Runtime metrics', + featureRuntimeMetricsDesc: '收集 Codex 运行时指标。', + featurePreventIdleSleepLabel: 'Prevent idle sleep', + featurePreventIdleSleepDesc: '保持机器在运行期间不休眠。', + featureFastModeLabel: 'Fast mode', + featureFastModeDesc: '允许使用快速服务层路径。', + featureAppsLabel: 'Apps', + featureAppsDesc: '启用 ChatGPT Apps 和连接器支持。', + featureSmartApprovalsLabel: 'Smart approvals', + featureSmartApprovalsDesc: '将符合条件的审批交给 guardian 流程处理。', + howCodexWorks: 'Codex 在 CCS 中的工作方式', + nativeConfigLabel: '原生配置:', + nativeConfigDesc: 'ccs-codex 和 ccsx 会使用你保存的默认值启动原生 Codex。', + transientOverridesLabel: '临时覆盖:', + transientOverridesDesc: 'ccsxp(或 ccs codex --target codex)会使用 CCS 提供商快捷方式。', + cliproxyDefaultLabel: 'CLIProxy 默认:', + cliproxyDefaultDesc: + '如果你想让普通的 codex 走 CLIProxy,请设置 model_provider = "cliproxy",并加入下面的配方。', + runtimeInstall: '运行时安装', + detected: '已检测到', + notFound: '未找到', + detectionSource: '检测来源', + binaryPath: '二进制路径', + installDirectory: '安装目录', + versionLabel: '版本', + nativeAliases: '原生别名', + ccsProviderShortcut: 'CCS 提供商快捷方式', + configOverrideSupport: '--config 覆盖支持', + available: '可用', + missing: '缺失', + cliproxyNativeCodex: '由 CLIProxy 支持的原生 Codex', + builtInLabel: '内置:', + builtInCcsxpDesc: '使用 ccsxp 作为 CCS 提供商快捷方式。', + nativeRecipeLabel: '原生:', + nativeRecipeDesc: '按下面的配方配置,即可让 codex 直接走 CLIProxy。', + saveProviderNamedCliproxy: + '保存一个名为 cliproxy 的 provider,并填入上面的 base URL 与 env key。', + inTopLevelSetDefault: '在顶层设置中,将 Default provider 设为 cliproxy。', + exportCliproxyApiKey: '在启动原生 Codex 之前,在 shell 中导出 CLIPROXY_API_KEY。', + noConfigOverrides: + '这个 Codex 版本仍然可以走原生路径,但在检测到的二进制支持 --config 覆盖之前,ccsxp 或 ccs codex --target codex 这样的 CCS 路由不可用。', + configFile: '配置文件', + path: '路径', + resolved: '已解析', + size: '大小', + lastModified: '最后修改', + tomlWarning: 'TOML 警告', + readWarning: '读取警告', + currentUserLayerSummary: '当前用户层摘要', + activeProfile: '当前激活的配置', + notSet: '未设置', + providersCount: 'providers: {{count}}', + profilesCount: 'profiles: {{count}}', + enabledFeaturesCount: 'enabled features: {{count}}', + mcpServersCount: 'mcp servers: {{count}}', + userLayerKeysPresent: '已存在的用户层键', + nativeShortAlias: '原生短别名', + ccsCodexShortcut: 'CCS Codex 快捷方式', + explicitProviderRoute: '显式 provider 路径', + runtimeVsProvider: '运行时与 provider 的区别', + honorsSavedNativeConfig: '遵循已保存的原生用户配置', + usesTransientOverrides: '使用临时覆盖', + unavailableNoConfig: '不可用(当前 Codex 版本缺少 --config 支持)。', + yes: '是', + no: '否', + warningsTitle: '警告', }, droidSettings: { quickControls: '快捷控制', @@ -4234,6 +4459,13 @@ const resources = { syncFailed: '同步失败:{{error}}', providerAuthSuccess: '{{provider}} 认证成功', providerDeviceCodeInCallback: '提供商在回调模式中返回了设备码流程', + providerAuthTimeout: '认证超时,请重试。', + providerAccountRegistrationFailed: '已认证账号无法注册', + providerLostStatusEndpoint: '与认证状态端点失去连接', + providerUnknown: '未知提供商:{{provider}}', + providerStartOAuthFailed: '启动 OAuth 失败', + providerCallbackFailed: '回调提交失败', + providerSubmitCallbackFailed: '提交回调失败', loggingConfigSaved: '日志配置已保存。', loggingConfigSaveFailed: '保存日志配置失败。', unifiedConfigUpdated: '配置更新成功', @@ -4250,6 +4482,9 @@ const resources = { legacyConfirmFailed: '旧版账号「{{name}}」确认失败:{{error}}', legacyConfirmSuccess_one: '{{count}} 个旧版账号已确认', legacyConfirmSuccess_other: '{{count}} 个旧版账号已确认', + legacyConfirmPartial: + '已确认 {{updated}} 个旧版账号,但有 {{failed}} 个更新失败。账号状态已刷新。', + legacyConfirmAllFailed: '确认 {{failed}} 个旧版账号失败。账号状态已刷新。', noLegacyAccounts: '没有需要确认的旧版账号', routingStrategySet: '路由策略已设为 {{strategy}}', variantCreated: '变体创建成功', @@ -5604,6 +5839,8 @@ const resources = { }, apiProfiles: { title: 'Hồ sơ API', + sidebarTitle: 'Hồ sơ API', + sidebarSubtitle: 'API cao cấp, runtime cục bộ, endpoint tùy chỉnh', new: 'Mới', searchPlaceholder: 'Tìm kiếm hồ sơ...', loadingProfiles: 'Đang tải hồ sơ...', @@ -5625,6 +5862,18 @@ const resources = { unsavedChangesDesc: 'Bạn có những thay đổi chưa được lưu trong "{{current}}". Hủy và chuyển sang "{{next}}"?', discardSwitch: 'Loại bỏ và chuyển đổi', + noOrphansFound: 'Không tìm thấy cấu hình hồ sơ mồ côi', + confirmRegisterOrphans: + 'Đã tìm thấy {{total}} tệp cấu hình hồ sơ mồ côi, trong đó {{valid}} tệp sẵn sàng đăng ký. Đăng ký ngay?', + registeredWithSkipped: ' (bỏ qua {{count}})', + registeredProfiles: 'Đã đăng ký {{count}} hồ sơ', + copyPrompt: 'Sao chép "{{name}}" sang tên hồ sơ mới nào?', + destinationEmpty: 'Tên hồ sơ đích không được để trống', + exportRedacted: 'Bản xuất đã được che token. Dùng luồng include-secrets trong CLI nếu cần.', + exportDownloaded: 'Đã tải xuống bản xuất hồ sơ', + importFailed: 'Không nhập được gói hồ sơ', + discoverOrphans: 'Phát hiện hồ sơ mồ côi', + importProfileBundle: 'Nhập gói hồ sơ', }, accountsPage: { title: 'Tài khoản', @@ -6116,6 +6365,11 @@ const resources = { fiveHourResets: 'Reset 5h lúc {{time}}', weeklyResets: 'Reset hàng tuần lúc {{time}}', }, + utils: { + codeReview5h: 'Đánh giá mã (5h)', + codeReviewWeekly: 'Đánh giá mã (hàng tuần)', + codeReview: 'Đánh giá mã', + }, sponsorButton: { title: 'Tài trợ dự án này trên GitHub', sponsor: 'Tài trợ', @@ -6173,6 +6427,8 @@ const resources = { 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 đè', + invalidJson: 'JSON không hợp lệ', + failedSave: 'Không lưu được', }, setupWizard: { title: 'Trình thiết lập nhanh', @@ -6438,6 +6694,91 @@ const resources = { configYaml: 'config.yaml', flow: 'Luồng', docsTab: 'Tài liệu', + features: 'Tính năng', + featuresDesc: 'Bật/tắt các cờ tính năng Codex mà CCS có thể quản lý an toàn.', + configOnlyFlags: 'Cờ chỉ có trong cấu hình', + configOnlyFlagsDesc: + 'Các khóa này đã tồn tại trong `config.toml`, nên CCS có thể hiển thị chúng mà không cần tuyên bố bao phủ toàn bộ danh mục.', + existing: 'đã có', + nonBooleanForm: + 'Giá trị đang được lưu ở dạng không phải boolean. Hãy dùng TOML thô nếu bạn muốn chỉnh sửa.', + discoveredFromFile: 'Được phát hiện từ tệp hiện tại thay vì từ danh mục tích hợp của CCS.', + featureMultiAgentLabel: 'Multi-agent', + featureMultiAgentDesc: 'Bật các công cụ cộng tác giữa subagent.', + featureUnifiedExecLabel: 'Unified exec', + featureUnifiedExecDesc: 'Dùng công cụ exec hợp nhất dựa trên PTY.', + featureShellSnapshotLabel: 'Shell snapshot', + featureShellSnapshotDesc: 'Tái sử dụng snapshot môi trường shell.', + featureApplyPatchLabel: 'Apply patch', + featureApplyPatchDesc: 'Bật chỉnh sửa apply_patch dạng tự do.', + featureJsReplLabel: 'JS REPL', + featureJsReplDesc: 'Bật JavaScript REPL dựa trên Node.', + featureRuntimeMetricsLabel: 'Runtime metrics', + featureRuntimeMetricsDesc: 'Thu thập chỉ số runtime của Codex.', + featurePreventIdleSleepLabel: 'Prevent idle sleep', + featurePreventIdleSleepDesc: 'Giữ máy không ngủ khi đang hoạt động.', + featureFastModeLabel: 'Fast mode', + featureFastModeDesc: 'Cho phép dùng đường dẫn fast service tier.', + featureAppsLabel: 'Apps', + featureAppsDesc: 'Bật hỗ trợ ChatGPT Apps và connectors.', + featureSmartApprovalsLabel: 'Smart approvals', + featureSmartApprovalsDesc: 'Đưa các phê duyệt phù hợp qua luồng guardian.', + howCodexWorks: 'Codex hoạt động trong CCS như thế nào', + nativeConfigLabel: 'Cấu hình gốc:', + nativeConfigDesc: 'ccs-codex và ccsx khởi chạy Codex gốc bằng các mặc định bạn đã lưu.', + transientOverridesLabel: 'Ghi đè tạm thời:', + transientOverridesDesc: + 'ccsxp (hoặc ccs codex --target codex) dùng đường tắt nhà cung cấp CCS.', + cliproxyDefaultLabel: 'Mặc định CLIProxy:', + cliproxyDefaultDesc: + 'Để lệnh codex thông thường đi qua CLIProxy, hãy đặt model_provider = "cliproxy" và dùng công thức bên dưới.', + runtimeInstall: 'Cài đặt runtime', + detected: 'Đã phát hiện', + notFound: 'Không tìm thấy', + detectionSource: 'Nguồn phát hiện', + binaryPath: 'Đường dẫn binary', + installDirectory: 'Thư mục cài đặt', + versionLabel: 'Phiên bản', + nativeAliases: 'Bí danh gốc', + ccsProviderShortcut: 'Lối tắt nhà cung cấp CCS', + configOverrideSupport: 'Hỗ trợ ghi đè --config', + available: 'Có sẵn', + missing: 'Thiếu', + cliproxyNativeCodex: 'Codex gốc dùng CLIProxy', + builtInLabel: 'Tích hợp:', + builtInCcsxpDesc: 'Dùng ccsxp cho lối tắt nhà cung cấp CCS.', + nativeRecipeLabel: 'Gốc:', + nativeRecipeDesc: 'Cấu hình công thức bên dưới để dùng CLIProxy trực tiếp với codex.', + saveProviderNamedCliproxy: 'Lưu một provider tên cliproxy với base URL và env key ở trên.', + inTopLevelSetDefault: 'Trong Top-level settings, đặt Default provider là cliproxy.', + exportCliproxyApiKey: 'Xuất CLIPROXY_API_KEY trong shell trước khi chạy Codex gốc.', + noConfigOverrides: + 'Bản Codex này vẫn có thể dùng đường gốc, nhưng định tuyến Codex qua CCS như ccsxp hoặc ccs codex --target codex sẽ không hoạt động cho đến khi binary hỗ trợ ghi đè --config.', + configFile: 'Tệp cấu hình', + path: 'Đường dẫn', + resolved: 'Đã phân giải', + size: 'Kích thước', + lastModified: 'Sửa đổi lần cuối', + tomlWarning: 'Cảnh báo TOML', + readWarning: 'Cảnh báo đọc', + currentUserLayerSummary: 'Tóm tắt lớp người dùng hiện tại', + activeProfile: 'Hồ sơ đang dùng', + notSet: 'Chưa đặt', + providersCount: 'providers: {{count}}', + profilesCount: 'profiles: {{count}}', + enabledFeaturesCount: 'enabled features: {{count}}', + mcpServersCount: 'mcp servers: {{count}}', + userLayerKeysPresent: 'Các khóa lớp người dùng hiện có', + nativeShortAlias: 'Bí danh gốc ngắn', + ccsCodexShortcut: 'Lối tắt CCS Codex', + explicitProviderRoute: 'Tuyến provider tường minh', + runtimeVsProvider: 'Runtime so với provider', + honorsSavedNativeConfig: 'Tuân theo cấu hình gốc đã lưu', + usesTransientOverrides: 'Dùng ghi đè tạm thời', + unavailableNoConfig: 'Không khả dụng (bản Codex này thiếu hỗ trợ --config).', + yes: 'Có', + no: 'Không', + warningsTitle: 'Cảnh báo', }, droidSettings: { quickControls: 'Điều khiển nhanh', @@ -6541,6 +6882,13 @@ const resources = { 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', + providerAuthTimeout: 'Đã hết thời gian xác thực. Vui lòng thử lại.', + providerAccountRegistrationFailed: 'Không thể đăng ký tài khoản đã xác thực', + providerLostStatusEndpoint: 'Mất kết nối với điểm cuối trạng thái xác thực', + providerUnknown: 'Nhà cung cấp không xác định: {{provider}}', + providerStartOAuthFailed: 'Không thể bắt đầu OAuth', + providerCallbackFailed: 'Gửi callback thất bại', + providerSubmitCallbackFailed: 'Không thể gửi 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', @@ -6558,6 +6906,10 @@ const resources = { 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ũ', + legacyConfirmPartial: + 'Đã xác nhận {{updated}} tài khoản cũ, nhưng {{failed}} lần cập nhật thất bại. Trạng thái tài khoản đã được làm mới.', + legacyConfirmAllFailed: + 'Không thể xác nhận {{failed}} tài khoản cũ. Trạng thái tài khoản đã được làm mới.', 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', @@ -7921,6 +8273,8 @@ const resources = { }, apiProfiles: { title: 'API プロファイル', + sidebarTitle: 'API プロファイル', + sidebarSubtitle: 'プレミアム API、ローカルランタイム、カスタムエンドポイント', new: '新規', searchPlaceholder: 'プロファイルを検索...', loadingProfiles: 'プロファイルを読み込み中...', @@ -7942,6 +8296,19 @@ const resources = { unsavedChangesDesc: '"{{current}}" に未保存の変更があります。破棄して "{{next}}" に切り替えますか?', discardSwitch: '破棄して切り替え', + noOrphansFound: '孤立したプロファイル設定は見つかりませんでした', + confirmRegisterOrphans: + '{{total}} 件の孤立したプロファイル設定ファイルが見つかりました。うち {{valid}} 件は登録可能です。今すぐ登録しますか?', + registeredWithSkipped: '({{count}} 件スキップ)', + registeredProfiles: '{{count}} 件のプロファイルを登録しました', + copyPrompt: '「{{name}}」をどの新しいプロファイル名にコピーしますか?', + destinationEmpty: 'コピー先のプロファイル名は空にできません', + exportRedacted: + 'トークンをマスクした状態でエクスポートしました。必要なら CLI の include-secrets フローを使ってください。', + exportDownloaded: 'プロファイルのエクスポートをダウンロードしました', + importFailed: 'プロファイルバンドルをインポートできませんでした', + discoverOrphans: '孤立プロファイルを検出', + importProfileBundle: 'プロファイルバンドルをインポート', }, accountsPage: { title: 'アカウント', @@ -8504,6 +8871,93 @@ const resources = { configYaml: 'config.yaml', flow: 'フロー', docsTab: 'ドキュメント', + features: '機能', + featuresDesc: 'CCS が安全に管理できる Codex の機能フラグを切り替えます。', + configOnlyFlags: '設定ファイル内のみの既存フラグ', + configOnlyFlagsDesc: + 'これらのキーはすでに `config.toml` に存在するため、CCS は完全なカタログ対応を主張せずに表示できます。', + existing: '既存', + nonBooleanForm: + '非 boolean 形式で保存されています。編集する場合は Raw TOML を使用してください。', + discoveredFromFile: 'CCS の内蔵カタログではなく現在のファイルから検出されました。', + featureMultiAgentLabel: 'Multi-agent', + featureMultiAgentDesc: 'subagent 連携ツールを有効にします。', + featureUnifiedExecLabel: 'Unified exec', + featureUnifiedExecDesc: 'PTY ベースの unified exec ツールを使用します。', + featureShellSnapshotLabel: 'Shell snapshot', + featureShellSnapshotDesc: 'shell 環境スナップショットを再利用します。', + featureApplyPatchLabel: 'Apply patch', + featureApplyPatchDesc: '自由形式の apply_patch 編集を有効にします。', + featureJsReplLabel: 'JS REPL', + featureJsReplDesc: 'Node ベースの JavaScript REPL を有効にします。', + featureRuntimeMetricsLabel: 'Runtime metrics', + featureRuntimeMetricsDesc: 'Codex のランタイム指標を収集します。', + featurePreventIdleSleepLabel: 'Prevent idle sleep', + featurePreventIdleSleepDesc: '動作中にマシンがスリープしないようにします。', + featureFastModeLabel: 'Fast mode', + featureFastModeDesc: '高速サービス階層パスを許可します。', + featureAppsLabel: 'Apps', + featureAppsDesc: 'ChatGPT Apps と connector のサポートを有効にします。', + featureSmartApprovalsLabel: 'Smart approvals', + featureSmartApprovalsDesc: '対象となる承認を guardian フローに回します。', + howCodexWorks: 'CCS での Codex の動作', + nativeConfigLabel: 'ネイティブ設定:', + nativeConfigDesc: 'ccs-codex と ccsx は保存済みデフォルトでネイティブ Codex を起動します。', + transientOverridesLabel: '一時オーバーライド:', + transientOverridesDesc: + 'ccsxp(または ccs codex --target codex)は CCS プロバイダーのショートカットを使います。', + cliproxyDefaultLabel: 'CLIProxy デフォルト:', + cliproxyDefaultDesc: + '通常の codex を CLIProxy 経由にするには、model_provider = "cliproxy" を設定し、下のレシピを追加します。', + runtimeInstall: 'ランタイムのインストール', + detected: '検出済み', + notFound: '見つかりません', + detectionSource: '検出元', + binaryPath: 'バイナリパス', + installDirectory: 'インストール先', + versionLabel: 'バージョン', + nativeAliases: 'ネイティブ別名', + ccsProviderShortcut: 'CCS プロバイダーショートカット', + configOverrideSupport: '--config オーバーライド対応', + available: '利用可能', + missing: '不足', + cliproxyNativeCodex: 'CLIProxy 経由のネイティブ Codex', + builtInLabel: '内蔵:', + builtInCcsxpDesc: 'CCS プロバイダーショートカットとして ccsxp を使用します。', + nativeRecipeLabel: 'ネイティブ:', + nativeRecipeDesc: '下のレシピを設定すると codex を直接 CLIProxy 経由で利用できます。', + saveProviderNamedCliproxy: + '上の base URL と env key を使って cliproxy という名前の provider を保存します。', + inTopLevelSetDefault: 'Top-level settings で Default provider を cliproxy に設定します。', + exportCliproxyApiKey: + 'ネイティブ Codex を起動する前に shell で CLIPROXY_API_KEY を export してください。', + noConfigOverrides: + 'この Codex ビルドでもネイティブパスは使えますが、ccsxp や ccs codex --target codex などの CCS 側ルーティングは、検出された Codex バイナリが --config オーバーライドに対応するまで利用できません。', + configFile: '設定ファイル', + path: 'パス', + resolved: '解決先', + size: 'サイズ', + lastModified: '最終更新', + tomlWarning: 'TOML 警告', + readWarning: '読み込み警告', + currentUserLayerSummary: '現在のユーザーレイヤー概要', + activeProfile: 'アクティブプロファイル', + notSet: '未設定', + providersCount: 'providers: {{count}}', + profilesCount: 'profiles: {{count}}', + enabledFeaturesCount: 'enabled features: {{count}}', + mcpServersCount: 'mcp servers: {{count}}', + userLayerKeysPresent: '現在存在するユーザーレイヤーキー', + nativeShortAlias: 'ネイティブ短縮エイリアス', + ccsCodexShortcut: 'CCS Codex ショートカット', + explicitProviderRoute: '明示的な provider ルート', + runtimeVsProvider: 'ランタイムと provider の違い', + honorsSavedNativeConfig: '保存済みのネイティブ設定を尊重', + usesTransientOverrides: '一時オーバーライドを使用', + unavailableNoConfig: '利用不可(この Codex ビルドには --config サポートがありません)。', + yes: 'はい', + no: 'いいえ', + warningsTitle: '警告', }, codexPage: { title: 'Codex', @@ -8913,6 +9367,11 @@ const resources = { fiveHourResets: '5時間リセット {{time}}', weeklyResets: '週間リセット {{time}}', }, + utils: { + codeReview5h: 'コードレビュー(5時間)', + codeReviewWeekly: 'コードレビュー(週次)', + codeReview: 'コードレビュー', + }, rawEditorSection: { rawConfig: 'Raw 設定', }, @@ -8946,6 +9405,8 @@ const resources = { conflictDesc: 'この設定ファイルは別のプロセスで変更されました。変更で上書きしますか?それとも破棄しますか?', overwrite: '上書き', + invalidJson: '無効な JSON です', + failedSave: '保存に失敗しました', }, settingsPage: { title: '設定', @@ -9080,6 +9541,13 @@ const resources = { providerAuthSuccess: '{{provider}} の認証に成功しました', providerDeviceCodeInCallback: 'コールバックモードでプロバイダーがデバイスコードフローを返しました', + providerAuthTimeout: '認証がタイムアウトしました。もう一度お試しください。', + providerAccountRegistrationFailed: '認証済みアカウントを登録できませんでした', + providerLostStatusEndpoint: '認証ステータスエンドポイントとの接続が失われました', + providerUnknown: '不明なプロバイダーです: {{provider}}', + providerStartOAuthFailed: 'OAuth を開始できませんでした', + providerCallbackFailed: 'コールバック送信に失敗しました', + providerSubmitCallbackFailed: 'コールバックを送信できませんでした', loggingConfigSaved: 'ログ設定を保存しました。', loggingConfigSaveFailed: 'ログ設定の保存に失敗しました。', unifiedConfigUpdated: '設定を更新しました', @@ -9097,6 +9565,10 @@ const resources = { legacyConfirmFailed: 'レガシーアカウント「{{name}}」の確認に失敗しました: {{error}}', legacyConfirmSuccess_one: '{{count}} 件のレガシーアカウントを確認しました', legacyConfirmSuccess_other: '{{count}} 件のレガシーアカウントを確認しました', + legacyConfirmPartial: + '{{updated}} 件のレガシーアカウントを確認しましたが、{{failed}} 件の更新に失敗しました。アカウント状態を再取得しました。', + legacyConfirmAllFailed: + '{{failed}} 件のレガシーアカウントの確認に失敗しました。アカウント状態を再取得しました。', noLegacyAccounts: '確認が必要なレガシーアカウントはありません', routingStrategySet: 'ルーティング戦略を {{strategy}} に設定しました', variantCreated: 'バリアントを作成しました', diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts index ac248b36..e52be324 100644 --- a/ui/src/lib/support-updates-catalog.ts +++ b/ui/src/lib/support-updates-catalog.ts @@ -1,3 +1,5 @@ +import i18n from '@/lib/i18n'; + export type SupportStatus = 'new' | 'stable' | 'planned'; export type SupportScope = 'target' | 'cliproxy' | 'api-profiles' | 'websearch'; @@ -47,14 +49,14 @@ export interface CliSupportEntry { notes?: string; } -export const SUPPORT_SCOPE_LABELS: Record = { - 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 +const BASE_SUPPORT_SCOPE_LABELS: Record = { + target: 'Target CLI', + cliproxy: 'CLIProxy Provider', + 'api-profiles': 'API Profile', + websearch: 'WebSearch', }; -export const SUPPORT_NOTICES: SupportNotice[] = [ +const BASE_SUPPORT_NOTICES: SupportNotice[] = [ { id: 'codex-target-runtime-support', title: 'Native Codex runtime support is live', @@ -204,7 +206,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [ }, ]; -export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ +const BASE_CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ { id: 'claude-target', name: 'Claude Code', @@ -348,20 +350,86 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [ }, ]; -const SUPPORT_ENTRY_LOOKUP = new Map(CLI_SUPPORT_ENTRIES.map((entry) => [entry.id, entry])); +function tx(key: string, defaultValue: string, options?: Record): string { + return i18n.t(key, { defaultValue, ...options }); +} + +export function getSupportScopeLabels(): Record { + return { + target: tx('supportCatalog.scope.target', BASE_SUPPORT_SCOPE_LABELS.target), + cliproxy: tx('supportCatalog.scope.cliproxy', BASE_SUPPORT_SCOPE_LABELS.cliproxy), + 'api-profiles': tx( + 'supportCatalog.scope.apiProfiles', + BASE_SUPPORT_SCOPE_LABELS['api-profiles'] + ), + websearch: tx('supportCatalog.scope.websearch', BASE_SUPPORT_SCOPE_LABELS.websearch), + }; +} + +function localizeSupportNotice(notice: SupportNotice): SupportNotice { + return { + ...notice, + title: tx(`supportCatalog.notice.${notice.id}.title`, notice.title), + summary: tx(`supportCatalog.notice.${notice.id}.summary`, notice.summary), + primaryAction: tx(`supportCatalog.notice.${notice.id}.primaryAction`, notice.primaryAction), + highlights: notice.highlights.map((highlight, index) => + tx(`supportCatalog.notice.${notice.id}.highlight.${index}`, highlight) + ), + actions: notice.actions.map((action) => ({ + ...action, + label: tx(`supportCatalog.notice.${notice.id}.action.${action.id}.label`, action.label), + description: tx( + `supportCatalog.notice.${notice.id}.action.${action.id}.description`, + action.description + ), + })), + routes: notice.routes.map((route, index) => ({ + ...route, + label: tx(`supportCatalog.notice.${notice.id}.route.${index}.label`, route.label), + })), + }; +} + +function localizeCliSupportEntry(entry: CliSupportEntry): CliSupportEntry { + return { + ...entry, + name: tx(`supportCatalog.entry.${entry.id}.name`, entry.name), + summary: tx(`supportCatalog.entry.${entry.id}.summary`, entry.summary), + pillars: { + baseUrl: tx(`supportCatalog.entry.${entry.id}.pillar.baseUrl`, entry.pillars.baseUrl), + auth: tx(`supportCatalog.entry.${entry.id}.pillar.auth`, entry.pillars.auth), + model: tx(`supportCatalog.entry.${entry.id}.pillar.model`, entry.pillars.model), + }, + routes: entry.routes.map((route, index) => ({ + ...route, + label: tx(`supportCatalog.entry.${entry.id}.route.${index}.label`, route.label), + })), + notes: entry.notes ? tx(`supportCatalog.entry.${entry.id}.notes`, entry.notes) : entry.notes, + }; +} + +export function getSupportNotices(): SupportNotice[] { + return BASE_SUPPORT_NOTICES.map(localizeSupportNotice); +} + +export function getCliSupportEntries(): CliSupportEntry[] { + return BASE_CLI_SUPPORT_ENTRIES.map(localizeCliSupportEntry); +} export function getSupportEntriesForNotice(notice: SupportNotice): CliSupportEntry[] { + const supportEntryLookup = new Map(getCliSupportEntries().map((entry) => [entry.id, entry])); return notice.entryIds - .map((entryId) => SUPPORT_ENTRY_LOOKUP.get(entryId)) + .map((entryId) => supportEntryLookup.get(entryId)) .filter((entry): entry is CliSupportEntry => Boolean(entry)); } export function getLatestSupportNotice(): SupportNotice | null { - if (SUPPORT_NOTICES.length === 0) { + const notices = getSupportNotices(); + if (notices.length === 0) { return null; } - return [...SUPPORT_NOTICES].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0]; + return [...notices].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0]; } export function formatCatalogDate(value: string): string { diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index 74cb842b..532126ad 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -38,10 +38,6 @@ 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'; diff --git a/ui/src/pages/claude-extension.tsx b/ui/src/pages/claude-extension.tsx index 9e20d896..251292b1 100644 --- a/ui/src/pages/claude-extension.tsx +++ b/ui/src/pages/claude-extension.tsx @@ -227,7 +227,10 @@ function TargetStatusCard({
- {status?.message || 'Verify the binding after saving to inspect the current file state.'} + {status?.message || + t('claudeExtensionPage.verifyAfterSaving', { + defaultValue: 'Verify the binding after saving to inspect the current file state.', + })}
@@ -374,7 +377,15 @@ export function ClaudeExtensionPage() { async function handleDelete(): Promise { if (!effectiveSelectedBindingId || !selectedBinding) return; - if (!window.confirm(`Delete binding "${selectedBinding.name}"?`)) return; + if ( + !window.confirm( + t('claudeExtensionPage.deleteBindingConfirm', { + name: selectedBinding.name, + defaultValue: 'Delete binding "{{name}}"?', + }) + ) + ) + return; await deleteBinding.mutateAsync(effectiveSelectedBindingId); const remaining = bindings.filter((binding) => binding.id !== effectiveSelectedBindingId); @@ -419,8 +430,9 @@ export function ClaudeExtensionPage() {

{t('claudeExtensionPage.title')}

- {/* TODO i18n: missing key for subtitle */} - Saved IDE bindings for CCS profiles + {t('claudeExtensionPage.savedBindingsSubtitle', { + defaultValue: 'Saved IDE bindings for CCS profiles', + })}

@@ -431,8 +443,7 @@ export function ClaudeExtensionPage() {
@@ -442,34 +453,45 @@ export function ClaudeExtensionPage() { - {/* TODO i18n: missing key for "Create binding"/"Binding editor" */} - {creating ? 'Create binding' : 'Binding editor'} + {creating + ? t('claudeExtensionPage.createBinding', { defaultValue: 'Create binding' }) + : t('claudeExtensionPage.bindingEditor', { defaultValue: 'Binding editor' })} - {/* TODO i18n: missing key for binding editor description */} - Save a profile + IDE path once, then apply or reset it from the dashboard. + {t('claudeExtensionPage.bindingEditorDescription', { + defaultValue: + 'Save a profile + IDE path once, then apply or reset it from the dashboard.', + })}
- {/* TODO i18n: missing key for "Binding name" */} -
Binding name
+
+ {t('settingsPage.thinkingSection.bindingName')} +
updateDraft('name', event.target.value)} - placeholder="VS Code · work profile" /* TODO i18n: missing key */ + placeholder={t('claudeExtensionPage.bindingNamePlaceholder', { + defaultValue: 'VS Code · work profile', + })} />
- {/* TODO i18n: missing key for "CCS profile" */} -
CCS profile
+
+ {t('claudeExtensionPage.ccsProfile', { defaultValue: 'CCS profile' })} +

{selectedProfile?.description || - 'Choose which CCS profile the IDE should inherit.'} + t('claudeExtensionPage.chooseProfileHint', { + defaultValue: 'Choose which CCS profile the IDE should inherit.', + })}

@@ -494,7 +518,11 @@ export function ClaudeExtensionPage() { onValueChange={(value) => updateDraft('host', value as BindingDraft['host'])} > - + {hosts.map((host) => ( @@ -515,22 +543,29 @@ export function ClaudeExtensionPage() { onChange={(event) => updateDraft('ideSettingsPath', event.target.value)} placeholder={ selectedHost?.defaultSettingsPath || - 'Leave blank for the default user settings path' /* TODO i18n: missing key */ + t('claudeExtensionPage.ideSettingsPathPlaceholder', { + defaultValue: 'Leave blank for the default user settings path', + }) } />

- Leave blank to use the default user settings path for{' '} + {t('claudeExtensionPage.ideSettingsPathHint', { + defaultValue: 'Leave blank to use the default user settings path for', + })}{' '} {selectedHost?.label || 'this IDE'}.

- {/* TODO i18n: missing key for "Notes" */} -
Notes
+
+ {t('settingsPage.thinkingSection.notes')} +
updateDraft('notes', event.target.value)} - placeholder="Optional reminder for this machine or workspace" /* TODO i18n: missing key */ + placeholder={t('claudeExtensionPage.notesPlaceholder', { + defaultValue: 'Optional reminder for this machine or workspace', + })} />
@@ -545,12 +580,12 @@ export function ClaudeExtensionPage() { ) : ( )} - {/* TODO i18n: missing key for "Create"/"Save" */} - {creating ? 'Create' : 'Save'} + {creating + ? t('claudeExtensionPage.create', { defaultValue: 'Create' }) + : t('claudeExtensionPage.save', { defaultValue: 'Save' })} @@ -562,8 +597,7 @@ export function ClaudeExtensionPage() { disabled={deleteBinding.isPending} > - {/* TODO i18n: missing key for "Delete binding" */} - Delete binding + {t('claudeExtensionPage.deleteBinding', { defaultValue: 'Delete binding' })} ) : null}
@@ -571,8 +605,7 @@ export function ClaudeExtensionPage() {
- {/* TODO i18n: missing key for "Saved bindings" */} - Saved bindings + {t('claudeExtensionPage.savedBindings', { defaultValue: 'Saved bindings' })}
{bindings.length > 0 ? ( @@ -591,9 +624,10 @@ 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. + {t('claudeExtensionPage.emptyBindings', { + defaultValue: + 'No saved bindings yet. Create one to manage apply, reset, and drift checks from the dashboard.', + })} )} @@ -626,13 +660,16 @@ export function ClaudeExtensionPage() {

- {/* TODO i18n: missing key for default binding name */} - {selectedBinding?.name || 'Claude extension binding'} + {selectedBinding?.name || + t('claudeExtensionPage.defaultBindingName', { + defaultValue: '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. + {t('claudeExtensionPage.bindingDescription', { + defaultValue: + 'Manage the shared Claude settings file and the IDE-local settings file as two scoped targets.', + })}

@@ -648,8 +685,7 @@ export function ClaudeExtensionPage() { ) : ( )} - {/* TODO i18n: missing key for "Verify" */} - Verify + {t('claudeExtensionPage.verify', { defaultValue: 'Verify' })} {setup ? ( @@ -669,8 +705,9 @@ export function ClaudeExtensionPage() { {!activeError ? ( - Overview{' '} - {/* TODO i18n: missing key */} + + {t('claudeExtensionPage.overview', { defaultValue: 'Overview' })} + {' '} {t('settingsPage.thinkingSection.advanced')} @@ -679,11 +716,20 @@ export function ClaudeExtensionPage() {
runBindingAction('shared', 'apply')} onReset={() => runBindingAction('shared', 'reset')} disabled={creating} @@ -693,8 +739,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" /* TODO i18n: missing key */ - resetLabel="Reset IDE" /* TODO i18n: missing key */ + applyLabel={t('claudeExtensionPage.applyIde', { defaultValue: 'Apply IDE' })} + resetLabel={t('claudeExtensionPage.resetIde', { defaultValue: 'Reset IDE' })} onApply={() => runBindingAction('ide', 'apply')} onReset={() => runBindingAction('ide', 'reset')} disabled={creating} @@ -708,32 +754,41 @@ export function ClaudeExtensionPage() { {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`. + {t('claudeExtensionPage.resolvedBindingDescription', { + defaultValue: + 'The binding uses the same profile resolution as `ccs persist` and `ccs env`.', + })} {currentDraft.notes.trim() ? ( - + ) : null} @@ -763,9 +828,11 @@ export function ClaudeExtensionPage() { {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. + {t('claudeExtensionPage.managedPayloadDescription', { + defaultValue: + 'Keep the main view short. The full JSON stays in the Advanced tab.', + })} @@ -788,17 +855,24 @@ export function ClaudeExtensionPage() { {setup?.env.length ? (
- CCS will inject {setup.env.length} environment values. + {t('claudeExtensionPage.envInjected', { + count: setup.env.length, + defaultValue: 'CCS will inject {{count}} environment values.', + })}
- The IDE-local target receives the extension schema. The shared - target receives the same env block through Claude settings. + {t('claudeExtensionPage.envInjectedDescription', { + defaultValue: + 'The IDE-local target receives the extension schema. The shared target receives the same env block through Claude settings.', + })}
) : (
- This profile resolves to native Claude defaults, so apply/reset mainly - clears existing CCS-managed overrides. + {t('claudeExtensionPage.nativeDefaultsDescription', { + defaultValue: + 'This profile resolves to native Claude defaults, so apply/reset mainly clears existing CCS-managed overrides.', + })}
)}
@@ -814,8 +888,9 @@ export function ClaudeExtensionPage() { applyBinding.variables?.target === 'all' ? ( ) : null} - {/* TODO i18n: missing key for "Apply both targets" */} - Apply both targets + {t('claudeExtensionPage.applyBothTargets', { + defaultValue: 'Apply both targets', + })} ) : (
- {/* TODO i18n: missing key for "Save this draft..." */} - Save this draft to unlock apply, reset, and verify actions. + {t('claudeExtensionPage.saveDraftToUnlock', { + defaultValue: + 'Save this draft to unlock apply, reset, and verify actions.', + })}
)}
@@ -905,13 +983,20 @@ export function ClaudeExtensionPage() { <>
@@ -921,16 +1006,22 @@ export function ClaudeExtensionPage() {
- Resolved environment payload + {t('claudeExtensionPage.resolvedEnvironmentPayload', { + defaultValue: 'Resolved environment payload', + })} - Exact environment values that the extension receives after CCS - expands this profile. + {t('claudeExtensionPage.resolvedEnvironmentPayloadDescription', { + defaultValue: + 'Exact environment values that the extension receives after CCS expands this profile.', + })}
diff --git a/ui/src/pages/codex.tsx b/ui/src/pages/codex.tsx index bd34659e..fd9de796 100644 --- a/ui/src/pages/codex.tsx +++ b/ui/src/pages/codex.tsx @@ -11,7 +11,7 @@ import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab import { RawConfigEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { - KNOWN_CODEX_FEATURES, + getKnownCodexFeatures, readCodexFeatureState, readCodexMcpServers, readCodexModelProviders, @@ -23,6 +23,7 @@ import { safeParseTomlObject } from '@shared/toml-object'; export function CodexPage() { const { t } = useTranslation(); + const featureCatalog = getKnownCodexFeatures(); const { diagnostics, diagnosticsLoading, @@ -198,7 +199,7 @@ export function CodexPage() { profileEntries={profileEntries} modelProviderEntries={modelProviderEntries} mcpServerEntries={mcpServerEntries} - featureCatalog={KNOWN_CODEX_FEATURES} + featureCatalog={featureCatalog} featureState={featureState} disabled={structuredControlsDisabled} disabledReason={controlsDisabledReason} diff --git a/ui/src/pages/settings/sections/channels.tsx b/ui/src/pages/settings/sections/channels.tsx index cf5ecc8e..05a3d754 100644 --- a/ui/src/pages/settings/sections/channels.tsx +++ b/ui/src/pages/settings/sections/channels.tsx @@ -185,15 +185,17 @@ export default function ChannelsSection() {

{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. + {t('settingsPage.channelsSection.configureDescription', { + defaultValue: + '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/. + {t('settingsPage.channelsSection.storageDescription', { + defaultValue: + 'CCS stores only channel selection in `config.yaml`. Claude keeps the machine-level channel state under `~/.claude/channels/`.', + })}

@@ -212,13 +214,23 @@ export default function ChannelsSection() {

{status.summary.nextStep}

- {/* TODO i18n: missing key for "Machine checks" */} -

Machine checks

+

+ {t('settingsPage.channelsSection.machineChecks', { + defaultValue: 'Machine checks', + })} +

Bun - {/* TODO i18n: missing key for "Installed"/"Missing" */} - {status.bunInstalled ? 'Installed' : 'Missing'} + + {status.bunInstalled + ? t('settingsPage.channelsSection.installed', { + defaultValue: 'Installed', + }) + : t('settingsPage.channelsSection.missing', { + defaultValue: 'Missing', + })} +
Claude Code @@ -230,8 +242,12 @@ export default function ChannelsSection() {
Claude auth - {/* TODO i18n: missing key for "Unknown" */} - {status.auth.authMethod ?? 'Unknown'} + + {status.auth.authMethod || + t('settingsPage.channelsSection.unknown', { + defaultValue: 'Unknown', + })} +
@@ -248,21 +264,36 @@ export default function ChannelsSection() { {status && (
- {/* TODO i18n: missing key for "Fastest path" and step descriptions */} -

Fastest path

+

+ {t('settingsPage.channelsSection.fastestPath', { + defaultValue: 'Fastest path', + })} +

-

1. Turn on the channels you want below.

-

2. Save Telegram or Discord bot tokens here if that channel needs one.

- 3. Run ccs or a native Claude account profile. CCS adds{' '} - --channels for you on supported runs. + {t('settingsPage.channelsSection.fastestStep1', { + defaultValue: '1. Turn on the channels you want below.', + })} +

+

+ {t('settingsPage.channelsSection.fastestStep2', { + defaultValue: + '2. Save Telegram or Discord bot tokens here if that channel needs one.', + })} +

+

+ {t('settingsPage.channelsSection.fastestStep3', { + defaultValue: + '3. Run `ccs` or a native Claude account profile. CCS adds `--channels` for you on supported runs.', + })}

{status.supportMessage}

- {/* TODO i18n: missing key for "Advanced notes and scope" */} - Advanced notes and scope + {t('settingsPage.channelsSection.advancedNotes', { + defaultValue: 'Advanced notes and scope', + })}

{status.accountStatusCaveat}

@@ -276,9 +307,10 @@ export default function ChannelsSection() {
- {/* TODO i18n: missing key for "If you run ccs now" */}

- If you run ccs now + {t('settingsPage.channelsSection.ifYouRunNow', { + defaultValue: 'If you run `ccs` now', + })}

{status.launchPreview.detail}

@@ -288,16 +320,20 @@ export default function ChannelsSection() {
- {/* TODO i18n: missing key for "You type:" */} - You type:{' '} + + {t('settingsPage.thinkingSection.youType')} + {' '} {status.launchPreview.command}
- {/* TODO i18n: missing key for "CCS adds:" */} - CCS adds:{' '} + + {t('settingsPage.thinkingSection.ccsAdds')} + {' '} {status.launchPreview.appendedArgs.length > 0 ? status.launchPreview.appendedArgs.join(' ') - : '(nothing yet)'} + : t('settingsPage.channelsSection.nothingYet', { + defaultValue: '(nothing yet)', + })}
{status.launchPreview.skippedMessages.length > 0 && ( @@ -367,10 +403,22 @@ export default function ChannelsSection() {

{!channel.tokenConfigured && channel.tokenSource === 'process_env' - ? `The current CCS process already has ${channel.envKey}. Save it here only if you want persistent Claude channel state.` + ? t('settingsPage.channelsSection.tokenFromProcessEnv', { + envKey: channel.envKey, + defaultValue: + 'The current CCS process already has {{envKey}}. Save it here only if you want persistent Claude channel state.', + }) : channel.tokenConfigured && channel.processEnvAvailable - ? `${channel.envKey} is saved in Claude channel state, and the current CCS process env also provides it.` - : `Save ${channel.envKey} in Claude's official channel env file. The dashboard never reads the token value back after save.`} + ? t('settingsPage.channelsSection.tokenSavedAndProcessEnv', { + envKey: channel.envKey, + defaultValue: + '{{envKey}} is saved in Claude channel state, and the current CCS process env also provides it.', + }) + : t('settingsPage.channelsSection.tokenSaveHint', { + envKey: channel.envKey, + defaultValue: + "Save {{envKey}} in Claude's official channel env file. The dashboard never reads the token value back after save.", + })}

{channel.tokenConfigured && (

@@ -385,10 +433,20 @@ export default function ChannelsSection() { onChange={(event) => updateTokenDraft(channel.id, event.target.value)} placeholder={ channel.tokenConfigured - ? `Configured. Enter a new ${channel.envKey} to replace it.` + ? t('settingsPage.channelsSection.tokenConfiguredPlaceholder', { + envKey: channel.envKey, + defaultValue: 'Configured. Enter a new {{envKey}} to replace it.', + }) : !channel.tokenConfigured && channel.tokenSource === 'process_env' - ? `Using current CCS process env. Enter a new ${channel.envKey} to save it for Claude.` - : `Paste ${channel.envKey}` + ? t('settingsPage.channelsSection.tokenProcessEnvPlaceholder', { + envKey: channel.envKey, + defaultValue: + 'Using current CCS process env. Enter a new {{envKey}} to save it for Claude.', + }) + : t('settingsPage.channelsSection.tokenPastePlaceholder', { + envKey: channel.envKey, + defaultValue: 'Paste {{envKey}}', + }) } disabled={saving} /> @@ -403,8 +461,9 @@ export default function ChannelsSection() { disabled={saving || !tokenDraft.trim()} > - {/* TODO i18n: missing key for "Save Token" */} - Save Token + {t('settingsPage.channelsSection.saveToken', { + defaultValue: 'Save Token', + })}

)}
- {/* TODO i18n: missing key for "Claude-side setup commands" */} - Claude-side setup commands + {t('settingsPage.channelsSection.claudeSideSetupCommands', { + defaultValue: 'Claude-side setup commands', + })}
{(channel.manualSetupCommands ?? []).map((command) => ( @@ -441,10 +502,11 @@ 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. + {t('settingsPage.channelsSection.injectionDisclaimer', { + defaultValue: + 'CCS injects `--channels` only for the current Claude session. Telegram, Discord, and iMessage stop receiving messages when that Claude session exits.', + })} @@ -456,11 +518,11 @@ export default function ChannelsSection() { - {/* 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 - already pass a permission flag yourself. + {t('settingsPage.channelsSection.skipPermissionDescription', { + defaultValue: + 'Optional advanced behavior. CCS adds `--dangerously-skip-permissions` only when at least one selected channel is being auto-enabled and you did not already pass a permission flag yourself.', + })}

diff --git a/ui/src/pages/settings/sections/image-analysis/index.tsx b/ui/src/pages/settings/sections/image-analysis/index.tsx index a4fb1545..f1bd9751 100644 --- a/ui/src/pages/settings/sections/image-analysis/index.tsx +++ b/ui/src/pages/settings/sections/image-analysis/index.tsx @@ -28,6 +28,7 @@ import { } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api, type ImageAnalysisDashboardData } from '@/lib/api-client'; +import i18n from '@/lib/i18n'; import { cn } from '@/lib/utils'; import { useRawConfig } from '../../hooks'; @@ -116,25 +117,24 @@ function backendStateClass(state: ImageAnalysisDashboardData['backends'][number] } } -// TODO i18n: missing keys for currentTargetModeLabel values function currentTargetModeLabel( mode: ImageAnalysisDashboardData['profiles'][number]['currentTargetMode'] ): string { switch (mode) { case 'active': - return 'Active'; + return i18n.t('imageAnalysisStatus.badgeReady'); case 'bypassed': - return 'Bypassed'; + return i18n.t('imageAnalysisStatus.badgeBypassed'); case 'fallback': - return 'Native fallback'; + return i18n.t('imageAnalysisStatus.nativeFallback'); case 'setup': - return 'Needs setup'; + return i18n.t('imageAnalysisStatus.badgeSetup'); case 'disabled': - return 'Disabled'; + return i18n.t('imageAnalysisStatus.badgeDisabled'); case 'native': - return 'Native'; + return i18n.t('imageAnalysisStatus.badgeNative'); case 'unresolved': - return 'Native only'; + return i18n.t('imageAnalysisStatus.nativeImageReading'); } } @@ -157,57 +157,84 @@ function currentTargetModeClass( } } -// TODO i18n: missing keys for backendStateLabel values function backendStateLabel(state: ImageBackend['state']): string { switch (state) { case 'starts_on_launch': - return 'Starts on launch'; + return i18n.t('imageAnalysisSection.backendStartsOnLaunch', { + defaultValue: 'Starts on launch', + }); case 'needs_auth': - return 'Needs auth'; + return i18n.t('imageAnalysisStatus.needsAuth'); case 'needs_proxy': - return 'Needs proxy'; + return i18n.t('imageAnalysisStatus.needsProxy'); case 'review': - return 'Review'; + return i18n.t('imageAnalysisSection.review', { defaultValue: 'Review' }); case 'ready': - return 'Ready'; + return i18n.t('imageAnalysisStatus.badgeReady'); } } -// TODO i18n: missing keys for backendStatusNote values function backendStatusNote(backend: ImageBackend | undefined): string | null { if (!backend) { - return 'No model configured.'; + return i18n.t('imageAnalysisSection.noModelConfigured', { + defaultValue: 'No model configured.', + }); } switch (backend.state) { case 'needs_auth': - return backend.authReason || 'Authenticate to route here.'; + return ( + backend.authReason || + i18n.t('imageAnalysisSection.authenticateToRoute', { + defaultValue: 'Authenticate to route here.', + }) + ); case 'needs_proxy': - return backend.proxyReason || 'Proxy unavailable.'; + return ( + backend.proxyReason || + i18n.t('imageAnalysisSection.proxyUnavailable', { + defaultValue: 'Proxy unavailable.', + }) + ); case 'starts_on_launch': - return 'Auth ready. Launches locally on demand.'; + return i18n.t('imageAnalysisSection.authReadyLaunchesLocally', { + defaultValue: 'Auth ready. Launches locally on demand.', + }); case 'review': - return 'Needs manual review.'; + return i18n.t('imageAnalysisSection.needsManualReview', { + defaultValue: 'Needs manual review.', + }); case 'ready': return null; } } -// TODO i18n: missing keys for routeSourceLabel values function routeSourceLabel(source: ImageProfile['resolutionSource']): string { switch (source) { case 'profile-backend': - return 'Explicit mapping'; + return i18n.t('imageAnalysisSection.explicitMapping', { + defaultValue: 'Explicit mapping', + }); case 'fallback-backend': - return 'Fallback backend'; + return i18n.t('imageAnalysisSection.fallbackBackend', { + defaultValue: 'Fallback backend', + }); case 'cliproxy-provider': - return 'Provider match'; + return i18n.t('imageAnalysisSection.providerMatch', { + defaultValue: 'Provider match', + }); case 'cliproxy-bridge': - return 'Bridge match'; + return i18n.t('imageAnalysisSection.bridgeMatch', { + defaultValue: 'Bridge match', + }); case 'native-compatible': - return 'Native path'; + return i18n.t('imageAnalysisSection.nativePath', { + defaultValue: 'Native path', + }); case 'copilot-alias': - return 'Copilot alias'; + return i18n.t('imageAnalysisSection.copilotAlias', { + defaultValue: 'Copilot alias', + }); default: return source.replace(/-/g, ' '); } @@ -970,7 +997,9 @@ export default function ImageAnalysisSection() {
@@ -999,8 +1028,7 @@ export default function ImageAnalysisSection() { void commitProviderModel(backendId, ''); }} > - {/* TODO i18n: missing key for "Clear" */} - Clear + {t('common.clear', { defaultValue: 'Clear' })} )}
@@ -1032,7 +1060,9 @@ export default function ImageAnalysisSection() { getInsetPanelClass('emerald') )} > - No profiles prefer native reading yet. + {t('imageAnalysisSection.noProfilesPreferNative', { + defaultValue: 'No profiles prefer native reading yet.', + })} ) : (
@@ -1051,15 +1081,28 @@ export default function ImageAnalysisSection() { {profile.name}
- {profile.kind === 'variant' ? 'Variant' : 'Profile'} + {profile.kind === 'variant' + ? t('cliproxyPage.variant') + : t('apiProfiles.title')} - {profile.nativeImageCapable ? 'Verified' : 'Review'} + {profile.nativeImageCapable + ? t('imageAnalysisStatus.capabilityVerified') + : t('imageAnalysisSection.review', { + defaultValue: 'Review', + })}
- {profile.profileModel || 'Model not detected'} ·{' '} - {profile.nativeImageReason || 'Native read preferred.'} + {profile.profileModel || + t('imageAnalysisSection.modelNotDetected', { + defaultValue: 'Model not detected', + })}{' '} + ·{' '} + {profile.nativeImageReason || + t('imageAnalysisSection.nativeReadPreferred', { + defaultValue: 'Native read preferred.', + })}
)} - {/* TODO i18n: missing key for "Hide"/"Show" */} - {showProfileRouting ? 'Hide' : 'Show'} + {showProfileRouting + ? t('common.hide', { defaultValue: 'Hide' }) + : t('common.show', { defaultValue: 'Show' })} {showProfileRouting && ( )} @@ -1156,7 +1201,11 @@ export default function ImageAnalysisSection() { >
- Direct override + + {t('imageAnalysisSection.directOverride', { + defaultValue: 'Direct override', + })} + {!(row.profileName.trim() && row.backendId) && ( Draft @@ -1176,8 +1225,7 @@ export default function ImageAnalysisSection() { }} > - {/* TODO i18n: missing key for "Remove" */} - Remove + {t('common.remove', { defaultValue: 'Remove' })}
@@ -1186,7 +1234,9 @@ export default function ImageAnalysisSection() { value={row.profileName} list="image-profile-suggestions" disabled={saving} - placeholder="Profile or variant name" /* TODO i18n: missing key */ + placeholder={t('imageAnalysisSection.profileOrVariantName', { + defaultValue: 'Profile or variant name', + })} className="h-10 border-slate-400/15 bg-background/88 text-base" onChange={(event) => { updateMappingRow(row.id, { profileName: event.target.value }); @@ -1281,7 +1331,9 @@ export default function ImageAnalysisSection() { {profile.name}
- {profile.kind === 'variant' ? 'Variant' : 'Profile'} + {profile.kind === 'variant' + ? t('cliproxyPage.variant') + : t('apiProfiles.title')} {profile.target} diff --git a/ui/src/pages/updates.tsx b/ui/src/pages/updates.tsx index 1a4048db..4df6dddb 100644 --- a/ui/src/pages/updates.tsx +++ b/ui/src/pages/updates.tsx @@ -6,7 +6,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { UpdatesDetailsPanel } from '@/components/updates/updates-details-panel'; import { UpdatesInboxItem } from '@/components/updates/updates-inbox-item'; import { - SUPPORT_NOTICES, + getSupportNotices, getSupportEntriesForNotice, type SupportNotice, } from '@/lib/support-updates-catalog'; @@ -45,9 +45,8 @@ function noticeMatchesQuery(notice: SupportNotice, queryValue: string): boolean export function UpdatesPage() { const { t } = useTranslation(); - const notices = useMemo( - () => [...SUPPORT_NOTICES].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)), - [] + const notices = [...getSupportNotices()].sort((a, b) => + b.publishedAt.localeCompare(a.publishedAt) ); const [viewMode, setViewMode] = useState('inbox'); const [query, setQuery] = useState(''); diff --git a/ui/tests/setup/vitest-setup.ts b/ui/tests/setup/vitest-setup.ts index 3633d6ac..0fa24398 100644 --- a/ui/tests/setup/vitest-setup.ts +++ b/ui/tests/setup/vitest-setup.ts @@ -3,6 +3,7 @@ * Global test configuration and matchers */ +import '../../src/lib/i18n'; import '@testing-library/jest-dom/vitest'; import { cleanup } from '@testing-library/react'; import { afterEach, vi } from 'vitest'; diff --git a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx index 7fdabedb..f3eef2bc 100644 --- a/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx +++ b/ui/tests/unit/components/cliproxy/provider-editor/model-config-section.test.tsx @@ -71,7 +71,8 @@ describe('ModelConfigSection presets', () => { expect(screen.queryByText('Free Tier')).not.toBeInTheDocument(); expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Claude Opus 4.6 Thinking' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Gemini Pro' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Gemini Pro High' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Gemini Pro Low' })).toBeInTheDocument(); expect(screen.getByTestId('extended-context-toggle')).toBeInTheDocument(); }); @@ -101,12 +102,12 @@ describe('ModelConfigSection presets', () => { /> ); - await userEvent.click(screen.getByRole('button', { name: 'Gemini Pro' })); + await userEvent.click(screen.getByRole('button', { name: 'Gemini Pro High' })); expect(onApplyPreset).toHaveBeenCalledWith({ - ANTHROPIC_MODEL: 'gemini-3.9-pro-preview', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3.9-pro-preview', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3.9-pro-preview', + ANTHROPIC_MODEL: 'gemini-3.1-pro-high', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3.1-pro-high', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3.1-pro-high', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-9-flash-preview', }); }); diff --git a/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx index 85aac1bc..27daf5ca 100644 --- a/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx +++ b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx @@ -73,13 +73,8 @@ describe('CodexOverviewTab', () => { expect(screen.getAllByText('codex').length).toBeGreaterThan(0); expect(screen.queryByText('codex --profile default')).not.toBeInTheDocument(); expect( - screen.getAllByText( - (_, node) => - node?.textContent?.includes( - 'Built-in openai and oss providers are also valid native defaults' - ) ?? false - ).length - ).toBeGreaterThan(0); + screen.getByText('API profiles continue to default to Claude or Droid.') + ).toBeInTheDocument(); }); it('uses the active named profile when one is selected', () => { diff --git a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx index 258df382..73273d05 100644 --- a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx +++ b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx @@ -45,7 +45,7 @@ describe('ProfileCreateDialog', () => { expect(screen.getByText('Featured Providers')).toBeInTheDocument(); expect(screen.getByText('More Presets')).toBeInTheDocument(); - expect(screen.getByText('Local Runtimes')).toBeInTheDocument(); + expect(screen.getByText('Local runtimes')).toBeInTheDocument(); expect(screen.getByText('Alibaba Coding Plan')).toBeVisible(); expect(screen.getByText('Hugging Face')).toBeVisible(); expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2); @@ -73,7 +73,7 @@ describe('ProfileCreateDialog', () => { expect(screen.getByDisplayValue('ollama')).toBeInTheDocument(); expect(screen.getByDisplayValue('http://localhost:11434')).toBeInTheDocument(); - expect(screen.getByText('Local Runtimes')).toBeInTheDocument(); + expect(screen.getByText('Local runtimes')).toBeInTheDocument(); }); it('steers the Hugging Face preset to the droid target by default', async () => { diff --git a/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor.test.tsx b/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor.test.tsx index c808338d..c7174ff6 100644 --- a/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor.test.tsx +++ b/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor.test.tsx @@ -32,12 +32,23 @@ vi.mock(import('react-i18next'), async (importOriginal) => { 'authMonitor.success': 'Success', 'authMonitor.failed': 'Failed', 'authMonitor.successRate': 'Success Rate', + 'authMonitorLive.live': 'LIVE', + 'authMonitorLive.accountMonitor': 'Account Monitor', + 'authMonitorLive.requestsLabel': 'req', }; if (key === 'authMonitor.accountsCount') { return `${options?.count ?? 0} accounts`; } + if (key === 'authMonitorLive.updated') { + return `Updated ${options && 'time' in options ? String((options as { time?: string }).time ?? '') : ''}`.trim(); + } + + if (key === 'authMonitorLive.updatedNow') { + return 'Updated now'; + } + return translations[key] ?? key; }, }), diff --git a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts index 85410d35..1e5481b1 100644 --- a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts +++ b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { CLI_SUPPORT_ENTRIES, SUPPORT_NOTICES } from '@/lib/support-updates-catalog'; +import { getCliSupportEntries, getSupportNotices } from '@/lib/support-updates-catalog'; describe('support-updates catalog codex routing', () => { it('routes the Codex runtime notice to the Codex dashboard', () => { - const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'codex-target-runtime-support'); + const notice = getSupportNotices().find((entry) => entry.id === 'codex-target-runtime-support'); expect(notice).toBeDefined(); expect(notice?.routes).toContainEqual({ label: 'Codex CLI', path: '/codex' }); @@ -17,14 +17,14 @@ describe('support-updates catalog codex routing', () => { }); it('routes the Codex target entry to the Codex dashboard', () => { - const entry = CLI_SUPPORT_ENTRIES.find((item) => item.id === 'codex-target'); + const entry = getCliSupportEntries().find((item) => item.id === 'codex-target'); expect(entry).toBeDefined(); expect(entry?.routes).toEqual([{ label: 'Codex CLI', path: '/codex' }]); }); it('uses Updates Center naming consistently for the rollout notice', () => { - const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'updates-center-launch'); + const notice = getSupportNotices().find((entry) => entry.id === 'updates-center-launch'); expect(notice).toBeDefined(); expect(notice?.title).toContain('Updates Center'); diff --git a/ui/tests/unit/ui/pages/image-analysis-section.test.tsx b/ui/tests/unit/ui/pages/image-analysis-section.test.tsx index d09dfa73..ebdc97aa 100644 --- a/ui/tests/unit/ui/pages/image-analysis-section.test.tsx +++ b/ui/tests/unit/ui/pages/image-analysis-section.test.tsx @@ -166,7 +166,7 @@ describe('ImageAnalysisSection', () => { it('renders global controls and saves updated config', async () => { render(, { withSettingsProvider: true }); - expect(await screen.findByText('Image')).toBeInTheDocument(); + expect(await screen.findByText('Image Analysis')).toBeInTheDocument(); expect(screen.getByText('Partially ready')).toBeInTheDocument(); expect(screen.getByText('Core setup')).toBeInTheDocument(); expect(screen.getAllByText('Native reading').length).toBeGreaterThan(0); @@ -206,7 +206,7 @@ describe('ImageAnalysisSection', () => { }, }); - expect(await screen.findByText('Image settings saved.')).toBeInTheDocument(); + expect(await screen.findByText('Settings saved')).toBeInTheDocument(); }); it('allows saving a disabled configuration even when every provider model is cleared', async () => { @@ -278,7 +278,7 @@ describe('ImageAnalysisSection', () => { it('auto-saves edits without rendering a dedicated save button', async () => { const { container } = render(, { withSettingsProvider: true }); - expect(await screen.findByText('Image')).toBeInTheDocument(); + expect(await screen.findByText('Image Analysis')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Refresh' })).toBeInTheDocument(); expect(container.firstElementChild).toHaveClass( diff --git a/ui/tests/unit/ui/pages/logs-page.test.tsx b/ui/tests/unit/ui/pages/logs-page.test.tsx index 32521ffa..4c7e01fd 100644 --- a/ui/tests/unit/ui/pages/logs-page.test.tsx +++ b/ui/tests/unit/ui/pages/logs-page.test.tsx @@ -143,7 +143,7 @@ describe('LogsPage', () => { render(); - expect(screen.getByLabelText('Loading logs workspace')).toBeInTheDocument(); + expect(screen.getByLabelText('Loading logs...')).toBeInTheDocument(); }); it('filters by source and search query', async () => {