diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml
index b0916e65f..6eb507fa7 100644
--- a/frontend/editor/public/locales/en-GB/translation.toml
+++ b/frontend/editor/public/locales/en-GB/translation.toml
@@ -5865,6 +5865,106 @@ deleteConfirmTitle = "Delete {{label}} policy?"
deleteConfirmBody = "This removes the policy and its workflow. Documents already processed are not affected."
deleteConfirmTitle = "Delete {{label}} policy?"
+[policies.catalog]
+compliance = "Compliance"
+ingestion = "Ingestion"
+retention = "Retention"
+routing = "Routing"
+security = "Security"
+
+[policies.detail]
+editSettings = "Edit Settings"
+enforces = "Enforces"
+managedByOrg = "Managed by your organization. Contact an admin to change this policy."
+noActivityDescription = "Documents will appear here once this policy runs."
+noActivityTitle = "No activity yet"
+onEveryUpload = "On every upload"
+originalsNote = "Originals stay untouched • Enforced version saved alongside"
+pause = "Pause"
+recentActivity = "Recent Activity"
+resume = "Resume"
+retry = "Retry"
+showLess = "Show less"
+showMore = "Show more"
+statActive = "Active"
+statDataProcessed = "Data processed"
+statDocsEnforced = "Docs enforced"
+statusActive = "Active"
+statusPaused = "Paused"
+
+[policies.fields]
+selectedCount = "{{count}} selected"
+
+[policies.pii]
+fieldLabel = "PII to redact"
+placeholder = "Select PII types"
+
+[policies.sidebar]
+activeCount = "{{count}} active"
+infoAriaLabel = "What is a policy?"
+infoTooltip = "A policy is a fixed set of tools that runs automatically whenever it's triggered — for example when a new document arrives — enforcing rules like redacting PII with no manual steps."
+loading = "Loading…"
+railAriaLabel = "{{label}} policy — {{status}}"
+railSuffixActive = " (Active)"
+railSuffixPaused = " (Paused)"
+setUp = "Set up"
+title = "Policies"
+upgradeToEnterprise = "Upgrade to enterprise"
+
+[policies.status]
+active = "Active"
+paused = "Paused"
+setup = "Set up"
+
+[policies.toolConfig]
+enableAriaLabel = "Enable {{tool}}"
+infoAriaLabel = "What does {{tool}} do?"
+
+[policies.wizard]
+allDocTypesDescription = "Enable the Classification policy to filter by document type."
+allDocTypesTitle = "All document types"
+back = "Back"
+builderDesc = "Build the sequence of tools this policy runs on each document."
+clear = "Clear"
+continue = "Continue"
+docTypesLabel = "Document types"
+edit = "Edit"
+editTitle = "Edit {{label}} Policy"
+enablePolicy = "Enable Policy"
+filenameAutoNumber = "Auto-number"
+filenamePositionAria = "Filename position"
+filenamePrefix = "Prefix"
+filenameSuffix = "Suffix"
+filenameTextAria = "Filename text"
+filenameTextPlaceholder = "Text to add (optional)"
+lockedDescription = "Contact an admin to change this policy."
+lockedTitle = "Managed by your organization"
+maxRetriesLabel = "Max retries"
+noToolsError = "Add at least one configured tool to the workflow first."
+outputAsLabel = "Output as"
+outputFilenameSubhead = "Output filename"
+outputModeAria = "Output mode"
+outputNewFile = "New file"
+outputNewVersion = "New version"
+outputRetriesLabel = "Output & retries"
+outputSubhead = "Output"
+retryDelayAria = "Retry delay minutes"
+retryDelayLabel = "Retry delay (min)"
+runOnExport = "Export"
+runOnLabel = "Run on"
+runOnSubhead = "Run on"
+runOnUpload = "Upload"
+saveChanges = "Save Changes"
+saveError = "Couldn't save the policy. Please try again."
+setupClassification = "Set up Classification"
+setupTitle = "Set up {{label}} Policy"
+sourcesDesc = "Choose where this policy runs and which document types it applies to."
+sourcesLabel = "Sources"
+stepOf = "Step {{step}} of {{total}}"
+toolChainDesc = "Configure the tools this policy runs on each document."
+typesSelected = "{{count}} types selected"
+
+
[printFile]
title = "Print File"
diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx
index cc34c5e5e..55e0231db 100644
--- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx
@@ -4,6 +4,29 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MantineProvider } from "@mantine/core";
+// The global setup mock returns the i18n KEY (t: (key) => key); these tests
+// assert the rendered English copy, so override locally to return the default
+// value passed to t() — mirroring i18next's runtime fallback when a key is
+// missing. (Interpolated strings are returned raw; no test here asserts them.)
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string, defaultValue?: string) =>
+ typeof defaultValue === "string" ? defaultValue : key,
+ i18n: { changeLanguage: vi.fn() },
+ }),
+ initReactI18next: { type: "3rdParty", init: vi.fn() },
+ I18nextProvider: ({ children }: { children: ReactNode }) => children,
+}));
+
+// usePolicies derives `canConfigure` from app-config; with no AppConfigProvider
+// here `config` is null, which (tri-state gate) hides the edit affordances. Mock
+// app-config as a single-user deployment (login off) so the local operator can
+// configure and the narrative view's Edit/Pause/Delete actions render.
+vi.mock("@app/contexts/AppConfigContext", async (orig) => ({
+ ...(await orig()),
+ useAppConfig: () => ({ config: { enableLogin: false } }),
+}));
+
// The shared Tooltip (used by the "what is a policy?" info button) pulls in
// preferences/sidebar contexts we don't set up here — passthrough it.
vi.mock("@app/components/shared/Tooltip", () => ({
diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx
index 5f401167c..a319ac6ee 100644
--- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx
@@ -13,6 +13,7 @@
*/
import { useState, useEffect, useMemo, type ReactNode } from "react";
+import { useTranslation } from "react-i18next";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import LocalIcon from "@app/components/shared/LocalIcon";
import { usePolicies } from "@app/hooks/usePolicies";
@@ -75,6 +76,7 @@ export function PoliciesSection({
* collapse button), mirroring the back-button + title in a policy. */
leadingControl?: ReactNode;
} = {}) {
+ const { t } = useTranslation();
const pol = usePolicies();
const { categories } = usePolicyCatalog();
// Persist the expand/collapse state across refreshes.
@@ -109,21 +111,26 @@ export function PoliciesSection({
{leadingControl}
{cat.icon}
- {cat.label}
+
+ {t(`policies.catalog.${cat.id}`, cat.label)}
+
- Upgrade to enterprise
+ {t(
+ "policies.sidebar.upgradeToEnterprise",
+ "Upgrade to enterprise",
+ )}
@@ -169,16 +181,20 @@ export function PoliciesSection({
{cat.icon}
- {cat.label}
+
+ {t(`policies.catalog.${cat.id}`, cat.label)}
+
{status === "setup" ? (
- Set up
+
+ {t("policies.sidebar.setUp", "Set up")}
+
) : (
- {STATUS_LABEL[status]}
+ {t(`policies.status.${status}`, STATUS_LABEL[status])}
)}
-
Loading…
+
+ {t("policies.sidebar.loading", "Loading…")}
+
);
@@ -404,6 +423,7 @@ export function PoliciesCollapsedButton({
}: {
onExpand: () => void;
}) {
+ const { t } = useTranslation();
const pol = usePolicies();
const { categories } = usePolicyCatalog();
@@ -416,16 +436,21 @@ export function PoliciesCollapsedButton({
.filter((cat) => !cat.comingSoon)
.map((cat) => {
const status = deriveRowStatus(pol.policies[cat.id]);
+ const label = t(`policies.catalog.${cat.id}`, cat.label);
+ const statusLabel = t(
+ `policies.status.${status}`,
+ STATUS_LABEL[status],
+ );
const suffix =
status === "active"
- ? " (Active)"
+ ? t("policies.sidebar.railSuffixActive", " (Active)")
: status === "paused"
- ? " (Paused)"
+ ? t("policies.sidebar.railSuffixPaused", " (Paused)")
: "";
return (
{
selectPolicy(cat.id);
onExpand();
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx
index f24f66ba8..76d5a9688 100644
--- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx
@@ -1,4 +1,5 @@
import { useState } from "react";
+import { useTranslation } from "react-i18next";
import PublicIcon from "@mui/icons-material/Public";
import ScheduleIcon from "@mui/icons-material/Schedule";
import HistoryIcon from "@mui/icons-material/History";
@@ -66,7 +67,13 @@ function humanizeOperation(op: string): string {
* lengthy is clamped and collapsed by default with a Show more/less toggle.
* Short messages (e.g. "Enforcement failed") render plainly with no toggle.
*/
-function ActivityError({ message }: { message: string }) {
+function ActivityError({
+ message,
+ t,
+}: {
+ message: string;
+ t: (key: string, defaultValue: string) => string;
+}) {
const [expanded, setExpanded] = useState(false);
const needsToggle = message.length > 80 || message.includes("\n");
if (!needsToggle) return <>{message}>;
@@ -82,7 +89,9 @@ function ActivityError({ message }: { message: string }) {
className="pol-activity-error__toggle"
onClick={() => setExpanded((v) => !v)}
>
- {expanded ? "Show less" : "Show more"}
+ {expanded
+ ? t("policies.detail.showLess", "Show less")
+ : t("policies.detail.showMore", "Show more")}
);
@@ -104,6 +113,7 @@ export function PolicyDetailPanel({
onDelete,
onRetry,
}: PolicyDetailPanelProps) {
+ const { t } = useTranslation();
const isPaused = status === "paused";
// Real configured steps drive the flow; fall back to the preset's rule labels.
const enforceItems =
@@ -123,7 +133,7 @@ export function PolicyDetailPanel({
- {isPaused ? "Paused" : "Active"}
+ {isPaused
+ ? t("policies.detail.statusPaused", "Paused")
+ : t("policies.detail.statusActive", "Active")}
}
/>
@@ -139,7 +151,9 @@ export function PolicyDetailPanel({
{/* Enforces */}
-
Enforces
+
+ {t("policies.detail.enforces", "Enforces")}
+
@@ -151,19 +165,24 @@ export function PolicyDetailPanel({
- On every upload
+ {t("policies.detail.onEveryUpload", "On every upload")}
- Originals stay untouched • Enforced version saved alongside
+ {t(
+ "policies.detail.originalsNote",
+ "Originals stay untouched • Enforced version saved alongside",
+ )}
{/* Recent Activity */}
-
Recent Activity
+
+ {t("policies.detail.recentActivity", "Recent Activity")}
+
{activityItems.length > 0 ? (
{activityItems.map((item, i) => (
@@ -192,7 +211,7 @@ export function PolicyDetailPanel({
title={item.doc}
description={
item.status === "flagged" ? (
-
+
) : (
item.action
)
@@ -205,7 +224,7 @@ export function PolicyDetailPanel({
size="sm"
onClick={() => onRetry(item)}
>
- Retry
+ {t("policies.detail.retry", "Retry")}
) : undefined
}
@@ -217,8 +236,11 @@ export function PolicyDetailPanel({
}
- title="No activity yet"
- description="Documents will appear here once this policy runs."
+ title={t("policies.detail.noActivityTitle", "No activity yet")}
+ description={t(
+ "policies.detail.noActivityDescription",
+ "Documents will appear here once this policy runs.",
+ )}
/>
)}
@@ -232,15 +254,21 @@ export function PolicyDetailPanel({
{statValues.enforced.toLocaleString()}
-
Docs enforced
+
+ {t("policies.detail.statDocsEnforced", "Docs enforced")}
+
{statValues.dataProcessed}
- Data processed
+
+ {t("policies.detail.statDataProcessed", "Data processed")}
+
{statValues.activeFor}
- Active
+
+ {t("policies.detail.statActive", "Active")}
+
@@ -249,7 +277,10 @@ export function PolicyDetailPanel({
}
- description="Managed by your organization. Contact an admin to change this policy."
+ description={t(
+ "policies.detail.managedByOrg",
+ "Managed by your organization. Contact an admin to change this policy.",
+ )}
/>
)}
@@ -265,14 +296,16 @@ export function PolicyDetailPanel({
onClick={onDelete}
style={{ marginRight: "auto" }}
>
- Delete
+ {t("delete", "Delete")}
)}
- {isPaused ? "Resume" : "Pause"}
+ {isPaused
+ ? t("policies.detail.resume", "Resume")
+ : t("policies.detail.pause", "Pause")}
- Edit Settings
+ {t("policies.detail.editSettings", "Edit Settings")}
)}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyFieldRow.tsx b/frontend/editor/src/proprietary/components/policies/PolicyFieldRow.tsx
index ff848100c..e169ffe8f 100644
--- a/frontend/editor/src/proprietary/components/policies/PolicyFieldRow.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PolicyFieldRow.tsx
@@ -1,3 +1,4 @@
+import { useTranslation } from "react-i18next";
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
import { Select } from "@shared/components/Select";
import { Input } from "@shared/components/Input";
@@ -25,6 +26,11 @@ export function PolicyFieldRow({
onChange,
first,
}: PolicyFieldRowProps) {
+ const { t } = useTranslation();
+ // Field labels and option labels come from the policy catalog data, so they're
+ // wrapped at the render site with data-keyed ids (English stays the fallback).
+ const fieldLabel = t(`policies.field.${field.key}`, field.label);
+
if (field.type === "chips") {
const selected = Array.isArray(value) ? value : [];
const toggle = (opt: string) =>
@@ -36,8 +42,12 @@ export function PolicyFieldRow({
return (
- {field.label}
- {selected.length} selected
+ {fieldLabel}
+
+ {t("policies.fields.selectedCount", "{{count}} selected", {
+ count: selected.length,
+ })}
+
{(field.options ?? []).map((opt) => (
@@ -47,7 +57,7 @@ export function PolicyFieldRow({
size="sm"
onClick={() => toggle(opt)}
>
- {opt}
+ {t(`policies.fieldOption.${field.key}.${opt}`, opt)}
))}
@@ -61,28 +71,31 @@ export function PolicyFieldRow({
size="sm"
checked={Boolean(value)}
onChange={(checked) => onChange(checked)}
- aria-label={field.label}
+ aria-label={fieldLabel}
/>
) : field.type === "select" ? (
({ value: o, label: o }))}
+ options={(field.options ?? []).map((o) => ({
+ value: o,
+ label: t(`policies.fieldOption.${field.key}.${o}`, o),
+ }))}
value={typeof value === "string" ? value : ""}
onChange={(e) => onChange(e.target.value)}
- aria-label={field.label}
+ aria-label={fieldLabel}
/>
) : (
onChange(e.target.value)}
- aria-label={field.label}
+ aria-label={fieldLabel}
/>
);
return (
-
+
);
}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyPiiField.tsx b/frontend/editor/src/proprietary/components/policies/PolicyPiiField.tsx
index ed09a0d1d..d9fef2361 100644
--- a/frontend/editor/src/proprietary/components/policies/PolicyPiiField.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PolicyPiiField.tsx
@@ -1,4 +1,5 @@
import { MultiSelect } from "@mantine/core";
+import { useTranslation } from "react-i18next";
import { PII_PRESETS } from "@app/data/policyDefinitions";
/** The set of preset regexes — used to separate preset words from custom ones. */
@@ -24,6 +25,7 @@ export function PolicyPiiField({
onChange,
disabled,
}: PolicyPiiFieldProps) {
+ const { t } = useTranslation();
const words = Array.isArray(parameters.wordsToRedact)
? (parameters.wordsToRedact as string[])
: [];
@@ -48,9 +50,14 @@ export function PolicyPiiField({
return (
({ value: p.value, label: p.label }))}
+ label={t("policies.pii.fieldLabel", "PII to redact")}
+ placeholder={t("policies.pii.placeholder", "Select PII types")}
+ data={PII_PRESETS.map((p) => ({
+ value: p.value,
+ // Preset labels are catalog data — keyed by preset value with the
+ // English label as fallback.
+ label: t(`policies.pii.${p.value}`, p.label),
+ }))}
value={selected}
onChange={handleChange}
disabled={disabled}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx
index a36adc1d5..82bf6820d 100644
--- a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx
@@ -1,4 +1,5 @@
import { useState, useMemo, useRef } from "react";
+import { useTranslation } from "react-i18next";
import CloseIcon from "@mui/icons-material/Close";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import { PanelHeader } from "@shared/components/PanelHeader";
@@ -98,6 +99,7 @@ export function PolicySetupWizard({
onCommitConfig,
onSetupClassification,
}: PolicySetupWizardProps) {
+ const { t } = useTranslation();
const isEdit = mode === "edit";
// Preset (tool-chain) policies render the locked tool config as their Workflow
// step instead of the add/remove builder.
@@ -158,13 +160,27 @@ export function PolicySetupWizard({
@@ -212,7 +228,12 @@ export function PolicySetupWizard({
}),
).catch(() => {
setSubmitting(false);
- setSaveError("Couldn't save the policy. Please try again.");
+ setSaveError(
+ t(
+ "policies.wizard.saveError",
+ "Couldn't save the policy. Please try again.",
+ ),
+ );
});
};
@@ -243,7 +264,12 @@ export function PolicySetupWizard({
}),
).catch(() => {
setSubmitting(false);
- setSaveError("Couldn't save the policy. Please try again.");
+ setSaveError(
+ t(
+ "policies.wizard.saveError",
+ "Couldn't save the policy. Please try again.",
+ ),
+ );
});
};
@@ -260,7 +286,12 @@ export function PolicySetupWizard({
// the user back to the Workflow step to fix it.
const handleSaveFailed = () => {
setSubmitting(false);
- setSaveError("Add at least one configured tool to the workflow first.");
+ setSaveError(
+ t(
+ "policies.wizard.noToolsError",
+ "Add at least one configured tool to the workflow first.",
+ ),
+ );
setStep(1);
};
@@ -269,14 +300,25 @@ export function PolicySetupWizard({
}
/>
@@ -302,7 +344,10 @@ export function PolicySetupWizard({
{toolChain ? (
<>
- Configure the tools this policy runs on each document.
+ {t(
+ "policies.wizard.toolChainDesc",
+ "Configure the tools this policy runs on each document.",
+ )}
- Build the sequence of tools this policy runs on each document.
+ {t(
+ "policies.wizard.builderDesc",
+ "Build the sequence of tools this policy runs on each document.",
+ )}
Output & retries
+
+ {t("policies.wizard.outputRetriesLabel", "Output & retries")}
+
{/* The editor event the policy runs on: input on upload, or
output on export (enforced before the file is exported). */}
- Run on
+
+ {t("policies.wizard.runOnSubhead", "Run on")}
+
setRunOn(e.target.value as "upload" | "export")
}
- aria-label="Run on"
+ aria-label={t("policies.wizard.runOnLabel", "Run on")}
options={[
- { value: "upload", label: "Upload" },
- { value: "export", label: "Export" },
+ {
+ value: "upload",
+ label: t("policies.wizard.runOnUpload", "Upload"),
+ },
+ {
+ value: "export",
+ label: t("policies.wizard.runOnExport", "Export"),
+ },
]}
/>
}
/>
- Output
+
+ {t("policies.wizard.outputSubhead", "Output")}
+
}
/>
{/* Output filename: position + custom text together as one row. */}
- Output filename
+
+ {t("policies.wizard.outputFilenameSubhead", "Output filename")}
+
@@ -435,15 +526,21 @@ export function PolicySetupWizard({
inputSize="sm"
value={outputName}
onChange={(e) => setOutputName(e.target.value)}
- placeholder="Text to add (optional)"
- aria-label="Filename text"
+ placeholder={t(
+ "policies.wizard.filenameTextPlaceholder",
+ "Text to add (optional)",
+ )}
+ aria-label={t(
+ "policies.wizard.filenameTextAria",
+ "Filename text",
+ )}
/>
)}
setMaxRetries(Math.max(0, Number(e.target.value) || 0))
}
- aria-label="Max retries"
+ aria-label={t(
+ "policies.wizard.maxRetriesLabel",
+ "Max retries",
+ )}
/>
}
/>
}
/>
@@ -484,10 +590,14 @@ export function PolicySetupWizard({
{SOURCES_IN_FLOW && step === 3 && (
<>
- Choose where this policy runs and which document types it applies
- to.
+ {t(
+ "policies.wizard.sourcesDesc",
+ "Choose where this policy runs and which document types it applies to.",
+ )}
+
+
+ {t("policies.wizard.sourcesLabel", "Sources")}
-
Sources
{sourceDefs.map((src, i) => (
-
Document types
+
+ {t("policies.wizard.docTypesLabel", "Document types")}
+
{!classificationEnabled ? (
}
- title="All document types"
- description="Enable the Classification policy to filter by document type."
+ title={t(
+ "policies.wizard.allDocTypesTitle",
+ "All document types",
+ )}
+ description={t(
+ "policies.wizard.allDocTypesDescription",
+ "Enable the Classification policy to filter by document type.",
+ )}
action={
- Set up Classification
+ {t(
+ "policies.wizard.setupClassification",
+ "Set up Classification",
+ )}
}
/>
@@ -528,14 +649,23 @@ export function PolicySetupWizard({
{scopeTypes.length === 0
- ? "All document types"
- : `${scopeTypes.length} types selected`}
+ ? t(
+ "policies.wizard.allDocTypesTitle",
+ "All document types",
+ )
+ : t(
+ "policies.wizard.typesSelected",
+ "{{count}} types selected",
+ { count: scopeTypes.length },
+ )}
setScopeNarrow((v) => !v)}
>
- {scopeNarrow ? "Clear" : "Edit"}
+ {scopeNarrow
+ ? t("policies.wizard.clear", "Clear")
+ : t("policies.wizard.edit", "Edit")}
{scopeNarrow && (
@@ -564,7 +694,9 @@ export function PolicySetupWizard({
- {step > 1 ? "Back" : "Cancel"}
+ {step > 1
+ ? t("policies.wizard.back", "Back")
+ : t("cancel", "Cancel")}
{step < TOTAL_STEPS ? (
setStep((s) => Math.min(TOTAL_STEPS, s + 1))}
>
- Continue
+ {t("policies.wizard.continue", "Continue")}
) : (
- {isEdit ? "Save Changes" : "Enable Policy"}
+ {isEdit
+ ? t("policies.wizard.saveChanges", "Save Changes")
+ : t("policies.wizard.enablePolicy", "Enable Policy")}
)}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyToolConfig.tsx b/frontend/editor/src/proprietary/components/policies/PolicyToolConfig.tsx
index 45d581d40..aad707fbe 100644
--- a/frontend/editor/src/proprietary/components/policies/PolicyToolConfig.tsx
+++ b/frontend/editor/src/proprietary/components/policies/PolicyToolConfig.tsx
@@ -1,4 +1,5 @@
import { Suspense } from "react";
+import { useTranslation } from "react-i18next";
import { Loader } from "@mantine/core";
import { ToggleSwitch } from "@shared/components/ToggleSwitch";
import { Card } from "@shared/components/Card";
@@ -9,13 +10,24 @@ import { PolicyWatermarkConfig } from "@app/components/policies/PolicyWatermarkC
import type { ToolRegistry } from "@app/data/toolsTaxonomy";
import type { ToolId } from "@app/types/toolId";
-/** Plain-language, non-technical descriptions shown by each tool's info button. */
-const TOOL_PLAIN_INFO: Record
= {
- redact:
+/**
+ * Plain-language, non-technical descriptions shown by each tool's info button.
+ * Stored as [i18n key, English default] pairs so they can be resolved with `t`
+ * at render (the map lives at module scope, outside the component).
+ */
+const TOOL_PLAIN_INFO: Record = {
+ redact: [
+ "policies.toolConfig.info.redact",
"Automatically finds and blacks out sensitive details — like Social Security and card numbers — so they can't be read in the document.",
- sanitize:
+ ],
+ sanitize: [
+ "policies.toolConfig.info.sanitize",
"Removes hidden JavaScript from the file, so nothing can run automatically when someone opens it.",
- watermark: "Stamps a visible mark (e.g. “Confidential”) across every page.",
+ ],
+ watermark: [
+ "policies.toolConfig.info.watermark",
+ "Stamps a visible mark (e.g. “Confidential”) across every page.",
+ ],
};
/** One tool in a policy's fixed chain: whether it runs + its configured params. */
@@ -51,6 +63,7 @@ export function PolicyToolConfig({
onChange,
editable = true,
}: PolicyToolConfigProps) {
+ const { t } = useTranslation();
const patchTool = (index: number, patch: Partial) =>
onChange(tools.map((t, i) => (i === index ? { ...t, ...patch } : t)));
@@ -59,23 +72,27 @@ export function PolicyToolConfig({
{tools.map((tool, index) => {
const entry = toolRegistry[tool.operation as ToolId];
const Settings = entry?.automationSettings ?? null;
+ const toolName = entry?.name ?? tool.operation;
+ const plainInfo = TOOL_PLAIN_INFO[tool.operation];
return (
{entry?.icon}
-
- {entry?.name ?? tool.operation}
-
- {TOOL_PLAIN_INFO[tool.operation] && (
+
{toolName}
+ {plainInfo && (
patchTool(index, { enabled: checked })}
- aria-label={`Enable ${entry?.name ?? tool.operation}`}
+ aria-label={t("policies.toolConfig.enableAriaLabel", "Enable {{tool}}", {
+ tool: toolName,
+ })}
/>
{tool.enabled &&