mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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
This commit is contained in:
@@ -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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Combos</h1>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Create model combos with weighted routing and fallback support
|
||||
</p>
|
||||
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
||||
<p className="text-sm text-text-muted mt-1">{t("description")}</p>
|
||||
</div>
|
||||
<Button icon="add" onClick={() => setShowCreateModal(true)}>
|
||||
Create Combo
|
||||
{t("createCombo")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -218,9 +219,9 @@ export default function CombosPage() {
|
||||
{combos.length === 0 ? (
|
||||
<EmptyState
|
||||
icon="🧩"
|
||||
title="No combos yet"
|
||||
description="Create model combos with weighted routing and fallback support"
|
||||
actionLabel="Create Combo"
|
||||
title={t("noCombosYet")}
|
||||
description={t("description")}
|
||||
actionLabel={t("createCombo")}
|
||||
onAction={() => setShowCreateModal(true)}
|
||||
/>
|
||||
) : (
|
||||
@@ -253,7 +254,7 @@ export default function CombosPage() {
|
||||
setTestResults(null);
|
||||
setTestingCombo(null);
|
||||
}}
|
||||
title={`Test Results — ${testingCombo}`}
|
||||
title={t("testResults", { name: testingCombo })}
|
||||
>
|
||||
<TestResultsView results={testResults} />
|
||||
</Modal>
|
||||
@@ -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 (
|
||||
<Card padding="sm" className={`group ${isDisabled ? "opacity-50" : ""}`}>
|
||||
@@ -346,7 +349,7 @@ function ComboCard({
|
||||
{hasProxy && (
|
||||
<span
|
||||
className="text-[9px] uppercase font-semibold px-1.5 py-0.5 rounded-full bg-primary/15 text-primary flex items-center gap-0.5"
|
||||
title="Proxy configured"
|
||||
title={t("proxyConfigured")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[11px]">vpn_lock</span>
|
||||
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")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied === `combo-${combo.id}` ? "check" : "content_copy"}
|
||||
@@ -369,7 +372,7 @@ function ComboCard({
|
||||
{/* Model tags with weights */}
|
||||
<div className="flex items-center gap-1 mt-0.5 flex-wrap">
|
||||
{models.length === 0 ? (
|
||||
<span className="text-xs text-text-muted italic">No models</span>
|
||||
<span className="text-xs text-text-muted italic">{t("noModels")}</span>
|
||||
) : (
|
||||
models.slice(0, 3).map((entry, index) => {
|
||||
const { model, weight } = normalizeModelEntry(entry);
|
||||
@@ -385,7 +388,9 @@ function ComboCard({
|
||||
})
|
||||
)}
|
||||
{models.length > 3 && (
|
||||
<span className="text-[10px] text-text-muted">+{models.length - 3} more</span>
|
||||
<span className="text-[10px] text-text-muted">
|
||||
{t("more", { count: models.length - 3 })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -394,9 +399,11 @@ function ComboCard({
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-[10px] text-text-muted">
|
||||
<span className="text-emerald-500">{metrics.totalSuccesses}</span>/
|
||||
{metrics.totalRequests} reqs
|
||||
{metrics.totalRequests} {t("reqs")}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted">
|
||||
{metrics.successRate}% {t("success")}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted">{metrics.successRate}% success</span>
|
||||
<span className="text-[10px] text-text-muted">~{metrics.avgLatencyMs}ms</span>
|
||||
{metrics.fallbackRate > 0 && (
|
||||
<span className="text-[10px] text-amber-500">
|
||||
@@ -414,14 +421,14 @@ function ComboCard({
|
||||
size="sm"
|
||||
checked={!isDisabled}
|
||||
onChange={onToggle}
|
||||
title={isDisabled ? "Enable combo" : "Disable combo"}
|
||||
title={isDisabled ? t("enableCombo") : t("disableCombo")}
|
||||
/>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={onTest}
|
||||
disabled={testing}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-emerald-500 transition-colors"
|
||||
title="Test combo"
|
||||
title={t("testCombo")}
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${testing ? "animate-spin" : ""}`}
|
||||
@@ -432,28 +439,28 @@ function ComboCard({
|
||||
<button
|
||||
onClick={onDuplicate}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Duplicate"
|
||||
title={t("duplicate")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onProxy}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Proxy configuration"
|
||||
title={t("proxyConfig")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">vpn_lock</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Edit"
|
||||
title={tc("edit")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">edit</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
|
||||
title="Delete"
|
||||
title={tc("delete")}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
@@ -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 (
|
||||
<>
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={isEdit ? "Edit Combo" : "Create Combo"}>
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={isEdit ? t("editCombo") : t("createCombo")}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<Input
|
||||
label="Combo Name"
|
||||
label={t("comboName")}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
placeholder="my-combo"
|
||||
placeholder={t("comboNamePlaceholder")}
|
||||
error={nameError}
|
||||
/>
|
||||
<p className="text-[10px] text-text-muted mt-0.5">
|
||||
Letters, numbers, -, _, / and . allowed
|
||||
</p>
|
||||
<p className="text-[10px] text-text-muted mt-0.5">{t("nameHint")}</p>
|
||||
</div>
|
||||
|
||||
{/* Strategy Toggle */}
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">Routing Strategy</label>
|
||||
<label className="text-sm font-medium mb-1.5 block">{t("routingStrategy")}</label>
|
||||
<div className="grid grid-cols-3 gap-1 p-0.5 bg-black/5 dark:bg-white/5 rounded-lg">
|
||||
{[
|
||||
{ value: "priority", label: "Priority", icon: "sort" },
|
||||
@@ -788,13 +795,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
{/* Models */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-sm font-medium">Models</label>
|
||||
<label className="text-sm font-medium">{t("models")}</label>
|
||||
{strategy === "weighted" && models.length > 1 && (
|
||||
<button
|
||||
onClick={handleAutoBalance}
|
||||
className="text-[10px] text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
Auto-balance
|
||||
{t("autoBalance")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -804,7 +811,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
<span className="material-symbols-outlined text-text-muted text-xl mb-1">
|
||||
layers
|
||||
</span>
|
||||
<p className="text-xs text-text-muted">No models added yet</p>
|
||||
<p className="text-xs text-text-muted">{t("noModelsYet")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1 max-h-[240px] overflow-y-auto">
|
||||
@@ -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"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">add</span>
|
||||
Add Model
|
||||
{t("addModel")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -912,14 +919,16 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{showAdvanced ? "expand_less" : "expand_more"}
|
||||
</span>
|
||||
Advanced Settings
|
||||
{t("advancedSettings")}
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="flex flex-col gap-2 p-3 bg-black/[0.02] dark:bg-white/[0.02] rounded-lg border border-black/5 dark:border-white/5">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">Max Retries</label>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">
|
||||
{t("maxRetries")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -937,7 +946,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">
|
||||
Retry Delay (ms)
|
||||
{t("retryDelay")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -956,7 +965,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">Timeout (ms)</label>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">{t("timeout")}</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1000"
|
||||
@@ -974,7 +983,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-[10px] text-text-muted">Healthcheck</label>
|
||||
<label className="text-[10px] text-text-muted">{t("healthcheck")}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.healthCheckEnabled !== false}
|
||||
@@ -987,7 +996,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
|
||||
<div>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">
|
||||
Concurrency / Model
|
||||
{t("concurrencyPerModel")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -1006,7 +1015,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] text-text-muted mb-0.5 block">
|
||||
Queue Timeout (ms)
|
||||
{t("queueTimeout")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
@@ -1026,16 +1035,14 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-[10px] text-text-muted">
|
||||
Leave empty to use global defaults. These override per-provider settings.
|
||||
</p>
|
||||
<p className="text-[10px] text-text-muted">{t("advancedHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||
Cancel
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
@@ -1043,7 +1050,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders }) {
|
||||
size="sm"
|
||||
disabled={!name.trim() || !!nameError || saving}
|
||||
>
|
||||
{saving ? "Saving..." : isEdit ? "Save" : "Create"}
|
||||
{saving ? t("saving") : isEdit ? tc("save") : t("createCombo")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user