From 3b8dd45a73cb2835ad2b5440ffcc3a166bf13aeb Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 31 Mar 2026 15:09:14 -0700 Subject: [PATCH] Translate siem --- messages/en-US.json | 86 +++++++- .../[orgId]/settings/logs/streaming/page.tsx | 137 ++++++------ src/components/HttpDestinationCredenza.tsx | 201 ++++++------------ 3 files changed, 224 insertions(+), 200 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index e8c7cb47d..412d50179 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2804,5 +2804,89 @@ "approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review", "approvalsEmptyStateButtonText": "Manage Roles", "domainErrorTitle": "We are having trouble verifying your domain", - "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the Auto Provision Settings tab." + "idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the Auto Provision Settings tab.", + "streamingTitle": "Event Streaming", + "streamingDescription": "Stream events from your organization to external destinations in real time.", + "streamingUnnamedDestination": "Unnamed destination", + "streamingNoUrlConfigured": "No URL configured", + "streamingAddDestination": "Add Destination", + "streamingHttpWebhookTitle": "HTTP Webhook", + "streamingHttpWebhookDescription": "Send events to any HTTP endpoint with flexible authentication and templating.", + "streamingS3Title": "Amazon S3", + "streamingS3Description": "Stream events to an S3-compatible object storage bucket. Coming soon.", + "streamingDatadogTitle": "Datadog", + "streamingDatadogDescription": "Forward events directly to your Datadog account. Coming soon.", + "streamingTypePickerDescription": "Choose a destination type to get started.", + "streamingFailedToLoad": "Failed to load destinations", + "streamingUnexpectedError": "An unexpected error occurred.", + "streamingFailedToUpdate": "Failed to update destination", + "streamingDeletedSuccess": "Destination deleted successfully", + "streamingFailedToDelete": "Failed to delete destination", + "streamingDeleteTitle": "Delete Destination", + "streamingDeleteButtonText": "Delete Destination", + "streamingDeleteDialogAreYouSure": "Are you sure you want to delete", + "streamingDeleteDialogThisDestination": "this destination", + "streamingDeleteDialogPermanentlyRemoved": "? All configuration will be permanently removed.", + "httpDestEditTitle": "Edit Destination", + "httpDestAddTitle": "Add HTTP Destination", + "httpDestEditDescription": "Update the configuration for this HTTP event streaming destination.", + "httpDestAddDescription": "Configure a new HTTP endpoint to receive your organization's events.", + "httpDestTabSettings": "Settings", + "httpDestTabHeaders": "Headers", + "httpDestTabBody": "Body", + "httpDestTabLogs": "Logs", + "httpDestNamePlaceholder": "My HTTP destination", + "httpDestUrlLabel": "Destination URL", + "httpDestUrlErrorHttpRequired": "URL must use http or https", + "httpDestUrlErrorHttpsRequired": "HTTPS is required on cloud deployments", + "httpDestUrlErrorInvalid": "Enter a valid URL (e.g. https://example.com/webhook)", + "httpDestAuthTitle": "Authentication", + "httpDestAuthDescription": "Choose how requests to your endpoint are authenticated.", + "httpDestAuthNoneTitle": "No Authentication", + "httpDestAuthNoneDescription": "Sends requests without an Authorization header.", + "httpDestAuthBearerTitle": "Bearer Token", + "httpDestAuthBearerDescription": "Adds an Authorization: Bearer header to each request.", + "httpDestAuthBearerPlaceholder": "Your API key or token", + "httpDestAuthBasicTitle": "Basic Auth", + "httpDestAuthBasicDescription": "Adds an Authorization: Basic header. Provide credentials as username:password.", + "httpDestAuthBasicPlaceholder": "username:password", + "httpDestAuthCustomTitle": "Custom Header", + "httpDestAuthCustomDescription": "Specify a custom HTTP header name and value for authentication (e.g. X-API-Key).", + "httpDestAuthCustomHeaderNamePlaceholder": "Header name (e.g. X-API-Key)", + "httpDestAuthCustomHeaderValuePlaceholder": "Header value", + "httpDestCustomHeadersTitle": "Custom HTTP Headers", + "httpDestCustomHeadersDescription": "Add custom headers to every outgoing request. Useful for static tokens or a custom Content-Type. By default, Content-Type: application/json is sent.", + "httpDestNoHeadersConfigured": "No custom headers configured. Click \"Add Header\" to add one.", + "httpDestHeaderNamePlaceholder": "Header name", + "httpDestHeaderValuePlaceholder": "Value", + "httpDestAddHeader": "Add Header", + "httpDestBodyTemplateTitle": "Custom Body Template", + "httpDestBodyTemplateDescription": "Control the JSON payload structure sent to your endpoint. If disabled, a default JSON object is sent for each event.", + "httpDestEnableBodyTemplate": "Enable custom body template", + "httpDestBodyTemplateLabel": "Body Template (JSON)", + "httpDestBodyTemplateHint": "Use template variables to reference event fields in your payload.", + "httpDestPayloadFormatTitle": "Payload Format", + "httpDestPayloadFormatDescription": "How events are serialised into each request body.", + "httpDestFormatJsonArrayTitle": "JSON Array", + "httpDestFormatJsonArrayDescription": "One request per batch, body is a JSON array. Compatible with most generic webhooks and Datadog.", + "httpDestFormatNdjsonTitle": "NDJSON", + "httpDestFormatNdjsonDescription": "One request per batch, body is newline-delimited JSON — one object per line, no outer array. Required by Splunk HEC, Elastic / OpenSearch, and Grafana Loki.", + "httpDestFormatSingleTitle": "One Event Per Request", + "httpDestFormatSingleDescription": "Sends a separate HTTP POST for each individual event. Use only for endpoints that cannot handle batches.", + "httpDestLogTypesTitle": "Log Types", + "httpDestLogTypesDescription": "Choose which log types are forwarded to this destination. Only enabled log types will be streamed.", + "httpDestAccessLogsTitle": "Access Logs", + "httpDestAccessLogsDescription": "Resource access attempts, including authenticated and denied requests.", + "httpDestActionLogsTitle": "Action Logs", + "httpDestActionLogsDescription": "Administrative actions performed by users within the organization.", + "httpDestConnectionLogsTitle": "Connection Logs", + "httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.", + "httpDestRequestLogsTitle": "Request Logs", + "httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.", + "httpDestSaveChanges": "Save Changes", + "httpDestCreateDestination": "Create Destination", + "httpDestUpdatedSuccess": "Destination updated successfully", + "httpDestCreatedSuccess": "Destination created successfully", + "httpDestUpdateFailed": "Failed to update destination", + "httpDestCreateFailed": "Failed to create destination" } diff --git a/src/app/[orgId]/settings/logs/streaming/page.tsx b/src/app/[orgId]/settings/logs/streaming/page.tsx index 843f1ddb7..44ec39fcd 100644 --- a/src/app/[orgId]/settings/logs/streaming/page.tsx +++ b/src/app/[orgId]/settings/logs/streaming/page.tsx @@ -38,6 +38,7 @@ import { HttpDestinationCredenza, parseHttpConfig } from "@app/components/HttpDestinationCredenza"; +import { useTranslations } from "next-intl"; // ── Re-export Destination so the rest of the file can use it ────────────────── @@ -69,6 +70,7 @@ function DestinationCard({ isToggling, disabled = false }: DestinationCardProps) { + const t = useTranslations(); const cfg = parseHttpConfig(destination.config); return ( @@ -84,7 +86,7 @@ function DestinationCard({

