feat(i18n): migrate Settings batch 2 — SecurityTab + RoutingTab (25+ strings)

- SecurityTab: password form, blocked providers, endpoint protection
- RoutingTab: strategy labels, model aliases, sticky limit
- Expanded settings namespace by 40 security/routing keys
This commit is contained in:
diegosouzapw
2026-02-25 14:22:49 -03:00
parent 0ac264b39d
commit 7f34835693
4 changed files with 111 additions and 31 deletions

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from "react";
import { Card, Input, Toggle, Button } from "@/shared/components";
import FallbackChainsEditor from "./FallbackChainsEditor";
import { useTranslations } from "next-intl";
const STRATEGIES = [
{
@@ -34,6 +35,7 @@ export default function RoutingTab() {
const [aliases, setAliases] = useState([]);
const [newPattern, setNewPattern] = useState("");
const [newTarget, setNewTarget] = useState("");
const t = useTranslations("settings");
useEffect(() => {
fetch("/api/settings")
@@ -86,7 +88,7 @@ export default function RoutingTab() {
route
</span>
</div>
<h3 className="text-lg font-semibold">Routing Strategy</h3>
<h3 className="text-lg font-semibold">{t("routingStrategy")}</h3>
</div>
<div className="grid grid-cols-3 gap-2 mb-4" style={{ gridAutoRows: "1fr" }}>
@@ -123,8 +125,8 @@ export default function RoutingTab() {
{settings.fallbackStrategy === "round-robin" && (
<div className="flex items-center justify-between pt-3 border-t border-border/30">
<div>
<p className="text-sm font-medium">Sticky Limit</p>
<p className="text-xs text-text-muted">Calls per account before switching</p>
<p className="text-sm font-medium">{t("stickyLimit")}</p>
<p className="text-xs text-text-muted">{t("stickyLimitDesc")}</p>
</div>
<Input
type="number"
@@ -163,10 +165,8 @@ export default function RoutingTab() {
</span>
</div>
<div>
<h3 className="text-lg font-semibold">Model Aliases</h3>
<p className="text-sm text-text-muted">
Wildcard patterns to remap model names Use * and ?
</p>
<h3 className="text-lg font-semibold">{t("modelAliases")}</h3>
<p className="text-sm text-text-muted">{t("modelAliasesDesc")}</p>
</div>
</div>
@@ -198,7 +198,7 @@ export default function RoutingTab() {
<div className="flex gap-2 items-end">
<div className="flex-1">
<Input
label="Pattern"
label={t("pattern")}
placeholder="claude-sonnet-*"
value={newPattern}
onChange={(e) => setNewPattern(e.target.value)}
@@ -206,14 +206,14 @@ export default function RoutingTab() {
</div>
<div className="flex-1">
<Input
label="Target Model"
label={t("targetModel")}
placeholder="claude-sonnet-4-20250514"
value={newTarget}
onChange={(e) => setNewTarget(e.target.value)}
/>
</div>
<Button size="sm" variant="primary" onClick={addAlias} className="mb-[2px]">
+ Add
{t("add")}
</Button>
</div>
</Card>

View File

@@ -5,6 +5,7 @@ import { Card, Button, Input, Toggle } from "@/shared/components";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import IPFilterSection from "./IPFilterSection";
import SessionInfoCard from "./SessionInfoCard";
import { useTranslations } from "next-intl";
export default function SecurityTab() {
const [settings, setSettings] = useState<any>({ requireLogin: false, hasPassword: false });
@@ -12,6 +13,7 @@ export default function SecurityTab() {
const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" });
const [passStatus, setPassStatus] = useState({ type: "", message: "" });
const [passLoading, setPassLoading] = useState(false);
const t = useTranslations("settings");
useEffect(() => {
fetch("/api/settings")
@@ -64,7 +66,7 @@ export default function SecurityTab() {
const handlePasswordChange = async (e) => {
e.preventDefault();
if (passwords.new !== passwords.confirm) {
setPassStatus({ type: "error", message: "Passwords do not match" });
setPassStatus({ type: "error", message: t("passwordsNoMatch") });
return;
}
@@ -82,13 +84,13 @@ export default function SecurityTab() {
});
const data = await res.json();
if (res.ok) {
setPassStatus({ type: "success", message: "Password updated successfully" });
setPassStatus({ type: "success", message: t("passwordUpdated") });
setPasswords({ current: "", new: "", confirm: "" });
} else {
setPassStatus({ type: "error", message: data.error || "Failed to update password" });
setPassStatus({ type: "error", message: data.error || t("failedUpdatePassword") });
}
} catch {
setPassStatus({ type: "error", message: "An error occurred" });
setPassStatus({ type: "error", message: t("errorOccurred") });
} finally {
setPassLoading(false);
}
@@ -105,15 +107,13 @@ export default function SecurityTab() {
shield
</span>
</div>
<h3 className="text-lg font-semibold">Security</h3>
<h3 className="text-lg font-semibold">{t("security")}</h3>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Require login</p>
<p className="text-sm text-text-muted">
When ON, dashboard requires password. When OFF, access without login.
</p>
<p className="font-medium">{t("requireLogin")}</p>
<p className="text-sm text-text-muted">{t("requireLoginDesc")}</p>
</div>
<Toggle
checked={settings.requireLogin === true}
@@ -128,9 +128,9 @@ export default function SecurityTab() {
>
{settings.hasPassword && (
<Input
label="Current Password"
label={t("currentPassword")}
type="password"
placeholder="Enter current password"
placeholder={t("enterCurrentPassword")}
value={passwords.current}
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
required
@@ -138,17 +138,17 @@ export default function SecurityTab() {
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="New Password"
label={t("newPassword")}
type="password"
placeholder="Enter new password"
placeholder={t("enterNewPassword")}
value={passwords.new}
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
required
/>
<Input
label="Confirm New Password"
label={t("confirmPassword")}
type="password"
placeholder="Confirm new password"
placeholder={t("confirmPasswordPlaceholder")}
value={passwords.confirm}
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
required
@@ -165,7 +165,7 @@ export default function SecurityTab() {
<div className="pt-2">
<Button type="submit" variant="primary" loading={passLoading}>
{settings.hasPassword ? "Update Password" : "Set Password"}
{settings.hasPassword ? t("updatePassword") : t("setPassword")}
</Button>
</div>
</form>
@@ -181,13 +181,13 @@ export default function SecurityTab() {
api
</span>
</div>
<h3 className="text-lg font-semibold">API Endpoint Protection</h3>
<h3 className="text-lg font-semibold">{t("apiEndpointProtection")}</h3>
</div>
<div className="flex flex-col gap-4">
{/* Require auth for /models */}
<div className="flex items-center justify-between">
<div>
<p className="font-medium">Require API key for /models</p>
<p className="font-medium">{t("requireAuthModels")}</p>
<p className="text-sm text-text-muted">
When ON, the{" "}
<code className="text-xs bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded">
@@ -207,7 +207,7 @@ export default function SecurityTab() {
{/* Blocked Providers */}
<div className="pt-4 border-t border-border/50">
<div className="mb-3">
<p className="font-medium">Blocked Providers</p>
<p className="font-medium">{t("blockedProviders")}</p>
<p className="text-sm text-text-muted">
Hide specific providers from the{" "}
<code className="text-xs bg-black/5 dark:bg-white/5 px-1 py-0.5 rounded">

View File

@@ -666,7 +666,47 @@
"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."
"adaptiveHint": "Adaptive mode scales from this base level based on message count, tool usage, and prompt length.",
"requireLogin": "Require login",
"requireLoginDesc": "When ON, dashboard requires password. When OFF, access without login.",
"currentPassword": "Current Password",
"enterCurrentPassword": "Enter current password",
"newPassword": "New Password",
"enterNewPassword": "Enter new password",
"confirmPassword": "Confirm New Password",
"confirmPasswordPlaceholder": "Confirm new password",
"passwordsNoMatch": "Passwords do not match",
"passwordUpdated": "Password updated successfully",
"failedUpdatePassword": "Failed to update password",
"errorOccurred": "An error occurred",
"updatePassword": "Update Password",
"setPassword": "Set Password",
"apiEndpointProtection": "API Endpoint Protection",
"requireAuthModels": "Require API key for /models",
"requireAuthModelsDesc": "When ON, the /v1/models endpoint returns 404 for unauthenticated requests. Prevents model discovery by unauthorized users.",
"blockedProviders": "Blocked Providers",
"blockedProvidersDesc": "Hide specific providers from the /v1/models response. Blocked providers will not appear in model listings.",
"providersBlocked": "{count} provider(s) blocked from /models",
"routingStrategy": "Routing Strategy",
"fillFirst": "Fill First",
"fillFirstDesc": "Use accounts in priority order",
"roundRobin": "Round Robin",
"roundRobinDesc": "Cycle through all accounts",
"p2c": "P2C",
"p2cDesc": "Pick 2 random, use the healthier one",
"random": "Random",
"randomDesc": "Random account each request",
"leastUsed": "Least Used",
"leastUsedDesc": "Pick least recently used account",
"costOpt": "Cost Opt",
"costOptDesc": "Prefer cheapest available account",
"stickyLimit": "Sticky Limit",
"stickyLimitDesc": "Calls per account before switching",
"modelAliases": "Model Aliases",
"modelAliasesDesc": "Wildcard patterns to remap model names • Use * and ?",
"pattern": "Pattern",
"targetModel": "Target Model",
"add": "+ Add"
},
"translator": {
"title": "Translator",

View File

@@ -666,7 +666,47 @@
"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."
"adaptiveHint": "Modo adaptativo escala a partir deste nível base baseado na quantidade de mensagens, uso de ferramentas e tamanho do prompt.",
"requireLogin": "Exigir login",
"requireLoginDesc": "Quando ATIVADO, dashboard exige senha. Quando DESATIVADO, acesso sem login.",
"currentPassword": "Senha Atual",
"enterCurrentPassword": "Digite a senha atual",
"newPassword": "Nova Senha",
"enterNewPassword": "Digite a nova senha",
"confirmPassword": "Confirmar Nova Senha",
"confirmPasswordPlaceholder": "Confirme a nova senha",
"passwordsNoMatch": "As senhas não coincidem",
"passwordUpdated": "Senha atualizada com sucesso",
"failedUpdatePassword": "Falha ao atualizar senha",
"errorOccurred": "Ocorreu um erro",
"updatePassword": "Atualizar Senha",
"setPassword": "Definir Senha",
"apiEndpointProtection": "Proteção de Endpoint da API",
"requireAuthModels": "Exigir chave de API para /models",
"requireAuthModelsDesc": "Quando ATIVADO, o endpoint /v1/models retorna 404 para requisições não autenticadas. Impede descoberta de modelos por usuários não autorizados.",
"blockedProviders": "Provedores Bloqueados",
"blockedProvidersDesc": "Ocultar provedores específicos da resposta /v1/models. Provedores bloqueados não aparecerão nas listagens de modelos.",
"providersBlocked": "{count} provedor(es) bloqueado(s) do /models",
"routingStrategy": "Estratégia de Roteamento",
"fillFirst": "Preencher Primeiro",
"fillFirstDesc": "Usar contas em ordem de prioridade",
"roundRobin": "Round Robin",
"roundRobinDesc": "Ciclar entre todas as contas",
"p2c": "P2C",
"p2cDesc": "Escolher 2 aleatórias, usar a mais saudável",
"random": "Aleatório",
"randomDesc": "Conta aleatória em cada requisição",
"leastUsed": "Menos Usado",
"leastUsedDesc": "Escolher a conta usada menos recentemente",
"costOpt": "Custo Otimizado",
"costOptDesc": "Preferir conta mais barata disponível",
"stickyLimit": "Limite Fixo",
"stickyLimitDesc": "Chamadas por conta antes de trocar",
"modelAliases": "Aliases de Modelo",
"modelAliasesDesc": "Padrões coringa para remapear nomes de modelos • Use * e ?",
"pattern": "Padrão",
"targetModel": "Modelo Alvo",
"add": "+ Adicionar"
},
"translator": {
"title": "Tradutor",