feat(ui): i18n for policy attachment and creation forms

This commit is contained in:
吹雪 2026-06-10 23:24:25 +09:00
parent 3b7cdc1246
commit d2c6db823e
4 changed files with 350 additions and 169 deletions

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
import { Button } from "@tremor/react";
import { useTranslation, Trans } from "react-i18next";
import { Policy } from "./types";
import { teamListCall, keyListCall, modelAvailableCall, estimateAttachmentImpactCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
@ -27,6 +28,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
policies,
createAttachment,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [scopeType, setScopeType] = useState<"global" | "specific">("global");
@ -158,11 +160,11 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
const failed = results.filter((r) => r.status === "rejected") as PromiseRejectedResult[];
if (successCount > 0 && failed.length === 0) {
NotificationsManager.success(
successCount === 1 ? "Attachment created successfully" : `${successCount} attachments created successfully`,
);
NotificationsManager.success(t("policies.addAttachmentForm.attachmentCreatedSuccess", { count: successCount }));
} else if (successCount > 0 && failed.length > 0) {
NotificationsManager.fromBackend(`${successCount} attachments created, ${failed.length} failed`);
NotificationsManager.fromBackend(
t("policies.addAttachmentForm.attachmentsPartialSuccess", { successCount, failedCount: failed.length }),
);
} else {
throw new Error(failed[0]?.reason instanceof Error ? failed[0].reason.message : "Failed to create attachments");
}
@ -173,7 +175,9 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
} catch (error) {
console.error("Failed to create attachment:", error);
NotificationsManager.fromBackend(
"Failed to create attachment: " + (error instanceof Error ? error.message : String(error)),
t("policies.addAttachmentForm.createAttachmentFailed", {
error: error instanceof Error ? error.message : String(error),
}),
);
} finally {
setIsSubmitting(false);
@ -186,7 +190,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
}));
return (
<Modal title="Create Policy Attachment" open={visible} onCancel={handleClose} footer={null} width={600}>
<Modal
title={t("policies.addAttachmentForm.modalTitle")}
open={visible}
onCancel={handleClose}
footer={null}
width={600}
>
<Form
form={form}
layout="vertical"
@ -196,12 +206,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
>
<Form.Item
name="policy_names"
label="Policies"
rules={[{ required: true, message: "Please select at least one policy" }]}
label={t("policies.addAttachmentForm.policiesLabel")}
rules={[{ required: true, message: t("policies.addAttachmentForm.policiesRequired") }]}
>
<Select
mode="multiple"
placeholder="Select policies to attach"
placeholder={t("policies.addAttachmentForm.policiesPlaceholder")}
options={policyOptions}
showSearch
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
@ -210,13 +220,13 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
</Form.Item>
<Divider orientation="left">
<Text strong>Scope</Text>
<Text strong>{t("policies.addAttachmentForm.scopeDivider")}</Text>
</Divider>
<Form.Item label="Scope Type">
<Form.Item label={t("policies.addAttachmentForm.scopeTypeLabel")}>
<Radio.Group value={scopeType} onChange={(e) => setScopeType(e.target.value)}>
<Radio value="specific">Specific (teams, keys, models, or tags)</Radio>
<Radio value="global">Global (applies to all requests)</Radio>
<Radio value="specific">{t("policies.addAttachmentForm.scopeSpecific")}</Radio>
<Radio value="global">{t("policies.addAttachmentForm.scopeGlobal")}</Radio>
</Radio.Group>
</Form.Item>
@ -224,12 +234,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<>
<Form.Item
name="teams"
label="Teams"
tooltip="Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)"
label={t("policies.addAttachmentForm.teamsLabel")}
tooltip={t("policies.addAttachmentForm.teamsTooltip")}
>
<Select
mode="tags"
placeholder={isLoadingTeams ? "Loading teams..." : "Select or enter team aliases"}
placeholder={isLoadingTeams ? t("common.loading") : t("policies.addAttachmentForm.teamsPlaceholder")}
loading={isLoadingTeams}
options={availableTeams.map((team) => ({
label: team,
@ -244,12 +254,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="keys"
label="Keys"
tooltip="Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)"
label={t("policies.addAttachmentForm.keysLabel")}
tooltip={t("policies.addAttachmentForm.keysTooltip")}
>
<Select
mode="tags"
placeholder={isLoadingKeys ? "Loading keys..." : "Select or enter key aliases"}
placeholder={isLoadingKeys ? t("common.loading") : t("policies.addAttachmentForm.keysPlaceholder")}
loading={isLoadingKeys}
options={availableKeys.map((key) => ({
label: key,
@ -264,14 +274,12 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="models"
label="Models"
tooltip="Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models."
label={t("policies.addAttachmentForm.modelsLabel")}
tooltip={t("policies.addAttachmentForm.modelsTooltip")}
>
<Select
mode="tags"
placeholder={
isLoadingModels ? "Loading models..." : "Select or enter model names (e.g., gpt-4, bedrock/*)"
}
placeholder={isLoadingModels ? t("common.loading") : t("policies.addAttachmentForm.modelsPlaceholder")}
loading={isLoadingModels}
options={availableModels.map((model) => ({
label: model,
@ -286,19 +294,17 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<Form.Item
name="tags"
label="Tags"
tooltip="Match against tags set in key or team metadata. Use exact values (e.g., healthcare) or wildcard patterns (e.g., health-*) where * matches any suffix."
label={t("policies.addAttachmentForm.tagsLabel")}
tooltip={t("policies.addAttachmentForm.tagsTooltip")}
extra={
<Text type="secondary" style={{ fontSize: 12 }}>
Matches tags from key/team <code>metadata.tags</code> or tags passed dynamically in the request body.
Use <code>*</code> as a suffix wildcard (e.g., <code>prod-*</code> matches <code>prod-us</code>,{" "}
<code>prod-eu</code>).
<Trans i18nKey="policies.addAttachmentForm.tagsExtra" components={{ code: <code /> }} />
</Text>
}
>
<Select
mode="tags"
placeholder="Type a tag and press Enter (e.g. healthcare, prod-*)"
placeholder={t("policies.addAttachmentForm.tagsPlaceholder")}
tokenSeparators={[",", " "]}
notFoundContent={null}
suffixIcon={null}
@ -313,15 +319,15 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
<div className="flex justify-end space-x-2 mt-4">
<Button variant="secondary" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
{scopeType === "specific" && (
<Button variant="secondary" onClick={handlePreviewImpact} loading={isEstimating}>
Estimate Impact
{t("policies.addAttachmentForm.estimateImpact")}
</Button>
)}
<Button onClick={handleSubmit} loading={isSubmitting}>
Create Attachment
{t("policies.addAttachmentForm.createAttachment")}
</Button>
</div>
</Form>

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "antd";
import { Button, TextInput, Textarea } from "@tremor/react";
import { useTranslation } from "react-i18next";
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
import { Guardrail } from "../guardrails/types";
import { getResolvedGuardrails, modelAvailableCall } from "../networking";
@ -32,116 +33,119 @@ interface ModePicker {
onSelect: (mode: "simple" | "flow_builder") => void;
}
const ModePicker: React.FC<ModePicker> = ({ selected, onSelect }) => (
<div className="flex gap-4" style={{ padding: "8px 0" }}>
{/* Simple Mode Card */}
<div
onClick={() => onSelect("simple")}
style={{
flex: 1,
padding: "24px 20px",
border: `2px solid ${selected === "simple" ? "#4f46e5" : "#e5e7eb"}`,
borderRadius: 12,
cursor: "pointer",
backgroundColor: selected === "simple" ? "#eef2ff" : "#fff",
transition: "all 0.15s ease",
}}
>
const ModePicker: React.FC<ModePicker> = ({ selected, onSelect }) => {
const { t } = useTranslation();
return (
<div className="flex gap-4" style={{ padding: "8px 0" }}>
{/* Simple Mode Card */}
<div
onClick={() => onSelect("simple")}
style={{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: selected === "simple" ? "#e0e7ff" : "#f3f4f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
flex: 1,
padding: "24px 20px",
border: `2px solid ${selected === "simple" ? "#4f46e5" : "#e5e7eb"}`,
borderRadius: 12,
cursor: "pointer",
backgroundColor: selected === "simple" ? "#eef2ff" : "#fff",
transition: "all 0.15s ease",
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke={selected === "simple" ? "#4f46e5" : "#6b7280"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
<div
style={{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: selected === "simple" ? "#e0e7ff" : "#f3f4f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
}}
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M8 7h8M8 12h8M8 17h5" />
</svg>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke={selected === "simple" ? "#4f46e5" : "#6b7280"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<path d="M8 7h8M8 12h8M8 17h5" />
</svg>
</div>
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
{t("policies.addPolicyForm.simpleModeTitle")}
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
{t("policies.addPolicyForm.simpleModeSubtitle")}
</Text>
</div>
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
Simple Mode
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
Pick guardrails from a list. All run in parallel.
</Text>
</div>
{/* Flow Builder Card */}
<div
onClick={() => onSelect("flow_builder")}
style={{
flex: 1,
padding: "24px 20px",
border: `2px solid ${selected === "flow_builder" ? "#4f46e5" : "#e5e7eb"}`,
borderRadius: 12,
cursor: "pointer",
backgroundColor: selected === "flow_builder" ? "#eef2ff" : "#fff",
transition: "all 0.15s ease",
position: "relative",
}}
>
<Tag
color="purple"
style={{
position: "absolute",
top: 12,
right: 12,
fontSize: 10,
fontWeight: 600,
margin: 0,
}}
>
NEW
</Tag>
{/* Flow Builder Card */}
<div
onClick={() => onSelect("flow_builder")}
style={{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: selected === "flow_builder" ? "#e0e7ff" : "#f3f4f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
flex: 1,
padding: "24px 20px",
border: `2px solid ${selected === "flow_builder" ? "#4f46e5" : "#e5e7eb"}`,
borderRadius: 12,
cursor: "pointer",
backgroundColor: selected === "flow_builder" ? "#eef2ff" : "#fff",
transition: "all 0.15s ease",
position: "relative",
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke={selected === "flow_builder" ? "#4f46e5" : "#6b7280"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
<Tag
color="purple"
style={{
position: "absolute",
top: 12,
right: 12,
fontSize: 10,
fontWeight: 600,
margin: 0,
}}
>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
{t("policies.addPolicyForm.newBadge")}
</Tag>
<div
style={{
width: 40,
height: 40,
borderRadius: 10,
backgroundColor: selected === "flow_builder" ? "#e0e7ff" : "#f3f4f6",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke={selected === "flow_builder" ? "#4f46e5" : "#6b7280"}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
</div>
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
{t("policies.addPolicyForm.flowBuilderTitle")}
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
{t("policies.addPolicyForm.flowBuilderSubtitle")}
</Text>
</div>
<Text strong style={{ fontSize: 15, display: "block", marginBottom: 4 }}>
Flow Builder
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
Define steps, conditions, and error responses.
</Text>
</div>
</div>
);
);
};
// ─────────────────────────────────────────────────────────────────────────────
// Main Component
@ -159,6 +163,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
createPolicy,
updatePolicy,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
const [resolvedGuardrails, setResolvedGuardrails] = useState<string[]>([]);
@ -327,10 +332,10 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
if (isEditing && editingPolicy) {
await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
NotificationsManager.success("Policy updated successfully");
NotificationsManager.success(t("policies.addPolicyForm.policyUpdatedSuccess"));
} else {
await createPolicy(accessToken, data as PolicyCreateRequest);
NotificationsManager.success("Policy created successfully");
NotificationsManager.success(t("policies.addPolicyForm.policyCreatedSuccess"));
}
resetForm();
@ -339,7 +344,9 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
} catch (error) {
console.error("Failed to save policy:", error);
NotificationsManager.fromBackend(
"Failed to save policy: " + (error instanceof Error ? error.message : String(error)),
t("policies.addPolicyForm.savePolicyFailed", {
error: error instanceof Error ? error.message : String(error),
}),
);
} finally {
setIsSubmitting(false);
@ -361,12 +368,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
// ── Mode Picker Step ──────────────────────────────────────────────────────
if (step === "pick_mode") {
return (
<Modal title="Create New Policy" open={visible} onCancel={handleClose} footer={null} width={620}>
<Modal
title={t("policies.addPolicyForm.createNewPolicyTitle")}
open={visible}
onCancel={handleClose}
footer={null}
width={620}
>
<ModePicker selected={selectedMode} onSelect={setSelectedMode} />
{selectedMode === "flow_builder" && (
<Alert
message="You'll be redirected to the full-screen Flow Builder to design your policy logic visually."
message={t("policies.addPolicyForm.flowBuilderRedirectAlert")}
type="info"
style={{
marginTop: 16,
@ -378,7 +391,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
<div className="flex justify-end gap-2" style={{ marginTop: 24 }}>
<Button variant="secondary" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
<Button
onClick={handleModeConfirm}
@ -388,7 +401,9 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
border: "none",
}}
>
{selectedMode === "flow_builder" ? "Continue to Builder" : "Create Policy"}
{selectedMode === "flow_builder"
? t("policies.addPolicyForm.continueToBuilder")
: t("policies.addPolicyForm.createPolicyButton")}
</Button>
</div>
</Modal>
@ -398,7 +413,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
// ── Simple Form Step ──────────────────────────────────────────────────────
return (
<Modal
title={isEditing ? "Edit Policy" : "Create New Policy"}
title={isEditing ? t("policies.addPolicyForm.editPolicyTitle") : t("policies.addPolicyForm.createNewPolicyTitle")}
open={visible}
onCancel={handleClose}
footer={null}
@ -415,52 +430,52 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
>
<Form.Item
name="policy_name"
label="Policy Name"
label={t("policies.addPolicyForm.policyNameLabel")}
rules={[
{ required: true, message: "Please enter a policy name" },
{ required: true, message: t("policies.addPolicyForm.policyNameRequired") },
{
pattern: /^[a-zA-Z0-9_-]+$/,
message: "Policy name can only contain letters, numbers, hyphens, and underscores",
message: t("policies.addPolicyForm.policyNamePattern"),
},
]}
>
<TextInput placeholder="e.g., global-baseline, healthcare-compliance" disabled={isEditing} />
<TextInput placeholder={t("policies.addPolicyForm.policyNamePlaceholder")} disabled={isEditing} />
</Form.Item>
<Form.Item name="description" label="Description">
<Textarea rows={2} placeholder="Describe what this policy does..." />
<Form.Item name="description" label={t("common.description")}>
<Textarea rows={2} placeholder={t("policies.addPolicyForm.descriptionPlaceholder")} />
</Form.Item>
<Divider orientation="left">
<Text strong>Inheritance</Text>
<Text strong>{t("policies.addPolicyForm.inheritanceDivider")}</Text>
</Divider>
<Form.Item
name="inherit"
label="Inherit From"
tooltip="Inherit guardrails from another policy. The child policy will include all guardrails from the parent."
label={t("policies.addPolicyForm.inheritFromLabel")}
tooltip={t("policies.addPolicyForm.inheritFromTooltip")}
>
<Select
allowClear
placeholder="Select a parent policy (optional)"
placeholder={t("policies.addPolicyForm.inheritFromPlaceholder")}
options={policyOptions}
style={{ width: "100%" }}
/>
</Form.Item>
<Divider orientation="left">
<Text strong>Guardrails</Text>
<Text strong>{t("policies.addPolicyForm.guardrailsDivider")}</Text>
</Divider>
<Form.Item
name="guardrails_add"
label="Guardrails to Add"
tooltip="These guardrails will be added to requests matching this policy"
label={t("policies.addPolicyForm.guardrailsAddLabel")}
tooltip={t("policies.addPolicyForm.guardrailsAddTooltip")}
>
<Select
mode="multiple"
allowClear
placeholder="Select guardrails to add"
placeholder={t("policies.addPolicyForm.guardrailsAddPlaceholder")}
options={guardrailOptions}
style={{ width: "100%" }}
/>
@ -468,13 +483,13 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
<Form.Item
name="guardrails_remove"
label="Guardrails to Remove"
tooltip="These guardrails will be removed from inherited guardrails"
label={t("policies.addPolicyForm.guardrailsRemoveLabel")}
tooltip={t("policies.addPolicyForm.guardrailsRemoveTooltip")}
>
<Select
mode="multiple"
allowClear
placeholder="Select guardrails to remove (from inherited)"
placeholder={t("policies.addPolicyForm.guardrailsRemovePlaceholder")}
options={guardrailOptions}
style={{ width: "100%" }}
/>
@ -482,11 +497,11 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
{resolvedGuardrails.length > 0 && (
<Alert
message="Resolved Guardrails"
message={t("policies.addPolicyForm.resolvedGuardrailsTitle")}
description={
<div>
<Text type="secondary" style={{ display: "block", marginBottom: 8 }}>
These are the final guardrails that will be applied (including inheritance):
{t("policies.addPolicyForm.resolvedGuardrailsDesc")}
</Text>
<div className="flex flex-wrap gap-1">
{resolvedGuardrails.map((g) => (
@ -504,18 +519,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
)}
<Divider orientation="left">
<Text strong>Conditions (Optional)</Text>
<Text strong>{t("policies.addPolicyForm.conditionsDivider")}</Text>
</Divider>
<Alert
message="Model Scope"
description="By default, this policy will run on all models. You can optionally restrict it to specific models below."
message={t("policies.addPolicyForm.modelScopeAlertTitle")}
description={t("policies.addPolicyForm.modelScopeAlertDesc")}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item label="Model Condition Type">
<Form.Item label={t("policies.addPolicyForm.modelConditionTypeLabel")}>
<Radio.Group
value={modelConditionType}
onChange={(e) => {
@ -523,25 +538,29 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
form.setFieldValue("model_condition", undefined);
}}
>
<Radio value="model">Select Model</Radio>
<Radio value="regex">Custom Regex Pattern</Radio>
<Radio value="model">{t("policies.addPolicyForm.modelConditionSelectModel")}</Radio>
<Radio value="regex">{t("policies.addPolicyForm.modelConditionRegex")}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="model_condition"
label={modelConditionType === "model" ? "Model (Optional)" : "Regex Pattern (Optional)"}
label={
modelConditionType === "model"
? t("policies.addPolicyForm.modelConditionLabelModel")
: t("policies.addPolicyForm.modelConditionLabelRegex")
}
tooltip={
modelConditionType === "model"
? "Select a specific model to apply this policy to. Leave empty to apply to all models."
: "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models."
? t("policies.addPolicyForm.modelConditionTooltipModel")
: t("policies.addPolicyForm.modelConditionTooltipRegex")
}
>
{modelConditionType === "model" ? (
<Select
showSearch
allowClear
placeholder="Leave empty to apply to all models"
placeholder={t("policies.addPolicyForm.modelConditionPlaceholderModel")}
options={availableModels.map((model) => ({
label: model,
value: model,
@ -550,16 +569,18 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
style={{ width: "100%" }}
/>
) : (
<TextInput placeholder="Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)" />
<TextInput placeholder={t("policies.addPolicyForm.modelConditionPlaceholderRegex")} />
)}
</Form.Item>
<div className="flex justify-end space-x-2 mt-4">
<Button variant="secondary" onClick={handleClose}>
Cancel
{t("common.cancel")}
</Button>
<Button onClick={handleSubmit} loading={isSubmitting}>
{isEditing ? "Update Policy" : "Create Policy"}
{isEditing
? t("policies.addPolicyForm.updatePolicyButton")
: t("policies.addPolicyForm.createPolicyButton")}
</Button>
</div>
</Form>

View file

@ -4654,5 +4654,82 @@
"editVariableName": "Edit variable name",
"variableNamePlaceholder": "Variable name"
}
},
"policies": {
"addAttachmentForm": {
"modalTitle": "Create Policy Attachment",
"policiesLabel": "Policies",
"policiesRequired": "Please select at least one policy",
"policiesPlaceholder": "Select policies to attach",
"scopeDivider": "Scope",
"scopeTypeLabel": "Scope Type",
"scopeSpecific": "Specific (teams, keys, models, or tags)",
"scopeGlobal": "Global (applies to all requests)",
"teamsLabel": "Teams",
"teamsTooltip": "Select team aliases or enter custom patterns. Supports wildcards (e.g., healthcare-*)",
"teamsPlaceholder": "Select or enter team aliases",
"keysLabel": "Keys",
"keysTooltip": "Select key aliases or enter custom patterns. Supports wildcards (e.g., dev-*)",
"keysPlaceholder": "Select or enter key aliases",
"modelsLabel": "Models",
"modelsTooltip": "Model names this attachment applies to. Supports wildcards (e.g., gpt-4*). Leave empty to apply to all models.",
"modelsPlaceholder": "Select or enter model names (e.g., gpt-4, bedrock/*)",
"tagsLabel": "Tags",
"tagsTooltip": "Match against tags set in key or team metadata. Use exact values (e.g., healthcare) or wildcard patterns (e.g., health-*) where * matches any suffix.",
"tagsExtra": "Matches tags from key/team <code>metadata.tags</code> or tags passed dynamically in the request body. Use <code>*</code> as a suffix wildcard (e.g., <code>prod-*</code> matches <code>prod-us</code>, <code>prod-eu</code>).",
"tagsPlaceholder": "Type a tag and press Enter (e.g. healthcare, prod-*)",
"estimateImpact": "Estimate Impact",
"createAttachment": "Create Attachment",
"attachmentCreatedSuccess_one": "Attachment created successfully",
"attachmentCreatedSuccess_other": "{{count}} attachments created successfully",
"attachmentsPartialSuccess": "{{successCount}} attachments created, {{failedCount}} failed",
"createAttachmentFailed": "Failed to create attachment: {{error}}"
},
"addPolicyForm": {
"simpleModeTitle": "Simple Mode",
"simpleModeSubtitle": "Pick guardrails from a list. All run in parallel.",
"flowBuilderTitle": "Flow Builder",
"flowBuilderSubtitle": "Define steps, conditions, and error responses.",
"newBadge": "NEW",
"createNewPolicyTitle": "Create New Policy",
"editPolicyTitle": "Edit Policy",
"flowBuilderRedirectAlert": "You'll be redirected to the full-screen Flow Builder to design your policy logic visually.",
"continueToBuilder": "Continue to Builder",
"createPolicyButton": "Create Policy",
"updatePolicyButton": "Update Policy",
"policyNameLabel": "Policy Name",
"policyNameRequired": "Please enter a policy name",
"policyNamePattern": "Policy name can only contain letters, numbers, hyphens, and underscores",
"policyNamePlaceholder": "e.g., global-baseline, healthcare-compliance",
"descriptionPlaceholder": "Describe what this policy does...",
"inheritanceDivider": "Inheritance",
"inheritFromLabel": "Inherit From",
"inheritFromTooltip": "Inherit guardrails from another policy. The child policy will include all guardrails from the parent.",
"inheritFromPlaceholder": "Select a parent policy (optional)",
"guardrailsDivider": "Guardrails",
"guardrailsAddLabel": "Guardrails to Add",
"guardrailsAddTooltip": "These guardrails will be added to requests matching this policy",
"guardrailsAddPlaceholder": "Select guardrails to add",
"guardrailsRemoveLabel": "Guardrails to Remove",
"guardrailsRemoveTooltip": "These guardrails will be removed from inherited guardrails",
"guardrailsRemovePlaceholder": "Select guardrails to remove (from inherited)",
"resolvedGuardrailsTitle": "Resolved Guardrails",
"resolvedGuardrailsDesc": "These are the final guardrails that will be applied (including inheritance):",
"conditionsDivider": "Conditions (Optional)",
"modelScopeAlertTitle": "Model Scope",
"modelScopeAlertDesc": "By default, this policy will run on all models. You can optionally restrict it to specific models below.",
"modelConditionTypeLabel": "Model Condition Type",
"modelConditionSelectModel": "Select Model",
"modelConditionRegex": "Custom Regex Pattern",
"modelConditionLabelModel": "Model (Optional)",
"modelConditionLabelRegex": "Regex Pattern (Optional)",
"modelConditionTooltipModel": "Select a specific model to apply this policy to. Leave empty to apply to all models.",
"modelConditionTooltipRegex": "Enter a regex pattern to match models (e.g., gpt-4.* or bedrock/.*). Leave empty to apply to all models.",
"modelConditionPlaceholderModel": "Leave empty to apply to all models",
"modelConditionPlaceholderRegex": "Leave empty to apply to all models (e.g., gpt-4.* or bedrock/claude-.*)",
"policyUpdatedSuccess": "Policy updated successfully",
"policyCreatedSuccess": "Policy created successfully",
"savePolicyFailed": "Failed to save policy: {{error}}"
}
}
}

View file

@ -4654,5 +4654,82 @@
"editVariableName": "编辑变量名",
"variableNamePlaceholder": "变量名"
}
},
"policies": {
"addAttachmentForm": {
"modalTitle": "创建策略附加",
"policiesLabel": "策略",
"policiesRequired": "请至少选择一个策略",
"policiesPlaceholder": "选择要附加的策略",
"scopeDivider": "范围",
"scopeTypeLabel": "范围类型",
"scopeSpecific": "特定(团队、密钥、模型或标签)",
"scopeGlobal": "全局(适用于所有请求)",
"teamsLabel": "团队",
"teamsTooltip": "选择团队别名或输入自定义模式,支持通配符(如 healthcare-*)",
"teamsPlaceholder": "选择或输入团队别名",
"keysLabel": "密钥",
"keysTooltip": "选择密钥别名或输入自定义模式,支持通配符(如 dev-*)",
"keysPlaceholder": "选择或输入密钥别名",
"modelsLabel": "模型",
"modelsTooltip": "此附加适用的模型名称,支持通配符(如 gpt-4*)。留空则适用于所有模型。",
"modelsPlaceholder": "选择或输入模型名称(如 gpt-4、bedrock/*)",
"tagsLabel": "标签",
"tagsTooltip": "匹配密钥或团队元数据中设置的标签。使用精确值(如 healthcare)或通配符模式(如 health-*),* 匹配任意后缀。",
"tagsExtra": "匹配密钥/团队 <code>metadata.tags</code> 中的标签或请求体中动态传入的标签。使用 <code>*</code> 作为后缀通配符(如 <code>prod-*</code> 匹配 <code>prod-us</code>、<code>prod-eu</code>)。",
"tagsPlaceholder": "输入标签后按 Enter(如 healthcare、prod-*)",
"estimateImpact": "预估影响",
"createAttachment": "创建附加",
"attachmentCreatedSuccess_one": "附加创建成功",
"attachmentCreatedSuccess_other": "成功创建 {{count}} 个附加",
"attachmentsPartialSuccess": "成功创建 {{successCount}} 个附加,{{failedCount}} 个失败",
"createAttachmentFailed": "创建附加失败:{{error}}"
},
"addPolicyForm": {
"simpleModeTitle": "简单模式",
"simpleModeSubtitle": "从列表中选择护栏,所有护栏并行运行。",
"flowBuilderTitle": "流程构建器",
"flowBuilderSubtitle": "定义步骤、条件和错误响应。",
"newBadge": "新",
"createNewPolicyTitle": "创建新策略",
"editPolicyTitle": "编辑策略",
"flowBuilderRedirectAlert": "您将被重定向到全屏流程构建器,以可视化方式设计策略逻辑。",
"continueToBuilder": "前往构建器",
"createPolicyButton": "创建策略",
"updatePolicyButton": "更新策略",
"policyNameLabel": "策略名称",
"policyNameRequired": "请输入策略名称",
"policyNamePattern": "策略名称只能包含字母、数字、连字符和下划线",
"policyNamePlaceholder": "如:global-baseline、healthcare-compliance",
"descriptionPlaceholder": "描述此策略的用途...",
"inheritanceDivider": "继承",
"inheritFromLabel": "继承自",
"inheritFromTooltip": "从另一个策略继承护栏,子策略将包含父策略的所有护栏。",
"inheritFromPlaceholder": "选择父策略(可选)",
"guardrailsDivider": "护栏",
"guardrailsAddLabel": "要添加的护栏",
"guardrailsAddTooltip": "这些护栏将被添加到匹配此策略的请求中",
"guardrailsAddPlaceholder": "选择要添加的护栏",
"guardrailsRemoveLabel": "要移除的护栏",
"guardrailsRemoveTooltip": "这些护栏将从继承的护栏中移除",
"guardrailsRemovePlaceholder": "选择要移除的护栏(来自继承)",
"resolvedGuardrailsTitle": "已解析护栏",
"resolvedGuardrailsDesc": "以下是最终将应用的护栏(含继承):",
"conditionsDivider": "条件(可选)",
"modelScopeAlertTitle": "模型范围",
"modelScopeAlertDesc": "默认情况下,此策略将在所有模型上运行。您可以在下方选择性限制为特定模型。",
"modelConditionTypeLabel": "模型条件类型",
"modelConditionSelectModel": "选择模型",
"modelConditionRegex": "自定义正则表达式",
"modelConditionLabelModel": "模型(可选)",
"modelConditionLabelRegex": "正则表达式(可选)",
"modelConditionTooltipModel": "选择将此策略应用于的特定模型,留空则应用于所有模型。",
"modelConditionTooltipRegex": "输入匹配模型的正则表达式(如 gpt-4.* 或 bedrock/.*),留空则应用于所有模型。",
"modelConditionPlaceholderModel": "留空则应用于所有模型",
"modelConditionPlaceholderRegex": "留空则应用于所有模型(如 gpt-4.* 或 bedrock/claude-.*)",
"policyUpdatedSuccess": "策略更新成功",
"policyCreatedSuccess": "策略创建成功",
"savePolicyFailed": "保存策略失败:{{error}}"
}
}
}