- {cfg.name || "Unnamed destination"} + {cfg.name || t("streamingUnnamedDestination")}

HTTP @@ -104,7 +106,7 @@ function DestinationCard({ {/* URL preview */}

{cfg.url || ( - No URL configured + {t("streamingNoUrlConfigured")} )}

@@ -116,7 +118,7 @@ function DestinationCard({ disabled={disabled} className="flex-1" > - Edit + {t("edit")} @@ -134,7 +136,7 @@ function DestinationCard({ className="text-destructive focus:text-destructive" onClick={() => onDelete(destination)} > - Delete + {t("delete")} @@ -146,6 +148,8 @@ function DestinationCard({ // ── Add destination card ─────────────────────────────────────────────────────── function AddDestinationCard({ onClick }: { onClick: () => void }) { + const t = useTranslations(); + return (
); @@ -166,48 +170,6 @@ function AddDestinationCard({ onClick }: { onClick: () => void }) { type DestinationType = "http" | "s3" | "datadog"; -const destinationTypeOptions: ReadonlyArray> = [ - { - id: "http", - title: "HTTP Webhook", - description: - "Send events to any HTTP endpoint with flexible authentication and templating.", - icon: - }, - { - id: "s3", - title: "Amazon S3", - description: - "Stream events to an S3-compatible object storage bucket. Coming soon.", - disabled: true, - icon: ( - Amazon S3 - ) - }, - { - id: "datadog", - title: "Datadog", - description: - "Forward events directly to your Datadog account. Coming soon.", - disabled: true, - icon: ( - Datadog - ) - } -]; - interface DestinationTypePickerProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -221,8 +183,48 @@ function DestinationTypePicker({ onSelect, isPaywalled = false }: DestinationTypePickerProps) { + const t = useTranslations(); const [selected, setSelected] = useState("http"); + const destinationTypeOptions: ReadonlyArray> = [ + { + id: "http", + title: t("streamingHttpWebhookTitle"), + description: t("streamingHttpWebhookDescription"), + icon: + }, + { + id: "s3", + title: t("streamingS3Title"), + description: t("streamingS3Description"), + disabled: true, + icon: ( + {t("streamingS3Title")} + ) + }, + { + id: "datadog", + title: t("streamingDatadogTitle"), + description: t("streamingDatadogDescription"), + disabled: true, + icon: ( + {t("streamingDatadogTitle")} + ) + } + ]; + useEffect(() => { if (open) setSelected("http"); }, [open]); @@ -231,9 +233,9 @@ function DestinationTypePicker({ - Add Destination + {t("streamingAddDestination")} - Choose a destination type to get started. + {t("streamingTypePickerDescription")} @@ -248,13 +250,13 @@ function DestinationTypePicker({ - + @@ -269,6 +271,7 @@ export default function StreamingDestinationsPage() { const api = createApiClient(useEnvContext()); const { isPaidUser } = usePaidStatus(); const isEnterprise = isPaidUser(tierMatrix[TierFeature.SIEM]); + const t = useTranslations(); const [destinations, setDestinations] = useState([]); const [loading, setLoading] = useState(true); @@ -297,10 +300,10 @@ export default function StreamingDestinationsPage() { } catch (e) { toast({ variant: "destructive", - title: "Failed to load destinations", + title: t("streamingFailedToLoad"), description: formatAxiosError( e, - "An unexpected error occurred." + t("streamingUnexpectedError") ) }); } finally { @@ -337,10 +340,10 @@ export default function StreamingDestinationsPage() { ); toast({ variant: "destructive", - title: "Failed to update destination", + title: t("streamingFailedToUpdate"), description: formatAxiosError( e, - "An unexpected error occurred." + t("streamingUnexpectedError") ) }); } finally { @@ -364,17 +367,17 @@ export default function StreamingDestinationsPage() { await api.delete( `/org/${orgId}/event-streaming-destination/${deleteTarget.destinationId}` ); - toast({ title: "Destination deleted successfully" }); + toast({ title: t("streamingDeletedSuccess") }); setDeleteDialogOpen(false); setDeleteTarget(null); loadDestinations(); } catch (e) { toast({ variant: "destructive", - title: "Failed to delete destination", + title: t("streamingFailedToDelete"), description: formatAxiosError( e, - "An unexpected error occurred." + t("streamingUnexpectedError") ) }); } finally { @@ -400,8 +403,8 @@ export default function StreamingDestinationsPage() { return ( <> @@ -456,23 +459,23 @@ export default function StreamingDestinationsPage() { if (!v) setDeleteTarget(null); }} string={ - parseHttpConfig(deleteTarget.config).name || "delete" + parseHttpConfig(deleteTarget.config).name || t("streamingDeleteDialogThisDestination") } - title="Delete Destination" + title={t("streamingDeleteTitle")} dialog={

- Are you sure you want to delete{" "} + {t("streamingDeleteDialogAreYouSure")}{" "} {parseHttpConfig(deleteTarget.config).name || - "this destination"} + t("streamingDeleteDialogThisDestination")} - ? All configuration will be permanently removed. + {t("streamingDeleteDialogPermanentlyRemoved")}

} - buttonText="Delete Destination" + buttonText={t("streamingDeleteButtonText")} onConfirm={handleDeleteConfirm} /> )} ); -} +} \ No newline at end of file diff --git a/src/components/HttpDestinationCredenza.tsx b/src/components/HttpDestinationCredenza.tsx index 205c17c84..e39567332 100644 --- a/src/components/HttpDestinationCredenza.tsx +++ b/src/components/HttpDestinationCredenza.tsx @@ -24,6 +24,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { build } from "@server/build"; +import { useTranslations } from "next-intl"; // ── Types ────────────────────────────────────────────────────────────────────── @@ -91,6 +92,8 @@ interface HeadersEditorProps { } function HeadersEditor({ headers, onChange }: HeadersEditorProps) { + const t = useTranslations(); + const addRow = () => onChange([...headers, { key: "", value: "" }]); const removeRow = (i: number) => @@ -106,8 +109,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
{headers.length === 0 && (

- No custom headers configured. Click "Add Header" to add - one. + {t("httpDestNoHeadersConfigured")}

)} {headers.map((h, i) => ( @@ -115,7 +117,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) { updateRow(i, "key", e.target.value)} - placeholder="Header name" + placeholder={t("httpDestHeaderNamePlaceholder")} className="flex-1" /> updateRow(i, "value", e.target.value) } - placeholder="Value" + placeholder={t("httpDestHeaderValuePlaceholder")} className="flex-1" />
); @@ -169,6 +171,7 @@ export function HttpDestinationCredenza({ onSaved }: HttpDestinationCredenzaProps) { const api = createApiClient(useEnvContext()); + const t = useTranslations(); const [saving, setSaving] = useState(false); const [cfg, setCfg] = useState(defaultHttpConfig()); @@ -201,14 +204,14 @@ export function HttpDestinationCredenza({ parsed.protocol !== "http:" && parsed.protocol !== "https:" ) { - return "URL must use http or https"; + return t("httpDestUrlErrorHttpRequired"); } if (build === "saas" && parsed.protocol !== "https:") { - return "HTTPS is required on cloud deployments"; + return t("httpDestUrlErrorHttpsRequired"); } return null; } catch { - return "Enter a valid URL (e.g. https://example.com/webhook)"; + return t("httpDestUrlErrorInvalid"); } })(); @@ -234,13 +237,13 @@ export function HttpDestinationCredenza({ `/org/${orgId}/event-streaming-destination/${editing.destinationId}`, payload ); - toast({ title: "Destination updated successfully" }); + toast({ title: t("httpDestUpdatedSuccess") }); } else { await api.put( `/org/${orgId}/event-streaming-destination`, payload ); - toast({ title: "Destination created successfully" }); + toast({ title: t("httpDestCreatedSuccess") }); } onSaved(); onOpenChange(false); @@ -248,11 +251,11 @@ export function HttpDestinationCredenza({ toast({ variant: "destructive", title: editing - ? "Failed to update destination" - : "Failed to create destination", + ? t("httpDestUpdateFailed") + : t("httpDestCreateFailed"), description: formatAxiosError( e, - "An unexpected error occurred." + t("streamingUnexpectedError") ) }); } finally { @@ -266,13 +269,13 @@ export function HttpDestinationCredenza({ {editing - ? "Edit Destination" - : "Add HTTP Destination"} + ? t("httpDestEditTitle") + : t("httpDestAddTitle")} {editing - ? "Update the configuration for this HTTP event streaming destination." - : "Configure a new HTTP endpoint to receive your organization's events."} + ? t("httpDestEditDescription") + : t("httpDestAddDescription")} @@ -280,20 +283,20 @@ export function HttpDestinationCredenza({ {/* ── Settings tab ────────────────────────────── */}
{/* Name */}
- + update({ name: e.target.value }) @@ -304,7 +307,7 @@ export function HttpDestinationCredenza({ {/* URL */}

- Choose how requests to your endpoint - are authenticated. + {t("httpDestAuthDescription")}

@@ -352,14 +354,10 @@ export function HttpDestinationCredenza({ htmlFor="auth-none" className="cursor-pointer font-medium" > - No Authentication + {t("httpDestAuthNoneTitle")}

- Sends requests without an{" "} - - Authorization - {" "} - header. + {t("httpDestAuthNoneDescription")}

@@ -377,20 +375,15 @@ export function HttpDestinationCredenza({ htmlFor="auth-bearer" className="cursor-pointer font-medium" > - Bearer Token + {t("httpDestAuthBearerTitle")}

- Adds an{" "} - - Authorization: Bearer - <token> - {" "} - header to each request. + {t("httpDestAuthBearerDescription")}

{cfg.authType === "bearer" && ( - Basic Auth + {t("httpDestAuthBasicTitle")}

- Adds an{" "} - - Authorization: Basic - <credentials> - {" "} - header. Provide credentials - as{" "} - - username:password - - . + {t("httpDestAuthBasicDescription")}

{cfg.authType === "basic" && ( - Custom Header + {t("httpDestAuthCustomTitle")}

- Specify a custom HTTP - header name and value for - authentication (e.g.{" "} - - X-API-Key - - ). + {t("httpDestAuthCustomDescription")}

{cfg.authType === "custom" && (

- Add custom headers to every outgoing - request. Useful for static tokens or - custom{" "} - - Content-Type - - . By default,{" "} - - Content-Type: application/json - {" "} - is sent. + {t("httpDestCustomHeadersDescription")}

- Control the JSON payload structure sent to - your endpoint. If disabled, a default JSON - object is sent for each event. + {t("httpDestBodyTemplateDescription")}

@@ -568,14 +533,14 @@ export function HttpDestinationCredenza({ htmlFor="use-body-template" className="cursor-pointer" > - Enable custom body template + {t("httpDestEnableBodyTemplate")}
{cfg.useBodyTemplate && (