From 27b9c331b710a8b3b616395eff7d3e683a18a0b6 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 25 Feb 2026 14:03:46 -0300 Subject: [PATCH] feat(i18n): migrate Combos page (1123 lines, 50+ strings) - CombosPage, ComboCard, ComboFormModal, TestResultsView: full migration - Headers, model tags, metrics, strategy descriptions, validation - Form labels, advanced settings, drag-and-drop model list, actions - Expanded combos namespace from 19 to 65 keys in en.json and pt-BR.json --- src/app/(dashboard)/dashboard/combos/page.tsx | 125 +++++++++--------- src/i18n/messages/en.json | 49 ++++++- src/i18n/messages/pt-BR.json | 49 ++++++- 3 files changed, 160 insertions(+), 63 deletions(-) diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index f0ac3c7a5a..8fc3729e3a 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -14,6 +14,7 @@ import { } from "@/shared/components"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; import { useNotificationStore } from "@/store/notificationStore"; +import { useTranslations } from "next-intl"; // Validate combo name: letters, numbers, -, _, /, . const VALID_NAME_REGEX = /^[a-zA-Z0-9_/.-]+$/; @@ -34,6 +35,8 @@ function getModelString(entry) { // Main Page // ───────────────────────────────────────────── export default function CombosPage() { + const t = useTranslations("combos"); + const tc = useTranslations("common"); const [combos, setCombos] = useState([]); const [loading, setLoading] = useState(true); const [showCreateModal, setShowCreateModal] = useState(false); @@ -91,13 +94,13 @@ export default function CombosPage() { if (res.ok) { await fetchData(); setShowCreateModal(false); - notify.success("Combo created successfully"); + notify.success(t("comboCreated")); } else { const err = await res.json(); - notify.error(err.error?.message || err.error || "Failed to create combo"); + notify.error(err.error?.message || err.error || t("failedCreate")); } } catch (error) { - notify.error("Error creating combo"); + notify.error(t("errorCreating")); } }; @@ -111,26 +114,26 @@ export default function CombosPage() { if (res.ok) { await fetchData(); setEditingCombo(null); - notify.success("Combo updated successfully"); + notify.success(t("comboUpdated")); } else { const err = await res.json(); - notify.error(err.error?.message || err.error || "Failed to update combo"); + notify.error(err.error?.message || err.error || t("failedUpdate")); } } catch (error) { - notify.error("Error updating combo"); + notify.error(t("errorUpdating")); } }; const handleDelete = async (id) => { - if (!confirm("Delete this combo?")) return; + if (!confirm(t("deleteConfirm"))) return; try { const res = await fetch(`/api/combos/${id}`, { method: "DELETE" }); if (res.ok) { setCombos(combos.filter((c) => c.id !== id)); - notify.success("Combo deleted"); + notify.success(t("comboDeleted")); } } catch (error) { - notify.error("Error deleting combo"); + notify.error(t("errorDeleting")); } }; @@ -166,8 +169,8 @@ export default function CombosPage() { const data = await res.json(); setTestResults(data); } catch (error) { - setTestResults({ error: "Test request failed" }); - notify.error("Test request failed"); + setTestResults({ error: t("testFailed") }); + notify.error(t("testFailed")); } }; @@ -186,7 +189,7 @@ export default function CombosPage() { setCombos((prev) => prev.map((c) => (c.id === combo.id ? { ...c, isActive: !newActive } : c)) ); - notify.error("Failed to toggle combo"); + notify.error(t("failedToggle")); } }; @@ -204,13 +207,11 @@ export default function CombosPage() { {/* Header */}
-

Combos

-

- Create model combos with weighted routing and fallback support -

+

{t("title")}

+

{t("description")}

@@ -218,9 +219,9 @@ export default function CombosPage() { {combos.length === 0 ? ( setShowCreateModal(true)} /> ) : ( @@ -253,7 +254,7 @@ export default function CombosPage() { setTestResults(null); setTestingCombo(null); }} - title={`Test Results — ${testingCombo}`} + title={t("testResults", { name: testingCombo })} > @@ -313,6 +314,8 @@ function ComboCard({ const strategy = combo.strategy || "priority"; const models = combo.models || []; const isDisabled = combo.isActive === false; + const t = useTranslations("combos"); + const tc = useTranslations("common"); return ( @@ -346,7 +349,7 @@ function ComboCard({ {hasProxy && ( vpn_lock proxy @@ -358,7 +361,7 @@ function ComboCard({ onCopy(combo.name, `combo-${combo.id}`); }} className="p-0.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors opacity-0 group-hover:opacity-100" - title="Copy combo name" + title={t("copyComboName")} > {copied === `combo-${combo.id}` ? "check" : "content_copy"} @@ -369,7 +372,7 @@ function ComboCard({ {/* Model tags with weights */}
{models.length === 0 ? ( - No models + {t("noModels")} ) : ( models.slice(0, 3).map((entry, index) => { const { model, weight } = normalizeModelEntry(entry); @@ -385,7 +388,9 @@ function ComboCard({ }) )} {models.length > 3 && ( - +{models.length - 3} more + + {t("more", { count: models.length - 3 })} + )}
@@ -394,9 +399,11 @@ function ComboCard({
{metrics.totalSuccesses}/ - {metrics.totalRequests} reqs + {metrics.totalRequests} {t("reqs")} + + + {metrics.successRate}% {t("success")} - {metrics.successRate}% success ~{metrics.avgLatencyMs}ms {metrics.fallbackRate > 0 && ( @@ -414,14 +421,14 @@ function ComboCard({ size="sm" checked={!isDisabled} onChange={onToggle} - title={isDisabled ? "Enable combo" : "Disable combo"} + title={isDisabled ? t("enableCombo") : t("disableCombo")} />
@@ -531,6 +538,8 @@ function TestResultsView({ results }) { // Combo Form Modal // ───────────────────────────────────────────── function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { + const t = useTranslations("combos"); + const tc = useTranslations("common"); const [name, setName] = useState(combo?.name || ""); const [models, setModels] = useState(() => { return (combo?.models || []).map((m) => normalizeModelEntry(m)); @@ -575,11 +584,11 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { const validateName = (value) => { if (!value.trim()) { - setNameError("Name is required"); + setNameError(t("nameRequired")); return false; } if (!VALID_NAME_REGEX.test(value)) { - setNameError("Only letters, numbers, -, _, / and . allowed"); + setNameError(t("nameInvalid")); return false; } setNameError(""); @@ -726,25 +735,23 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { return ( <> - +
{/* Name */}
-

- Letters, numbers, -, _, / and . allowed -

+

{t("nameHint")}

{/* Strategy Toggle */}
- +
{[ { value: "priority", label: "Priority", icon: "sort" }, @@ -788,13 +795,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { {/* Models */}
- + {strategy === "weighted" && models.length > 1 && ( )}
@@ -804,7 +811,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { layers -

No models added yet

+

{t("noModelsYet")}

) : (
@@ -900,7 +907,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { className="w-full mt-2 py-2 border border-dashed border-black/10 dark:border-white/10 rounded-lg text-xs text-text-muted hover:text-primary hover:border-primary/30 transition-colors flex items-center justify-center gap-1" > add - Add Model + {t("addModel")}
@@ -912,14 +919,16 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { {showAdvanced ? "expand_less" : "expand_more"} - Advanced Settings + {t("advancedSettings")} {showAdvanced && (
- +
- +
- +
)} -

- Leave empty to use global defaults. These override per-provider settings. -

+

{t("advancedHint")}

)} {/* Actions */}
@@ -1056,7 +1063,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) { onSelect={handleAddModel} activeProviders={activeProviders} modelAliases={modelAliases} - title="Add Model to Combo" + title={t("addModelToCombo")} selectedModel={null} /> diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 3ff00d2d01..a52dd01d4d 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -246,12 +246,14 @@ }, "combos": { "title": "Combos", + "description": "Create model combos with weighted routing and fallback support", "createCombo": "Create Combo", "editCombo": "Edit Combo", "deleteCombo": "Delete Combo", "noModels": "No models", "noModelsYet": "No models added yet", "addModel": "Add Model", + "addModelToCombo": "Add Model to Combo", "routingStrategy": "Routing Strategy", "maxRetries": "Max Retries", "timeout": "Timeout (ms)", @@ -262,8 +264,51 @@ "random": "Random", "leastLatency": "Least Latency", "comboName": "Combo Name", - "comboNamePlaceholder": "e.g. my-smart-combo", - "deleteConfirm": "Are you sure you want to delete this combo?" + "comboNamePlaceholder": "my-combo", + "deleteConfirm": "Delete this combo?", + "noCombosYet": "No combos yet", + "comboCreated": "Combo created successfully", + "comboUpdated": "Combo updated successfully", + "comboDeleted": "Combo deleted", + "failedCreate": "Failed to create combo", + "failedUpdate": "Failed to update combo", + "errorCreating": "Error creating combo", + "errorUpdating": "Error updating combo", + "errorDeleting": "Error deleting combo", + "testFailed": "Test request failed", + "failedToggle": "Failed to toggle combo", + "testResults": "Test Results — {name}", + "resolvedBy": "Resolved by:", + "more": "+{count} more", + "reqs": "reqs", + "success": "success", + "proxyConfigured": "Proxy configured", + "copyComboName": "Copy combo name", + "enableCombo": "Enable combo", + "disableCombo": "Disable combo", + "testCombo": "Test combo", + "duplicate": "Duplicate", + "proxyConfig": "Proxy configuration", + "nameRequired": "Name is required", + "nameInvalid": "Only letters, numbers, -, _, / and . allowed", + "nameHint": "Letters, numbers, -, _, / and . allowed", + "priorityDesc": "Sequential fallback: tries model 1 first, then 2, etc.", + "weightedDesc": "Distributes traffic by weight percentage with fallback", + "roundRobinDesc": "Circular distribution: each request goes to the next model in rotation", + "randomDesc": "Uniform random selection, then fallback to remaining models", + "leastUsedDesc": "Picks the model with fewest requests, balancing load over time", + "costOptimizedDesc": "Routes to the cheapest model first based on pricing", + "models": "Models", + "autoBalance": "Auto-balance", + "advancedSettings": "Advanced Settings", + "retryDelay": "Retry Delay (ms)", + "concurrencyPerModel": "Concurrency / Model", + "queueTimeout": "Queue Timeout (ms)", + "advancedHint": "Leave empty to use global defaults. These override per-provider settings.", + "saving": "Saving...", + "weighted": "Weighted", + "leastUsed": "Least-Used", + "costOpt": "Cost-Opt" }, "costs": { "title": "Costs", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 017e68c758..68feb895df 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -246,12 +246,14 @@ }, "combos": { "title": "Combos", + "description": "Crie combos de modelos com roteamento ponderado e suporte a fallback", "createCombo": "Criar Combo", "editCombo": "Editar Combo", "deleteCombo": "Excluir Combo", "noModels": "Sem modelos", "noModelsYet": "Nenhum modelo adicionado", "addModel": "Adicionar Modelo", + "addModelToCombo": "Adicionar Modelo ao Combo", "routingStrategy": "Estratégia de Roteamento", "maxRetries": "Máximo de Tentativas", "timeout": "Timeout (ms)", @@ -262,8 +264,51 @@ "random": "Aleatório", "leastLatency": "Menor Latência", "comboName": "Nome do Combo", - "comboNamePlaceholder": "ex: meu-combo-inteligente", - "deleteConfirm": "Tem certeza que deseja excluir este combo?" + "comboNamePlaceholder": "meu-combo", + "deleteConfirm": "Excluir este combo?", + "noCombosYet": "Nenhum combo ainda", + "comboCreated": "Combo criado com sucesso", + "comboUpdated": "Combo atualizado com sucesso", + "comboDeleted": "Combo excluído", + "failedCreate": "Falha ao criar combo", + "failedUpdate": "Falha ao atualizar combo", + "errorCreating": "Erro ao criar combo", + "errorUpdating": "Erro ao atualizar combo", + "errorDeleting": "Erro ao excluir combo", + "testFailed": "Requisição de teste falhou", + "failedToggle": "Falha ao alternar combo", + "testResults": "Resultados do Teste \u2014 {name}", + "resolvedBy": "Resolvido por:", + "more": "+{count} mais", + "reqs": "reqs", + "success": "sucesso", + "proxyConfigured": "Proxy configurado", + "copyComboName": "Copiar nome do combo", + "enableCombo": "Ativar combo", + "disableCombo": "Desativar combo", + "testCombo": "Testar combo", + "duplicate": "Duplicar", + "proxyConfig": "Configuração de proxy", + "nameRequired": "Nome é obrigatório", + "nameInvalid": "Apenas letras, números, -, _, / e . permitidos", + "nameHint": "Letras, números, -, _, / e . permitidos", + "priorityDesc": "Fallback sequencial: tenta modelo 1 primeiro, depois 2, etc.", + "weightedDesc": "Distribui tráfego por porcentagem de peso com fallback", + "roundRobinDesc": "Distribuição circular: cada requisição vai para o próximo modelo na rotação", + "randomDesc": "Seleção aleatória uniforme, depois fallback para modelos restantes", + "leastUsedDesc": "Escolhe o modelo com menos requisições, equilibrando carga ao longo do tempo", + "costOptimizedDesc": "Roteia para o modelo mais barato primeiro baseado em preços", + "models": "Modelos", + "autoBalance": "Auto-balancear", + "advancedSettings": "Configurações Avançadas", + "retryDelay": "Intervalo de Tentativa (ms)", + "concurrencyPerModel": "Concorrência / Modelo", + "queueTimeout": "Timeout da Fila (ms)", + "advancedHint": "Deixe vazio para usar padrões globais. Estes substituem configurações por provedor.", + "saving": "Salvando...", + "weighted": "Ponderado", + "leastUsed": "Menos Usado", + "costOpt": "Custo-Otim" }, "costs": { "title": "Custos",