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": {
"messages": { "completed": "Operation completed successfully" },
"errors": { "commit": "Failed to commit this operation" }
@@ -1331,10 +1331,10 @@
"membershipsDropdown": { "searchPlaceholder": "Search organizations...", "loading": "Loading organizations...", "addOrganization": "Add organization" },
"membershipsDropdownInvitingItem": { "title": "Check your email to accept the invitation" },
"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": {
"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" } },
"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." },
"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" } },
"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": {
"expired": "Expired",
"notes": "Notes"

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
interface OrganizationOption {
id: string;
@@ -38,12 +38,12 @@ export function EnrollOrganizationPicker(
onChange,
}: EnrollOrganizationPickerProps,
) {
const { __ } = useTranslate();
const { t } = useTranslation();
return (
<section className="space-y-4">
<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
id="organization-select"
value={selectedOrganizationId ?? ""}

View File

@@ -271,15 +271,15 @@ export function APIKeyConnectorDialog({
if (provider.provider === "SEGMENT") {
return (
<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
value={extraSettingValues.region ?? ""}
onValueChange={(val: string) =>
setExtraSettingValues(prev => ({ ...prev, region: val }))}
placeholder={__("Select a region")}
placeholder={t("accessReviewSource.regions.placeholder")}
>
<Option value="US">{__("United States")}</Option>
<Option value="EU">{__("Europe")}</Option>
<Option value="US">{t("accessReviewSource.regions.unitedStates")}</Option>
<Option value="EU">{t("accessReviewSource.regions.europe")}</Option>
</Select>
</div>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Button, Card, useToast } from "@probo/ui";
const RELEASE_BASE_URL
@@ -32,7 +32,7 @@ interface EnrollmentInstructionsProps {
export function EnrollmentInstructions(
{ enrollmentToken, serverUrl }: EnrollmentInstructionsProps,
) {
const { __ } = useTranslate();
const { t } = useTranslation();
const unixCommand = `# 1. Download and install the probo-agent binary
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 (
<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">
{__(
"Share this enrollment token only with the device owner through a secure channel. It can be used once and expires after seven days.",
)}
{t("deviceEnrollment.token.description")}
</p>
<CopyableCodeBlock code={enrollmentToken} />
<details>
<summary className="cursor-pointer text-sm font-medium">
{__("Manual install (CLI / MDM)")}
{t("deviceEnrollment.token.manualInstall")}
</summary>
<div className="mt-4 space-y-4">
<div className="space-y-2">
<h3 className="text-sm font-medium">
{__(
"Install on macOS or Linux (run from a shell with sudo access)",
)}
{t("deviceEnrollment.token.installUnix")}
</h3>
<CopyableCodeBlock code={unixCommand} />
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium">
{__(
"Install on Windows (run from an elevated PowerShell session)",
)}
{t("deviceEnrollment.token.installWindows")}
</h3>
<CopyableCodeBlock code={windowsCommand} />
</div>
<p className="text-xs text-txt-secondary">
{__(
"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.",
)}
{t("deviceEnrollment.token.securityNotice")}
</p>
</div>
</details>
@@ -109,22 +101,22 @@ Remove-Item -Recurse -Force $zip, "$env:TEMP\\$name"
}
function CopyableCodeBlock({ code }: { code: string }) {
const { __ } = useTranslate();
const { t } = useTranslation();
const { toast } = useToast();
const handleCopy = () => {
navigator.clipboard.writeText(code).then(
() => {
toast({
title: __("Copied"),
description: __("Copied to clipboard"),
title: t("common.messages.copied"),
description: t("common.messages.copiedToClipboard"),
variant: "success",
});
},
() => {
toast({
title: __("Error"),
description: __("Failed to copy to clipboard"),
title: t("common.error"),
description: t("deviceEnrollment.errors.copyToClipboard"),
variant: "error",
});
},
@@ -135,7 +127,7 @@ function CopyableCodeBlock({ code }: { code: string }) {
<Card className="rounded-lg border">
<div className="flex items-center justify-end border-b border-border-low px-1 py-1">
<Button type="button" variant="secondary" onClick={handleCopy}>
{__("Copy")}
{t("common.actions.copy")}
</Button>
</div>
<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
// SOFTWARE.
import { formatError, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { formatError } from "@probo/helpers";
import { useTranslation } from "react-i18next";
import {
Button,
IconArrowsClockwise,
@@ -80,7 +80,7 @@ function EnrollDeviceButtonContent(
onComplete,
}: EnrollDeviceButtonProps,
) {
const { __ } = useTranslate();
const { t } = useTranslation();
const { toast } = useToast();
const environment = useRelayEnvironment();
const matchingSession
@@ -212,7 +212,7 @@ function EnrollDeviceButtonContent(
onCompleted(response, errors) {
if (errors?.length) {
toast({
title: __("Error"),
title: t("common.error"),
description: errors[0].message,
variant: "error",
});
@@ -232,9 +232,9 @@ function EnrollDeviceButtonContent(
},
onError(error) {
toast({
title: __("Error"),
title: t("common.error"),
description: formatError(
__("Failed to create device"),
t("devices.errors.create"),
error,
),
variant: "error",
@@ -249,11 +249,11 @@ function EnrollDeviceButtonContent(
<div className="flex items-center gap-2 text-sm font-medium text-txt-success">
<IconCircleCheck size={18} />
{deviceHostname
? sprintf(__("%s is enrolled."), deviceHostname)
: __("This device is enrolled.")}
? t("deviceEnrollment.status.enrolledWithHostname", { hostname: deviceHostname })
: t("deviceEnrollment.status.enrolled")}
</div>
<p className="text-sm text-txt-secondary">
{__("You can close this window.")}
{t("deviceEnrollment.status.closeWindow")}
</p>
</div>
);
@@ -263,7 +263,7 @@ function EnrollDeviceButtonContent(
return (
<div className="flex items-center gap-2 text-sm text-txt-secondary">
<IconArrowsClockwise size={18} className="animate-spin" />
{__("Waiting for the agent's first check-in…")}
{t("deviceEnrollment.status.waitingForCheckIn")}
</div>
);
}
@@ -272,9 +272,7 @@ function EnrollDeviceButtonContent(
<div className="space-y-2">
{hasTimedOut && (
<p className="text-sm text-txt-secondary">
{__(
"We haven't heard from the agent yet. Make sure the desktop agent is installed and running, then try again.",
)}
{t("deviceEnrollment.status.timedOut")}
</p>
)}
<Button
@@ -282,10 +280,10 @@ function EnrollDeviceButtonContent(
disabled={isCreating || organizationId == null}
>
{isCreating
? __("Preparing…")
? t("deviceEnrollment.actions.preparing")
: hasTimedOut
? __("Try again")
: __("Open Probo agent")}
? t("common.actions.tryAgain")
: t("deviceEnrollment.actions.openAgent")}
</Button>
</div>
);