feat(i18n): migrate Settings page batch 1 (7 files, 28+ strings)

- Main page.tsx: tab labels, footer
- AppearanceTab: dark mode, health check logs
- CacheStatsCard: prompt cache stats
- ProxyTab: global proxy
- SystemPromptTab: global system prompt
- ThinkingBudgetTab: mode labels, effort levels
- Expanded settings namespace from 63 to 100 keys
This commit is contained in:
diegosouzapw
2026-02-25 14:20:06 -03:00
parent 87c7c83dd9
commit 0ac264b39d
8 changed files with 135 additions and 48 deletions

View File

@@ -4,9 +4,11 @@ import { useState, useEffect } from "react";
import { Card, Toggle } from "@/shared/components";
import { useTheme } from "@/shared/hooks/useTheme";
import { cn } from "@/shared/utils/cn";
import { useTranslations } from "next-intl";
export default function AppearanceTab() {
const { theme, setTheme, isDark } = useTheme();
const t = useTranslations("settings");
const [settings, setSettings] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
@@ -48,13 +50,13 @@ export default function AppearanceTab() {
palette
</span>
</div>
<h3 className="text-lg font-semibold">Appearance</h3>
<h3 className="text-lg font-semibold">{t("appearance")}</h3>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Dark Mode</p>
<p className="text-sm text-text-muted">Switch between light and dark themes</p>
<p className="font-medium">{t("darkMode")}</p>
<p className="text-sm text-text-muted">{t("switchThemes")}</p>
</div>
<Toggle checked={isDark} onChange={() => setTheme(isDark ? "light" : "dark")} />
</div>
@@ -90,10 +92,8 @@ export default function AppearanceTab() {
<div className="pt-4 border-t border-border">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Hide Health Check Logs</p>
<p className="text-sm text-text-muted">
When ON, suppress [HealthCheck] messages in server console
</p>
<p className="font-medium">{t("hideHealthLogs")}</p>
<p className="text-sm text-text-muted">{t("hideHealthLogsDesc")}</p>
</div>
<Toggle
checked={settings.hideHealthCheckLogs === true}

View File

@@ -2,10 +2,12 @@
import { useState, useEffect } from "react";
import { Card } from "@/shared/components";
import { useTranslations } from "next-intl";
export default function CacheStatsCard() {
const [cache, setCache] = useState(null);
const [flushing, setFlushing] = useState(false);
const t = useTranslations("settings");
const fetchStats = () => {
fetch("/api/cache/stats")
@@ -31,40 +33,40 @@ export default function CacheStatsCard() {
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-[20px]">cached</span>
Prompt Cache
{t("promptCache")}
</h3>
<button
onClick={handleFlush}
disabled={flushing}
className="px-3 py-1.5 text-xs rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors disabled:opacity-50"
>
{flushing ? "Flushing" : "Flush Cache"}
{flushing ? t("flushing") : t("flushCache")}
</button>
</div>
{cache ? (
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-text-muted">Size</p>
<p className="text-text-muted">{t("size")}</p>
<p className="font-mono text-lg text-text-main">
{cache.size}/{cache.maxSize}
</p>
</div>
<div>
<p className="text-text-muted">Hit Rate</p>
<p className="text-text-muted">{t("hitRate")}</p>
<p className="font-mono text-lg text-text-main">{cache.hitRate?.toFixed(1) ?? 0}%</p>
</div>
<div>
<p className="text-text-muted">Hits</p>
<p className="text-text-muted">{t("hits")}</p>
<p className="font-mono text-text-main">{cache.hits ?? 0}</p>
</div>
<div>
<p className="text-text-muted">Evictions</p>
<p className="text-text-muted">{t("evictions")}</p>
<p className="font-mono text-text-main">{cache.evictions ?? 0}</p>
</div>
</div>
) : (
<p className="text-sm text-text-muted">Loading cache stats</p>
<p className="text-sm text-text-muted">{t("loadingCacheStats")}</p>
)}
</Card>
);

View File

@@ -2,11 +2,14 @@
import { useState, useEffect, useRef } from "react";
import { Card, Button, ProxyConfigModal } from "@/shared/components";
import { useTranslations } from "next-intl";
export default function ProxyTab() {
const [proxyModalOpen, setProxyModalOpen] = useState(false);
const [globalProxy, setGlobalProxy] = useState(null);
const mountedRef = useRef(true);
const t = useTranslations("settings");
const tc = useTranslations("common");
const loadGlobalProxy = async () => {
try {
@@ -44,12 +47,9 @@ export default function ProxyTab() {
<span className="material-symbols-outlined text-xl text-primary" aria-hidden="true">
vpn_lock
</span>
<h2 className="text-lg font-bold">Global Proxy</h2>
<h2 className="text-lg font-bold">{t("globalProxy")}</h2>
</div>
<p className="text-sm text-text-muted mb-4">
Configure a global outbound proxy for all API calls. Individual providers, combos, and
keys can override this.
</p>
<p className="text-sm text-text-muted mb-4">{t("globalProxyDesc")}</p>
<div className="flex items-center gap-3">
{globalProxy ? (
<div className="flex items-center gap-2">
@@ -58,7 +58,7 @@ export default function ProxyTab() {
</span>
</div>
) : (
<span className="text-sm text-text-muted">No global proxy configured</span>
<span className="text-sm text-text-muted">{t("noGlobalProxy")}</span>
)}
<Button
size="sm"
@@ -69,7 +69,7 @@ export default function ProxyTab() {
setProxyModalOpen(true);
}}
>
{globalProxy ? "Edit" : "Configure"}
{globalProxy ? tc("edit") : t("configure")}
</Button>
</div>
</div>

View File

@@ -2,12 +2,14 @@
import { useState, useEffect } from "react";
import { Card, Toggle } from "@/shared/components";
import { useTranslations } from "next-intl";
export default function SystemPromptTab() {
const [config, setConfig] = useState({ enabled: false, prompt: "" });
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState("");
const [debounceTimer, setDebounceTimer] = useState(null);
const t = useTranslations("settings");
useEffect(() => {
fetch("/api/settings/system-prompt")
@@ -57,13 +59,14 @@ export default function SystemPromptTab() {
</span>
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold">Global System Prompt</h3>
<p className="text-sm text-text-muted">Injected into all requests at proxy level</p>
<h3 className="text-lg font-semibold">{t("globalSystemPrompt")}</h3>
<p className="text-sm text-text-muted">{t("systemPromptDesc")}</p>
</div>
<div className="flex items-center gap-3">
{status === "saved" && (
<span className="text-xs font-medium text-emerald-500 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">check_circle</span> Saved
<span className="material-symbols-outlined text-[14px]">check_circle</span>{" "}
{t("saved")}
</span>
)}
<Toggle
@@ -80,7 +83,7 @@ export default function SystemPromptTab() {
<textarea
value={config.prompt}
onChange={(e) => handlePromptChange(e.target.value)}
placeholder="Enter system prompt to inject into all requests..."
placeholder={t("systemPromptPlaceholder")}
rows={5}
className="w-full px-4 py-3 rounded-lg border border-border/50 bg-surface/30 text-sm
placeholder:text-text-muted/50 resize-y min-h-[120px]
@@ -94,8 +97,9 @@ export default function SystemPromptTab() {
</div>
<p className="text-xs text-text-muted/70 flex items-center gap-1.5">
<span className="material-symbols-outlined text-[14px]">info</span>
This prompt is prepended to the system message of every request. Use for global instructions,
safety guidelines, or response formatting rules. Send <code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a request to bypass.
{t("systemPromptHint")} Send{" "}
<code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a
request to bypass.
</p>
</div>
)}

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from "react";
import { Card, Button, Select } from "@/shared/components";
import { useTranslations } from "next-intl";
const MODES = [
{
@@ -46,6 +47,7 @@ export default function ThinkingBudgetTab() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState("");
const t = useTranslations("settings");
useEffect(() => {
fetch("/api/settings/thinking-budget")
@@ -90,12 +92,12 @@ export default function ThinkingBudgetTab() {
</span>
</div>
<div>
<h3 className="text-lg font-semibold">Thinking Budget</h3>
<p className="text-sm text-text-muted">Control AI reasoning token usage across all requests</p>
<h3 className="text-lg font-semibold">{t("thinkingBudgetTitle")}</h3>
<p className="text-sm text-text-muted">{t("thinkingBudgetDesc")}</p>
</div>
{status === "saved" && (
<span className="ml-auto text-xs font-medium text-emerald-500 flex items-center gap-1">
<span className="material-symbols-outlined text-[14px]">check_circle</span> Saved
<span className="material-symbols-outlined text-[14px]">check_circle</span> {t("saved")}
</span>
)}
</div>
@@ -121,7 +123,9 @@ export default function ThinkingBudgetTab() {
{m.icon}
</span>
<div className="min-w-0">
<p className={`text-sm font-medium ${config.mode === m.value ? "text-violet-400" : ""}`}>
<p
className={`text-sm font-medium ${config.mode === m.value ? "text-violet-400" : ""}`}
>
{m.label}
</p>
<p className="text-xs text-text-muted mt-0.5 leading-relaxed">{m.desc}</p>
@@ -134,9 +138,9 @@ export default function ThinkingBudgetTab() {
{config.mode === "custom" && (
<div className="p-4 rounded-lg bg-surface/30 border border-border/30 mb-4">
<div className="flex items-center justify-between mb-3">
<p className="text-sm font-medium">Token Budget</p>
<p className="text-sm font-medium">{t("tokenBudget")}</p>
<span className="text-sm font-mono tabular-nums text-violet-400">
{config.customBudget.toLocaleString()} tokens
{config.customBudget.toLocaleString()} {t("tokens")}
</span>
</div>
<input
@@ -161,10 +165,8 @@ export default function ThinkingBudgetTab() {
{/* Adaptive effort level */}
{config.mode === "adaptive" && (
<div className="p-4 rounded-lg bg-surface/30 border border-border/30">
<p className="text-sm font-medium mb-3">Base Effort Level</p>
<p className="text-xs text-text-muted mb-3">
Adaptive mode scales from this base level based on message count, tool usage, and prompt length.
</p>
<p className="text-sm font-medium mb-3">{t("baseEffortLevel")}</p>
<p className="text-xs text-text-muted mb-3">{t("adaptiveHint")}</p>
<div className="grid grid-cols-4 gap-2">
{EFFORTS.map((e) => (
<button

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import { useSearchParams } from "next/navigation";
import { cn } from "@/shared/utils/cn";
import { APP_CONFIG } from "@/shared/constants/config";
import { useTranslations } from "next-intl";
import SystemStorageTab from "./components/SystemStorageTab";
import SecurityTab from "./components/SecurityTab";
import RoutingTab from "./components/RoutingTab";
@@ -17,15 +18,16 @@ import CacheStatsCard from "./components/CacheStatsCard";
import ResilienceTab from "./components/ResilienceTab";
const tabs = [
{ id: "general", label: "General", icon: "settings" },
{ id: "ai", label: "AI", icon: "smart_toy" },
{ id: "security", label: "Security", icon: "shield" },
{ id: "routing", label: "Routing", icon: "route" },
{ id: "resilience", label: "Resilience", icon: "electrical_services" },
{ id: "advanced", label: "Advanced", icon: "tune" },
{ id: "general", labelKey: "general", icon: "settings" },
{ id: "ai", labelKey: "ai", icon: "smart_toy" },
{ id: "security", labelKey: "security", icon: "shield" },
{ id: "routing", labelKey: "routing", icon: "route" },
{ id: "resilience", labelKey: "resilience", icon: "electrical_services" },
{ id: "advanced", labelKey: "advanced", icon: "tune" },
];
export default function SettingsPage() {
const t = useTranslations("settings");
const searchParams = useSearchParams();
const tabParam = searchParams.get("tab");
const [userSelectedTab, setUserSelectedTab] = useState(null);
@@ -57,13 +59,16 @@ export default function SettingsPage() {
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
{tab.icon}
</span>
<span className="hidden sm:inline">{tab.label}</span>
<span className="hidden sm:inline">{t(tab.labelKey)}</span>
</button>
))}
</div>
{/* Tab contents */}
<div role="tabpanel" aria-label={tabs.find((t) => t.id === activeTab)?.label}>
<div
role="tabpanel"
aria-label={t(tabs.find((t2) => t2.id === activeTab)?.labelKey || "general")}
>
{activeTab === "general" && (
<>
<div className="flex flex-col gap-6">
@@ -100,7 +105,7 @@ export default function SettingsPage() {
<p>
{APP_CONFIG.name} v{APP_CONFIG.version}
</p>
<p className="mt-1">Local Mode All data stored on your machine</p>
<p className="mt-1">{t("localMode")}</p>
</div>
</div>
</div>

View File

@@ -629,7 +629,44 @@
"ipWhitelist": "IP Whitelist",
"ipBlacklist": "IP Blacklist",
"addIP": "Add IP",
"savedSuccessfully": "Settings saved successfully"
"savedSuccessfully": "Settings saved successfully",
"ai": "AI",
"advanced": "Advanced",
"localMode": "Local Mode — All data stored on your machine",
"switchThemes": "Switch between light and dark themes",
"hideHealthLogs": "Hide Health Check Logs",
"hideHealthLogsDesc": "When ON, suppress [HealthCheck] messages in server console",
"promptCache": "Prompt Cache",
"flushCache": "Flush Cache",
"flushing": "Flushing…",
"size": "Size",
"hits": "Hits",
"evictions": "Evictions",
"loadingCacheStats": "Loading cache stats…",
"globalProxy": "Global Proxy",
"globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.",
"noGlobalProxy": "No global proxy configured",
"configure": "Configure",
"globalSystemPrompt": "Global System Prompt",
"systemPromptDesc": "Injected into all requests at proxy level",
"saved": "Saved",
"systemPromptPlaceholder": "Enter system prompt to inject into all requests...",
"systemPromptHint": "This prompt is prepended to the system message of every request. Use for global instructions, safety guidelines, or response formatting rules.",
"chars": "{count} chars",
"thinkingBudgetTitle": "Thinking Budget",
"thinkingBudgetDesc": "Control AI reasoning token usage across all requests",
"passthrough": "Passthrough",
"passthroughDesc": "No changes — client controls thinking budget",
"auto": "Auto",
"autoDesc": "Strip all thinking config — let provider decide",
"custom": "Custom",
"customDesc": "Set a fixed token budget for all requests",
"adaptive": "Adaptive",
"adaptiveDesc": "Scale budget based on request complexity",
"tokenBudget": "Token Budget",
"tokens": "tokens",
"baseEffortLevel": "Base Effort Level",
"adaptiveHint": "Adaptive mode scales from this base level based on message count, tool usage, and prompt length."
},
"translator": {
"title": "Translator",

View File

@@ -629,7 +629,44 @@
"ipWhitelist": "Lista de IPs Permitidos",
"ipBlacklist": "Lista de IPs Bloqueados",
"addIP": "Adicionar IP",
"savedSuccessfully": "Configurações salvas com sucesso"
"savedSuccessfully": "Configurações salvas com sucesso",
"ai": "IA",
"advanced": "Avançado",
"localMode": "Modo Local — Todos os dados armazenados na sua máquina",
"switchThemes": "Alternar entre temas claro e escuro",
"hideHealthLogs": "Ocultar Logs de Health Check",
"hideHealthLogsDesc": "Quando ATIVADO, suprime mensagens [HealthCheck] no console do servidor",
"promptCache": "Cache de Prompt",
"flushCache": "Limpar Cache",
"flushing": "Limpando…",
"size": "Tamanho",
"hits": "Acertos",
"evictions": "Remoções",
"loadingCacheStats": "Carregando estatísticas do cache…",
"globalProxy": "Proxy Global",
"globalProxyDesc": "Configure um proxy de saída global para todas as chamadas de API. Provedores individuais, combos e chaves podem sobrescrever.",
"noGlobalProxy": "Nenhum proxy global configurado",
"configure": "Configurar",
"globalSystemPrompt": "Prompt de Sistema Global",
"systemPromptDesc": "Injetado em todas as requisições no nível do proxy",
"saved": "Salvo",
"systemPromptPlaceholder": "Digite o prompt de sistema para injetar em todas as requisições...",
"systemPromptHint": "Este prompt é adicionado antes da mensagem de sistema de cada requisição. Use para instruções globais, diretrizes de segurança ou regras de formatação.",
"chars": "{count} caracteres",
"thinkingBudgetTitle": "Orçamento de Raciocínio",
"thinkingBudgetDesc": "Controle o uso de tokens de raciocínio da IA em todas as requisições",
"passthrough": "Passagem Direta",
"passthroughDesc": "Sem alterações — cliente controla orçamento de raciocínio",
"auto": "Automático",
"autoDesc": "Remove toda configuração de raciocínio — provedor decide",
"custom": "Personalizado",
"customDesc": "Define um orçamento fixo de tokens para todas as requisições",
"adaptive": "Adaptativo",
"adaptiveDesc": "Escala orçamento baseado na complexidade da requisição",
"tokenBudget": "Orçamento de Tokens",
"tokens": "tokens",
"baseEffortLevel": "Nível de Esforço Base",
"adaptiveHint": "Modo adaptativo escala a partir deste nível base baseado na quantidade de mensagens, uso de ferramentas e tamanho do prompt."
},
"translator": {
"title": "Tradutor",