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}
@@ -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")} )} )} 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" ? ( 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")} +