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);