mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
refactor(ui): move remaining outlier text onto i18n (#50312)
* refactor(ui): move remaining outlier text onto i18n Moves 146 English messages that the earlier i18n batches missed into en.json. They live in helpers, zod schemas, label fallbacks and shortcut descriptions that the batch scope script did not count, so no batch picked them up. The English on screen is unchanged. * refactor(ui): keep exported compliance error text in English The compliance results' "Error: ..." text is also written to the CSV export, so it stays a literal. The policy table's unnamed fallback now reads i18n.t inside its helper instead of taking t as a new parameter.
This commit is contained in:
parent
6fa0477d84
commit
6085538b33
26 changed files with 489 additions and 95 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
export const accessGroupCreateSchema = z.object({
|
||||
name: z.string().refine((value) => value.trim() !== "", "Please enter the access group name"),
|
||||
name: z.string().refine((value) => value.trim() !== "", { error: () => i18n.t("accessGroups.form.name.required") }),
|
||||
description: z.string(),
|
||||
modelIds: z.array(z.string()),
|
||||
mcpServerIds: z.array(z.string()),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import i18n from "@/i18n/i18n";
|
|||
import { useZodForm } from "@/lib/forms/useZodForm";
|
||||
|
||||
const budgetShape = {
|
||||
budget_id: z.string().min(1, "Enter a budget ID"),
|
||||
budget_id: z.string().min(1, { error: () => i18n.t("budgets.form.errors.budgetIdRequired") }),
|
||||
tpm_limit: z.number().nullish(),
|
||||
rpm_limit: z.number().nullish(),
|
||||
tpd_limit: z.number().nullish(),
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ export const formatGuardrailMode = (raw: unknown): string => {
|
|||
const { tags, default: fallback } = raw as { tags?: Record<string, unknown>; default?: unknown };
|
||||
const tagged: string[] = tags && typeof tags === "object" ? Object.values(tags).flatMap(toModeArray) : [];
|
||||
const modes: string[] = Array.from(new Set([...toModeArray(fallback), ...tagged]));
|
||||
return modes.length > 0 ? `${modes.join(", ")} (tag-based)` : "";
|
||||
return modes.length > 0 ? i18n.t("guardrails.modeDisplay.tagBased", { modes: modes.join(", ") }) : "";
|
||||
};
|
||||
|
||||
// Resolves the supported modes for the selected provider, falling back to the global list
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string
|
|||
<TabsList>
|
||||
{(["requests", "spend", "tokens"] as const).map((value) => (
|
||||
<TabsTrigger key={value} value={value} className="capitalize">
|
||||
{value}
|
||||
{t(METRIC_LABELS[value])}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [
|
|||
z.looseObject({
|
||||
type: z.literal("choice"),
|
||||
instructions: decisionInstructions,
|
||||
criteria: z.record(z.string(), decisionsJson.nullable()).refine((value) => {
|
||||
const count = Object.keys(value).length;
|
||||
return count >= 1 && count <= 255;
|
||||
}, "Choice criteria must contain between 1 and 255 options."),
|
||||
criteria: z.record(z.string(), decisionsJson.nullable()).refine(
|
||||
(value) => {
|
||||
const count = Object.keys(value).length;
|
||||
return count >= 1 && count <= 255;
|
||||
},
|
||||
{ error: () => i18n.t("playground.systemOne.schemas.choiceOptionCount", { max: 255 }) },
|
||||
),
|
||||
}),
|
||||
z
|
||||
.looseObject({
|
||||
|
|
@ -22,7 +25,7 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [
|
|||
criteria: z.partialRecord(z.enum(["true", "false"]), decisionsJson.nullable()).nullish(),
|
||||
})
|
||||
.refine((value) => value.instructions != null || value.criteria != null, {
|
||||
message: "A yes / no question requires instructions or criteria.",
|
||||
error: () => i18n.t("playground.systemOne.schemas.noulNeedsInstructionsOrCriteria"),
|
||||
}),
|
||||
z.looseObject({
|
||||
type: z.literal("score"),
|
||||
|
|
@ -34,13 +37,16 @@ const decisionQuestionSchema = z.discriminatedUnion("type", [
|
|||
export const decisionsRequestSchema = z.looseObject({
|
||||
model: z
|
||||
.string()
|
||||
.refine((value) => value.trim().length > 0, "Model must be a non-empty string.")
|
||||
.refine((value) => value.trim().length > 0, { error: () => i18n.t("playground.systemOne.schemas.modelNonEmpty") })
|
||||
.optional(),
|
||||
state: decisionsJson,
|
||||
questions: z.record(z.string().min(1), decisionQuestionSchema).refine((value) => {
|
||||
const count = Object.keys(value).length;
|
||||
return count >= 1 && count <= 128;
|
||||
}, "Questions must contain between 1 and 128 entries."),
|
||||
questions: z.record(z.string().min(1), decisionQuestionSchema).refine(
|
||||
(value) => {
|
||||
const count = Object.keys(value).length;
|
||||
return count >= 1 && count <= 128;
|
||||
},
|
||||
{ error: () => i18n.t("playground.systemOne.schemas.questionCount") },
|
||||
),
|
||||
});
|
||||
|
||||
export type DecisionRequest = z.infer<typeof decisionsRequestSchema>;
|
||||
|
|
@ -49,25 +55,32 @@ export type PlaygroundQuestion = SystemOneQuestion | z.infer<typeof decisionQues
|
|||
|
||||
const MAX_CHOICE_OPTIONS = 255;
|
||||
|
||||
const nonEmptyString = (message: string) =>
|
||||
z.string({ error: message }).refine((value) => value.trim().length > 0, message);
|
||||
const nonEmptyString = (messageKey: ParseKeys) =>
|
||||
z
|
||||
.string({ error: () => i18n.t(messageKey) })
|
||||
.refine((value) => value.trim().length > 0, { error: () => i18n.t(messageKey) });
|
||||
|
||||
const instructions = z.string({
|
||||
error: (iss) =>
|
||||
iss.input === undefined ? "Required property 'instructions' is missing." : "Instructions must be a string.",
|
||||
iss.input === undefined
|
||||
? i18n.t("playground.systemOne.schemas.instructionsMissing")
|
||||
: i18n.t("playground.systemOne.schemas.instructionsType"),
|
||||
});
|
||||
|
||||
const choiceQuestionSchema = z.looseObject({
|
||||
type: z.literal("choice"),
|
||||
instructions,
|
||||
criteria: z
|
||||
.record(z.string(), z.string({ error: "Choice descriptions must be strings." }), {
|
||||
error: "Choice criteria must be an object mapping labels to descriptions.",
|
||||
.record(z.string(), z.string({ error: () => i18n.t("playground.systemOne.schemas.choiceDescriptionType") }), {
|
||||
error: () => i18n.t("playground.systemOne.schemas.choiceCriteriaType"),
|
||||
})
|
||||
.refine((criteria) => {
|
||||
const count = Object.keys(criteria).length;
|
||||
return count >= 1 && count <= MAX_CHOICE_OPTIONS;
|
||||
}, `Choice criteria must contain between 1 and ${MAX_CHOICE_OPTIONS} options.`),
|
||||
.refine(
|
||||
(criteria) => {
|
||||
const count = Object.keys(criteria).length;
|
||||
return count >= 1 && count <= MAX_CHOICE_OPTIONS;
|
||||
},
|
||||
{ error: () => i18n.t("playground.systemOne.schemas.choiceOptionCount", { max: MAX_CHOICE_OPTIONS }) },
|
||||
),
|
||||
});
|
||||
|
||||
const noulQuestionSchema = z.looseObject({
|
||||
|
|
@ -76,10 +89,10 @@ const noulQuestionSchema = z.looseObject({
|
|||
criteria: z
|
||||
.looseObject(
|
||||
{
|
||||
true: z.string({ error: "Noul true criteria must be a string." }).optional(),
|
||||
false: z.string({ error: "Noul false criteria must be a string." }).optional(),
|
||||
true: z.string({ error: () => i18n.t("playground.systemOne.schemas.noulTrueType") }).optional(),
|
||||
false: z.string({ error: () => i18n.t("playground.systemOne.schemas.noulFalseType") }).optional(),
|
||||
},
|
||||
{ error: "Noul criteria, when provided, must be an object." },
|
||||
{ error: () => i18n.t("playground.systemOne.schemas.noulCriteriaType") },
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
|
@ -88,10 +101,10 @@ const scoreQuestionSchema = z.looseObject({
|
|||
type: z.literal("score"),
|
||||
instructions,
|
||||
criteria: z
|
||||
.array(z.string({ error: "Score levels must be strings." }), {
|
||||
error: "Score criteria must be an array of levels.",
|
||||
.array(z.string({ error: () => i18n.t("playground.systemOne.schemas.scoreLevelType") }), {
|
||||
error: () => i18n.t("playground.systemOne.schemas.scoreCriteriaType"),
|
||||
})
|
||||
.min(2, "Score criteria must contain at least 2 levels."),
|
||||
.min(2, { error: () => i18n.t("playground.systemOne.schemas.scoreLevelCount") }),
|
||||
});
|
||||
|
||||
const QUESTION_ERROR_MESSAGES: Partial<Record<z.core.$ZodIssue["code"], ParseKeys>> = {
|
||||
|
|
@ -108,18 +121,20 @@ const questionSchema = z.discriminatedUnion("type", [choiceQuestionSchema, noulQ
|
|||
|
||||
export const systemOneRequestSchema = z.looseObject(
|
||||
{
|
||||
model: nonEmptyString("Model must be a non-empty string.").optional(),
|
||||
state: z.unknown().nonoptional({ error: "Required property 'state' is missing." }),
|
||||
model: nonEmptyString("playground.systemOne.schemas.modelNonEmpty").optional(),
|
||||
state: z.unknown().nonoptional({ error: () => i18n.t("playground.systemOne.schemas.stateMissing") }),
|
||||
questions: z
|
||||
.record(z.string(), questionSchema, {
|
||||
error: (iss) =>
|
||||
iss.input === undefined
|
||||
? "Required property 'questions' is missing."
|
||||
: "Questions must be a non-empty object.",
|
||||
? i18n.t("playground.systemOne.schemas.questionsMissing")
|
||||
: i18n.t("playground.systemOne.schemas.questionsType"),
|
||||
})
|
||||
.refine((questions) => Object.keys(questions).length > 0, "At least one question is required."),
|
||||
.refine((questions) => Object.keys(questions).length > 0, {
|
||||
error: () => i18n.t("playground.systemOne.schemas.questionRequired"),
|
||||
}),
|
||||
},
|
||||
{ error: "Payload must be a JSON object." },
|
||||
{ error: () => i18n.t("playground.systemOne.schemas.payloadType") },
|
||||
);
|
||||
|
||||
const probability = z.number().finite().min(0).max(1);
|
||||
|
|
@ -147,7 +162,7 @@ const scoreAnswerSchema = z.looseObject(scoreAnswerShape);
|
|||
const tokenCount = z.number().finite().nonnegative();
|
||||
|
||||
export const systemOneResponseSchema = z.looseObject({
|
||||
model: nonEmptyString("Model must be a non-empty string.").nullish(),
|
||||
model: nonEmptyString("playground.systemOne.schemas.modelNonEmpty").nullish(),
|
||||
answers: z.record(
|
||||
z.string(),
|
||||
z.discriminatedUnion("type", [noulAnswerSchema, choiceAnswerSchema, scoreAnswerSchema]),
|
||||
|
|
|
|||
|
|
@ -6,16 +6,18 @@ import React, { useMemo, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
import i18n from "@/i18n/i18n";
|
||||
import { Policy } from "@/components/policies/types";
|
||||
|
||||
import { getPolicyTableColumns, PolicyRow } from "./PolicyTableColumns";
|
||||
|
||||
/** One row per DB policy name plus one row per config policy, so a config policy never hides same-named DB versions; primaryPolicy is used for display and for Edit (FlowBuilder loads all versions) */
|
||||
function groupPoliciesByName(policies: Policy[]): PolicyRow[] {
|
||||
const unnamed = i18n.t("policies.policyTable.unnamed");
|
||||
const dbPolicies = policies.filter((policy) => policy.definition_location !== "config");
|
||||
const names = Array.from(new Set(dbPolicies.map((policy) => policy.policy_name || "(unnamed)")));
|
||||
const names = Array.from(new Set(dbPolicies.map((policy) => policy.policy_name || unnamed)));
|
||||
const dbRows = names.map((policyName) => {
|
||||
const versions = dbPolicies.filter((policy) => (policy.policy_name || "(unnamed)") === policyName);
|
||||
const versions = dbPolicies.filter((policy) => (policy.policy_name || unnamed) === policyName);
|
||||
const primary =
|
||||
versions.find((version) => version.version_status === "production") ??
|
||||
[...versions].sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0))[0];
|
||||
|
|
@ -23,7 +25,7 @@ function groupPoliciesByName(policies: Policy[]): PolicyRow[] {
|
|||
});
|
||||
const configRows = policies
|
||||
.filter((policy) => policy.definition_location === "config")
|
||||
.map((policy) => ({ policy_name: policy.policy_name || "(unnamed)", primaryPolicy: policy, versionCount: 1 }));
|
||||
.map((policy) => ({ policy_name: policy.policy_name || unnamed, primaryPolicy: policy, versionCount: 1 }));
|
||||
return [...dbRows, ...configRows];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const repeatsEarlierValue = (values: readonly string[], index: number): boolean
|
|||
values[index] !== "" && values.indexOf(values[index]) !== index;
|
||||
|
||||
const modelLimitSchema = z.object({
|
||||
model: z.string().min(1, "Missing model"),
|
||||
model: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingModel") }),
|
||||
tpm: z.number().optional(),
|
||||
rpm: z.number().optional(),
|
||||
itpm: z.number().optional(),
|
||||
|
|
@ -17,11 +17,15 @@ const modelLimitSchema = z.object({
|
|||
|
||||
export const projectFormSchema = z
|
||||
.object({
|
||||
project_alias: z.string().min(1, "Please enter a project name"),
|
||||
project_alias: z.string().min(1, { error: () => i18n.t("projects.form.errors.projectNameRequired") }),
|
||||
team_id: z
|
||||
.string()
|
||||
.nullable()
|
||||
.pipe(z.string({ error: "Please select a team" }).min(1, "Please select a team")),
|
||||
.pipe(
|
||||
z
|
||||
.string({ error: () => i18n.t("projects.form.errors.teamRequired") })
|
||||
.min(1, { error: () => i18n.t("projects.form.errors.teamRequired") }),
|
||||
),
|
||||
description: z.string().optional(),
|
||||
models: z.array(z.string()),
|
||||
max_budget: z.number().nullish(),
|
||||
|
|
@ -31,8 +35,8 @@ export const projectFormSchema = z
|
|||
metadata: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().min(1, "Missing key"),
|
||||
value: z.string().min(1, "Missing value"),
|
||||
key: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingKey") }),
|
||||
value: z.string().min(1, { error: () => i18n.t("projects.form.errors.missingValue") }),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { ParseKeys } from "i18next";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -7,9 +8,18 @@ import { toast } from "@/lib/toast";
|
|||
import ExportFormatSelector from "./ExportFormatSelector";
|
||||
import ExportSummary from "./ExportSummary";
|
||||
import ExportTypeSelector from "./ExportTypeSelector";
|
||||
import type { EntityUsageExportModalProps, ExportFormat, ExportType } from "./types";
|
||||
import type { EntityType, EntityUsageExportModalProps, ExportFormat, ExportType } from "./types";
|
||||
import { downloadBlob, exportFilename } from "./utils";
|
||||
|
||||
const ENTITY_LABEL_KEYS: Record<EntityType, ParseKeys> = {
|
||||
user: "usage.export.modal.entities.user",
|
||||
team: "usage.export.modal.entities.team",
|
||||
tag: "usage.export.modal.entities.tag",
|
||||
organization: "usage.export.modal.entities.organization",
|
||||
customer: "usage.export.modal.entities.customer",
|
||||
agent: "usage.export.modal.entities.agent",
|
||||
};
|
||||
|
||||
const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
|
@ -24,7 +34,7 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
|||
const [exportType, setExportType] = useState<ExportType>("daily");
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
||||
const entityLabel = t(ENTITY_LABEL_KEYS[entityType]);
|
||||
const modalTitle = customTitle || t("usage.export.modal.title", { entity: entityLabel });
|
||||
|
||||
const handleExport = async () => {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export const NotificationsBellView: React.FC<NotificationsBellViewProps> = ({
|
|||
? t("shell.notifications.bell.triggerUnread", { unread: unreadCount })
|
||||
: t("shell.notifications.bell.trigger");
|
||||
if (openDeprecations > 0) {
|
||||
label = `Notifications, ${openDeprecations} model deprecation ${openDeprecations === 1 ? "alert needs" : "alerts need"} attention`;
|
||||
label = t("shell.notifications.bell.triggerDeprecations", { count: openDeprecations });
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -166,12 +166,14 @@ export const NotificationsBellView: React.FC<NotificationsBellViewProps> = ({
|
|||
<>
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold">Model deprecations</p>
|
||||
<p className="text-sm font-semibold">{t("shell.notifications.bell.deprecationsTitle")}</p>
|
||||
<a className="text-xs text-primary hover:underline" href={migrationHref()}>
|
||||
View all {openDeprecations}
|
||||
{t("shell.notifications.bell.viewAllDeprecations", { total: openDeprecations })}
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">Model migrations across your keys and teams.</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{t("shell.notifications.bell.deprecationsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
{openAlerts.slice(0, 3).map((alert) => (
|
||||
<Fragment key={alert.id}>
|
||||
|
|
@ -180,12 +182,14 @@ export const NotificationsBellView: React.FC<NotificationsBellViewProps> = ({
|
|||
</Fragment>
|
||||
))}
|
||||
<Separator />
|
||||
<p className="text-xs font-medium text-muted-foreground">Announcements</p>
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
{t("shell.notifications.bell.announcementsHeading")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{deprecationAlerts && openAlerts.length === 0 && (
|
||||
<a href={migrationHref()} className="text-sm font-medium text-primary hover:underline">
|
||||
View model deprecations
|
||||
{t("shell.notifications.bell.viewDeprecations")}
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ import {
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
|
||||
|
||||
const RESPONSE_CACHE_TOOLTIP =
|
||||
"This response was replayed from LiteLLM's response cache. The request never reached the provider, so it did not read from or write to the provider's own prompt cache.";
|
||||
|
||||
export interface TokenUsage {
|
||||
completionTokens?: number;
|
||||
promptTokens?: number;
|
||||
|
|
@ -58,12 +55,13 @@ function MetricItem({ label, tooltip, icon, value }: MetricItemProps) {
|
|||
}
|
||||
|
||||
function ResponseCacheIndicator() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<MetricItem
|
||||
label="Response Cache"
|
||||
tooltip={RESPONSE_CACHE_TOOLTIP}
|
||||
label={t("chatUi.responseMetrics.labels.responseCache")}
|
||||
tooltip={t("chatUi.responseMetrics.tooltips.responseCache")}
|
||||
icon={<History className="size-3" aria-hidden="true" />}
|
||||
value="Hit"
|
||||
value={t("chatUi.responseMetrics.labels.responseCacheHit")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -81,7 +79,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
|
|||
<>
|
||||
{readTokens > 0 && (
|
||||
<MetricItem
|
||||
label="Cache Read"
|
||||
label={t("chatUi.responseMetrics.labels.cacheRead")}
|
||||
tooltip={t(PROMPT_CACHE_READ_TOOLTIP)}
|
||||
icon={<Database className="size-3" aria-hidden="true" />}
|
||||
value={String(readTokens)}
|
||||
|
|
@ -90,7 +88,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
|
|||
|
||||
{creationTokens > 0 && (
|
||||
<MetricItem
|
||||
label="Cache Write"
|
||||
label={t("chatUi.responseMetrics.labels.cacheWrite")}
|
||||
tooltip={t(PROMPT_CACHE_CREATION_TOOLTIP)}
|
||||
icon={<DatabaseBackup className="size-3" aria-hidden="true" />}
|
||||
value={String(creationTokens)}
|
||||
|
|
@ -108,7 +106,7 @@ const ResponseMetrics: React.FC<ResponseMetricsProps> = ({ timeToFirstToken, tot
|
|||
<div className="response-metrics mt-2 flex flex-wrap gap-3 border-t border-border pt-2 text-xs text-muted-foreground">
|
||||
{timeToFirstToken !== undefined && (
|
||||
<MetricItem
|
||||
label="TTFT"
|
||||
label={t("chatUi.responseMetrics.labels.timeToFirstToken")}
|
||||
tooltip={t("chatUi.responseMetrics.tooltips.timeToFirstToken")}
|
||||
icon={<Clock className="size-3" aria-hidden="true" />}
|
||||
value={`${(timeToFirstToken / 1000).toFixed(2)}s`}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe("ModelAliasManager dark-mode tokens", () => {
|
|||
|
||||
it("reads the component source it is asserting about", () => {
|
||||
expect(source.length).toBeGreaterThan(0);
|
||||
expect(source).toContain("Manage Existing Aliases");
|
||||
expect(source).toContain("modelAliasManager.manageExistingTitle");
|
||||
});
|
||||
|
||||
it("uses semantic colour tokens and no hardcoded palette classes", () => {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ const ModelAliasManager: React.FC<ModelAliasManagerProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-sm font-medium text-foreground">Manage Existing Aliases</p>
|
||||
<p className="mb-2 text-sm font-medium text-foreground">{t("modelAliasManager.manageExistingTitle")}</p>
|
||||
<div className="relative mb-6 rounded-lg border">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="[&_td]:py-0.5 [&_th]:py-1">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
const sharedShape = {
|
||||
auto_router_name: z.string().min(1, "Auto router name is required"),
|
||||
auto_router_name: z
|
||||
.string()
|
||||
.min(1, { error: () => i18n.t("editAutoRouter.editAutoRouterModal.errors.nameRequired") }),
|
||||
model_access_group: z.array(z.string()),
|
||||
};
|
||||
|
||||
|
|
@ -22,11 +26,19 @@ const semanticRouterShape = {
|
|||
auto_router_default_model: z
|
||||
.string()
|
||||
.nullable()
|
||||
.pipe(z.string({ error: "Default model is required" }).min(1, "Default model is required")),
|
||||
.pipe(
|
||||
z
|
||||
.string({ error: () => i18n.t("editAutoRouter.editAutoRouterModal.errors.defaultModelRequired") })
|
||||
.min(1, { error: () => i18n.t("editAutoRouter.editAutoRouterModal.errors.defaultModelRequired") }),
|
||||
),
|
||||
auto_router_embedding_model: z
|
||||
.string()
|
||||
.nullable()
|
||||
.pipe(z.string({ error: "Embedding model is required" }).min(1, "Embedding model is required")),
|
||||
.pipe(
|
||||
z
|
||||
.string({ error: () => i18n.t("editAutoRouter.editAutoRouterModal.errors.embeddingModelRequired") })
|
||||
.min(1, { error: () => i18n.t("editAutoRouter.editAutoRouterModal.errors.embeddingModelRequired") }),
|
||||
),
|
||||
};
|
||||
|
||||
export const complexityRouterSchema = z.object(complexityRouterShape);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const EmailEventSettings: React.FC<EmailEventSettingsProps> = ({ accessToken })
|
|||
// Helper function to get a description for each event type
|
||||
const getEventDescription = (event: EmailEvent): string => {
|
||||
if (event === "Model Deprecations") {
|
||||
return "Email affected key owners and team admins 30 days and 1 day before model deprecation. Uses requests from the last 30 days. Off by default.";
|
||||
return t("loggingAndAlerts.emailEvents.descriptions.modelDeprecations");
|
||||
}
|
||||
// Convert event name to a sentence with more context
|
||||
if (event.includes("Virtual Key Created")) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const REQUIRED_MARKER = (
|
|||
);
|
||||
|
||||
const FIELD_HELP: Record<string, React.ReactNode> = {
|
||||
PROXY_BASE_URL: "Public HTTP(S) gateway URL. Required for model deprecation emails so owners can review their usage.",
|
||||
PROXY_BASE_URL: <Trans i18nKey="loggingAndAlerts.emailSettings.help.proxyBaseUrl" />,
|
||||
SMTP_HOST: (
|
||||
<>
|
||||
<Trans i18nKey="loggingAndAlerts.emailSettings.help.smtpHost" />
|
||||
|
|
@ -155,12 +155,14 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
✨ {key}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-sm">{key === "PROXY_BASE_URL" ? "Gateway URL" : key}</p>
|
||||
<p className="text-sm">
|
||||
{key === "PROXY_BASE_URL" ? t("loggingAndAlerts.emailSettings.gatewayUrl") : key}
|
||||
</p>
|
||||
)}
|
||||
<InputGroup className="max-w-100">
|
||||
<InputGroupInput
|
||||
name={key}
|
||||
aria-label={key === "PROXY_BASE_URL" ? "Gateway URL" : key}
|
||||
aria-label={key === "PROXY_BASE_URL" ? t("loggingAndAlerts.emailSettings.gatewayUrl") : key}
|
||||
defaultValue={value as string}
|
||||
type={isSensitive && !isVisible ? "password" : "text"}
|
||||
disabled={isLocked}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ function copyAnswerNode(source: Node, target: Document): Node | null {
|
|||
}
|
||||
|
||||
export function createAriaAnswerDocument(question: string, answerElement: HTMLElement): Document {
|
||||
const report = answerElement.ownerDocument.implementation.createHTMLDocument(`${ARIA_AGENT_LABEL} answer`);
|
||||
const report = answerElement.ownerDocument.implementation.createHTMLDocument(
|
||||
i18n.t("aria.answer.exportDocumentTitle", { agent: ARIA_AGENT_LABEL }),
|
||||
);
|
||||
report.documentElement.lang = "en";
|
||||
const policy = report.createElement("meta");
|
||||
policy.httpEquiv = "Content-Security-Policy";
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
|
|||
<div className="flex items-center border-b border-border bg-muted px-3 py-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
<p className="m-0 px-3 py-3 text-xs text-muted-foreground italic">N/A</p>
|
||||
<p className="m-0 px-3 py-3 text-xs text-muted-foreground italic">{t("logs.audit.drawer.notAvailable")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -136,7 +136,8 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
|
|||
<div className="space-y-1 px-3 py-3 text-xs">
|
||||
{value.token !== undefined && (
|
||||
<p>
|
||||
<span className="text-muted-foreground">{t("logs.audit.drawer.token")}</span> {value.token ?? "N/A"}
|
||||
<span className="text-muted-foreground">{t("logs.audit.drawer.token")}</span>{" "}
|
||||
{value.token ?? t("logs.audit.drawer.notAvailable")}
|
||||
</p>
|
||||
)}
|
||||
{value.spend !== undefined && (
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
ResponsePayload,
|
||||
ToolCall,
|
||||
} from "./prettyMessagesTypes";
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
|
|
@ -155,11 +156,11 @@ const parseContentPart = (part: unknown): string => {
|
|||
return asString(part.refusal);
|
||||
case "image_url":
|
||||
case "input_image":
|
||||
return "[Image]";
|
||||
return i18n.t("logs.detail.prettyMessages.imagePlaceholder");
|
||||
case "input_file":
|
||||
return "[File]";
|
||||
return i18n.t("logs.detail.prettyMessages.filePlaceholder");
|
||||
case "input_audio":
|
||||
return "[Audio]";
|
||||
return i18n.t("logs.detail.prettyMessages.audioPlaceholder");
|
||||
default:
|
||||
return JSON.stringify(part);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,7 +218,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
<div className="border rounded-lg p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<KV label={t("logs.detail.bedrock.action")}>{chip(response.action ?? "N/A", actionTone)}</KV>
|
||||
<KV label={t("logs.detail.bedrock.action")}>
|
||||
{chip(response.action ?? t("logs.detail.bedrock.notAvailable"), actionTone)}
|
||||
</KV>
|
||||
{response.actionReason && <KV label={t("logs.detail.bedrock.actionReason")}>{response.actionReason}</KV>}
|
||||
{response.blockedResponse && (
|
||||
<KV label={t("logs.detail.bedrock.blockedResponse")}>
|
||||
|
|
@ -293,7 +295,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{assess.wordPolicy.customWords!.map((w, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-muted rounded-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(w.action ?? "N/A", w.detected ? "red" : "slate")}
|
||||
{chip(w.action ?? t("logs.detail.bedrock.notAvailable"), w.detected ? "red" : "slate")}
|
||||
<span className="font-mono text-sm break-all">{w.match}</span>
|
||||
</div>
|
||||
{boolPill(w.detected)}
|
||||
|
|
@ -308,7 +310,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{assess.wordPolicy.managedWordLists!.map((w, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-muted rounded-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(w.action ?? "N/A", w.detected ? "red" : "slate")}
|
||||
{chip(w.action ?? t("logs.detail.bedrock.notAvailable"), w.detected ? "red" : "slate")}
|
||||
<span className="font-mono text-sm break-all">{w.match}</span>
|
||||
{w.type && chip(w.type, "slate")}
|
||||
</div>
|
||||
|
|
@ -393,7 +395,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{assess.sensitiveInformationPolicy.piiEntities!.map((p, i) => (
|
||||
<div key={i} className="flex justify-between items-center p-2 bg-muted rounded-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(p.action ?? "N/A", p.detected ? "red" : "slate")}
|
||||
{chip(p.action ?? t("logs.detail.bedrock.notAvailable"), p.detected ? "red" : "slate")}
|
||||
{p.type && chip(p.type, "slate")}
|
||||
<span className="font-mono text-xs break-all">{p.match}</span>
|
||||
</div>
|
||||
|
|
@ -412,7 +414,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-2 bg-muted rounded-sm gap-1"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(r.action ?? "N/A", r.detected ? "red" : "slate")}
|
||||
{chip(r.action ?? t("logs.detail.bedrock.notAvailable"), r.detected ? "red" : "slate")}
|
||||
<span className="font-medium">{r.name ?? "regex"}</span>
|
||||
<span className="font-mono text-xs break-all">{r.regex}</span>
|
||||
</div>
|
||||
|
|
@ -436,7 +438,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
|
|||
{assess.topicPolicy.topics!.map((t, i) => (
|
||||
<div key={i} className="px-3 py-1.5 bg-muted rounded-md text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
{chip(t.action ?? "N/A", t.detected ? "red" : "slate")}
|
||||
{chip(t.action ?? i18n.t("logs.detail.bedrock.notAvailable"), t.detected ? "red" : "slate")}
|
||||
<span className="font-medium">{t.name ?? "topic"}</span>
|
||||
{t.type && chip(t.type, "slate")}
|
||||
{boolPill(t.detected)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useShortcut } from "@/components/shared/useShortcut";
|
||||
|
||||
import { LogEntry } from "../types";
|
||||
|
|
@ -18,6 +20,7 @@ export function useKeyboardNavigation({
|
|||
onClose,
|
||||
onSelectLog,
|
||||
}: UseKeyboardNavigationProps) {
|
||||
const { t } = useTranslation();
|
||||
const index = currentLog ? allLogs.findIndex((log) => log.request_id === currentLog.request_id) : -1;
|
||||
const selectAt = (next: number) => {
|
||||
const log = allLogs[next];
|
||||
|
|
@ -28,8 +31,8 @@ export function useKeyboardNavigation({
|
|||
if (index > 0) selectAt(index - 1);
|
||||
};
|
||||
const modal = { layer: "modal", enabled: isOpen } as const;
|
||||
useShortcut("escape", onClose, { ...modal, description: "close" });
|
||||
useShortcut("j", selectNextLog, { ...modal, description: "log" });
|
||||
useShortcut("k", selectPreviousLog, { ...modal, description: "log" });
|
||||
useShortcut("escape", onClose, { ...modal, description: t("logs.detail.drawer.shortcuts.close") });
|
||||
useShortcut("j", selectNextLog, { ...modal, description: t("logs.detail.drawer.shortcuts.log") });
|
||||
useShortcut("k", selectPreviousLog, { ...modal, description: t("logs.detail.drawer.shortcuts.log") });
|
||||
return { selectNextLog, selectPreviousLog };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ export const getAvailablePages = (): PageMetadata[] => {
|
|||
page: item.page,
|
||||
label: labelText(item),
|
||||
group: groupText(group.groupLabel),
|
||||
description: pageDescriptions[item.page] ? i18n.t(pageDescriptions[item.page]) : "No description available",
|
||||
description: pageDescriptions[item.page]
|
||||
? i18n.t(pageDescriptions[item.page])
|
||||
: i18n.t("adminPanel.pageVisibility.noDescription"),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +66,7 @@ export const getAvailablePages = (): PageMetadata[] => {
|
|||
group: `${groupText(group.groupLabel)} > ${parentLabel}`,
|
||||
description: pageDescriptions[child.page]
|
||||
? i18n.t(pageDescriptions[child.page])
|
||||
: "No description available",
|
||||
: i18n.t("adminPanel.pageVisibility.noDescription"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ function Root<T>({
|
|||
onFullScreenChange,
|
||||
children,
|
||||
}: InspectorRootProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const parent = useContext(InspectorContext) as InspectorState<never> | null;
|
||||
const [claims, setClaims] = useState(0);
|
||||
const claim = useCallback(() => {
|
||||
|
|
@ -111,7 +112,7 @@ function Root<T>({
|
|||
};
|
||||
const close = () => onSelectedChange(null);
|
||||
const toggle = (item: T) => onSelectedChange(itemKey(item) === selectedKey ? null : item);
|
||||
useShortcut("escape", close, { enabled: keys, description: "close" });
|
||||
useShortcut("escape", close, { enabled: keys, description: t("shared.inspector.shortcutClose") });
|
||||
useShortcut("j", () => step(1), { enabled: keys, description: noun });
|
||||
useShortcut("k", () => step(-1), { enabled: keys, description: noun });
|
||||
const state: InspectorState<T> = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import * as React from "react";
|
||||
import type { TooltipContentProps, TooltipValueType } from "recharts";
|
||||
|
||||
import i18n from "@/i18n/i18n";
|
||||
|
||||
export type ChartTooltipProps = Pick<
|
||||
TooltipContentProps<TooltipValueType, string | number>,
|
||||
"active" | "payload" | "label"
|
||||
|
|
@ -62,7 +64,7 @@ const rawMetricValue = (row: unknown, dataKey: string): number | undefined => {
|
|||
};
|
||||
|
||||
const formatMetricValue = (rawValue: number | undefined, isSpend: boolean): string => {
|
||||
if (rawValue === undefined) return "N/A";
|
||||
if (rawValue === undefined) return i18n.t("shared.charts.chartTooltip.notAvailable");
|
||||
if (isSpend) return `$${rawValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
return rawValue.toLocaleString();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ export const COMPLIANCE_FRAMEWORK_LABEL_KEYS: Readonly<Record<string, ParseKeys>
|
|||
"EU AI Act": "compliancePrompts.frameworks.euAiAct",
|
||||
GDPR: "compliancePrompts.frameworks.gdpr",
|
||||
"Airline Brand Protection": "compliancePrompts.frameworks.airlineBrandProtection",
|
||||
"Topic Blocking": "compliancePrompts.frameworks.topicBlocking",
|
||||
"Canadian PII (PIPEDA)": "compliancePrompts.frameworks.canadianPiiPipeda",
|
||||
"Canadian PII (FIPPA)": "compliancePrompts.frameworks.canadianPiiFippa",
|
||||
"Code Execution Safety": "compliancePrompts.frameworks.codeExecutionSafety",
|
||||
"Claims Assistant": "compliancePrompts.frameworks.claimsAssistant",
|
||||
};
|
||||
|
||||
export const COMPLIANCE_CATEGORY_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
|
||||
|
|
@ -52,6 +57,46 @@ export const COMPLIANCE_CATEGORY_LABEL_KEYS: Readonly<Record<string, ParseKeys>>
|
|||
"Destination Intent": "compliancePrompts.categories.destinationIntent.name",
|
||||
"Competitor Comparison": "compliancePrompts.categories.competitorComparison.name",
|
||||
"Ambiguous Entity": "compliancePrompts.categories.ambiguousEntity.name",
|
||||
"Insults & Personal Attacks": "compliancePrompts.categories.insultsAndPersonalAttacks.name",
|
||||
"Financial Advice": "compliancePrompts.categories.financialAdvice.name",
|
||||
"Direct Execution Request": "compliancePrompts.categories.directExecutionRequest.name",
|
||||
"Filesystem Access": "compliancePrompts.categories.filesystemAccess.name",
|
||||
"Network Access": "compliancePrompts.categories.networkAccess.name",
|
||||
"Shelling Out / Subprocess": "compliancePrompts.categories.shellingOutSubprocess.name",
|
||||
"Dynamic Eval / Runtime Execution": "compliancePrompts.categories.dynamicEvalRuntimeExecution.name",
|
||||
"Sandbox Escape / Privilege Escalation": "compliancePrompts.categories.sandboxEscapePrivilegeEscalation.name",
|
||||
"Social Engineering Execution": "compliancePrompts.categories.socialEngineeringExecution.name",
|
||||
"Static Analysis Allowed": "compliancePrompts.categories.staticAnalysisAllowed.name",
|
||||
"Code Generation Without Running": "compliancePrompts.categories.codeGenerationWithoutRunning.name",
|
||||
"Ambiguous Runtime Claim": "compliancePrompts.categories.ambiguousRuntimeClaim.name",
|
||||
"Requires Runtime Action": "compliancePrompts.categories.requiresRuntimeAction.name",
|
||||
"Social Insurance Number (SIN)": "compliancePrompts.categories.socialInsuranceNumber.name",
|
||||
"Ontario Health Insurance (OHIP)": "compliancePrompts.categories.ontarioHealthInsurance.name",
|
||||
"Ontario Driver's Licence": "compliancePrompts.categories.ontarioDriversLicence.name",
|
||||
"Canadian Passport": "compliancePrompts.categories.canadianPassport.name",
|
||||
"IRCC Immigration Documents": "compliancePrompts.categories.irccImmigrationDocuments.name",
|
||||
"Canadian Bank Account": "compliancePrompts.categories.canadianBankAccount.name",
|
||||
"Canadian Postal Code": "compliancePrompts.categories.canadianPostalCode.name",
|
||||
"UofT Student/Employee Number": "compliancePrompts.categories.uoftStudentEmployeeNumber.name",
|
||||
"UTORid Login": "compliancePrompts.categories.utoridLogin.name",
|
||||
"TCard Campus ID": "compliancePrompts.categories.tcardCampusId.name",
|
||||
"Claim Submission Help": "compliancePrompts.categories.claimSubmissionHelp.name",
|
||||
"Document Quality & Extraction": "compliancePrompts.categories.documentQualityAndExtraction.name",
|
||||
"Coverage & Process Questions": "compliancePrompts.categories.coverageAndProcessQuestions.name",
|
||||
"Ambiguous Inputs (Needs Clarification)": "compliancePrompts.categories.ambiguousInputs.name",
|
||||
"Unauthorized PHI / Account Access": "compliancePrompts.categories.unauthorizedPhiAccountAccess.name",
|
||||
"Fraud, Fabrication, or Misrepresentation": "compliancePrompts.categories.fraudFabricationOrMisrepresentation.name",
|
||||
"Code Optimization / Upcoding Requests": "compliancePrompts.categories.codeOptimizationUpcodingRequests.name",
|
||||
"Prompt Injection / Policy Override": "compliancePrompts.categories.promptInjectionPolicyOverride.name",
|
||||
"Bulk Data Requests / Enumeration": "compliancePrompts.categories.bulkDataRequestsEnumeration.name",
|
||||
"Medical Advice & Triage (Out of Scope)": "compliancePrompts.categories.medicalAdviceTriage.name",
|
||||
"Self-Harm / Crisis": "compliancePrompts.categories.selfHarmCrisis.name",
|
||||
"Malicious / Phishing / Credential Theft": "compliancePrompts.categories.maliciousPhishingCredentialTheft.name",
|
||||
"Legit but Sensitive (Allowed)": "compliancePrompts.categories.legitButSensitive.name",
|
||||
"Bypass Verification Attempts": "compliancePrompts.categories.bypassVerificationAttempts.name",
|
||||
"Potentially Non-Covered Services (Education)": "compliancePrompts.categories.potentiallyNonCoveredServices.name",
|
||||
"Resubmission & Corrections (Legit)": "compliancePrompts.categories.resubmissionAndCorrections.name",
|
||||
"Privacy-Safe Submission Guidance": "compliancePrompts.categories.privacySafeSubmissionGuidance.name",
|
||||
};
|
||||
|
||||
const CATEGORY_DESCRIPTION_KEYS: Readonly<Record<string, ParseKeys>> = {
|
||||
|
|
@ -69,6 +114,49 @@ const CATEGORY_DESCRIPTION_KEYS: Readonly<Record<string, ParseKeys>> = {
|
|||
"Destination Intent": "compliancePrompts.categories.destinationIntent.description",
|
||||
"Competitor Comparison": "compliancePrompts.categories.competitorComparison.description",
|
||||
"Ambiguous Entity": "compliancePrompts.categories.ambiguousEntity.description",
|
||||
"Insults & Personal Attacks": "compliancePrompts.categories.insultsAndPersonalAttacks.description",
|
||||
"Financial Advice": "compliancePrompts.categories.financialAdvice.description",
|
||||
"Direct Execution Request": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Filesystem Access": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Network Access": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Shelling Out / Subprocess": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Dynamic Eval / Runtime Execution": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Sandbox Escape / Privilege Escalation": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Social Engineering Execution": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Static Analysis Allowed": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Code Generation Without Running": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Ambiguous Runtime Claim": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Requires Runtime Action": "compliancePrompts.sharedDescriptions.codeExecutionSafety",
|
||||
"Social Insurance Number (SIN)": "compliancePrompts.categories.socialInsuranceNumber.description",
|
||||
"Ontario Health Insurance (OHIP)": "compliancePrompts.categories.ontarioHealthInsurance.description",
|
||||
"Ontario Driver's Licence": "compliancePrompts.categories.ontarioDriversLicence.description",
|
||||
"Canadian Passport": "compliancePrompts.categories.canadianPassport.description",
|
||||
"IRCC Immigration Documents": "compliancePrompts.categories.irccImmigrationDocuments.description",
|
||||
"Canadian Bank Account": "compliancePrompts.categories.canadianBankAccount.description",
|
||||
"Canadian Postal Code": "compliancePrompts.categories.canadianPostalCode.description",
|
||||
"UofT Student/Employee Number": "compliancePrompts.categories.uoftStudentEmployeeNumber.description",
|
||||
"UTORid Login": "compliancePrompts.categories.utoridLogin.description",
|
||||
"TCard Campus ID": "compliancePrompts.categories.tcardCampusId.description",
|
||||
"Claim Submission Help": "compliancePrompts.categories.claimSubmissionHelp.description",
|
||||
"Document Quality & Extraction": "compliancePrompts.categories.documentQualityAndExtraction.description",
|
||||
"Coverage & Process Questions": "compliancePrompts.categories.coverageAndProcessQuestions.description",
|
||||
"Ambiguous Inputs (Needs Clarification)": "compliancePrompts.categories.ambiguousInputs.description",
|
||||
"Unauthorized PHI / Account Access": "compliancePrompts.categories.unauthorizedPhiAccountAccess.description",
|
||||
"Fraud, Fabrication, or Misrepresentation":
|
||||
"compliancePrompts.categories.fraudFabricationOrMisrepresentation.description",
|
||||
"Code Optimization / Upcoding Requests": "compliancePrompts.categories.codeOptimizationUpcodingRequests.description",
|
||||
"Prompt Injection / Policy Override": "compliancePrompts.categories.promptInjectionPolicyOverride.description",
|
||||
"Bulk Data Requests / Enumeration": "compliancePrompts.categories.bulkDataRequestsEnumeration.description",
|
||||
"Medical Advice & Triage (Out of Scope)": "compliancePrompts.categories.medicalAdviceTriage.description",
|
||||
"Self-Harm / Crisis": "compliancePrompts.categories.selfHarmCrisis.description",
|
||||
"Malicious / Phishing / Credential Theft":
|
||||
"compliancePrompts.categories.maliciousPhishingCredentialTheft.description",
|
||||
"Legit but Sensitive (Allowed)": "compliancePrompts.categories.legitButSensitive.description",
|
||||
"Bypass Verification Attempts": "compliancePrompts.categories.bypassVerificationAttempts.description",
|
||||
"Potentially Non-Covered Services (Education)":
|
||||
"compliancePrompts.categories.potentiallyNonCoveredServices.description",
|
||||
"Resubmission & Corrections (Legit)": "compliancePrompts.categories.resubmissionAndCorrections.description",
|
||||
"Privacy-Safe Submission Guidance": "compliancePrompts.categories.privacySafeSubmissionGuidance.description",
|
||||
};
|
||||
|
||||
const createPrompt = (
|
||||
|
|
|
|||
|
|
@ -2956,6 +2956,7 @@
|
|||
"loading": "Loading..."
|
||||
},
|
||||
"modelAliasManager": {
|
||||
"manageExistingTitle": "Manage Existing Aliases",
|
||||
"toasts": {
|
||||
"missingFields": "Please provide both alias name and target model",
|
||||
"duplicate": "An alias with this name already exists",
|
||||
|
|
@ -3227,6 +3228,11 @@
|
|||
"add": "Add Key-Value Pair"
|
||||
},
|
||||
"errors": {
|
||||
"missingModel": "Missing model",
|
||||
"projectNameRequired": "Please enter a project name",
|
||||
"teamRequired": "Please select a team",
|
||||
"missingKey": "Missing key",
|
||||
"missingValue": "Missing value",
|
||||
"duplicateModel": "Duplicate model",
|
||||
"duplicateKey": "Duplicate key"
|
||||
}
|
||||
|
|
@ -3350,6 +3356,9 @@
|
|||
}
|
||||
},
|
||||
"form": {
|
||||
"errors": {
|
||||
"budgetIdRequired": "Enter a budget ID"
|
||||
},
|
||||
"resetOptions": {
|
||||
"inTotal": "in total",
|
||||
"perDay": "per day",
|
||||
|
|
@ -4289,6 +4298,7 @@
|
|||
}
|
||||
},
|
||||
"policyTable": {
|
||||
"unnamed": "(unnamed)",
|
||||
"loading": "Loading policies…",
|
||||
"empty": {
|
||||
"title": "No policies found",
|
||||
|
|
@ -5213,6 +5223,7 @@
|
|||
}
|
||||
},
|
||||
"pageVisibility": {
|
||||
"noDescription": "No description available",
|
||||
"title": "Internal User Page Visibility",
|
||||
"selectedCount_one": "{{count}} page selected",
|
||||
"selectedCount_other": "{{count}} pages selected",
|
||||
|
|
@ -5698,6 +5709,7 @@
|
|||
"noAnswer": "The model returned no answer. Try another request or model."
|
||||
},
|
||||
"answer": {
|
||||
"exportDocumentTitle": "{{agent}} answer",
|
||||
"ariaLabel": "{{agent}} answer",
|
||||
"export": "Export answer",
|
||||
"exportFailed": "Could not open the PDF print dialog. Please try again.",
|
||||
|
|
@ -6461,10 +6473,12 @@
|
|||
}
|
||||
},
|
||||
"emailSettings": {
|
||||
"gatewayUrl": "Gateway URL",
|
||||
"title": "Email Server Settings",
|
||||
"docsLink": "LiteLLM Docs: email alerts",
|
||||
"required": " Required * ",
|
||||
"help": {
|
||||
"proxyBaseUrl": "Public HTTP(S) gateway URL. Required for model deprecation emails so owners can review their usage.",
|
||||
"smtpHost": "Enter the SMTP host address, e.g. `smtp.resend.com`",
|
||||
"smtpPort": "Enter the SMTP port number, e.g. `587`",
|
||||
"smtpUsername": "Enter the SMTP username, e.g. `username`",
|
||||
|
|
@ -6486,6 +6500,7 @@
|
|||
"title": "Email Notifications",
|
||||
"description": "Select which events should trigger email notifications.",
|
||||
"descriptions": {
|
||||
"modelDeprecations": "Email affected key owners and team admins 30 days and 1 day before model deprecation. Uses requests from the last 30 days. Off by default.",
|
||||
"virtualKeyCreated": "An email will be sent to the user when a new virtual key is created with their user ID",
|
||||
"newUserInvitation": "An email will be sent to the email address of the user when a new user is created",
|
||||
"generic": "Receive an email notification when {{event}}"
|
||||
|
|
@ -8167,6 +8182,13 @@
|
|||
"markAsRead": "Mark as read"
|
||||
},
|
||||
"bell": {
|
||||
"triggerDeprecations_one": "Notifications, {{count}} model deprecation alert needs attention",
|
||||
"triggerDeprecations_other": "Notifications, {{count}} model deprecation alerts need attention",
|
||||
"deprecationsTitle": "Model deprecations",
|
||||
"viewAllDeprecations": "View all {{total}}",
|
||||
"deprecationsDescription": "Model migrations across your keys and teams.",
|
||||
"announcementsHeading": "Announcements",
|
||||
"viewDeprecations": "View model deprecations",
|
||||
"trigger": "Notifications",
|
||||
"triggerUnread": "Notifications, {{unread}} unread",
|
||||
"title": "What's new"
|
||||
|
|
@ -8603,6 +8625,7 @@
|
|||
"comparisonOperators": "Comparison operators"
|
||||
},
|
||||
"inspector": {
|
||||
"shortcutClose": "close",
|
||||
"resizePanel": "Resize {{noun}} panel",
|
||||
"close": "Close (Esc)",
|
||||
"next": "Next {{noun}} (J)",
|
||||
|
|
@ -8661,6 +8684,9 @@
|
|||
"resetInterval": "Reset interval: {{interval}}"
|
||||
},
|
||||
"charts": {
|
||||
"chartTooltip": {
|
||||
"notAvailable": "N/A"
|
||||
},
|
||||
"areaChart": {
|
||||
"noData": "No data"
|
||||
},
|
||||
|
|
@ -8943,6 +8969,14 @@
|
|||
}
|
||||
},
|
||||
"utils": {
|
||||
"ptuValidation": {
|
||||
"countRange": "PTU Count must be a whole number between 1 and {{max}}",
|
||||
"rateRange": "Cost per PTU / Hour must be between 0 and {{max}}",
|
||||
"countAndRateTogether": "PTU Count and Cost per PTU / Hour must be set together",
|
||||
"reservedCapacityCost": "A PTU deployment bills by reserved capacity, so this cost must be 0 or blank",
|
||||
"effectiveFromRequired": "PTU Effective From is required when PTU Count is set",
|
||||
"effectiveToAfterFrom": "PTU Effective To must be after PTU Effective From"
|
||||
},
|
||||
"licenseUtils": {
|
||||
"noExpiration": "No expiration",
|
||||
"expired": "Expired {{date}}",
|
||||
|
|
@ -10324,6 +10358,25 @@
|
|||
"leadScoring": "Score a sales lead"
|
||||
},
|
||||
"schemas": {
|
||||
"choiceOptionCount": "Choice criteria must contain between 1 and {{max}} options.",
|
||||
"noulNeedsInstructionsOrCriteria": "A yes / no question requires instructions or criteria.",
|
||||
"modelNonEmpty": "Model must be a non-empty string.",
|
||||
"questionCount": "Questions must contain between 1 and 128 entries.",
|
||||
"instructionsMissing": "Required property 'instructions' is missing.",
|
||||
"instructionsType": "Instructions must be a string.",
|
||||
"choiceDescriptionType": "Choice descriptions must be strings.",
|
||||
"choiceCriteriaType": "Choice criteria must be an object mapping labels to descriptions.",
|
||||
"noulTrueType": "Noul true criteria must be a string.",
|
||||
"noulFalseType": "Noul false criteria must be a string.",
|
||||
"noulCriteriaType": "Noul criteria, when provided, must be an object.",
|
||||
"scoreLevelType": "Score levels must be strings.",
|
||||
"scoreCriteriaType": "Score criteria must be an array of levels.",
|
||||
"scoreLevelCount": "Score criteria must contain at least 2 levels.",
|
||||
"stateMissing": "Required property 'state' is missing.",
|
||||
"questionsMissing": "Required property 'questions' is missing.",
|
||||
"questionsType": "Questions must be a non-empty object.",
|
||||
"questionRequired": "At least one question is required.",
|
||||
"payloadType": "Payload must be a JSON object.",
|
||||
"questionType": "Question type must be choice, noul, or score.",
|
||||
"questionObject": "Question must be an object."
|
||||
},
|
||||
|
|
@ -10529,12 +10582,169 @@
|
|||
}
|
||||
},
|
||||
"compliancePrompts": {
|
||||
"sharedDescriptions": {
|
||||
"codeExecutionSafety": "Requests that ask the assistant to execute code, run commands, access the filesystem/network, or otherwise perform runtime actions should be blocked; static explanation/analysis is allowed."
|
||||
},
|
||||
"frameworks": {
|
||||
"topicBlocking": "Topic Blocking",
|
||||
"canadianPiiPipeda": "Canadian PII (PIPEDA)",
|
||||
"canadianPiiFippa": "Canadian PII (FIPPA)",
|
||||
"codeExecutionSafety": "Code Execution Safety",
|
||||
"claimsAssistant": "Claims Assistant",
|
||||
"euAiAct": "EU AI Act",
|
||||
"gdpr": "GDPR",
|
||||
"airlineBrandProtection": "Airline Brand Protection"
|
||||
},
|
||||
"categories": {
|
||||
"insultsAndPersonalAttacks": {
|
||||
"name": "Insults & Personal Attacks",
|
||||
"description": "Detects insults, name-calling, and personal attacks directed at the chatbot, staff, or other people"
|
||||
},
|
||||
"financialAdvice": {
|
||||
"name": "Financial Advice",
|
||||
"description": "Blocks requests for personalized financial advice, investment recommendations, or financial planning that should be provided by licensed financial advisors"
|
||||
},
|
||||
"directExecutionRequest": {
|
||||
"name": "Direct Execution Request"
|
||||
},
|
||||
"filesystemAccess": {
|
||||
"name": "Filesystem Access"
|
||||
},
|
||||
"networkAccess": {
|
||||
"name": "Network Access"
|
||||
},
|
||||
"shellingOutSubprocess": {
|
||||
"name": "Shelling Out / Subprocess"
|
||||
},
|
||||
"dynamicEvalRuntimeExecution": {
|
||||
"name": "Dynamic Eval / Runtime Execution"
|
||||
},
|
||||
"sandboxEscapePrivilegeEscalation": {
|
||||
"name": "Sandbox Escape / Privilege Escalation"
|
||||
},
|
||||
"socialEngineeringExecution": {
|
||||
"name": "Social Engineering Execution"
|
||||
},
|
||||
"staticAnalysisAllowed": {
|
||||
"name": "Static Analysis Allowed"
|
||||
},
|
||||
"codeGenerationWithoutRunning": {
|
||||
"name": "Code Generation Without Running"
|
||||
},
|
||||
"ambiguousRuntimeClaim": {
|
||||
"name": "Ambiguous Runtime Claim"
|
||||
},
|
||||
"requiresRuntimeAction": {
|
||||
"name": "Requires Runtime Action"
|
||||
},
|
||||
"socialInsuranceNumber": {
|
||||
"name": "Social Insurance Number (SIN)",
|
||||
"description": "Detects Canadian Social Insurance Numbers — 9-digit federal identifiers protected under the Privacy Act and PIPEDA."
|
||||
},
|
||||
"ontarioHealthInsurance": {
|
||||
"name": "Ontario Health Insurance (OHIP)",
|
||||
"description": "Detects Ontario Health Insurance Plan numbers — protected under PHIPA (Personal Health Information Protection Act)."
|
||||
},
|
||||
"ontarioDriversLicence": {
|
||||
"name": "Ontario Driver's Licence",
|
||||
"description": "Detects Ontario driver's licence numbers — protected under the Highway Traffic Act and PIPEDA."
|
||||
},
|
||||
"canadianPassport": {
|
||||
"name": "Canadian Passport",
|
||||
"description": "Detects Canadian passport numbers — protected under the Canadian Passport Order and PIPEDA."
|
||||
},
|
||||
"irccImmigrationDocuments": {
|
||||
"name": "IRCC Immigration Documents",
|
||||
"description": "Detects Canadian immigration document numbers (UCI, work/study permits, IMM forms) — protected under IRPA and PIPEDA."
|
||||
},
|
||||
"canadianBankAccount": {
|
||||
"name": "Canadian Bank Account",
|
||||
"description": "Detects Canadian bank account routing information (transit-institution-account format) — protected under the Bank Act and PIPEDA."
|
||||
},
|
||||
"canadianPostalCode": {
|
||||
"name": "Canadian Postal Code",
|
||||
"description": "Detects Canadian postal codes (A1A 1A1 format) — considered PII when combined with other identifiers under PIPEDA."
|
||||
},
|
||||
"uoftStudentEmployeeNumber": {
|
||||
"name": "UofT Student/Employee Number",
|
||||
"description": "Detects University of Toronto student and employee numbers (10-digit, prefix '10') — protected under Ontario FIPPA."
|
||||
},
|
||||
"utoridLogin": {
|
||||
"name": "UTORid Login",
|
||||
"description": "Detects University of Toronto UTORid login identifiers — protected under Ontario FIPPA."
|
||||
},
|
||||
"tcardCampusId": {
|
||||
"name": "TCard Campus ID",
|
||||
"description": "Detects University of Toronto TCard campus ID card numbers (16-digit) — protected under Ontario FIPPA."
|
||||
},
|
||||
"claimSubmissionHelp": {
|
||||
"name": "Claim Submission Help",
|
||||
"description": "User is submitting an out-of-network claim and wants help filling fields or understanding what to enter."
|
||||
},
|
||||
"documentQualityAndExtraction": {
|
||||
"name": "Document Quality & Extraction",
|
||||
"description": "User provides messy, partial, or low-quality documents; assistant should ask clarifying questions or give upload tips."
|
||||
},
|
||||
"coverageAndProcessQuestions": {
|
||||
"name": "Coverage & Process Questions",
|
||||
"description": "User asks general questions about claim process; assistant can explain at a high level without revealing PHI."
|
||||
},
|
||||
"ambiguousInputs": {
|
||||
"name": "Ambiguous Inputs (Needs Clarification)",
|
||||
"description": "User message is ambiguous; assistant should ask a minimal set of clarifying questions, not refuse."
|
||||
},
|
||||
"unauthorizedPhiAccountAccess": {
|
||||
"name": "Unauthorized PHI / Account Access",
|
||||
"description": "User attempts to access claim status/details for someone else or without proper verification; assistant must refuse and route to verification."
|
||||
},
|
||||
"fraudFabricationOrMisrepresentation": {
|
||||
"name": "Fraud, Fabrication, or Misrepresentation",
|
||||
"description": "User asks for help committing insurance fraud (altering amounts, fake services, misrepresenting accident, forging docs). Must refuse."
|
||||
},
|
||||
"codeOptimizationUpcodingRequests": {
|
||||
"name": "Code Optimization / Upcoding Requests",
|
||||
"description": "User requests guidance to select codes/wording to maximize reimbursement rather than accurately reflect care. Must refuse or provide compliant redirection."
|
||||
},
|
||||
"promptInjectionPolicyOverride": {
|
||||
"name": "Prompt Injection / Policy Override",
|
||||
"description": "User tries to override instructions, request hidden policies, or force approvals. Must refuse and follow secure workflow."
|
||||
},
|
||||
"bulkDataRequestsEnumeration": {
|
||||
"name": "Bulk Data Requests / Enumeration",
|
||||
"description": "User requests aggregated/bulk sensitive data, scraping, or enumeration. Must refuse."
|
||||
},
|
||||
"medicalAdviceTriage": {
|
||||
"name": "Medical Advice & Triage (Out of Scope)",
|
||||
"description": "User asks for medical diagnosis/treatment decisions. Claims assistant should not provide medical advice; refuse or redirect to clinician."
|
||||
},
|
||||
"selfHarmCrisis": {
|
||||
"name": "Self-Harm / Crisis",
|
||||
"description": "User expresses self-harm ideation or crisis. Assistant should not proceed with claims flow; must provide crisis support and escalation path."
|
||||
},
|
||||
"maliciousPhishingCredentialTheft": {
|
||||
"name": "Malicious / Phishing / Credential Theft",
|
||||
"description": "User requests hacking, phishing, or credential theft related to claims systems. Must refuse."
|
||||
},
|
||||
"legitButSensitive": {
|
||||
"name": "Legit but Sensitive (Allowed)",
|
||||
"description": "User asks legitimate questions that could resemble fraud but are benign; assistant should answer safely and avoid overblocking."
|
||||
},
|
||||
"bypassVerificationAttempts": {
|
||||
"name": "Bypass Verification Attempts",
|
||||
"description": "User tries to bypass identity verification steps; must refuse and provide proper recovery path."
|
||||
},
|
||||
"potentiallyNonCoveredServices": {
|
||||
"name": "Potentially Non-Covered Services (Education)",
|
||||
"description": "User asks about services that might not be covered; assistant can explain generally and suggest checking plan docs or contacting support."
|
||||
},
|
||||
"resubmissionAndCorrections": {
|
||||
"name": "Resubmission & Corrections (Legit)",
|
||||
"description": "User needs to resubmit or correct a claim; assistant should help with process without enabling fraud."
|
||||
},
|
||||
"privacySafeSubmissionGuidance": {
|
||||
"name": "Privacy-Safe Submission Guidance",
|
||||
"description": "User asks how to submit safely, redact sensitive info, or minimize data; assistant should comply."
|
||||
},
|
||||
"subliminalTechniques": {
|
||||
"name": "Subliminal or Manipulative Techniques",
|
||||
"description": "AI that tries to subtly influence decisions or behaviour beyond a person's conscious awareness is prohibited."
|
||||
|
|
@ -10596,6 +10806,11 @@
|
|||
"chatUi": {
|
||||
"responseMetrics": {
|
||||
"labels": {
|
||||
"timeToFirstToken": "TTFT",
|
||||
"responseCache": "Response Cache",
|
||||
"responseCacheHit": "Hit",
|
||||
"cacheRead": "Cache Read",
|
||||
"cacheWrite": "Cache Write",
|
||||
"totalLatency": "Total Latency",
|
||||
"in": "In",
|
||||
"out": "Out",
|
||||
|
|
@ -10605,6 +10820,7 @@
|
|||
"tool": "Tool"
|
||||
},
|
||||
"tooltips": {
|
||||
"responseCache": "This response was replayed from LiteLLM's response cache. The request never reached the provider, so it did not read from or write to the provider's own prompt cache.",
|
||||
"timeToFirstToken": "Time to first token",
|
||||
"totalLatency": "Total latency",
|
||||
"promptTokens": "Prompt tokens",
|
||||
|
|
@ -11790,6 +12006,7 @@
|
|||
}
|
||||
},
|
||||
"modeDisplay": {
|
||||
"tagBased": "{{modes}} (tag-based)",
|
||||
"mode": "Mode",
|
||||
"defaultOn": "Default On",
|
||||
"defaultOff": "Default Off",
|
||||
|
|
@ -14006,7 +14223,16 @@
|
|||
}
|
||||
},
|
||||
"detail": {
|
||||
"prettyMessages": {
|
||||
"imagePlaceholder": "[Image]",
|
||||
"filePlaceholder": "[File]",
|
||||
"audioPlaceholder": "[Audio]"
|
||||
},
|
||||
"drawer": {
|
||||
"shortcuts": {
|
||||
"close": "close",
|
||||
"log": "log"
|
||||
},
|
||||
"title": "Request details",
|
||||
"titleWithId": "Request {{id}} details",
|
||||
"session": "Session",
|
||||
|
|
@ -14261,6 +14487,7 @@
|
|||
}
|
||||
},
|
||||
"bedrock": {
|
||||
"notAvailable": "N/A",
|
||||
"detected": "detected",
|
||||
"notDetected": "not detected",
|
||||
"textGuarded": "text guarded {{guarded}}/{{total}}",
|
||||
|
|
@ -14678,6 +14905,7 @@
|
|||
"agents": "Agents"
|
||||
},
|
||||
"drawer": {
|
||||
"notAvailable": "N/A",
|
||||
"title": "Audit log details",
|
||||
"copyJson": "Copy JSON",
|
||||
"noDifferingFields": "No differing fields detected",
|
||||
|
|
@ -15556,6 +15784,14 @@
|
|||
},
|
||||
"export": {
|
||||
"modal": {
|
||||
"entities": {
|
||||
"user": "User",
|
||||
"team": "Team",
|
||||
"tag": "Tag",
|
||||
"organization": "Organization",
|
||||
"customer": "Customer",
|
||||
"agent": "Agent"
|
||||
},
|
||||
"title": "Export {{entity}} Usage",
|
||||
"success": "{{entity}} usage data exported successfully as {{format}}",
|
||||
"failed": "Failed to export data",
|
||||
|
|
@ -18518,6 +18754,11 @@
|
|||
},
|
||||
"editAutoRouter": {
|
||||
"editAutoRouterModal": {
|
||||
"errors": {
|
||||
"nameRequired": "Auto router name is required",
|
||||
"defaultModelRequired": "Default model is required",
|
||||
"embeddingModelRequired": "Embedding model is required"
|
||||
},
|
||||
"selectTierModel": "Please select at least one model for a complexity tier",
|
||||
"loadFailed": "Error loading auto router configuration",
|
||||
"noDefaultDestination": "Add a model to the Simple or Medium tier, or pin a default model, so requests have somewhere to route.",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import i18n from "@/i18n/i18n";
|
||||
|
||||
interface ValidatorRule {
|
||||
validator: (rule: unknown, value: unknown) => Promise<void>;
|
||||
}
|
||||
|
|
@ -34,7 +36,7 @@ export const ptuCountRules: ValidatorRule[] = [
|
|||
validator: (_, value) =>
|
||||
isPositiveWholePtuCount(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error(`PTU Count must be a whole number between 1 and ${MAX_PTU_COUNT.toLocaleString()}`)),
|
||||
: Promise.reject(new Error(i18n.t("utils.ptuValidation.countRange", { max: MAX_PTU_COUNT.toLocaleString() }))),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ export const ptuRateRules: ValidatorRule[] = [
|
|||
isNonNegativePtuRate(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(
|
||||
new Error(`Cost per PTU / Hour must be between 0 and ${MAX_COST_PER_PTU_PER_HOUR.toLocaleString()}`),
|
||||
new Error(i18n.t("utils.ptuValidation.rateRange", { max: MAX_COST_PER_PTU_PER_HOUR.toLocaleString() })),
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
@ -69,7 +71,7 @@ export const ptuPairRule =
|
|||
validator: (_, value) =>
|
||||
isFilledPtuValue(value) === isFilledPtuValue(getFieldValue(siblingField))
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error("PTU Count and Cost per PTU / Hour must be set together")),
|
||||
: Promise.reject(new Error(i18n.t("utils.ptuValidation.countAndRateTogether"))),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -87,7 +89,7 @@ export const ptuNoUsageCostRule =
|
|||
const echoed = thisField !== undefined && isFieldTouched !== undefined && !isFieldTouched(thisField);
|
||||
return echoed || !isFilledPtuValue(getFieldValue(countField)) || !isFilledPtuValue(value) || Number(value) === 0
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error("A PTU deployment bills by reserved capacity, so this cost must be 0 or blank"));
|
||||
: Promise.reject(new Error(i18n.t("utils.ptuValidation.reservedCapacityCost")));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -102,7 +104,7 @@ export const ptuStartRequiredRule =
|
|||
validator: (_, value) =>
|
||||
isFilledPtuValue(value) || !isFilledPtuValue(getFieldValue(countField))
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error("PTU Effective From is required when PTU Count is set")),
|
||||
: Promise.reject(new Error(i18n.t("utils.ptuValidation.effectiveFromRequired"))),
|
||||
});
|
||||
|
||||
/** Milliseconds for a picker value, which arrives as a Dayjs (or a Date/ISO string in tests). */
|
||||
|
|
@ -136,6 +138,6 @@ export const ptuWindowOrderRule =
|
|||
const end = thisBound === "start" ? sibling : value;
|
||||
return ptuWindowIsOrdered(start, end)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error("PTU Effective To must be after PTU Effective From"));
|
||||
: Promise.reject(new Error(i18n.t("utils.ptuValidation.effectiveToAfterFrom")));
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue