Merge pull request #1324 from clousky2020/feat/i18n-clean

fix(i18n): resolve code review issues for PR #1318
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-04-16 10:17:42 -03:00
committed by GitHub
8 changed files with 379 additions and 111 deletions
+24 -24
View File
@@ -121,19 +121,19 @@ const STRATEGY_GUIDANCE_FALLBACK = {
example: "Example: Multiple accounts of the same model to distribute usage evenly.",
},
auto: {
when: "Use when you have one preferred model and only want fallback on failure.",
avoid: "Avoid when you need balanced load between models.",
example: "Example: Primary coding model with cheaper backup for outages.",
when: "Use when you want multi-factor scoring based on cost, latency, and quality.",
avoid: "Avoid when you need strict priority ordering or historical persistence.",
example: "Example: Balance requests between models with different strengths.",
},
lkgp: {
when: "Use when you have one preferred model and only want fallback on failure.",
avoid: "Avoid when you need balanced load between models.",
example: "Example: Primary coding model with cheaper backup for outages.",
when: "Use when you want routing based on historical success rates and performance.",
avoid: "Avoid when historical data is limited or unreliable.",
example: "Example: Route to models with proven track records for specific tasks.",
},
"context-optimized": {
when: "Use when you have one preferred model and only want fallback on failure.",
avoid: "Avoid when you need balanced load between models.",
example: "Example: Primary coding model with cheaper backup for outages.",
when: "Use when you need to optimize for context window usage across models.",
avoid: "Avoid when models have similar context lengths or simple tasks.",
example: "Example: Distribute long conversations across models with large context windows.",
},
};
@@ -240,30 +240,30 @@ const STRATEGY_RECOMMENDATIONS_FALLBACK = {
],
},
auto: {
title: "Fail-safe baseline",
description: "Use one primary model and keep fallback chain short and reliable.",
title: "Multi-factor optimization",
description: "Routes based on real-time scoring of cost, latency, quality, and health.",
tips: [
"Put your most reliable model first.",
"Keep 1-2 backup models with similar quality.",
"Use safe retries to absorb transient provider failures.",
"Let the engine balance across multiple factors automatically.",
"Monitor which factors drive routing decisions in the logs.",
"Use for complex workloads where no single factor dominates.",
],
},
lkgp: {
title: "Fail-safe baseline",
description: "Use one primary model and keep fallback chain short and reliable.",
title: "History-based routing",
description: "Routes based on historical success rates and persistent performance data.",
tips: [
"Put your most reliable model first.",
"Keep 1-2 backup models with similar quality.",
"Use safe retries to absorb transient provider failures.",
"Let success history accumulate before relying on this strategy.",
"Models with better track records get preference over time.",
"Ideal for stable workloads with consistent model availability.",
],
},
"context-optimized": {
title: "Fail-safe baseline",
description: "Use one primary model and keep fallback chain short and reliable.",
title: "Context-aware distribution",
description: "Routes to optimize context window usage and conversation continuity.",
tips: [
"Put your most reliable model first.",
"Keep 1-2 backup models with similar quality.",
"Use safe retries to absorb transient provider failures.",
"Best for long conversations that span multiple requests.",
"Selects models with appropriate context capacity automatically.",
"Use when context limits are a bottleneck for your workload.",
],
},
};
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useTranslations } from "next-intl";
import { Card, Button, Select, Badge } from "@/shared/components";
import { ALIAS_TO_ID } from "@/shared/constants/providers";
@@ -185,18 +185,21 @@ export default function PlaygroundPage() {
const t = useTranslations("playground");
// Get translated endpoint options
const getEndpointOptions = () => [
{ value: "chat", label: t("endpointOptions.chat") },
{ value: "responses", label: t("endpointOptions.responses") },
{ value: "images", label: t("endpointOptions.images") },
{ value: "embeddings", label: t("endpointOptions.embeddings") },
{ value: "speech", label: t("endpointOptions.speech") },
{ value: "transcription", label: t("endpointOptions.transcription") },
{ value: "video", label: t("endpointOptions.video") },
{ value: "music", label: t("endpointOptions.music") },
{ value: "rerank", label: t("endpointOptions.rerank") },
{ value: "search", label: t("endpointOptions.search") },
];
const endpointOptions = useMemo(
() => [
{ value: "chat", label: t("endpointOptions.chat") },
{ value: "responses", label: t("endpointOptions.responses") },
{ value: "images", label: t("endpointOptions.images") },
{ value: "embeddings", label: t("endpointOptions.embeddings") },
{ value: "speech", label: t("endpointOptions.speech") },
{ value: "transcription", label: t("endpointOptions.transcription") },
{ value: "video", label: t("endpointOptions.video") },
{ value: "music", label: t("endpointOptions.music") },
{ value: "rerank", label: t("endpointOptions.rerank") },
{ value: "search", label: t("endpointOptions.search") },
],
[t]
);
const [models, setModels] = useState<ModelInfo[]>([]);
const [providers, setProviders] = useState<ProviderOption[]>([]);
@@ -495,7 +498,7 @@ export default function PlaygroundPage() {
<Select
value={selectedEndpoint}
onChange={(e: any) => handleEndpointChange(e.target.value)}
options={getEndpointOptions()}
options={endpointOptions}
className="w-full"
/>
</div>
+255
View File
@@ -3318,5 +3318,260 @@
"deduplicatedRequests": "Deduplicated Requests",
"savedCalls": "Saved API Calls",
"totalProcessed": "Total Requests Processed"
},
"proxyConfigModal": {
"levelGlobal": "Global",
"levelProvider": "Provider",
"levelCombo": "Combo",
"levelKey": "Key",
"levelDirect": "Direct (no proxy)",
"titleGlobal": "Global Proxy Configuration",
"titleLevel": "{level} Proxy — {label}",
"loading": "Loading proxy configuration...",
"inheritingFrom": "Inheriting from",
"source": "Source",
"savedProxy": "Saved Proxy",
"custom": "Custom",
"selectSavedProxyPlaceholder": "Select saved proxy...",
"proxyType": "Proxy Type",
"host": "Host",
"hostPlaceholder": "1.2.3.4 or proxy.example.com",
"port": "Port",
"authOptional": "Authentication (optional)",
"username": "Username",
"usernamePlaceholder": "Username",
"password": "Password",
"passwordPlaceholder": "Password",
"connected": "Connected",
"ip": "IP:",
"connectionFailed": "Connection failed",
"testConnection": "Test connection",
"clear": "Clear",
"cancel": "Cancel",
"save": "Save",
"errorSelectSavedProxy": "Please select a saved proxy first.",
"errorSelectProxyFirst": "Please select a proxy first.",
"errorProxyNotFound": "Selected proxy not found.",
"errorClearSavedProxy": "Failed to clear saved proxy",
"errorSaveProxy": "Failed to save proxy configuration",
"errorClearProxy": "Failed to clear proxy configuration",
"errorSocks5Hidden": "SOCKS5 is configured but hidden because NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=false."
},
"oauthModal": {
"title": "Connect {providerName}",
"waiting": "Waiting for authorization",
"completeAuthInPopup": "Complete authorization in popup.",
"popupClosedHint": "If the popup closed without redirecting back (e.g. Qoder), this dialog will auto-switch to manual URL entry mode.",
"popupBlocked": "Popup blocked? Enter URL manually",
"deviceCodeVisitUrl": "Visit the URL below and enter code:",
"deviceCodeVerificationUrl": "Verification URL",
"deviceCodeYourCode": "Your code",
"deviceCodeWaiting": "Waiting for authorization...",
"googleOAuthWarning": "Remote access + Google OAuth: Default credentials only accept redirects to <code>localhost</code>. After authorizing, your browser will try to open <code>localhost</code> — copy that full URL and paste it below. For fully remote use without this manual step, <a>configure your own OAuth credentials</a>.",
"remoteAccessInfo": "Remote access: Since you're accessing OmniRoute remotely, after authorization you'll see an error page (localhost not found). This is normal — just copy the full URL from your browser address bar and paste it below.",
"step1OpenUrl": "Step 1: Open this URL in your browser",
"copy": "Copy",
"step2PasteCallback": "Step 2: Paste callback URL or authorization code here",
"step2Hint": "After authorization, paste the full callback URL. For Claude Code and Cline, you can also paste the authentication code directly, e.g. <code>code#state</code>.",
"connect": "Connect",
"cancel": "Cancel",
"success": "Connection successful!",
"successMessage": "Your {providerName} account has been connected.",
"done": "Done",
"error": "Connection failed",
"tryAgain": "Try again"
},
"cursorAuthModal": {
"title": "Connect Cursor IDE",
"autoDetecting": "Auto-detecting tokens...",
"readingFromCursor": "Reading from Cursor IDE or cursor-agent",
"tokensAutoDetected": "Tokens successfully auto-detected from Cursor IDE!",
"cursorNotDetected": "Cursor IDE not detected. Please manually paste your token.",
"accessToken": "Access Token",
"required": "*",
"accessTokenPlaceholder": "Access token will auto-populate...",
"machineId": "Machine ID",
"optional": "(optional)",
"machineIdPlaceholder": "Machine ID will auto-populate...",
"importing": "Importing...",
"importToken": "Import Token",
"cancel": "Cancel",
"errorAutoDetect": "Unable to auto-detect tokens",
"errorAutoDetectFailed": "Auto-detect tokens failed",
"errorEnterToken": "Please enter access token",
"errorImportFailed": "Import failed"
},
"pricingModal": {
"title": "Pricing Configuration",
"loading": "Loading pricing data...",
"pricingRatesFormat": "Pricing Rates Format",
"ratesDescription": "All rates are in <strong>dollars per million tokens</strong> ($/1M tokens). Example: Input rate of 2.50 means $2.50 per 1,000,000 input tokens.",
"model": "Model",
"input": "Input",
"output": "Output",
"cached": "Cached",
"reasoning": "Reasoning",
"cacheCreation": "Cache Creation",
"noPricingData": "No pricing data available",
"resetToDefaults": "Reset to defaults",
"cancel": "Cancel",
"saving": "Saving...",
"saveChanges": "Save changes",
"resetConfirm": "Reset all pricing to defaults? This cannot be undone.",
"errorSaveFailed": "Failed to save pricing",
"errorResetFailed": "Failed to reset pricing"
},
"proxyRegistry": {
"title": "Proxy Registry",
"description": "Store reusable proxies and track assignments.",
"importLegacy": "Import Legacy",
"bulkAssign": "Bulk Assign",
"addProxy": "Add Proxy",
"loading": "Loading proxies...",
"noProxies": "No saved proxies yet.",
"tableName": "Name",
"tableEndpoint": "Endpoint",
"tableStatus": "Status",
"tableHealth": "Health (24h)",
"tableUsage": "Usage",
"tableActions": "Actions",
"test": "Test",
"edit": "Edit",
"delete": "Delete",
"modalCreateTitle": "Create Proxy",
"modalEditTitle": "Edit Proxy",
"labelName": "Name",
"labelType": "Type",
"labelHost": "Host",
"labelPort": "Port",
"labelUsername": "Username",
"labelPassword": "Password",
"labelRegion": "Region",
"labelStatus": "Status",
"labelNotes": "Notes",
"usernamePlaceholderEdit": "Leave blank to keep current username",
"passwordPlaceholderEdit": "Leave blank to keep current password",
"statusActive": "Active",
"statusInactive": "Inactive",
"cancel": "Cancel",
"save": "Save",
"bulkModalTitle": "Bulk Proxy Assignment",
"bulkLabelScope": "Scope",
"bulkLabelProxy": "Proxy",
"bulkClearAssignment": "(Clear assignment)",
"bulkLabelScopeIds": "Scope IDs (comma or newline separated)",
"bulkScopeIdsPlaceholder": "provider-openai,provider-anthropic",
"bulkApply": "Apply",
"errorLoadFailed": "Failed to load proxy registry",
"errorNameHostRequired": "Name and host are required",
"errorSaveFailed": "Failed to save proxy",
"errorDeleteFailed": "Failed to delete proxy",
"errorForceDeleteConfirm": "This proxy is still assigned. Force delete and remove all assignments?",
"errorMigrateFailed": "Failed to migrate legacy proxy config",
"errorBulkFailed": "Failed to execute bulk assignment",
"success": "✓",
"failure": "✗",
"failed": "Failed",
"successRate": "{rate}% success",
"avgLatency": "{latency}ms average",
"assignmentsCount": "{count} assignments",
"noData": "—",
"testSuccess": "✓ {ip}",
"testLatency": "{latency}ms",
"testFailure": "✗ {error}"
},
"playground": {
"title": "Model Playground",
"description": "Test any model directly from the dashboard. Select provider, model, and endpoint type, then send a request to see the raw response.",
"endpoint": "Endpoint",
"provider": "Provider",
"model": "Model",
"accountKey": "Account / Key",
"autoAccounts": "Auto ({count} accounts)",
"noAccounts": "No accounts",
"send": "Send",
"cancel": "Cancel",
"audioFile": "Audio File",
"attachImages": "Attach Images (Vision)",
"multipartFormData": "multipart/form-data",
"upToImages": "Up to 4 images",
"selectAudioFile": "Select audio file for transcription (mp3, wav, m4a, ogg, flac…)",
"clearAll": "Clear All",
"request": "Request",
"response": "Response",
"transcription": "Transcription",
"copy": "Copy",
"resetToDefault": "Reset to default",
"downloadAudio": "Download audio",
"copyText": "Copy text",
"transcriptionHint": "Transcription uses multipart/form-data. Upload the audio file above — the JSON below controls extra parameters (model, language).",
"imagesGenerated": "Generated {count} images",
"generatedImage": "Generated image {index}",
"save": "Save",
"endpointOptions": {
"chat": "Chat completions",
"responses": "Responses",
"images": "Image generation",
"embeddings": "Embeddings",
"speech": "Text-to-speech",
"transcription": "Audio transcription",
"video": "Video generation",
"music": "Music generation",
"rerank": "Rerank",
"search": "Web search"
}
},
"requestLogger": {
"recording": "Recording",
"paused": "Paused",
"pipelineLogsOn": "Pipeline logs on",
"pipelineLogsOff": "Pipeline logs off",
"updatingPipelineLogs": "Updating pipeline logs...",
"searchPlaceholder": "Search models, providers, accounts, API keys, combos...",
"allProviders": "All providers",
"allModels": "All models",
"allAccounts": "All accounts",
"allApiKeys": "All API keys",
"total": "Total",
"ok": "Success",
"err": "Error",
"combo": "Combo",
"keys": "Keys",
"shown": "Shown",
"sortNewest": "Newest",
"sortOldest": "Oldest",
"sortTokensDesc": "Tokens ↓",
"sortTokensAsc": "Tokens ↑",
"sortDurationDesc": "Duration ↓",
"sortDurationAsc": "Duration ↑",
"sortStatusDesc": "Status ↓",
"sortStatusAsc": "Status ↑",
"sortModelAsc": "Model A-Z",
"sortModelDesc": "Model Z-A",
"statusFilters": {
"all": "All",
"error": "Error",
"success": "Success",
"combo": "Combo"
},
"columns": {
"status": "Status",
"cacheSource": "Cache Source",
"model": "Model",
"requested": "Requested",
"provider": "Provider",
"protocol": "Request Protocol",
"account": "Account",
"apiKey": "API Key",
"combo": "Combo",
"tokens": "Tokens",
"tps": "TPS",
"duration": "Duration",
"time": "Time"
},
"loadingLogs": "Loading logs...",
"noLogs": "No logs yet. Make some API calls to see them here.",
"noMatchingLogs": "No logs matching current filters.",
"callLogsInfo": "Call logs are also saved as JSON files to {dataDir} and rotated based on {retentionDays} and {maxEntries}."
}
}
+26 -24
View File
@@ -921,19 +921,19 @@
"example": "示例:在使用完所有 200 美元的 Deepgram 额度后再回退到 Groq。"
},
"auto": {
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
"when": "当您需要基于成本、延迟和质量的多因素评分路由时使用。",
"avoid": "当您需要严格的优先级排序或历史持久性时避免使用。",
"example": "示例:在具有不同优势的模型之间平衡请求。"
},
"lkgp": {
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
"when": "当您希望基于历史成功率和性能进行路由时使用。",
"avoid": "当历史数据有限或不可靠时避免使用。",
"example": "示例:路由到在特定任务上有良好记录的模型。"
},
"context-optimized": {
"when": "当您有一个首选模型且只希望在失败时才回退到备用模型时使用。",
"avoid": "当您需要在多个模型之间平衡负载时避免使用。",
"example": "示例:主力编码模型搭配更便宜的备用模型以应对故障。"
"when": "当您需要优化模型间的上下文窗口使用时使用。",
"avoid": "当模型具有相似的上下文长度或任务简单时避免使用。",
"example": "示例:在具有大上下文窗口的模型之间分配长对话。"
}
},
"advancedHelp": {
@@ -1120,25 +1120,25 @@
"tip3": "非常适合在多个 API 账户之间做负载均衡。"
},
"auto": {
"title": "稳妥基线",
"description": "使用一个主模型,并保持回退链路简短且可靠。",
"tip1": "把最可靠的模型放在第一位。",
"tip2": "保留 1 到 2 个质量相近的备用模型。",
"tip3": "开启安全重试,吸收临时性的提供商故障。"
"title": "多因素优化",
"description": "基于成本、延迟、质量和健康的实时评分进行路由。",
"tip1": "让引擎自动平衡多个因素。",
"tip2": "在日志中监控哪些因素驱动路由决策。",
"tip3": "用于复杂工作负载,其中没有单一因素占主导。"
},
"lkgp": {
"title": "稳妥基线",
"description": "使用一个主模型,并保持回退链路简短且可靠。",
"tip1": "把最可靠的模型放在第一位。",
"tip2": "保留 1 到 2 个质量相近的备用模型。",
"tip3": "开启安全重试,吸收临时性的提供商故障。"
"title": "历史路由",
"description": "基于历史成功率和持久性能数据进行路由。",
"tip1": "在依赖此策略之前让成功历史积累足够数据。",
"tip2": "最适合具有稳定性能特征的工作负载。",
"tip3": "定期审查历史数据,确保路由决策保持准确。"
},
"context-optimized": {
"title": "稳妥基线",
"description": "使用一个主模型,并保持回退链路简短且可靠。",
"tip1": "把最可靠的模型放在第一位。",
"tip2": "保留 1 到 2 个质量相近的备用模型。",
"tip3": "开启安全重试,吸收临时性的提供商故障。"
"title": "上下文优化",
"description": "基于上下文窗口使用情况和令牌效率优化路由。",
"tip1": "将长对话路由到具有更大上下文窗口的模型。",
"tip2": "监控上下文利用率以避免令牌浪费。",
"tip3": "最适合需要大量上下文保留的对话式 AI。"
}
},
"templateFreeStack": "免费栈($0",
@@ -3658,6 +3658,7 @@
},
"columns": {
"status": "状态",
"cacheSource": "缓存来源",
"model": "模型",
"requested": "请求",
"provider": "提供商",
@@ -3666,6 +3667,7 @@
"apiKey": "API密钥",
"combo": "组合",
"tokens": "Tokens",
"tps": "TPS",
"duration": "时长",
"time": "时间"
},
+18 -17
View File
@@ -646,16 +646,21 @@ export default function OAuthModal({
<span className="material-symbols-outlined text-sm align-middle mr-1">
warning
</span>
<strong
dangerouslySetInnerHTML={{
__html: t("googleOAuthWarning")
.replace(
"<a>",
'<a href="https://github.com/diegosouzapw/OmniRoute#oauth-on-a-remote-server" target="_blank" rel="noreferrer" class="underline">'
)
.replace("</a>", "</a>"),
}}
/>
<strong>
{t.rich("googleOAuthWarning", {
code: (chunks) => <code>{chunks}</code>,
a: (chunks) => (
<a
href="https://github.com/diegosouzapw/OmniRoute#oauth-on-a-remote-server"
target="_blank"
rel="noreferrer"
className="underline"
>
{chunks}
</a>
),
})}
</strong>
</div>
)}
{/* Generic remote info for other providers */}
@@ -686,13 +691,9 @@ export default function OAuthModal({
<div>
<p className="text-sm font-medium mb-2">{t("step2PasteCallback")}</p>
<p className="text-xs text-text-muted mb-2">
<span
dangerouslySetInnerHTML={{
__html: t("step2Hint")
.replace("<code>", "<code>")
.replace("</code>", "</code>"),
}}
/>
{t.rich("step2Hint", {
code: (chunks) => <code>{chunks}</code>,
})}
</p>
<Input
value={callbackUrl}
+3 -7
View File
@@ -119,13 +119,9 @@ export default function PricingModal({ isOpen, onClose, onSave }) {
<div className="bg-bg-subtle border border-border rounded-lg p-3 text-sm">
<p className="font-medium mb-1">{t("pricingRatesFormat")}</p>
<p className="text-text-muted">
<span
dangerouslySetInnerHTML={{
__html: t("ratesDescription")
.replace("<strong>", "<strong>")
.replace("</strong>", "</strong>"),
}}
/>
{t.rich("ratesDescription", {
strong: (chunks) => <strong>{chunks}</strong>,
})}
</p>
</div>
+4 -1
View File
@@ -354,7 +354,10 @@ export default function ProxyConfigModal({
const title =
level === "global"
? t("titleGlobal")
: `${t(`level${level.charAt(0).toUpperCase() + level.slice(1)}` as any)} Proxy — ${levelLabel || levelId || ""}`;
: t("titleLevel", {
level: t(`level${level.charAt(0).toUpperCase() + level.slice(1)}` as any),
label: levelLabel || levelId || "",
});
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} maxWidth="lg">
+32 -24
View File
@@ -116,27 +116,35 @@ export default function RequestLoggerV2() {
const t = useTranslations("requestLogger");
// Get translated status filters
const getStatusFilters = () => [
{ key: "all", label: t("statusFilters.all"), icon: "" },
{ key: "error", label: t("statusFilters.error"), icon: "error" },
{ key: "ok", label: t("statusFilters.success"), icon: "check_circle" },
{ key: "combo", label: t("statusFilters.combo"), icon: "hub" },
];
const statusFilters = useMemo(
() => [
{ key: "all", label: t("statusFilters.all"), icon: "" },
{ key: "error", label: t("statusFilters.error"), icon: "error" },
{ key: "ok", label: t("statusFilters.success"), icon: "check_circle" },
{ key: "combo", label: t("statusFilters.combo"), icon: "hub" },
],
[t]
);
// Get translated columns
const getColumns = () => [
{ key: "status", label: t("columns.status") },
{ key: "model", label: t("columns.model") },
{ key: "requestedModel", label: t("columns.requested") },
{ key: "provider", label: t("columns.provider") },
{ key: "protocol", label: t("columns.protocol") },
{ key: "account", label: t("columns.account") },
{ key: "apiKey", label: t("columns.apiKey") },
{ key: "combo", label: t("columns.combo") },
{ key: "tokens", label: t("columns.tokens") },
{ key: "duration", label: t("columns.duration") },
{ key: "time", label: t("columns.time") },
];
const columns = useMemo(
() => [
{ key: "status", label: t("columns.status") },
{ key: "cacheSource", label: t("columns.cacheSource") },
{ key: "model", label: t("columns.model") },
{ key: "requestedModel", label: t("columns.requested") },
{ key: "provider", label: t("columns.provider") },
{ key: "protocol", label: t("columns.protocol") },
{ key: "account", label: t("columns.account") },
{ key: "apiKey", label: t("columns.apiKey") },
{ key: "combo", label: t("columns.combo") },
{ key: "tokens", label: t("columns.tokens") },
{ key: "tps", label: t("columns.tps") },
{ key: "duration", label: t("columns.duration") },
{ key: "time", label: t("columns.time") },
],
[t]
);
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
@@ -159,7 +167,7 @@ export default function RequestLoggerV2() {
// Column visibility with localStorage persistence
const [visibleColumns, setVisibleColumns] = useState(() => {
const defaultVisible = Object.fromEntries(getColumns().map((c) => [c.key, true]));
const defaultVisible = Object.fromEntries(columns.map((c) => [c.key, true]));
if (typeof window === "undefined") return defaultVisible;
try {
const saved = localStorage.getItem("loggerVisibleColumns");
@@ -525,7 +533,7 @@ export default function RequestLoggerV2() {
{/* Quick Filters */}
<div className="flex flex-wrap items-center gap-2">
{/* Status Filters */}
{getStatusFilters().map((f) => (
{statusFilters.map((f) => (
<button
key={f.key}
onClick={() => setActiveFilter(activeFilter === f.key ? "all" : f.key)}
@@ -582,7 +590,7 @@ export default function RequestLoggerV2() {
{/* Column Visibility Toggles */}
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-[10px] text-text-muted uppercase tracking-wider mr-1">Columns</span>
{getColumns().map((col) => (
{columns.map((col) => (
<button
key={col.key}
onClick={() => toggleColumn(col.key)}
@@ -628,7 +636,7 @@ export default function RequestLoggerV2() {
)}
{visibleColumns.cacheSource && (
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px]">
Cache Source
{t("columns.cacheSource")}
</th>
)}
{visibleColumns.model && (
@@ -673,7 +681,7 @@ export default function RequestLoggerV2() {
)}
{visibleColumns.tps && (
<th className="px-3 py-2.5 font-semibold text-text-muted uppercase tracking-wider text-[10px] text-right">
TPS
{t("columns.tps")}
</th>
)}
{visibleColumns.duration && (