feat(ui): complete dashboard i18n coverage across 162 files

Replace all remaining hardcoded English strings with t() calls across
the CCS dashboard UI. Add zh-CN, vi, ja translations for all new keys.

- 162 files updated (components, hooks, pages, lib utilities)
- 7 domains covered: layout/shared, accounts/auth, CLIProxy,
  compatible CLI (Codex/Cursor/Droid/Copilot), logs/monitoring,
  analytics, and lib utilities
- 4 locales: en, zh-CN, vi, ja with natural (not literal) translations
- Fix duplicate customPresetDialog key shadowing in en/zh-CN
- Fix missing ja nav.logs and providerModelSelector keys
- Fix openrouter-banner count type (string → number)
- Fix add-account-dialog double setLocalError call
- Fix codex-model-providers-card missing useTranslation

Closes #983
This commit is contained in:
Tam Nhu Tran committed 2026-04-13 20:52:30 -04:00
1 parent 4874c154cb
commit c3401f0a91
162 files changed
+5350 -892

No files matched your search

@@ -115,6 +115,7 @@ export function AccountSafetyWarningCard({
variant="outline" variant="outline"
className="border-amber-500/40 text-amber-700 dark:text-amber-300" className="border-amber-500/40 text-amber-700 dark:text-amber-300"
> >
{/* TODO i18n: missing key for "High Risk" badge */}
High Risk High Risk
</Badge> </Badge>
</div> </div>
@@ -123,6 +124,7 @@ export function AccountSafetyWarningCard({
<p>{firstLine}</p> <p>{firstLine}</p>
<p className="font-medium text-amber-900 dark:text-amber-200">{secondLine}</p> <p className="font-medium text-amber-900 dark:text-amber-200">{secondLine}</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key for disclaimer text */}
CCS is provided as-is and does not take responsibility for suspension, bans, or access CCS is provided as-is and does not take responsibility for suspension, bans, or access
loss from upstream providers. loss from upstream providers.
</p> </p>
@@ -148,6 +150,7 @@ export function AccountSafetyWarningCard({
</a> </a>
)} )}
<span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground"> <span className="rounded-md border border-border/70 bg-muted/60 px-2.5 py-1 text-xs text-muted-foreground">
{/* TODO i18n: missing key for "Applies to CLI and dashboard auth" */}
Applies to CLI and dashboard auth Applies to CLI and dashboard auth
</span> </span>
</div> </div>
@@ -155,6 +158,7 @@ export function AccountSafetyWarningCard({
{showAcknowledgement && onAcknowledgementTextChange && ( {showAcknowledgement && onAcknowledgementTextChange && (
<div className="rounded-lg border border-amber-500/25 bg-amber-500/5 p-2.5"> <div className="rounded-lg border border-amber-500/25 bg-amber-500/5 p-2.5">
<Label htmlFor="account-risk-ack-text" className="text-xs leading-5"> <Label htmlFor="account-risk-ack-text" className="text-xs leading-5">
{/* TODO i18n: missing key */}
Type exact phrase to continue:{' '} Type exact phrase to continue:{' '}
<code className="rounded bg-muted px-1 py-0.5 font-mono"> <code className="rounded bg-muted px-1 py-0.5 font-mono">
{acknowledgementPhrase} {acknowledgementPhrase}
@@ -294,6 +294,7 @@ export function AddAccountDialog({
} }
if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) { if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) {
setLocalError( setLocalError(
// TODO i18n: missing key for AGY responsibility error
'Complete all Antigravity responsibility steps before authenticating this provider.' 'Complete all Antigravity responsibility steps before authenticating this provider.'
); );
return; return;
@@ -431,6 +432,7 @@ export function AddAccountDialog({
<p className="text-xs text-muted-foreground">{kiroMethodOption.description}</p> <p className="text-xs text-muted-foreground">{kiroMethodOption.description}</p>
{isKiroSocial && ( {isKiroSocial && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key for Kiro social browser hint */}
If your browser does not return automatically after login, CCS can accept the If your browser does not return automatically after login, CCS can accept the
final final
<span className="mx-1 rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]"> <span className="mx-1 rounded bg-muted px-1.5 py-0.5 font-mono text-[11px]">
@@ -445,7 +447,9 @@ export function AddAccountDialog({
{isKiroIdc && !showAuthUI && ( {isKiroIdc && !showAuthUI && (
<div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4"> <div className="space-y-4 rounded-lg border border-border/60 bg-muted/20 p-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="kiro-idc-start-url">IDC Start URL</Label> <Label htmlFor="kiro-idc-start-url">
{/* TODO i18n: missing key */}IDC Start URL
</Label>
<Input <Input
id="kiro-idc-start-url" id="kiro-idc-start-url"
value={kiroIDCStartUrl} value={kiroIDCStartUrl}
@@ -457,12 +461,12 @@ export function AddAccountDialog({
disabled={isPending} disabled={isPending}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Required for organization IAM Identity Center login. {/* TODO i18n: missing key */}Required for organization IAM Identity Center login.
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="kiro-idc-region">IDC Region</Label> <Label htmlFor="kiro-idc-region">{/* TODO i18n: missing key */}IDC Region</Label>
<Input <Input
id="kiro-idc-region" id="kiro-idc-region"
value={kiroIDCRegion} value={kiroIDCRegion}
@@ -474,12 +478,13 @@ export function AddAccountDialog({
disabled={isPending} disabled={isPending}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Optional. Leave blank to use the upstream default region. {/* TODO i18n: missing key */}Optional. Leave blank to use the upstream default
region.
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="kiro-idc-flow">IDC Flow</Label> <Label htmlFor="kiro-idc-flow">{/* TODO i18n: missing key */}IDC Flow</Label>
<Select <Select
value={kiroIDCFlow} value={kiroIDCFlow}
onValueChange={(value) => { onValueChange={(value) => {
@@ -488,16 +493,20 @@ export function AddAccountDialog({
}} }}
> >
<SelectTrigger id="kiro-idc-flow"> <SelectTrigger id="kiro-idc-flow">
<SelectValue placeholder="Select IDC flow" /> <SelectValue placeholder="{/* TODO i18n: missing key */}Select IDC flow" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="authcode">Authorization Code</SelectItem> <SelectItem value="authcode">
<SelectItem value="device">Device Code</SelectItem> {/* TODO i18n: missing key */}Authorization Code
</SelectItem>
<SelectItem value="device">
{/* TODO i18n: missing key */}Device Code
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Auth Code opens a browser and may need the final callback URL pasted back. Device {/* TODO i18n: missing key */}Auth Code opens a browser and may need the final
Code shows a verification code instead. callback URL pasted back. Device Code shows a verification code instead.
</p> </p>
</div> </div>
</div> </div>
@@ -540,7 +549,8 @@ export function AddAccountDialog({
{authFlow.isDeviceCodeFlow {authFlow.isDeviceCodeFlow
? t('addAccountDialog.deviceCodeHint') ? t('addAccountDialog.deviceCodeHint')
: isKiroSocial : isKiroSocial
? 'Complete sign-in in your browser. If it does not return automatically, paste the final kiro:// callback URL below.' ? // TODO i18n: missing key for Kiro social callback hint
'Complete sign-in in your browser. If it does not return automatically, paste the final kiro:// callback URL below.'
: t('addAccountDialog.browserHint')} : t('addAccountDialog.browserHint')}
</p> </p>
</div> </div>
@@ -590,7 +600,8 @@ export function AddAccountDialog({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="callback-url" className="text-xs"> <Label htmlFor="callback-url" className="text-xs">
{isKiroSocial {isKiroSocial
? 'Browser did not return? Paste the final kiro:// callback URL:' ? // TODO i18n: missing key
'Browser did not return? Paste the final kiro:// callback URL:'
: t('addAccountDialog.redirectPasteLabel')} : t('addAccountDialog.redirectPasteLabel')}
</Label> </Label>
<Input <Input
@@ -626,7 +637,8 @@ export function AddAccountDialog({
{!authFlow.authUrl && !authFlow.isDeviceCodeFlow && ( {!authFlow.authUrl && !authFlow.isDeviceCodeFlow && (
<p className="text-xs text-center text-muted-foreground"> <p className="text-xs text-center text-muted-foreground">
{isKiroSocial {isKiroSocial
? 'Preparing the Kiro sign-in URL. If it does not open automatically, it will appear here shortly.' ? // TODO i18n: missing key for Kiro social preparing URL
'Preparing the Kiro sign-in URL. If it does not open automatically, it will appear here shortly.'
: t('addAccountDialog.preparingUrl')} : t('addAccountDialog.preparingUrl')}
</p> </p>
)} )}
@@ -78,6 +78,7 @@ export function AntigravityResponsibilityChecklist({
disabled={disabled} disabled={disabled}
/> />
<Label htmlFor="agy-step-reviewed-issue" className="text-xs leading-5"> <Label htmlFor="agy-step-reviewed-issue" className="text-xs leading-5">
{/* TODO i18n: missing key for step 1 */}
Step 1: I reviewed issue #509 and understand AGY OAuth can trigger account Step 1: I reviewed issue #509 and understand AGY OAuth can trigger account
bans/suspensions. bans/suspensions.
</Label> </Label>
@@ -91,6 +92,7 @@ export function AntigravityResponsibilityChecklist({
disabled={disabled} disabled={disabled}
/> />
<Label htmlFor="agy-step-understands-risk" className="text-xs leading-5"> <Label htmlFor="agy-step-understands-risk" className="text-xs leading-5">
{/* TODO i18n: missing key for step 2 */}
Step 2: I understand this OAuth action is my own decision and I accept the upstream Step 2: I understand this OAuth action is my own decision and I accept the upstream
risk. risk.
</Label> </Label>
@@ -106,6 +108,7 @@ export function AntigravityResponsibilityChecklist({
disabled={disabled} disabled={disabled}
/> />
<Label htmlFor="agy-step-accept-responsibility" className="text-xs leading-5"> <Label htmlFor="agy-step-accept-responsibility" className="text-xs leading-5">
{/* TODO i18n: missing key for step 3 */}
Step 3: I accept full responsibility. CCS is not liable for suspension, bans, or Step 3: I accept full responsibility. CCS is not liable for suspension, bans, or
access loss. access loss.
</Label> </Label>
@@ -115,6 +118,7 @@ export function AntigravityResponsibilityChecklist({
<div className="space-y-2 rounded-lg border border-amber-500/25 bg-amber-500/5 p-3"> <div className="space-y-2 rounded-lg border border-amber-500/25 bg-amber-500/5 p-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-amber-800 dark:text-amber-200"> <div className="flex items-center gap-1.5 text-xs font-medium text-amber-800 dark:text-amber-200">
<ShieldAlert className="h-3.5 w-3.5" /> <ShieldAlert className="h-3.5 w-3.5" />
{/* TODO i18n: missing key for step 4 */}
Step 4: Type exact phrase to continue Step 4: Type exact phrase to continue
</div> </div>
<Input <Input
@@ -133,7 +137,7 @@ export function AntigravityResponsibilityChecklist({
rel="noreferrer" rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md border border-rose-500/30 bg-rose-500/10 px-2.5 py-1 font-medium text-rose-800 transition-colors hover:bg-rose-500/15 dark:text-rose-200" className="inline-flex items-center gap-1.5 rounded-md border border-rose-500/30 bg-rose-500/10 px-2.5 py-1 font-medium text-rose-800 transition-colors hover:bg-rose-500/15 dark:text-rose-200"
> >
Read issue #509 {/* TODO i18n: missing key */}Read issue #509
<ExternalLink className="h-3.5 w-3.5" /> <ExternalLink className="h-3.5 w-3.5" />
</a> </a>
</div> </div>
@@ -4,6 +4,7 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CheckCircle2, XCircle } from 'lucide-react'; import { CheckCircle2, XCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface AccountCardStatsProps { interface AccountCardStatsProps {
success: number; success: number;
@@ -12,6 +13,7 @@ interface AccountCardStatsProps {
} }
export function AccountCardStats({ success, failure, showDetails }: AccountCardStatsProps) { export function AccountCardStats({ success, failure, showDetails }: AccountCardStatsProps) {
const { t } = useTranslation();
const total = success + failure; const total = success + failure;
const successRate = total > 0 ? (success / total) * 100 : 100; const successRate = total > 0 ? (success / total) * 100 : 100;
@@ -21,7 +23,7 @@ export function AccountCardStats({ success, failure, showDetails }: AccountCardS
<div className="flex items-end justify-between px-0.5"> <div className="flex items-end justify-between px-0.5">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight"> <span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight">
Success Rate {t('authMonitorLive.successRate')}
</span> </span>
<span <span
className={cn( className={cn(
@@ -37,6 +39,7 @@ export function AccountCardStats({ success, failure, showDetails }: AccountCardS
</span> </span>
</div> </div>
<div className="flex flex-col items-end"> <div className="flex flex-col items-end">
{/* TODO i18n: missing key for "Volume" */}
<span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight"> <span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight">
Volume Volume
</span> </span>
@@ -173,7 +173,10 @@ export function AccountCard({
<div <div
className="inline-flex shrink-0 items-center overflow-hidden rounded-md border border-border/60 bg-muted/60 shadow-sm shadow-black/5 dark:bg-zinc-900/80" className="inline-flex shrink-0 items-center overflow-hidden rounded-md border border-border/60 bg-muted/60 shadow-sm shadow-black/5 dark:bg-zinc-900/80"
title={groupedHeaderVariants title={groupedHeaderVariants
.map((variant) => variant.audienceLabel ?? variant.detailLabel ?? 'Variant') .map(
(variant) =>
variant.audienceLabel ?? variant.detailLabel ?? t('accountSurfaceCard.variant')
)
.join(' • ')} .join(' • ')}
> >
{groupedVariantSummaryLabel ? ( {groupedVariantSummaryLabel ? (
@@ -204,6 +207,7 @@ export function AccountCard({
</div> </div>
{account.paused && ( {account.paused && (
<span className="text-[7px] font-bold uppercase tracking-wide px-1 py-px rounded shrink-0 bg-amber-500/15 text-amber-700 dark:bg-amber-500/25 dark:text-amber-300"> <span className="text-[7px] font-bold uppercase tracking-wide px-1 py-px rounded shrink-0 bg-amber-500/15 text-amber-700 dark:bg-amber-500/25 dark:text-amber-300">
{/* TODO i18n: missing key for compact "Paused" */}
Paused Paused
</span> </span>
)} )}
@@ -144,7 +144,9 @@ export function AccountQuotaPanel({
return ( return (
<div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}> <div className={cn('flex items-center gap-1.5 text-xs text-muted-foreground', className)}>
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-3 h-3 animate-spin" />
<span>{mode === 'compact' ? t('accountCard.quotaLoading') : 'Loading quota...'}</span> <span>
{mode === 'compact' ? t('accountCard.quotaLoading') : t('accountQuotaPanel.loadingQuota')}
</span>
</div> </div>
); );
} }
@@ -204,20 +206,24 @@ export function AccountQuotaPanel({
<> <>
<CheckCircle2 className="w-3 h-3 text-emerald-500" /> <CheckCircle2 className="w-3 h-3 text-emerald-500" />
<span className="text-emerald-600 dark:text-emerald-400"> <span className="text-emerald-600 dark:text-emerald-400">
Active · {formatRelativeTime(runtimeLastUsed)} {/* TODO i18n: missing key for "Active" */}Active ·{' '}
{formatRelativeTime(runtimeLastUsed)}
</span> </span>
</> </>
) : runtimeLastUsed ? ( ) : runtimeLastUsed ? (
<> <>
<Clock className="w-3 h-3 text-muted-foreground" /> <Clock className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground"> <span className="text-muted-foreground">
Last used {formatRelativeTime(runtimeLastUsed)} {/* TODO i18n: missing key for "Last used" */}Last used{' '}
{formatRelativeTime(runtimeLastUsed)}
</span> </span>
</> </>
) : ( ) : (
<> <>
<HelpCircle className="w-3 h-3 text-muted-foreground" /> <HelpCircle className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">Not used yet</span> <span className="text-muted-foreground">
{t('accountCardStats.notUsedYet')}
</span>
</> </>
)} )}
</div> </div>
@@ -5,6 +5,7 @@ import type { UnifiedQuotaResult } from '@/hooks/use-cliproxy-stats';
import { getAccountIdentityPresentation } from '@/lib/account-identity'; import { getAccountIdentityPresentation } from '@/lib/account-identity';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Pause, Star, User } from 'lucide-react'; import { Pause, Star, User } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { AccountQuotaPanel } from './account-quota-panel'; import { AccountQuotaPanel } from './account-quota-panel';
@@ -53,9 +54,12 @@ function getTierBadgeClass(tier: AccountTier | undefined) {
: 'bg-yellow-500/15 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400'; : 'bg-yellow-500/15 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400';
} }
function getCompactAudienceBadgeLabel(audience: 'business' | 'personal' | 'unknown') { function getCompactAudienceBadgeLabel(
if (audience === 'business') return 'Biz'; audience: 'business' | 'personal' | 'unknown',
if (audience === 'personal') return 'Pers'; t: (key: string) => string
) {
if (audience === 'business') return t('accountSurfaceCard.business');
if (audience === 'personal') return t('accountSurfaceCard.personal');
return '?'; return '?';
} }
@@ -95,6 +99,7 @@ export function AccountSurfaceCard({
quotaInsetClassName, quotaInsetClassName,
className, className,
}: AccountSurfaceCardProps) { }: AccountSurfaceCardProps) {
const { t } = useTranslation();
const identity = getAccountIdentityPresentation(accountId, email, tokenFile); const identity = getAccountIdentityPresentation(accountId, email, tokenFile);
const title = displayEmail || identity.email || accountId; const title = displayEmail || identity.email || accountId;
const normalizedProvider = provider.toLowerCase(); const normalizedProvider = provider.toLowerCase();
@@ -129,11 +134,12 @@ export function AccountSurfaceCard({
: 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/25 dark:text-emerald-300' : 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-500/25 dark:text-emerald-300'
)} )}
> >
{getCompactAudienceBadgeLabel(identity.audience)} {getCompactAudienceBadgeLabel(identity.audience, t)}
</span> </span>
)} )}
{paused && ( {paused && (
<span className="text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0 bg-amber-500/15 text-amber-700 dark:bg-amber-500/25 dark:text-amber-300"> <span className="text-[8px] font-semibold px-1.5 py-0.5 rounded-md shrink-0 bg-amber-500/15 text-amber-700 dark:bg-amber-500/25 dark:text-amber-300">
{/* TODO i18n: missing key for compact "Paused" badge */}
Paused Paused
</span> </span>
)} )}
@@ -205,6 +211,7 @@ export function AccountSurfaceCard({
{!isCompact && isDefault && ( {!isCompact && isDefault && (
<Badge variant="secondary" className="text-[10px] h-4 px-1.5 gap-0.5"> <Badge variant="secondary" className="text-[10px] h-4 px-1.5 gap-0.5">
<Star className="w-2.5 h-2.5 fill-current" /> <Star className="w-2.5 h-2.5 fill-current" />
{/* TODO i18n: missing key for "Default" badge */}
Default Default
</Badge> </Badge>
)} )}
@@ -214,6 +221,7 @@ export function AccountSurfaceCard({
className="text-[10px] h-4 px-1.5 border-yellow-500 text-yellow-600" className="text-[10px] h-4 px-1.5 border-yellow-500 text-yellow-600"
> >
<Pause className="w-2 h-2 mr-0.5" /> <Pause className="w-2 h-2 mr-0.5" />
{/* TODO i18n: missing key for "Paused" badge */}
Paused Paused
</Badge> </Badge>
)} )}
@@ -12,6 +12,7 @@ import { Database, TrendingUp, Zap } from 'lucide-react';
import type { UsageSummary } from '@/hooks/use-usage'; import type { UsageSummary } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useTranslation } from 'react-i18next';
interface CacheEfficiencyCardProps { interface CacheEfficiencyCardProps {
data: UsageSummary | undefined; data: UsageSummary | undefined;
@@ -21,6 +22,7 @@ interface CacheEfficiencyCardProps {
export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficiencyCardProps) { export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficiencyCardProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const metrics = useMemo(() => { const metrics = useMemo(() => {
if (!data) return null; if (!data) return null;
@@ -73,11 +75,14 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<CardHeader className="px-3 py-2"> <CardHeader className="px-3 py-2">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Database className="w-4 h-4" /> <Database className="w-4 h-4" />
{/* TODO i18n: missing key for "Cache Efficiency" */}
Cache Efficiency Cache Efficiency
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center"> <CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center">
<p className="text-sm text-muted-foreground text-center">No cache data available</p> <p className="text-sm text-muted-foreground text-center">
{t('analyticsCards.noCacheData')}
</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -88,6 +93,7 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<CardHeader className="px-3 py-2"> <CardHeader className="px-3 py-2">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Database className="w-4 h-4" /> <Database className="w-4 h-4" />
{/* TODO i18n: missing key for "Cache Efficiency" */}
Cache Efficiency Cache Efficiency
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -101,6 +107,7 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
</span> </span>
</div> </div>
<p className="text-[11px] text-muted-foreground uppercase tracking-wider mt-0.5"> <p className="text-[11px] text-muted-foreground uppercase tracking-wider mt-0.5">
{/* TODO i18n: missing key for "Estimated Savings" */}
Estimated Savings Estimated Savings
</p> </p>
</div> </div>
@@ -115,7 +122,9 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
{metrics.cacheHitRate.toFixed(0)}% {metrics.cacheHitRate.toFixed(0)}%
</span> </span>
</div> </div>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Hit Rate</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">
{t('analyticsCards.hitRate')}
</p>
</div> </div>
{/* Cache Cost */} {/* Cache Cost */}
@@ -123,7 +132,9 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
<span className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}> <span className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${metrics.cacheCost.toFixed(2)} ${metrics.cacheCost.toFixed(2)}
</span> </span>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Cache Cost</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">
{t('analyticsCards.cacheCost')}
</p>
</div> </div>
</div> </div>
@@ -137,6 +148,7 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
> >
<span>Reads: {formatCompact(metrics.totalCacheReads)}</span> <span>Reads: {formatCompact(metrics.totalCacheReads)}</span>
<span>Writes: {formatCompact(metrics.totalCacheWrites)}</span> <span>Writes: {formatCompact(metrics.totalCacheWrites)}</span>
{/* TODO i18n: missing keys for "Reads:" / "Writes:" */}
</div> </div>
<div className="h-2 bg-muted rounded-full overflow-hidden flex"> <div className="h-2 bg-muted rounded-full overflow-hidden flex">
<div <div
@@ -145,7 +157,7 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
backgroundColor: '#9e2a2b', backgroundColor: '#9e2a2b',
width: `${(metrics.totalCacheReads / metrics.totalCacheTokens) * 100}%`, width: `${(metrics.totalCacheReads / metrics.totalCacheTokens) * 100}%`,
}} }}
title={`Cache Reads: ${metrics.totalCacheReads.toLocaleString()}`} title={`Cache Reads: ${metrics.totalCacheReads.toLocaleString()}`} // TODO i18n: missing key
/> />
<div <div
className="h-full" className="h-full"
@@ -153,16 +165,18 @@ export function CacheEfficiencyCard({ data, isLoading, className }: CacheEfficie
backgroundColor: '#e09f3e', backgroundColor: '#e09f3e',
width: `${(metrics.totalCacheWrites / metrics.totalCacheTokens) * 100}%`, width: `${(metrics.totalCacheWrites / metrics.totalCacheTokens) * 100}%`,
}} }}
title={`Cache Writes: ${metrics.totalCacheWrites.toLocaleString()}`} title={`Cache Writes: ${metrics.totalCacheWrites.toLocaleString()}`} // TODO i18n: missing key
/> />
</div> </div>
<div className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground"> <div className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#9e2a2b' }} /> <div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#9e2a2b' }} />
{/* TODO i18n: missing key for "Read" */}
Read Read
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#e09f3e' }} /> <div className="w-2 h-2 rounded-full" style={{ backgroundColor: '#e09f3e' }} />
{/* TODO i18n: missing key for "Write" */}
Write Write
</span> </span>
</div> </div>
@@ -15,6 +15,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Server, Zap, Cpu, Coins } from 'lucide-react'; import { Server, Zap, Cpu, Coins } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
import { useTranslation } from 'react-i18next';
interface CliproxyStatsCardProps { interface CliproxyStatsCardProps {
className?: string; className?: string;
@@ -27,6 +28,7 @@ export function CliproxyStatsCard({
}: CliproxyStatsCardProps) { }: CliproxyStatsCardProps) {
const { data: status, isLoading: statusLoading } = useCliproxyStatus(); const { data: status, isLoading: statusLoading } = useCliproxyStatus();
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running); const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
const { t } = useTranslation();
const isLoading = externalLoading || statusLoading || (status?.running && statsLoading); const isLoading = externalLoading || statusLoading || (status?.running && statsLoading);
@@ -36,6 +38,7 @@ export function CliproxyStatsCard({
<CardHeader className="px-3 py-2"> <CardHeader className="px-3 py-2">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
{/* TODO i18n: missing key for "CLIProxy Stats" */}
CLIProxy Stats CLIProxy Stats
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -62,16 +65,17 @@ export function CliproxyStatsCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Server className="h-4 w-4 text-muted-foreground" /> <Server className="h-4 w-4 text-muted-foreground" />
{/* TODO i18n: missing key for "CLIProxy Stats" */}
CLIProxy Stats CLIProxy Stats
</CardTitle> </CardTitle>
<Badge variant="secondary" className="text-[10px] h-5"> <Badge variant="secondary" className="text-[10px] h-5">
Offline {t('cliproxyStatsOverview.offline')}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center"> <CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center">
<p className="text-xs text-muted-foreground text-center"> <p className="text-xs text-muted-foreground text-center">
Start a CLIProxy session (gemini, codex, agy) to collect stats. {t('cliproxyStatsOverview.noActiveSessionHint')}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -91,9 +95,11 @@ export function CliproxyStatsCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
{/* TODO i18n: missing key for "CLIProxy Stats" */}
CLIProxy Stats CLIProxy Stats
</CardTitle> </CardTitle>
<Badge variant="destructive" className="text-[10px] h-5"> <Badge variant="destructive" className="text-[10px] h-5">
{/* TODO i18n: missing key for "Error" */}
Error Error
</Badge> </Badge>
</div> </div>
@@ -125,6 +131,7 @@ export function CliproxyStatsCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Server className="h-4 w-4" /> <Server className="h-4 w-4" />
{/* TODO i18n: missing key for "CLIProxy Stats" */}
CLIProxy Stats CLIProxy Stats
</CardTitle> </CardTitle>
<Badge <Badge
@@ -132,7 +139,7 @@ export function CliproxyStatsCard({
className="text-[10px] h-5 text-green-600 border-green-200 bg-green-50 dark:bg-green-900/10 dark:border-green-800" className="text-[10px] h-5 text-green-600 border-green-200 bg-green-50 dark:bg-green-900/10 dark:border-green-800"
> >
<Zap className="h-3 w-3 mr-0.5" /> <Zap className="h-3 w-3 mr-0.5" />
Running {t('cliproxyStatsOverview.running')}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
@@ -176,6 +183,7 @@ export function CliproxyStatsCard({
</div> </div>
<div className="text-[9px] text-muted-foreground mt-0.5"> <div className="text-[9px] text-muted-foreground mt-0.5">
{failedRequests > 0 ? `${failedRequests} failed` : 'All success'} {failedRequests > 0 ? `${failedRequests} failed` : 'All success'}
{/* TODO i18n: missing keys for "failed" / "All success" */}
</div> </div>
</div> </div>
</div> </div>
@@ -187,7 +195,9 @@ export function CliproxyStatsCard({
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="text-lg font-bold leading-none">{formatNumber(totalTokens)}</div> <div className="text-lg font-bold leading-none">{formatNumber(totalTokens)}</div>
<div className="text-[9px] text-muted-foreground mt-0.5">Total tokens</div> <div className="text-[9px] text-muted-foreground mt-0.5">
{t('cliproxyStatsOverview.totalTokens')}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -197,7 +207,7 @@ export function CliproxyStatsCard({
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center gap-1.5 text-[10px] font-medium text-muted-foreground"> <div className="flex items-center gap-1.5 text-[10px] font-medium text-muted-foreground">
<Cpu className="h-3 w-3" /> <Cpu className="h-3 w-3" />
Models Used {t('cliproxyStatsOverview.modelsUsed')}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{models.map(([model, count]) => { {models.map(([model, count]) => {
@@ -14,6 +14,7 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useTranslation } from 'react-i18next';
interface DateRangeFilterProps { interface DateRangeFilterProps {
value?: DateRange; value?: DateRange;
@@ -54,6 +55,7 @@ export function DateRangeFilter({
className, className,
}: DateRangeFilterProps) { }: DateRangeFilterProps) {
const [isOpen, setIsOpen] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false);
const { t } = useTranslation();
// Helper to check if a preset is currently selected // Helper to check if a preset is currently selected
const isPresetSelected = (presetRange: DateRange) => { const isPresetSelected = (presetRange: DateRange) => {
@@ -101,7 +103,7 @@ export function DateRangeFilter({
format(value.from, 'LLL dd, y') format(value.from, 'LLL dd, y')
) )
) : ( ) : (
<span>Pick a date</span> <span>{t('dateRangeFilter.pickADate')}</span>
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -11,6 +11,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import type { ModelUsage } from '@/hooks/use-usage'; import type { ModelUsage } from '@/hooks/use-usage';
import { cn, getModelColor } from '@/lib/utils'; import { cn, getModelColor } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useTranslation } from 'react-i18next';
interface ModelBreakdownChartProps { interface ModelBreakdownChartProps {
data: ModelUsage[]; data: ModelUsage[];
@@ -20,6 +21,7 @@ interface ModelBreakdownChartProps {
export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdownChartProps) { export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdownChartProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data || data.length === 0) return []; if (!data || data.length === 0) return [];
@@ -40,7 +42,7 @@ export function ModelBreakdownChart({ data, isLoading, className }: ModelBreakdo
if (!data || data.length === 0) { if (!data || data.length === 0) {
return ( return (
<div className={cn('h-full min-h-[100px] flex items-center justify-center', className)}> <div className={cn('h-full min-h-[100px] flex items-center justify-center', className)}>
<p className="text-muted-foreground">No model data available</p> <p className="text-muted-foreground">{t('analyticsCards.noModelData')}</p>
</div> </div>
); );
} }
@@ -3,6 +3,7 @@ import { ArrowDownRight, ArrowUpRight, Database, Gauge, Sparkles } from 'lucide-
import type { ModelUsage } from '@/hooks/use-usage'; import type { ModelUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface ModelDetailsContentProps { interface ModelDetailsContentProps {
model: ModelUsage; model: ModelUsage;
@@ -10,6 +11,7 @@ interface ModelDetailsContentProps {
export function ModelDetailsContent({ model }: ModelDetailsContentProps) { export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const ioRatioStatus = getIoRatioStatus(model.ioRatio); const ioRatioStatus = getIoRatioStatus(model.ioRatio);
return ( return (
@@ -38,22 +40,28 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
<p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}> <p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
${model.cost.toFixed(2)} ${model.cost.toFixed(2)}
</p> </p>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Cost</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">
{t('analyticsCards.totalCost')}
</p>
</div> </div>
<div className="p-2 rounded-md bg-muted/50 border text-center"> <div className="p-2 rounded-md bg-muted/50 border text-center">
<p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}> <p className={cn('text-lg font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
{formatCompactNumber(model.tokens)} {formatCompactNumber(model.tokens)}
</p> </p>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider">Total Tokens</p> <p className="text-[10px] text-muted-foreground uppercase tracking-wider">
{t('analyticsCards.totalTokens')}
</p>
</div> </div>
</div> </div>
{/* Token Breakdown */} {/* Token Breakdown */}
<div className="space-y-2"> <div className="space-y-2">
<h5 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider"> <h5 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
{/* TODO i18n: missing key for "Token Breakdown" */}
Token Breakdown Token Breakdown
</h5> </h5>
<div className={cn('space-y-1', privacyMode && PRIVACY_BLUR_CLASS)}> <div className={cn('space-y-1', privacyMode && PRIVACY_BLUR_CLASS)}>
{/* TODO i18n: missing keys for Input/Output/Cache Write/Cache Read labels */}
<TokenRow <TokenRow
label="Input" label="Input"
tokens={model.inputTokens} tokens={model.inputTokens}
@@ -89,7 +97,7 @@ export function ModelDetailsContent({ model }: ModelDetailsContentProps) {
<div className="p-2.5 rounded-md border bg-muted/20 space-y-1.5"> <div className="p-2.5 rounded-md border bg-muted/20 space-y-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Gauge className="h-3.5 w-3.5 text-muted-foreground" /> <Gauge className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs font-medium">Input/Output Ratio</span> <span className="text-xs font-medium">{t('analyticsCards.inputOutputRatio')}</span>
</div> </div>
<p className="text-[11px] text-muted-foreground leading-snug"> <p className="text-[11px] text-muted-foreground leading-snug">
{ioRatioStatus.description} {ioRatioStatus.description}
@@ -14,6 +14,7 @@ import { cn } from '@/lib/utils';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { getProjectDisplayName } from './project-name-utils'; import { getProjectDisplayName } from './project-name-utils';
import { useTranslation } from 'react-i18next';
interface SessionStatsCardProps { interface SessionStatsCardProps {
data: PaginatedSessions | undefined; data: PaginatedSessions | undefined;
@@ -23,6 +24,7 @@ interface SessionStatsCardProps {
export function SessionStatsCard({ data, isLoading, className }: SessionStatsCardProps) { export function SessionStatsCard({ data, isLoading, className }: SessionStatsCardProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const stats = useMemo(() => { const stats = useMemo(() => {
if (!data?.sessions || data.sessions.length === 0) return null; if (!data?.sessions || data.sessions.length === 0) return null;
@@ -72,11 +74,14 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
<CardHeader className="px-3 py-2"> <CardHeader className="px-3 py-2">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Terminal className="w-4 h-4" /> <Terminal className="w-4 h-4" />
{/* TODO i18n: missing key for "Session Stats" */}
Session Stats Session Stats
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center"> <CardContent className="px-3 pb-3 pt-0 flex-1 flex items-center justify-center">
<p className="text-sm text-muted-foreground text-center">No session data available</p> <p className="text-sm text-muted-foreground text-center">
{t('analyticsCards.noSessionData')}
</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -89,6 +94,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
<CardHeader className="px-3 py-2"> <CardHeader className="px-3 py-2">
<CardTitle className="text-base font-semibold flex items-center gap-2"> <CardTitle className="text-base font-semibold flex items-center gap-2">
<Terminal className="w-4 h-4" /> <Terminal className="w-4 h-4" />
{/* TODO i18n: missing key for "Session Stats" */}
Session Stats Session Stats
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -102,6 +108,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
<span className="text-xl font-bold">{stats.totalSessions}</span> <span className="text-xl font-bold">{stats.totalSessions}</span>
</div> </div>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5"> <p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">
{/* TODO i18n: missing key for "Total Sessions" */}
Total Sessions Total Sessions
</p> </p>
</div> </div>
@@ -115,6 +122,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
</span> </span>
</div> </div>
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5"> <p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">
{/* TODO i18n: missing key for "Avg Cost/Session" */}
Avg Cost/Session Avg Cost/Session
</p> </p>
</div> </div>
@@ -124,6 +132,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
<div className="flex-1 min-h-0 space-y-2"> <div className="flex-1 min-h-0 space-y-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground font-medium mb-1"> <div className="flex items-center gap-1 text-xs text-muted-foreground font-medium mb-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
{/* TODO i18n: missing key for "Recent Activity" */}
Recent Activity Recent Activity
</div> </div>
<div className="space-y-1.5 max-h-full overflow-y-auto pr-1"> <div className="space-y-1.5 max-h-full overflow-y-auto pr-1">
@@ -20,6 +20,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import type { TokenBreakdown } from '@/hooks/use-usage'; import type { TokenBreakdown } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useTranslation } from 'react-i18next';
interface TokenBreakdownChartProps { interface TokenBreakdownChartProps {
data?: TokenBreakdown; data?: TokenBreakdown;
@@ -36,6 +37,7 @@ const COLORS = {
export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdownChartProps) { export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdownChartProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data) return []; if (!data) return [];
@@ -82,7 +84,7 @@ export function TokenBreakdownChart({ data, isLoading, className }: TokenBreakdo
if (!data || chartData.every((d) => d.tokens === 0)) { if (!data || chartData.every((d) => d.tokens === 0)) {
return ( return (
<div className={cn('h-[250px] flex items-center justify-center', className)}> <div className={cn('h-[250px] flex items-center justify-center', className)}>
<p className="text-muted-foreground">No token data available</p> <p className="text-muted-foreground">{t('analyticsCards.noTokenData')}</p>
</div> </div>
); );
} }
@@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { CheckCircle2, Zap, Gauge, DollarSign, Database, Lightbulb } from 'lucide-react'; import { CheckCircle2, Zap, Gauge, DollarSign, Database, Lightbulb } from 'lucide-react';
import type { Anomaly, AnomalySummary, AnomalyType } from '@/hooks/use-usage'; import type { Anomaly, AnomalySummary, AnomalyType } from '@/hooks/use-usage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface UsageInsightsCardProps { interface UsageInsightsCardProps {
anomalies?: Anomaly[]; anomalies?: Anomaly[];
@@ -53,6 +54,8 @@ export function UsageInsightsCard({
isLoading, isLoading,
className, className,
}: UsageInsightsCardProps) { }: UsageInsightsCardProps) {
const { t } = useTranslation();
if (isLoading) { if (isLoading) {
return ( return (
<Card <Card
@@ -90,7 +93,9 @@ export function UsageInsightsCard({
<div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center mb-3 ring-4 ring-green-50 dark:ring-green-900/10"> <div className="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/20 flex items-center justify-center mb-3 ring-4 ring-green-50 dark:ring-green-900/10">
<CheckCircle2 className="w-6 h-6 text-green-600 dark:text-green-400" /> <CheckCircle2 className="w-6 h-6 text-green-600 dark:text-green-400" />
</div> </div>
<h3 className="font-medium text-foreground text-sm">All Systems Nominal</h3> <h3 className="font-medium text-foreground text-sm">
{t('healthCard.allSystemsNominal')}
</h3>
<p className="text-xs mt-1.5 max-w-[200px] leading-relaxed"> <p className="text-xs mt-1.5 max-w-[200px] leading-relaxed">
Your usage patterns are within normal ranges for the selected period. Your usage patterns are within normal ranges for the selected period.
</p> </p>
@@ -104,13 +109,15 @@ export function UsageInsightsCard({
<div className="px-1 py-2 flex items-center justify-between mb-2"> <div className="px-1 py-2 flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Lightbulb className="w-4 h-4 text-amber-500" /> <Lightbulb className="w-4 h-4 text-amber-500" />
<h3 className="font-semibold text-sm">Usage Insights</h3> <h3 className="font-semibold text-sm">{t('analyticsCards.usageInsights')}</h3>
</div> </div>
<Badge <Badge
variant="outline" variant="outline"
className="h-5 px-2 text-[10px] font-medium border-amber-200 bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:border-amber-800 dark:text-amber-400" className="h-5 px-2 text-[10px] font-medium border-amber-200 bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:border-amber-800 dark:text-amber-400"
> >
{summary.totalAnomalies} {summary.totalAnomalies === 1 ? 'Alert' : 'Alerts'} {summary.totalAnomalies}{' '}
{/* TODO i18n: missing key for singular/plural "Alert"/"Alerts" */}{' '}
{summary.totalAnomalies === 1 ? 'Alert' : 'Alerts'}
</Badge> </Badge>
</div> </div>
@@ -21,6 +21,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage'; import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
type ChartData = DailyUsage | HourlyUsage; type ChartData = DailyUsage | HourlyUsage;
@@ -38,6 +39,8 @@ export function UsageTrendChart({
className, className,
}: UsageTrendChartProps) { }: UsageTrendChartProps) {
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
// TODO i18n: uncomment when keys for "No usage data for today" / "No usage data available" are added
// const { t } = useTranslation();
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!data || data.length === 0) return []; if (!data || data.length === 0) return [];
@@ -64,6 +67,7 @@ export function UsageTrendChart({
return ( return (
<div className={cn('h-full flex items-center justify-center', className)}> <div className={cn('h-full flex items-center justify-center', className)}>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{/* TODO i18n: missing keys for "No usage data for today" / "No usage data available" */}
{granularity === 'hourly' ? 'No usage data for today' : 'No usage data available'} {granularity === 'hourly' ? 'No usage data for today' : 'No usage data available'}
</p> </p>
</div> </div>
+1 -1
View File
@@ -36,7 +36,7 @@ export function UserMenu() {
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleLogout} className="gap-2 text-destructive"> <DropdownMenuItem onClick={handleLogout} className="gap-2 text-destructive">
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
Sign Out {/* TODO i18n: missing key for "Sign Out" */}Sign Out
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -19,19 +19,19 @@ function getStatusState(status: AiProviderFamilyState['status']) {
case 'ready': case 'ready':
return { return {
icon: Check, icon: Check,
text: 'Ready', text: 'Ready', // TODO i18n: missing key
className: 'text-green-600', className: 'text-green-600',
}; };
case 'partial': case 'partial':
return { return {
icon: AlertCircle, icon: AlertCircle,
text: 'Needs attention', text: 'Needs attention', // TODO i18n: missing key
className: 'text-amber-600', className: 'text-amber-600',
}; };
default: default:
return { return {
icon: Circle, icon: Circle,
text: 'Not configured', text: 'Not configured', // TODO i18n: missing key
className: 'text-muted-foreground', className: 'text-muted-foreground',
}; };
} }
@@ -33,6 +33,7 @@ function renderSecretBadge(entry: AiProviderEntryView) {
: 'bg-muted text-muted-foreground hover:bg-muted' : 'bg-muted text-muted-foreground hover:bg-muted'
)} )}
> >
{/* TODO i18n: missing keys for 'Configured' / 'Missing secret' */}
{entry.secretConfigured ? 'Configured' : 'Missing secret'} {entry.secretConfigured ? 'Configured' : 'Missing secret'}
</Badge> </Badge>
); );
@@ -47,6 +48,7 @@ export function ProviderEntryCard({
isSelected = false, isSelected = false,
variant = 'detail', variant = 'detail',
}: ProviderEntryCardProps) { }: ProviderEntryCardProps) {
// i18n: most strings in this file lack keys. See TODO comments below.
const hasAdvancedRouting = entry.prefix || entry.proxyUrl || entry.excludedModels.length > 0; const hasAdvancedRouting = entry.prefix || entry.proxyUrl || entry.excludedModels.length > 0;
if (variant === 'row') { if (variant === 'row') {
@@ -21,6 +21,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ChevronDown, KeyRound, SlidersHorizontal } from 'lucide-react'; import { ChevronDown, KeyRound, SlidersHorizontal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { import type {
AiProviderEntryView, AiProviderEntryView,
AiProviderFamilyId, AiProviderFamilyId,
@@ -271,6 +272,7 @@ export function ProviderEntryDialog({
onSubmit, onSubmit,
isSaving, isSaving,
}: ProviderEntryDialogProps) { }: ProviderEntryDialogProps) {
const { t } = useTranslation();
const guide = useMemo(() => getDialogGuide(family), [family]); const guide = useMemo(() => getDialogGuide(family), [family]);
const isEditing = Boolean(entry); const isEditing = Boolean(entry);
const supportsOpenAiCompat = family === 'openai-compatibility'; const supportsOpenAiCompat = family === 'openai-compatibility';
@@ -382,7 +384,9 @@ export function ProviderEntryDialog({
<div className="space-y-6 px-6 py-6"> <div className="space-y-6 px-6 py-6">
<section className="space-y-4"> <section className="space-y-4">
<div> <div>
<div className="text-sm font-semibold">Required setup</div> <div className="text-sm font-semibold">
{t('aiProvidersEntryDialog.requiredSetup')}
</div>
<div className="mt-1 text-sm text-muted-foreground"> <div className="mt-1 text-sm text-muted-foreground">
Save the smallest working configuration first. Save the smallest working configuration first.
</div> </div>
@@ -391,7 +395,9 @@ export function ProviderEntryDialog({
{supportsOpenAiCompat ? ( {supportsOpenAiCompat ? (
<div className="grid gap-4"> <div className="grid gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="connector-name">Connector Name</Label> <Label htmlFor="connector-name">
{t('aiProvidersEntryDialog.connectorName')}
</Label>
<Input <Input
id="connector-name" id="connector-name"
value={name} value={name}
@@ -440,7 +446,9 @@ export function ProviderEntryDialog({
<section className="space-y-4"> <section className="space-y-4">
<div> <div>
<div className="text-sm font-semibold">Optional routing</div> <div className="text-sm font-semibold">
{t('aiProvidersEntryDialog.optionalRouting')}
</div>
<div className="mt-1 text-sm text-muted-foreground"> <div className="mt-1 text-sm text-muted-foreground">
Only fill these when the route needs more than the default behavior. Only fill these when the route needs more than the default behavior.
</div> </div>
@@ -72,7 +72,7 @@ const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({
label: getProviderDisplayName(id), label: getProviderDisplayName(id),
})); }));
const AGY_DENYLIST_MESSAGE = const AGY_DENYLIST_MESSAGE =
'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'; 'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'; // TODO i18n: use t('providerEditor.agyDenylist')
function isDeniedAgyModelForProvider(provider: string, modelId: string | undefined): boolean { function isDeniedAgyModelForProvider(provider: string, modelId: string | undefined): boolean {
return provider === 'agy' && typeof modelId === 'string' && isDeniedAgyModelId(modelId); return provider === 'agy' && typeof modelId === 'string' && isDeniedAgyModelId(modelId);
@@ -152,7 +152,7 @@ export function CliproxyDialog({ open, onClose }: CliproxyDialogProps) {
<Dialog open={open} onOpenChange={onClose}> <Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Create CLIProxy Variant</DialogTitle> <DialogTitle>{t('providerEditor.createVariant')}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
@@ -64,7 +64,7 @@ const providerOptions = CLIPROXY_PROVIDERS.map((id) => ({
const COMPOSITE_TIERS = ['opus', 'sonnet', 'haiku'] as const; const COMPOSITE_TIERS = ['opus', 'sonnet', 'haiku'] as const;
const AGY_DENYLIST_MESSAGE = const AGY_DENYLIST_MESSAGE =
'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'; 'Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'; // TODO i18n: use t('providerEditor.agyDenylist')
function normalizeOptionalValue(value?: string): string | undefined { function normalizeOptionalValue(value?: string): string | undefined {
const trimmed = value?.trim(); const trimmed = value?.trim();
@@ -11,6 +11,7 @@ import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow'; import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config'; import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
import { useTranslation } from 'react-i18next';
interface VersionInfo { interface VersionInfo {
currentVersion: string; currentVersion: string;
@@ -117,6 +118,7 @@ export function CliproxyHeader({
}: CliproxyHeaderProps) { }: CliproxyHeaderProps) {
const { data: authData } = useCliproxyAuth(); const { data: authData } = useCliproxyAuth();
const { provider: authProvider, isAuthenticating, startAuth } = useCliproxyAuthFlow(); const { provider: authProvider, isAuthenticating, startAuth } = useCliproxyAuthFlow();
const { t } = useTranslation();
const lastUpdatedText = useRelativeTime(lastUpdated); const lastUpdatedText = useRelativeTime(lastUpdated);
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null); const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
@@ -157,7 +159,9 @@ export function CliproxyHeader({
<h1 className="text-2xl font-bold tracking-tight"> <h1 className="text-2xl font-bold tracking-tight">
{versionInfo?.backendLabel ?? 'CLIProxy'} {versionInfo?.backendLabel ?? 'CLIProxy'}
</h1> </h1>
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p> <p className="text-sm text-muted-foreground mt-1">
{t('cliproxyHeader.ccsLevelAccountManagement')}
</p>
</div> </div>
{/* Login Buttons - Wrap on mobile */} {/* Login Buttons - Wrap on mobile */}
@@ -188,7 +192,7 @@ export function CliproxyHeader({
isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground' isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground'
)} )}
/> />
{isRunning ? 'Running' : 'Offline'} {isRunning ? t('cliproxyStatsOverview.running') : t('cliproxyStatsOverview.offline')}
</Badge> </Badge>
{versionInfo && ( {versionInfo && (
@@ -29,12 +29,14 @@ import { cn } from '@/lib/utils';
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy'; import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { useTranslation } from 'react-i18next';
interface CliproxyStatsOverviewProps { interface CliproxyStatsOverviewProps {
className?: string; className?: string;
} }
export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) { export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps) {
const { t } = useTranslation();
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { data: status, isLoading: statusLoading } = useCliproxyStatus(); const { data: status, isLoading: statusLoading } = useCliproxyStatus();
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running); const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
@@ -71,15 +73,15 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<div> <div>
<h2 className="text-lg font-semibold tracking-tight flex items-center gap-2"> <h2 className="text-lg font-semibold tracking-tight flex items-center gap-2">
<Activity className="h-5 w-5" /> <Activity className="h-5 w-5" />
Session Statistics {t('cliproxyStatsOverview.sessionStatistics')}
</h2> </h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Real-time usage metrics from {backendLabel} {t('cliproxyStatsOverview.realTimeMetrics', { backend: backendLabel })}
</p> </p>
</div> </div>
<Badge variant="secondary" className="w-fit gap-1.5"> <Badge variant="secondary" className="w-fit gap-1.5">
<ZapOff className="h-3.5 w-3.5" /> <ZapOff className="h-3.5 w-3.5" />
Offline {t('cliproxyStatsOverview.offline')}
</Badge> </Badge>
</div> </div>
@@ -88,13 +90,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<div className="p-4 rounded-full bg-muted/50 mb-4"> <div className="p-4 rounded-full bg-muted/50 mb-4">
<Server className="h-8 w-8 text-muted-foreground" /> <Server className="h-8 w-8 text-muted-foreground" />
</div> </div>
<h3 className="font-medium mb-1">No Active Session</h3> <h3 className="font-medium mb-1">{t('cliproxyStatsOverview.noActiveSession')}</h3>
<p className="text-sm text-muted-foreground max-w-md"> <p className="text-sm text-muted-foreground max-w-md">
Start a CLIProxy session using{' '} {t('cliproxyStatsOverview.noActiveSessionHint')}
<code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">ccs gemini</code>,{' '}
<code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">ccs codex</code>,
or <code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">ccs agy</code>{' '}
to view real-time statistics.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -110,7 +108,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardContent className="flex items-center gap-4 py-6"> <CardContent className="flex items-center gap-4 py-6">
<XCircle className="h-8 w-8 text-destructive shrink-0" /> <XCircle className="h-8 w-8 text-destructive shrink-0" />
<div> <div>
<h3 className="font-medium">Failed to Load Statistics</h3> <h3 className="font-medium">{t('cliproxyStatsOverview.failedLoadStats')}</h3>
<p className="text-sm text-muted-foreground">{error.message}</p> <p className="text-sm text-muted-foreground">{error.message}</p>
</div> </div>
</CardContent> </CardContent>
@@ -147,10 +145,10 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<div> <div>
<h2 className="text-lg font-semibold tracking-tight flex items-center gap-2"> <h2 className="text-lg font-semibold tracking-tight flex items-center gap-2">
<Activity className="h-5 w-5" /> <Activity className="h-5 w-5" />
Session Statistics {t('cliproxyStatsOverview.sessionStatistics')}
</h2> </h2>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Real-time usage metrics from {backendLabel} {t('cliproxyStatsOverview.realTimeMetrics', { backend: backendLabel })}
</p> </p>
</div> </div>
<Badge <Badge
@@ -158,7 +156,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
className="w-fit gap-1.5 text-green-600 border-green-200 bg-green-50 dark:bg-green-900/10 dark:border-green-800" className="w-fit gap-1.5 text-green-600 border-green-200 bg-green-50 dark:bg-green-900/10 dark:border-green-800"
> >
<Zap className="h-3.5 w-3.5" /> <Zap className="h-3.5 w-3.5" />
Running {t('cliproxyStatsOverview.running')}
</Badge> </Badge>
</div> </div>
@@ -169,11 +167,15 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Total Requests</p> <p className="text-xs font-medium text-muted-foreground">
{t('cliproxyStatsOverview.totalRequests')}
</p>
<p className="text-2xl font-bold">{formatNumber(totalRequests)}</p> <p className="text-2xl font-bold">{formatNumber(totalRequests)}</p>
<div className="flex items-center gap-1.5 text-[10px]"> <div className="flex items-center gap-1.5 text-[10px]">
<CheckCircle2 className="h-3 w-3 text-green-500" /> <CheckCircle2 className="h-3 w-3 text-green-500" />
<span className="text-muted-foreground">{successRequests} success</span> <span className="text-muted-foreground">
{t('cliproxyStatsOverview.successCount', { count: successRequests })}
</span>
</div> </div>
</div> </div>
<div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/20"> <div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900/20">
@@ -188,7 +190,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1 flex-1"> <div className="space-y-1 flex-1">
<p className="text-xs font-medium text-muted-foreground">Success Rate</p> <p className="text-xs font-medium text-muted-foreground">
{t('cliproxyStatsOverview.successRate')}
</p>
<p className="text-2xl font-bold">{successRate}%</p> <p className="text-2xl font-bold">{successRate}%</p>
<div className="h-1.5 mt-2 bg-muted/50 rounded-full overflow-hidden"> <div className="h-1.5 mt-2 bg-muted/50 rounded-full overflow-hidden">
<div <div
@@ -223,7 +227,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Total Tokens</p> <p className="text-xs font-medium text-muted-foreground">
{t('cliproxyStatsOverview.totalTokens')}
</p>
<p className={cn('text-2xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}> <p className={cn('text-2xl font-bold', privacyMode && PRIVACY_BLUR_CLASS)}>
{formatNumber(totalTokens)} {formatNumber(totalTokens)}
</p> </p>
@@ -233,7 +239,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
privacyMode && PRIVACY_BLUR_CLASS privacyMode && PRIVACY_BLUR_CLASS
)} )}
> >
~${estimateCost(totalTokens).toFixed(2)} estimated {t('cliproxyStatsOverview.estimatedCost', {
cost: estimateCost(totalTokens).toFixed(2),
})}
</p> </p>
</div> </div>
<div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900/20"> <div className="p-2 rounded-lg bg-purple-100 dark:bg-purple-900/20">
@@ -248,7 +256,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium text-muted-foreground">Models Used</p> <p className="text-xs font-medium text-muted-foreground">
{t('cliproxyStatsOverview.modelsUsed')}
</p>
<p className="text-2xl font-bold">{models.length}</p> <p className="text-2xl font-bold">{models.length}</p>
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
{models.length > 0 ? formatModelName(models[0][0]) : 'None'} {models.length > 0 ? formatModelName(models[0][0]) : 'None'}
@@ -268,7 +278,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Cpu className="h-4 w-4" /> <Cpu className="h-4 w-4" />
Model Usage Distribution {t('cliproxyStatsOverview.modelUsageDistribution')}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
@@ -289,7 +299,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
</span> </span>
</div> </div>
<div className="flex items-center gap-3 text-muted-foreground shrink-0"> <div className="flex items-center gap-3 text-muted-foreground shrink-0">
<span>{count} requests</span> <span>{t('cliproxyStatsOverview.requestCount', { count })}</span>
<span className="text-xs font-medium w-10 text-right">{percentage}%</span> <span className="text-xs font-medium w-10 text-right">{percentage}%</span>
</div> </div>
</div> </div>
+15 -7
View File
@@ -28,6 +28,7 @@ import { useDeleteVariant } from '@/hooks/use-cliproxy';
import { CliproxyEditDialog } from './cliproxy-edit-dialog'; import { CliproxyEditDialog } from './cliproxy-edit-dialog';
import type { Variant } from '@/lib/api-client'; import type { Variant } from '@/lib/api-client';
import { getProviderDisplayName } from '@/lib/provider-config'; import { getProviderDisplayName } from '@/lib/provider-config';
import { useTranslation } from 'react-i18next';
interface CliproxyTableProps { interface CliproxyTableProps {
data: Variant[]; data: Variant[];
@@ -36,30 +37,35 @@ interface CliproxyTableProps {
export function CliproxyTable({ data }: CliproxyTableProps) { export function CliproxyTable({ data }: CliproxyTableProps) {
const deleteMutation = useDeleteVariant(); const deleteMutation = useDeleteVariant();
const [editingVariant, setEditingVariant] = useState<Variant | null>(null); const [editingVariant, setEditingVariant] = useState<Variant | null>(null);
const { t } = useTranslation();
const columns: ColumnDef<Variant>[] = [ const columns: ColumnDef<Variant>[] = [
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name', header: t('cliproxyTable.name'),
cell: ({ row }) => <span className="font-medium">{row.original.name}</span>, cell: ({ row }) => <span className="font-medium">{row.original.name}</span>,
}, },
{ {
accessorKey: 'provider', accessorKey: 'provider',
header: 'Provider', header: t('cliproxyTable.provider'),
cell: ({ row }) => { cell: ({ row }) => {
if (row.original.type === 'composite') { if (row.original.type === 'composite') {
return <Badge variant="secondary">composite</Badge>; return <Badge variant="secondary">{t('providerEditor.composite')}</Badge>;
} }
return getProviderDisplayName(row.original.provider); return getProviderDisplayName(row.original.provider);
}, },
}, },
{ {
accessorKey: 'account', accessorKey: 'account',
header: 'Account', header: t('cliproxyTable.account'),
cell: ({ row }) => { cell: ({ row }) => {
const account = row.original.account; const account = row.original.account;
if (!account) { if (!account) {
return <span className="text-muted-foreground text-xs italic">default</span>; return (
<span className="text-muted-foreground text-xs italic">
{t('providerEditor.defaultLabel')}
</span>
);
} }
return ( return (
<Badge variant="secondary" className="text-xs font-normal"> <Badge variant="secondary" className="text-xs font-normal">
@@ -89,7 +95,7 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
}, },
{ {
id: 'actions', id: 'actions',
header: 'Actions', header: t('cliproxyTable.actions'),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex justify-end"> <div className="flex justify-end">
<DropdownMenu> <DropdownMenu>
@@ -102,6 +108,7 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
<DropdownMenuContent align="end" className="bg-white dark:bg-zinc-950"> <DropdownMenuContent align="end" className="bg-white dark:bg-zinc-950">
<DropdownMenuItem onClick={() => setEditingVariant(row.original)}> <DropdownMenuItem onClick={() => setEditingVariant(row.original)}>
<Pencil className="w-4 h-4 mr-2" /> <Pencil className="w-4 h-4 mr-2" />
{/* TODO i18n: missing key for "Edit" */}
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
@@ -109,6 +116,7 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
onClick={() => deleteMutation.mutate(row.original.name)} onClick={() => deleteMutation.mutate(row.original.name)}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
{/* TODO i18n: missing key for "Delete" */}
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -128,7 +136,7 @@ export function CliproxyTable({ data }: CliproxyTableProps) {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<div className="text-center py-12 border rounded-lg bg-muted/5 border-dashed"> <div className="text-center py-12 border rounded-lg bg-muted/5 border-dashed">
<div className="text-muted-foreground text-sm">No CLIProxy variants found.</div> <div className="text-muted-foreground text-sm">{t('cliproxyHeader.noVariants')}</div>
<div className="text-xs text-muted-foreground mt-1"> <div className="text-xs text-muted-foreground mt-1">
Create one to use OAuth-based providers with specific account configurations. Create one to use OAuth-based providers with specific account configurations.
</div> </div>
+7 -5
View File
@@ -6,6 +6,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { LayoutDashboard, FileCode, ScrollText } from 'lucide-react'; import { LayoutDashboard, FileCode, ScrollText } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export type CliproxyTabValue = 'overview' | 'config' | 'logs'; export type CliproxyTabValue = 'overview' | 'config' | 'logs';
@@ -20,12 +21,13 @@ interface CliproxyTabsProps {
} }
const TAB_CONFIG = [ const TAB_CONFIG = [
{ value: 'overview' as const, label: 'Overview', icon: LayoutDashboard }, { value: 'overview' as const, labelKey: 'cliproxyTabs.overview', icon: LayoutDashboard },
{ value: 'config' as const, label: 'Config', icon: FileCode }, { value: 'config' as const, labelKey: 'Config', icon: FileCode },
{ value: 'logs' as const, label: 'Logs', icon: ScrollText }, { value: 'logs' as const, labelKey: 'Logs', icon: ScrollText },
]; ];
export function CliproxyTabs({ activeTab, onTabChange, children }: CliproxyTabsProps) { export function CliproxyTabs({ activeTab, onTabChange, children }: CliproxyTabsProps) {
const { t } = useTranslation();
return ( return (
<Tabs <Tabs
value={activeTab} value={activeTab}
@@ -33,10 +35,10 @@ export function CliproxyTabs({ activeTab, onTabChange, children }: CliproxyTabsP
className="w-full" className="w-full"
> >
<TabsList className="grid w-full grid-cols-3 max-w-md"> <TabsList className="grid w-full grid-cols-3 max-w-md">
{TAB_CONFIG.map(({ value, label, icon: Icon }) => ( {TAB_CONFIG.map(({ value, labelKey, icon: Icon }) => (
<TabsTrigger key={value} value={value} className="gap-2"> <TabsTrigger key={value} value={value} className="gap-2">
<Icon className="w-4 h-4" /> <Icon className="w-4 h-4" />
<span className="hidden sm:inline">{label}</span> <span className="hidden sm:inline">{t(labelKey)}</span>
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
@@ -12,8 +12,10 @@ import { buildFileTree } from './file-tree-utils';
import { YamlEditor, EditorStatusBar } from './yaml-editor'; import { YamlEditor, EditorStatusBar } from './yaml-editor';
import { DiffDialog } from './diff-dialog'; import { DiffDialog } from './diff-dialog';
import { useCliproxyConfig, useCliproxyAuthFile } from '@/hooks/use-cliproxy-config'; import { useCliproxyConfig, useCliproxyAuthFile } from '@/hooks/use-cliproxy-config';
import { useTranslation } from 'react-i18next';
export function ConfigSplitView() { export function ConfigSplitView() {
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState<string>('config.yaml'); const [selectedFile, setSelectedFile] = useState<string>('config.yaml');
const [showDiff, setShowDiff] = useState(false); const [showDiff, setShowDiff] = useState(false);
@@ -82,7 +84,7 @@ export function ConfigSplitView() {
<span className="font-medium text-sm">{selectedFile}</span> <span className="font-medium text-sm">{selectedFile}</span>
{isDirty && isEditingConfig && ( {isDirty && isEditingConfig && (
<span className="text-xs text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded"> <span className="text-xs text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded">
Modified {t('cliproxyConfig.modified')}
</span> </span>
)} )}
</div> </div>
@@ -12,6 +12,7 @@ import {
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { useTranslation } from 'react-i18next';
interface DiffDialogProps { interface DiffDialogProps {
open: boolean; open: boolean;
@@ -30,6 +31,7 @@ export function DiffDialog({
onConfirmSave, onConfirmSave,
isSaving, isSaving,
}: DiffDialogProps) { }: DiffDialogProps) {
const { t } = useTranslation();
const originalLines = original.split('\n'); const originalLines = original.split('\n');
const modifiedLines = modified.split('\n'); const modifiedLines = modified.split('\n');
@@ -63,13 +65,13 @@ export function DiffDialog({
<Dialog open={open} onOpenChange={onClose}> <Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[80vh]"> <DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader> <DialogHeader>
<DialogTitle>Review Changes</DialogTitle> <DialogTitle>{t('cliproxyConfig.reviewChanges')}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="border rounded-lg overflow-hidden"> <div className="border rounded-lg overflow-hidden">
<div className="grid grid-cols-2 text-xs font-medium bg-muted p-2 border-b"> <div className="grid grid-cols-2 text-xs font-medium bg-muted p-2 border-b">
<div className="text-red-600">Original</div> <div className="text-red-600">{t('cliproxyConfig.original')}</div>
<div className="text-green-600 border-l pl-2">Modified</div> <div className="text-green-600 border-l pl-2">{t('cliproxyConfig.modified')}</div>
</div> </div>
<ScrollArea className="h-[400px]"> <ScrollArea className="h-[400px]">
<div className="divide-y">{renderDiff()}</div> <div className="divide-y">{renderDiff()}</div>
@@ -78,10 +80,13 @@ export function DiffDialog({
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={onClose}> <Button variant="outline" onClick={onClose}>
{/* TODO i18n: missing key for "Cancel" in diff context */}
Cancel Cancel
</Button> </Button>
<Button onClick={onConfirmSave} disabled={isSaving}> <Button onClick={onConfirmSave} disabled={isSaving}>
{isSaving ? 'Saving...' : 'Save Changes'} {isSaving
? /* TODO i18n: missing key */ 'Saving...'
: /* TODO i18n: missing key */ 'Save Changes'}
</Button> </Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -9,6 +9,7 @@ import { Highlight, themes } from 'prism-react-renderer';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { AlertCircle, CheckCircle2 } from 'lucide-react'; import { AlertCircle, CheckCircle2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface YamlEditorProps { interface YamlEditorProps {
value: string; value: string;
@@ -100,6 +101,7 @@ export function EditorStatusBar({
cursorLine, cursorLine,
cursorCol, cursorCol,
}: EditorStatusBarProps) { }: EditorStatusBarProps) {
const { t } = useTranslation();
return ( return (
<div className="flex items-center justify-between px-3 py-1.5 border-t bg-muted/30 text-xs"> <div className="flex items-center justify-between px-3 py-1.5 border-t bg-muted/30 text-xs">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -116,7 +118,7 @@ export function EditorStatusBar({
</span> </span>
)} )}
{isDirty && <span className="text-amber-500">Unsaved changes</span>} {isDirty && <span className="text-amber-500">{t('cliproxyConfig.unsavedChanges')}</span>}
</div> </div>
{cursorLine && cursorCol && ( {cursorLine && cursorCol && (
@@ -12,6 +12,7 @@ import { useQuery } from '@tanstack/react-query';
import { api, withApiBase } from '@/lib/api-client'; import { api, withApiBase } from '@/lib/api-client';
import type { CliproxyServerConfig } from '@/lib/api-client'; import type { CliproxyServerConfig } from '@/lib/api-client';
import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils'; import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils';
import { useTranslation } from 'react-i18next';
interface AuthTokensResponse { interface AuthTokensResponse {
apiKey: { value: string; isCustom: boolean }; apiKey: { value: string; isCustom: boolean };
@@ -57,6 +58,7 @@ function clearLocalControlPanelSession(): void {
} }
export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanelEmbedProps) { export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanelEmbedProps) {
const { t } = useTranslation();
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const [loadedFrameKey, setLoadedFrameKey] = useState<string | null>(null); const [loadedFrameKey, setLoadedFrameKey] = useState<string | null>(null);
const [iframeRevision, setIframeRevision] = useState(0); const [iframeRevision, setIframeRevision] = useState(0);
@@ -280,7 +282,7 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel
<div className="flex items-center justify-between p-4 border-b"> <div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Gauge className="w-5 h-5 text-primary" /> <Gauge className="w-5 h-5 text-primary" />
<h2 className="font-semibold">CLIProxy Control Panel</h2> <h2 className="font-semibold">{t('cliproxyHeader.cliproxyControlPanel')}</h2>
</div> </div>
<button <button
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm border rounded-md hover:bg-muted" className="inline-flex items-center gap-2 px-3 py-1.5 text-sm border rounded-md hover:bg-muted"
@@ -295,7 +297,9 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel
<div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center mx-auto mb-6"> <div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center mx-auto mb-6">
<AlertCircle className="w-8 h-8 text-destructive" /> <AlertCircle className="w-8 h-8 text-destructive" />
</div> </div>
<h3 className="text-lg font-semibold mb-2">CLIProxy Not Available</h3> <h3 className="text-lg font-semibold mb-2">
{t('cliproxyHeader.cliproxyNotAvailable')}
</h3>
<p className="text-muted-foreground mb-4">{error}</p> <p className="text-muted-foreground mb-4">{error}</p>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Start a CLIProxy session with{' '} Start a CLIProxy session with{' '}
@@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { isNativeGeminiModel } from '@/lib/extended-context-utils'; import { isNativeGeminiModel } from '@/lib/extended-context-utils';
import type { ModelEntry } from './provider-model-selector'; import type { ModelEntry } from './provider-model-selector';
import { useTranslation } from 'react-i18next';
interface ExtendedContextToggleProps { interface ExtendedContextToggleProps {
/** Compatible selected models */ /** Compatible selected models */
@@ -33,6 +34,7 @@ export function ExtendedContextToggle({
disabled, disabled,
className, className,
}: ExtendedContextToggleProps) { }: ExtendedContextToggleProps) {
const { t } = useTranslation();
if (models.length === 0) { if (models.length === 0) {
return null; return null;
} }
@@ -61,7 +63,7 @@ export function ExtendedContextToggle({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap className={cn('w-4 h-4', enabled ? 'text-primary' : 'text-muted-foreground')} /> <Zap className={cn('w-4 h-4', enabled ? 'text-primary' : 'text-muted-foreground')} />
<span className="text-sm font-medium">Extended Context</span> <span className="text-sm font-medium">{t('extendedContext.extendedContext')}</span>
<Badge variant={enabled ? 'default' : 'secondary'} className="text-[10px] h-5 px-1.5"> <Badge variant={enabled ? 'default' : 'secondary'} className="text-[10px] h-5 px-1.5">
1M tokens 1M tokens
</Badge> </Badge>
@@ -29,8 +29,14 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import type { AccountItemProps } from './types'; import type { AccountItemProps } from './types';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
function renderProjectId(projectId: string | undefined, privacyMode: boolean | undefined) { function renderProjectId(
projectId: string | undefined,
privacyMode: boolean | undefined,
t: TFunction
) {
if (projectId) { if (projectId) {
return ( return (
<TooltipProvider> <TooltipProvider>
@@ -50,7 +56,7 @@ function renderProjectId(projectId: string | undefined, privacyMode: boolean | u
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom"> <TooltipContent side="bottom">
<p className="text-xs">GCP Project ID (read-only)</p> <p className="text-xs">{t('providerEditor.gcpProjectIdReadonly')}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -63,13 +69,13 @@ function renderProjectId(projectId: string | undefined, privacyMode: boolean | u
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-500"> <div className="flex items-center gap-1 text-xs text-amber-600 dark:text-amber-500">
<AlertTriangle className="w-3 h-3" aria-label="Warning" /> <AlertTriangle className="w-3 h-3" aria-label="Warning" />
<span>Project ID: N/A</span> <span>{t('providerEditor.projectIdNA')}</span>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[250px]"> <TooltipContent side="bottom" className="max-w-[250px]">
<div className="text-xs space-y-1"> <div className="text-xs space-y-1">
<p className="font-medium text-amber-600">Missing Project ID</p> <p className="font-medium text-amber-600">{t('providerEditor.missingProjectId')}</p>
<p>This may cause errors. Remove the account and re-add it to fetch the project ID.</p> <p>{t('providerEditor.missingProjectIdHint')}</p>
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -90,6 +96,7 @@ export function AccountItem({
selected, selected,
onSelectChange, onSelectChange,
}: AccountItemProps) { }: AccountItemProps) {
const { t } = useTranslation();
const normalizedProvider = account.provider.toLowerCase(); const normalizedProvider = account.provider.toLowerCase();
const { data: stats } = useCliproxyStats(showQuota); const { data: stats } = useCliproxyStats(showQuota);
const { data: quota, isLoading: quotaLoading } = useAccountQuota( const { data: quota, isLoading: quotaLoading } = useAccountQuota(
@@ -199,7 +206,7 @@ export function AccountItem({
beforeIdentity={beforeIdentity} beforeIdentity={beforeIdentity}
headerEnd={headerEnd} headerEnd={headerEnd}
bodySlot={ bodySlot={
account.provider === 'agy' ? renderProjectId(account.projectId, privacyMode) : null account.provider === 'agy' ? renderProjectId(account.projectId, privacyMode, t) : null
} }
quotaInsetClassName="pl-11" quotaInsetClassName="pl-11"
/> />
@@ -12,6 +12,7 @@ import { User, Plus, Globe } from 'lucide-react';
import { AccountItem } from './account-item'; import { AccountItem } from './account-item';
import { BulkActionBar } from './bulk-action-bar'; import { BulkActionBar } from './bulk-action-bar';
import type { OAuthAccount } from '@/lib/api-client'; import type { OAuthAccount } from '@/lib/api-client';
import { useTranslation } from 'react-i18next';
interface AccountsSectionProps { interface AccountsSectionProps {
accounts: OAuthAccount[]; accounts: OAuthAccount[];
@@ -65,6 +66,7 @@ export function AccountsSection({
onKiroNoIncognitoChange, onKiroNoIncognitoChange,
kiroSettingsLoading, kiroSettingsLoading,
}: AccountsSectionProps) { }: AccountsSectionProps) {
const { t } = useTranslation();
// Multi-select state - raw selection (may contain stale IDs) // Multi-select state - raw selection (may contain stale IDs)
const [rawSelectedIds, setRawSelectedIds] = useState<Set<string>>(new Set()); const [rawSelectedIds, setRawSelectedIds] = useState<Set<string>>(new Set());
@@ -140,6 +142,7 @@ export function AccountsSection({
/> />
)} )}
<User className="w-4 h-4" /> <User className="w-4 h-4" />
{/* TODO i18n: missing key for "Accounts" */}
Accounts Accounts
{accounts.length > 0 && ( {accounts.length > 0 && (
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
@@ -191,8 +194,8 @@ export function AccountsSection({
) : ( ) : (
<div className="py-6 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed"> <div className="py-6 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed">
<User className="w-8 h-8 mx-auto mb-2 opacity-50" /> <User className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No accounts connected</p> <p className="text-sm">{t('providerEditor.noAccountsConnected')}</p>
<p className="text-xs opacity-70">Add an account to get started</p> <p className="text-xs opacity-70">{t('providerEditor.addAccountToStart')}</p>
</div> </div>
)} )}
@@ -202,7 +205,7 @@ export function AccountsSection({
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<Globe className="w-3.5 h-3.5" /> <Globe className="w-3.5 h-3.5" />
<span>Use incognito</span> <span>{t('providerEditor.useIncognito')}</span>
</div> </div>
<Switch <Switch
checked={!kiroNoIncognito} checked={!kiroNoIncognito}
@@ -49,6 +49,7 @@ export function BulkActionBar({
disabled={isLoading} disabled={isLoading}
> >
{isPausing ? <Loader2 className="w-3 h-3 animate-spin" /> : <Pause className="w-3 h-3" />} {isPausing ? <Loader2 className="w-3 h-3 animate-spin" /> : <Pause className="w-3 h-3" />}
{/* TODO i18n: missing key for "Pause Selected" */}
Pause Selected Pause Selected
</Button> </Button>
<Button <Button
@@ -59,6 +60,7 @@ export function BulkActionBar({
disabled={isLoading} disabled={isLoading}
> >
{isResuming ? <Loader2 className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />} {isResuming ? <Loader2 className="w-3 h-3 animate-spin" /> : <Play className="w-3 h-3" />}
{/* TODO i18n: missing key for "Resume Selected" */}
Resume Selected Resume Selected
</Button> </Button>
</div> </div>
@@ -20,6 +20,7 @@ import { CLIPROXY_DEFAULT_PORT } from '@/lib/preset-utils';
import { isDeniedAgyModelId } from '@/lib/utils'; import { isDeniedAgyModelId } from '@/lib/utils';
import i18n from '@/lib/i18n'; import i18n from '@/lib/i18n';
import { usePrivacy } from '@/contexts/privacy-context'; import { usePrivacy } from '@/contexts/privacy-context';
import { useTranslation } from 'react-i18next';
import { useProviderEditor } from './use-provider-editor'; import { useProviderEditor } from './use-provider-editor';
import { CustomPresetDialog } from './custom-preset-dialog'; import { CustomPresetDialog } from './custom-preset-dialog';
import { RawEditorSection } from './raw-editor-section'; import { RawEditorSection } from './raw-editor-section';
@@ -55,6 +56,7 @@ export function ProviderEditor({
}: ProviderEditorProps) { }: ProviderEditorProps) {
const [customPresetOpen, setCustomPresetOpen] = useState(false); const [customPresetOpen, setCustomPresetOpen] = useState(false);
const { privacyMode } = usePrivacy(); const { privacyMode } = usePrivacy();
const { t } = useTranslation();
const { data: modelsData } = useCliproxyModels(); const { data: modelsData } = useCliproxyModels();
const { data: presetsData } = usePresets(provider); const { data: presetsData } = usePresets(provider);
@@ -158,7 +160,7 @@ export function ProviderEditor({
updates.ANTHROPIC_DEFAULT_HAIKU_MODEL, updates.ANTHROPIC_DEFAULT_HAIKU_MODEL,
].some((modelId) => typeof modelId === 'string' && isDeniedAgyModelId(modelId)) ].some((modelId) => typeof modelId === 'string' && isDeniedAgyModelId(modelId))
) { ) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'); toast.error(t('providerEditor.agyDenylist'));
return; return;
} }
@@ -178,7 +180,7 @@ export function ProviderEditor({
isDeniedAgyModelId(modelId) isDeniedAgyModelId(modelId)
) )
) { ) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'); toast.error(t('providerEditor.agyDenylist'));
return; return;
} }
@@ -206,7 +208,7 @@ export function ProviderEditor({
isDeniedAgyModelId(modelId) isDeniedAgyModelId(modelId)
) )
) { ) {
toast.error('Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.'); toast.error(t('providerEditor.agyDenylist'));
return; return;
} }
createPresetMutation.mutate({ profile: provider, data: { name: presetName, ...values } }); createPresetMutation.mutate({ profile: provider, data: { name: presetName, ...values } });
@@ -234,7 +236,7 @@ export function ProviderEditor({
{isLoading ? ( {isLoading ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /> <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
<span className="ml-3 text-muted-foreground">Loading settings...</span> <span className="ml-3 text-muted-foreground">{t('providerEditor.loadingSettings')}</span>
</div> </div>
) : ( ) : (
<div className="min-h-0 flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden"> <div className="min-h-0 flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden">
@@ -312,7 +314,7 @@ export function ProviderEditor({
<div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]"> <div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]">
<Code2 className="w-4 h-4 text-muted-foreground" /> <Code2 className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
Raw Configuration (JSON) {t('rawEditorSection.rawConfig')} (JSON)
</span> </span>
</div> </div>
<RawEditorSection <RawEditorSection
@@ -13,6 +13,7 @@ import { ExtendedContextToggle } from '../extended-context-toggle';
import { stripExtendedContextSuffix } from '@/lib/extended-context-utils'; import { stripExtendedContextSuffix } from '@/lib/extended-context-utils';
import { findCatalogModel, getResolvedCatalogModels } from '@/lib/model-catalogs'; import { findCatalogModel, getResolvedCatalogModels } from '@/lib/model-catalogs';
import type { ModelConfigSectionProps } from './types'; import type { ModelConfigSectionProps } from './types';
import { useTranslation } from 'react-i18next';
type CatalogPresetModel = NonNullable<ModelConfigSectionProps['catalog']>['models'][number]; type CatalogPresetModel = NonNullable<ModelConfigSectionProps['catalog']>['models'][number];
@@ -53,6 +54,7 @@ export function ModelConfigSection({
onDeletePreset, onDeletePreset,
isDeletePending, isDeletePending,
}: ModelConfigSectionProps) { }: ModelConfigSectionProps) {
const { t } = useTranslation();
const pinningReady = (routing?.models ?? []).some((hint) => hint.pinnedAvailable); const pinningReady = (routing?.models ?? []).some((hint) => hint.pinnedAvailable);
const routingHintMap = useMemo( const routingHintMap = useMemo(
() => () =>
@@ -118,9 +120,10 @@ export function ModelConfigSection({
<div> <div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2"> <h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4" />
{/* TODO i18n: missing key for "Presets" */}
Presets Presets
</h3> </h3>
<p className="text-xs text-muted-foreground mb-3">Apply pre-configured model mappings</p> <p className="text-xs text-muted-foreground mb-3">{t('providerEditor.presets')}</p>
<div className="space-y-4"> <div className="space-y-4">
{presetGroups.map((group) => ( {presetGroups.map((group) => (
<div key={group.key}> <div key={group.key}>
@@ -204,7 +207,7 @@ export function ModelConfigSection({
{/* Model Mapping */} {/* Model Mapping */}
<div> <div>
<h3 className="text-sm font-medium mb-2">Model Mapping</h3> <h3 className="text-sm font-medium mb-2">{t('providerEditor.modelMapping')}</h3>
<p className="text-xs text-muted-foreground mb-4"> <p className="text-xs text-muted-foreground mb-4">
Configure which models to use for each tier Configure which models to use for each tier
</p> </p>
@@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge';
import { Save, Loader2, RefreshCw, Globe, Network } from 'lucide-react'; import { Save, Loader2, RefreshCw, Globe, Network } from 'lucide-react';
import { ProviderLogo } from '../provider-logo'; import { ProviderLogo } from '../provider-logo';
import type { SettingsResponse } from './types'; import type { SettingsResponse } from './types';
import { useTranslation } from 'react-i18next';
interface ProviderEditorHeaderProps { interface ProviderEditorHeaderProps {
provider: string; provider: string;
@@ -38,6 +39,7 @@ export function ProviderEditorHeader({
onRefetch, onRefetch,
onSave, onSave,
}: ProviderEditorHeaderProps) { }: ProviderEditorHeaderProps) {
const { t } = useTranslation();
return ( return (
<div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0"> <div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -67,12 +69,13 @@ export function ProviderEditorHeader({
</div> </div>
{isRemoteMode ? ( {isRemoteMode ? (
<p className="text-xs text-blue-600 dark:text-blue-400 mt-0.5"> <p className="text-xs text-blue-600 dark:text-blue-400 mt-0.5">
{/* TODO i18n: missing key for remote traffic text */}
Traffic auto-routed to remote server Traffic auto-routed to remote server
</p> </p>
) : ( ) : (
data && ( data && (
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Last modified: {new Date(data.mtime).toLocaleString()} {t('providerEditor.lastModified')}: {new Date(data.mtime).toLocaleString()}
</p> </p>
) )
)} )}
@@ -10,6 +10,7 @@ import { Info, Shield } from 'lucide-react';
import { UsageCommand } from './usage-command'; import { UsageCommand } from './usage-command';
import type { SettingsResponse } from './types'; import type { SettingsResponse } from './types';
import type { AuthStatus, CliTarget } from '@/lib/api-client'; import type { AuthStatus, CliTarget } from '@/lib/api-client';
import { useTranslation } from 'react-i18next';
interface ProviderInfoTabProps { interface ProviderInfoTabProps {
provider: string; provider: string;
@@ -28,6 +29,7 @@ export function ProviderInfoTab({
authStatus, authStatus,
supportsModelConfig = false, supportsModelConfig = false,
}: ProviderInfoTabProps) { }: ProviderInfoTabProps) {
const { t } = useTranslation();
const resolvedTarget = defaultTarget || 'claude'; const resolvedTarget = defaultTarget || 'claude';
const isDroidTarget = resolvedTarget === 'droid'; const isDroidTarget = resolvedTarget === 'droid';
const isCodexProvider = provider === 'codex'; const isCodexProvider = provider === 'codex';
@@ -44,17 +46,22 @@ export function ProviderInfoTab({
<div> <div>
<h3 className="text-sm font-medium flex items-center gap-2 mb-3"> <h3 className="text-sm font-medium flex items-center gap-2 mb-3">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />
{/* TODO i18n: missing key for "Provider Information" */}
Provider Information Provider Information
</h3> </h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm"> <div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Provider</span> <span className="font-medium text-muted-foreground">
{t('providerEditor.provider')}
</span>
<span className="font-mono">{displayName}</span> <span className="font-mono">{displayName}</span>
</div> </div>
{data && ( {data && (
<> <>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">File Path</span> <span className="font-medium text-muted-foreground">
{t('providerEditor.filePath')}
</span>
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all"> <code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
{data.path} {data.path}
@@ -63,13 +70,17 @@ export function ProviderInfoTab({
</div> </div>
</div> </div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Last Modified</span> <span className="font-medium text-muted-foreground">
{t('providerEditor.lastModified')}
</span>
<span className="text-xs">{new Date(data.mtime).toLocaleString()}</span> <span className="text-xs">{new Date(data.mtime).toLocaleString()}</span>
</div> </div>
</> </>
)} )}
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Status</span> <span className="font-medium text-muted-foreground">
{t('providerEditor.status')}
</span>
{authStatus.authenticated ? ( {authStatus.authenticated ? (
<Badge <Badge
variant="outline" variant="outline"
@@ -85,7 +96,9 @@ export function ProviderInfoTab({
)} )}
</div> </div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Default Target</span> <span className="font-medium text-muted-foreground">
{t('providerEditor.defaultTarget')}
</span>
<span className="font-mono">{resolvedTarget}</span> <span className="font-mono">{resolvedTarget}</span>
</div> </div>
</div> </div>
@@ -93,7 +106,7 @@ export function ProviderInfoTab({
{/* Quick Usage */} {/* Quick Usage */}
<div> <div>
<h3 className="text-sm font-medium mb-3">Quick Usage</h3> <h3 className="text-sm font-medium mb-3">{t('providerEditor.quickUsage')}</h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm"> <div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
<UsageCommand label="Run with prompt" command={`ccs ${provider} "your prompt"`} /> <UsageCommand label="Run with prompt" command={`ccs ${provider} "your prompt"`} />
{isCodexProvider && ( {isCodexProvider && (
@@ -7,6 +7,7 @@ import { lazy, Suspense } from 'react';
import { Loader2, X, AlertTriangle } from 'lucide-react'; import { Loader2, X, AlertTriangle } from 'lucide-react';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator'; import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
import type { RawEditorSectionProps } from './types'; import type { RawEditorSectionProps } from './types';
import { useTranslation } from 'react-i18next';
// Lazy load CodeEditor // Lazy load CodeEditor
const CodeEditor = lazy(() => const CodeEditor = lazy(() =>
@@ -21,6 +22,7 @@ export function RawEditorSection({
profileEnv, profileEnv,
missingRequiredFields = [], missingRequiredFields = [],
}: RawEditorSectionProps) { }: RawEditorSectionProps) {
const { t } = useTranslation();
const hasMissingFields = missingRequiredFields.length > 0; const hasMissingFields = missingRequiredFields.length > 0;
return ( return (
@@ -28,7 +30,7 @@ export function RawEditorSection({
fallback={ fallback={
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" /> <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span> <span className="ml-2 text-muted-foreground">{t('providerEditor.loadingEditor')}</span>
</div> </div>
} }
> >
@@ -6,6 +6,7 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import i18n from '@/lib/i18n';
import type { SettingsResponse, UseProviderEditorReturn } from './types'; import type { SettingsResponse, UseProviderEditorReturn } from './types';
import type { ProviderCatalog } from '../provider-model-selector'; import type { ProviderCatalog } from '../provider-model-selector';
import { import {
@@ -177,11 +178,11 @@ export function useProviderEditor(
setRawJsonEdits(null); setRawJsonEdits(null);
// Show warning if fields missing (runtime uses defaults) // Show warning if fields missing (runtime uses defaults)
if (responseData?.warning) { if (responseData?.warning) {
toast.success('Settings saved', { toast.success(i18n.t('settings.saved'), {
description: responseData.warning, description: responseData.warning,
}); });
} else { } else {
toast.success('Settings saved'); toast.success(i18n.t('settings.saved'));
} }
}, },
onError: (error: Error) => { onError: (error: Error) => {
@@ -379,12 +379,12 @@ export function FlexibleModelSelector({
{model.tier === 'paid' && <PaidBadge label={t('providerModelSelector.paid')} />} {model.tier === 'paid' && <PaidBadge label={t('providerModelSelector.paid')} />}
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Shadowed {t('providerModelSelector.shadowed')}
</Badge> </Badge>
) : null} ) : null}
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? ( {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Prefix only {t('providerModelSelector.prefixOnly')}
</Badge> </Badge>
) : null} ) : null}
{isCodexProvider && <CodexEffortBadge modelId={model.id} />} {isCodexProvider && <CodexEffortBadge modelId={model.id} />}
@@ -425,12 +425,12 @@ export function FlexibleModelSelector({
</span> </span>
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Shadowed {t('providerModelSelector.shadowed')}
</Badge> </Badge>
) : null} ) : null}
{routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? ( {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? (
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Prefix only {t('providerModelSelector.prefixOnly')}
</Badge> </Badge>
) : null} ) : null}
{isCodexProvider && <CodexEffortBadge modelId={model.id} />} {isCodexProvider && <CodexEffortBadge modelId={model.id} />}
@@ -450,7 +450,7 @@ export function FlexibleModelSelector({
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs">{value}</span> <span className="truncate font-mono text-xs">{value}</span>
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Current {t('providerModelSelector.current')}
</Badge> </Badge>
</div> </div>
), ),
@@ -458,7 +458,7 @@ export function FlexibleModelSelector({
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="truncate font-mono text-xs">{value}</span> <span className="truncate font-mono text-xs">{value}</span>
<Badge variant="outline" className="text-[9px] h-4 px-1"> <Badge variant="outline" className="text-[9px] h-4 px-1">
Current {t('providerModelSelector.current')}
</Badge> </Badge>
</div> </div>
), ),
@@ -489,7 +489,11 @@ export function FlexibleModelSelector({
? [ ? [
{ {
key: 'current', key: 'current',
label: <span className="text-xs text-muted-foreground">Current value</span>, label: (
<span className="text-xs text-muted-foreground">
{t('providerModelSelector.currentValue')}
</span>
),
}, },
] ]
: []), : []),
@@ -531,8 +535,8 @@ export function FlexibleModelSelector({
> >
<div className="font-medium"> <div className="font-medium">
{selectedRoutingHint.pinnedAvailable {selectedRoutingHint.pinnedAvailable
? 'Preferred pinned model:' ? t('providerModelSelector.preferredPinnedModel')
: 'Pinned route status:'}{' '} : t('providerModelSelector.pinnedRouteStatus')}{' '}
<code> <code>
{selectedRoutingHint.pinnedAvailable {selectedRoutingHint.pinnedAvailable
? selectedRoutingHint.recommendedModelId ? selectedRoutingHint.recommendedModelId
@@ -544,7 +548,7 @@ export function FlexibleModelSelector({
) : null} ) : null}
{value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? ( {value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? (
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2.5 py-2 text-[11px] text-amber-900 dark:border-amber-500/30 dark:bg-amber-950/25 dark:text-amber-100"> <div className="rounded-md border border-amber-300/60 bg-amber-50 px-2.5 py-2 text-[11px] text-amber-900 dark:border-amber-500/30 dark:bg-amber-950/25 dark:text-amber-100">
Pinned model is not currently advertised by the proxy: <code>{value}</code> {t('providerModelSelector.pinnedModelNotAdvertised', { model: value })}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -4,6 +4,7 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import type { CliproxyRoutingState, RoutingStrategy } from '@/lib/api-client'; import type { CliproxyRoutingState, RoutingStrategy } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface RoutingGuidanceCardProps { interface RoutingGuidanceCardProps {
className?: string; className?: string;
@@ -35,6 +36,7 @@ export function RoutingGuidanceCard({
error, error,
onApply, onApply,
}: RoutingGuidanceCardProps) { }: RoutingGuidanceCardProps) {
const { t } = useTranslation();
const currentStrategy = state?.strategy ?? 'round-robin'; const currentStrategy = state?.strategy ?? 'round-robin';
const [selected, setSelected] = useState<RoutingStrategy>(currentStrategy); const [selected, setSelected] = useState<RoutingStrategy>(currentStrategy);
const [detailsOpen, setDetailsOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false);
@@ -114,7 +116,7 @@ export function RoutingGuidanceCard({
<div className="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-muted text-primary"> <div className="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-muted text-primary">
<ArrowRightLeft className="h-4 w-4" /> <ArrowRightLeft className="h-4 w-4" />
</div> </div>
<div className="text-sm font-medium">Routing strategy</div> <div className="text-sm font-medium">{t('routingGuidance.routingStrategy')}</div>
<Badge variant="secondary">{currentStrategy}</Badge> <Badge variant="secondary">{currentStrategy}</Badge>
{state ? <Badge variant="outline">{sourceLabel}</Badge> : null} {state ? <Badge variant="outline">{sourceLabel}</Badge> : null}
{state ? <Badge variant="outline">{state.target}</Badge> : null} {state ? <Badge variant="outline">{state.target}</Badge> : null}
@@ -164,6 +166,7 @@ export function RoutingGuidanceCard({
) : ( ) : (
<ChevronDown className="mr-1 h-3.5 w-3.5" /> <ChevronDown className="mr-1 h-3.5 w-3.5" />
)} )}
{/* TODO i18n: missing key for detail toggle */}
{detailToggleLabel} {detailToggleLabel}
</Button> </Button>
<Button size="sm" onClick={() => onApply(selected)} disabled={saveDisabled || !!error}> <Button size="sm" onClick={() => onApply(selected)} disabled={saveDisabled || !!error}>
@@ -173,9 +176,9 @@ export function RoutingGuidanceCard({
</div> </div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground xl:col-span-2"> <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground xl:col-span-2">
<span>Round robin spreads usage.</span> <span>{t('routingGuidance.roundRobin')}</span>
<span className="hidden text-border sm:inline">•</span> <span className="hidden text-border sm:inline">•</span>
<span>Fill first keeps backup accounts cold until they are needed.</span> <span>{t('routingGuidance.fillFirst')}</span>
</div> </div>
{error ? ( {error ? (
@@ -1,4 +1,5 @@
import { FileCode2, History, PenLine, Settings2 } from 'lucide-react'; import { FileCode2, History, PenLine, Settings2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { CodexFeaturesCard } from '@/components/compatible-cli/codex-features-card'; import { CodexFeaturesCard } from '@/components/compatible-cli/codex-features-card';
import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card'; import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card';
import { CodexModelProvidersCard } from '@/components/compatible-cli/codex-model-providers-card'; import { CodexModelProvidersCard } from '@/components/compatible-cli/codex-model-providers-card';
@@ -51,6 +52,8 @@ export function CodexControlCenterTab({
saving, saving,
onPatch, onPatch,
}: CodexControlCenterTabProps) { }: CodexControlCenterTabProps) {
const { t } = useTranslation();
return ( return (
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<div className="space-y-6 pr-1 pb-6"> <div className="space-y-6 pr-1 pb-6">
@@ -65,7 +68,7 @@ export function CodexControlCenterTab({
</div> </div>
<div> <div>
<h2 className="text-base font-semibold tracking-tight text-foreground"> <h2 className="text-base font-semibold tracking-tight text-foreground">
Structured controls boundary {t('codex.controlCenter')}
</h2> </h2>
</div> </div>
</div> </div>
@@ -74,6 +77,7 @@ export function CodexControlCenterTab({
<li className="flex items-start gap-2.5"> <li className="flex items-start gap-2.5">
<PenLine className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" /> <PenLine className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" />
<span className="leading-relaxed"> <span className="leading-relaxed">
{/* TODO i18n: missing key codex.writesUserLayer */}
Writes exclusively to user-layer{' '} Writes exclusively to user-layer{' '}
<code className="text-[11px] bg-muted/70 px-1.5 py-0.5 rounded border border-border/50"> <code className="text-[11px] bg-muted/70 px-1.5 py-0.5 rounded border border-border/50">
config.toml config.toml
@@ -83,6 +87,7 @@ export function CodexControlCenterTab({
<li className="flex items-start gap-2.5"> <li className="flex items-start gap-2.5">
<History className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" /> <History className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" />
<span className="leading-relaxed"> <span className="leading-relaxed">
{/* TODO i18n: missing key codex.noRepoTrustReflection */}
Does not reflect repo trust layers or CLI overrides Does not reflect repo trust layers or CLI overrides
</span> </span>
</li> </li>
@@ -95,9 +100,11 @@ export function CodexControlCenterTab({
<FileCode2 className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" /> <FileCode2 className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[11px] font-bold text-amber-700 dark:text-amber-300 uppercase tracking-wider"> <p className="text-[11px] font-bold text-amber-700 dark:text-amber-300 uppercase tracking-wider">
{/* TODO i18n: missing key codex.formattingNote */}
Formatting Note Formatting Note
</p> </p>
<p className="text-[13px] leading-relaxed text-amber-800/80 dark:text-amber-200/70"> <p className="text-[13px] leading-relaxed text-amber-800/80 dark:text-amber-200/70">
{/* TODO i18n: missing key codex.formattingNoteDesc */}
Saves normalize TOML formatting and strip comments. Switch to the raw editor Saves normalize TOML formatting and strip comments. Switch to the raw editor
if exact layout matters. if exact layout matters.
</p> </p>
@@ -115,7 +122,7 @@ export function CodexControlCenterTab({
disabledReason={disabledReason} disabledReason={disabledReason}
saving={saving} saving={saving}
onSave={(values: CodexTopLevelSettingsPatch) => onSave={(values: CodexTopLevelSettingsPatch) =>
onPatch({ kind: 'top-level', values }, 'Saved top-level Codex settings.') onPatch({ kind: 'top-level', values }, t('toasts.codexSaved'))
} }
/> />
@@ -128,6 +135,7 @@ export function CodexControlCenterTab({
onSave={(projectPath, trustLevel) => onSave={(projectPath, trustLevel) =>
onPatch( onPatch(
{ kind: 'project-trust', path: projectPath, trustLevel }, { kind: 'project-trust', path: projectPath, trustLevel },
// TODO i18n: missing keys codex.savedProjectTrust / codex.removedProjectTrust
trustLevel ? 'Saved project trust entry.' : 'Removed project trust entry.' trustLevel ? 'Saved project trust entry.' : 'Removed project trust entry.'
) )
} }
@@ -143,14 +151,23 @@ export function CodexControlCenterTab({
onSave={(name, values: CodexProfilePatchValues, setAsActive) => onSave={(name, values: CodexProfilePatchValues, setAsActive) =>
onPatch( onPatch(
{ kind: 'profile', action: 'upsert', name, values, setAsActive }, { kind: 'profile', action: 'upsert', name, values, setAsActive },
// TODO i18n: missing key codex.savedProfile
'Saved profile.' 'Saved profile.'
) )
} }
onDelete={(name) => onDelete={(name) =>
onPatch({ kind: 'profile', action: 'delete', name }, 'Deleted profile.') onPatch(
{ kind: 'profile', action: 'delete', name },
// TODO i18n: missing key codex.deletedProfile
'Deleted profile.'
)
} }
onSetActive={(name) => onSetActive={(name) =>
onPatch({ kind: 'profile', action: 'set-active', name }, 'Set active profile.') onPatch(
{ kind: 'profile', action: 'set-active', name },
// TODO i18n: missing key codex.setActiveProfile
'Set active profile.'
)
} }
/> />
@@ -162,11 +179,16 @@ export function CodexControlCenterTab({
onSave={(name, values) => onSave={(name, values) =>
onPatch( onPatch(
{ kind: 'model-provider', action: 'upsert', name, values }, { kind: 'model-provider', action: 'upsert', name, values },
// TODO i18n: missing key codex.savedModelProvider
'Saved model provider.' 'Saved model provider.'
) )
} }
onDelete={(name) => onDelete={(name) =>
onPatch({ kind: 'model-provider', action: 'delete', name }, 'Deleted model provider.') onPatch(
{ kind: 'model-provider', action: 'delete', name },
// TODO i18n: missing key codex.deletedModelProvider
'Deleted model provider.'
)
} }
/> />
@@ -176,10 +198,18 @@ export function CodexControlCenterTab({
disabledReason={disabledReason} disabledReason={disabledReason}
saving={saving} saving={saving}
onSave={(name, values) => onSave={(name, values) =>
onPatch({ kind: 'mcp-server', action: 'upsert', name, values }, 'Saved MCP server.') onPatch(
{ kind: 'mcp-server', action: 'upsert', name, values },
// TODO i18n: missing key codex.savedMcpServer
'Saved MCP server.'
)
} }
onDelete={(name) => onDelete={(name) =>
onPatch({ kind: 'mcp-server', action: 'delete', name }, 'Deleted MCP server.') onPatch(
{ kind: 'mcp-server', action: 'delete', name },
// TODO i18n: missing key codex.deletedMcpServer
'Deleted MCP server.'
)
} }
/> />
@@ -189,7 +219,11 @@ export function CodexControlCenterTab({
disabled={disabled} disabled={disabled}
disabledReason={disabledReason} disabledReason={disabledReason}
onToggle={(feature, enabled) => onToggle={(feature, enabled) =>
onPatch({ kind: 'feature', feature, enabled }, 'Saved feature toggle.') onPatch(
{ kind: 'feature', feature, enabled },
// TODO i18n: missing key codex.savedFeatureToggle
'Saved feature toggle.'
)
} }
/> />
</div> </div>
@@ -1,5 +1,6 @@
import { type ReactNode } from 'react'; import { type ReactNode } from 'react';
import { ExternalLink, ShieldCheck } from 'lucide-react'; import { ExternalLink, ShieldCheck } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
@@ -85,6 +86,8 @@ interface CodexDocsTabProps {
} }
export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) { export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
const { t } = useTranslation();
const docsReference = diagnostics.docsReference ?? { const docsReference = diagnostics.docsReference ?? {
notes: [], notes: [],
links: [], links: [],
@@ -103,15 +106,18 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<ShieldCheck className="h-4 w-4" /> <ShieldCheck className="h-4 w-4" />
{/* TODO i18n: missing key codex.ccsBridgeRecipe */}
CCS bridge recipe CCS bridge recipe
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground"> <CardContent className="space-y-3 text-sm text-muted-foreground">
<div className="space-y-1.5"> <div className="space-y-1.5">
<p> <p>
{/* TODO i18n: missing key codex.builtInLabel */}
<strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider shortcut. <strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider shortcut.
</p> </p>
<p> <p>
{/* TODO i18n: missing key codex.nativeLabelRecipe */}
<strong>Native:</strong> Configure the recipe below to use CLIProxy directly with{' '} <strong>Native:</strong> Configure the recipe below to use CLIProxy directly with{' '}
<code>codex</code>. <code>codex</code>.
</p> </p>
@@ -121,12 +127,15 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
</pre> </pre>
<ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1"> <ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1">
<li> <li>
{/* TODO i18n: missing key codex.saveCliproxyProvider */}
Save the <code>cliproxy</code> provider in your user config. Save the <code>cliproxy</code> provider in your user config.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.setTopLevelModelProvider */}
Set top-level <code>model_provider</code> to <code>cliproxy</code>. Set top-level <code>model_provider</code> to <code>cliproxy</code>.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.exportCliproxyApiKey */}
Export <code>CLIPROXY_API_KEY</code> before launching native Codex. Export <code>CLIPROXY_API_KEY</code> before launching native Codex.
</li> </li>
</ol> </ol>
@@ -137,6 +146,7 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<ShieldCheck className="h-4 w-4" /> <ShieldCheck className="h-4 w-4" />
{/* TODO i18n: missing key codex.upstreamNotes */}
Upstream notes Upstream notes
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -150,7 +160,9 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
)} )}
<Separator /> <Separator />
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs uppercase tracking-wide text-muted-foreground">Codex docs</p> <p className="text-xs uppercase tracking-wide text-muted-foreground">
{t('codex.codexDocs')}
</p>
<div className="space-y-1.5"> <div className="space-y-1.5">
{docsLinks.map((link) => ( {docsLinks.map((link) => (
<a <a
@@ -175,6 +187,7 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
<Separator /> <Separator />
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs uppercase tracking-wide text-muted-foreground"> <p className="text-xs uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.providerBridgeReference */}
Provider / bridge reference Provider / bridge reference
</p> </p>
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -204,12 +217,14 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
<> <>
<Separator /> <Separator />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.providerValues */}
Provider values: {docsReference.providerValues.join(', ')} Provider values: {docsReference.providerValues.join(', ')}
</p> </p>
</> </>
)} )}
{docsReference.settingsHierarchy.length > 0 && ( {docsReference.settingsHierarchy.length > 0 && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.settingsHierarchy */}
Settings hierarchy: {docsReference.settingsHierarchy.join(' -> ')} Settings hierarchy: {docsReference.settingsHierarchy.join(' -> ')}
</p> </p>
)} )}
@@ -1,4 +1,5 @@
import { Sparkles } from 'lucide-react'; import { Sparkles } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -20,6 +21,8 @@ export function CodexFeaturesCard({
disabledReason, disabledReason,
onToggle, onToggle,
}: CodexFeaturesCardProps) { }: CodexFeaturesCardProps) {
const { t } = useTranslation();
const knownFeatureNames = new Set(catalog.map((feature) => feature.name)); const knownFeatureNames = new Set(catalog.map((feature) => feature.name));
const configOnlyFeatures = Object.entries(state) const configOnlyFeatures = Object.entries(state)
.filter(([name]) => !knownFeatureNames.has(name)) .filter(([name]) => !knownFeatureNames.has(name))
@@ -27,9 +30,11 @@ export function CodexFeaturesCard({
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
// TODO i18n: missing key codex.features
title="Features" title="Features"
badge="features" badge="features"
icon={<Sparkles className="h-4 w-4" />} icon={<Sparkles className="h-4 w-4" />}
// TODO i18n: missing key codex.featuresDesc
description="Toggle the supported Codex feature flags CCS can safely manage." description="Toggle the supported Codex feature flags CCS can safely manage."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
@@ -58,7 +63,7 @@ export function CodexFeaturesCard({
onClick={() => onToggle(feature.name, null)} onClick={() => onToggle(feature.name, null)}
disabled={disabled} disabled={disabled}
> >
Use default {t('codex.useDefault')}
</Button> </Button>
) : null} ) : null}
<Switch <Switch
@@ -76,9 +81,11 @@ export function CodexFeaturesCard({
<div className="space-y-2"> <div className="space-y-2">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground"> <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.configOnlyFlags */}
Existing config-only flags Existing config-only flags
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.configOnlyFlagsDesc */}
These feature keys already exist in your `config.toml`, so CCS can surface them These feature keys already exist in your `config.toml`, so CCS can surface them
without claiming full catalog coverage. without claiming full catalog coverage.
</p> </p>
@@ -92,17 +99,19 @@ export function CodexFeaturesCard({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-sm font-medium">{name}</p> <p className="text-sm font-medium">{name}</p>
<Badge variant="secondary" className="text-[10px]"> <Badge variant="secondary" className="text-[10px]">
{/* TODO i18n: missing key codex.existing */}
existing existing
</Badge> </Badge>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing keys codex.nonBooleanForm / codex.discoveredFromFile */}
{current === null {current === null
? 'Stored in a non-boolean form. Use raw TOML if you need to edit it.' ? '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."} : "Discovered from the current file instead of CCS's built-in catalog."}
</p> </p>
</div> </div>
{current === null ? ( {current === null ? (
<Badge variant="outline">Raw only</Badge> <Badge variant="outline">{t('codex.rawOnly')}</Badge>
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -111,7 +120,7 @@ export function CodexFeaturesCard({
onClick={() => onToggle(name, null)} onClick={() => onToggle(name, null)}
disabled={disabled} disabled={disabled}
> >
Use default {t('codex.useDefault')}
</Button> </Button>
<Switch <Switch
checked={current === true} checked={current === true}
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Loader2, PlugZap, Trash2 } from 'lucide-react'; import { Loader2, PlugZap, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
@@ -67,6 +68,7 @@ function McpServerEditor({
onSave, onSave,
onDelete, onDelete,
}: McpServerEditorProps) { }: McpServerEditorProps) {
const { t } = useTranslation();
const [draft, setDraft] = useState<CodexMcpServerEntry>(initialDraft); const [draft, setDraft] = useState<CodexMcpServerEntry>(initialDraft);
return ( return (
@@ -92,8 +94,8 @@ function McpServerEditor({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="stdio">stdio</SelectItem> <SelectItem value="stdio">{t('codex.stdio')}</SelectItem>
<SelectItem value="streamable-http">streamable-http</SelectItem> <SelectItem value="streamable-http">{t('codex.streamableHttp')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{draft.transport === 'stdio' ? ( {draft.transport === 'stdio' ? (
@@ -136,6 +138,7 @@ function McpServerEditor({
startupTimeoutSec: event.target.value ? Number(event.target.value) : null, startupTimeoutSec: event.target.value ? Number(event.target.value) : null,
})) }))
} }
// TODO i18n: missing key codex.startupTimeoutSec
placeholder="Startup timeout (sec)" placeholder="Startup timeout (sec)"
disabled={disabled} disabled={disabled}
/> />
@@ -149,6 +152,7 @@ function McpServerEditor({
toolTimeoutSec: event.target.value ? Number(event.target.value) : null, toolTimeoutSec: event.target.value ? Number(event.target.value) : null,
})) }))
} }
// TODO i18n: missing key codex.toolTimeoutSec
placeholder="Tool timeout (sec)" placeholder="Tool timeout (sec)"
disabled={disabled} disabled={disabled}
/> />
@@ -172,6 +176,7 @@ function McpServerEditor({
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> <label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
{/* TODO i18n: missing key codex.enabled */}
Enabled Enabled
<Switch <Switch
checked={draft.enabled} checked={draft.enabled}
@@ -180,6 +185,7 @@ function McpServerEditor({
/> />
</label> </label>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> <label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
{/* TODO i18n: missing key codex.required */}
Required Required
<Switch <Switch
checked={draft.required} checked={draft.required}
@@ -192,6 +198,7 @@ function McpServerEditor({
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}> <Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
{/* TODO i18n: missing key common.delete */}
Delete Delete
</Button> </Button>
<Button <Button
@@ -212,6 +219,7 @@ function McpServerEditor({
disabled={disabled || saving || draft.name.trim().length === 0} disabled={disabled || saving || draft.name.trim().length === 0}
> >
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveMcpServer */}
Save MCP server Save MCP server
</Button> </Button>
</div> </div>
@@ -227,6 +235,8 @@ export function CodexMcpServersCard({
onSave, onSave,
onDelete, onDelete,
}: CodexMcpServersCardProps) { }: CodexMcpServersCardProps) {
const { t } = useTranslation();
const [selectedName, setSelectedName] = useState('new'); const [selectedName, setSelectedName] = useState('new');
const selectedEntry = useMemo( const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null, () => entries.find((entry) => entry.name === selectedName) ?? null,
@@ -237,18 +247,21 @@ export function CodexMcpServersCard({
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
// TODO i18n: missing key codex.mcpServers
title="MCP servers" title="MCP servers"
badge="mcp_servers" badge="mcp_servers"
icon={<PlugZap className="h-4 w-4" />} icon={<PlugZap className="h-4 w-4" />}
// TODO i18n: missing key codex.mcpServersDesc
description="Manage the safe MCP transport fields. Keep auth headers and bearer tokens in raw TOML." description="Manage the safe MCP transport fields. Keep auth headers and bearer tokens in raw TOML."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}> <Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger> <SelectTrigger>
{/* TODO i18n: missing key codex.selectMcpServer */}
<SelectValue placeholder="Select MCP server" /> <SelectValue placeholder="Select MCP server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="new">Create new MCP server</SelectItem> <SelectItem value="new">{t('codex.createNewMcpServer')}</SelectItem>
{entries.map((entry) => ( {entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}> <SelectItem key={entry.name} value={entry.name}>
{entry.name} {entry.name}
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { KeyRound, Loader2, Trash2 } from 'lucide-react'; import { KeyRound, Loader2, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
@@ -62,6 +63,7 @@ function ModelProviderEditor({
onSave, onSave,
onDelete, onDelete,
}: ModelProviderEditorProps) { }: ModelProviderEditorProps) {
const { t } = useTranslation();
const [draft, setDraft] = useState<CodexModelProviderEntry>(initialDraft); const [draft, setDraft] = useState<CodexModelProviderEntry>(initialDraft);
return ( return (
@@ -69,8 +71,10 @@ function ModelProviderEditor({
{isNew && ( {isNew && (
<div className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-dashed px-3 py-2 text-xs text-muted-foreground"> <div className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-dashed px-3 py-2 text-xs text-muted-foreground">
<p> <p>
{/* TODO i18n: missing key codex.cliproxyQuickStart */}
Quick start: apply the CLIProxy Codex preset here, then set{' '} Quick start: apply the CLIProxy Codex preset here, then set{' '}
<strong>Default provider</strong> to <code>cliproxy</code> in Top-level settings. <strong>{t('codex.defaultProvider')}</strong> to <code>cliproxy</code> in Top-level
settings.
</p> </p>
<Button <Button
variant="outline" variant="outline"
@@ -78,6 +82,7 @@ function ModelProviderEditor({
onClick={() => setDraft(CLIPROXY_CODEX_PROVIDER_PRESET)} onClick={() => setDraft(CLIPROXY_CODEX_PROVIDER_PRESET)}
disabled={disabled} disabled={disabled}
> >
{/* TODO i18n: missing key codex.useCliproxyCodexPreset */}
Use CLIProxy Codex preset Use CLIProxy Codex preset
</Button> </Button>
</div> </div>
@@ -87,6 +92,7 @@ function ModelProviderEditor({
<Input <Input
value={draft.name} value={draft.name}
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
// TODO i18n: missing key codex.providerId
placeholder="Provider id" placeholder="Provider id"
disabled={disabled || !isNew} disabled={disabled || !isNew}
/> />
@@ -95,6 +101,7 @@ function ModelProviderEditor({
onChange={(event) => onChange={(event) =>
setDraft((current) => ({ ...current, displayName: event.target.value || null })) setDraft((current) => ({ ...current, displayName: event.target.value || null }))
} }
// TODO i18n: missing key codex.displayName
placeholder="Display name" placeholder="Display name"
disabled={disabled} disabled={disabled}
/> />
@@ -126,10 +133,11 @@ function ModelProviderEditor({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="responses">responses</SelectItem> <SelectItem value="responses">{t('codex.responses')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> <label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
{/* TODO i18n: missing key codex.requiresOpenaiAuth */}
Requires OpenAI auth Requires OpenAI auth
<Switch <Switch
checked={draft.requiresOpenaiAuth} checked={draft.requiresOpenaiAuth}
@@ -140,6 +148,7 @@ function ModelProviderEditor({
/> />
</label> </label>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm"> <label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
{/* TODO i18n: missing key codex.supportsWebsockets */}
Supports websockets Supports websockets
<Switch <Switch
checked={draft.supportsWebsockets} checked={draft.supportsWebsockets}
@@ -154,6 +163,7 @@ function ModelProviderEditor({
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}> <Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
{/* TODO i18n: missing key common.delete */}
Delete Delete
</Button> </Button>
<Button <Button
@@ -170,14 +180,16 @@ function ModelProviderEditor({
disabled={disabled || saving || draft.name.trim().length === 0} disabled={disabled || saving || draft.name.trim().length === 0}
> >
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveProvider */}
Save provider Save provider
</Button> </Button>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.nativeCodexCliproxyHint */}
If you want plain native <code>codex</code> to default to CLIProxy, save a provider named{' '} If you want plain native <code>codex</code> to default to CLIProxy, save a provider named{' '}
<code>cliproxy</code> with <code>CLIPROXY_API_KEY</code> here, then pick{' '} <code>cliproxy</code> with <code>CLIPROXY_API_KEY</code> here, then pick{' '}
<code>cliproxy</code> in the <strong>Default provider</strong> control above. <code>cliproxy</code> in the <strong>{t('codex.defaultProvider')}</strong> control above.
</p> </p>
</> </>
); );
@@ -191,6 +203,7 @@ export function CodexModelProvidersCard({
onSave, onSave,
onDelete, onDelete,
}: CodexModelProvidersCardProps) { }: CodexModelProvidersCardProps) {
const { t } = useTranslation();
const [selectedName, setSelectedName] = useState<string>('new'); const [selectedName, setSelectedName] = useState<string>('new');
const selectedEntry = useMemo( const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null, () => entries.find((entry) => entry.name === selectedName) ?? null,
@@ -201,18 +214,21 @@ export function CodexModelProvidersCard({
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
// TODO i18n: missing key codex.modelProviders
title="Model providers" title="Model providers"
badge="model_providers" badge="model_providers"
icon={<KeyRound className="h-4 w-4" />} icon={<KeyRound className="h-4 w-4" />}
// TODO i18n: missing key codex.modelProvidersDesc
description="Edit the common provider fields CCS can support safely. Keep secret migration and inline bearer tokens in raw TOML." description="Edit the common provider fields CCS can support safely. Keep secret migration and inline bearer tokens in raw TOML."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}> <Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger> <SelectTrigger>
{/* TODO i18n: missing key codex.selectProvider */}
<SelectValue placeholder="Select provider" /> <SelectValue placeholder="Select provider" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="new">Create new provider</SelectItem> <SelectItem value="new">{t('codex.createNewProvider')}</SelectItem>
{entries.map((entry) => ( {entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}> <SelectItem key={entry.name} value={entry.name}>
{entry.name} {entry.name}
@@ -8,6 +8,7 @@ import {
TerminalSquare, TerminalSquare,
XCircle, XCircle,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { QuickCommands } from '@/components/shared'; import { QuickCommands } from '@/components/shared';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -59,6 +60,8 @@ interface CodexOverviewTabProps {
} }
export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
const { t } = useTranslation();
const inspectProfileCommand = diagnostics.config.activeProfile const inspectProfileCommand = diagnostics.config.activeProfile
? `codex --profile ${diagnostics.config.activeProfile}` ? `codex --profile ${diagnostics.config.activeProfile}`
: 'codex'; : 'codex';
@@ -71,25 +74,29 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
{/* TODO i18n: missing key codex.howCodexWorks */}
How Codex works in CCS How Codex works in CCS
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-muted-foreground"> <CardContent className="text-sm text-muted-foreground">
<ul className="ml-4 list-disc space-y-1.5 [&>li]:pl-1"> <ul className="ml-4 list-disc space-y-1.5 [&>li]:pl-1">
<li>Codex is a first-class, runtime-only target in CCS v1.</li> <li>{t('codex.nativeDesc')}</li>
<li> <li>
{/* TODO i18n: missing key codex.nativeConfigLabel */}
<strong>Native config:</strong> <code>ccs-codex</code> and <code>ccsx</code> launch <strong>Native config:</strong> <code>ccs-codex</code> and <code>ccsx</code> launch
native Codex using your saved defaults. native Codex using your saved defaults.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.transientOverrides */}
<strong>Transient overrides:</strong> <code>ccsxp</code> (or{' '} <strong>Transient overrides:</strong> <code>ccsxp</code> (or{' '}
<code>ccs codex --target codex</code>) uses the CCS provider shortcut. <code>ccs codex --target codex</code>) uses the CCS provider shortcut.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.cliproxyDefault */}
<strong>CLIProxy default:</strong> To make plain <code>codex</code> use CLIProxy, <strong>CLIProxy default:</strong> To make plain <code>codex</code> use CLIProxy,
set <code>model_provider = "cliproxy"</code> and add the recipe below. set <code>model_provider = "cliproxy"</code> and add the recipe below.
</li> </li>
<li>API profiles continue to default to Claude or Droid.</li> <li>{t('codex.apiProfilesDefault')}</li>
</ul> </ul>
</CardContent> </CardContent>
</Card> </Card>
@@ -98,16 +105,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<TerminalSquare className="h-4 w-4" /> <TerminalSquare className="h-4 w-4" />
{/* TODO i18n: missing key codex.runtimeInstall */}
Runtime install Runtime install
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Status</span> <span className="text-sm text-muted-foreground">{t('codex.status')}</span>
<Badge variant={diagnostics.binary.installed ? 'default' : 'secondary'}> <Badge variant={diagnostics.binary.installed ? 'default' : 'secondary'}>
{/* TODO i18n: missing keys codex.detected / codex.notFound */}
{diagnostics.binary.installed ? 'Detected' : 'Not found'} {diagnostics.binary.installed ? 'Detected' : 'Not found'}
</Badge> </Badge>
</div> </div>
{/* TODO i18n: missing keys codex.detectionSource / codex.binaryPath / codex.installDirectory / codex.version / codex.nativeAliases / codex.ccsProviderShortcut / codex.configOverrideSupport */}
<DetailRow label="Detection source" value={diagnostics.binary.source} mono /> <DetailRow label="Detection source" value={diagnostics.binary.source} mono />
<DetailRow label="Binary path" value={diagnostics.binary.path || 'Not found'} mono /> <DetailRow label="Binary path" value={diagnostics.binary.path || 'Not found'} mono />
<DetailRow <DetailRow
@@ -121,6 +131,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<div className="flex items-center justify-between rounded-md border px-3 py-2"> <div className="flex items-center justify-between rounded-md border px-3 py-2">
<span className="text-sm text-muted-foreground">--config override support</span> <span className="text-sm text-muted-foreground">--config override support</span>
<Badge variant={diagnostics.binary.supportsConfigOverrides ? 'default' : 'secondary'}> <Badge variant={diagnostics.binary.supportsConfigOverrides ? 'default' : 'secondary'}>
{/* TODO i18n: missing keys codex.available / codex.missing */}
{diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'} {diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'}
</Badge> </Badge>
</div> </div>
@@ -131,6 +142,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<Route className="h-4 w-4" /> <Route className="h-4 w-4" />
{/* TODO i18n: missing key codex.cliproxyNativeCodex */}
CLIProxy-backed native Codex CLIProxy-backed native Codex
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -139,34 +151,39 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<> <>
<div className="space-y-1.5"> <div className="space-y-1.5">
<p> <p>
<strong>Two supported paths:</strong> <strong>{t('codex.twoSupportedPaths')}</strong>
</p> </p>
<ul className="ml-4 list-disc space-y-1 [&>li]:pl-1"> <ul className="ml-4 list-disc space-y-1 [&>li]:pl-1">
<li> <li>
{/* TODO i18n: missing key codex.builtInCcsxp */}
<strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider <strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider
shortcut. shortcut.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.nativeRecipe */}
<strong>Native:</strong> Configure the recipe below to use CLIProxy directly <strong>Native:</strong> Configure the recipe below to use CLIProxy directly
with <code>codex</code>. with <code>codex</code>.
</li> </li>
</ul> </ul>
</div> </div>
<div className="rounded-md border bg-muted/20 p-3"> <div className="rounded-md border bg-muted/20 p-3">
<p className="font-medium text-foreground">Saved native Codex recipe</p> <p className="font-medium text-foreground">{t('codex.codexNativeRecipe')}</p>
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground"> <pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground">
{CLIPROXY_NATIVE_CODEX_RECIPE} {CLIPROXY_NATIVE_CODEX_RECIPE}
</pre> </pre>
</div> </div>
<ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1"> <ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1">
<li> <li>
{/* TODO i18n: missing key codex.saveProviderNamedCliproxy */}
Save a provider named <code>cliproxy</code> with the base URL and env key above. Save a provider named <code>cliproxy</code> with the base URL and env key above.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.inTopLevelSetDefault */}
In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '} In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '}
<code>cliproxy</code>. <code>cliproxy</code>.
</li> </li>
<li> <li>
{/* TODO i18n: missing key codex.exportCliproxyApiKey */}
Export <code>CLIPROXY_API_KEY</code> in your shell before launching native Export <code>CLIPROXY_API_KEY</code> in your shell before launching native
Codex. Codex.
</li> </li>
@@ -174,6 +191,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
</> </>
) : ( ) : (
<p> <p>
{/* TODO i18n: missing key codex.noConfigOverrides */}
This Codex build can still use the native path, but CCS-backed Codex routing via{' '} This Codex build can still use the native path, but CCS-backed Codex routing via{' '}
<code>ccsxp</code> or <code>ccs codex --target codex</code> stays unavailable until <code>ccsxp</code> or <code>ccs codex --target codex</code> stays unavailable until
the detected Codex binary exposes <code>--config</code> overrides. the detected Codex binary exposes <code>--config</code> overrides.
@@ -186,30 +204,34 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<Folder className="h-4 w-4" /> <Folder className="h-4 w-4" />
{/* TODO i18n: missing key codex.configFile */}
Config file Config file
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<div className="rounded-md border p-3 space-y-1.5"> <div className="rounded-md border p-3 space-y-1.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="font-medium text-sm">User config</span> <span className="font-medium text-sm">{t('codex.userConfig')}</span>
{diagnostics.file.exists ? ( {diagnostics.file.exists ? (
<CheckCircle2 className="h-4 w-4 text-green-600" /> <CheckCircle2 className="h-4 w-4 text-green-600" />
) : ( ) : (
<XCircle className="h-4 w-4 text-muted-foreground" /> <XCircle className="h-4 w-4 text-muted-foreground" />
)} )}
</div> </div>
{/* TODO i18n: missing keys codex.path / codex.resolved / codex.size / codex.lastModified */}
<DetailRow label="Path" value={diagnostics.file.path} mono /> <DetailRow label="Path" value={diagnostics.file.path} mono />
<DetailRow label="Resolved" value={diagnostics.file.resolvedPath} mono /> <DetailRow label="Resolved" value={diagnostics.file.resolvedPath} mono />
<DetailRow label="Size" value={formatBytes(diagnostics.file.sizeBytes)} /> <DetailRow label="Size" value={formatBytes(diagnostics.file.sizeBytes)} />
<DetailRow label="Last modified" value={formatTimestamp(diagnostics.file.mtimeMs)} /> <DetailRow label="Last modified" value={formatTimestamp(diagnostics.file.mtimeMs)} />
{diagnostics.file.parseError && ( {diagnostics.file.parseError && (
<p className="text-xs text-amber-600"> <p className="text-xs text-amber-600">
{/* TODO i18n: missing key codex.tomlWarning */}
TOML warning: {diagnostics.file.parseError} TOML warning: {diagnostics.file.parseError}
</p> </p>
)} )}
{diagnostics.file.readError && ( {diagnostics.file.readError && (
<p className="text-xs text-destructive"> <p className="text-xs text-destructive">
{/* TODO i18n: missing key codex.readWarning */}
Read warning: {diagnostics.file.readError} Read warning: {diagnostics.file.readError}
</p> </p>
)} )}
@@ -221,34 +243,46 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<ShieldCheck className="h-4 w-4" /> <ShieldCheck className="h-4 w-4" />
{/* TODO i18n: missing key codex.currentUserLayerSummary */}
Current user-layer summary Current user-layer summary
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
<DetailRow label="Model" value={diagnostics.config.model || 'Not set'} mono /> {/* TODO i18n: missing key codex.notSet */}
<DetailRow <DetailRow
label="Model provider" label={t('codex.model')}
value={diagnostics.config.model || 'Not set'}
mono
/>
<DetailRow
label={t('codex.defaultProvider')}
value={diagnostics.config.modelProvider || 'Not set'} value={diagnostics.config.modelProvider || 'Not set'}
mono mono
/> />
<DetailRow <DetailRow
// TODO i18n: missing key codex.activeProfile
label="Active profile" label="Active profile"
value={diagnostics.config.activeProfile || 'Not set'} value={diagnostics.config.activeProfile || 'Not set'}
mono mono
/> />
<DetailRow <DetailRow
label="Approval policy" label={t('codex.approvalPolicy')}
value={diagnostics.config.approvalPolicy || 'Not set'} value={diagnostics.config.approvalPolicy || 'Not set'}
mono mono
/> />
<DetailRow <DetailRow
label="Sandbox mode" label={t('codex.sandboxMode')}
value={diagnostics.config.sandboxMode || 'Not set'} value={diagnostics.config.sandboxMode || 'Not set'}
mono mono
/> />
<DetailRow label="Web search" value={diagnostics.config.webSearch || 'Not set'} mono /> <DetailRow
label={t('codex.webSearch')}
value={diagnostics.config.webSearch || 'Not set'}
mono
/>
<Separator /> <Separator />
<div className="grid grid-cols-2 gap-2 text-xs"> <div className="grid grid-cols-2 gap-2 text-xs">
{/* TODO i18n: missing keys codex.providersCount / codex.profilesCount / codex.enabledFeaturesCount / codex.mcpServersCount */}
<Badge variant="outline" className="justify-center"> <Badge variant="outline" className="justify-center">
providers: {diagnostics.config.modelProviderCount} providers: {diagnostics.config.modelProviderCount}
</Badge> </Badge>
@@ -265,6 +299,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
{diagnostics.config.topLevelKeys.length > 0 && ( {diagnostics.config.topLevelKeys.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs uppercase tracking-wide text-muted-foreground"> <p className="text-xs uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.userLayerKeysPresent */}
User-layer keys present User-layer keys present
</p> </p>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -282,28 +317,31 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<QuickCommands <QuickCommands
snippets={[ snippets={[
{ {
// TODO i18n: missing key codex.nativeShortAlias
label: 'Native short alias', label: 'Native short alias',
command: 'ccsx', command: 'ccsx',
description: 'Launch the short native Codex runtime alias.', description: 'Launch the short native Codex runtime alias.',
}, },
{ {
// TODO i18n: missing key codex.ccsCodexShortcut
label: 'CCS Codex shortcut', label: 'CCS Codex shortcut',
command: 'ccsxp "your prompt"', command: 'ccsxp "your prompt"',
description: supportsManagedRouting description: supportsManagedRouting
? 'Run the built-in CCS Codex provider on native Codex.' ? t('codex.runBuiltInCodex')
: 'Requires a Codex build that exposes --config overrides.', : 'Requires a Codex build that exposes --config overrides.',
}, },
{ {
// TODO i18n: missing key codex.explicitProviderRoute
label: 'Explicit provider route', label: 'Explicit provider route',
command: 'ccs codex --target codex "your prompt"', command: 'ccs codex --target codex "your prompt"',
description: supportsManagedRouting description: supportsManagedRouting
? 'Use the explicit built-in Codex provider route.' ? t('codex.runBuiltInCodexExplicit')
: 'Requires a Codex build that exposes --config overrides.', : 'Requires a Codex build that exposes --config overrides.',
}, },
{ {
label: diagnostics.config.activeProfile label: diagnostics.config.activeProfile
? 'Inspect active profile' ? 'Inspect active profile'
: 'Open native Codex', : t('codex.openNativeCodex'),
command: inspectProfileCommand, command: inspectProfileCommand,
description: diagnostics.config.activeProfile description: diagnostics.config.activeProfile
? 'Inspect the active named profile directly in native Codex.' ? 'Inspect the active named profile directly in native Codex.'
@@ -316,12 +354,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<Route className="h-4 w-4" /> <Route className="h-4 w-4" />
{/* TODO i18n: missing key codex.runtimeVsProvider */}
Runtime vs provider Runtime vs provider
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="grid gap-3 md:grid-cols-2"> <CardContent className="grid gap-3 md:grid-cols-2">
<div className="flex flex-col rounded-md border p-3 text-sm"> <div className="flex flex-col rounded-md border p-3 text-sm">
<p className="font-medium text-foreground">Native Codex runtime</p> <p className="font-medium text-foreground">{t('codex.nativeCodexRuntime')}</p>
<ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1"> <ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1">
<li> <li>
<code>ccs-codex</code> <code>ccs-codex</code>
@@ -334,11 +373,12 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
</li> </li>
</ul> </ul>
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal"> <Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
{/* TODO i18n: missing key codex.honorsSavedNativeConfig */}
Honors saved native user config Honors saved native user config
</Badge> </Badge>
</div> </div>
<div className="flex flex-col rounded-md border p-3 text-sm"> <div className="flex flex-col rounded-md border p-3 text-sm">
<p className="font-medium text-foreground">CCS Codex provider / bridge</p> <p className="font-medium text-foreground">{t('codex.ccsCodexProvider')}</p>
{supportsManagedRouting ? ( {supportsManagedRouting ? (
<> <>
<ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1"> <ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1">
@@ -350,11 +390,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
</li> </li>
</ul> </ul>
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal"> <Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
{/* TODO i18n: missing key codex.usesTransientOverrides */}
Uses transient overrides Uses transient overrides
</Badge> </Badge>
</> </>
) : ( ) : (
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
{/* TODO i18n: missing key codex.unavailableNoConfig */}
Unavailable (Codex build lacks <code>--config</code> support). Unavailable (Codex build lacks <code>--config</code> support).
</p> </p>
)} )}
@@ -364,15 +406,15 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Supported flows</CardTitle> <CardTitle className="text-base">{t('codex.supportedFlows')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>Flow</TableHead> <TableHead>{t('codex.flow')}</TableHead>
<TableHead>Status</TableHead> <TableHead>{t('codex.status')}</TableHead>
<TableHead>Notes</TableHead> <TableHead>{t('codex.notes')}</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -381,6 +423,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<TableCell className="font-mono text-xs">{entry.label}</TableCell> <TableCell className="font-mono text-xs">{entry.label}</TableCell>
<TableCell> <TableCell>
<Badge variant={entry.supported ? 'default' : 'secondary'}> <Badge variant={entry.supported ? 'default' : 'secondary'}>
{/* TODO i18n: missing keys common.yes / common.no */}
{entry.supported ? 'Yes' : 'No'} {entry.supported ? 'Yes' : 'No'}
</Badge> </Badge>
</TableCell> </TableCell>
@@ -397,6 +440,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base"> <CardTitle className="flex items-center gap-2 text-base">
<AlertTriangle className="h-4 w-4 text-amber-600" /> <AlertTriangle className="h-4 w-4 text-amber-600" />
{/* TODO i18n: missing key codex.warnings */}
Warnings Warnings
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { Layers3, Loader2, Trash2 } from 'lucide-react'; import { Layers3, Loader2, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
@@ -62,6 +63,7 @@ function ProfileEditor({
onDelete, onDelete,
onSetActive, onSetActive,
}: ProfileEditorProps) { }: ProfileEditorProps) {
const { t } = useTranslation();
const [nameDraft, setNameDraft] = useState(initialName); const [nameDraft, setNameDraft] = useState(initialName);
const [modelDraft, setModelDraft] = useState<string | null>(initialModel); const [modelDraft, setModelDraft] = useState<string | null>(initialModel);
const [providerDraft, setProviderDraft] = useState<string | null>(initialProvider); const [providerDraft, setProviderDraft] = useState<string | null>(initialProvider);
@@ -88,10 +90,10 @@ function ProfileEditor({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use global provider" /> <SelectValue placeholder={t('codex.useGlobalProvider')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__unset__">Use global provider</SelectItem> <SelectItem value="__unset__">{t('codex.useGlobalProvider')}</SelectItem>
{providerNames.map((name) => ( {providerNames.map((name) => (
<SelectItem key={name} value={name}> <SelectItem key={name} value={name}>
{name} {name}
@@ -105,10 +107,10 @@ function ProfileEditor({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use global effort" /> <SelectValue placeholder={t('codex.useGlobalEffort')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__unset__">Use global effort</SelectItem> <SelectItem value="__unset__">{t('codex.useGlobalEffort')}</SelectItem>
{['minimal', 'low', 'medium', 'high', 'xhigh'].map((value) => ( {['minimal', 'low', 'medium', 'high', 'xhigh'].map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -126,6 +128,7 @@ function ProfileEditor({
disabled={disabled || saving || !selectedEntryName} disabled={disabled || saving || !selectedEntryName}
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
{/* TODO i18n: missing key common.delete */}
Delete Delete
</Button> </Button>
<Button <Button
@@ -135,6 +138,7 @@ function ProfileEditor({
disabled || saving || !selectedEntryName || selectedEntryName === activeProfile disabled || saving || !selectedEntryName || selectedEntryName === activeProfile
} }
> >
{/* TODO i18n: missing key codex.setActive */}
Set active Set active
</Button> </Button>
</div> </div>
@@ -155,6 +159,7 @@ function ProfileEditor({
disabled={disabled || saving || nameDraft.trim().length === 0} disabled={disabled || saving || nameDraft.trim().length === 0}
> >
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveProfile */}
Save profile Save profile
</Button> </Button>
<Button <Button
@@ -172,6 +177,7 @@ function ProfileEditor({
disabled={disabled || saving || nameDraft.trim().length === 0} disabled={disabled || saving || nameDraft.trim().length === 0}
> >
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveAndActivate */}
Save + activate Save + activate
</Button> </Button>
</div> </div>
@@ -191,6 +197,8 @@ export function CodexProfilesCard({
onDelete, onDelete,
onSetActive, onSetActive,
}: CodexProfilesCardProps) { }: CodexProfilesCardProps) {
const { t } = useTranslation();
const [selectedName, setSelectedName] = useState('new'); const [selectedName, setSelectedName] = useState('new');
const selectedEntry = useMemo( const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null, () => entries.find((entry) => entry.name === selectedName) ?? null,
@@ -200,21 +208,24 @@ export function CodexProfilesCard({
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
title="Profiles" title={t('codex.profiles')}
badge="profiles" badge="profiles"
icon={<Layers3 className="h-4 w-4" />} icon={<Layers3 className="h-4 w-4" />}
// TODO i18n: missing key codex.profilesDesc
description="Create reusable Codex overlays and set the active default profile." description="Create reusable Codex overlays and set the active default profile."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}> <Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger> <SelectTrigger>
{/* TODO i18n: missing key codex.selectProfile */}
<SelectValue placeholder="Select profile" /> <SelectValue placeholder="Select profile" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="new">Create new profile</SelectItem> <SelectItem value="new">{t('codex.createNewProfile')}</SelectItem>
{entries.map((entry) => ( {entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}> <SelectItem key={entry.name} value={entry.name}>
{entry.name} {entry.name}
{/* TODO i18n: missing key codex.activeSuffix */}
{entry.name === activeProfile ? ' (active)' : ''} {entry.name === activeProfile ? ' (active)' : ''}
</SelectItem> </SelectItem>
))} ))}
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { FolderCheck, Loader2, Trash2 } from 'lucide-react'; import { FolderCheck, Loader2, Trash2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
@@ -34,6 +35,7 @@ function ProjectTrustComposer({
saving, saving,
onSave, onSave,
}: ProjectTrustComposerProps) { }: ProjectTrustComposerProps) {
const { t } = useTranslation();
const [pathDraft, setPathDraft] = useState(workspacePath); const [pathDraft, setPathDraft] = useState(workspacePath);
const [trustLevel, setTrustLevel] = useState('trusted'); const [trustLevel, setTrustLevel] = useState('trusted');
@@ -50,12 +52,13 @@ function ProjectTrustComposer({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="trusted">trusted</SelectItem> <SelectItem value="trusted">{t('codex.trusted')}</SelectItem>
<SelectItem value="untrusted">untrusted</SelectItem> <SelectItem value="untrusted">{t('codex.untrusted')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={() => onSave(pathDraft, trustLevel)} disabled={disabled || saving}> <Button onClick={() => onSave(pathDraft, trustLevel)} disabled={disabled || saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveTrust */}
Save trust Save trust
</Button> </Button>
</div> </div>
@@ -70,15 +73,20 @@ export function CodexProjectTrustCard({
saving = false, saving = false,
onSave, onSave,
}: CodexProjectTrustCardProps) { }: CodexProjectTrustCardProps) {
const { t } = useTranslation();
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
// TODO i18n: missing key codex.projectTrust
title="Project trust" title="Project trust"
badge="projects" badge="projects"
icon={<FolderCheck className="h-4 w-4" />} icon={<FolderCheck className="h-4 w-4" />}
// TODO i18n: missing key codex.projectTrustDesc
description="Trust current workspaces or remove stale trust entries without opening raw TOML." description="Trust current workspaces or remove stale trust entries without opening raw TOML."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.trustPathsHint */}
Paths must be absolute or start with <code>~/</code>. Relative paths are rejected so CCS Paths must be absolute or start with <code>~/</code>. Relative paths are rejected so CCS
does not trust the wrong folder. does not trust the wrong folder.
</p> </p>
@@ -96,12 +104,13 @@ export function CodexProjectTrustCard({
onClick={() => onSave(workspacePath, 'trusted')} onClick={() => onSave(workspacePath, 'trusted')}
disabled={disabled || saving} disabled={disabled || saving}
> >
{/* TODO i18n: missing key codex.trustCurrentWorkspace */}
Trust current workspace Trust current workspace
</Button> </Button>
<div className="space-y-2"> <div className="space-y-2">
{entries.length === 0 ? ( {entries.length === 0 ? (
<p className="text-xs text-muted-foreground">No explicit project trust entries saved.</p> <p className="text-xs text-muted-foreground">{t('codex.noProjectTrustEntries')}</p>
) : ( ) : (
entries.map((entry) => ( entries.map((entry) => (
<div <div
@@ -121,6 +130,7 @@ export function CodexProjectTrustCard({
} }
disabled={disabled || saving} disabled={disabled || saving}
> >
{/* TODO i18n: missing key codex.toggle */}
Toggle Toggle
</Button> </Button>
<Button <Button
@@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { CircleAlert, Loader2, SlidersHorizontal } from 'lucide-react'; import { CircleAlert, Loader2, SlidersHorizontal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -93,6 +94,8 @@ function TopLevelControlsForm({
saving, saving,
onSave, onSave,
}: TopLevelControlsFormProps) { }: TopLevelControlsFormProps) {
const { t } = useTranslation();
const [draft, setDraft] = useState<CodexTopLevelSettingsView>(initialValues); const [draft, setDraft] = useState<CodexTopLevelSettingsView>(initialValues);
const reasoningOptions = withCurrentValue( const reasoningOptions = withCurrentValue(
['minimal', 'low', 'medium', 'high', 'xhigh'], ['minimal', 'low', 'medium', 'high', 'xhigh'],
@@ -121,7 +124,7 @@ function TopLevelControlsForm({
<> <>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Model</p> <p className="text-xs font-medium">{t('codex.model')}</p>
<Input <Input
value={draft.model ?? ''} value={draft.model ?? ''}
onChange={(event) => onChange={(event) =>
@@ -133,7 +136,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Reasoning effort</p> <p className="text-xs font-medium">{t('codex.reasoningEffort')}</p>
<Select <Select
value={toSelectValue(draft.modelReasoningEffort)} value={toSelectValue(draft.modelReasoningEffort)}
onValueChange={(next) => onValueChange={(next) =>
@@ -145,10 +148,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem> <SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
{reasoningOptions.map((value) => ( {reasoningOptions.map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -159,7 +162,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Default provider</p> <p className="text-xs font-medium">{t('codex.defaultProvider')}</p>
<Select <Select
value={toSelectValue(draft.modelProvider)} value={toSelectValue(draft.modelProvider)}
onValueChange={(next) => onValueChange={(next) =>
@@ -168,10 +171,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use Codex default" /> <SelectValue placeholder={t('codex.useCodexDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use Codex default</SelectItem> <SelectItem value={UNSET}>{t('codex.useCodexDefault')}</SelectItem>
{providerOptions.map((name) => ( {providerOptions.map((name) => (
<SelectItem key={name} value={name}> <SelectItem key={name} value={name}>
{name} {name}
@@ -182,7 +185,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Approval policy</p> <p className="text-xs font-medium">{t('codex.approvalPolicy')}</p>
<Select <Select
value={toSelectValue(draft.approvalPolicy)} value={toSelectValue(draft.approvalPolicy)}
onValueChange={(next) => onValueChange={(next) =>
@@ -191,10 +194,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem> <SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
{approvalOptions.map((value) => ( {approvalOptions.map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -205,7 +208,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Sandbox mode</p> <p className="text-xs font-medium">{t('codex.sandboxMode')}</p>
<Select <Select
value={toSelectValue(draft.sandboxMode)} value={toSelectValue(draft.sandboxMode)}
onValueChange={(next) => onValueChange={(next) =>
@@ -214,10 +217,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem> <SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
{sandboxOptions.map((value) => ( {sandboxOptions.map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -228,7 +231,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Web search</p> <p className="text-xs font-medium">{t('codex.webSearch')}</p>
<Select <Select
value={toSelectValue(draft.webSearch)} value={toSelectValue(draft.webSearch)}
onValueChange={(next) => onValueChange={(next) =>
@@ -237,10 +240,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem> <SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
{webSearchOptions.map((value) => ( {webSearchOptions.map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -251,7 +254,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Tool output token limit</p> <p className="text-xs font-medium">{t('codex.toolOutputTokenLimit')}</p>
<Input <Input
type="number" type="number"
min={1} min={1}
@@ -268,7 +271,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Personality</p> <p className="text-xs font-medium">{t('codex.personality')}</p>
<Select <Select
value={toSelectValue(draft.personality)} value={toSelectValue(draft.personality)}
onValueChange={(next) => onValueChange={(next) =>
@@ -277,10 +280,10 @@ function TopLevelControlsForm({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem> <SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
{personalityOptions.map((value) => ( {personalityOptions.map((value) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{value} {value}
@@ -296,21 +299,24 @@ function TopLevelControlsForm({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<CircleAlert className="h-4 w-4 text-amber-600 dark:text-amber-300" /> <CircleAlert className="h-4 w-4 text-amber-600 dark:text-amber-300" />
<p className="text-sm font-semibold">Long context override</p> <p className="text-sm font-semibold">{t('codex.longContextOverride')}</p>
<Badge <Badge
variant="outline" variant="outline"
className="border-amber-500/40 bg-background/80 text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300" className="border-amber-500/40 bg-background/80 text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300"
> >
{/* TODO i18n: missing key codex.manualOptInOnly */}
Manual opt-in only Manual opt-in only
</Badge> </Badge>
<Badge <Badge
variant="secondary" variant="secondary"
className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground" className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground"
> >
{/* TODO i18n: missing keys codex.gpt54Selected / codex.gpt54Reference */}
{isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'} {isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'}
</Badge> </Badge>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.draftValuesOnly */}
Draft values only. Nothing applies until Save. Draft values only. Nothing applies until Save.
</p> </p>
</div> </div>
@@ -329,6 +335,7 @@ function TopLevelControlsForm({
})) }))
} }
> >
{/* TODO i18n: missing key codex.fillCautiousPair */}
Fill cautious pair Fill cautious pair
</Button> </Button>
<Button <Button
@@ -343,6 +350,7 @@ function TopLevelControlsForm({
})) }))
} }
> >
{/* TODO i18n: missing key codex.setOfficialMaxWindow */}
Set official max window Set official max window
</Button> </Button>
<Button <Button
@@ -358,6 +366,7 @@ function TopLevelControlsForm({
})) }))
} }
> >
{/* TODO i18n: missing key codex.clear */}
Clear Clear
</Button> </Button>
</div> </div>
@@ -366,26 +375,31 @@ function TopLevelControlsForm({
<div className="grid gap-2 sm:grid-cols-3"> <div className="grid gap-2 sm:grid-cols-3">
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5"> <div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground"> <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.officialMax */}
Official max Official max
</p> </p>
<p className="mt-1 font-mono text-base font-semibold text-foreground">1.05M / 1M</p> <p className="mt-1 font-mono text-base font-semibold text-foreground">1.05M / 1M</p>
<p className="mt-1 text-[11px] text-muted-foreground">GPT-5.4 context cap</p> <p className="mt-1 text-[11px] text-muted-foreground">{t('codex.gptContextCap')}</p>
</div> </div>
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5"> <div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground"> <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.standardWindow */}
Standard window Standard window
</p> </p>
<p className="mt-1 font-mono text-base font-semibold text-foreground"> <p className="mt-1 font-mono text-base font-semibold text-foreground">
{formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)} {formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)}
</p> </p>
<p className="mt-1 text-[11px] text-muted-foreground">Normal usage window</p> <p className="mt-1 text-[11px] text-muted-foreground">{t('codex.normalUsageWindow')}</p>
</div> </div>
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5"> <div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground"> <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.above272k */}
Above 272K Above 272K
</p> </p>
<p className="mt-1 font-mono text-base font-semibold text-foreground">Counts 2x</p> <p className="mt-1 font-mono text-base font-semibold text-foreground">
<p className="mt-1 text-[11px] text-muted-foreground">Usage-limit cost above 272K</p> {t('codex.counts2x')}
</p>
<p className="mt-1 text-[11px] text-muted-foreground">{t('codex.usageLimitCost')}</p>
</div> </div>
</div> </div>
@@ -393,12 +407,15 @@ function TopLevelControlsForm({
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground"> <p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{/* TODO i18n: missing key codex.oneCautiousPair */}
One cautious pair One cautious pair
</p> </p>
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium"> <div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
{/* TODO i18n: missing key codex.context */}
Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)} Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)}
</div> </div>
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium"> <div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
{/* TODO i18n: missing key codex.autoCompact */}
Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)} Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)}
</div> </div>
</div> </div>
@@ -407,20 +424,23 @@ function TopLevelControlsForm({
variant="outline" variant="outline"
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground" className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
> >
{/* TODO i18n: missing key codex.notOfficial */}
Not official Not official
</Badge> </Badge>
<Badge <Badge
variant="outline" variant="outline"
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground" className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
> >
{/* TODO i18n: missing key codex.draftOnly */}
Draft only Draft only
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
<span>Quick-fill only. Review before saving.</span> <span>{t('codex.quickFillWarning')}</span>
{!isGpt54Selected && draft.model ? ( {!isGpt54Selected && draft.model ? (
<span> <span>
{/* TODO i18n: missing key codex.shouldBeCheckedSeparately */}
<code>{draft.model}</code> should be checked separately. <code>{draft.model}</code> should be checked separately.
</span> </span>
) : null} ) : null}
@@ -429,9 +449,9 @@ function TopLevelControlsForm({
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Model context window</p> <p className="text-xs font-medium">{t('codex.modelContextWindow')}</p>
<Input <Input
aria-label="Model context window" aria-label={t('codex.modelContextWindow')}
type="number" type="number"
min={1} min={1}
value={draft.modelContextWindow ?? ''} value={draft.modelContextWindow ?? ''}
@@ -441,18 +461,20 @@ function TopLevelControlsForm({
modelContextWindow: parseOptionalInteger(event.target.value), modelContextWindow: parseOptionalInteger(event.target.value),
})) }))
} }
// TODO i18n: missing key codex.unset
placeholder="Unset" placeholder="Unset"
disabled={disabled} disabled={disabled}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.writesModelContextWindow */}
Writes <code>model_context_window</code>. Leave unset to keep Codex defaults. Writes <code>model_context_window</code>. Leave unset to keep Codex defaults.
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs font-medium">Auto-compact token limit</p> <p className="text-xs font-medium">{t('codex.autoCompactTokenLimit')}</p>
<Input <Input
aria-label="Auto-compact token limit" aria-label={t('codex.autoCompactTokenLimit')}
type="number" type="number"
min={1} min={1}
value={draft.modelAutoCompactTokenLimit ?? ''} value={draft.modelAutoCompactTokenLimit ?? ''}
@@ -462,10 +484,12 @@ function TopLevelControlsForm({
modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value), modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value),
})) }))
} }
// TODO i18n: missing key codex.unset
placeholder="Unset" placeholder="Unset"
disabled={disabled} disabled={disabled}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key codex.writesAutoCompactTokenLimit */}
Writes <code>model_auto_compact_token_limit</code>. Leave unset to keep model Writes <code>model_auto_compact_token_limit</code>. Leave unset to keep model
defaults. defaults.
</p> </p>
@@ -473,6 +497,7 @@ function TopLevelControlsForm({
</div> </div>
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
{/* TODO i18n: missing key codex.docs */}
<span className="text-[10px] uppercase tracking-[0.14em]">Docs</span> <span className="text-[10px] uppercase tracking-[0.14em]">Docs</span>
<a <a
href="https://developers.openai.com/api/docs/models/gpt-5.4" href="https://developers.openai.com/api/docs/models/gpt-5.4"
@@ -480,6 +505,7 @@ function TopLevelControlsForm({
rel="noreferrer" rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground" className="underline underline-offset-2 hover:text-foreground"
> >
{/* TODO i18n: missing key codex.gpt54ModelPage */}
GPT-5.4 model page GPT-5.4 model page
</a> </a>
<a <a
@@ -488,6 +514,7 @@ function TopLevelControlsForm({
rel="noreferrer" rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground" className="underline underline-offset-2 hover:text-foreground"
> >
{/* TODO i18n: missing key codex.releaseNotes */}
Release notes Release notes
</a> </a>
<a <a
@@ -496,6 +523,7 @@ function TopLevelControlsForm({
rel="noreferrer" rel="noreferrer"
className="underline underline-offset-2 hover:text-foreground" className="underline underline-offset-2 hover:text-foreground"
> >
{/* TODO i18n: missing key codex.configReference */}
Config reference Config reference
</a> </a>
</div> </div>
@@ -504,6 +532,7 @@ function TopLevelControlsForm({
<div className="flex justify-end"> <div className="flex justify-end">
<Button onClick={() => onSave(patch)} disabled={disabled || saving || !hasChanges}> <Button onClick={() => onSave(patch)} disabled={disabled || saving || !hasChanges}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null} {saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{/* TODO i18n: missing key codex.saveTopLevelSettings */}
Save top-level settings Save top-level settings
</Button> </Button>
</div> </div>
@@ -521,9 +550,11 @@ export function CodexTopLevelControlsCard({
}: CodexTopLevelControlsCardProps) { }: CodexTopLevelControlsCardProps) {
return ( return (
<CodexConfigCardShell <CodexConfigCardShell
// TODO i18n: missing key codex.topLevelControls
title="Top-level controls" title="Top-level controls"
badge="config.toml" badge="config.toml"
icon={<SlidersHorizontal className="h-4 w-4" />} icon={<SlidersHorizontal className="h-4 w-4" />}
// TODO i18n: missing key codex.topLevelControlsDesc
description="Structured controls for the stable top-level Codex settings users touch most often. Unsupported upstream shapes stay untouched and should be edited in raw TOML." description="Structured controls for the stable top-level Codex settings users touch most often. Unsupported upstream shapes stay untouched and should be edited in raw TOML."
disabledReason={disabledReason} disabledReason={disabledReason}
> >
@@ -1,4 +1,5 @@
import { BrainCircuit } from 'lucide-react'; import { BrainCircuit } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -39,12 +40,14 @@ export function DroidByokReasoningControlsCard({
onEffortChange, onEffortChange,
onAnthropicBudgetChange, onAnthropicBudgetChange,
}: DroidByokReasoningControlsCardProps) { }: DroidByokReasoningControlsCardProps) {
const { t } = useTranslation();
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
<BrainCircuit className="h-4 w-4" /> <BrainCircuit className="h-4 w-4" />
BYOK Reasoning / Thinking {t('droidSettings.reasoningControls')}
<Badge variant="outline" className="text-[10px] font-normal"> <Badge variant="outline" className="text-[10px] font-normal">
customModels customModels
</Badge> </Badge>
@@ -55,6 +58,7 @@ export function DroidByokReasoningControlsCard({
{models.length === 0 ? ( {models.length === 0 ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{/* TODO i18n: missing key droidSettings.noByokModels */}
No BYOK custom models found in settings.json (`customModels` or `custom_models`). No BYOK custom models found in settings.json (`customModels` or `custom_models`).
</p> </p>
) : ( ) : (
@@ -75,7 +79,9 @@ export function DroidByokReasoningControlsCard({
<div className="grid gap-2 sm:grid-cols-2"> <div className="grid gap-2 sm:grid-cols-2">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[11px] font-medium">Reasoning Effort</p> <p className="text-[11px] font-medium">
{t('codex.reasoningEffortCapitalized')}
</p>
<Select <Select
value={model.effort ?? UNSET_VALUE} value={model.effort ?? UNSET_VALUE}
onValueChange={(next) => onValueChange={(next) =>
@@ -84,9 +90,11 @@ export function DroidByokReasoningControlsCard({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-8 text-xs">
{/* TODO i18n: missing key droidSettings.useProviderDefault */}
<SelectValue placeholder="Use provider default" /> <SelectValue placeholder="Use provider default" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{/* TODO i18n: missing key droidSettings.useProviderDefault */}
<SelectItem value={UNSET_VALUE}>Use provider default</SelectItem> <SelectItem value={UNSET_VALUE}>Use provider default</SelectItem>
{DROID_REASONING_EFFORT_OPTIONS.map((option) => ( {DROID_REASONING_EFFORT_OPTIONS.map((option) => (
<SelectItem key={option} value={option}> <SelectItem key={option} value={option}>
@@ -99,7 +107,7 @@ export function DroidByokReasoningControlsCard({
{model.providerKind === 'anthropic' && ( {model.providerKind === 'anthropic' && (
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[11px] font-medium">Thinking Budget Tokens</p> <p className="text-[11px] font-medium">{t('codex.thinkingBudgetTokens')}</p>
<Input <Input
type="number" type="number"
min={1024} min={1024}
@@ -1,4 +1,5 @@
import { SlidersHorizontal } from 'lucide-react'; import { SlidersHorizontal } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -123,12 +124,14 @@ export function DroidSettingsQuickControlsCard({
onBooleanSettingChange, onBooleanSettingChange,
onNumberSettingChange, onNumberSettingChange,
}: DroidSettingsQuickControlsCardProps) { }: DroidSettingsQuickControlsCardProps) {
const { t } = useTranslation();
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
<SlidersHorizontal className="h-4 w-4" /> <SlidersHorizontal className="h-4 w-4" />
Quick Settings {t('droidSettings.quickControls')}
<Badge variant="outline" className="text-[10px] font-normal"> <Badge variant="outline" className="text-[10px] font-normal">
settings.json settings.json
</Badge> </Badge>
@@ -140,6 +143,7 @@ export function DroidSettingsQuickControlsCard({
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{enumFieldConfig.map((field) => ( {enumFieldConfig.map((field) => (
<div key={field.key} className="space-y-1"> <div key={field.key} className="space-y-1">
{/* TODO i18n: missing keys for droidSettings enum/boolean/number field labels */}
<p className="text-xs font-medium">{field.label}</p> <p className="text-xs font-medium">{field.label}</p>
<Select <Select
value={values[field.key] ?? UNSET_VALUE} value={values[field.key] ?? UNSET_VALUE}
@@ -149,10 +153,10 @@ export function DroidSettingsQuickControlsCard({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET_VALUE}>Use default</SelectItem> <SelectItem value={UNSET_VALUE}>{t('codex.useDefault')}</SelectItem>
{field.options.map((option) => ( {field.options.map((option) => (
<SelectItem key={option.value} value={option.value}> <SelectItem key={option.value} value={option.value}>
{option.label} {option.label}
@@ -166,6 +170,7 @@ export function DroidSettingsQuickControlsCard({
{numberFieldConfig.map((field) => ( {numberFieldConfig.map((field) => (
<div key={field.key} className="space-y-1"> <div key={field.key} className="space-y-1">
{/* TODO i18n: missing keys for droidSettings number field labels */}
<p className="text-xs font-medium">{field.label}</p> <p className="text-xs font-medium">{field.label}</p>
<Input <Input
type="number" type="number"
@@ -190,6 +195,7 @@ export function DroidSettingsQuickControlsCard({
{booleanFieldConfig.map((field) => ( {booleanFieldConfig.map((field) => (
<div key={field.key} className="space-y-1"> <div key={field.key} className="space-y-1">
{/* TODO i18n: missing keys for droidSettings boolean field labels */}
<p className="text-xs font-medium">{field.label}</p> <p className="text-xs font-medium">{field.label}</p>
<Select <Select
value={toBooleanSelectValue(values[field.key])} value={toBooleanSelectValue(values[field.key])}
@@ -197,10 +203,10 @@ export function DroidSettingsQuickControlsCard({
disabled={disabled} disabled={disabled}
> >
<SelectTrigger className="h-8 text-xs"> <SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="Use default" /> <SelectValue placeholder={t('codex.useDefault')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value={UNSET_VALUE}>Use default</SelectItem> <SelectItem value={UNSET_VALUE}>{t('codex.useDefault')}</SelectItem>
<SelectItem value="true">true</SelectItem> <SelectItem value="true">true</SelectItem>
<SelectItem value="false">false</SelectItem> <SelectItem value="false">false</SelectItem>
</SelectContent> </SelectContent>
@@ -67,6 +67,7 @@ export function RawConfigEditorPanel({
{title} {title}
{dirty && ( {dirty && (
<Badge variant="secondary" className="text-[10px]"> <Badge variant="secondary" className="text-[10px]">
{/* TODO i18n: missing key rawJsonSettingsEditor.unsaved */}
Unsaved Unsaved
</Badge> </Badge>
)} )}
@@ -80,15 +81,18 @@ export function RawConfigEditorPanel({
) : ( ) : (
<Save className="h-4 w-4 mr-1" /> <Save className="h-4 w-4 mr-1" />
)} )}
{/* TODO i18n: missing key rawJsonSettingsEditor.save */}
Save Save
</Button> </Button>
{onDiscard ? ( {onDiscard ? (
<Button variant="outline" size="sm" onClick={onDiscard} disabled={!dirty || loading}> <Button variant="outline" size="sm" onClick={onDiscard} disabled={!dirty || loading}>
{/* TODO i18n: missing key rawJsonSettingsEditor.discard */}
Discard Discard
</Button> </Button>
) : null} ) : null}
<Button variant="outline" size="sm" onClick={handleCopy} disabled={!value}> <Button variant="outline" size="sm" onClick={handleCopy} disabled={!value}>
<Copy className="h-4 w-4 mr-1" /> <Copy className="h-4 w-4 mr-1" />
{/* TODO i18n: missing keys rawJsonSettingsEditor.copied / rawJsonSettingsEditor.copy */}
{copied ? 'Copied' : 'Copy'} {copied ? 'Copied' : 'Copy'}
</Button> </Button>
<Button variant="outline" size="sm" onClick={onRefresh} aria-label="Refresh raw config"> <Button variant="outline" size="sm" onClick={onRefresh} aria-label="Refresh raw config">
@@ -113,6 +117,7 @@ export function RawConfigEditorPanel({
)} )}
{readWarning && ( {readWarning && (
<div className="mx-4 mt-4 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive"> <div className="mx-4 mt-4 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive">
{/* TODO i18n: missing key rawJsonSettingsEditor.readOnly */}
Read-only: {readWarning} Read-only: {readWarning}
</div> </div>
)} )}
@@ -6,6 +6,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Loader2, Save, RefreshCw } from 'lucide-react'; import { Loader2, Save, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface RawSettings { interface RawSettings {
path: string; path: string;
@@ -35,12 +36,14 @@ export function HeaderSection({
onRefresh, onRefresh,
onSave, onSave,
}: HeaderSectionProps) { }: HeaderSectionProps) {
const { t } = useTranslation();
return ( return (
<div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0"> <div className="px-6 py-4 border-b bg-background flex items-center justify-between shrink-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">Copilot Configuration</h2> <h2 className="text-lg font-semibold">{t('copilotConfigForm.copilotConfiguration')}</h2>
{rawSettings && ( {rawSettings && (
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
copilot.settings.json copilot.settings.json
@@ -49,7 +52,8 @@ export function HeaderSection({
</div> </div>
{rawSettings && ( {rawSettings && (
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Last modified:{' '} {/* TODO i18n: missing key copilotConfigForm.lastModified */}
Last modified: {/* TODO i18n: missing key copilotConfigForm.neverSaved */}
{rawSettings.exists ? new Date(rawSettings.mtime).toLocaleString() : 'Never saved'} {rawSettings.exists ? new Date(rawSettings.mtime).toLocaleString() : 'Never saved'}
</p> </p>
)} )}
@@ -67,11 +71,13 @@ export function HeaderSection({
{isUpdating || isSavingRawSettings ? ( {isUpdating || isSavingRawSettings ? (
<> <>
<Loader2 className="w-4 h-4 mr-1 animate-spin" /> <Loader2 className="w-4 h-4 mr-1 animate-spin" />
{/* TODO i18n: missing key copilotConfigForm.saving */}
Saving... Saving...
</> </>
) : ( ) : (
<> <>
<Save className="w-4 h-4 mr-1" /> <Save className="w-4 h-4 mr-1" />
{/* TODO i18n: missing key copilotConfigForm.save */}
Save Save
</> </>
)} )}
@@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AlertTriangle, Code2 } from 'lucide-react'; import { AlertTriangle, Code2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ConfirmDialog } from '@/components/shared/confirm-dialog'; import { ConfirmDialog } from '@/components/shared/confirm-dialog';
import { HeaderSection } from './header-section'; import { HeaderSection } from './header-section';
@@ -22,6 +23,7 @@ import { RawEditorSection } from './raw-editor-section';
import { useCopilotConfigForm } from './use-copilot-config-form'; import { useCopilotConfigForm } from './use-copilot-config-form';
export function CopilotConfigForm() { export function CopilotConfigForm() {
const { t } = useTranslation();
const { const {
configLoading, configLoading,
rawSettingsLoading, rawSettingsLoading,
@@ -83,9 +85,10 @@ export function CopilotConfigForm() {
<div className="px-6 pt-4 shrink-0"> <div className="px-6 pt-4 shrink-0">
<Alert variant="warning"> <Alert variant="warning">
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>Deprecated Copilot models detected</AlertTitle> <AlertTitle>{t('copilotConfigForm.deprecatedModels')}</AlertTitle>
<AlertDescription className="space-y-2"> <AlertDescription className="space-y-2">
<p> <p>
{/* TODO i18n: missing key copilotConfigForm.deprecatedModelsDesc */}
Loading this page did not rewrite your files. Save the Copilot configuration to Loading this page did not rewrite your files. Save the Copilot configuration to
persist these replacements. persist these replacements.
</p> </p>
@@ -108,12 +111,15 @@ export function CopilotConfigForm() {
<div className="px-4 pt-4 shrink-0"> <div className="px-4 pt-4 shrink-0">
<TabsList className="w-full"> <TabsList className="w-full">
<TabsTrigger value="config" className="flex-1"> <TabsTrigger value="config" className="flex-1">
{/* TODO i18n: missing key copilotConfigForm.modelConfig */}
Model Config Model Config
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="settings" className="flex-1"> <TabsTrigger value="settings" className="flex-1">
{/* TODO i18n: missing key copilotConfigForm.settings */}
Settings Settings
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="info" className="flex-1"> <TabsTrigger value="info" className="flex-1">
{/* TODO i18n: missing key copilotConfigForm.info */}
Info Info
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -8,6 +8,7 @@ import { CopyButton } from '@/components/ui/copy-button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { TabsContent } from '@/components/ui/tabs'; import { TabsContent } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import { UsageCommand } from './usage-command'; import { UsageCommand } from './usage-command';
interface RawSettings { interface RawSettings {
@@ -22,6 +23,7 @@ interface InfoTabProps {
} }
export function InfoTab({ rawSettings }: InfoTabProps) { export function InfoTab({ rawSettings }: InfoTabProps) {
const { t } = useTranslation();
return ( return (
<TabsContent value="info" className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden"> <TabsContent value="info" className="h-full mt-0 border-0 p-0 data-[state=inactive]:hidden">
<ScrollArea className="h-full"> <ScrollArea className="h-full">
@@ -29,17 +31,22 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
<div> <div>
<h3 className="text-sm font-medium flex items-center gap-2 mb-3"> <h3 className="text-sm font-medium flex items-center gap-2 mb-3">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />
{/* TODO i18n: missing key for 'Configuration Info' */}
Configuration Info Configuration Info
</h3> </h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm"> <div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Provider</span> <span className="font-medium text-muted-foreground">
{t('copilotConfigForm.provider')}
</span>
<span className="font-mono">GitHub Copilot</span> <span className="font-mono">GitHub Copilot</span>
</div> </div>
{rawSettings && ( {rawSettings && (
<> <>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">File Path</span> <span className="font-medium text-muted-foreground">
{t('copilotConfigForm.filePath')}
</span>
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all"> <code className="bg-muted px-1.5 py-0.5 rounded text-xs break-all">
{rawSettings.path} {rawSettings.path}
@@ -48,7 +55,9 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
</div> </div>
</div> </div>
<div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center"> <div className="grid grid-cols-[100px_1fr] gap-2 text-sm items-center">
<span className="font-medium text-muted-foreground">Status</span> <span className="font-medium text-muted-foreground">
{t('copilotConfigForm.status')}
</span>
<Badge <Badge
variant="outline" variant="outline"
className={ className={
@@ -57,6 +66,7 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
: 'w-fit text-muted-foreground' : 'w-fit text-muted-foreground'
} }
> >
{/* TODO i18n: missing key for 'File exists' / 'Using defaults' */}
{rawSettings.exists ? 'File exists' : 'Using defaults'} {rawSettings.exists ? 'File exists' : 'Using defaults'}
</Badge> </Badge>
</div> </div>
@@ -66,8 +76,9 @@ export function InfoTab({ rawSettings }: InfoTabProps) {
</div> </div>
<div> <div>
<h3 className="text-sm font-medium mb-3">Quick Usage</h3> <h3 className="text-sm font-medium mb-3">{t('copilotConfigForm.quickUsage')}</h3>
<div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm"> <div className="space-y-3 bg-card rounded-lg border p-4 shadow-sm">
{/* TODO i18n: missing keys for usage command labels */}
<UsageCommand label="Run with Copilot" command="ccs copilot" /> <UsageCommand label="Run with Copilot" command="ccs copilot" />
<UsageCommand label="Authenticate" command="ccs copilot auth" /> <UsageCommand label="Authenticate" command="ccs copilot auth" />
<UsageCommand label="Start daemon" command="ccs copilot --start" /> <UsageCommand label="Start daemon" command="ccs copilot --start" />
@@ -9,6 +9,7 @@ import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Sparkles, Zap } from 'lucide-react'; import { Sparkles, Zap } from 'lucide-react';
import { TabsContent } from '@/components/ui/tabs'; import { TabsContent } from '@/components/ui/tabs';
import { useTranslation } from 'react-i18next';
import type { CopilotModel } from '@/hooks/use-copilot'; import type { CopilotModel } from '@/hooks/use-copilot';
import { FREE_PRESETS, PAID_PRESETS } from './presets'; import { FREE_PRESETS, PAID_PRESETS } from './presets';
import { FlexibleModelSelector } from './model-selector'; import { FlexibleModelSelector } from './model-selector';
@@ -70,6 +71,7 @@ export function ModelConfigTab({
onUpdateSonnetModel, onUpdateSonnetModel,
onUpdateHaikuModel, onUpdateHaikuModel,
}: ModelConfigTabProps) { }: ModelConfigTabProps) {
const { t } = useTranslation();
const mappedModelLimits = [ const mappedModelLimits = [
{ label: 'Default', id: currentModel }, { label: 'Default', id: currentModel },
{ label: 'Opus', id: opusModel || currentModel }, { label: 'Opus', id: opusModel || currentModel },
@@ -94,10 +96,10 @@ export function ModelConfigTab({
<div> <div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2"> <h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4" />
Presets {t('providerEditor.presets')}
</h3> </h3>
<p className="text-xs text-muted-foreground mb-3"> <p className="text-xs text-muted-foreground mb-3">
Apply pre-configured model mappings {t('copilotConfigForm.modelMapping')}
</p> </p>
{/* Free Tier Presets */} {/* Free Tier Presets */}
@@ -107,9 +109,12 @@ export function ModelConfigTab({
variant="outline" variant="outline"
className="text-[10px] bg-green-100 text-green-700 border-green-200" className="text-[10px] bg-green-100 text-green-700 border-green-200"
> >
{/* TODO i18n: missing key for 'Free Tier' */}
Free Tier Free Tier
</Badge> </Badge>
<span className="text-[10px] text-muted-foreground">No premium usage count</span> <span className="text-[10px] text-muted-foreground">
{t('copilotConfigForm.noPremiumUsage')}
</span>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{FREE_PRESETS.map((preset) => ( {FREE_PRESETS.map((preset) => (
@@ -135,9 +140,11 @@ export function ModelConfigTab({
variant="outline" variant="outline"
className="text-[10px] bg-blue-100 text-blue-700 border-blue-200" className="text-[10px] bg-blue-100 text-blue-700 border-blue-200"
> >
{/* TODO i18n: missing key for 'Pro+ Required' */}
Pro+ Required Pro+ Required
</Badge> </Badge>
<span className="text-[10px] text-muted-foreground"> <span className="text-[10px] text-muted-foreground">
{/* TODO i18n: missing key for 'Uses premium request quota' */}
Uses premium request quota Uses premium request quota
</span> </span>
</div> </div>
@@ -163,13 +170,15 @@ export function ModelConfigTab({
{/* Model Mapping */} {/* Model Mapping */}
<div> <div>
<h3 className="text-sm font-medium mb-2">Model Mapping</h3> <h3 className="text-sm font-medium mb-2">{t('copilotConfigForm.modelMapping')}</h3>
<p className="text-xs text-muted-foreground mb-4"> <p className="text-xs text-muted-foreground mb-4">
{/* TODO i18n: missing key for model mapping description */}
Configure which models to use for each tier Configure which models to use for each tier
</p> </p>
<div className="mb-4 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-900 dark:text-amber-200"> <div className="mb-4 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-xs text-amber-900 dark:text-amber-200">
<p className="font-medium">GitHub Copilot controls prompt/context limits upstream.</p> <p className="font-medium">{t('copilotConfigForm.githubCopilotControls')}</p>
<p className="mt-1"> <p className="mt-1">
{/* TODO i18n: missing key for 'CCS can switch Copilot models...' */}
CCS can switch Copilot models, but it cannot increase the provider&apos;s max prompt CCS can switch Copilot models, but it cannot increase the provider&apos;s max prompt
or context window. or context window.
</p> </p>
@@ -183,6 +192,7 @@ export function ModelConfigTab({
</div> </div>
) : ( ) : (
<p className="mt-2 text-[11px] font-mono"> <p className="mt-2 text-[11px] font-mono">
{/* TODO i18n: missing key for 'Start the daemon to inspect...' */}
Start the daemon to inspect live model limits from GitHub Copilot metadata. Start the daemon to inspect live model limits from GitHub Copilot metadata.
</p> </p>
)} )}
@@ -5,6 +5,7 @@
import type { ModelPreset } from './types'; import type { ModelPreset } from './types';
// TODO i18n: missing keys for preset descriptions ("Free tier - no premium usage", etc.)
// Note: ALL Claude models require paid Copilot subscription // Note: ALL Claude models require paid Copilot subscription
export const FREE_PRESETS: ModelPreset[] = [ export const FREE_PRESETS: ModelPreset[] = [
{ {
@@ -25,6 +26,7 @@ export const FREE_PRESETS: ModelPreset[] = [
}, },
]; ];
// TODO i18n: missing keys for paid preset descriptions
export const PAID_PRESETS: ModelPreset[] = [ export const PAID_PRESETS: ModelPreset[] = [
{ {
name: 'Claude Opus 4.5', name: 'Claude Opus 4.5',
@@ -5,6 +5,7 @@
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { Loader2, X, AlertTriangle } from 'lucide-react'; import { Loader2, X, AlertTriangle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator'; import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
// Lazy load CodeEditor // Lazy load CodeEditor
@@ -29,6 +30,7 @@ export function RawEditorSection({
onChange, onChange,
missingRequiredFields = [], missingRequiredFields = [],
}: RawEditorSectionProps) { }: RawEditorSectionProps) {
const { t } = useTranslation();
const hasMissingFields = missingRequiredFields.length > 0; const hasMissingFields = missingRequiredFields.length > 0;
return ( return (
@@ -36,7 +38,7 @@ export function RawEditorSection({
fallback={ fallback={
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" /> <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span> <span className="ml-2 text-muted-foreground">{t('providerEditor.loadingEditor')}</span>
</div> </div>
} }
> >
@@ -44,6 +46,7 @@ export function RawEditorSection({
{!isRawJsonValid && rawJsonEdits !== null && ( {!isRawJsonValid && rawJsonEdits !== null && (
<div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0"> <div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
{/* TODO i18n: missing key for 'Invalid JSON syntax' */}
Invalid JSON syntax Invalid JSON syntax
</div> </div>
)} )}
@@ -52,12 +55,14 @@ export function RawEditorSection({
<AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" /> <AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" />
<div> <div>
<span className="font-medium text-amber-600 dark:text-amber-400"> <span className="font-medium text-amber-600 dark:text-amber-400">
{/* TODO i18n: missing key for 'Missing required fields:' */}
Missing required fields: Missing required fields:
</span>{' '} </span>{' '}
<code className="text-xs bg-muted px-1 py-0.5 rounded"> <code className="text-xs bg-muted px-1 py-0.5 rounded">
{missingRequiredFields.join(', ')} {missingRequiredFields.join(', ')}
</code> </code>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{/* TODO i18n: missing key for 'These fields will use default values at runtime.' */}
These fields will use default values at runtime. These fields will use default values at runtime.
</p> </p>
</div> </div>
@@ -8,6 +8,7 @@ import { useCopilot } from '@/hooks/use-copilot';
import type { CopilotNormalizationWarning } from '@/hooks/use-copilot'; import type { CopilotNormalizationWarning } from '@/hooks/use-copilot';
import { isApiConflictError } from '@/lib/api-client'; import { isApiConflictError } from '@/lib/api-client';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import type { ModelPreset } from './types'; import type { ModelPreset } from './types';
/** Required env vars for Copilot settings (informational only - runtime fills defaults) */ /** Required env vars for Copilot settings (informational only - runtime fills defaults) */
@@ -31,6 +32,7 @@ function dedupeWarnings(
} }
export function useCopilotConfigForm() { export function useCopilotConfigForm() {
const { t } = useTranslation();
const { const {
config, config,
configLoading, configLoading,
@@ -91,7 +93,7 @@ export function useCopilotConfigForm() {
sonnetModel: preset.sonnet, sonnetModel: preset.sonnet,
haikuModel: preset.haiku, haikuModel: preset.haiku,
})); }));
toast.success(`Applied "${preset.name}" preset`); toast.success(t('toasts.presetApplied', { name: preset.name }));
}; };
// Raw JSON content // Raw JSON content
@@ -191,15 +193,15 @@ export function useCopilotConfigForm() {
} }
if (uniqueWarnings.length > 0) { if (uniqueWarnings.length > 0) {
toast.warning('Copilot configuration saved with model adjustments', { toast.warning(t('toasts.settingsSavedWithAdjustments'), {
description: descriptions.join(' '), description: descriptions.join(' '),
}); });
} else if (descriptions.length > 0) { } else if (descriptions.length > 0) {
toast.success('Copilot configuration saved', { toast.success(t('toasts.settingsSaved'), {
description: descriptions.join(' '), description: descriptions.join(' '),
}); });
} else { } else {
toast.success('Copilot configuration saved'); toast.success(t('toasts.settingsSaved'));
} }
// Clear local state // Clear local state
@@ -209,7 +211,7 @@ export function useCopilotConfigForm() {
if (isApiConflictError(error)) { if (isApiConflictError(error)) {
setConflictDialog(true); setConflictDialog(true);
} else { } else {
toast.error('Failed to save settings'); toast.error(t('toasts.failedSaveSettings'));
} }
} }
}; };
@@ -25,6 +25,7 @@ export function getPlanBadgeStyle(plan?: CopilotPlanTier): string {
/** Get multiplier display */ /** Get multiplier display */
export function getMultiplierDisplay(multiplier?: number): string | null { export function getMultiplierDisplay(multiplier?: number): string | null {
if (multiplier === undefined || multiplier === null) return null; if (multiplier === undefined || multiplier === null) return null;
// TODO i18n: missing key for 'Free' multiplier label
if (multiplier === 0) return 'Free'; if (multiplier === 0) return 'Free';
if (multiplier < 1) return `${multiplier}x`; if (multiplier < 1) return `${multiplier}x`;
if (multiplier === 1) return '1x'; if (multiplier === 1) return '1x';
@@ -9,8 +9,10 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useCopilot } from '@/hooks/use-copilot'; import { useCopilot } from '@/hooks/use-copilot';
import { CheckCircle2, XCircle, AlertTriangle, Loader2, Download } from 'lucide-react'; import { CheckCircle2, XCircle, AlertTriangle, Loader2, Download } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function CopilotStatusCard() { export function CopilotStatusCard() {
const { t } = useTranslation();
const { const {
status, status,
statusLoading, statusLoading,
@@ -28,7 +30,7 @@ export function CopilotStatusCard() {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>GitHub Copilot Status</CardTitle> <CardTitle>{t('copilotPage.status')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex items-center justify-center py-8"> <CardContent className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
@@ -41,10 +43,10 @@ export function CopilotStatusCard() {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>GitHub Copilot Status</CardTitle> <CardTitle>{t('copilotPage.status')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-muted-foreground">Failed to load status</p> <p className="text-muted-foreground">{t('copilotConfigForm.failedLoadStatus')}</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@@ -54,21 +56,21 @@ export function CopilotStatusCard() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
GitHub Copilot Status {t('copilotPage.status')}
{status.enabled ? ( {status.enabled ? (
<Badge variant="default">Enabled</Badge> <Badge variant="default">{t('copilotPage.enabled')}</Badge>
) : ( ) : (
<Badge variant="secondary">Disabled</Badge> <Badge variant="secondary">{t('copilotPage.disabled')}</Badge>
)} )}
</CardTitle> </CardTitle>
<CardDescription>Use your GitHub Copilot subscription with Claude Code</CardDescription> <CardDescription>{t('copilotConfigForm.useWithClaudeCode')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Warning Banner */} {/* Warning Banner */}
<div className="flex items-start gap-2 rounded-md border border-yellow-500/20 bg-yellow-500/10 p-3"> <div className="flex items-start gap-2 rounded-md border border-yellow-500/20 bg-yellow-500/10 p-3">
<AlertTriangle className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" /> <AlertTriangle className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" />
<p className="text-sm text-yellow-700 dark:text-yellow-300"> <p className="text-sm text-yellow-700 dark:text-yellow-300">
This uses a reverse-engineered API. Excessive usage may trigger GitHub abuse detection. {t('copilotPage.unofficialItem2')}
</p> </p>
</div> </div>
@@ -82,7 +84,7 @@ export function CopilotStatusCard() {
<XCircle className="h-5 w-5 text-red-500" /> <XCircle className="h-5 w-5 text-red-500" />
)} )}
<span className="text-sm"> <span className="text-sm">
copilot-api {status.installed ? `v${status.version}` : 'Not Installed'} copilot-api {status.installed ? `v${status.version}` : t('copilotPage.missing')}
</span> </span>
</div> </div>
@@ -94,7 +96,7 @@ export function CopilotStatusCard() {
<XCircle className="h-5 w-5 text-red-500" /> <XCircle className="h-5 w-5 text-red-500" />
)} )}
<span className="text-sm"> <span className="text-sm">
{status.authenticated ? 'Authenticated' : 'Not Authenticated'} {status.authenticated ? t('copilotPage.connected') : t('copilotPage.notConnected')}
</span> </span>
</div> </div>
@@ -105,15 +107,25 @@ export function CopilotStatusCard() {
) : ( ) : (
<XCircle className="h-5 w-5 text-muted-foreground" /> <XCircle className="h-5 w-5 text-muted-foreground" />
)} )}
<span className="text-sm">Daemon {status.daemon_running ? 'Running' : 'Stopped'}</span> <span className="text-sm">
{t('copilotPage.daemon')}{' '}
{status.daemon_running ? t('copilotPage.running') : t('copilotPage.stopped')}
</span>
</div> </div>
</div> </div>
{/* Quick Info */} {/* Quick Info */}
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground"> <div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<span>Port: {status.port}</span> <span>
<span>Model: {status.model}</span> {t('copilotPage.port')}: {status.port}
<span>Auto-start: {status.auto_start ? 'Yes' : 'No'}</span> </span>
<span>
{t('providerEditor.modelMapping')}: {status.model}
</span>
<span>
{/* TODO i18n: missing key for 'Auto-start' */}
Auto-start: {status.auto_start ? t('copilotPage.yes') : t('copilotPage.no')}
</span>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -123,12 +135,12 @@ export function CopilotStatusCard() {
{isInstalling ? ( {isInstalling ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Installing... {t('copilotPage.installing')}
</> </>
) : ( ) : (
<> <>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
Install copilot-api {t('copilotPage.installCopilotApi')}
</> </>
)} )}
</Button> </Button>
@@ -143,10 +155,10 @@ export function CopilotStatusCard() {
{isAuthenticating ? ( {isAuthenticating ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Authenticating... {t('copilotPage.authenticating')}
</> </>
) : ( ) : (
'Authenticate with GitHub' t('copilotPage.authenticate')
)} )}
</Button> </Button>
)} )}
@@ -161,10 +173,10 @@ export function CopilotStatusCard() {
{isStoppingDaemon ? ( {isStoppingDaemon ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Stopping... {t('copilotPage.stopping')}
</> </>
) : ( ) : (
'Stop Daemon' t('copilotPage.stop')
)} )}
</Button> </Button>
) : ( ) : (
@@ -177,10 +189,10 @@ export function CopilotStatusCard() {
{isStartingDaemon ? ( {isStartingDaemon ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
Starting... {t('copilotPage.starting')}
</> </>
) : ( ) : (
'Start Daemon' t('copilotPage.start')
)} )}
</Button> </Button>
)} )}
+3 -1
View File
@@ -2,6 +2,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { CheckCircle, AlertTriangle, XCircle, Wrench } from 'lucide-react'; import { CheckCircle, AlertTriangle, XCircle, Wrench } from 'lucide-react';
import { useFixHealth } from '@/hooks/use-health'; import { useFixHealth } from '@/hooks/use-health';
import { useTranslation } from 'react-i18next';
interface HealthCheck { interface HealthCheck {
id: string; id: string;
@@ -35,6 +36,7 @@ const statusConfig = {
export function HealthCard({ check }: { check: HealthCheck }) { export function HealthCard({ check }: { check: HealthCheck }) {
const fixMutation = useFixHealth(); const fixMutation = useFixHealth();
const { t } = useTranslation();
const config = statusConfig[check.status]; const config = statusConfig[check.status];
const Icon = config.icon; const Icon = config.icon;
@@ -54,7 +56,7 @@ export function HealthCard({ check }: { check: HealthCheck }) {
disabled={fixMutation.isPending} disabled={fixMutation.isPending}
> >
<Wrench className="w-3 h-3 mr-1" /> <Wrench className="w-3 h-3 mr-1" />
Fix {t('health.fix')}
</Button> </Button>
)} )}
</div> </div>
+5 -2
View File
@@ -1,24 +1,27 @@
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { CcsLogo } from '@/components/shared/ccs-logo'; import { CcsLogo } from '@/components/shared/ccs-logo';
import { useTranslation } from 'react-i18next';
interface HeroSectionProps { interface HeroSectionProps {
version?: string; version?: string;
} }
export function HeroSection({ version }: HeroSectionProps) { export function HeroSection({ version }: HeroSectionProps) {
const { t } = useTranslation();
return ( return (
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<CcsLogo size="lg" showText={false} /> <CcsLogo size="lg" showText={false} />
<div> <div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">CCS Config</h1> <h1 className="text-2xl font-bold">{t('heroSection.title')}</h1>
{version && ( {version && (
<Badge variant="outline" className="font-mono text-xs"> <Badge variant="outline" className="font-mono text-xs">
v{version} v{version}
</Badge> </Badge>
)} )}
</div> </div>
<p className="text-muted-foreground text-sm mt-1">Claude Code Switch Dashboard</p> <p className="text-muted-foreground text-sm mt-1">{t('heroSection.subtitle')}</p>
</div> </div>
</div> </div>
); );
+6 -4
View File
@@ -1,26 +1,28 @@
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { FileTextIcon, SettingsIcon, GithubIcon, ExternalLinkIcon } from 'lucide-react'; import { FileTextIcon, SettingsIcon, GithubIcon, ExternalLinkIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export function HubFooter() { export function HubFooter() {
const { t } = useTranslation();
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const footerLinks = [ const footerLinks = [
{ {
icon: <FileTextIcon className="w-4 h-4" />, icon: <FileTextIcon className="w-4 h-4" />,
label: 'Logs', label: t('hubFooter.logs'),
href: '#logs', href: '#logs',
onClick: () => console.log('Navigate to Logs'), onClick: () => console.log('Navigate to Logs'),
}, },
{ {
icon: <SettingsIcon className="w-4 h-4" />, icon: <SettingsIcon className="w-4 h-4" />,
label: 'Settings', label: t('hubFooter.settings'),
href: '#settings', href: '#settings',
onClick: () => console.log('Navigate to Settings'), onClick: () => console.log('Navigate to Settings'),
}, },
{ {
icon: <GithubIcon className="w-4 h-4" />, icon: <GithubIcon className="w-4 h-4" />,
label: 'GitHub', label: t('hubFooter.github'),
href: 'https://github.com/kaitranntt/ccs', href: 'https://github.com/kaitranntt/ccs',
external: true, external: true,
}, },
@@ -32,7 +34,7 @@ export function HubFooter() {
<div className="flex items-center space-x-2 text-sm text-muted-foreground"> <div className="flex items-center space-x-2 text-sm text-muted-foreground">
<span>CCS v0.0.0</span> <span>CCS v0.0.0</span>
<Separator orientation="vertical" className="h-4" /> <Separator orientation="vertical" className="h-4" />
<span>© {currentYear} kaitranntt</span> <span>{t('hubFooter.copyright', { year: currentYear })}</span>
</div> </div>
<div className="ml-auto flex items-center space-x-2"> <div className="ml-auto flex items-center space-x-2">
+3 -1
View File
@@ -1,9 +1,11 @@
import { Moon, Sun } from 'lucide-react'; import { Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useTheme } from '@/hooks/use-theme'; import { useTheme } from '@/hooks/use-theme';
import { useTranslation } from 'react-i18next';
export function ThemeToggle() { export function ThemeToggle() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { t } = useTranslation();
return ( return (
<Button <Button
@@ -13,7 +15,7 @@ export function ThemeToggle() {
> >
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">{t('themeToggle.srLabel')}</span>
</Button> </Button>
); );
} }
@@ -13,6 +13,7 @@ import {
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client'; import type { LogsConfig, UpdateLogsConfigPayload } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
function parseInteger(value: string, fallback: number) { function parseInteger(value: string, fallback: number) {
const parsed = Number.parseInt(value, 10); const parsed = Number.parseInt(value, 10);
@@ -32,6 +33,8 @@ export function LogsConfigCard({
onSave: (payload: UpdateLogsConfigPayload) => void; onSave: (payload: UpdateLogsConfigPayload) => void;
isPending: boolean; isPending: boolean;
}) { }) {
// TODO i18n: uncomment when keys for Commit Changes, Rollback Draft, etc. are added
// const { t } = useTranslation();
const [draft, setDraft] = useState(config); const [draft, setDraft] = useState(config);
useEffect(() => { useEffect(() => {
@@ -247,6 +250,7 @@ export function LogsConfigCard({
className="h-10 w-full gap-2 rounded-xl bg-primary text-[11px] font-semibold uppercase tracking-[0.14em] shadow-lg shadow-primary/20 transition-all hover:scale-[1.02] active:scale-[0.98]" className="h-10 w-full gap-2 rounded-xl bg-primary text-[11px] font-semibold uppercase tracking-[0.14em] shadow-lg shadow-primary/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
> >
<Save className="h-3.5 w-3.5" /> <Save className="h-3.5 w-3.5" />
{/* TODO i18n: missing key for "Commit Changes" */}
Commit Changes Commit Changes
</Button> </Button>
<Button <Button
@@ -256,6 +260,7 @@ export function LogsConfigCard({
className="h-9 gap-2 text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/45 hover:text-foreground" className="h-9 gap-2 text-[10px] font-medium uppercase tracking-[0.12em] text-foreground/45 hover:text-foreground"
> >
<RotateCcw className="h-3 w-3" /> <RotateCcw className="h-3 w-3" />
{/* TODO i18n: missing key for "Rollback Draft" */}
Rollback Draft Rollback Draft
</Button> </Button>
</div> </div>
+6 -1
View File
@@ -15,6 +15,7 @@ import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge'; import { LogLevelBadge } from './log-level-badge';
import { formatJson } from './utils'; import { formatJson } from './utils';
import { useTranslation } from 'react-i18next';
function MetaRow({ function MetaRow({
label, label,
@@ -52,6 +53,8 @@ export function LogsDetailPanel({
entry: LogsEntry | null; entry: LogsEntry | null;
sourceLabel?: string; sourceLabel?: string;
}) { }) {
const { t } = useTranslation();
if (!entry) { if (!entry) {
return ( return (
<div className="flex h-full flex-col items-center justify-center p-8 text-center animate-in fade-in duration-1000"> <div className="flex h-full flex-col items-center justify-center p-8 text-center animate-in fade-in duration-1000">
@@ -63,6 +66,7 @@ export function LogsDetailPanel({
</div> </div>
<div className="max-w-xs space-y-3"> <div className="max-w-xs space-y-3">
<h3 className="text-[15px] font-semibold uppercase tracking-[0.14em] text-foreground/65"> <h3 className="text-[15px] font-semibold uppercase tracking-[0.14em] text-foreground/65">
{/* TODO i18n: missing key for "Inspector Standby" */}
Inspector Standby Inspector Standby
</h3> </h3>
<p className="text-[13px] leading-relaxed text-muted-foreground/55 font-medium"> <p className="text-[13px] leading-relaxed text-muted-foreground/55 font-medium">
@@ -134,13 +138,14 @@ export function LogsDetailPanel({
className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm" className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm"
> >
<Info className="h-3.5 w-3.5" /> <Info className="h-3.5 w-3.5" />
Details {t('logsDetailPanel.details')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="raw" value="raw"
className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm" className="min-w-0 gap-2 rounded-lg px-3 py-2 text-[11px] font-semibold uppercase tracking-[0.1em] transition-all data-[state=active]:bg-background data-[state=active]:text-primary data-[state=active]:shadow-sm"
> >
<FileJson className="h-3.5 w-3.5" /> <FileJson className="h-3.5 w-3.5" />
{/* TODO i18n: missing key for "Raw Context" */}
Raw Context Raw Context
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
+20 -7
View File
@@ -3,6 +3,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import type { LogsEntry } from '@/lib/api-client'; import type { LogsEntry } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { LogLevelBadge } from './log-level-badge'; import { LogLevelBadge } from './log-level-badge';
import { useTranslation } from 'react-i18next';
export function LogsEntryList({ export function LogsEntryList({
entries, entries,
@@ -19,6 +20,8 @@ export function LogsEntryList({
isLoading: boolean; isLoading: boolean;
isFetching: boolean; isFetching: boolean;
}) { }) {
const { t } = useTranslation();
return ( return (
<div className="flex min-h-0 flex-1 flex-col bg-background/50 backdrop-blur-sm"> <div className="flex min-h-0 flex-1 flex-col bg-background/50 backdrop-blur-sm">
<div className="flex shrink-0 items-center justify-between border-b border-border bg-card/40 px-6 py-3 shadow-sm"> <div className="flex shrink-0 items-center justify-between border-b border-border bg-card/40 px-6 py-3 shadow-sm">
@@ -53,13 +56,23 @@ export function LogsEntryList({
</div> </div>
<div className="flex items-center gap-0 border-b border-border bg-muted/30 px-0 py-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-foreground/45"> <div className="flex items-center gap-0 border-b border-border bg-muted/30 px-0 py-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-foreground/45">
<div className="w-[6.5rem] shrink-0 px-6">Time</div> <div className="w-[6.5rem] shrink-0 px-6">{t('logsConfig.time')}</div>
<div className="w-14 shrink-0 border-l border-border/10 px-2 text-center">Lvl</div> <div className="w-14 shrink-0 border-l border-border/10 px-2 text-center">
<div className="w-[15rem] shrink-0 border-l border-border/10 px-4">Source</div> {t('logsConfig.level')}
<div className="flex-1 border-l border-border/10 px-4">Message</div> </div>
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-2 text-center">Proc</div> <div className="w-[15rem] shrink-0 border-l border-border/10 px-4">
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-3 text-center">Run</div> {t('logsConfig.source')}
<div className="w-11 shrink-0 border-l border-border/10 px-2 text-center">Open</div> </div>
<div className="flex-1 border-l border-border/10 px-4">{t('logsConfig.message')}</div>
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-2 text-center">
{t('logsConfig.proc')}
</div>
<div className="w-[5.5rem] shrink-0 border-l border-border/10 px-3 text-center">
{t('logsConfig.run')}
</div>
<div className="w-11 shrink-0 border-l border-border/10 px-2 text-center">
{t('logsConfig.open')}
</div>
</div> </div>
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
+3 -1
View File
@@ -6,6 +6,7 @@ import type { LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs'; import type { LogsLevelFilter, LogsSourceFilter } from '@/hooks/use-logs';
import { getLogLevelOptions } from '@/hooks/use-logs'; import { getLogLevelOptions } from '@/hooks/use-logs';
import { useTranslation } from 'react-i18next';
export function LogsFilters({ export function LogsFilters({
sources, sources,
@@ -32,6 +33,7 @@ export function LogsFilters({
onRefresh: () => void; onRefresh: () => void;
isRefreshing: boolean; isRefreshing: boolean;
}) { }) {
const { t } = useTranslation();
const levels = getLogLevelOptions(); const levels = getLogLevelOptions();
const limits = [50, 100, 150, 250]; const limits = [50, 100, 150, 250];
@@ -188,7 +190,7 @@ export function LogsFilters({
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000" /> <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RefreshCw className={cn('h-3.5 w-3.5', isRefreshing && 'animate-spin')} /> <RefreshCw className={cn('h-3.5 w-3.5', isRefreshing && 'animate-spin')} />
<span>Refresh Entries</span> <span>{t('logsConfig.refreshEntries')}</span>
</div> </div>
</Button> </Button>
</div> </div>
@@ -2,6 +2,7 @@ import { Activity, Archive, Database, RadioTower } from 'lucide-react';
import type { LogsConfig, LogsEntry, LogsSource } from '@/lib/api-client'; import type { LogsConfig, LogsEntry, LogsSource } from '@/lib/api-client';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { formatCount, formatLogTimestamp, formatRelativeLogTime } from './utils'; import { formatCount, formatLogTimestamp, formatRelativeLogTime } from './utils';
// TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready
function MetricCard({ function MetricCard({
label, label,
@@ -43,12 +44,15 @@ export function LogsOverviewCards({
entries: LogsEntry[]; entries: LogsEntry[];
latestTimestamp: string | null; latestTimestamp: string | null;
}) { }) {
// TODO i18n: uncomment when keys for Pipeline/Retention/Coverage/Visible Entries are added
// const { t } = useTranslation();
const nativeSources = sources.filter((source) => source.kind === 'native').length; const nativeSources = sources.filter((source) => source.kind === 'native').length;
const legacySources = sources.length - nativeSources; const legacySources = sources.length - nativeSources;
const errorCount = entries.filter((entry) => entry.level === 'error').length; const errorCount = entries.filter((entry) => entry.level === 'error').length;
return ( return (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{/* TODO i18n: missing keys for Pipeline/Retention/Coverage/Visible Entries labels and detail strings */}
<MetricCard <MetricCard
label="Pipeline" label="Pipeline"
value={config.enabled ? 'Enabled' : 'Disabled'} value={config.enabled ? 'Enabled' : 'Disabled'}
@@ -1,9 +1,12 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { useTranslation } from 'react-i18next';
export function LogsPageSkeleton() { export function LogsPageSkeleton() {
const { t } = useTranslation();
return ( return (
<div className="space-y-6" aria-label="Loading logs workspace"> <div className="space-y-6" aria-label={t('logsPageSkeleton.loadingLogs')}>
<Card className="gap-4"> <Card className="gap-4">
<CardHeader className="space-y-3"> <CardHeader className="space-y-3">
<Skeleton className="h-4 w-28" /> <Skeleton className="h-4 w-28" />
+5
View File
@@ -1,4 +1,9 @@
import type { LogsLevel } from '@/lib/api-client'; import type { LogsLevel } from '@/lib/api-client';
// NOTE: This module contains utility functions that are not directly i18n-aware.
// String literals here ("No activity yet", "Error", etc.) are used as fallbacks
// and defaults in non-component contexts. Components consuming these values
// should wrap them with t() calls when rendering.
// TODO i18n: Consider making formatRelativeLogTime/formatLogTimestamp i18n-aware
export function formatLogTimestamp(timestamp: string | null | undefined) { export function formatLogTimestamp(timestamp: string | null | undefined) {
if (!timestamp) { if (!timestamp) {
@@ -3,6 +3,7 @@
*/ */
import { CheckCircle2, XCircle } from 'lucide-react'; import { CheckCircle2, XCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
interface InlineStatsBadgeProps { interface InlineStatsBadgeProps {
success: number; success: number;
@@ -10,8 +11,13 @@ interface InlineStatsBadgeProps {
} }
export function InlineStatsBadge({ success, failure }: InlineStatsBadgeProps) { export function InlineStatsBadge({ success, failure }: InlineStatsBadgeProps) {
const { t } = useTranslation();
if (success === 0 && failure === 0) { if (success === 0 && failure === 0) {
return <span className="text-[9px] text-muted-foreground/50 font-mono">no activity</span>; return (
<span className="text-[9px] text-muted-foreground/50 font-mono">
{t('authMonitorLive.noActivity')}
</span>
);
} }
return ( return (
@@ -12,6 +12,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import type { ProviderStats } from '../types'; import type { ProviderStats } from '../types';
import { getSuccessRate } from '../utils'; import { getSuccessRate } from '../utils';
import { InlineStatsBadge } from './inline-stats-badge'; import { InlineStatsBadge } from './inline-stats-badge';
import { useTranslation } from 'react-i18next';
interface ProviderCardProps { interface ProviderCardProps {
stats: ProviderStats; stats: ProviderStats;
@@ -30,6 +31,7 @@ export function ProviderCard({
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
}: ProviderCardProps) { }: ProviderCardProps) {
const { t } = useTranslation();
const successRate = getSuccessRate(stats.successCount, stats.failureCount); const successRate = getSuccessRate(stats.successCount, stats.failureCount);
const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280'; const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280';
@@ -59,6 +61,7 @@ export function ProviderCard({
{stats.displayName} {stats.displayName}
</h3> </h3>
<p className="text-[10px] text-muted-foreground"> <p className="text-[10px] text-muted-foreground">
{/* TODO i18n: missing key for account count */}
{stats.accountCount} account{stats.accountCount !== 1 ? 's' : ''} {stats.accountCount} account{stats.accountCount !== 1 ? 's' : ''}
</p> </p>
</div> </div>
@@ -73,11 +76,11 @@ export function ProviderCard({
<div className="space-y-2"> <div className="space-y-2">
{/* Inline success/failure stats - immediately visible */} {/* Inline success/failure stats - immediately visible */}
<div className="flex justify-between items-center text-xs"> <div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">Stats</span> <span className="text-muted-foreground">{t('authMonitorLive.stats')}</span>
<InlineStatsBadge success={stats.successCount} failure={stats.failureCount} /> <InlineStatsBadge success={stats.successCount} failure={stats.failureCount} />
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-muted-foreground">Success Rate</span> <span className="text-muted-foreground">{t('authMonitorLive.successRate')}</span>
<span <span
className="font-mono font-semibold" className="font-mono font-semibold"
style={{ style={{
@@ -125,11 +128,11 @@ export function ProviderCard({
<TooltipTrigger asChild> <TooltipTrigger asChild>
<AlertTriangle <AlertTriangle
className="absolute -top-1 -right-1 w-2.5 h-2.5 text-amber-500" className="absolute -top-1 -right-1 w-2.5 h-2.5 text-amber-500"
aria-label="Missing Project ID" aria-label={t('providerCard.missingProjectIdAria')}
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="text-xs"> <TooltipContent side="top" className="text-xs">
Missing Project ID - re-add account to fix {/* TODO i18n: missing key */}Missing Project ID - re-add account to fix
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -146,17 +146,27 @@ export function AuthMonitor() {
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-gradient-to-r from-emerald-500/5 via-transparent to-transparent dark:from-emerald-500/10"> <div className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-gradient-to-r from-emerald-500/5 via-transparent to-transparent dark:from-emerald-500/10">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LivePulse /> <LivePulse />
<span className="text-xs font-semibold tracking-tight text-foreground">LIVE</span> <span className="text-xs font-semibold tracking-tight text-foreground">
<span className="text-[10px] text-muted-foreground">Account Monitor</span> {t('authMonitorLive.live')}
</span>
<span className="text-[10px] text-muted-foreground">
{t('authMonitorLive.accountMonitor')}
</span>
</div> </div>
<div className="flex items-center gap-4 text-[10px] text-muted-foreground"> <div className="flex items-center gap-4 text-[10px] text-muted-foreground">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Radio className="w-3 h-3 animate-pulse" /> <Radio className="w-3 h-3 animate-pulse" />
<span>Updated {timeSinceUpdate || 'now'}</span> <span>
{timeSinceUpdate
? t('authMonitorLive.updated', { time: timeSinceUpdate })
: t('authMonitorLive.updatedNow')}
</span>
</div> </div>
<span className="text-muted-foreground/50">|</span> <span className="text-muted-foreground/50">|</span>
<span>{t('authMonitor.accountsCount', { count: displayedAccountCount })}</span> <span>{t('authMonitor.accountsCount', { count: displayedAccountCount })}</span>
<span className="font-mono">{displayedTotalRequests.toLocaleString()} req</span> <span className="font-mono">
{displayedTotalRequests.toLocaleString()} {t('authMonitorLive.requestsLabel')}
</span>
</div> </div>
</div> </div>
@@ -210,6 +220,7 @@ export function AuthMonitor() {
/> />
) : ( ) : (
<div className="p-6"> <div className="p-6">
{/* TODO i18n: missing key for "Request Distribution by Provider" */}
<div className="text-[10px] text-muted-foreground uppercase tracking-widest mb-4"> <div className="text-[10px] text-muted-foreground uppercase tracking-widest mb-4">
Request Distribution by Provider Request Distribution by Provider
</div> </div>
@@ -133,8 +133,12 @@ export function LogContentPanel({ name, absolutePath }: LogContentPanelProps) {
<div className="flex-1 overflow-hidden bg-card/30"> <div className="flex-1 overflow-hidden bg-card/30">
{activeTab === 'overview' && <OverviewTab parsed={parsed} />} {activeTab === 'overview' && <OverviewTab parsed={parsed} />}
{activeTab === 'headers' && <HeadersTab headers={parsed.requestHeaders} />} {activeTab === 'headers' && <HeadersTab headers={parsed.requestHeaders} />}
{activeTab === 'request' && <BodyTab content={parsed.requestBody} label="Request" />} {activeTab === 'request' && (
{activeTab === 'response' && <BodyTab content={parsed.responseBody} label="Response" />} <BodyTab content={parsed.requestBody} label={t('errorLogs.tabRequest')} />
)}
{activeTab === 'response' && (
<BodyTab content={parsed.responseBody} label={t('errorLogs.tabResponse')} />
)}
{activeTab === 'raw' && <RawTab content={content} />} {activeTab === 'raw' && <RawTab content={content} />}
</div> </div>
</div> </div>
@@ -144,9 +144,10 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
/** Headers tab content */ /** Headers tab content */
export function HeadersTab({ headers }: { headers: Record<string, string> }) { export function HeadersTab({ headers }: { headers: Record<string, string> }) {
const { t } = useTranslation();
const entries = Object.entries(headers); const entries = Object.entries(headers);
if (entries.length === 0) { if (entries.length === 0) {
return <div className="p-4 text-xs text-muted-foreground">No headers available</div>; return <div className="p-4 text-xs text-muted-foreground">{t('errorLogs.noHeaders')}</div>;
} }
return ( return (
@@ -20,6 +20,7 @@ import { isOpenRouterProfile, extractTierMapping, applyTierMapping } from './uti
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import i18n from '@/lib/i18n'; import i18n from '@/lib/i18n';
import { useTranslation } from 'react-i18next';
import type { Settings, SettingsResponse } from './types'; import type { Settings, SettingsResponse } from './types';
import type { CliTarget } from '@/lib/api-client'; import type { CliTarget } from '@/lib/api-client';
@@ -50,6 +51,7 @@ export function FriendlyUISection({
onAddEnvVar, onAddEnvVar,
onEnvBulkChange, onEnvBulkChange,
}: FriendlyUISectionProps) { }: FriendlyUISectionProps) {
const { t } = useTranslation();
const isOpenRouter = isOpenRouterProfile(currentSettings); const isOpenRouter = isOpenRouterProfile(currentSettings);
const settingsEnv = currentSettings?.env; const settingsEnv = currentSettings?.env;
@@ -124,11 +126,13 @@ export function FriendlyUISection({
<Tabs defaultValue="env" className="h-full w-full min-w-0 flex flex-col"> <Tabs defaultValue="env" className="h-full w-full min-w-0 flex flex-col">
<div className="px-4 pt-4 shrink-0"> <div className="px-4 pt-4 shrink-0">
<TabsList className="w-full"> <TabsList className="w-full">
{/* TODO i18n: missing key for "Configuration" tab label */}
<TabsTrigger value="env" className="flex-1"> <TabsTrigger value="env" className="flex-1">
{isOpenRouter ? 'Configuration' : 'Environment Variables'} {isOpenRouter ? 'Configuration' : t('settingsDialog.envTab')}
</TabsTrigger> </TabsTrigger>
{/* TODO i18n: missing key for "Info & Usage" tab label */}
<TabsTrigger value="info" className="flex-1"> <TabsTrigger value="info" className="flex-1">
Info & Usage Info &amp; Usage
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -145,6 +149,7 @@ export function FriendlyUISection({
<div className="h-full overflow-y-auto overflow-x-hidden p-4 space-y-6"> <div className="h-full overflow-y-auto overflow-x-hidden p-4 space-y-6">
{/* Model Selection - Primary Focus */} {/* Model Selection - Primary Focus */}
<div className="space-y-3"> <div className="space-y-3">
{/* TODO i18n: missing key for "Model Selection" label */}
<Label className="text-sm font-medium">Model Selection</Label> <Label className="text-sm font-medium">Model Selection</Label>
<OpenRouterModelPicker <OpenRouterModelPicker
value={currentEnv.ANTHROPIC_MODEL} value={currentEnv.ANTHROPIC_MODEL}
@@ -162,7 +167,7 @@ export function FriendlyUISection({
{/* API Key */} {/* API Key */}
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">API Key</Label> <Label className="text-sm font-medium">{t('profileDialog.apiKey')}</Label>
<MaskedInput <MaskedInput
value={currentEnv.ANTHROPIC_AUTH_TOKEN || ''} value={currentEnv.ANTHROPIC_AUTH_TOKEN || ''}
onChange={(e) => onEnvValueChange('ANTHROPIC_AUTH_TOKEN', e.target.value)} onChange={(e) => onEnvValueChange('ANTHROPIC_AUTH_TOKEN', e.target.value)}
@@ -193,6 +198,7 @@ export function FriendlyUISection({
)} )}
/> />
<Settings2 className="h-4 w-4" /> <Settings2 className="h-4 w-4" />
{/* TODO i18n: missing key for "Additional Variables" label */}
<span>Additional Variables</span> <span>Additional Variables</span>
<span className="text-xs font-normal opacity-70"> <span className="text-xs font-normal opacity-70">
({unmanagedEnvVars.length}) ({unmanagedEnvVars.length})
@@ -220,18 +226,18 @@ export function FriendlyUISection({
{/* Fixed Add Variable Input at Bottom */} {/* Fixed Add Variable Input at Bottom */}
<div className="p-4 border-t bg-background shrink-0"> <div className="p-4 border-t bg-background shrink-0">
<Label className="text-xs font-medium text-muted-foreground"> <Label className="text-xs font-medium text-muted-foreground">
Add Environment Variable {t('envEditor.addVariable')}
</Label> </Label>
<div className="flex gap-2 mt-2"> <div className="flex gap-2 mt-2">
<Input <Input
placeholder="VARIABLE_NAME" placeholder={t('envEditor.keyPlaceholder')}
value={newEnvKey} value={newEnvKey}
onChange={(e) => onNewEnvKeyChange(e.target.value.toUpperCase())} onChange={(e) => onNewEnvKeyChange(e.target.value.toUpperCase())}
className="font-mono text-sm h-8 w-2/5" className="font-mono text-sm h-8 w-2/5"
onKeyDown={(e) => e.key === 'Enter' && newEnvKey.trim() && onAddEnvVar()} onKeyDown={(e) => e.key === 'Enter' && newEnvKey.trim() && onAddEnvVar()}
/> />
<Input <Input
placeholder="value" placeholder={t('envEditor.valuePlaceholder')}
value={newEnvValue} value={newEnvValue}
onChange={(e) => onNewEnvValueChange(e.target.value)} onChange={(e) => onNewEnvValueChange(e.target.value)}
className="font-mono text-sm h-8 flex-1" className="font-mono text-sm h-8 flex-1"
@@ -13,6 +13,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Save, Loader2, Trash2, RefreshCw } from 'lucide-react'; import { Save, Loader2, Trash2, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { OpenRouterBadge } from '@/components/profiles/openrouter-badge'; import { OpenRouterBadge } from '@/components/profiles/openrouter-badge';
import { isOpenRouterProfile } from './utils'; import { isOpenRouterProfile } from './utils';
import type { Settings } from './types'; import type { Settings } from './types';
@@ -49,6 +50,7 @@ export function HeaderSection({
onDelete, onDelete,
onSave, onSave,
}: HeaderSectionProps) { }: HeaderSectionProps) {
const { t } = useTranslation();
const isMutating = isSaving || isTargetSaving; const isMutating = isSaving || isTargetSaving;
const disableHeaderActions = isLoading || isMutating; const disableHeaderActions = isLoading || isMutating;
@@ -66,11 +68,11 @@ export function HeaderSection({
</div> </div>
{data && ( {data && (
<p className="text-xs text-muted-foreground mt-0.5"> <p className="text-xs text-muted-foreground mt-0.5">
Last modified: {new Date(data.mtime).toLocaleString()} {t('profileEditor.lastModified')}: {new Date(data.mtime).toLocaleString()}
</p> </p>
)} )}
<div className="mt-2 flex items-center gap-2"> <div className="mt-2 flex items-center gap-2">
<span className="text-xs text-muted-foreground">Default target:</span> <span className="text-xs text-muted-foreground">{t('profileEditor.defaultTarget')}:</span>
<Select <Select
value={target} value={target}
onValueChange={(value) => { onValueChange={(value) => {
@@ -83,9 +85,9 @@ export function HeaderSection({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="claude">Claude Code</SelectItem> <SelectItem value="claude">{t('profileEditor.targetClaude')}</SelectItem>
<SelectItem value="droid">Factory Droid</SelectItem> <SelectItem value="droid">{t('profileEditor.targetDroid')}</SelectItem>
<SelectItem value="codex">Codex CLI</SelectItem> <SelectItem value="codex">{t('profileEditor.targetCodex')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{isTargetSaving && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />} {isTargetSaving && <Loader2 className="w-3.5 h-3.5 animate-spin text-muted-foreground" />}
@@ -104,12 +106,12 @@ export function HeaderSection({
{isSaving ? ( {isSaving ? (
<> <>
<Loader2 className="w-4 h-4 mr-1 animate-spin" /> <Loader2 className="w-4 h-4 mr-1 animate-spin" />
Saving... {t('profileEditor.saving')}
</> </>
) : ( ) : (
<> <>
<Save className="w-4 h-4 mr-1" /> <Save className="w-4 h-4 mr-1" />
Save {t('settingsAuth.save')}
</> </>
)} )}
</Button> </Button>
@@ -1,5 +1,6 @@
import { ArrowUpRight, Image as ImageIcon } from 'lucide-react'; import { ArrowUpRight, Image as ImageIcon } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
@@ -15,33 +16,38 @@ interface ImageAnalysisStatusSectionProps {
onToggleNativeRead?: (enabled: boolean) => void; onToggleNativeRead?: (enabled: boolean) => void;
} }
const TARGET_LABELS: Record<CliTarget, string> = {
claude: 'Claude Code',
droid: 'Factory Droid',
codex: 'Codex CLI',
};
function getPreviewLabel( function getPreviewLabel(
t: (key: string) => string,
source: 'saved' | 'editor', source: 'saved' | 'editor',
previewState: ImageAnalysisStatusSectionProps['previewState'] previewState: ImageAnalysisStatusSectionProps['previewState']
) { ) {
if (previewState === 'refreshing') return 'Refreshing preview'; if (previewState === 'refreshing') return t('imageAnalysisStatus.refreshingPreview');
if (previewState === 'invalid') return 'Saved status'; if (previewState === 'invalid') return t('imageAnalysisStatus.savedStatus');
return source === 'editor' ? 'Live preview' : 'Saved status'; return source === 'editor'
? t('imageAnalysisStatus.livePreview')
: t('imageAnalysisStatus.savedStatus');
} }
function getHeaderLabel(status: ImageAnalysisStatus, target: CliTarget): string { function getHeaderLabel(
if (status.status === 'disabled') return 'Disabled globally'; t: (key: string, options?: Record<string, unknown>) => string,
if (target !== 'claude') return `${TARGET_LABELS[target]} bypasses the hook`; status: ImageAnalysisStatus,
if (status.nativeReadPreference) return 'Native image reading'; target: CliTarget
if (status.status === 'hook-missing') return 'Setup needed'; ): string {
if (status.authReadiness === 'missing') return 'Needs auth'; if (status.status === 'disabled') return t('imageAnalysisStatus.disabledGlobally');
if (status.proxyReadiness === 'unavailable') return 'Needs proxy'; if (target !== 'claude')
if (status.effectiveRuntimeMode === 'native-read') return 'Native fallback'; return t('imageAnalysisStatus.targetBypassesHook', {
return 'Transformer ready'; target: t(`imageAnalysisStatus.targetLabel.${target}`),
});
if (status.nativeReadPreference) return t('imageAnalysisStatus.nativeImageReading');
if (status.status === 'hook-missing') return t('imageAnalysisStatus.setupNeeded');
if (status.authReadiness === 'missing') return t('imageAnalysisStatus.needsAuth');
if (status.proxyReadiness === 'unavailable') return t('imageAnalysisStatus.needsProxy');
if (status.effectiveRuntimeMode === 'native-read') return t('imageAnalysisStatus.nativeFallback');
return t('imageAnalysisStatus.transformerReady');
} }
function getHeaderBadge( function getHeaderBadge(
t: (key: string) => string,
status: ImageAnalysisStatus, status: ImageAnalysisStatus,
target: CliTarget target: CliTarget
): { ): {
@@ -50,75 +56,93 @@ function getHeaderBadge(
} { } {
if (status.status === 'disabled') { if (status.status === 'disabled') {
return { return {
label: 'Disabled', label: t('imageAnalysisStatus.badgeDisabled'),
className: 'border-border/80 bg-background/85 text-muted-foreground', className: 'border-border/80 bg-background/85 text-muted-foreground',
}; };
} }
if (target !== 'claude') { if (target !== 'claude') {
return { return {
label: 'Bypassed', label: t('imageAnalysisStatus.badgeBypassed'),
className: 'border-sky-500/25 bg-sky-500/10 text-sky-800 dark:text-sky-200', className: 'border-sky-500/25 bg-sky-500/10 text-sky-800 dark:text-sky-200',
}; };
} }
if (status.nativeReadPreference) { if (status.nativeReadPreference) {
return { return {
label: 'Native', label: t('imageAnalysisStatus.badgeNative'),
className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200', className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200',
}; };
} }
if (status.status === 'hook-missing' || status.authReadiness === 'missing') { if (status.status === 'hook-missing' || status.authReadiness === 'missing') {
return { return {
label: status.status === 'hook-missing' ? 'Setup' : 'Auth', label:
status.status === 'hook-missing'
? t('imageAnalysisStatus.badgeSetup')
: t('imageAnalysisStatus.badgeAuth'),
className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200', className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200',
}; };
} }
if (status.proxyReadiness === 'unavailable') { if (status.proxyReadiness === 'unavailable') {
return { return {
label: 'Proxy', label: t('imageAnalysisStatus.badgeProxy'),
className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200', className: 'border-amber-500/25 bg-amber-500/10 text-amber-800 dark:text-amber-200',
}; };
} }
return { return {
label: 'Ready', label: t('imageAnalysisStatus.badgeReady'),
className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200', className: 'border-emerald-500/25 bg-emerald-500/10 text-emerald-800 dark:text-emerald-200',
}; };
} }
function getToggleSummary(status: ImageAnalysisStatus, target: CliTarget): string { function getToggleSummary(
t: (key: string, options?: Record<string, unknown>) => string,
status: ImageAnalysisStatus,
target: CliTarget
): string {
if (status.nativeReadPreference) { if (status.nativeReadPreference) {
if (status.profileModel && status.nativeImageCapable) { if (status.profileModel && status.nativeImageCapable) {
return `${status.profileModel} looks image-ready. CCS will bypass the transformer here.`; return t('imageAnalysisStatus.toggleSummaryNativeCapable', { model: status.profileModel });
} }
if (status.profileModel) { if (status.profileModel) {
return `CCS will prefer native reading for ${status.profileModel}.`; return t('imageAnalysisStatus.toggleSummaryNativeModel', { model: status.profileModel });
} }
return 'CCS will prefer native image reading for this profile.'; return t('imageAnalysisStatus.toggleSummaryNativeDefault');
} }
if (!status.backendDisplayName && target === 'claude') { if (!status.backendDisplayName && target === 'claude') {
return 'This profile currently stays on native file access.'; return t('imageAnalysisStatus.toggleSummaryNativeFileAccess');
} }
if (!status.backendDisplayName) { if (!status.backendDisplayName) {
return `Saved Claude-side image routing is inactive while ${TARGET_LABELS[target]} is selected.`; return t('imageAnalysisStatus.toggleSummaryInactiveTarget', {
target: t(`imageAnalysisStatus.targetLabel.${target}`),
});
} }
const modelSuffix = status.model ? ` · ${status.model}` : ''; const modelSuffix = status.model ? ` · ${status.model}` : '';
return `Transformer route: ${status.backendDisplayName}${modelSuffix}.`; return t('imageAnalysisStatus.toggleSummaryTransformerRoute', {
backend: status.backendDisplayName,
modelSuffix,
});
} }
function getExceptionalNote(status: ImageAnalysisStatus, target: CliTarget): string | null { function getExceptionalNote(
t: (key: string, options?: Record<string, unknown>) => string,
status: ImageAnalysisStatus,
target: CliTarget
): string | null {
if (status.status === 'disabled') { if (status.status === 'disabled') {
return 'Image is disabled globally in CCS settings.'; return t('imageAnalysisStatus.noteDisabledGlobally');
} }
if (target !== 'claude') { if (target !== 'claude') {
return `Current target ${TARGET_LABELS[target]} bypasses the Claude Read hook.`; return t('imageAnalysisStatus.noteTargetBypassesHook', {
target: t(`imageAnalysisStatus.targetLabel.${target}`),
});
} }
if (status.nativeReadPreference) { if (status.nativeReadPreference) {
return status.nativeImageCapable === true ? null : status.nativeImageReason; return status.nativeImageCapable === true ? null : status.nativeImageReason;
} }
if (status.status === 'hook-missing') { if (status.status === 'hook-missing') {
return 'Persist the profile hook before transformer routing can run here.'; return t('imageAnalysisStatus.notePersistHook');
} }
if (status.authReadiness === 'missing') { if (status.authReadiness === 'missing') {
return status.authReason; return status.authReason;
@@ -137,6 +161,8 @@ export function ImageAnalysisStatusSection({
nativeReadPreferenceOverride, nativeReadPreferenceOverride,
onToggleNativeRead, onToggleNativeRead,
}: ImageAnalysisStatusSectionProps) { }: ImageAnalysisStatusSectionProps) {
const { t } = useTranslation();
if (!status) { if (!status) {
return ( return (
<div className="rounded-2xl border bg-muted/20 px-4 py-3" aria-live="polite"> <div className="rounded-2xl border bg-muted/20 px-4 py-3" aria-live="polite">
@@ -148,12 +174,12 @@ export function ImageAnalysisStatusSection({
const nativeReadChecked = nativeReadPreferenceOverride ?? status.nativeReadPreference; const nativeReadChecked = nativeReadPreferenceOverride ?? status.nativeReadPreference;
const effectiveStatus = { ...status, nativeReadPreference: nativeReadChecked }; const effectiveStatus = { ...status, nativeReadPreference: nativeReadChecked };
const headerBadge = getHeaderBadge(effectiveStatus, target); const headerBadge = getHeaderBadge(t, effectiveStatus, target);
const note = getExceptionalNote(effectiveStatus, target); const note = getExceptionalNote(t, effectiveStatus, target);
const capabilityLabel = status.nativeImageCapable const capabilityLabel = status.nativeImageCapable
? 'Verified' ? t('imageAnalysisStatus.capabilityVerified')
: status.profileModel : status.profileModel
? 'Unknown' ? t('imageAnalysisStatus.capabilityUnknown')
: null; : null;
return ( return (
@@ -166,13 +192,14 @@ export function ImageAnalysisStatusSection({
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">Image</h3> <h3 className="text-sm font-semibold">{t('imageAnalysisStatus.sectionTitle')}</h3>
<Badge className={cn('h-5 border px-1.5 text-[10px]', headerBadge.className)}> <Badge className={cn('h-5 border px-1.5 text-[10px]', headerBadge.className)}>
{headerBadge.label} {headerBadge.label}
</Badge> </Badge>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{getPreviewLabel(source, previewState)} · {getHeaderLabel(effectiveStatus, target)} {getPreviewLabel(t, source, previewState)} ·{' '}
{getHeaderLabel(t, effectiveStatus, target)}
</p> </p>
</div> </div>
</div> </div>
@@ -180,7 +207,7 @@ export function ImageAnalysisStatusSection({
<Button size="sm" variant="outline" className="h-8 shrink-0" asChild> <Button size="sm" variant="outline" className="h-8 shrink-0" asChild>
<Link to="/settings?tab=image"> <Link to="/settings?tab=image">
Open Settings {t('imageAnalysisStatus.openSettings')}
<ArrowUpRight className="ml-1 h-3.5 w-3.5" /> <ArrowUpRight className="ml-1 h-3.5 w-3.5" />
</Link> </Link>
</Button> </Button>
@@ -190,7 +217,9 @@ export function ImageAnalysisStatusSection({
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<div className="text-sm font-medium text-foreground">Use native image reading</div> <div className="text-sm font-medium text-foreground">
{t('imageAnalysisStatus.useNativeImageReading')}
</div>
{capabilityLabel && ( {capabilityLabel && (
<Badge variant="outline" className="h-5 px-1.5 text-[10px]"> <Badge variant="outline" className="h-5 px-1.5 text-[10px]">
{capabilityLabel} {capabilityLabel}
@@ -198,7 +227,7 @@ export function ImageAnalysisStatusSection({
)} )}
</div> </div>
<p className="mt-1 text-xs leading-5 text-muted-foreground"> <p className="mt-1 text-xs leading-5 text-muted-foreground">
{getToggleSummary(effectiveStatus, target)} {getToggleSummary(t, effectiveStatus, target)}
</p> </p>
</div> </div>
@@ -206,7 +235,7 @@ export function ImageAnalysisStatusSection({
checked={nativeReadChecked} checked={nativeReadChecked}
onCheckedChange={onToggleNativeRead} onCheckedChange={onToggleNativeRead}
disabled={!onToggleNativeRead} disabled={!onToggleNativeRead}
aria-label="Use native image reading" aria-label={t('imageAnalysisStatus.useNativeImageReading')}
/> />
</div> </div>
</div> </div>
+9 -7
View File
@@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/shared/confirm-dialog'; import { ConfirmDialog } from '@/components/shared/confirm-dialog';
import { Loader2, Code2, RefreshCw } from 'lucide-react'; import { Loader2, Code2, RefreshCw } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { HeaderSection } from './header-section'; import { HeaderSection } from './header-section';
import { FriendlyUISection } from './friendly-ui-section'; import { FriendlyUISection } from './friendly-ui-section';
@@ -24,6 +25,7 @@ export function ProfileEditor({
onDelete, onDelete,
onHasChangesUpdate, onHasChangesUpdate,
}: ProfileEditorProps) { }: ProfileEditorProps) {
const { t } = useTranslation();
const [localEdits, setLocalEdits] = useState<Record<string, string>>({}); const [localEdits, setLocalEdits] = useState<Record<string, string>>({});
const [conflictDialog, setConflictDialog] = useState(false); const [conflictDialog, setConflictDialog] = useState(false);
const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null); const [rawJsonEdits, setRawJsonEdits] = useState<string | null>(null);
@@ -299,15 +301,15 @@ export function ProfileEditor({
{isLoading ? ( {isLoading ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /> <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
<span className="ml-3 text-muted-foreground">Loading settings...</span> <span className="ml-3 text-muted-foreground">{t('settingsDialog.loadingSettings')}</span>
</div> </div>
) : isError ? ( ) : isError ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="text-center space-y-3"> <div className="text-center space-y-3">
<p className="text-sm text-muted-foreground">Failed to load settings.</p> <p className="text-sm text-muted-foreground">{t('settingsPage.failedLoad')}</p>
<Button variant="outline" size="sm" onClick={() => refetch()}> <Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4 mr-1" /> <RefreshCw className="w-4 h-4 mr-1" />
Retry {t('apiProfiles.retry')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -332,7 +334,7 @@ export function ProfileEditor({
<div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]"> <div className="px-6 py-2 bg-muted/30 border-b flex items-center gap-2 shrink-0 h-[45px]">
<Code2 className="w-4 h-4 text-muted-foreground" /> <Code2 className="w-4 h-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground"> <span className="text-sm font-medium text-muted-foreground">
Raw Configuration (JSON) {t('rawEditorSection.rawConfig')}
</span> </span>
</div> </div>
<RawEditorSection <RawEditorSection
@@ -355,9 +357,9 @@ export function ProfileEditor({
<ConfirmDialog <ConfirmDialog
open={conflictDialog} open={conflictDialog}
title="File Modified Externally" title={t('settingsDialog.conflictTitle')}
description="Overwrite with your changes or discard?" description={t('settingsDialog.conflictDesc')}
confirmText="Overwrite" confirmText={t('settingsDialog.overwrite')}
variant="destructive" variant="destructive"
onConfirm={() => handleConflictResolve(true)} onConfirm={() => handleConflictResolve(true)}
onCancel={() => handleConflictResolve(false)} onCancel={() => handleConflictResolve(false)}
@@ -5,6 +5,7 @@
import { Suspense, lazy } from 'react'; import { Suspense, lazy } from 'react';
import { Loader2, X, AlertTriangle } from 'lucide-react'; import { Loader2, X, AlertTriangle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator'; import { GlobalEnvIndicator } from '@/components/shared/global-env-indicator';
import { ImageAnalysisStatusSection } from './image-analysis-status-section'; import { ImageAnalysisStatusSection } from './image-analysis-status-section';
import type { Settings } from './types'; import type { Settings } from './types';
@@ -44,6 +45,7 @@ export function RawEditorSection({
onChange, onChange,
missingRequiredFields = [], missingRequiredFields = [],
}: RawEditorSectionProps) { }: RawEditorSectionProps) {
const { t } = useTranslation();
const hasMissingFields = missingRequiredFields.length > 0; const hasMissingFields = missingRequiredFields.length > 0;
return ( return (
@@ -51,7 +53,9 @@ export function RawEditorSection({
fallback={ fallback={
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" /> <Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
<span className="ml-2 text-muted-foreground">Loading editor...</span> <span className="ml-2 text-muted-foreground">
{t('profileEditorSections.loadingEditor')}
</span>
</div> </div>
} }
> >
@@ -59,7 +63,7 @@ export function RawEditorSection({
{!isRawJsonValid && rawJsonEdits !== null && ( {!isRawJsonValid && rawJsonEdits !== null && (
<div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0"> <div className="mb-2 px-3 py-2 bg-destructive/10 text-destructive text-sm rounded-md flex items-center gap-2 mx-6 mt-4 shrink-0">
<X className="w-4 h-4" /> <X className="w-4 h-4" />
Invalid JSON syntax {t('profileEditor.invalidJson')}
</div> </div>
)} )}
{isRawJsonValid && hasMissingFields && ( {isRawJsonValid && hasMissingFields && (
@@ -67,13 +71,13 @@ export function RawEditorSection({
<AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" /> <AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" />
<div> <div>
<span className="font-medium text-amber-600 dark:text-amber-400"> <span className="font-medium text-amber-600 dark:text-amber-400">
Missing required fields: {t('profileEditor.missingFields')}:
</span>{' '} </span>{' '}
<code className="text-xs bg-muted px-1 py-0.5 rounded"> <code className="text-xs bg-muted px-1 py-0.5 rounded">
{missingRequiredFields.join(', ')} {missingRequiredFields.join(', ')}
</code> </code>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
These fields will use default values at runtime. {t('profileEditor.missingFieldsHint')}
</p> </p>
</div> </div>
</div> </div>
@@ -6,6 +6,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import type { Settings, SettingsResponse } from './types'; import type { Settings, SettingsResponse } from './types';
/** Required env vars for profiles to function (informational only - runtime fills defaults) */ /** Required env vars for profiles to function (informational only - runtime fills defaults) */
@@ -34,6 +35,7 @@ export function useProfileEditor({
onSuccess, onSuccess,
onConflict, onConflict,
}: UseProfileEditorOptions) { }: UseProfileEditorOptions) {
const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// Fetch settings for selected profile // Fetch settings for selected profile
@@ -132,11 +134,11 @@ export function useProfileEditor({
onSuccess(); onSuccess();
// Show warning if fields missing (runtime uses defaults) // Show warning if fields missing (runtime uses defaults)
if (data?.warning) { if (data?.warning) {
toast.success('Settings saved', { toast.success(t('commonToast.settingsSaved'), {
description: data.warning, description: data.warning,
}); });
} else { } else {
toast.success('Settings saved'); toast.success(t('commonToast.settingsSaved'));
} }
}, },
onError: (error: Error) => { onError: (error: Error) => {
@@ -3,6 +3,7 @@
* Visual indicator for OpenRouter-configured profiles * Visual indicator for OpenRouter-configured profiles
*/ */
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -13,6 +14,8 @@ interface OpenRouterBadgeProps {
} }
export function OpenRouterBadge({ className, showTooltip = true }: OpenRouterBadgeProps) { export function OpenRouterBadge({ className, showTooltip = true }: OpenRouterBadgeProps) {
const { t } = useTranslation();
const badge = ( const badge = (
<Badge <Badge
variant="outline" variant="outline"
@@ -33,7 +36,7 @@ export function OpenRouterBadge({ className, showTooltip = true }: OpenRouterBad
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{badge}</TooltipTrigger> <TooltipTrigger asChild>{badge}</TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Access 349+ models via OpenRouter</p> <p>{t('openrouterBadge.integration')}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );
@@ -5,6 +5,7 @@
/* eslint-disable react-hooks/set-state-in-effect */ /* eslint-disable react-hooks/set-state-in-effect */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { X, Sparkles, ExternalLink } from 'lucide-react'; import { X, Sparkles, ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useOpenRouterReady } from '@/hooks/use-openrouter-models'; import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
@@ -16,6 +17,7 @@ interface OpenRouterBannerProps {
} }
export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) { export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
const { t } = useTranslation();
const [dismissed, setDismissed] = useState(true); // Start hidden to avoid flash const [dismissed, setDismissed] = useState(true); // Start hidden to avoid flash
const { modelCount, isLoading } = useOpenRouterReady(); const { modelCount, isLoading } = useOpenRouterReady();
@@ -40,10 +42,13 @@ export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
<Sparkles className="w-4 h-4" /> <Sparkles className="w-4 h-4" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium text-sm">NEW: OpenRouter Integration</p> <p className="font-medium text-sm">
{t('openrouterBadge.new')}: {t('openrouterBadge.integration')}
</p>
<p className="text-xs text-white/80 truncate"> <p className="text-xs text-white/80 truncate">
Browse {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google, {t('openrouterBanner.accessModels', {
Meta and more. count: isLoading ? 300 : modelCount,
})}
</p> </p>
</div> </div>
</div> </div>
@@ -56,7 +61,7 @@ export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
onClick={onCreateClick} onClick={onCreateClick}
className="bg-white text-accent hover:bg-white/90 h-8" className="bg-white text-accent hover:bg-white/90 h-8"
> >
Try it now {t('openrouterBanner.add')}
</Button> </Button>
)} )}
<a <a
@@ -4,6 +4,7 @@
*/ */
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -33,9 +34,10 @@ interface OpenRouterModelPickerProps {
export function OpenRouterModelPicker({ export function OpenRouterModelPicker({
value, value,
onChange, onChange,
placeholder = 'Search models...', placeholder,
className, className,
}: OpenRouterModelPickerProps) { }: OpenRouterModelPickerProps) {
const { t } = useTranslation();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedCategory, setSelectedCategory] = useState<ModelCategory | null>(null); const [selectedCategory, setSelectedCategory] = useState<ModelCategory | null>(null);
@@ -105,7 +107,7 @@ export function OpenRouterModelPicker({
<Input <Input
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={placeholder} placeholder={placeholder ?? t('openrouterModelPicker.searchModels')}
className="pl-9" className="pl-9"
/> />
</div> </div>
@@ -180,7 +182,7 @@ export function OpenRouterModelPicker({
<div> <div>
<div className="text-muted-foreground bg-background sticky top-0 mb-2 flex items-center gap-1.5 py-1.5 text-xs font-semibold border-b pb-2"> <div className="text-muted-foreground bg-background sticky top-0 mb-2 flex items-center gap-1.5 py-1.5 text-xs font-semibold border-b pb-2">
<Sparkles className="h-3 w-3 text-accent" /> <Sparkles className="h-3 w-3 text-accent" />
<span>Newest Models</span> <span>{t('openrouterModelPicker.newestModels')}</span>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{newestModels.map((model) => ( {newestModels.map((model) => (
@@ -3,6 +3,7 @@
* Permanent promotional card for OpenRouter - always visible in sidebar footer * Permanent promotional card for OpenRouter - always visible in sidebar footer
*/ */
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useOpenRouterReady } from '@/hooks/use-openrouter-models'; import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
import { Zap } from 'lucide-react'; import { Zap } from 'lucide-react';
@@ -12,7 +13,8 @@ interface OpenRouterPromoCardProps {
} }
export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps) { export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps) {
const { modelCount, isLoading } = useOpenRouterReady(); const { t } = useTranslation();
useOpenRouterReady();
return ( return (
<div className="p-3 border-t bg-gradient-to-r from-accent/5 to-accent/10 dark:from-accent/10 dark:to-accent/15"> <div className="p-3 border-t bg-gradient-to-r from-accent/5 to-accent/10 dark:from-accent/10 dark:to-accent/15">
@@ -21,9 +23,11 @@ export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps)
<img src="/icons/openrouter.svg" alt="" className="w-4 h-4" /> <img src="/icons/openrouter.svg" alt="" className="w-4 h-4" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-xs font-medium text-accent dark:text-accent-foreground">OpenRouter</p> <p className="text-xs font-medium text-accent dark:text-accent-foreground">
{t('openrouterPromoCard.title')}
</p>
<p className="text-[10px] text-muted-foreground truncate"> <p className="text-[10px] text-muted-foreground truncate">
{isLoading ? '300+' : `${modelCount}+`} models available {t('openrouterPromoCard.description')}
</p> </p>
</div> </div>
<Button <Button
@@ -33,7 +37,7 @@ export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps)
className="h-7 px-2 text-accent hover:text-accent hover:bg-accent/10 dark:hover:bg-accent/20" className="h-7 px-2 text-accent hover:text-accent hover:bg-accent/10 dark:hover:bg-accent/20"
> >
<Zap className="w-3 h-3 mr-1" /> <Zap className="w-3 h-3 mr-1" />
<span className="text-xs">Add</span> <span className="text-xs">{t('openrouterBanner.add')}</span>
</Button> </Button>
</div> </div>
</div> </div>
+3 -1
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -23,6 +24,7 @@ interface ProfileCardProps {
} }
export function ProfileCard({ profile, settings, onSwitch, onConfig, onTest }: ProfileCardProps) { export function ProfileCard({ profile, settings, onSwitch, onConfig, onTest }: ProfileCardProps) {
const { t } = useTranslation();
const showOpenRouterIcon = isOpenRouterProfile(settings); const showOpenRouterIcon = isOpenRouterProfile(settings);
return ( return (
@@ -36,7 +38,7 @@ export function ProfileCard({ profile, settings, onSwitch, onConfig, onTest }: P
<TooltipTrigger asChild> <TooltipTrigger asChild>
<img src="/icons/openrouter.svg" alt="OpenRouter" className="w-4 h-4" /> <img src="/icons/openrouter.svg" alt="OpenRouter" className="w-4 h-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>OpenRouter profile</TooltipContent> <TooltipContent>{t('profileCard.openRouter')}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
{profile.isActive && ( {profile.isActive && (
@@ -230,7 +230,7 @@ export function ProfileCreateDialog({
setValue('haikuModel', model.id); setValue('haikuModel', model.id);
setModelSearch(model.name); setModelSearch(model.name);
// Show feedback that model was applied to all tiers // Show feedback that model was applied to all tiers
toast.success(`Applied "${model.name}" to all model tiers`, { toast.success(t('profileCreateDialog.appliedModelToTiers', { model: model.name }), {
duration: 2000, duration: 2000,
}); });
}; };
@@ -268,11 +268,11 @@ export function ProfileCreateDialog({
}; };
try { try {
await createMutation.mutateAsync(finalData); await createMutation.mutateAsync(finalData);
toast.success(`Profile "${finalData.name}" created`); toast.success(t('profileCreateDialog.profileCreated', { name: finalData.name }));
onSuccess(finalData.name); onSuccess(finalData.name);
onOpenChange(false); onOpenChange(false);
} catch (error) { } catch (error) {
toast.error((error as Error).message || 'Failed to create profile'); toast.error((error as Error).message || t('profileCreateDialog.failedCreate'));
} }
}; };
@@ -289,11 +289,9 @@ export function ProfileCreateDialog({
<DialogHeader className="p-6 pb-4 border-b"> <DialogHeader className="p-6 pb-4 border-b">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Plus className="w-5 h-5 text-primary" /> <Plus className="w-5 h-5 text-primary" />
Create API Profile {t('profileCreateDialog.createProfile')}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>{t('profileCreateDialog.chooseProviderHint')}</DialogDescription>
Choose a provider preset or configure a custom API endpoint.
</DialogDescription>
</DialogHeader> </DialogHeader>
<form <form
@@ -361,7 +359,7 @@ export function ProfileCreateDialog({
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs font-medium uppercase tracking-[0.12em] text-foreground/70"> <Label className="text-xs font-medium uppercase tracking-[0.12em] text-foreground/70">
Local Runtimes {t('openrouterQuickStart.localRuntimesTitle')}
</Label> </Label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{LOCAL_RUNTIME_PRESETS.map((preset) => ( {LOCAL_RUNTIME_PRESETS.map((preset) => (
@@ -386,13 +384,13 @@ export function ProfileCreateDialog({
<div className="px-6 pt-4"> <div className="px-6 pt-4">
<TabsList className="grid w-full grid-cols-2"> <TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="basic" className="relative"> <TabsTrigger value="basic" className="relative">
Basic Information {t('profileCreateDialog.basicInformation')}
{hasBasicErrors && ( {hasBasicErrors && (
<span className="absolute top-1 right-2 w-2 h-2 rounded-full bg-destructive animate-pulse" /> <span className="absolute top-1 right-2 w-2 h-2 rounded-full bg-destructive animate-pulse" />
)} )}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="models" className="relative"> <TabsTrigger value="models" className="relative">
Model Configuration {t('profileCreateDialog.modelConfiguration')}
{hasModelErrors && ( {hasModelErrors && (
<span className="absolute top-1 right-2 w-2 h-2 rounded-full bg-destructive animate-pulse" /> <span className="absolute top-1 right-2 w-2 h-2 rounded-full bg-destructive animate-pulse" />
)} )}
@@ -405,19 +403,19 @@ export function ProfileCreateDialog({
{/* Profile Name */} {/* Profile Name */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="name"> <Label htmlFor="name">
Profile Name <span className="text-destructive">*</span> {t('profileDialog.name')} <span className="text-destructive">*</span>
</Label> </Label>
<Input <Input
id="name" id="name"
{...register('name')} {...register('name')}
placeholder="my-api" placeholder={t('profileDialog.namePlaceholder')}
className="font-mono" className="font-mono"
/> />
{errors.name ? ( {errors.name ? (
<p className="text-xs text-destructive">{errors.name.message}</p> <p className="text-xs text-destructive">{errors.name.message}</p>
) : ( ) : (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Used in CLI:{' '} {t('profileCreateDialog.usedInCli')}{' '}
<code className="bg-muted px-1 rounded text-[10px]">ccs my-api "prompt"</code> <code className="bg-muted px-1 rounded text-[10px]">ccs my-api "prompt"</code>
</p> </p>
)} )}
@@ -425,11 +423,11 @@ export function ProfileCreateDialog({
{/* Base URL - always editable, pre-filled from preset */} {/* Base URL - always editable, pre-filled from preset */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="baseUrl">API Base URL</Label> <Label htmlFor="baseUrl">{t('profileCreateDialog.apiBaseUrl')}</Label>
<Input <Input
id="baseUrl" id="baseUrl"
{...register('baseUrl')} {...register('baseUrl')}
placeholder="https://api.example.com/v1" placeholder={t('profileCreateDialog.baseUrlPlaceholder')}
/> />
{errors.baseUrl ? ( {errors.baseUrl ? (
<p className="text-xs text-destructive">{errors.baseUrl.message}</p> <p className="text-xs text-destructive">{errors.baseUrl.message}</p>
@@ -441,12 +439,12 @@ export function ProfileCreateDialog({
) : currentPreset ? ( ) : currentPreset ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{currentPreset.baseUrl {currentPreset.baseUrl
? `Pre-filled from ${currentPreset.name}. You can customize if needed.` ? t('profileCreateDialog.prefilledFromPreset', { name: currentPreset.name })
: `Optional for ${currentPreset.name}. Leave blank to use native Anthropic auth.`} : t('profileCreateDialog.optionalForPreset', { name: currentPreset.name })}
</p> </p>
) : ( ) : (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
The endpoint that accepts OpenAI-compatible and Anthropic requests {t('profileCreateDialog.endpointHint')}
</p> </p>
)} )}
</div> </div>
@@ -454,12 +452,14 @@ export function ProfileCreateDialog({
{/* API Key - optional for presets that don't require it */} {/* API Key - optional for presets that don't require it */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="apiKey"> <Label htmlFor="apiKey">
API Key{' '} {t('profileDialog.apiKey')}{' '}
{currentPreset?.requiresApiKey !== false && ( {currentPreset?.requiresApiKey !== false && (
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
)} )}
{currentPreset?.requiresApiKey === false && ( {currentPreset?.requiresApiKey === false && (
<span className="text-muted-foreground text-xs font-normal">(optional)</span> <span className="text-muted-foreground text-xs font-normal">
{t('profileCreateDialog.optional')}
</span>
)} )}
</Label> </Label>
<div className="relative"> <div className="relative">
@@ -469,8 +469,9 @@ export function ProfileCreateDialog({
{...register('apiKey')} {...register('apiKey')}
placeholder={ placeholder={
currentPreset?.requiresApiKey === false currentPreset?.requiresApiKey === false
? 'Optional - only if auth is enabled' ? t('profileCreateDialog.apiKeyOptionalPlaceholder')
: (currentPreset?.apiKeyPlaceholder ?? 'sk-...') : (currentPreset?.apiKeyPlaceholder ??
t('profileCreateDialog.apiKeyPlaceholder'))
} }
className="pr-10" className="pr-10"
/> />
@@ -489,7 +490,7 @@ export function ProfileCreateDialog({
<p className="text-xs text-destructive">{errors.apiKey.message}</p> <p className="text-xs text-destructive">{errors.apiKey.message}</p>
) : currentPreset?.requiresApiKey === false ? ( ) : currentPreset?.requiresApiKey === false ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Only needed if your local endpoint has authentication enabled {t('profileCreateDialog.apiKeyOptionalHint')}
</p> </p>
) : ( ) : (
currentPreset?.apiKeyHint && ( currentPreset?.apiKeyHint && (
@@ -499,7 +500,7 @@ export function ProfileCreateDialog({
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="target">Default Target CLI</Label> <Label htmlFor="target">{t('codex.defaultTargetCli')}</Label>
<Select <Select
value={targetValue} value={targetValue}
onValueChange={(value) => setValue('target', value as CliTarget)} onValueChange={(value) => setValue('target', value as CliTarget)}
@@ -508,9 +509,9 @@ export function ProfileCreateDialog({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="claude">Claude Code (default)</SelectItem> <SelectItem value="claude">{t('profileCard.claudeCodeDefault')}</SelectItem>
<SelectItem value="droid">Factory Droid</SelectItem> <SelectItem value="droid">{t('profileCard.factoryDroid')}</SelectItem>
<SelectItem value="codex">Codex CLI</SelectItem> <SelectItem value="codex">{t('profileCard.codexCli')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
@@ -553,10 +554,9 @@ export function ProfileCreateDialog({
<div className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-950/20 text-blue-800 dark:text-blue-300 rounded-md text-sm border border-blue-100 dark:border-blue-900/30"> <div className="flex items-start gap-3 p-3 bg-blue-50 dark:bg-blue-950/20 text-blue-800 dark:text-blue-300 rounded-md text-sm border border-blue-100 dark:border-blue-900/30">
<Info className="w-5 h-5 shrink-0 mt-0.5" /> <Info className="w-5 h-5 shrink-0 mt-0.5" />
<div> <div>
<p className="font-medium mb-1">Model Mapping</p> <p className="font-medium mb-1">{t('profileCreateDialog.modelMapping')}</p>
<p className="text-xs opacity-90"> <p className="text-xs opacity-90">
Map Claude Code tiers (Opus/Sonnet/Haiku) to models supported by your {t('profileCreateDialog.modelMappingDesc')}
provider.
</p> </p>
</div> </div>
</div> </div>
@@ -564,11 +564,11 @@ export function ProfileCreateDialog({
{/* OpenRouter Model Picker */} {/* OpenRouter Model Picker */}
{isOpenRouter && ( {isOpenRouter && (
<div className="space-y-2"> <div className="space-y-2">
<Label>Search Models</Label> <Label>{t('openrouterModelPicker.searchModels')}</Label>
<Input <Input
value={modelSearch} value={modelSearch}
onChange={(e) => setModelSearch(e.target.value)} onChange={(e) => setModelSearch(e.target.value)}
placeholder="Type to search (e.g., opus, sonnet, gpt-4o)..." placeholder={t('profileCreateDialog.searchModelsPlaceholder')}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter' && filteredModels.length > 0) { if (e.key === 'Enter' && filteredModels.length > 0) {
e.preventDefault(); e.preventDefault();
@@ -580,15 +580,15 @@ export function ProfileCreateDialog({
{filteredModels.length === 0 ? ( {filteredModels.length === 0 ? (
<p className="text-sm text-muted-foreground p-3 text-center"> <p className="text-sm text-muted-foreground p-3 text-center">
{modelSearch {modelSearch
? `No models found for "${modelSearch}"` ? t('profileCreateDialog.noModelsFound', { query: modelSearch })
: 'Loading models...'} : t('profileCreateDialog.loadingModels')}
</p> </p>
) : ( ) : (
<div className="p-1"> <div className="p-1">
{!modelSearch && ( {!modelSearch && (
<div className="flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground">
<Sparkles className="w-3 h-3 text-accent" /> <Sparkles className="w-3 h-3 text-accent" />
<span>Newest Models</span> <span>{t('openrouterModelPicker.newestModels')}</span>
</div> </div>
)} )}
{filteredModels.map((model) => ( {filteredModels.map((model) => (
@@ -609,7 +609,7 @@ export function ProfileCreateDialog({
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="model"> <Label htmlFor="model">
Default Model {t('profileCreateDialog.defaultModel')}
<Badge variant="outline" className="ml-2 text-[10px] font-mono"> <Badge variant="outline" className="ml-2 text-[10px] font-mono">
ANTHROPIC_MODEL ANTHROPIC_MODEL
</Badge> </Badge>
@@ -625,7 +625,7 @@ export function ProfileCreateDialog({
<div className="grid gap-3 pt-2 border-t"> <div className="grid gap-3 pt-2 border-t">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="sonnetModel" className="text-sm"> <Label htmlFor="sonnetModel" className="text-sm">
Sonnet Mapping {t('profileCreateDialog.sonnetMapping')}
<Badge variant="outline" className="ml-2 text-[10px] font-mono"> <Badge variant="outline" className="ml-2 text-[10px] font-mono">
DEFAULT_SONNET DEFAULT_SONNET
</Badge> </Badge>
@@ -633,14 +633,14 @@ export function ProfileCreateDialog({
<Input <Input
id="sonnetModel" id="sonnetModel"
{...register('sonnetModel')} {...register('sonnetModel')}
placeholder="e.g. gpt-4o, claude-sonnet-4" placeholder={t('profileCreateDialog.sonnetMappingPlaceholder')}
className="font-mono text-sm h-9" className="font-mono text-sm h-9"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="opusModel" className="text-sm"> <Label htmlFor="opusModel" className="text-sm">
Opus Mapping {t('profileCreateDialog.opusMapping')}
<Badge variant="outline" className="ml-2 text-[10px] font-mono"> <Badge variant="outline" className="ml-2 text-[10px] font-mono">
DEFAULT_OPUS DEFAULT_OPUS
</Badge> </Badge>
@@ -648,14 +648,14 @@ export function ProfileCreateDialog({
<Input <Input
id="opusModel" id="opusModel"
{...register('opusModel')} {...register('opusModel')}
placeholder="e.g. o1, claude-opus-4.5" placeholder={t('profileCreateDialog.opusMappingPlaceholder')}
className="font-mono text-sm h-9" className="font-mono text-sm h-9"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="haikuModel" className="text-sm"> <Label htmlFor="haikuModel" className="text-sm">
Haiku Mapping {t('profileCreateDialog.haikuMapping')}
<Badge variant="outline" className="ml-2 text-[10px] font-mono"> <Badge variant="outline" className="ml-2 text-[10px] font-mono">
DEFAULT_HAIKU DEFAULT_HAIKU
</Badge> </Badge>
@@ -663,7 +663,7 @@ export function ProfileCreateDialog({
<Input <Input
id="haikuModel" id="haikuModel"
{...register('haikuModel')} {...register('haikuModel')}
placeholder="e.g. gpt-4o-mini, claude-3.5-haiku" placeholder={t('profileCreateDialog.haikuMappingPlaceholder')}
className="font-mono text-sm h-9" className="font-mono text-sm h-9"
/> />
</div> </div>
@@ -674,7 +674,7 @@ export function ProfileCreateDialog({
<DialogFooter className="p-6 pt-4 border-t bg-muted/10"> <DialogFooter className="p-6 pt-4 border-t bg-muted/10">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}> <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel {t('profileDialog.cancel')}
</Button> </Button>
<Button <Button
type="submit" type="submit"
@@ -684,12 +684,12 @@ export function ProfileCreateDialog({
{isCreating ? ( {isCreating ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating... {t('cliproxyDialog.creating')}
</> </>
) : ( ) : (
<> <>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Create Profile {t('profileCreateDialog.createProfile')}
</> </>
)} )}
</Button> </Button>
@@ -803,6 +803,7 @@ function ModelSearchItem({
onClick: () => void; onClick: () => void;
showAge?: boolean; showAge?: boolean;
}) { }) {
const { t } = useTranslation();
return ( return (
<button <button
type="button" type="button"
@@ -824,7 +825,7 @@ function ModelSearchItem({
variant="secondary" variant="secondary"
className="text-xs group-hover:bg-accent-foreground/20 group-hover:text-accent-foreground" className="text-xs group-hover:bg-accent-foreground/20 group-hover:text-accent-foreground"
> >
Free {t('profileCreateDialog.free')}
</Badge> </Badge>
) : ( ) : (
<span>{formatPricingPair(model.pricing)}</span> <span>{formatPricingPair(model.pricing)}</span>
+9 -3
View File
@@ -1,8 +1,10 @@
import { useProfiles } from '@/hooks/use-profiles'; import { useProfiles } from '@/hooks/use-profiles';
import { ProfileCard } from './profile-card'; import { ProfileCard } from './profile-card';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { useTranslation } from 'react-i18next';
export function ProfileDeck() { export function ProfileDeck() {
const { t } = useTranslation();
const { data: response, isLoading, error } = useProfiles(); const { data: response, isLoading, error } = useProfiles();
if (isLoading) { if (isLoading) {
@@ -18,7 +20,11 @@ export function ProfileDeck() {
} }
if (error) { if (error) {
return <div className="text-destructive text-sm">Failed to load profiles: {error.message}</div>; return (
<div className="text-destructive text-sm">
{t('apiProfiles.failedLoadTitle')}: {error.message}
</div>
);
} }
const profiles = response?.profiles || []; const profiles = response?.profiles || [];
@@ -26,7 +32,7 @@ export function ProfileDeck() {
if (!profiles || profiles.length === 0) { if (!profiles || profiles.length === 0) {
return ( return (
<div className="text-muted-foreground text-center py-8"> <div className="text-muted-foreground text-center py-8">
No profiles configured. Create your first profile to get started. {t('apiProfiles.noProfilesDesc')}
</div> </div>
); );
} }
@@ -39,7 +45,7 @@ export function ProfileDeck() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-lg font-semibold">Profiles</h2> <h2 className="text-lg font-semibold">{t('profileDeck.profiles')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{normalizedProfiles.map((profile) => ( {normalizedProfiles.map((profile) => (
<ProfileCard <ProfileCard
@@ -19,6 +19,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const DEFAULT_MODEL = 'claude-sonnet-4-6'; const DEFAULT_MODEL = 'claude-sonnet-4-6';
// TODO i18n: missing keys for Zod validation messages ("Invalid URL", "Name is required", "Invalid profile name", "API key must be at least 10 characters")
const optionalUrlSchema = z const optionalUrlSchema = z
.string() .string()
.refine((value) => value.trim().length === 0 || z.string().url().safeParse(value).success, { .refine((value) => value.trim().length === 0 || z.string().url().safeParse(value).success, {
@@ -23,6 +23,7 @@ import {
import { MoreHorizontal, Trash2, Edit } from 'lucide-react'; import { MoreHorizontal, Trash2, Edit } from 'lucide-react';
import { useDeleteProfile } from '@/hooks/use-profiles'; import { useDeleteProfile } from '@/hooks/use-profiles';
import type { Profile } from '@/lib/api-client'; import type { Profile } from '@/lib/api-client';
import { useTranslation } from 'react-i18next';
interface ProfilesTableProps { interface ProfilesTableProps {
data: Profile[]; data: Profile[];
@@ -31,20 +32,21 @@ interface ProfilesTableProps {
export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) { export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
const deleteMutation = useDeleteProfile(); const deleteMutation = useDeleteProfile();
const { t } = useTranslation();
const columns: ColumnDef<Profile>[] = [ const columns: ColumnDef<Profile>[] = [
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name', header: t('profilesTable.name'),
size: 200, size: 200,
}, },
{ {
accessorKey: 'settingsPath', accessorKey: 'settingsPath',
header: 'Settings Path', header: t('profileEditor.filePath'),
}, },
{ {
accessorKey: 'configured', accessorKey: 'configured',
header: 'Status', header: t('cliproxyTable.status'),
size: 100, size: 100,
cell: ({ row }) => ( cell: ({ row }) => (
<span className={row.original.configured ? 'text-green-600' : 'text-yellow-600'}> <span className={row.original.configured ? 'text-green-600' : 'text-yellow-600'}>
@@ -54,7 +56,7 @@ export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
}, },
{ {
id: 'actions', id: 'actions',
header: 'Actions', header: t('profilesTable.actions'),
size: 100, size: 100,
cell: ({ row }) => ( cell: ({ row }) => (
<DropdownMenu> <DropdownMenu>
@@ -67,7 +69,7 @@ export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
{onEditSettings && ( {onEditSettings && (
<DropdownMenuItem onClick={() => onEditSettings(row.original)}> <DropdownMenuItem onClick={() => onEditSettings(row.original)}>
<Edit className="w-4 h-4 mr-2" /> <Edit className="w-4 h-4 mr-2" />
Edit {t('profilesTable.edit')}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -76,7 +78,7 @@ export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
onClick={() => deleteMutation.mutate(row.original.name)} onClick={() => deleteMutation.mutate(row.original.name)}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Delete {t('apiProfiles.delete')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -93,9 +95,7 @@ export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
if (data.length === 0) { if (data.length === 0) {
return ( return (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">{t('apiProfiles.noProfilesYet')}</div>
No profiles found. Create one to get started.
</div>
); );
} }
+6 -6
View File
@@ -192,14 +192,14 @@ export function QuickSetupWizard({ open, onClose }: QuickSetupWizardProps) {
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Sparkles className="w-5 h-5 text-primary" /> <Sparkles className="w-5 h-5 text-primary" />
Quick Setup Wizard {i18n.t('setupWizard.title')}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{step === 'provider' && 'Select a provider to get started'} {step === 'provider' && i18n.t('setupWizard.stepProviderDesc')}
{step === 'auth' && 'Authenticate with your provider'} {step === 'auth' && i18n.t('setupWizard.stepAuthDesc')}
{step === 'account' && 'Select which account to use'} {step === 'account' && i18n.t('setupWizard.stepAccountDesc')}
{step === 'variant' && 'Create your custom variant'} {step === 'variant' && i18n.t('setupWizard.stepVariantDesc')}
{step === 'success' && 'Setup complete!'} {step === 'success' && i18n.t('setupWizard.stepSuccessDesc')}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -9,6 +9,7 @@ import { getAccountIdentityPresentation } from '@/lib/account-identity';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import type { AccountStepProps } from '../types'; import type { AccountStepProps } from '../types';
import { useTranslation } from 'react-i18next';
export function AccountStep({ export function AccountStep({
accounts, accounts,
@@ -17,11 +18,12 @@ export function AccountStep({
onAddNew, onAddNew,
onBack, onBack,
}: AccountStepProps) { }: AccountStepProps) {
const { t } = useTranslation();
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Existing accounts header */} {/* Existing accounts header */}
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Select an account ({accounts.length}) {t('setupWizard.accountStep.selectAccount', { count: accounts.length })}
</div> </div>
{/* Scrollable account list with max-height for many accounts */} {/* Scrollable account list with max-height for many accounts */}
@@ -63,7 +65,9 @@ export function AccountStep({
</Badge> </Badge>
)} )}
{acc.isDefault && ( {acc.isDefault && (
<span className="text-xs text-muted-foreground">Default account</span> <span className="text-xs text-muted-foreground">
{t('setupWizard.accountStep.defaultAccount')}
</span>
)} )}
</div> </div>
</div> </div>
@@ -80,7 +84,9 @@ export function AccountStep({
<span className="w-full border-t" /> <span className="w-full border-t" />
</div> </div>
<div className="relative flex justify-center text-xs uppercase"> <div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or</span> <span className="bg-background px-2 text-muted-foreground">
{t('setupWizard.accountStep.or')}
</span>
</div> </div>
</div> </div>
@@ -94,15 +100,19 @@ export function AccountStep({
<ExternalLink className="w-4 h-4 text-primary" /> <ExternalLink className="w-4 h-4 text-primary" />
</div> </div>
<div> <div>
<div className="font-medium text-primary">Add new account</div> <div className="font-medium text-primary">
<div className="text-xs text-muted-foreground">Authenticate with a different account</div> {t('setupWizard.accountStep.addNewAccount')}
</div>
<div className="text-xs text-muted-foreground">
{t('setupWizard.accountStep.addNewAccountDesc')}
</div>
</div> </div>
</button> </button>
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2">
<Button variant="ghost" onClick={onBack}> <Button variant="ghost" onClick={onBack}>
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back {t('setupWizard.accountStep.back')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Copy, Check, RefreshCw, ArrowLeft, Terminal, ExternalLink, Loader2 } from 'lucide-react'; import { Copy, Check, RefreshCw, ArrowLeft, Terminal, ExternalLink, Loader2 } from 'lucide-react';
import type { AuthStepProps } from '../types'; import type { AuthStepProps } from '../types';
import { useTranslation } from 'react-i18next';
export function AuthStep({ export function AuthStep({
selectedProvider, selectedProvider,
@@ -18,6 +19,7 @@ export function AuthStep({
onStartAuth, onStartAuth,
onRefresh, onRefresh,
}: AuthStepProps) { }: AuthStepProps) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const copyCommand = async (cmd: string) => { const copyCommand = async (cmd: string) => {
@@ -31,26 +33,25 @@ export function AuthStep({
{/* Primary: OAuth Button */} {/* Primary: OAuth Button */}
<div className="text-center space-y-3"> <div className="text-center space-y-3">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Authenticate with {providers.find((p) => p.id === selectedProvider)?.name} to add an {t('setupWizard.authStep.authenticateWith', {
account provider: providers.find((p) => p.id === selectedProvider)?.name,
})}
</p> </p>
<Button onClick={onStartAuth} disabled={isPending} className="w-full gap-2" size="lg"> <Button onClick={onStartAuth} disabled={isPending} className="w-full gap-2" size="lg">
{isPending ? ( {isPending ? (
<> <>
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
Authenticating... {t('setupWizard.authStep.authenticating')}
</> </>
) : ( ) : (
<> <>
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
Authenticate in Browser {t('setupWizard.authStep.authenticateInBrowser')}
</> </>
)} )}
</Button> </Button>
{isPending && ( {isPending && (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">{t('setupWizard.authStep.completeOAuth')}</p>
Complete the OAuth flow in your browser...
</p>
)} )}
</div> </div>
@@ -60,7 +61,9 @@ export function AuthStep({
<span className="w-full border-t" /> <span className="w-full border-t" />
</div> </div>
<div className="relative flex justify-center text-xs uppercase"> <div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">Or use terminal</span> <span className="bg-background px-2 text-muted-foreground">
{t('setupWizard.authStep.orUseTerminal')}
</span>
</div> </div>
</div> </div>
@@ -69,7 +72,7 @@ export function AuthStep({
<CardContent className="p-4 space-y-3"> <CardContent className="p-4 space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<Terminal className="w-4 h-4" /> <Terminal className="w-4 h-4" />
Run this command in your terminal: {t('setupWizard.authStep.runCommandHint')}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm"> <code className="flex-1 px-3 py-2 bg-muted rounded-md font-mono text-sm">
@@ -85,11 +88,13 @@ export function AuthStep({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button variant="ghost" onClick={onBack} disabled={isPending}> <Button variant="ghost" onClick={onBack} disabled={isPending}>
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />
Back {t('setupWizard.authStep.back')}
</Button> </Button>
<Button variant="outline" onClick={onRefresh} disabled={isRefreshing || isPending}> <Button variant="outline" onClick={onRefresh} disabled={isRefreshing || isPending}>
<RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${isRefreshing ? 'animate-spin' : ''}`} />
{isRefreshing ? 'Checking...' : 'Refresh Status'} {isRefreshing
? t('setupWizard.authStep.checking')
: t('setupWizard.authStep.refreshStatus')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -6,8 +6,10 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import type { SuccessStepProps } from '../types'; import type { SuccessStepProps } from '../types';
import { useTranslation } from 'react-i18next';
export function SuccessStep({ variantName, onClose }: SuccessStepProps) { export function SuccessStep({ variantName, onClose }: SuccessStepProps) {
const { t } = useTranslation();
return ( return (
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<div className="flex justify-center"> <div className="flex justify-center">
@@ -16,19 +18,19 @@ export function SuccessStep({ variantName, onClose }: SuccessStepProps) {
</div> </div>
</div> </div>
<div> <div>
<div className="font-semibold text-lg">Variant Created!</div> <div className="font-semibold text-lg">{t('setupWizard.successStep.title')}</div>
<div className="text-sm text-muted-foreground">Your custom variant is ready to use</div> <div className="text-sm text-muted-foreground">{t('setupWizard.successStep.subtitle')}</div>
</div> </div>
<Card> <Card>
<CardContent className="p-4 space-y-2"> <CardContent className="p-4 space-y-2">
<div className="text-sm text-muted-foreground">Usage:</div> <div className="text-sm text-muted-foreground">{t('setupWizard.successStep.usage')}</div>
<code className="block px-3 py-2 bg-muted rounded-md font-mono text-sm"> <code className="block px-3 py-2 bg-muted rounded-md font-mono text-sm">
ccs {variantName} "your prompt here" ccs {variantName} "your prompt here"
</code> </code>
</CardContent> </CardContent>
</Card> </Card>
<Button onClick={onClose} className="w-full"> <Button onClick={onClose} className="w-full">
Done {t('setupWizard.successStep.done')}
</Button> </Button>
</div> </div>
); );
@@ -127,9 +127,7 @@ export function VariantStep({
placeholder={t('setupVariant.modelPlaceholder')} placeholder={t('setupVariant.modelPlaceholder')}
/> />
{deniedCustomModel && ( {deniedCustomModel && (
<p className="text-xs text-destructive"> <p className="text-xs text-destructive">{t('providerEditor.agyDenylist')}</p>
Antigravity denylist: Claude Opus 4.5 and Claude Sonnet 4.5 are deprecated.
</p>
)} )}
<button <button
type="button" type="button"
+4 -2
View File
@@ -1,4 +1,5 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
interface CcsLogoProps { interface CcsLogoProps {
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
@@ -13,18 +14,19 @@ const sizeMap = {
}; };
export function CcsLogo({ size = 'md', className, showText = true }: CcsLogoProps) { export function CcsLogo({ size = 'md', className, showText = true }: CcsLogoProps) {
const { t } = useTranslation();
const dimension = sizeMap[size]; const dimension = sizeMap[size];
return ( return (
<div className={cn('flex items-center gap-2', className)}> <div className={cn('flex items-center gap-2', className)}>
<img <img
src="/logo/ccs-logo-256.png" src="/logo/ccs-logo-256.png"
alt="CCS Logo" alt={t('ccsLogo.alt')}
width={dimension} width={dimension}
height={dimension} height={dimension}
className="rounded" className="rounded"
/> />
{showText && <span className="font-bold text-lg">CCS Config</span>} {showText && <span className="font-bold text-lg">{t('ccsLogo.text')}</span>}
</div> </div>
); );
} }
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff. Show more