diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index af5ed16169..f8287a89b0 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -6958,6 +6958,12 @@ sectionsAriaLabel = "Infrastructure sections" subtitle = "Deployments, credentials, security posture, storage, and the audit trail for your Stirling workspace." title = "Infrastructure" +# Fixed-enum label maps rendered via t(MAP[value]) in the infrastructure tabs. +[portal.infrastructure.apiKeyPermission] +admin = "Admin" +read = "Read" +write = "Write" + [portal.infrastructure.apiKeys] createKey = "Create key" heading = "API keys" @@ -6978,6 +6984,11 @@ usageToday = "Usage today" description = "Create a scoped key to start calling the Stirling API." title = "No API keys yet" +[portal.infrastructure.attestationLabel] +attested = "Attested" +inScope = "In scope" +notApplicable = "N/A" + [portal.infrastructure.audit] filterAriaLabel = "Filter audit events by category" heading = "Audit logs" @@ -7034,6 +7045,24 @@ elevation = "Elevation" processing = "Processing" totalEvents = "Total events · 24h" +[portal.infrastructure.auditCatLabel] +auth = "Auth" +config = "Config" +elevation = "Elevation" +processing = "Processing" +security = "Security" + +[portal.infrastructure.auditStatusLabel] +danger = "Error" +info = "Info" +success = "Success" +warning = "Warning" + +[portal.infrastructure.certLabel] +certified = "Certified" +inProgress = "In progress" +notStarted = "Not started" + [portal.infrastructure.createKey] cancel = "Cancel" createKey = "Create key" @@ -7050,6 +7079,12 @@ subtitleCreated = "Copy this secret now — it won't be shown again." title = "Create API key" titleCreated = "Key created" +[portal.infrastructure.deployLabel] +live = "Live" +queued = "Queued" +rolledBack = "Rolled back" +rolling = "Rolling out" + [portal.infrastructure.deployments] msValue = "{{value}} ms" throughputValue = "{{value}}/min" @@ -7085,6 +7120,16 @@ subheading = "Live health for every deployed Stirling region — latency, load, description = "Deployed regions appear here once your workspace is provisioned." title = "No regions deployed" +[portal.infrastructure.keyLabel] +active = "Active" +revoked = "Revoked" +rotateSoon = "Rotate soon" + +[portal.infrastructure.modelLabel] +active = "Active" +degraded = "Degraded" +disabled = "Disabled" + [portal.infrastructure.models] heading = "Models" msValue = "{{value}} ms" @@ -7112,6 +7157,10 @@ status = "Status" type = "Type" version = "Version" +[portal.infrastructure.models.cost] +perCall = "{{price}}/call" +perThousand = "{{price}}/1k" + [portal.infrastructure.models.metrics] activeModels = "Active models" avgLatency = "Avg latency" @@ -7135,6 +7184,17 @@ modelForAria = "Model for {{operation}}" operation = "Operation" routedTo = "Routed to" +[portal.infrastructure.modelTypeLabel] +classification = "Classification" +extraction = "Extraction" +llm = "LLM" +ocr = "OCR" + +[portal.infrastructure.regionLabel] +degraded = "Degraded" +down = "Down" +healthy = "Healthy" + [portal.infrastructure.security.access.byok] description = "Supply a key from your own KMS. Stirling encrypts with it but can still read." label = "Bring your own key (BYOK)" @@ -7809,6 +7869,56 @@ blurb = "Evaluate Stirling against your documents and workflows." gatingAction = "Build your quote" label = "Trial" +[portal.procurement.keyDocs] +oneTimeFee = " · one-time {{amount}}" +subtitle = "Everything for each stage of your rollout, in one place." +title = "Key documents" + +[portal.procurement.keyDocs.docs.baa] +name = "Business Associate Agreement" +sub = "HIPAA · available on request" + +[portal.procurement.keyDocs.docs.bankTransfer] +name = "Bank transfer instructions" +sub = "Wire details for your AP team" + +[portal.procurement.keyDocs.docs.coi] +name = "Certificate of Insurance" +sub = "Cyber + E&O · current policy" + +[portal.procurement.keyDocs.docs.formalQuote] +name = "Formal quote" +sub = "Built to your volume, term, and service level" + +[portal.procurement.keyDocs.docs.msa] +name = "Master Services Agreement" +sub = "One signature - MSA, order form, EULA, and DPA combined" + +[portal.procurement.keyDocs.docs.purchaseOrder] +name = "Purchase order" +sub = "Issuing a PO? Upload it and we invoice against it" + +[portal.procurement.keyDocs.docs.securityReview] +name = "Custom security review" +sub = "We complete your questionnaire and join your review call" + +[portal.procurement.keyDocs.docs.soc2] +name = "SOC 2 Type II report" +sub = "Audited · NDA-gated" + +[portal.procurement.keyDocs.docs.w9] +name = "IRS Form W-9" +sub = "Stirling PDF Inc." + +[portal.procurement.keyDocs.groups] +evaluation = "Supporting your evaluation" +yourDeal = "Your deal" + +[portal.procurement.keyDocs.status] +action = "Action needed" +available = "Download" +request = "Request" + [portal.procurement.license] copied = "Copied" copy = "Copy key" @@ -8114,6 +8224,10 @@ summary = "Owns a team — manages its members' resources and shared configs." 2 = "Portal access via the default policy" 3 = "Everything Member can do" +[portal.users.seats] +limited = "{{used}} / {{limit}}" +unlimited = "{{used}} · Unlimited" + [portal.welcome] ariaLabel = "Welcome to Stirling PDF" badge = "Open-source" diff --git a/frontend/editor/src/portal/components/RecentActivity.tsx b/frontend/editor/src/portal/components/RecentActivity.tsx index 28a273bb40..3ebd940151 100644 --- a/frontend/editor/src/portal/components/RecentActivity.tsx +++ b/frontend/editor/src/portal/components/RecentActivity.tsx @@ -111,10 +111,10 @@ export function RecentActivity() {
{event.target}
- {AUDIT_CAT_LABEL[event.category]} · {event.actor} + {t(AUDIT_CAT_LABEL[event.category])} · {event.actor} - {AUDIT_STATUS_LABEL[event.status]} + {t(AUDIT_STATUS_LABEL[event.status])}
diff --git a/frontend/editor/src/portal/components/documents/DocumentOverview.tsx b/frontend/editor/src/portal/components/documents/DocumentOverview.tsx index cfe7a38118..593ff1fb99 100644 --- a/frontend/editor/src/portal/components/documents/DocumentOverview.tsx +++ b/frontend/editor/src/portal/components/documents/DocumentOverview.tsx @@ -22,7 +22,9 @@ export function DocumentOverview({ doc }: { doc: ReviewDocument }) { = { cloud: "Cloud", docker: "Docker", diff --git a/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx b/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx index 58008b0837..221ad0543a 100644 --- a/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx +++ b/frontend/editor/src/portal/components/infrastructure/ApiKeyCard.tsx @@ -25,7 +25,7 @@ export function ApiKeyCard({ apiKey }: { apiKey: ApiKey }) { - {KEY_LABEL[apiKey.status]} + {t(KEY_LABEL[apiKey.status])} {apiKey.permissions.map((p) => ( - {p} + {t( + `portal.infrastructure.apiKeyPermission.${p.toLowerCase()}`, + p, + )} ))} diff --git a/frontend/editor/src/portal/components/infrastructure/AuditTab.tsx b/frontend/editor/src/portal/components/infrastructure/AuditTab.tsx index 0b0d806c59..07bff185da 100644 --- a/frontend/editor/src/portal/components/infrastructure/AuditTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/AuditTab.tsx @@ -68,7 +68,7 @@ export function AuditTab() { render: (e) => (
- {AUDIT_CAT_LABEL[e.category]} + {t(AUDIT_CAT_LABEL[e.category])} {e.action}
@@ -89,7 +89,7 @@ export function AuditTab() { header: t("portal.infrastructure.audit.columns.status"), render: (e) => ( - {AUDIT_STATUS_LABEL[e.status]} + {t(AUDIT_STATUS_LABEL[e.status])} ), }, diff --git a/frontend/editor/src/portal/components/infrastructure/CreateKeyModal.tsx b/frontend/editor/src/portal/components/infrastructure/CreateKeyModal.tsx index 613eb97377..8461b9dd43 100644 --- a/frontend/editor/src/portal/components/infrastructure/CreateKeyModal.tsx +++ b/frontend/editor/src/portal/components/infrastructure/CreateKeyModal.tsx @@ -126,7 +126,10 @@ export function CreateKeyModal({ {PERMISSION_OPTS.map((p) => ( togglePerm(p)} /> diff --git a/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx b/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx index 18967c2aaa..59e4b04d54 100644 --- a/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/DeploymentsTab.tsx @@ -22,8 +22,8 @@ import { DEPLOY_LABEL, DEPLOY_TONE, pct, + REGION_LABEL, REGION_TONE, - titleCase, } from "@portal/components/infrastructure/infraFormat"; export function DeploymentsTab() { @@ -79,7 +79,7 @@ export function DeploymentsTab() { size="sm" pulse={r.status === "healthy"} > - {titleCase(r.status)} + {t(REGION_LABEL[r.status])} ), }, @@ -164,7 +164,7 @@ export function DeploymentsTab() { header: t("portal.infrastructure.deployments.deployColumns.status"), render: (d) => ( - {DEPLOY_LABEL[d.status]} + {t(DEPLOY_LABEL[d.status])} ), }, diff --git a/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx b/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx index 56f0583cdb..86b563aabd 100644 --- a/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/ModelsTab.tsx @@ -57,7 +57,7 @@ export function ModelsTab() { header: t("portal.infrastructure.models.columns.type"), render: (m) => ( - {MODEL_TYPE_LABEL[m.type]} + {t(MODEL_TYPE_LABEL[m.type])} ), }, @@ -70,7 +70,7 @@ export function ModelsTab() { size="sm" pulse={m.status === "active"} > - {MODEL_LABEL[m.status]} + {t(MODEL_LABEL[m.status])} ), }, @@ -101,7 +101,7 @@ export function ModelsTab() { align: "right", render: (m) => ( - {modelCost(m.cost, m.costUnit)} + {modelCost(t, m.cost, m.costUnit)} ), }, diff --git a/frontend/editor/src/portal/components/infrastructure/SecurityTab.tsx b/frontend/editor/src/portal/components/infrastructure/SecurityTab.tsx index 0d299d7032..ccc1b1d3c5 100644 --- a/frontend/editor/src/portal/components/infrastructure/SecurityTab.tsx +++ b/frontend/editor/src/portal/components/infrastructure/SecurityTab.tsx @@ -259,7 +259,7 @@ export function SecurityTab() {
{c.name} - {CERT_LABEL[c.status]} + {t(CERT_LABEL[c.status])}

{c.detail}

@@ -283,7 +283,7 @@ export function SecurityTab() {
{a.name} - {ATTESTATION_LABEL[a.status]} + {t(ATTESTATION_LABEL[a.status])}
{a.framework} diff --git a/frontend/editor/src/portal/components/infrastructure/infraFormat.ts b/frontend/editor/src/portal/components/infrastructure/infraFormat.ts index df3f993360..975a3a61e4 100644 --- a/frontend/editor/src/portal/components/infrastructure/infraFormat.ts +++ b/frontend/editor/src/portal/components/infrastructure/infraFormat.ts @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next"; import type { ChipAccent, StatusTone } from "@app/ui"; import type { ApiKeyStatus, @@ -30,6 +31,13 @@ export const REGION_TONE: Record = { down: "danger", }; +// Label maps hold i18n keys, resolved via t(MAP[value]) at the render sites. +export const REGION_LABEL: Record = { + healthy: "portal.infrastructure.regionLabel.healthy", + degraded: "portal.infrastructure.regionLabel.degraded", + down: "portal.infrastructure.regionLabel.down", +}; + export const DEPLOY_TONE: Record = { live: "success", rolling: "info", @@ -38,10 +46,10 @@ export const DEPLOY_TONE: Record = { }; export const DEPLOY_LABEL: Record = { - live: "Live", - rolling: "Rolling out", - "rolled-back": "Rolled back", - queued: "Queued", + live: "portal.infrastructure.deployLabel.live", + rolling: "portal.infrastructure.deployLabel.rolling", + "rolled-back": "portal.infrastructure.deployLabel.rolledBack", + queued: "portal.infrastructure.deployLabel.queued", }; export const KEY_TONE: Record = { @@ -51,9 +59,9 @@ export const KEY_TONE: Record = { }; export const KEY_LABEL: Record = { - active: "Active", - revoked: "Revoked", - "rotate-soon": "Rotate soon", + active: "portal.infrastructure.keyLabel.active", + revoked: "portal.infrastructure.keyLabel.revoked", + "rotate-soon": "portal.infrastructure.keyLabel.rotateSoon", }; export const CERT_TONE: Record = { @@ -63,11 +71,12 @@ export const CERT_TONE: Record = { }; export const CERT_LABEL: Record = { - certified: "Certified", - "in-progress": "In progress", - "not-started": "Not started", + certified: "portal.infrastructure.certLabel.certified", + "in-progress": "portal.infrastructure.certLabel.inProgress", + "not-started": "portal.infrastructure.certLabel.notStarted", }; +// Brand/acronym key-management modes are not localised. export const KEY_MODE_LABEL: Record = { managed: "Stirling-managed", byok: "BYOK", @@ -82,9 +91,9 @@ export const KEY_MODE_TONE: Record = { }; export const ATTESTATION_LABEL: Record = { - attested: "Attested", - "in-scope": "In scope", - "not-applicable": "N/A", + attested: "portal.infrastructure.attestationLabel.attested", + "in-scope": "portal.infrastructure.attestationLabel.inScope", + "not-applicable": "portal.infrastructure.attestationLabel.notApplicable", }; export const ATTESTATION_TONE: Record = { @@ -102,18 +111,18 @@ export const AUDIT_TONE: Record = { // Human labels for the status badge: danger/warning are tones, not outcomes (read "Error"). export const AUDIT_STATUS_LABEL: Record = { - success: "Success", - warning: "Warning", - danger: "Error", - info: "Info", + success: "portal.infrastructure.auditStatusLabel.success", + warning: "portal.infrastructure.auditStatusLabel.warning", + danger: "portal.infrastructure.auditStatusLabel.danger", + info: "portal.infrastructure.auditStatusLabel.info", }; export const AUDIT_CAT_LABEL: Record = { - auth: "Auth", - config: "Config", - elevation: "Elevation", - processing: "Processing", - security: "Security", + auth: "portal.infrastructure.auditCatLabel.auth", + config: "portal.infrastructure.auditCatLabel.config", + elevation: "portal.infrastructure.auditCatLabel.elevation", + processing: "portal.infrastructure.auditCatLabel.processing", + security: "portal.infrastructure.auditCatLabel.security", }; export const AUDIT_CAT_TONE: Record = { @@ -131,16 +140,16 @@ export const MODEL_TONE: Record = { }; export const MODEL_LABEL: Record = { - active: "Active", - degraded: "Degraded", - disabled: "Disabled", + active: "portal.infrastructure.modelLabel.active", + degraded: "portal.infrastructure.modelLabel.degraded", + disabled: "portal.infrastructure.modelLabel.disabled", }; export const MODEL_TYPE_LABEL: Record = { - extraction: "Extraction", - classification: "Classification", - ocr: "OCR", - llm: "LLM", + extraction: "portal.infrastructure.modelTypeLabel.extraction", + classification: "portal.infrastructure.modelTypeLabel.classification", + ocr: "portal.infrastructure.modelTypeLabel.ocr", + llm: "portal.infrastructure.modelTypeLabel.llm", }; export const MODEL_TYPE_TONE: Record = { @@ -150,6 +159,7 @@ export const MODEL_TYPE_TONE: Record = { llm: "warning", }; +// Provider names are proper nouns, not localised. export const MODEL_PROVIDER_LABEL: Record = { stirling: "Stirling", openai: "OpenAI", @@ -158,8 +168,14 @@ export const MODEL_PROVIDER_LABEL: Record = { }; /** Render a model's cost with the unit it's billed against. */ -export function modelCost(cost: number, unit: ModelCostUnit): string { - if (cost === 0) return "Included"; +export function modelCost( + t: TFunction, + cost: number, + unit: ModelCostUnit, +): string { + if (cost === 0) return t("portal.infrastructure.models.metrics.included"); const price = `$${cost.toFixed(unit === "per-call" ? 3 : 2)}`; - return unit === "per-call" ? `${price}/call` : `${price}/1k`; + return unit === "per-call" + ? t("portal.infrastructure.models.cost.perCall", { price }) + : t("portal.infrastructure.models.cost.perThousand", { price }); } diff --git a/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx b/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx index 3046e895f2..048bc2d606 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementExtras.tsx @@ -77,70 +77,76 @@ function SideModal({ // ── Key documents ──────────────────────────────────────────────────────────── type DocStatus = "available" | "action" | "request"; interface DocRow { - name: string; - sub: string; + nameKey: string; + subKey: string; status: DocStatus; fee?: number; } -const STAGE_DOCS: { group: string; docs: DocRow[] }[] = [ +// Static demo catalogue for the pilot; the copy lives in the locale under +// portal.procurement.keyDocs and is resolved via t() at render time. +const STAGE_DOCS: { groupKey: string; docs: DocRow[] }[] = [ { - group: "Your deal", + groupKey: "portal.procurement.keyDocs.groups.yourDeal", docs: [ { - name: "Formal quote", - sub: "Built to your volume, term, and service level", + nameKey: "portal.procurement.keyDocs.docs.formalQuote.name", + subKey: "portal.procurement.keyDocs.docs.formalQuote.sub", status: "available", }, { - name: "Master Services Agreement", - sub: "One signature — MSA, order form, EULA, and DPA combined", + nameKey: "portal.procurement.keyDocs.docs.msa.name", + subKey: "portal.procurement.keyDocs.docs.msa.sub", status: "action", }, { - name: "Bank transfer instructions", - sub: "Wire details for your AP team", + nameKey: "portal.procurement.keyDocs.docs.bankTransfer.name", + subKey: "portal.procurement.keyDocs.docs.bankTransfer.sub", status: "available", }, { - name: "Purchase order", - sub: "Issuing a PO? Upload it and we invoice against it", + nameKey: "portal.procurement.keyDocs.docs.purchaseOrder.name", + subKey: "portal.procurement.keyDocs.docs.purchaseOrder.sub", status: "request", }, ], }, { - group: "Supporting your evaluation", + groupKey: "portal.procurement.keyDocs.groups.evaluation", docs: [ { - name: "SOC 2 Type II report", - sub: "Audited · NDA-gated", + nameKey: "portal.procurement.keyDocs.docs.soc2.name", + subKey: "portal.procurement.keyDocs.docs.soc2.sub", status: "available", }, { - name: "Custom security review", - sub: "We complete your questionnaire and join your review call", + nameKey: "portal.procurement.keyDocs.docs.securityReview.name", + subKey: "portal.procurement.keyDocs.docs.securityReview.sub", status: "request", fee: 5000, }, { - name: "Business Associate Agreement", - sub: "HIPAA · available on request", + nameKey: "portal.procurement.keyDocs.docs.baa.name", + subKey: "portal.procurement.keyDocs.docs.baa.sub", status: "request", fee: 2500, }, - { name: "IRS Form W-9", sub: "Stirling PDF Inc.", status: "available" }, { - name: "Certificate of Insurance", - sub: "Cyber + E&O · current policy", + nameKey: "portal.procurement.keyDocs.docs.w9.name", + subKey: "portal.procurement.keyDocs.docs.w9.sub", + status: "available", + }, + { + nameKey: "portal.procurement.keyDocs.docs.coi.name", + subKey: "portal.procurement.keyDocs.docs.coi.sub", status: "available", }, ], }, ]; const STATUS_LABEL: Record = { - available: "Download", - action: "Action needed", - request: "Request", + available: "portal.procurement.keyDocs.status.available", + action: "portal.procurement.keyDocs.status.action", + request: "portal.procurement.keyDocs.status.request", }; export function KeyDocumentsModal({ @@ -150,31 +156,36 @@ export function KeyDocumentsModal({ open: boolean; onClose: () => void; }) { + const { t } = useTranslation(); return ( {STAGE_DOCS.map((g) => ( -
-
{g.group}
+
+
{t(g.groupKey)}
    {g.docs.map((d) => ( -
  • +
  • - {d.name} + {t(d.nameKey)} - {d.sub} - {d.fee ? ` · one-time $${d.fee.toLocaleString()}` : ""} + {t(d.subKey)} + {d.fee + ? t("portal.procurement.keyDocs.oneTimeFee", { + amount: `$${d.fee.toLocaleString()}`, + }) + : ""}
    - {STATUS_LABEL[d.status]} + {t(STATUS_LABEL[d.status])}
  • ))} diff --git a/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx b/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx index bf3f463585..852c4e68c4 100644 --- a/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx +++ b/frontend/editor/src/portal/components/procurement/ProcurementModal.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; +import { useTranslation } from "react-i18next"; import "@portal/views/Procurement.css"; /** Keep keyboard focus inside an open dialog: focus it on open and wrap Tab at the edges. */ @@ -58,6 +59,7 @@ export function ProcurementModal({ subtitle?: string; children: React.ReactNode; }) { + const { t } = useTranslation(); const trapRef = useFocusTrap(open); useEffect(() => { @@ -86,7 +88,7 @@ export function ProcurementModal({ type="button" className="portal-procmodal__close" onClick={onClose} - aria-label="Close" + aria-label={t("portal.procurement.modal.close")} > ✕ diff --git a/frontend/editor/src/portal/components/users/format.ts b/frontend/editor/src/portal/components/users/format.ts index f17abe85e0..334509bf0f 100644 --- a/frontend/editor/src/portal/components/users/format.ts +++ b/frontend/editor/src/portal/components/users/format.ts @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next"; import type { AvatarTone, ChipAccent } from "@app/ui"; import type { Member, RoleId } from "@portal/api/users"; import { ROLE_TONE } from "@portal/api/users"; @@ -23,8 +24,14 @@ export function chipAccentForRole(role: RoleId): ChipAccent { } /** Seats used / limit as display copy; null limit → "Unlimited". */ -export function seatsLabel(used: number, limit: number | null): string { - return limit === null ? `${used} · Unlimited` : `${used} / ${limit}`; +export function seatsLabel( + t: TFunction, + used: number, + limit: number | null, +): string { + return limit === null + ? t("portal.users.seats.unlimited", { used }) + : t("portal.users.seats.limited", { used, limit }); } /** diff --git a/frontend/editor/src/portal/views/Documents.tsx b/frontend/editor/src/portal/views/Documents.tsx index b94102bd4f..c083071bcb 100644 --- a/frontend/editor/src/portal/views/Documents.tsx +++ b/frontend/editor/src/portal/views/Documents.tsx @@ -1,3 +1,4 @@ +import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; import { Button } from "@app/ui"; import { useTier } from "@portal/contexts/TierContext"; @@ -30,20 +31,23 @@ function csvCell(value: string): string { } /** Flatten the visible columns to CSV, matching the on-screen table. */ -function toCsv(docs: ReviewDocument[]): string { +function toCsv(docs: ReviewDocument[], t: TFunction): string { const header = [ - "Document", - "Product", - "Pipeline / Action", - "User", - "Status", - "Time", + t("portal.documents.table.columns.document"), + t("portal.documents.table.columns.product"), + t("portal.documents.table.columns.action"), + t("portal.documents.table.columns.user"), + t("portal.documents.table.columns.status"), + t("portal.documents.table.columns.time"), ]; const lines = [header.map(csvCell).join(",")]; for (const d of docs) { - const action = d.product === "Editor" || !d.action ? "Editor" : d.action; + const action = + d.product === "Editor" || !d.action + ? t("portal.documents.table.editorAction") + : d.action; const status = - DOCUMENT_STATUS_LABEL[d.status] + + t(DOCUMENT_STATUS_LABEL[d.status]) + (d.status === "in-review" && d.reviewer ? ` · ${d.reviewer}` : ""); lines.push( [d.name, d.product, action, d.user || "", status, d.time] @@ -66,7 +70,7 @@ export function Documents() { const documents = state.data?.documents ?? []; function exportCsv() { - const blob = new Blob([toCsv(documents)], { + const blob = new Blob([toCsv(documents, t)], { type: "text/csv;charset=utf-8", }); const url = URL.createObjectURL(blob);