Remove new useTranslate usage

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2026-07-27 10:58:18 +02:00
committed by Bryan Frimin
parent 13ba251aba
commit 7d2b9b73b7
24 changed files with 229 additions and 270 deletions

View File

@@ -1170,7 +1170,7 @@
} }
} }
}, },
"common": { "error": "Error", "success": "Success", "create": "Create" }, "common": { "error": "Error", "success": "Success", "create": "Create", "actions": { "back": "Back", "close": "Close", "continue": "Continue", "copy": "Copy", "tryAgain": "Try again" }, "messages": { "copied": "Copied", "copiedToClipboard": "Copied to clipboard" } },
"mutation": { "mutation": {
"messages": { "completed": "Operation completed successfully" }, "messages": { "completed": "Operation completed successfully" },
"errors": { "commit": "Failed to commit this operation" } "errors": { "commit": "Failed to commit this operation" }
@@ -1331,10 +1331,10 @@
"membershipsDropdown": { "searchPlaceholder": "Search organizations...", "loading": "Loading organizations...", "addOrganization": "Add organization" }, "membershipsDropdown": { "searchPlaceholder": "Search organizations...", "loading": "Loading organizations...", "addOrganization": "Add organization" },
"membershipsDropdownInvitingItem": { "title": "Check your email to accept the invitation" }, "membershipsDropdownInvitingItem": { "title": "Check your email to accept the invitation" },
"membershipsDropdownMenu": { "pendingInvitations": "Pending invitations" }, "membershipsDropdownMenu": { "pendingInvitations": "Pending invitations" },
"sidebar": { "context": "Context", "tasks": "Tasks", "measures": "Measures", "risks": "Risks", "frameworks": "Frameworks", "people": "People", "thirdParties": "Third parties", "documents": "Documents", "assets": "Assets", "data": "Data", "audits": "Audits", "findings": "Findings", "obligations": "Obligations", "processingActivities": "Processing Activities", "statementsOfApplicability": "Statements of Applicability", "rightsRequests": "Rights Requests", "accessReviews": "Access Reviews", "compliancePage": "Compliance Page", "cookieBanners": "Cookie Banners", "settings": "Settings" }, "sidebar": { "context": "Context", "tasks": "Tasks", "measures": "Measures", "risks": "Risks", "frameworks": "Frameworks", "people": "People", "thirdParties": "Third parties", "documents": "Documents", "assets": "Assets", "devices": "Devices", "data": "Data", "audits": "Audits", "findings": "Findings", "obligations": "Obligations", "processingActivities": "Processing Activities", "statementsOfApplicability": "Statements of Applicability", "rightsRequests": "Rights Requests", "accessReviews": "Access Reviews", "compliancePage": "Compliance Page", "cookieBanners": "Cookie Banners", "settings": "Settings" },
"viewerMembershipDropdown": { "viewerMembershipDropdown": {
"errors": { "requestFailed": "Request failed", "cannotSignOut": "Cannot sign out" }, "errors": { "requestFailed": "Request failed", "cannotSignOut": "Cannot sign out" },
"actions": { "apiKeys": "API Keys", "oauthTokens": "OAuth tokens", "mySignatures": "My Signatures", "help": "Help", "logout": "Logout" } "actions": { "apiKeys": "API Keys", "oauthTokens": "OAuth tokens", "mySignatures": "My Signatures", "employeePortal": "Employee Portal", "help": "Help", "logout": "Logout" }
}, },
"peoplePage": { "title": "People", "actions": { "add": "Add Person" } }, "peoplePage": { "title": "People", "actions": { "add": "Add Person" } },
"personPage": { "personPage": {
@@ -2461,6 +2461,11 @@
"magicLinkExpired": { "title": "Link Expired", "description": "This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one." }, "magicLinkExpired": { "title": "Link Expired", "description": "This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one." },
"magicLinkForm": { "errors": { "send": "Cannot send magic link" }, "messages": { "sent": "Magic link sent!" }, "fields": { "email": "Email" }, "sentDescription": "Magic link sent! Check your email and use the link to continue.", "actions": { "resend": "Resend Link", "resendIn": "Resend Link in {{count}}s", "send": "Send Magic Link" } }, "magicLinkForm": { "errors": { "send": "Cannot send magic link" }, "messages": { "sent": "Magic link sent!" }, "fields": { "email": "Email" }, "sentDescription": "Magic link sent! Check your email and use the link to continue.", "actions": { "resend": "Resend Link", "resendIn": "Resend Link in {{count}}s", "send": "Send Magic Link" } },
"auth": { "actions": { "signIn": "Sign in" } }, "auth": { "actions": { "signIn": "Sign in" } },
"authError": { "enterpriseAccountRequired": { "title": "Enterprise account required", "description": "Personal Google and Microsoft accounts cannot be used to sign in. Please use your work or school account instead." }, "emailNotVerified": { "title": "Email not verified", "description": "Your email address is not verified with the identity provider. Please verify it, then try signing in again." }, "signInSessionExpired": { "title": "Sign-in session expired", "description": "This sign-in attempt is no longer valid. Please start again from the sign-in page." }, "magicLinkExpired": { "title": "Link Expired", "description": "This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one." }, "magicLinkAlreadyUsed": { "title": "Link Already Used", "description": "This magic link has already been used. Please request a new one." }, "invalidLink": { "title": "Invalid link", "description": "This magic link is invalid. Please request a new one." }, "default": { "title": "Authentication failed", "description": "We could not complete your sign-in. Please try again." } },
"accessReviewSource": { "documentation": "Documentation", "regions": { "label": "Region", "placeholder": "Select a region", "unitedStates": "United States", "europe": "Europe" } },
"devices": { "title": "Devices", "description": "Manage computers enrolled with the Probo posture agent.", "empty": "No devices enrolled yet", "actions": { "add": "Add device", "new": "New device", "reassign": "Re-assign", "revoke": "Revoke" }, "fields": { "organization": "Organization", "hostname": "Hostname", "owner": "Owner", "state": "State", "platform": "Platform", "osVersion": "OS version", "hardwareUuid": "Hardware UUID", "serialNumber": "Serial number", "agentVersion": "Agent version", "enrolledAt": "Enrolled at", "lastSeen": "Last seen" }, "values": { "pending": "(pending)", "never": "Never", "unassigned": "Unassigned" }, "messages": { "ownerUpdated": "Device owner updated", "revoked": "Device revoked" }, "errors": { "create": "Failed to create device", "reassign": "Failed to re-assign device", "revoke": "Failed to revoke device" }, "confirmations": { "revoke": "Revoke device \"{{hostname}}\"? The agent on the device will stop reporting and must be re-enrolled." }, "postures": { "title": "Posture", "navigation": "Postures", "summaryHelp": "How posture summary is calculated", "summaryDescription": "Shown as pass / fail / total.", "empty": "No posture checks recorded", "columns": { "check": "Check", "status": "Status", "observedAt": "Observed at" }, "status": { "pass": "Pass", "fail": "Fail", "unknown": "Unknown", "notApplicable": "Not applicable" }, "checks": { "diskEncryption": "Disk encryption", "screenLock": "Screen lock", "firewallEnabled": "Firewall enabled", "timeSync": "Time sync", "osVersion": "OS version", "autoUpdate": "Auto update", "passwordPolicy": "Password policy", "remoteLogin": "Remote login", "malwareProtection": "Malware protection" } } },
"employeeDevices": { "title": "Your devices" },
"deviceEnrollment": { "pageTitle": "Enroll device", "title": "Device enrollment", "setup": "Setup", "stepProgress": "Step {{current}} of {{total}}", "steps": { "privacy": { "title": "Privacy", "description": "Review collected data" }, "organization": { "title": "Organization", "description": "Choose destination workspace" }, "enroll": { "title": "Open and wait", "description": "Finish setup in the desktop agent" } }, "unavailable": { "title": "Enrollment unavailable", "description": "You do not have permission to enroll devices in any organization." }, "intro": { "title": "Before you start", "description": "Probo collects the following device metadata for inventory and posture reporting:" }, "privacy": { "identity": "Device identity: hardware UUID, hostname, and serial number (when available).", "systemDetails": "System details: platform, OS version, and Probo agent version.", "activitySignals": "Activity signals: enrollment time, heartbeats, and posture check results." }, "organization": { "title": "Choose organization", "description": "Pick which organization will own and manage this device." }, "openAgent": { "title": "Open the Probo agent", "description": "Open the desktop agent to finish setup, then keep this page open until enrollment is confirmed." }, "actions": { "backToOrganizations": "Back to organizations", "enrollNew": "Enroll new device", "openAgent": "Open Probo agent", "preparing": "Preparing…" }, "status": { "enrolledWithHostname": "{{hostname}} is enrolled.", "enrolled": "This device is enrolled.", "closeWindow": "You can close this window.", "waitingForCheckIn": "Waiting for the agent's first check-in…", "timedOut": "We haven't heard from the agent yet. Make sure the desktop agent is installed and running, then try again." }, "manual": { "cannotEnroll": "Can't enroll new device?", "tryCreating": "Try creating it manually", "title": "Manual enrollment", "creating": "Creating device…" }, "token": { "title": "Enrollment token generated", "description": "Share this enrollment token only with the device owner through a secure channel. It can be used once and expires after seven days.", "manualInstall": "Manual install (CLI / MDM)", "installUnix": "Install on macOS or Linux (run from a shell with sudo access)", "installWindows": "Install on Windows (run from an elevated PowerShell session)", "securityNotice": "The token is passed as a CLI flag (not via curl-piped-to-shell or sudo env vars). Once installed, the agent self-updates from GitHub Releases with cosign signature verification." }, "messages": { "created": "Device created. Copy the enrollment token now — it will not be shown again." }, "errors": { "copyToClipboard": "Failed to copy to clipboard" } },
"thirdPartyRiskAssessmentRow": { "thirdPartyRiskAssessmentRow": {
"expired": "Expired", "expired": "Expired",
"notes": "Notes" "notes": "Notes"

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button } from "@probo/ui"; import { Button } from "@probo/ui";
import { useSearchParams } from "react-router"; import { useSearchParams } from "react-router";
@@ -29,63 +29,49 @@ type AuthErrorContent = {
}; };
function useAuthErrorContent(code: string | null): AuthErrorContent { function useAuthErrorContent(code: string | null): AuthErrorContent {
const { __ } = useTranslate(); const { t } = useTranslation();
switch (code) { switch (code) {
case "personal_account_not_allowed": case "personal_account_not_allowed":
return { return {
title: __("Enterprise account required"), title: t("authError.enterpriseAccountRequired.title"),
description: __( description: t("authError.enterpriseAccountRequired.description"),
"Personal Google and Microsoft accounts cannot be used to sign in. Please use your work or school account instead.",
),
}; };
case "email_not_verified": case "email_not_verified":
return { return {
title: __("Email not verified"), title: t("authError.emailNotVerified.title"),
description: __( description: t("authError.emailNotVerified.description"),
"Your email address is not verified with the identity provider. Please verify it, then try signing in again.",
),
}; };
case "invalid_state": case "invalid_state":
return { return {
title: __("Sign-in session expired"), title: t("authError.signInSessionExpired.title"),
description: __( description: t("authError.signInSessionExpired.description"),
"This sign-in attempt is no longer valid. Please start again from the sign-in page.",
),
}; };
case "magic_link_expired": case "magic_link_expired":
return { return {
title: __("Link Expired"), title: t("authError.magicLinkExpired.title"),
description: __( description: t("authError.magicLinkExpired.description"),
"This magic link has expired. Magic links are only valid for 15 minutes. Please request a new one.",
),
}; };
case "magic_link_already_used": case "magic_link_already_used":
return { return {
title: __("Link Already Used"), title: t("authError.magicLinkAlreadyUsed.title"),
description: __( description: t("authError.magicLinkAlreadyUsed.description"),
"This magic link has already been used. Please request a new one.",
),
}; };
case "magic_link_invalid": case "magic_link_invalid":
return { return {
title: __("Invalid link"), title: t("authError.invalidLink.title"),
description: __( description: t("authError.invalidLink.description"),
"This magic link is invalid. Please request a new one.",
),
}; };
default: default:
return { return {
title: __("Authentication failed"), title: t("authError.default.title"),
description: __( description: t("authError.default.description"),
"We could not complete your sign-in. Please try again.",
),
}; };
} }
} }
export default function AuthErrorPage() { export default function AuthErrorPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const content = useAuthErrorContent(searchParams.get("error")); const content = useAuthErrorContent(searchParams.get("error"));
@@ -98,7 +84,7 @@ export default function AuthErrorPage() {
<p className="text-txt-tertiary">{content.description}</p> <p className="text-txt-tertiary">{content.description}</p>
</div> </div>
<Button className="w-full h-10" to="/auth/login"> <Button className="w-full h-10" to="/auth/login">
{__("Sign in")} {t("auth.actions.signIn")}
</Button> </Button>
</div> </div>
); );

View File

@@ -19,9 +19,8 @@
// SOFTWARE. // SOFTWARE.
import { LaptopIcon } from "@phosphor-icons/react"; import { LaptopIcon } from "@phosphor-icons/react";
import { sprintf } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button, Card } from "@probo/ui"; import { Button, Card } from "@probo/ui";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
@@ -65,8 +64,8 @@ interface EnrollDevicePageProps {
} }
export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) { export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
usePageTitle(__("Enroll device")); usePageTitle(t("deviceEnrollment.pageTitle"));
const { viewer } = usePreloadedQuery<EnrollDevicePageQuery>( const { viewer } = usePreloadedQuery<EnrollDevicePageQuery>(
enrollDevicePageQuery, enrollDevicePageQuery,
@@ -125,18 +124,18 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
const steps = [ const steps = [
{ {
key: "intro", key: "intro",
title: __("Privacy"), title: t("deviceEnrollment.steps.privacy.title"),
description: __("Review collected data"), description: t("deviceEnrollment.steps.privacy.description"),
}, },
{ {
key: "organization", key: "organization",
title: __("Organization"), title: t("devices.fields.organization"),
description: __("Choose destination workspace"), description: t("deviceEnrollment.steps.organization.description"),
}, },
{ {
key: "enroll", key: "enroll",
title: __("Open and wait"), title: t("deviceEnrollment.steps.enroll.title"),
description: __("Finish setup in the desktop agent"), description: t("deviceEnrollment.steps.enroll.description"),
}, },
] as const; ] as const;
@@ -147,16 +146,14 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
<Card className="space-y-4 border-border-low p-6"> <Card className="space-y-4 border-border-low p-6">
<div className="space-y-1"> <div className="space-y-1">
<h2 className="text-lg font-medium"> <h2 className="text-lg font-medium">
{__("Enrollment unavailable")} {t("deviceEnrollment.unavailable.title")}
</h2> </h2>
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__( {t("deviceEnrollment.unavailable.description")}
"You do not have permission to enroll devices in any organization.",
)}
</p> </p>
</div> </div>
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<Link to="/">{__("Back to organizations")}</Link> <Link to="/">{t("deviceEnrollment.actions.backToOrganizations")}</Link>
</Button> </Button>
</Card> </Card>
) )
@@ -169,8 +166,8 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
<LaptopIcon size={18} weight="duotone" /> <LaptopIcon size={18} weight="duotone" />
</div> </div>
<div> <div>
<p className="text-sm font-semibold text-txt-primary">{__("Device enrollment")}</p> <p className="text-sm font-semibold text-txt-primary">{t("deviceEnrollment.title")}</p>
<p className="text-xs text-txt-secondary">{__("Setup")}</p> <p className="text-xs text-txt-secondary">{t("deviceEnrollment.setup")}</p>
</div> </div>
</div> </div>
@@ -211,7 +208,7 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
<div className="mt-8 md:mt-auto"> <div className="mt-8 md:mt-auto">
<p className="text-xs font-semibold uppercase tracking-[0.12em] text-txt-secondary"> <p className="text-xs font-semibold uppercase tracking-[0.12em] text-txt-secondary">
{sprintf(__("Step %s of %s"), activeStepIndex, steps.length)} {t("deviceEnrollment.stepProgress", { current: activeStepIndex, total: steps.length })}
</p> </p>
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
{steps.map((item, index) => ( {steps.map((item, index) => (
@@ -231,21 +228,19 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
{step === "intro" && ( {step === "intro" && (
<section className="space-y-5"> <section className="space-y-5">
<header className="space-y-2"> <header className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">{__("Before you start")}</h1> <h1 className="text-2xl font-semibold tracking-tight">{t("deviceEnrollment.intro.title")}</h1>
<p className="text-sm leading-6 text-txt-secondary"> <p className="text-sm leading-6 text-txt-secondary">
{__( {t("deviceEnrollment.intro.description")}
"Probo collects the following device metadata for inventory and posture reporting:",
)}
</p> </p>
</header> </header>
<ul className="list-disc space-y-1.5 pl-5 text-sm text-txt-secondary"> <ul className="list-disc space-y-1.5 pl-5 text-sm text-txt-secondary">
<li>{__("Device identity: hardware UUID, hostname, and serial number (when available).")}</li> <li>{t("deviceEnrollment.privacy.identity")}</li>
<li>{__("System details: platform, OS version, and Probo agent version.")}</li> <li>{t("deviceEnrollment.privacy.systemDetails")}</li>
<li>{__("Activity signals: enrollment time, heartbeats, and posture check results.")}</li> <li>{t("deviceEnrollment.privacy.activitySignals")}</li>
</ul> </ul>
<div className="flex flex-wrap gap-3 pt-2"> <div className="flex flex-wrap gap-3 pt-2">
<Button onClick={() => setStep("organization")}> <Button onClick={() => setStep("organization")}>
{__("Continue")} {t("common.actions.continue")}
</Button> </Button>
</div> </div>
</section> </section>
@@ -254,11 +249,9 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
{step === "organization" && ( {step === "organization" && (
<section className="space-y-5"> <section className="space-y-5">
<header className="space-y-2"> <header className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">{__("Choose organization")}</h1> <h1 className="text-2xl font-semibold tracking-tight">{t("deviceEnrollment.organization.title")}</h1>
<p className="text-sm leading-6 text-txt-secondary"> <p className="text-sm leading-6 text-txt-secondary">
{__( {t("deviceEnrollment.organization.description")}
"Pick which organization will own and manage this device.",
)}
</p> </p>
</header> </header>
<EnrollOrganizationPicker <EnrollOrganizationPicker
@@ -274,10 +267,10 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
onClick={() => setStep("enroll")} onClick={() => setStep("enroll")}
disabled={selectedOrganizationId == null} disabled={selectedOrganizationId == null}
> >
{__("Continue")} {t("common.actions.continue")}
</Button> </Button>
<Button variant="secondary" onClick={() => setStep("intro")}> <Button variant="secondary" onClick={() => setStep("intro")}>
{__("Back")} {t("common.actions.back")}
</Button> </Button>
</div> </div>
</section> </section>
@@ -286,11 +279,9 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
{step === "enroll" && ( {step === "enroll" && (
<section className="space-y-5"> <section className="space-y-5">
<header className="space-y-2"> <header className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">{__("Open the Probo agent")}</h1> <h1 className="text-2xl font-semibold tracking-tight">{t("deviceEnrollment.openAgent.title")}</h1>
<p className="text-sm leading-6 text-txt-secondary"> <p className="text-sm leading-6 text-txt-secondary">
{__( {t("deviceEnrollment.openAgent.description")}
"Open the desktop agent to finish setup, then keep this page open until enrollment is confirmed.",
)}
</p> </p>
</header> </header>
<CoreRelayProvider> <CoreRelayProvider>
@@ -304,7 +295,7 @@ export function EnrollDevicePage({ queryRef }: EnrollDevicePageProps) {
{!isEnrollmentComplete && ( {!isEnrollmentComplete && (
<div> <div>
<Button variant="secondary" onClick={() => setStep("organization")}> <Button variant="secondary" onClick={() => setStep("organization")}>
{__("Back")} {t("common.actions.back")}
</Button> </Button>
</div> </div>
)} )}

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
interface OrganizationOption { interface OrganizationOption {
id: string; id: string;
@@ -38,12 +38,12 @@ export function EnrollOrganizationPicker(
onChange, onChange,
}: EnrollOrganizationPickerProps, }: EnrollOrganizationPickerProps,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<label htmlFor="organization-select" className="space-y-2 text-xs font-medium text-txt-secondary"> <label htmlFor="organization-select" className="space-y-2 text-xs font-medium text-txt-secondary">
<span>{__("Organization")}</span> <span>{t("devices.fields.organization")}</span>
<select <select
id="organization-select" id="organization-select"
value={selectedOrganizationId ?? ""} value={selectedOrganizationId ?? ""}

View File

@@ -271,15 +271,15 @@ export function APIKeyConnectorDialog({
if (provider.provider === "SEGMENT") { if (provider.provider === "SEGMENT") {
return ( return (
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">{__("Region")}</label> <label className="text-sm font-medium">{t("accessReviewSource.regions.label")}</label>
<Select <Select
value={extraSettingValues.region ?? ""} value={extraSettingValues.region ?? ""}
onValueChange={(val: string) => onValueChange={(val: string) =>
setExtraSettingValues(prev => ({ ...prev, region: val }))} setExtraSettingValues(prev => ({ ...prev, region: val }))}
placeholder={__("Select a region")} placeholder={t("accessReviewSource.regions.placeholder")}
> >
<Option value="US">{__("United States")}</Option> <Option value="US">{t("accessReviewSource.regions.unitedStates")}</Option>
<Option value="EU">{__("Europe")}</Option> <Option value="EU">{t("accessReviewSource.regions.europe")}</Option>
</Select> </Select>
</div> </div>
); );

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button, IconArrowLink } from "@probo/ui"; import { Button, IconArrowLink } from "@probo/ui";
type Props = { type Props = {
@@ -33,7 +33,7 @@ type Props = {
// the provider has no documentation URL. Shared by the provider card and the // the provider has no documentation URL. Shared by the provider card and the
// connect dialogs so the URL/label/target markup lives in one place. // connect dialogs so the URL/label/target markup lives in one place.
export function ConnectorDocumentationLink({ url, variant = "link" }: Props) { export function ConnectorDocumentationLink({ url, variant = "link" }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
if (!url) { if (!url) {
return null; return null;
@@ -45,7 +45,7 @@ export function ConnectorDocumentationLink({ url, variant = "link" }: Props) {
return ( return (
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<a href={url} target="_blank" rel="noopener noreferrer"> <a href={url} target="_blank" rel="noopener noreferrer">
{__("Documentation")} {t("accessReviewSource.documentation")}
<IconArrowLink size={16} /> <IconArrowLink size={16} />
</a> </a>
</Button> </Button>
@@ -59,7 +59,7 @@ export function ConnectorDocumentationLink({ url, variant = "link" }: Props) {
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-txt-tertiary underline hover:no-underline" className="inline-flex items-center gap-1 text-xs text-txt-tertiary underline hover:no-underline"
> >
{__("Documentation")} {t("accessReviewSource.documentation")}
<IconArrowLink size={12} /> <IconArrowLink size={12} />
</a> </a>
); );

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -67,9 +67,9 @@ interface DeviceLayoutProps {
} }
export function DeviceLayout({ queryRef }: DeviceLayoutProps) { export function DeviceLayout({ queryRef }: DeviceLayoutProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const pendingLabel = __("(pending)"); const pendingLabel = t("devices.values.pending");
const { device, organization } = usePreloadedQuery<DeviceLayoutQuery>( const { device, organization } = usePreloadedQuery<DeviceLayoutQuery>(
deviceLayoutQuery, deviceLayoutQuery,
@@ -96,7 +96,7 @@ export function DeviceLayout({ queryRef }: DeviceLayoutProps) {
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("Devices"), label: t("devices.title"),
to: `/organizations/${organizationId}/devices`, to: `/organizations/${organizationId}/devices`,
}, },
{ label: hostnameLabel }, { label: hostnameLabel },
@@ -113,7 +113,7 @@ export function DeviceLayout({ queryRef }: DeviceLayoutProps) {
confirmRevoke({ id: device.id, hostname: device.hostname })} confirmRevoke({ id: device.id, hostname: device.hostname })}
disabled={isRevoking} disabled={isRevoking}
> >
{__("Revoke")} {t("devices.actions.revoke")}
</Button> </Button>
)} )}
</PageHeader> </PageHeader>
@@ -124,7 +124,7 @@ export function DeviceLayout({ queryRef }: DeviceLayoutProps) {
<TabLink <TabLink
to={`/organizations/${organizationId}/devices/${device.id}/postures`} to={`/organizations/${organizationId}/devices/${device.id}/postures`}
> >
{__("Postures")} {t("devices.postures.navigation")}
</TabLink> </TabLink>
</Tabs> </Tabs>

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button, IconPlusLarge, PageHeader, Tbody, Th, Thead, Tr } from "@probo/ui"; import { Button, IconPlusLarge, PageHeader, Tbody, Th, Thead, Tr } from "@probo/ui";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { import {
@@ -89,10 +89,10 @@ interface DevicesPageProps {
} }
export function DevicesPage({ queryRef }: DevicesPageProps) { export function DevicesPage({ queryRef }: DevicesPageProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
usePageTitle(__("Devices")); usePageTitle(t("devices.title"));
const { organization } = usePreloadedQuery<DevicesPageQuery>( const { organization } = usePreloadedQuery<DevicesPageQuery>(
devicesPageQuery, devicesPageQuery,
@@ -112,10 +112,8 @@ export function DevicesPage({ queryRef }: DevicesPageProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
title={__("Devices")} title={t("devices.title")}
description={__( description={t("devices.description")}
"Manage computers enrolled with the Probo posture agent.",
)}
> >
{organization.canCreateDevice && ( {organization.canCreateDevice && (
<CreateDeviceDialog <CreateDeviceDialog
@@ -124,7 +122,7 @@ export function DevicesPage({ queryRef }: DevicesPageProps) {
pagination.refetch({}, { fetchPolicy: "store-and-network" }); pagination.refetch({}, { fetchPolicy: "store-and-network" });
}} }}
> >
<Button icon={IconPlusLarge}>{__("Add device")}</Button> <Button icon={IconPlusLarge}>{t("devices.actions.add")}</Button>
</CreateDeviceDialog> </CreateDeviceDialog>
)} )}
</PageHeader> </PageHeader>
@@ -137,12 +135,12 @@ export function DevicesPage({ queryRef }: DevicesPageProps) {
> >
<Thead> <Thead>
<Tr> <Tr>
<SortableTh field="HOSTNAME">{__("Hostname")}</SortableTh> <SortableTh field="HOSTNAME">{t("devices.fields.hostname")}</SortableTh>
<Th>{__("Owner")}</Th> <Th>{t("devices.fields.owner")}</Th>
<Th>{__("State")}</Th> <Th>{t("devices.fields.state")}</Th>
<Th>{__("Platform")}</Th> <Th>{t("devices.fields.platform")}</Th>
<Th>{__("OS version")}</Th> <Th>{t("devices.fields.osVersion")}</Th>
<SortableTh field="LAST_SEEN_AT">{__("Last seen")}</SortableTh> <SortableTh field="LAST_SEEN_AT">{t("devices.fields.lastSeen")}</SortableTh>
<Th> <Th>
<PostureColumnHeader /> <PostureColumnHeader />
</Th> </Th>

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Badge, Card } from "@probo/ui"; import { Badge, Card } from "@probo/ui";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
@@ -48,52 +48,52 @@ const deviceFragment = graphql`
export function DeviceDetailsCard(props: { export function DeviceDetailsCard(props: {
deviceFragmentRef: DeviceDetailsCard_deviceFragment$key; deviceFragmentRef: DeviceDetailsCard_deviceFragment$key;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const pendingLabel = __("(pending)"); const pendingLabel = t("devices.values.pending");
const device = useFragment(deviceFragment, props.deviceFragmentRef); const device = useFragment(deviceFragment, props.deviceFragmentRef);
return ( return (
<Card className="space-y-4" padded> <Card className="space-y-4" padded>
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<DetailField <DetailField
label={__("State")} label={t("devices.fields.state")}
value={ value={
<Badge variant={stateVariant(device.state)}>{device.state}</Badge> <Badge variant={stateVariant(device.state)}>{device.state}</Badge>
} }
/> />
<DetailField <DetailField
label={__("Owner")} label={t("devices.fields.owner")}
value={device.owner?.fullName ?? __("Unassigned")} value={device.owner?.fullName ?? t("devices.values.unassigned")}
/> />
<DetailField <DetailField
label={__("Hardware UUID")} label={t("devices.fields.hardwareUuid")}
value={displayValue(device.hardwareUuid, pendingLabel)} value={displayValue(device.hardwareUuid, pendingLabel)}
/> />
<DetailField <DetailField
label={__("Serial number")} label={t("devices.fields.serialNumber")}
value={displayValue(device.serialNumber, pendingLabel)} value={displayValue(device.serialNumber, pendingLabel)}
/> />
<DetailField <DetailField
label={__("Platform")} label={t("devices.fields.platform")}
value={displayValue(device.platform, pendingLabel)} value={displayValue(device.platform, pendingLabel)}
/> />
<DetailField <DetailField
label={__("OS version")} label={t("devices.fields.osVersion")}
value={displayValue(device.osVersion, pendingLabel)} value={displayValue(device.osVersion, pendingLabel)}
/> />
<DetailField <DetailField
label={__("Agent version")} label={t("devices.fields.agentVersion")}
value={displayValue(device.agentVersion, pendingLabel)} value={displayValue(device.agentVersion, pendingLabel)}
/> />
<DetailField <DetailField
label={__("Enrolled at")} label={t("devices.fields.enrolledAt")}
value={ value={
device.enrolledAt ? formatDate(device.enrolledAt) : pendingLabel device.enrolledAt ? formatDate(device.enrolledAt) : pendingLabel
} }
/> />
<DetailField <DetailField
label={__("Last seen")} label={t("devices.fields.lastSeen")}
value={device.lastSeenAt ? formatDate(device.lastSeenAt) : __("Never")} value={device.lastSeenAt ? formatDate(device.lastSeenAt) : t("devices.values.never")}
/> />
</div> </div>
</Card> </Card>

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
ActionDropdown, ActionDropdown,
Badge, Badge,
@@ -67,10 +67,10 @@ interface DeviceRowProps {
} }
export function DeviceRow({ canAssignDevice, canRevoke, fKey }: DeviceRowProps) { export function DeviceRow({ canAssignDevice, canRevoke, fKey }: DeviceRowProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const reassignDialogRef = useDialogRef(); const reassignDialogRef = useDialogRef();
const pendingLabel = __("(pending)"); const pendingLabel = t("devices.values.pending");
const device = useFragment(deviceRowFragment, fKey); const device = useFragment(deviceRowFragment, fKey);
@@ -89,13 +89,13 @@ export function DeviceRow({ canAssignDevice, canRevoke, fKey }: DeviceRowProps)
/> />
<Tr to={`/organizations/${organizationId}/devices/${device.id}`}> <Tr to={`/organizations/${organizationId}/devices/${device.id}`}>
<Td>{displayValue(device.hostname, pendingLabel)}</Td> <Td>{displayValue(device.hostname, pendingLabel)}</Td>
<Td>{device.owner?.fullName ?? __("Unassigned")}</Td> <Td>{device.owner?.fullName ?? t("devices.values.unassigned")}</Td>
<Td> <Td>
<Badge variant={stateVariant(device.state)}>{device.state}</Badge> <Badge variant={stateVariant(device.state)}>{device.state}</Badge>
</Td> </Td>
<Td>{displayValue(device.platform, pendingLabel)}</Td> <Td>{displayValue(device.platform, pendingLabel)}</Td>
<Td>{displayValue(device.osVersion, pendingLabel)}</Td> <Td>{displayValue(device.osVersion, pendingLabel)}</Td>
<Td>{device.lastSeenAt ? formatDate(device.lastSeenAt) : __("Never")}</Td> <Td>{device.lastSeenAt ? formatDate(device.lastSeenAt) : t("devices.values.never")}</Td>
<Td> <Td>
<span className="text-txt-success">{summary.pass}</span> <span className="text-txt-success">{summary.pass}</span>
{" / "} {" / "}
@@ -113,7 +113,7 @@ export function DeviceRow({ canAssignDevice, canRevoke, fKey }: DeviceRowProps)
icon={IconUser} icon={IconUser}
onClick={() => reassignDialogRef.current?.open()} onClick={() => reassignDialogRef.current?.open()}
> >
{__("Re-assign")} {t("devices.actions.reassign")}
</DropdownItem> </DropdownItem>
)} )}
{canRevoke && ( {canRevoke && (
@@ -124,7 +124,7 @@ export function DeviceRow({ canAssignDevice, canRevoke, fKey }: DeviceRowProps)
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Revoke")} {t("devices.actions.revoke")}
</DropdownItem> </DropdownItem>
)} )}
</ActionDropdown> </ActionDropdown>

View File

@@ -18,22 +18,22 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { IconCircleInfo } from "@probo/ui"; import { IconCircleInfo } from "@probo/ui";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
export function PostureColumnHeader() { export function PostureColumnHeader() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
{__("Posture")} {t("devices.postures.title")}
<Popover.Root> <Popover.Root>
<Popover.Trigger asChild> <Popover.Trigger asChild>
<button <button
type="button" type="button"
className="inline-flex text-txt-tertiary hover:text-txt-secondary" className="inline-flex text-txt-tertiary hover:text-txt-secondary"
aria-label={__("How posture summary is calculated")} aria-label={t("devices.postures.summaryHelp")}
> >
<IconCircleInfo size={14} /> <IconCircleInfo size={14} />
</button> </button>
@@ -45,9 +45,7 @@ export function PostureColumnHeader() {
align="start" align="start"
> >
<p> <p>
{__( {t("devices.postures.summaryDescription")}
"Shown as pass / fail / total.",
)}
</p> </p>
</Popover.Content> </Popover.Content>
</Popover.Portal> </Popover.Portal>

View File

@@ -18,8 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { formatError, sprintf } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { useConfirm, useToast } from "@probo/ui"; import { useConfirm, useToast } from "@probo/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
@@ -48,10 +48,10 @@ interface RevokeDeviceInput {
} }
export function useRevokeDevice() { export function useRevokeDevice() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const confirm = useConfirm(); const confirm = useConfirm();
const pendingLabel = __("(pending)"); const pendingLabel = t("devices.values.pending");
const [revokeDevice, isRevoking] = useMutation<useRevokeDeviceMutation>( const [revokeDevice, isRevoking] = useMutation<useRevokeDeviceMutation>(
revokeDeviceMutation, revokeDeviceMutation,
@@ -67,14 +67,14 @@ export function useRevokeDevice() {
onCompleted(_, errors) { onCompleted(_, errors) {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: errors[0].message, description: errors[0].message,
variant: "error", variant: "error",
}); });
} else { } else {
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Device revoked"), description: t("devices.messages.revoked"),
variant: "success", variant: "success",
}); });
} }
@@ -82,9 +82,9 @@ export function useRevokeDevice() {
}, },
onError(error) { onError(error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to revoke device"), t("devices.errors.revoke"),
error, error,
), ),
variant: "error", variant: "error",
@@ -94,18 +94,13 @@ export function useRevokeDevice() {
}); });
}), }),
{ {
message: sprintf( message: t("devices.confirmations.revoke", { hostname: displayValue(device.hostname, pendingLabel) }),
__(
"Revoke device \"%s\"? The agent on the device will stop reporting and must be re-enrolled.",
),
displayValue(device.hostname, pendingLabel),
),
variant: "danger", variant: "danger",
label: __("Revoke"), label: t("devices.actions.revoke"),
}, },
); );
}, },
[__, confirm, pendingLabel, revokeDevice, toast], [t, confirm, pendingLabel, revokeDevice, toast],
); );
return [confirmRevoke, isRevoking] as const; return [confirmRevoke, isRevoking] as const;

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -67,7 +67,7 @@ export function CreateDeviceDialog({
organizationId, organizationId,
onCreated, onCreated,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [enrollment, setEnrollment] = useState<{ const [enrollment, setEnrollment] = useState<{
@@ -115,7 +115,7 @@ export function CreateDeviceDialog({
onCompleted(response, errors) { onCompleted(response, errors) {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: errors[0].message, description: errors[0].message,
variant: "error", variant: "error",
}); });
@@ -127,18 +127,16 @@ export function CreateDeviceDialog({
serverUrl: response.createDevice.serverUrl, serverUrl: response.createDevice.serverUrl,
}); });
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __( description: t("deviceEnrollment.messages.created"),
"Device created. Copy the enrollment token now — it will not be shown again.",
),
variant: "success", variant: "success",
}); });
}, },
onError(error) { onError(error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to create device"), t("devices.errors.create"),
error, error,
), ),
variant: "error", variant: "error",
@@ -153,7 +151,7 @@ export function CreateDeviceDialog({
trigger={children} trigger={children}
onClose={handleClose} onClose={handleClose}
closable={!isCreating} closable={!isCreating}
title={<Breadcrumb items={[__("Devices"), __("New device")]} />} title={<Breadcrumb items={[t("devices.title"), t("devices.actions.new")]} />}
> >
<form onSubmit={e => void onSubmit(e)} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
@@ -169,7 +167,7 @@ export function CreateDeviceDialog({
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="ownerId" name="ownerId"
label={__("Owner")} label={t("devices.fields.owner")}
optional optional
/> />
)} )}
@@ -181,14 +179,14 @@ export function CreateDeviceDialog({
type="button" type="button"
onClick={closeDialog} onClick={closeDialog}
> >
{__("Close")} {t("common.actions.close")}
</Button> </Button>
</footer> </footer>
) )
: ( : (
<DialogFooter> <DialogFooter>
<Button disabled={formState.isSubmitting || isCreating} type="submit"> <Button disabled={formState.isSubmitting || isCreating} type="submit">
{__("Create")} {t("common.create")}
</Button> </Button>
</DialogFooter> </DialogFooter>
)} )}

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -77,7 +77,7 @@ export function ReassignDeviceDialog({
organizationId, organizationId,
ref: refProps, ref: refProps,
}: ReassignDeviceDialogProps) { }: ReassignDeviceDialogProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const ref = refProps ?? dialogRef; const ref = refProps ?? dialogRef;
@@ -121,7 +121,7 @@ export function ReassignDeviceDialog({
onCompleted(_, errors) { onCompleted(_, errors) {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: errors[0].message, description: errors[0].message,
variant: "error", variant: "error",
}); });
@@ -129,8 +129,8 @@ export function ReassignDeviceDialog({
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Device owner updated"), description: t("devices.messages.ownerUpdated"),
variant: "success", variant: "success",
}); });
handleClose(); handleClose();
@@ -138,9 +138,9 @@ export function ReassignDeviceDialog({
}, },
onError(error) { onError(error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to re-assign device"), t("devices.errors.reassign"),
error, error,
), ),
variant: "error", variant: "error",
@@ -154,7 +154,7 @@ export function ReassignDeviceDialog({
ref={ref} ref={ref}
onClose={handleClose} onClose={handleClose}
onOpenChange={setOpen} onOpenChange={setOpen}
title={<Breadcrumb items={[__("Devices"), __("Re-assign")]} />} title={<Breadcrumb items={[t("devices.title"), t("devices.actions.reassign")]} />}
> >
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
@@ -163,14 +163,14 @@ export function ReassignDeviceDialog({
organizationId={organizationId} organizationId={organizationId}
control={control} control={control}
name="ownerId" name="ownerId"
label={__("Owner")} label={t("devices.fields.owner")}
optional optional
/> />
)} )}
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button disabled={formState.isSubmitting || isInFlight} type="submit"> <Button disabled={formState.isSubmitting || isInFlight} type="submit">
{__("Re-assign")} {t("devices.actions.reassign")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Table, Tbody, Td, Th, Thead, Tr } from "@probo/ui"; import { Table, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -41,23 +41,23 @@ interface DevicePostureListProps {
} }
export function DevicePostureList({ deviceFragmentRef }: DevicePostureListProps) { export function DevicePostureList({ deviceFragmentRef }: DevicePostureListProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const device = useFragment(deviceFragment, deviceFragmentRef); const device = useFragment(deviceFragment, deviceFragmentRef);
return ( return (
<Table> <Table>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Check")}</Th> <Th>{t("devices.postures.columns.check")}</Th>
<Th>{__("Status")}</Th> <Th>{t("devices.postures.columns.status")}</Th>
<Th className="text-end">{__("Observed at")}</Th> <Th className="text-end">{t("devices.postures.columns.observedAt")}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{device.latestPostures.length === 0 && ( {device.latestPostures.length === 0 && (
<Tr> <Tr>
<Td colSpan={3} className="text-center text-txt-secondary"> <Td colSpan={3} className="text-center text-txt-secondary">
{__("No posture checks recorded")} {t("devices.postures.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Badge, Td, Tr } from "@probo/ui"; import { Badge, Td, Tr } from "@probo/ui";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -42,19 +42,19 @@ interface DevicePostureListItemProps {
} }
export function DevicePostureListItem({ postureKey }: DevicePostureListItemProps) { export function DevicePostureListItem({ postureKey }: DevicePostureListItemProps) {
const { __, dateTimeFormat } = useTranslate(); const { i18n, t } = useTranslation();
const posture = useFragment(postureFragment, postureKey); const posture = useFragment(postureFragment, postureKey);
return ( return (
<Tr> <Tr>
<Td>{getPostureCheckLabel(__, posture.checkKey)}</Td> <Td>{getPostureCheckLabel(t, posture.checkKey)}</Td>
<Td> <Td>
<Badge variant={statusVariant(posture.status)}> <Badge variant={statusVariant(posture.status)}>
{getPostureStatusLabel(__, posture.status)} {getPostureStatusLabel(t, posture.status)}
</Badge> </Badge>
</Td> </Td>
<Td className="text-end whitespace-nowrap"> <Td className="text-end whitespace-nowrap">
{dateTimeFormat(posture.observedAt, { {new Intl.DateTimeFormat(i18n.language, {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
@@ -62,7 +62,7 @@ export function DevicePostureListItem({ postureKey }: DevicePostureListItemProps
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hour12: false, hour12: false,
})} }).format(posture.observedAt)}
</Td> </Td>
</Tr> </Tr>
); );

View File

@@ -18,21 +18,21 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
type Translator = (s: string) => string; type Translator = (key: string) => string;
const checkKeyLabels: Record<string, string> = { const checkKeyLabels: Record<string, string> = {
DISK_ENCRYPTION: "Disk encryption", DISK_ENCRYPTION: "devices.postures.checks.diskEncryption",
SCREEN_LOCK: "Screen lock", SCREEN_LOCK: "devices.postures.checks.screenLock",
FIREWALL_ENABLED: "Firewall enabled", FIREWALL_ENABLED: "devices.postures.checks.firewallEnabled",
TIME_SYNC: "Time sync", TIME_SYNC: "devices.postures.checks.timeSync",
OS_VERSION: "OS version", OS_VERSION: "devices.postures.checks.osVersion",
AUTO_UPDATE: "Auto update", AUTO_UPDATE: "devices.postures.checks.autoUpdate",
PASSWORD_POLICY: "Password policy", PASSWORD_POLICY: "devices.postures.checks.passwordPolicy",
REMOTE_LOGIN: "Remote login", REMOTE_LOGIN: "devices.postures.checks.remoteLogin",
MALWARE_PROTECTION: "Malware protection", MALWARE_PROTECTION: "devices.postures.checks.malwareProtection",
}; };
export function getPostureCheckLabel(__: Translator, checkKey: string) { export function getPostureCheckLabel(t: Translator, checkKey: string) {
const label = checkKeyLabels[checkKey]; const label = checkKeyLabels[checkKey];
return label ? __(label) : checkKey; return label ? t(label) : checkKey;
} }

View File

@@ -18,16 +18,16 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
type Translator = (s: string) => string; type Translator = (key: string) => string;
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
PASS: "Pass", PASS: "devices.postures.status.pass",
FAIL: "Fail", FAIL: "devices.postures.status.fail",
UNKNOWN: "Unknown", UNKNOWN: "devices.postures.status.unknown",
NOT_APPLICABLE: "Not applicable", NOT_APPLICABLE: "devices.postures.status.notApplicable",
}; };
export function getPostureStatusLabel(__: Translator, status: string) { export function getPostureStatusLabel(t: Translator, status: string) {
const label = statusLabels[status]; const label = statusLabels[status];
return label ? __(label) : status; return label ? t(label) : status;
} }

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button, Card, IconPlusLarge, Tbody, Th, Thead, Tr } from "@probo/ui"; import { Button, Card, IconPlusLarge, Tbody, Th, Thead, Tr } from "@probo/ui";
import { useTransition } from "react"; import { useTransition } from "react";
import { import {
@@ -74,9 +74,9 @@ interface EmployeeDevicesPageProps {
} }
export function EmployeeDevicesPage({ queryRef }: EmployeeDevicesPageProps) { export function EmployeeDevicesPage({ queryRef }: EmployeeDevicesPageProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
usePageTitle(__("Devices")); usePageTitle(t("devices.title"));
const { viewer, organization } = usePreloadedQuery<EmployeeDevicesPageQuery>( const { viewer, organization } = usePreloadedQuery<EmployeeDevicesPageQuery>(
employeeDevicesPageQuery, employeeDevicesPageQuery,
@@ -105,10 +105,10 @@ export function EmployeeDevicesPage({ queryRef }: EmployeeDevicesPageProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<header className="flex items-center justify-between gap-4"> <header className="flex items-center justify-between gap-4">
<h1 className="text-2xl font-semibold">{__("Your devices")}</h1> <h1 className="text-2xl font-semibold">{t("employeeDevices.title")}</h1>
{canEnrollDevice && ( {canEnrollDevice && (
<Button to="/enroll" icon={IconPlusLarge}> <Button to="/enroll" icon={IconPlusLarge}>
{__("Enroll new device")} {t("deviceEnrollment.actions.enrollNew")}
</Button> </Button>
)} )}
</header> </header>
@@ -119,11 +119,11 @@ export function EmployeeDevicesPage({ queryRef }: EmployeeDevicesPageProps) {
<table className="w-full table-fixed"> <table className="w-full table-fixed">
<Thead> <Thead>
<Tr> <Tr>
<Th className="text-left">{__("Hostname")}</Th> <Th className="text-left">{t("devices.fields.hostname")}</Th>
<Th className="w-32 text-left">{__("State")}</Th> <Th className="w-32 text-left">{t("devices.fields.state")}</Th>
<Th className="w-32 text-left">{__("Platform")}</Th> <Th className="w-32 text-left">{t("devices.fields.platform")}</Th>
<Th className="w-40 text-left">{__("OS version")}</Th> <Th className="w-40 text-left">{t("devices.fields.osVersion")}</Th>
<Th className="w-40 text-left">{__("Last seen")}</Th> <Th className="w-40 text-left">{t("devices.fields.lastSeen")}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -139,7 +139,7 @@ export function EmployeeDevicesPage({ queryRef }: EmployeeDevicesPageProps) {
: ( : (
<div className="px-4 py-12 text-center"> <div className="px-4 py-12 text-center">
<h3 className="text-lg font-semibold"> <h3 className="text-lg font-semibold">
{__("No devices enrolled yet")} {t("devices.empty")}
</h3> </h3>
</div> </div>
)} )}

View File

@@ -36,7 +36,7 @@ export default function EmployeeTabsLayout() {
{t("employeeTabsLayout.tabs.approvals")} {t("employeeTabsLayout.tabs.approvals")}
</TabLink> </TabLink>
<TabLink to="devices" end> <TabLink to="devices" end>
{__("Devices")} {t("devices.title")}
</TabLink> </TabLink>
</Tabs> </Tabs>
<Outlet /> <Outlet />

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
Button, Button,
Dialog, Dialog,
@@ -53,7 +53,7 @@ interface CreateDeviceFormProps {
} }
export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) { export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
@@ -90,7 +90,7 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
onCompleted(response, errors) { onCompleted(response, errors) {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: errors[0].message, description: errors[0].message,
variant: "error", variant: "error",
}); });
@@ -104,18 +104,16 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
}); });
dialogRef.current?.open(); dialogRef.current?.open();
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __( description: t("deviceEnrollment.messages.created"),
"Device created. Copy the enrollment token now — it will not be shown again.",
),
variant: "success", variant: "success",
}); });
}, },
onError(error) { onError(error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to create device"), t("devices.errors.create"),
error, error,
), ),
variant: "error", variant: "error",
@@ -135,7 +133,7 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
return ( return (
<> <>
<p className="text-center text-xs text-txt-secondary"> <p className="text-center text-xs text-txt-secondary">
{__("Can't enroll new device?")} {t("deviceEnrollment.manual.cannotEnroll")}
{" "} {" "}
<button <button
type="button" type="button"
@@ -143,7 +141,7 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
disabled={isCreating} disabled={isCreating}
className="text-txt-primary underline hover:no-underline disabled:opacity-60" className="text-txt-primary underline hover:no-underline disabled:opacity-60"
> >
{__("Try creating it manually")} {t("deviceEnrollment.manual.tryCreating")}
</button> </button>
</p> </p>
@@ -151,11 +149,11 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
ref={dialogRef} ref={dialogRef}
onClose={handleClose} onClose={handleClose}
closable={!(isCreating && !enrollment)} closable={!(isCreating && !enrollment)}
title={__("Manual enrollment")} title={t("deviceEnrollment.manual.title")}
> >
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
{isCreating && !enrollment {isCreating && !enrollment
? <p>{__("Creating device…")}</p> ? <p>{t("deviceEnrollment.manual.creating")}</p>
: null} : null}
{enrollment {enrollment
? ( ? (
@@ -170,7 +168,7 @@ export function CreateDeviceForm({ onDeviceCreated }: CreateDeviceFormProps) {
? ( ? (
<footer className="flex items-center justify-end gap-2 border-t border-t-border-low p-3"> <footer className="flex items-center justify-end gap-2 border-t border-t-border-low p-3">
<Button type="button" onClick={closeDialog}> <Button type="button" onClick={closeDialog}>
{__("Close")} {t("common.actions.close")}
</Button> </Button>
</footer> </footer>
) )

View File

@@ -19,7 +19,7 @@
// SOFTWARE. // SOFTWARE.
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Badge, Td, Tr } from "@probo/ui"; import { Badge, Td, Tr } from "@probo/ui";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -54,8 +54,8 @@ function stateVariant(state: string): "success" | "warning" | "info" {
} }
export function EmployeeDeviceListItem({ deviceKey }: EmployeeDeviceListItemProps) { export function EmployeeDeviceListItem({ deviceKey }: EmployeeDeviceListItemProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const pendingLabel = __("(pending)"); const pendingLabel = t("devices.values.pending");
const device = useFragment(employeeDeviceListItemFragment, deviceKey); const device = useFragment(employeeDeviceListItemFragment, deviceKey);
@@ -67,7 +67,7 @@ export function EmployeeDeviceListItem({ deviceKey }: EmployeeDeviceListItemProp
</Td> </Td>
<Td>{displayValue(device.platform, pendingLabel)}</Td> <Td>{displayValue(device.platform, pendingLabel)}</Td>
<Td>{displayValue(device.osVersion, pendingLabel)}</Td> <Td>{displayValue(device.osVersion, pendingLabel)}</Td>
<Td>{device.lastSeenAt ? formatDate(device.lastSeenAt) : __("Never")}</Td> <Td>{device.lastSeenAt ? formatDate(device.lastSeenAt) : t("devices.values.never")}</Td>
</Tr> </Tr>
); );
} }

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { Button, Card, useToast } from "@probo/ui"; import { Button, Card, useToast } from "@probo/ui";
const RELEASE_BASE_URL const RELEASE_BASE_URL
@@ -32,7 +32,7 @@ interface EnrollmentInstructionsProps {
export function EnrollmentInstructions( export function EnrollmentInstructions(
{ enrollmentToken, serverUrl }: EnrollmentInstructionsProps, { enrollmentToken, serverUrl }: EnrollmentInstructionsProps,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const unixCommand = `# 1. Download and install the probo-agent binary const unixCommand = `# 1. Download and install the probo-agent binary
OS=$(uname -s); ARCH=$(uname -m | sed 's/aarch64/arm64/; s/amd64/x86_64/') OS=$(uname -s); ARCH=$(uname -m | sed 's/aarch64/arm64/; s/amd64/x86_64/')
@@ -65,42 +65,34 @@ Remove-Item -Recurse -Force $zip, "$env:TEMP\\$name"
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h2 className="font-medium">{__("Enrollment token generated")}</h2> <h2 className="font-medium">{t("deviceEnrollment.token.title")}</h2>
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__( {t("deviceEnrollment.token.description")}
"Share this enrollment token only with the device owner through a secure channel. It can be used once and expires after seven days.",
)}
</p> </p>
<CopyableCodeBlock code={enrollmentToken} /> <CopyableCodeBlock code={enrollmentToken} />
<details> <details>
<summary className="cursor-pointer text-sm font-medium"> <summary className="cursor-pointer text-sm font-medium">
{__("Manual install (CLI / MDM)")} {t("deviceEnrollment.token.manualInstall")}
</summary> </summary>
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-medium"> <h3 className="text-sm font-medium">
{__( {t("deviceEnrollment.token.installUnix")}
"Install on macOS or Linux (run from a shell with sudo access)",
)}
</h3> </h3>
<CopyableCodeBlock code={unixCommand} /> <CopyableCodeBlock code={unixCommand} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-medium"> <h3 className="text-sm font-medium">
{__( {t("deviceEnrollment.token.installWindows")}
"Install on Windows (run from an elevated PowerShell session)",
)}
</h3> </h3>
<CopyableCodeBlock code={windowsCommand} /> <CopyableCodeBlock code={windowsCommand} />
</div> </div>
<p className="text-xs text-txt-secondary"> <p className="text-xs text-txt-secondary">
{__( {t("deviceEnrollment.token.securityNotice")}
"The token is passed as a CLI flag (not via curl-piped-to-shell or sudo env vars). Once installed, the agent self-updates from GitHub Releases with cosign signature verification.",
)}
</p> </p>
</div> </div>
</details> </details>
@@ -109,22 +101,22 @@ Remove-Item -Recurse -Force $zip, "$env:TEMP\\$name"
} }
function CopyableCodeBlock({ code }: { code: string }) { function CopyableCodeBlock({ code }: { code: string }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const handleCopy = () => { const handleCopy = () => {
navigator.clipboard.writeText(code).then( navigator.clipboard.writeText(code).then(
() => { () => {
toast({ toast({
title: __("Copied"), title: t("common.messages.copied"),
description: __("Copied to clipboard"), description: t("common.messages.copiedToClipboard"),
variant: "success", variant: "success",
}); });
}, },
() => { () => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: __("Failed to copy to clipboard"), description: t("deviceEnrollment.errors.copyToClipboard"),
variant: "error", variant: "error",
}); });
}, },
@@ -135,7 +127,7 @@ function CopyableCodeBlock({ code }: { code: string }) {
<Card className="rounded-lg border"> <Card className="rounded-lg border">
<div className="flex items-center justify-end border-b border-border-low px-1 py-1"> <div className="flex items-center justify-end border-b border-border-low px-1 py-1">
<Button type="button" variant="secondary" onClick={handleCopy}> <Button type="button" variant="secondary" onClick={handleCopy}>
{__("Copy")} {t("common.actions.copy")}
</Button> </Button>
</div> </div>
<pre className="overflow-x-auto whitespace-pre p-4 text-sm font-mono rounded-b-lg text-invert bg-accent"> <pre className="overflow-x-auto whitespace-pre p-4 text-sm font-mono rounded-b-lg text-invert bg-accent">

View File

@@ -18,8 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
import { formatError, sprintf } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslation } from "react-i18next";
import { import {
Button, Button,
IconArrowsClockwise, IconArrowsClockwise,
@@ -80,7 +80,7 @@ function EnrollDeviceButtonContent(
onComplete, onComplete,
}: EnrollDeviceButtonProps, }: EnrollDeviceButtonProps,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const environment = useRelayEnvironment(); const environment = useRelayEnvironment();
const matchingSession const matchingSession
@@ -212,7 +212,7 @@ function EnrollDeviceButtonContent(
onCompleted(response, errors) { onCompleted(response, errors) {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: errors[0].message, description: errors[0].message,
variant: "error", variant: "error",
}); });
@@ -232,9 +232,9 @@ function EnrollDeviceButtonContent(
}, },
onError(error) { onError(error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to create device"), t("devices.errors.create"),
error, error,
), ),
variant: "error", variant: "error",
@@ -249,11 +249,11 @@ function EnrollDeviceButtonContent(
<div className="flex items-center gap-2 text-sm font-medium text-txt-success"> <div className="flex items-center gap-2 text-sm font-medium text-txt-success">
<IconCircleCheck size={18} /> <IconCircleCheck size={18} />
{deviceHostname {deviceHostname
? sprintf(__("%s is enrolled."), deviceHostname) ? t("deviceEnrollment.status.enrolledWithHostname", { hostname: deviceHostname })
: __("This device is enrolled.")} : t("deviceEnrollment.status.enrolled")}
</div> </div>
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__("You can close this window.")} {t("deviceEnrollment.status.closeWindow")}
</p> </p>
</div> </div>
); );
@@ -263,7 +263,7 @@ function EnrollDeviceButtonContent(
return ( return (
<div className="flex items-center gap-2 text-sm text-txt-secondary"> <div className="flex items-center gap-2 text-sm text-txt-secondary">
<IconArrowsClockwise size={18} className="animate-spin" /> <IconArrowsClockwise size={18} className="animate-spin" />
{__("Waiting for the agent's first check-in…")} {t("deviceEnrollment.status.waitingForCheckIn")}
</div> </div>
); );
} }
@@ -272,9 +272,7 @@ function EnrollDeviceButtonContent(
<div className="space-y-2"> <div className="space-y-2">
{hasTimedOut && ( {hasTimedOut && (
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__( {t("deviceEnrollment.status.timedOut")}
"We haven't heard from the agent yet. Make sure the desktop agent is installed and running, then try again.",
)}
</p> </p>
)} )}
<Button <Button
@@ -282,10 +280,10 @@ function EnrollDeviceButtonContent(
disabled={isCreating || organizationId == null} disabled={isCreating || organizationId == null}
> >
{isCreating {isCreating
? __("Preparing…") ? t("deviceEnrollment.actions.preparing")
: hasTimedOut : hasTimedOut
? __("Try again") ? t("common.actions.tryAgain")
: __("Open Probo agent")} : t("deviceEnrollment.actions.openAgent")}
</Button> </Button>
</div> </div>
); );