Add react i18next to console

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2026-07-21 16:09:41 +02:00
committed by Bryan Frimin
parent 1b7b2594eb
commit a7ff5f07bc
420 changed files with 10251 additions and 6825 deletions

View File

@@ -20,7 +20,7 @@
"pdfjs-dist": "^5.4.296", "pdfjs-dist": "^5.4.296",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.10",
"react-pdf": "^10.3.0", "react-pdf": "^10.3.0",
"react-relay": "^21.0.1", "react-relay": "^21.0.1",
"react-router": "^8.1.0", "react-router": "^8.1.0",

View File

@@ -11,6 +11,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist-ssr dist-ssr
scripts
*.local *.local
# Editor directories and files # Editor directories and files

View File

@@ -22,11 +22,13 @@
"@probo/ui": "1.0.0", "@probo/ui": "1.0.0",
"@tanstack/react-query": "^5.76.1", "@tanstack/react-query": "^5.76.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"i18next": "^26.3.6",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^19.2.7", "react-dom": "^19.2.7",
"react-dropzone": "^15.0.0", "react-dropzone": "^15.0.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4", "react-hook-form": "^7.56.4",
"react-i18next": "^17.0.10",
"react-pdf": "^10.3.0", "react-pdf": "^10.3.0",
"react-relay": "^21.0.1", "react-relay": "^21.0.1",
"react-router": "^8.1.0", "react-router": "^8.1.0",

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
ErrorDetailMessage, ErrorDetailMessage,
@@ -26,6 +25,7 @@ import {
ErrorLayout, ErrorLayout,
} from "@probo/ui"; } from "@probo/ui";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation, useRouteError } from "react-router"; import { Link, useLocation, useRouteError } from "react-router";
type Props = { type Props = {
@@ -36,7 +36,7 @@ type Props = {
export function PageError({ resetErrorBoundary, error: propsError }: Props) { export function PageError({ resetErrorBoundary, error: propsError }: Props) {
const routeError = useRouteError(); const routeError = useRouteError();
const error = routeError ?? propsError; const error = routeError ?? propsError;
const { __ } = useTranslate(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const baseLocation = useRef(location); const baseLocation = useRef(location);
@@ -56,7 +56,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
const actions = ( const actions = (
<Button asChild> <Button asChild>
<Link to="/">{__("Go home")}</Link> <Link to="/">{t("pageError.actions.goHome")}</Link>
</Button> </Button>
); );
@@ -70,8 +70,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
return ( return (
<ErrorLayout <ErrorLayout
{...layoutProps} {...layoutProps}
title={__("Page not found")} title={t("pageError.notFound.title")}
description={__("The page you are looking for does not exist.")} description={t("pageError.notFound.description")}
/> />
); );
} }
@@ -80,8 +80,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
return ( return (
<ErrorLayout <ErrorLayout
{...layoutProps} {...layoutProps}
title={__("Page not found")} title={t("pageError.notFound.title")}
description={__("The page you are looking for does not exist.")} description={t("pageError.notFound.description")}
/> />
); );
} }
@@ -89,11 +89,11 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
return ( return (
<ErrorLayout <ErrorLayout
{...layoutProps} {...layoutProps}
title={__("Something went wrong")} title={t("pageError.unexpected.title")}
description={__("We hit an unexpected error. Head back home to continue.")} description={t("pageError.unexpected.description")}
> >
{error instanceof Error && ( {error instanceof Error && (
<ErrorDetails summary={__("Technical details")}> <ErrorDetails summary={t("pageError.technicalDetails")}>
<ErrorDetailMessage>{error.message}</ErrorDetailMessage> <ErrorDetailMessage>{error.message}</ErrorDetailMessage>
</ErrorDetails> </ErrorDetails>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
IconChevronDown, IconChevronDown,
@@ -35,6 +34,7 @@ import {
useContext, useContext,
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import type { LoadMoreFn } from "react-relay"; import type { LoadMoreFn } from "react-relay";
import type { OperationType } from "relay-runtime"; import type { OperationType } from "relay-runtime";
@@ -75,7 +75,7 @@ export function SortableTable({
isLoadingNext?: boolean; isLoadingNext?: boolean;
pageSize?: number; pageSize?: number;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [order, setOrder] = useState(defaultOrder); const [order, setOrder] = useState(defaultOrder);
const changeOrder = (o: Order) => { const changeOrder = (o: Order) => {
startTransition(() => { startTransition(() => {
@@ -95,7 +95,7 @@ export function SortableTable({
disabled={isLoadingNext} disabled={isLoadingNext}
icon={isLoadingNext ? Spinner : IconChevronDown} icon={isLoadingNext ? Spinner : IconChevronDown}
> >
{__("Show more")} {t("sortableTable.actions.showMore")}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -21,9 +21,7 @@
import { import {
getAssetTypeVariant, getAssetTypeVariant,
promisifyMutation, promisifyMutation,
sprintf,
} from "@probo/helpers"; } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
ActionDropdown, ActionDropdown,
Badge, Badge,
@@ -34,6 +32,7 @@ import {
TextCell, TextCell,
useConfirm, useConfirm,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment"; import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
import { Link } from "react-router"; import { Link } from "react-router";
@@ -90,7 +89,7 @@ export function AssetsTable(props: Props) {
const { connectionId, pagination, assets } = props; const { connectionId, pagination, assets } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
const deleteAsset = useDeleteAsset(connectionId); const deleteAsset = useDeleteAsset(connectionId);
return ( return (
@@ -100,17 +99,17 @@ export function AssetsTable(props: Props) {
pagination={pagination} pagination={pagination}
items={assets} items={assets}
columns={[ columns={[
__("Name"), t("assetsTable.columns.name"),
__("Type"), t("assetsTable.columns.type"),
__("Data Types stored"), t("assetsTable.columns.dataTypesStored"),
__("Amount"), t("assetsTable.columns.amount"),
__("Owner"), t("assetsTable.columns.owner"),
__("Third parties"), t("assetsTable.columns.thirdParties"),
]} ]}
schema={schema} schema={schema}
updateMutation={updateAssetMutation} updateMutation={updateAssetMutation}
createMutation={createAssetMutation} createMutation={createAssetMutation}
addLabel={__("Add a new asset")} addLabel={t("assetsTable.actions.add")}
defaultValue={{ defaultValue={{
...defaultValue, ...defaultValue,
organizationId, organizationId,
@@ -120,7 +119,7 @@ export function AssetsTable(props: Props) {
<DropdownItem asChild> <DropdownItem asChild>
<Link to={`/organizations/${organizationId}/assets/${item.id}`}> <Link to={`/organizations/${organizationId}/assets/${item.id}`}>
<IconPencil size={16} /> <IconPencil size={16} />
{__("Edit")} {t("assetsTable.actions.edit")}
</Link> </Link>
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
@@ -128,7 +127,7 @@ export function AssetsTable(props: Props) {
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Delete")} {t("assetsTable.actions.delete")}
</DropdownItem> </DropdownItem>
</ActionDropdown> </ActionDropdown>
)} )}
@@ -140,7 +139,9 @@ export function AssetsTable(props: Props) {
items={["VIRTUAL", "PHYSICAL"]} items={["VIRTUAL", "PHYSICAL"]}
itemRenderer={({ item }) => ( itemRenderer={({ item }) => (
<Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}> <Badge variant={getAssetTypeVariant(item ?? "VIRTUAL")}>
{item === "PHYSICAL" ? __("Physical") : __("Virtual")} {item === "PHYSICAL"
? t("assetsTable.assetTypes.physical")
: t("assetsTable.assetTypes.virtual")}
</Badge> </Badge>
)} )}
defaultValue={item?.assetType ?? defaultValue.assetType} defaultValue={item?.assetType ?? defaultValue.assetType}
@@ -174,11 +175,11 @@ export function AssetsTable(props: Props) {
const useDeleteAsset = (connectionId: string) => { const useDeleteAsset = (connectionId: string) => {
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation); const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const { __ } = useTranslate(); const { t } = useTranslation();
return (asset: { id: string; name: string }) => { return (asset: { id: string; name: string }) => {
if (!asset.id || !asset.name) { if (!asset.id || !asset.name) {
return alert(__("Failed to delete asset: missing id or name")); return alert(t("assetsTable.delete.missingIdOrName"));
} }
confirm( confirm(
() => () =>
@@ -191,12 +192,7 @@ const useDeleteAsset = (connectionId: string) => {
}, },
}), }),
{ {
message: sprintf( message: t("assetsTable.delete.confirmation", { name: asset.name }),
__(
"This will permanently delete \"%s\". This action cannot be undone.",
),
asset.name,
),
}, },
); );
}; };

View File

@@ -19,8 +19,8 @@
// SOFTWARE. // SOFTWARE.
import { faviconUrl, getAssetTypeVariant } from "@probo/helpers"; import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui"; import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import { useTranslation } from "react-i18next";
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment"; import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
import type { OperationType } from "relay-runtime"; import type { OperationType } from "relay-runtime";
@@ -46,17 +46,17 @@ type Props = {
export function ReadOnlyAssetsTable(props: Props) { export function ReadOnlyAssetsTable(props: Props) {
const { pagination, assets } = props; const { pagination, assets } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<SortableTable {...pagination} pageSize={10}> <SortableTable {...pagination} pageSize={10}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("readOnlyAssetsTable.columns.name")}</Th>
<Th>{__("Type")}</Th> <Th>{t("readOnlyAssetsTable.columns.type")}</Th>
<Th>{__("Amount")}</Th> <Th>{t("readOnlyAssetsTable.columns.amount")}</Th>
<Th>{__("Owner")}</Th> <Th>{t("readOnlyAssetsTable.columns.owner")}</Th>
<Th>{__("Third parties")}</Th> <Th>{t("readOnlyAssetsTable.columns.thirdParties")}</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
@@ -70,7 +70,7 @@ export function ReadOnlyAssetsTable(props: Props) {
function AssetRow({ entry }: { entry: AssetEntry }) { function AssetRow({ entry }: { entry: AssetEntry }) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? []; const thirdParties = entry.thirdParties?.edges.map(edge => edge.node) ?? [];
return ( return (
@@ -78,11 +78,13 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
<Td>{entry.name}</Td> <Td>{entry.name}</Td>
<Td> <Td>
<Badge variant={getAssetTypeVariant(entry.assetType)}> <Badge variant={getAssetTypeVariant(entry.assetType)}>
{entry.assetType === "PHYSICAL" ? __("Physical") : __("Virtual")} {entry.assetType === "PHYSICAL"
? t("readOnlyAssetsTable.assetTypes.physical")
: t("readOnlyAssetsTable.assetTypes.virtual")}
</Badge> </Badge>
</Td> </Td>
<Td>{entry.amount}</Td> <Td>{entry.amount}</Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td> <Td>{entry.owner?.fullName ?? t("readOnlyAssetsTable.unassigned")}</Td>
<Td> <Td>
{thirdParties.length > 0 {thirdParties.length > 0
? ( ? (
@@ -110,7 +112,7 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
</div> </div>
) )
: ( : (
<span className="text-txt-secondary text-sm">{__("None")}</span> <span className="text-txt-secondary text-sm">{t("readOnlyAssetsTable.none")}</span>
)} )}
</Td> </Td>
</Tr> </Tr>

View File

@@ -18,8 +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 { getAuditStateVariant, sprintf } from "@probo/helpers"; import { getAuditStateVariant } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -37,6 +36,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -78,7 +78,7 @@ type Props<Params> = {
}; };
export function LinkedAuditsCard<Params>(props: Props<Params>) { export function LinkedAuditsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [limit, setLimit] = useState<number | null>(4); const [limit, setLimit] = useState<number | null>(4);
const audits = useMemo(() => { const audits = useMemo(() => {
return limit ? props.audits.slice(0, limit) : props.audits; return limit ? props.audits.slice(0, limit) : props.audits;
@@ -116,7 +116,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
<Wrapper padded className="space-y-[10px]"> <Wrapper padded className="space-y-[10px]">
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Audits")}</div> <div className="text-lg font-semibold">{t("linkedAuditsCard.title")}</div>
{!props.readOnly && ( {!props.readOnly && (
<LinkedAuditsDialog <LinkedAuditsDialog
disabled={props.disabled} disabled={props.disabled}
@@ -125,7 +125,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<Button variant="tertiary" icon={IconPlusLarge}> <Button variant="tertiary" icon={IconPlusLarge}>
{__("Link audit")} {t("linkedAuditsCard.actions.link")}
</Button> </Button>
</LinkedAuditsDialog> </LinkedAuditsDialog>
)} )}
@@ -134,8 +134,8 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("linkedAuditsCard.columns.name")}</Th>
<Th>{__("State")}</Th> <Th>{t("linkedAuditsCard.columns.state")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -146,7 +146,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 2 : 3} colSpan={props.readOnly ? 2 : 3}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No audits linked")} {t("linkedAuditsCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -166,7 +166,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={3} icon={IconPlusLarge}> <TrButton colspan={3} icon={IconPlusLarge}>
{__("Link audit")} {t("linkedAuditsCard.actions.link")}
</TrButton> </TrButton>
</LinkedAuditsDialog> </LinkedAuditsDialog>
)} )}
@@ -179,7 +179,9 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
className="mt-3 mx-auto" className="mt-3 mx-auto"
icon={IconChevronDown} icon={IconChevronDown}
> >
{sprintf(__("Show %s more"), props.audits.length - limit)} {t("linkedAuditsCard.actions.showMore", {
count: props.audits.length - limit,
})}
</Button> </Button>
)} )}
</Wrapper> </Wrapper>
@@ -193,7 +195,7 @@ function AuditRow(props: {
}) { }) {
const audit = useFragment(linkedAuditFragment, props.audit); const audit = useFragment(linkedAuditFragment, props.audit);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr to={`/organizations/${organizationId}/audits/${audit.id}`}> <Tr to={`/organizations/${organizationId}/audits/${audit.id}`}>
@@ -217,7 +219,7 @@ function AuditRow(props: {
onClick={() => props.onClick(audit.id)} onClick={() => props.onClick(audit.id)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedAuditsCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { getAuditStateVariant } from "@probo/helpers"; import { getAuditStateVariant } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -34,6 +33,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, Suspense, useMemo, useState } from "react"; import { type ReactNode, Suspense, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -98,16 +98,16 @@ type Props = {
}; };
export function LinkedAuditsDialog({ children, ...props }: Props) { export function LinkedAuditsDialog({ children, ...props }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog trigger={children} title={__("Link audits")}> <Dialog trigger={children} title={t("linkedAuditsDialog.title")}>
<DialogContent> <DialogContent>
<Suspense fallback={<Spinner centered />}> <Suspense fallback={<Spinner centered />}>
<LinkedAuditsDialogContent {...props} /> <LinkedAuditsDialogContent {...props} />
</Suspense> </Suspense>
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")} /> <DialogFooter exitLabel={t("linkedAuditsDialog.actions.close")} />
</Dialog> </Dialog>
); );
} }
@@ -122,7 +122,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
auditsFragment, auditsFragment,
query.organization as LinkedAuditsDialogFragment$key, query.organization as LinkedAuditsDialogFragment$key,
); );
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const audits = useMemo( const audits = useMemo(
() => data.audits?.edges?.map(edge => edge.node) ?? [], () => data.audits?.edges?.map(edge => edge.node) ?? [],
@@ -143,7 +143,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search audits...")} placeholder={t("linkedAuditsDialog.searchPlaceholder")}
onValueChange={setSearch} onValueChange={setSearch}
/> />
</div> </div>
@@ -180,7 +180,7 @@ type RowProps = {
}; };
function AuditRow(props: RowProps) { function AuditRow(props: RowProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.linkedAudits.has(props.audit.id); const isLinked = props.linkedAudits.has(props.audit.id);
const onClick = isLinked ? props.onUnlink : props.onLink; const onClick = isLinked ? props.onUnlink : props.onLink;
@@ -209,7 +209,9 @@ function AuditRow(props: RowProps) {
<span> <span>
<IconComponent size={16} /> <IconComponent size={16} />
{" "} {" "}
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedAuditsDialog.actions.unlink")
: t("linkedAuditsDialog.actions.link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { acceptImage } from "@probo/helpers"; import { acceptImage } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Dialog, Dialog,
@@ -32,6 +31,7 @@ import {
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react"; import { forwardRef, type ReactNode, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql"; import type { CompliancePageReferenceListItemFragment$data } from "#/__generated__/core/CompliancePageReferenceListItemFragment.graphql";
@@ -57,7 +57,7 @@ export type CompliancePageReferenceDialogRef = {
export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceDialogRef, { children?: ReactNode }>( export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceDialogRef, { children?: ReactNode }>(
function CompliancePageReferenceDialog({ children }, ref) { function CompliancePageReferenceDialog({ children }, ref) {
const { __ } = useTranslate(); const { t } = useTranslation();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [mode, setMode] = useState<"create" | "edit">("create"); const [mode, setMode] = useState<"create" | "edit">("create");
const [compliancePageId, setCompliancePageId] = useState<string>(""); const [compliancePageId, setCompliancePageId] = useState<string>("");
@@ -182,7 +182,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
}; };
const isSubmitting = isCreating || isUpdating; const isSubmitting = isCreating || isUpdating;
const title = mode === "create" ? __("Add Reference") : __("Edit Reference"); const title = mode === "create" ? t("trustCenterReferenceDialog.actions.add") : t("trustCenterReferenceDialog.actions.edit");
return ( return (
<> <>
@@ -202,45 +202,45 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
<DialogContent padded className="space-y-6"> <DialogContent padded className="space-y-6">
<Field <Field
{...register("name")} {...register("name")}
label={__("Reference Name")} label={t("trustCenterReferenceDialog.fields.name.label")}
type="text" type="text"
required required
error={errors.name?.message} error={errors.name?.message}
placeholder={__("Company or organization name")} placeholder={t("trustCenterReferenceDialog.fields.name.placeholder")}
/> />
<Field label={__("Description")} error={errors.description?.message}> <Field label={t("trustCenterReferenceDialog.fields.description.label")} error={errors.description?.message}>
<Textarea <Textarea
{...register("description")} {...register("description")}
placeholder={__("Brief description of the reference")} placeholder={t("trustCenterReferenceDialog.fields.description.placeholder")}
rows={3} rows={3}
/> />
</Field> </Field>
<Field <Field
{...register("websiteUrl")} {...register("websiteUrl")}
label={__("Website URL")} label={t("trustCenterReferenceDialog.fields.websiteUrl.label")}
type="url" type="url"
required required
error={errors.websiteUrl?.message} error={errors.websiteUrl?.message}
placeholder={__("https://example.com")} placeholder={t("trustCenterReferenceDialog.fields.websiteUrl.placeholder")}
/> />
{mode === "edit" && ( {mode === "edit" && (
<Field <Field
{...register("rank", { valueAsNumber: true })} {...register("rank", { valueAsNumber: true })}
label={__("Rank")} label={t("trustCenterReferenceDialog.fields.rank.label")}
type="number" type="number"
min={1} min={1}
error={errors.rank?.message} error={errors.rank?.message}
placeholder={__("Display order (1, 2, 3...)")} placeholder={t("trustCenterReferenceDialog.fields.rank.placeholder")}
help={__("Lower numbers appear first")} help={t("trustCenterReferenceDialog.fields.rank.help")}
/> />
)} )}
<Field label={__("Logo")}> <Field label={t("trustCenterReferenceDialog.fields.logo.label")}>
<Dropzone <Dropzone
description={__("Upload logo image (PNG, JPG, WEBP, SVG up to 5MB)")} description={t("trustCenterReferenceDialog.fields.logo.description")}
isUploading={isSubmitting} isUploading={isSubmitting}
onDrop={handleDrop} onDrop={handleDrop}
accept={acceptImage} accept={acceptImage}
@@ -249,7 +249,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
{uploadedFile && ( {uploadedFile && (
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg"> <div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{__("Selected file")} {t("trustCenterReferenceDialog.fields.logo.selectedFile")}
: :
</p> </p>
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p> <p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
@@ -258,14 +258,14 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
{mode === "edit" && !uploadedFile && ( {mode === "edit" && !uploadedFile && (
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg"> <div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__("Current logo will be kept if no new file is uploaded")} {t("trustCenterReferenceDialog.fields.logo.keepCurrent")}
</p> </p>
</div> </div>
)} )}
{mode === "create" && !uploadedFile && ( {mode === "create" && !uploadedFile && (
<div className="mt-2 p-3 bg-warning-subtle rounded-lg"> <div className="mt-2 p-3 bg-warning-subtle rounded-lg">
<p className="text-sm"> <p className="text-sm">
{__("Logo is required for new references")} {t("trustCenterReferenceDialog.fields.logo.required")}
</p> </p>
</div> </div>
)} )}
@@ -278,7 +278,7 @@ export const CompliancePageReferenceDialog = forwardRef<CompliancePageReferenceD
disabled={isSubmitting || (mode === "create" && !uploadedFile)} disabled={isSubmitting || (mode === "create" && !uploadedFile)}
icon={isSubmitting ? Spinner : undefined} icon={isSubmitting ? Spinner : undefined}
> >
{mode === "create" ? __("Add Reference") : __("Update Reference")} {mode === "create" ? t("trustCenterReferenceDialog.actions.add") : t("trustCenterReferenceDialog.actions.update")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Dialog, Dialog,
@@ -29,6 +27,7 @@ import {
Spinner, Spinner,
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useDeleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations"; import { useDeleteCompliancePageReferenceMutation } from "#/pages/organizations/compliance-page/_lib/compliancePageReferenceMutations";
@@ -47,7 +46,7 @@ export function DeleteCompliancePageReferenceDialog({
connectionId, connectionId,
onSuccess, onSuccess,
}: Props) { }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
const ref = useDialogRef(); const ref = useDialogRef();
const [deleteReference, isDeleting] = useDeleteCompliancePageReferenceMutation(); const [deleteReference, isDeleting] = useDeleteCompliancePageReferenceMutation();
@@ -70,18 +69,15 @@ export function DeleteCompliancePageReferenceDialog({
<Dialog <Dialog
ref={ref} ref={ref}
trigger={children} trigger={children}
title={__("Delete Reference")} title={t("deleteTrustCenterReferenceDialog.title")}
className="max-w-md" className="max-w-md"
> >
<DialogContent padded> <DialogContent padded>
<p className="text-txt-secondary"> <p className="text-txt-secondary">
{sprintf( {t("deleteTrustCenterReferenceDialog.description", { referenceName })}
__("Are you sure you want to delete the reference \"%s\"?"),
referenceName,
)}
</p> </p>
<p className="text-txt-secondary mt-2"> <p className="text-txt-secondary mt-2">
{__("This action cannot be undone.")} {t("deleteTrustCenterReferenceDialog.warning")}
</p> </p>
</DialogContent> </DialogContent>
@@ -92,7 +88,7 @@ export function DeleteCompliancePageReferenceDialog({
disabled={isDeleting} disabled={isDeleting}
icon={isDeleting ? Spinner : IconTrashCan} icon={isDeleting ? Spinner : IconTrashCan}
> >
{isDeleting ? __("Deleting...") : __("Delete")} {isDeleting ? t("deleteTrustCenterReferenceDialog.actions.deleting") : t("deleteTrustCenterReferenceDialog.actions.delete")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>

View File

@@ -18,7 +18,6 @@
// 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 { import {
Badge, Badge,
Button, Button,
@@ -31,6 +30,7 @@ import {
TrButton, TrButton,
} from "@probo/ui"; } from "@probo/ui";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -84,7 +84,7 @@ type Props<Params> = {
* Reusable component that displays a list of linked controls * Reusable component that displays a list of linked controls
*/ */
export function LinkedControlsCard<Params>(props: Props<Params>) { export function LinkedControlsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const controls = props.controls; const controls = props.controls;
const onDetach = (controlId: string) => { const onDetach = (controlId: string) => {
@@ -118,8 +118,8 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
<SortableTable refetch={props.refetch}> <SortableTable refetch={props.refetch}>
<Thead> <Thead>
<Tr> <Tr>
<SortableTh field="SECTION_TITLE">{__("Reference")}</SortableTh> <SortableTh field="SECTION_TITLE">{t("linkedControlsCard.columns.reference")}</SortableTh>
<Th>{__("Name")}</Th> <Th>{t("linkedControlsCard.columns.name")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -130,7 +130,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 2 : 3} colSpan={props.readOnly ? 2 : 3}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No controls linked")} {t("linkedControlsCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -151,7 +151,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
onLink={onAttach} onLink={onAttach}
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={3}>{__("Link control")}</TrButton> <TrButton colspan={3}>{t("linkedControlsCard.actions.link")}</TrButton>
</LinkedControlsDialog> </LinkedControlsDialog>
)} )}
</Tbody> </Tbody>
@@ -167,7 +167,7 @@ function ControlRow(props: {
}) { }) {
const control = useFragment(linkedControlFragment, props.control); const control = useFragment(linkedControlFragment, props.control);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr <Tr
@@ -188,7 +188,7 @@ function ControlRow(props: {
onClick={() => props.onClick(control.id)} onClick={() => props.onClick(control.id)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedControlsCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Badge, Badge,
Button, Button,
@@ -40,6 +39,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { useDebounceCallback } from "usehooks-ts"; import { useDebounceCallback } from "usehooks-ts";
@@ -107,7 +107,7 @@ type Props = {
type SearchRef = RefObject<{ search: (v: string) => void } | null>; type SearchRef = RefObject<{ search: (v: string) => void } | null>;
export function LinkedControlsDialog(props: Props) { export function LinkedControlsDialog(props: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
const searchRef: SearchRef = useRef(null); const searchRef: SearchRef = useRef(null);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const [minHeight, setMinHeight] = useState(0); const [minHeight, setMinHeight] = useState(0);
@@ -116,12 +116,12 @@ export function LinkedControlsDialog(props: Props) {
searchRef.current?.search(v); searchRef.current?.search(v);
}; };
return ( return (
<Dialog trigger={props.children} title={__("Link controls")}> <Dialog trigger={props.children} title={t("linkedControlsDialog.title")}>
<DialogContent> <DialogContent>
<div className="flex items-center gap-2 sticky top-0 py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search controls...")} placeholder={t("linkedControlsDialog.searchPlaceholder")}
onValueChange={onSearch} onValueChange={onSearch}
/> />
</div> </div>
@@ -202,7 +202,7 @@ function ControlRow(
controlIds: Set<string>; controlIds: Set<string>;
} & Props, } & Props,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.controlIds.has(props.control.id); const isLinked = props.controlIds.has(props.control.id);
const onClick = isLinked ? props.onUnlink : props.onLink; const onClick = isLinked ? props.onUnlink : props.onLink;
const IconComponent = isLinked ? IconTrashCan : IconPlusLarge; const IconComponent = isLinked ? IconTrashCan : IconPlusLarge;
@@ -225,7 +225,9 @@ function ControlRow(
<span> <span>
<IconComponent size={16} /> <IconComponent size={16} />
{" "} {" "}
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedControlsDialog.actions.unlink")
: t("linkedControlsDialog.actions.link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -31,6 +29,7 @@ import {
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { forwardRef, type ReactNode, useImperativeHandle } from "react"; import { forwardRef, type ReactNode, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -69,7 +68,7 @@ export type BulkExportDialogRef = {
export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>( export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
({ children, onExport, isLoading = false, defaultEmail, selectedCount }, ref) => { ({ children, onExport, isLoading = false, defaultEmail, selectedCount }, ref) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema( const { register, handleSubmit, formState, watch, setValue } = useFormWithSchema(
@@ -106,7 +105,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
<Dialog <Dialog
className="max-w-md" className="max-w-md"
ref={dialogRef} ref={dialogRef}
title={sprintf(__("Export %s Documents"), selectedCount)} title={t("bulkExportDialog.title", { count: selectedCount })}
> >
<form onSubmit={e => void handleSubmit(onSubmit)(e)}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent className="space-y-4" padded> <DialogContent className="space-y-4" padded>
@@ -118,10 +117,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
{__("Include signatures")} {t("bulkExportDialog.includeSignatures.label")}
</label> </label>
<p className="text-xs text-txt-secondary mt-1"> <p className="text-xs text-txt-secondary mt-1">
{__("Show signature information and approval details in the PDFs")} {t("bulkExportDialog.includeSignatures.description")}
</p> </p>
</div> </div>
</div> </div>
@@ -133,10 +132,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
{__("Add watermark")} {t("bulkExportDialog.watermark.label")}
</label> </label>
<p className="text-xs text-txt-secondary mt-1"> <p className="text-xs text-txt-secondary mt-1">
{__("Add confidential watermark with email and timestamp to all PDFs")} {t("bulkExportDialog.watermark.description")}
</p> </p>
</div> </div>
</div> </div>
@@ -144,10 +143,10 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
{watchWatermark && ( {watchWatermark && (
<div className="ml-6"> <div className="ml-6">
<Field <Field
label={__("Watermark email")} label={t("bulkExportDialog.watermark.emailLabel")}
{...register("watermarkEmail")} {...register("watermarkEmail")}
type="email" type="email"
placeholder={__("Enter email address")} placeholder={t("bulkExportDialog.watermark.emailPlaceholder")}
error={formState.errors.watermarkEmail?.message} error={formState.errors.watermarkEmail?.message}
autoComplete="off" autoComplete="off"
required required
@@ -158,7 +157,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
<div className="bg-level-1 p-3 rounded-lg border border-border-subtle"> <div className="bg-level-1 p-3 rounded-lg border border-border-subtle">
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__("The documents will be exported as individual PDFs in a ZIP file. You will receive an email when the export is ready for download.")} {t("bulkExportDialog.notice")}
</p> </p>
</div> </div>
</DialogContent> </DialogContent>
@@ -171,11 +170,11 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
? ( ? (
<> <>
<Spinner size={16} /> <Spinner size={16} />
{__("Exporting...")} {t("bulkExportDialog.actions.exporting")}
</> </>
) )
: ( : (
__("Export Documents") t("bulkExportDialog.actions.export")
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -38,6 +36,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -92,7 +91,7 @@ type Props<Params> = {
* Reusable component that displays a list of linked documents * Reusable component that displays a list of linked documents
*/ */
export function LinkedDocumentsCard<Params>(props: Props<Params>) { export function LinkedDocumentsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [limit, setLimit] = useState<number | null>(4); const [limit, setLimit] = useState<number | null>(4);
const documents = useMemo(() => { const documents = useMemo(() => {
return limit ? props.documents.slice(0, limit) : props.documents; return limit ? props.documents.slice(0, limit) : props.documents;
@@ -130,7 +129,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
<Wrapper padded className="space-y-[10px]"> <Wrapper padded className="space-y-[10px]">
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Documents")}</div> <div className="text-lg font-semibold">{t("linkedDocumentsCard.title")}</div>
{!props.readOnly && ( {!props.readOnly && (
<LinkedDocumentDialog <LinkedDocumentDialog
connectionId={props.connectionId} connectionId={props.connectionId}
@@ -140,7 +139,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<Button variant="tertiary" icon={IconPlusLarge}> <Button variant="tertiary" icon={IconPlusLarge}>
{__("Link document")} {t("linkedDocumentsCard.actions.link")}
</Button> </Button>
</LinkedDocumentDialog> </LinkedDocumentDialog>
)} )}
@@ -149,9 +148,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("linkedDocumentsCard.columns.name")}</Th>
<Th>{__("Type")}</Th> <Th>{t("linkedDocumentsCard.columns.type")}</Th>
<Th>{__("State")}</Th> <Th>{t("linkedDocumentsCard.columns.state")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -162,7 +161,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 3 : 4} colSpan={props.readOnly ? 3 : 4}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No documents linked")} {t("linkedDocumentsCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -183,7 +182,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={4} icon={IconPlusLarge}> <TrButton colspan={4} icon={IconPlusLarge}>
{__("Link document")} {t("linkedDocumentsCard.actions.link")}
</TrButton> </TrButton>
</LinkedDocumentDialog> </LinkedDocumentDialog>
)} )}
@@ -196,7 +195,9 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
className="mt-3 mx-auto" className="mt-3 mx-auto"
icon={IconChevronDown} icon={IconChevronDown}
> >
{sprintf(__("Show %s more"), props.documents.length - limit)} {t("linkedDocumentsCard.actions.showMore", {
count: props.documents.length - limit,
})}
</Button> </Button>
)} )}
</Wrapper> </Wrapper>
@@ -210,7 +211,7 @@ function DocumentRow(props: {
}) { }) {
const document = useFragment(linkedDocumentFragment, props.document); const document = useFragment(linkedDocumentFragment, props.document);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr to={`/organizations/${organizationId}/documents/${document.id}`}> <Tr to={`/organizations/${organizationId}/documents/${document.id}`}>
@@ -239,7 +240,7 @@ function DocumentRow(props: {
onClick={() => props.onClick(document.id)} onClick={() => props.onClick(document.id)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedDocumentsCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
Dialog, Dialog,
@@ -33,6 +32,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, Suspense, useMemo, useState } from "react"; import { type ReactNode, Suspense, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -101,16 +101,16 @@ type Props = {
}; };
export function LinkedDocumentDialog({ children, ...props }: Props) { export function LinkedDocumentDialog({ children, ...props }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog trigger={children} title={__("Link documents")}> <Dialog trigger={children} title={t("linkedDocumentsDialog.title")}>
<DialogContent> <DialogContent>
<Suspense fallback={<Spinner centered />}> <Suspense fallback={<Spinner centered />}>
<LinkedDocumentsDialogContent {...props} /> <LinkedDocumentsDialogContent {...props} />
</Suspense> </Suspense>
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")} /> <DialogFooter exitLabel={t("linkedDocumentsDialog.actions.close")} />
</Dialog> </Dialog>
); );
} }
@@ -129,7 +129,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
documentsFragment, documentsFragment,
query.organization as LinkedDocumentsDialogFragment$key, query.organization as LinkedDocumentsDialogFragment$key,
); );
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const documents = useMemo( const documents = useMemo(
() => data.documents?.edges?.map(edge => edge.node) ?? [], () => data.documents?.edges?.map(edge => edge.node) ?? [],
@@ -150,7 +150,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search documents...")} placeholder={t("linkedDocumentsDialog.searchPlaceholder")}
onValueChange={setSearch} onValueChange={setSearch}
/> />
</div> </div>
@@ -187,7 +187,7 @@ type RowProps = {
}; };
function DocumentRow(props: RowProps) { function DocumentRow(props: RowProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.linkedDocuments.has(props.document.id); const isLinked = props.linkedDocuments.has(props.document.id);
const onClick = isLinked ? props.onUnlink : props.onLink; const onClick = isLinked ? props.onUnlink : props.onLink;
@@ -209,7 +209,9 @@ function DocumentRow(props: RowProps) {
<span> <span>
<IconComponent size={16} /> <IconComponent size={16} />
{" "} {" "}
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedDocumentsDialog.actions.unlink")
: t("linkedDocumentsDialog.actions.link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
Checkbox, Checkbox,
@@ -30,6 +29,7 @@ import {
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { forwardRef, type ReactNode, useImperativeHandle } from "react"; import { forwardRef, type ReactNode, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -60,7 +60,7 @@ export type PdfDownloadDialogRef = {
export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>( export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
({ children, onDownload, isLoading = false, defaultEmail }, ref) => { ({ children, onDownload, isLoading = false, defaultEmail }, ref) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { register, handleSubmit, formState, watch, setValue } const { register, handleSubmit, formState, watch, setValue }
@@ -95,7 +95,7 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
<Dialog <Dialog
className="max-w-md" className="max-w-md"
ref={dialogRef} ref={dialogRef}
title={__("Download PDF Options")} title={t("pdfDownloadDialog.title")}
> >
<form onSubmit={e => void handleSubmit(onSubmit)(e)}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent className="space-y-4" padded> <DialogContent className="space-y-4" padded>
@@ -107,12 +107,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
{__("Include signatures")} {t("pdfDownloadDialog.includeSignatures.label")}
</label> </label>
<p className="text-xs text-txt-secondary mt-1"> <p className="text-xs text-txt-secondary mt-1">
{__( {t("pdfDownloadDialog.includeSignatures.description")}
"Show signature information and approval details in the PDF",
)}
</p> </p>
</div> </div>
</div> </div>
@@ -124,12 +122,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
{__("Add watermark")} {t("pdfDownloadDialog.watermark.label")}
</label> </label>
<p className="text-xs text-txt-secondary mt-1"> <p className="text-xs text-txt-secondary mt-1">
{__( {t("pdfDownloadDialog.watermark.description")}
"Add confidential watermark with email and timestamp",
)}
</p> </p>
</div> </div>
</div> </div>
@@ -137,10 +133,10 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
{watchWatermark && ( {watchWatermark && (
<div className="ml-6"> <div className="ml-6">
<Field <Field
label={__("Watermark email")} label={t("pdfDownloadDialog.watermark.emailLabel")}
{...register("watermarkEmail")} {...register("watermarkEmail")}
type="email" type="email"
placeholder={__("Enter email address")} placeholder={t("pdfDownloadDialog.watermark.emailPlaceholder")}
error={formState.errors.watermarkEmail?.message} error={formState.errors.watermarkEmail?.message}
autoComplete="off" autoComplete="off"
required required
@@ -155,11 +151,11 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
? ( ? (
<> <>
<Spinner size={16} /> <Spinner size={16} />
{__("Downloading...")} {t("pdfDownloadDialog.actions.downloading")}
</> </>
) )
: ( : (
__("Download PDF") t("pdfDownloadDialog.actions.download")
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { getAuditStateLabel, getAuditStateVariant } from "@probo/helpers"; import { getAuditStateLabel, getAuditStateVariant } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Badge, Field, Option, Select } from "@probo/ui"; import { Badge, Field, Option, Select } from "@probo/ui";
import { type ComponentProps, Suspense } from "react"; import { type ComponentProps, Suspense } from "react";
import { import {
@@ -28,6 +27,7 @@ import {
type FieldValues, type FieldValues,
type Path, type Path,
} from "react-hook-form"; } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { graphql, useLazyLoadQuery } from "react-relay"; import { graphql, useLazyLoadQuery } from "react-relay";
import type { AuditSelectFieldQuery } from "#/__generated__/core/AuditSelectFieldQuery.graphql"; import type { AuditSelectFieldQuery } from "#/__generated__/core/AuditSelectFieldQuery.graphql";
@@ -86,7 +86,7 @@ export function AuditSelectField<T extends FieldValues = FieldValues>({
function AuditSelectWithQuery<T extends FieldValues = FieldValues>( function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled">, props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled">,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { name, organizationId, control } = props; const { name, organizationId, control } = props;
const data = useLazyLoadQuery<AuditSelectFieldQuery>( const data = useLazyLoadQuery<AuditSelectFieldQuery>(
auditsQuery, auditsQuery,
@@ -109,7 +109,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
disabled={props.disabled} disabled={props.disabled}
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Select an audit")} placeholder={t("auditSelectField.placeholder")}
onValueChange={value => onValueChange={value =>
field.onChange(value === NONE_VALUE ? "" : value)} field.onChange(value === NONE_VALUE ? "" : value)}
key={audits?.length.toString() ?? "0"} key={audits?.length.toString() ?? "0"}
@@ -118,7 +118,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
value={field.value || NONE_VALUE} value={field.value || NONE_VALUE}
> >
<Option value={NONE_VALUE}> <Option value={NONE_VALUE}>
<span className="text-txt-tertiary">{__("None")}</span> <span className="text-txt-tertiary">{t("auditSelectField.none")}</span>
</Option> </Option>
{audits?.map(audit => ( {audits?.map(audit => (
<Option key={audit.id} value={audit.id}> <Option key={audit.id} value={audit.id}>
@@ -130,7 +130,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
</span> </span>
<div className="ml-3"> <div className="ml-3">
<Badge variant={getAuditStateVariant(audit.state)}> <Badge variant={getAuditStateVariant(audit.state)}>
{getAuditStateLabel(__, audit.state)} {getAuditStateLabel(t, audit.state)}
</Badge> </Badge>
</div> </div>
</div> </div>

View File

@@ -18,21 +18,18 @@
// 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 { import { controlMaturityLevels } from "@probo/helpers";
controlMaturityLevels,
getControlMaturityLevelLabel,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui"; import { Option } from "@probo/ui";
import { useTranslation } from "react-i18next";
export function ControlMaturityLevelOptions() { export function ControlMaturityLevelOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<> <>
{controlMaturityLevels.map(level => ( {controlMaturityLevels.map(level => (
<Option key={level} value={level}> <Option key={level} value={level}>
{getControlMaturityLevelLabel(__, level)} {t(`controlMaturityLevelOptions.levels.${level}`)}
</Option> </Option>
))} ))}
</> </>

View File

@@ -18,12 +18,17 @@
// 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 { countries, type CountryCode, getCountryName, getCountryOptions } from "@probo/helpers"; import { countries, type CountryCode, getCountryName } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Badge, IconCrossLargeX, Input } from "@probo/ui"; import { Badge, IconCrossLargeX, Input } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import {
type Control,
Controller,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
type Props<T extends FieldValues = FieldValues> = { type Props<T extends FieldValues = FieldValues> = {
control: Control<T>; control: Control<T>;
@@ -31,7 +36,11 @@ type Props<T extends FieldValues = FieldValues> = {
disabled?: boolean; disabled?: boolean;
}; };
export function CountriesField<T extends FieldValues = FieldValues>({ control, name, disabled }: Props<T>) { export function CountriesField<T extends FieldValues = FieldValues>({
control,
name,
disabled,
}: Props<T>) {
return ( return (
<Controller <Controller
control={control} control={control}
@@ -54,7 +63,7 @@ type CountriesFieldInputProps = {
}; };
function CountriesFieldInput(props: CountriesFieldInputProps) { function CountriesFieldInput(props: CountriesFieldInputProps) {
const { __ } = useTranslate(); const { i18n } = useTranslation();
const [animateBadge, setAnimateBadge] = useState(false); const [animateBadge, setAnimateBadge] = useState(false);
const addCountry = (code: string) => { const addCountry = (code: string) => {
@@ -85,7 +94,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
&& "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent", && "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent",
)} )}
> >
{getCountryName(__, countryCode as CountryCode)} {getCountryName(i18n.language, countryCode as CountryCode)}
<div className="w-0 overflow-hidden group-hover:w-4 duration-200"> <div className="w-0 overflow-hidden group-hover:w-4 duration-200">
<IconCrossLargeX size={12} /> <IconCrossLargeX size={12} />
</div> </div>
@@ -113,10 +122,13 @@ type CountryInputProps = {
}; };
function CountryInput({ availableCountries, onAdd }: CountryInputProps) { function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
const { __ } = useTranslate(); const { t, i18n } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const countryOptions = getCountryOptions(__); const countryOptions = availableCountries.map(code => ({
value: code,
label: getCountryName(i18n.language, code),
}));
useEffect(() => { useEffect(() => {
document.body.style.overflow = isOpen ? "hidden" : ""; document.body.style.overflow = isOpen ? "hidden" : "";
@@ -126,7 +138,9 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
}, [isOpen]); }, [isOpen]);
const filteredCountries = countryOptions const filteredCountries = countryOptions
.filter((option: { value: string; label: string }) => availableCountries.includes(option.value as CountryCode)) .filter((option: { value: string; label: string }) =>
availableCountries.includes(option.value as CountryCode),
)
.filter((option: { value: string; label: string }) => .filter((option: { value: string; label: string }) =>
option.label.toLowerCase().includes(search.toLowerCase()), option.label.toLowerCase().includes(search.toLowerCase()),
); );
@@ -145,7 +159,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
onFocus={() => setIsOpen(true)} onFocus={() => setIsOpen(true)}
placeholder={__("Search and add countries...")} placeholder={t("countriesField.searchPlaceholder")}
className="w-full pr-8" className="w-full pr-8"
/> />
<button <button
@@ -186,10 +200,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
)} )}
{isOpen && ( {isOpen && (
<div <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
/>
)} )}
</div> </div>
); );

View File

@@ -18,21 +18,18 @@
// 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 { import { documentClassifications } from "@probo/helpers";
documentClassifications,
getDocumentClassificationLabel,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui"; import { Option } from "@probo/ui";
import { useTranslation } from "react-i18next";
export function DocumentClassificationOptions() { export function DocumentClassificationOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<> <>
{documentClassifications.map(classification => ( {documentClassifications.map(classification => (
<Option key={classification} value={classification}> <Option key={classification} value={classification}>
{getDocumentClassificationLabel(__, classification)} {t(`documentClassificationOptions.classifications.${classification}`)}
</Option> </Option>
))} ))}
</> </>

View File

@@ -18,18 +18,18 @@
// 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 { documentTypes, getDocumentTypeLabel } from "@probo/helpers"; import { documentTypes } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui"; import { Option } from "@probo/ui";
import { useTranslation } from "react-i18next";
export function DocumentTypeOptions() { export function DocumentTypeOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<> <>
{documentTypes.map(type => ( {documentTypes.map(type => (
<Option key={type} value={type}> <Option key={type} value={type}>
{getDocumentTypeLabel(__, type)} {t(`documentTypeOptions.types.${type}`)}
</Option> </Option>
))} ))}
</> </>

View File

@@ -18,10 +18,17 @@
// 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 { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui"; import { Button, IconPlusLarge, IconTrashCan, Input, Label } from "@probo/ui";
import type { ArrayPath, Control, FieldValue, FieldValues, Path, UseFormRegister } from "react-hook-form"; import type {
ArrayPath,
Control,
FieldValue,
FieldValues,
Path,
UseFormRegister,
} from "react-hook-form";
import { useFieldArray } from "react-hook-form"; import { useFieldArray } from "react-hook-form";
import { useTranslation } from "react-i18next";
type Props<TFieldValues extends FieldValues = FieldValues> = { type Props<TFieldValues extends FieldValues = FieldValues> = {
disabled: boolean; disabled: boolean;
@@ -32,10 +39,12 @@ type Props<TFieldValues extends FieldValues = FieldValues> = {
/** /**
* A field to handle multiple emails * A field to handle multiple emails
*/ */
export function EmailsField< export function EmailsField<TFieldValues extends FieldValues = FieldValues>({
TFieldValues extends FieldValues = FieldValues, control,
>({ control, register, disabled }: Props<TFieldValues>) { register,
const { __ } = useTranslate(); disabled,
}: Props<TFieldValues>) {
const { t } = useTranslation();
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>, name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,
control, control,
@@ -43,12 +52,14 @@ export function EmailsField<
return ( return (
<fieldset className="space-y-2"> <fieldset className="space-y-2">
{fields.length > 0 && <Label>{__("Additional emails")}</Label>} {fields.length > 0 && <Label>{t("emailsField.additionalEmails")}</Label>}
{fields.map((field, index) => ( {fields.map((field, index) => (
<div key={field.id} className="flex items-stretch"> <div key={field.id} className="flex items-stretch">
<Input <Input
className="w-full" className="w-full"
{...register(`additionalEmailAddresses.${index}` as Path<TFieldValues>)} {...register(
`additionalEmailAddresses.${index}` as Path<TFieldValues>,
)}
type="email" type="email"
disabled={disabled} disabled={disabled}
/> />
@@ -67,7 +78,7 @@ export function EmailsField<
onClick={() => append("" as FieldValue<TFieldValues>)} onClick={() => append("" as FieldValue<TFieldValues>)}
disabled={disabled} disabled={disabled}
> >
{__("Add email")} {t("emailsField.actions.add")}
</Button> </Button>
</fieldset> </fieldset>
); );

View File

@@ -18,10 +18,15 @@
// 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 { Combobox, ComboboxItem, Field } from "@probo/ui"; import { Combobox, ComboboxItem, Field } from "@probo/ui";
import { type ComponentProps, Suspense, useMemo, useState } from "react"; import { type ComponentProps, Suspense, useMemo, useState } from "react";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import {
type Control,
Controller,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures"; import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
@@ -38,7 +43,9 @@ type Props<
optional?: boolean; optional?: boolean;
} & ComponentProps<typeof Field>; } & ComponentProps<typeof Field>;
export function MeasureSelectField<TFieldValues extends FieldValues = FieldValues>({ export function MeasureSelectField<
TFieldValues extends FieldValues = FieldValues,
>({
organizationId, organizationId,
control, control,
disabled, disabled,
@@ -48,7 +55,11 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
return ( return (
<Field {...props}> <Field {...props}>
<Suspense <Suspense
fallback={<Combobox onSearch={() => {}} placeholder="Loading..." disabled><div /></Combobox>} fallback={(
<Combobox onSearch={() => {}} placeholder="Loading..." disabled>
<div />
</Combobox>
)}
> >
<MeasureSelectWithQuery<TFieldValues> <MeasureSelectWithQuery<TFieldValues>
organizationId={organizationId} organizationId={organizationId}
@@ -63,9 +74,12 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
} }
function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>( function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
props: Pick<Props<TFieldValues>, "organizationId" | "control" | "name" | "disabled" | "optional">, props: Pick<
Props<TFieldValues>,
"organizationId" | "control" | "name" | "disabled" | "optional"
>,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { name, organizationId, control, disabled, optional } = props; const { name, organizationId, control, disabled, optional } = props;
const { data } = usePaginatedMeasures(organizationId); const { data } = usePaginatedMeasures(organizationId);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -91,12 +105,14 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
control={control} control={control}
name={name} name={name}
render={({ field }) => { render={({ field }) => {
const selectedMeasure = field.value ? allMeasures?.find(m => m.id === field.value) : null; const selectedMeasure = field.value
? allMeasures?.find(m => m.id === field.value)
: null;
return ( return (
<Combobox <Combobox
id={name} id={name}
placeholder={__("Select a measure")} placeholder={t("measureSelectField.placeholder")}
value={selectedMeasure ? selectedMeasure.name : search} value={selectedMeasure ? selectedMeasure.name : search}
onSearch={setSearch} onSearch={setSearch}
disabled={disabled} disabled={disabled}
@@ -108,7 +124,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
setSearch(""); setSearch("");
}} }}
> >
{__("None")} {t("measureSelectField.none")}
</ComboboxItem> </ComboboxItem>
)} )}
{measures?.map(m => ( {measures?.map(m => (
@@ -123,7 +139,9 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
<div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap"> <div className="max-w-75 ellipsis overflow-hidden whitespace-pre-wrap">
{m.name} {m.name}
</div> </div>
<div className="text-sm text-txt-secondary">{m.category}</div> <div className="text-sm text-txt-secondary">
{m.category}
</div>
</div> </div>
</ComboboxItem> </ComboboxItem>
))} ))}

View File

@@ -18,10 +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 {
import { Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui"; Badge,
Button,
Field,
IconCrossLargeX,
Option,
Select,
} from "@probo/ui";
import { type ComponentProps, Suspense, useState } from "react"; import { type ComponentProps, Suspense, useState } from "react";
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form"; import {
type Control,
Controller,
type FieldValues,
type Path,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { usePeople } from "#/hooks/graph/PeopleGraph"; import { usePeople } from "#/hooks/graph/PeopleGraph";
@@ -67,10 +79,24 @@ export function PeopleMultiSelectField<T extends FieldValues = FieldValues>({
} }
function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>( function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople" | "placeholder">, props: Pick<
Props<T>,
| "organizationId"
| "control"
| "name"
| "disabled"
| "selectedPeople"
| "placeholder"
>,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { name, organizationId, control, selectedPeople = [], placeholder } = props; const {
name,
organizationId,
control,
selectedPeople = [],
placeholder,
} = props;
const people = usePeople(organizationId, { contractEnded: false }); const people = usePeople(organizationId, { contractEnded: false });
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -91,10 +117,16 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
control={control} control={control}
name={name as Path<T>} name={name as Path<T>}
render={({ field }) => { render={({ field }) => {
const selectedPeopleIds = (Array.isArray(field.value) ? field.value : []) as string[]; const selectedPeopleIds = (
Array.isArray(field.value) ? field.value : []
) as string[];
const selectedPeople = allPeople.filter(p => selectedPeopleIds.includes(p.id)); const selectedPeople = allPeople.filter(p =>
const availablePeople = allPeople.filter(p => !selectedPeopleIds.includes(p.id)); selectedPeopleIds.includes(p.id),
);
const availablePeople = allPeople.filter(
p => !selectedPeopleIds.includes(p.id),
);
const handleAddPerson = (personId: string) => { const handleAddPerson = (personId: string) => {
const newValue = [...selectedPeopleIds, personId]; const newValue = [...selectedPeopleIds, personId];
@@ -103,7 +135,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
}; };
const handleRemovePerson = (personId: string) => { const handleRemovePerson = (personId: string) => {
const newValue = selectedPeopleIds.filter((id: string) => id !== personId); const newValue = selectedPeopleIds.filter(
(id: string) => id !== personId,
);
field.onChange(newValue); field.onChange(newValue);
}; };
@@ -114,7 +148,9 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
disabled={props.disabled} disabled={props.disabled}
id={name} id={name}
variant="editor" variant="editor"
placeholder={placeholder ?? __("Add people...")} placeholder={
placeholder ?? t("peopleMultiSelectField.addPlaceholder")
}
onValueChange={handleAddPerson} onValueChange={handleAddPerson}
key={`${selectedPeopleIds.length}-${people.length}`} key={`${selectedPeopleIds.length}-${people.length}`}
className="w-full" className="w-full"
@@ -123,7 +159,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
{availablePeople.map(person => ( {availablePeople.map(person => (
<Option key={person.id} value={person.id} className="flex gap-2"> <Option
key={person.id}
value={person.id}
className="flex gap-2"
>
<div className="flex flex-col"> <div className="flex flex-col">
<span>{person.fullName}</span> <span>{person.fullName}</span>
{person.emailAddress && ( {person.emailAddress && (
@@ -140,7 +180,11 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedPeople.length > 0 && ( {selectedPeople.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedPeople.map(person => ( {selectedPeople.map(person => (
<Badge key={person.id} variant="neutral" className="flex items-center gap-2"> <Badge
key={person.id}
variant="neutral"
className="flex items-center gap-2"
>
<span>{person.fullName}</span> <span>{person.fullName}</span>
{!props.disabled && ( {!props.disabled && (
<Button <Button
@@ -158,7 +202,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedPeople.length === 0 && availablePeople.length === 0 && ( {selectedPeople.length === 0 && availablePeople.length === 0 && (
<div className="text-sm text-txt-secondary py-2"> <div className="text-sm text-txt-secondary py-2">
{__("No people available")} {t("peopleMultiSelectField.empty")}
</div> </div>
)} )}
</div> </div>

View File

@@ -18,12 +18,12 @@
// 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 { Avatar, Field, Option, Select } from "@probo/ui"; import { Avatar, Field, Option, Select } from "@probo/ui";
import { type ComponentProps, Suspense } from "react"; import { type ComponentProps, Suspense } from "react";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { usePeople } from "#/hooks/graph/PeopleGraph"; import { usePeople } from "#/hooks/graph/PeopleGraph";
import { useTranslation } from "react-i18next";
type Props< type Props<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@@ -42,7 +42,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
control, control,
...props ...props
}: Props<TFieldValues>) { }: Props<TFieldValues>) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Field {...props}> <Field {...props}>
@@ -51,7 +51,7 @@ export function PeopleSelectField<TFieldValues extends FieldValues = FieldValues
<Select <Select
variant="editor" variant="editor"
loading loading
placeholder={__("Select an owner")} placeholder={t("peopleSelectField.placeholder")}
className="w-full" className="w-full"
/> />
)} )}
@@ -74,7 +74,7 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
"organizationId" | "control" | "name" | "disabled" | "optional" "organizationId" | "control" | "name" | "disabled" | "optional"
>, >,
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { name, organizationId, control } = props; const { name, organizationId, control } = props;
const people = usePeople(organizationId, { contractEnded: false }); const people = usePeople(organizationId, { contractEnded: false });
@@ -87,14 +87,16 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
disabled={props.disabled} disabled={props.disabled}
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Select an owner")} placeholder={t("peopleSelectField.placeholder")}
onValueChange={value => onValueChange={value =>
field.onChange(value === "__NONE__" ? null : value)} field.onChange(value === "__NONE__" ? null : value)}
{...field} {...field}
className="w-full" className="w-full"
value={field.value ?? (props.optional ? "__NONE__" : "")} value={field.value ?? (props.optional ? "__NONE__" : "")}
> >
{props.optional && <Option value="__NONE__">{__("None")}</Option>} {props.optional && (
<Option value="__NONE__">{t("peopleSelectField.none")}</Option>
)}
{people?.map(p => ( {people?.map(p => (
<Option key={p.id} value={p.id} className="flex gap-2"> <Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} /> <Avatar name={p.fullName} />

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 { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui"; import { Option } from "@probo/ui";
import { useTranslation } from "react-i18next";
import type { import type {
ProcessingActivityDataProtectionImpactAssessment, ProcessingActivityDataProtectionImpactAssessment,
@@ -28,16 +28,27 @@ import type {
ProcessingActivityTransferImpactAssessment, ProcessingActivityTransferImpactAssessment,
} from "#/__generated__/core/ProcessingActivityGraphCreateMutation.graphql"; } from "#/__generated__/core/ProcessingActivityGraphCreateMutation.graphql";
type Translator = (key: string) => string;
export function SpecialOrCriminalDataOptions() { export function SpecialOrCriminalDataOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: ProcessingActivitySpecialOrCriminalDatum; value: ProcessingActivitySpecialOrCriminalDatum;
label: string; label: string;
}> = [ }> = [
{ value: "YES", label: __("Yes") }, {
{ value: "NO", label: __("No") }, value: "YES",
{ value: "POSSIBLE", label: __("Possible") }, label: t("processingActivityEnumOptions.specialOrCriminalData.yes"),
},
{
value: "NO",
label: t("processingActivityEnumOptions.specialOrCriminalData.no"),
},
{
value: "POSSIBLE",
label: t("processingActivityEnumOptions.specialOrCriminalData.possible"),
},
]; ];
return ( return (
@@ -52,18 +63,38 @@ export function SpecialOrCriminalDataOptions() {
} }
export function LawfulBasisOptions() { export function LawfulBasisOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: ProcessingActivityLawfulBasis; value: ProcessingActivityLawfulBasis;
label: string; label: string;
}> = [ }> = [
{ value: "CONSENT", label: __("Consent") }, {
{ value: "CONTRACTUAL_NECESSITY", label: __("Contractual Necessity") }, value: "CONSENT",
{ value: "LEGAL_OBLIGATION", label: __("Legal Obligation") }, label: t("processingActivityEnumOptions.lawfulBasis.consent"),
{ value: "LEGITIMATE_INTEREST", label: __("Legitimate Interest") }, },
{ value: "PUBLIC_TASK", label: __("Public Task") }, {
{ value: "VITAL_INTERESTS", label: __("Vital Interests") }, value: "CONTRACTUAL_NECESSITY",
label: t(
"processingActivityEnumOptions.lawfulBasis.contractualNecessity",
),
},
{
value: "LEGAL_OBLIGATION",
label: t("processingActivityEnumOptions.lawfulBasis.legalObligation"),
},
{
value: "LEGITIMATE_INTEREST",
label: t("processingActivityEnumOptions.lawfulBasis.legitimateInterest"),
},
{
value: "PUBLIC_TASK",
label: t("processingActivityEnumOptions.lawfulBasis.publicTask"),
},
{
value: "VITAL_INTERESTS",
label: t("processingActivityEnumOptions.lawfulBasis.vitalInterests"),
},
]; ];
return ( return (
@@ -79,17 +110,23 @@ export function LawfulBasisOptions() {
export function getLawfulBasisLabel( export function getLawfulBasisLabel(
value: ProcessingActivityLawfulBasis | null | undefined, value: ProcessingActivityLawfulBasis | null | undefined,
__: (key: string) => string, t: Translator,
): string { ): string {
if (!value) return "-"; if (!value) return "-";
const labels = { const labels = {
CONSENT: __("Consent"), CONSENT:
CONTRACTUAL_NECESSITY: __("Contractual Necessity"), t("processingActivityEnumOptions.lawfulBasis.consent"),
LEGAL_OBLIGATION: __("Legal Obligation"), CONTRACTUAL_NECESSITY:
LEGITIMATE_INTEREST: __("Legitimate Interest"), t("processingActivityEnumOptions.lawfulBasis.contractualNecessity"),
PUBLIC_TASK: __("Public Task"), LEGAL_OBLIGATION:
VITAL_INTERESTS: __("Vital Interests"), t("processingActivityEnumOptions.lawfulBasis.legalObligation"),
LEGITIMATE_INTEREST:
t("processingActivityEnumOptions.lawfulBasis.legitimateInterest"),
PUBLIC_TASK:
t("processingActivityEnumOptions.lawfulBasis.publicTask"),
VITAL_INTERESTS:
t("processingActivityEnumOptions.lawfulBasis.vitalInterests"),
}; };
return labels[value] || value; return labels[value] || value;
@@ -97,38 +134,63 @@ export function getLawfulBasisLabel(
export function getResidualRiskLabel( export function getResidualRiskLabel(
value: "LOW" | "MEDIUM" | "HIGH" | null | undefined, value: "LOW" | "MEDIUM" | "HIGH" | null | undefined,
__: (key: string) => string, t: Translator,
): string { ): string {
if (!value) return "-"; if (!value) return "-";
const labels = { const labels = {
LOW: __("Low"), LOW: t("processingActivityEnumOptions.residualRisk.low") || "Low",
MEDIUM: __("Medium"), MEDIUM: t("processingActivityEnumOptions.residualRisk.medium") || "Medium",
HIGH: __("High"), HIGH: t("processingActivityEnumOptions.residualRisk.high") || "High",
}; };
return labels[value] || value; return labels[value] || value;
} }
export function TransferSafeguardsOptions() { export function TransferSafeguardsOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: string; value: string;
label: string; label: string;
}> = [ }> = [
{ value: "__NONE__", label: __("None") }, {
value: "__NONE__",
label: t("processingActivityEnumOptions.transferSafeguards.none"),
},
{ {
value: "STANDARD_CONTRACTUAL_CLAUSES", value: "STANDARD_CONTRACTUAL_CLAUSES",
label: __("Standard Contractual Clauses"), label: t(
"processingActivityEnumOptions.transferSafeguards.standardContractualClauses",
),
},
{
value: "BINDING_CORPORATE_RULES",
label: t(
"processingActivityEnumOptions.transferSafeguards.bindingCorporateRules",
),
},
{
value: "ADEQUACY_DECISION",
label: t(
"processingActivityEnumOptions.transferSafeguards.adequacyDecision",
),
},
{
value: "DEROGATIONS",
label: t("processingActivityEnumOptions.transferSafeguards.derogations"),
},
{
value: "CODES_OF_CONDUCT",
label: t(
"processingActivityEnumOptions.transferSafeguards.codesOfConduct",
),
}, },
{ value: "BINDING_CORPORATE_RULES", label: __("Binding Corporate Rules") },
{ value: "ADEQUACY_DECISION", label: __("Adequacy Decision") },
{ value: "DEROGATIONS", label: __("Derogations") },
{ value: "CODES_OF_CONDUCT", label: __("Codes of Conduct") },
{ {
value: "CERTIFICATION_MECHANISMS", value: "CERTIFICATION_MECHANISMS",
label: __("Certification Mechanisms"), label: t(
"processingActivityEnumOptions.transferSafeguards.certificationMechanisms",
),
}, },
]; ];
@@ -144,14 +206,24 @@ export function TransferSafeguardsOptions() {
} }
export function DataProtectionImpactAssessmentOptions() { export function DataProtectionImpactAssessmentOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: ProcessingActivityDataProtectionImpactAssessment; value: ProcessingActivityDataProtectionImpactAssessment;
label: string; label: string;
}> = [ }> = [
{ value: "NEEDED", label: __("Needed") }, {
{ value: "NOT_NEEDED", label: __("Not Needed") }, value: "NEEDED",
label: t(
"processingActivityEnumOptions.dataProtectionImpactAssessment.needed",
),
},
{
value: "NOT_NEEDED",
label: t(
"processingActivityEnumOptions.dataProtectionImpactAssessment.notNeeded",
),
},
]; ];
return ( return (
@@ -166,14 +238,22 @@ export function DataProtectionImpactAssessmentOptions() {
} }
export function TransferImpactAssessmentOptions() { export function TransferImpactAssessmentOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: ProcessingActivityTransferImpactAssessment; value: ProcessingActivityTransferImpactAssessment;
label: string; label: string;
}> = [ }> = [
{ value: "NEEDED", label: __("Needed") }, {
{ value: "NOT_NEEDED", label: __("Not Needed") }, value: "NEEDED",
label: t("processingActivityEnumOptions.transferImpactAssessment.needed"),
},
{
value: "NOT_NEEDED",
label: t(
"processingActivityEnumOptions.transferImpactAssessment.notNeeded",
),
},
]; ];
return ( return (
@@ -188,14 +268,20 @@ export function TransferImpactAssessmentOptions() {
} }
export function RoleOptions() { export function RoleOptions() {
const { __ } = useTranslate(); const { t } = useTranslation();
const options: Array<{ const options: Array<{
value: "CONTROLLER" | "PROCESSOR"; value: "CONTROLLER" | "PROCESSOR";
label: string; label: string;
}> = [ }> = [
{ value: "CONTROLLER", label: __("Controller") }, {
{ value: "PROCESSOR", label: __("Processor") }, value: "CONTROLLER",
label: t("processingActivityEnumOptions.roles.controller"),
},
{
value: "PROCESSOR",
label: t("processingActivityEnumOptions.roles.processor"),
},
]; ];
return ( return (

View File

@@ -19,11 +19,28 @@
// SOFTWARE. // SOFTWARE.
import { faviconUrl } from "@probo/helpers"; import { faviconUrl } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import {
import { Avatar, Badge, Button, Field, IconCrossLargeX, Option, Select } from "@probo/ui"; Avatar,
Badge,
Button,
Field,
IconCrossLargeX,
Option,
Select,
} from "@probo/ui";
import { type ComponentProps, Suspense, useEffect, useState } from "react"; import { type ComponentProps, Suspense, useEffect, useState } from "react";
import { type Control, Controller, type FieldValues, type Path } from "react-hook-form"; import {
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay"; type Control,
Controller,
type FieldValues,
type Path,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
type PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
} from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql"; import type { ThirdPartiesMultiSelectFieldQuery } from "#/__generated__/core/ThirdPartiesMultiSelectFieldQuery.graphql";
@@ -32,10 +49,7 @@ const thirdPartiesQuery = graphql`
query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) { query ThirdPartiesMultiSelectFieldQuery($organizationId: ID!) {
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
... on Organization { ... on Organization {
thirdParties( thirdParties(first: 100, orderBy: { direction: ASC, field: NAME }) {
first: 100
orderBy: { direction: ASC, field: NAME }
) {
edges { edges {
node { node {
id id
@@ -64,12 +78,9 @@ type Props<T extends FieldValues = FieldValues> = {
selectedThirdParties?: ThirdParty[]; selectedThirdParties?: ThirdParty[];
} & ComponentProps<typeof Field>; } & ComponentProps<typeof Field>;
export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues>({ export function ThirdPartiesMultiSelectField<
organizationId, T extends FieldValues = FieldValues,
control, >({ organizationId, control, selectedThirdParties = [], ...props }: Props<T>) {
selectedThirdParties = [],
...props
}: Props<T>) {
const [queryRef, loadQuery] const [queryRef, loadQuery]
= useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery); = useQueryLoader<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery);
@@ -103,14 +114,21 @@ export function ThirdPartiesMultiSelectField<T extends FieldValues = FieldValues
} }
function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>( function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "control" | "name" | "disabled" | "selectedThirdParties"> & { props: Pick<
Props<T>,
"control" | "name" | "disabled" | "selectedThirdParties"
> & {
queryRef: PreloadedQuery<ThirdPartiesMultiSelectFieldQuery>; queryRef: PreloadedQuery<ThirdPartiesMultiSelectFieldQuery>;
}, },
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { name, control, selectedThirdParties = [] } = props; const { name, control, selectedThirdParties = [] } = props;
const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(thirdPartiesQuery, props.queryRef); const data = usePreloadedQuery<ThirdPartiesMultiSelectFieldQuery>(
const thirdParties = data.organization?.thirdParties?.edges.map(edge => edge.node) ?? []; thirdPartiesQuery,
props.queryRef,
);
const thirdParties
= data.organization?.thirdParties?.edges.map(edge => edge.node) ?? [];
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const allThirdParties: ThirdParty[] = [...thirdParties]; const allThirdParties: ThirdParty[] = [...thirdParties];
@@ -128,10 +146,16 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
control={control} control={control}
name={name as Path<T>} name={name as Path<T>}
render={({ field }) => { render={({ field }) => {
const selectedThirdPartyIds = (Array.isArray(field.value) ? field.value : []) as string[]; const selectedThirdPartyIds = (
Array.isArray(field.value) ? field.value : []
) as string[];
const selectedThirdParties = allThirdParties.filter(v => selectedThirdPartyIds.includes(v.id)); const selectedThirdParties = allThirdParties.filter(v =>
const availableThirdParties = allThirdParties.filter(v => !selectedThirdPartyIds.includes(v.id)); selectedThirdPartyIds.includes(v.id),
);
const availableThirdParties = allThirdParties.filter(
v => !selectedThirdPartyIds.includes(v.id),
);
const handleAddThirdParty = (thirdPartyId: string) => { const handleAddThirdParty = (thirdPartyId: string) => {
const newValue = [...selectedThirdPartyIds, thirdPartyId]; const newValue = [...selectedThirdPartyIds, thirdPartyId];
@@ -140,7 +164,9 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
}; };
const handleRemoveThirdParty = (thirdPartyId: string) => { const handleRemoveThirdParty = (thirdPartyId: string) => {
const newValue = selectedThirdPartyIds.filter((id: string) => id !== thirdPartyId); const newValue = selectedThirdPartyIds.filter(
(id: string) => id !== thirdPartyId,
);
field.onChange(newValue); field.onChange(newValue);
}; };
@@ -151,7 +177,7 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
disabled={props.disabled} disabled={props.disabled}
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Add third parties...")} placeholder={t("thirdPartiesMultiSelectField.addPlaceholder")}
onValueChange={handleAddThirdParty} onValueChange={handleAddThirdParty}
key={`${selectedThirdPartyIds.length}-${thirdParties.length}`} key={`${selectedThirdPartyIds.length}-${thirdParties.length}`}
className="w-full" className="w-full"
@@ -160,7 +186,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
{availableThirdParties.map(thirdParty => ( {availableThirdParties.map(thirdParty => (
<Option key={thirdParty.id} value={thirdParty.id} className="flex gap-2"> <Option
key={thirdParty.id}
value={thirdParty.id}
className="flex gap-2"
>
<Avatar <Avatar
name={thirdParty.name} name={thirdParty.name}
src={faviconUrl(thirdParty.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
@@ -182,7 +212,11 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedThirdParties.length > 0 && ( {selectedThirdParties.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedThirdParties.map(thirdParty => ( {selectedThirdParties.map(thirdParty => (
<Badge key={thirdParty.id} variant="neutral" className="flex items-center gap-2"> <Badge
key={thirdParty.id}
variant="neutral"
className="flex items-center gap-2"
>
<Avatar <Avatar
name={thirdParty.name} name={thirdParty.name}
src={faviconUrl(thirdParty.websiteUrl)} src={faviconUrl(thirdParty.websiteUrl)}
@@ -202,9 +236,10 @@ function ThirdPartiesMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</div> </div>
)} )}
{selectedThirdParties.length === 0 && availableThirdParties.length === 0 && ( {selectedThirdParties.length === 0
&& availableThirdParties.length === 0 && (
<div className="text-sm text-txt-secondary py-2"> <div className="text-sm text-txt-secondary py-2">
{__("No third parties available")} {t("thirdPartiesMultiSelectField.empty")}
</div> </div>
)} )}
</div> </div>

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -37,6 +35,7 @@ import {
import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge"; import { MeasureBadge } from "@probo/ui/src/Molecules/Badge/MeasureBadge";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -83,7 +82,7 @@ type Props<Params> = {
* Reusable component that displays a list of linked measures * Reusable component that displays a list of linked measures
*/ */
export function LinkedMeasuresCard<Params>(props: Props<Params>) { export function LinkedMeasuresCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [limit, setLimit] = useState<number | null>( const [limit, setLimit] = useState<number | null>(
props.variant === "card" ? 4 : null, props.variant === "card" ? 4 : null,
); );
@@ -123,7 +122,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
<Wrapper padded className="space-y-[10px]"> <Wrapper padded className="space-y-[10px]">
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Measures")}</div> <div className="text-lg font-semibold">
{t("linkedMeasuresCard.title")}
</div>
{!props.readOnly && ( {!props.readOnly && (
<LinkedMeasureDialog <LinkedMeasureDialog
connectionId={props.connectionId} connectionId={props.connectionId}
@@ -133,7 +134,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<Button variant="tertiary" icon={IconPlusLarge}> <Button variant="tertiary" icon={IconPlusLarge}>
{__("Link measure")} {t("linkedMeasuresCard.actions.link")}
</Button> </Button>
</LinkedMeasureDialog> </LinkedMeasureDialog>
)} )}
@@ -142,8 +143,8 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("linkedMeasuresCard.columns.name")}</Th>
<Th>{__("State")}</Th> <Th>{t("linkedMeasuresCard.columns.state")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -154,7 +155,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 2 : 3} colSpan={props.readOnly ? 2 : 3}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No measures linked")} {t("linkedMeasuresCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -175,7 +176,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={3} icon={IconPlusLarge}> <TrButton colspan={3} icon={IconPlusLarge}>
{__("Link measure")} {t("linkedMeasuresCard.actions.link")}
</TrButton> </TrButton>
</LinkedMeasureDialog> </LinkedMeasureDialog>
)} )}
@@ -188,7 +189,9 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
className="mt-3 mx-auto" className="mt-3 mx-auto"
icon={IconChevronDown} icon={IconChevronDown}
> >
{sprintf(__("Show %s more"), props.measures.length - limit)} {t("linkedMeasuresCard.actions.showMore", {
count: props.measures.length - limit,
})}
</Button> </Button>
)} )}
</Wrapper> </Wrapper>
@@ -202,7 +205,7 @@ function MeasureRow(props: {
}) { }) {
const measure = useFragment(linkedMeasureFragment, props.measure); const measure = useFragment(linkedMeasureFragment, props.measure);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr to={`/organizations/${organizationId}/measures/${measure.id}`}> <Tr to={`/organizations/${organizationId}/measures/${measure.id}`}>
@@ -217,7 +220,7 @@ function MeasureRow(props: {
onClick={() => props.onClick(measure.id)} onClick={() => props.onClick(measure.id)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedMeasuresCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Badge, Badge,
Button, Button,
@@ -35,6 +34,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, Suspense, useMemo, useState } from "react"; import { type ReactNode, Suspense, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures"; import { usePaginatedMeasures } from "#/hooks/graph/usePaginatedMeasures";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -49,16 +49,16 @@ type Props = {
}; };
export function LinkedMeasureDialog({ children, ...props }: Props) { export function LinkedMeasureDialog({ children, ...props }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog trigger={children} title={__("Link measures")}> <Dialog trigger={children} title={t("linkedMeasuresDialog.title")}>
<DialogContent> <DialogContent>
<Suspense fallback={<Spinner centered />}> <Suspense fallback={<Spinner centered />}>
<LinkedMeasuresDialogContent {...props} /> <LinkedMeasuresDialogContent {...props} />
</Suspense> </Suspense>
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")} /> <DialogFooter exitLabel={t("linkedMeasuresDialog.actions.close")} />
</Dialog> </Dialog>
); );
} }
@@ -67,10 +67,13 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { data, loadNext, hasNext, isLoadingNext } const { data, loadNext, hasNext, isLoadingNext }
= usePaginatedMeasures(organizationId); = usePaginatedMeasures(organizationId);
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null); const [category, setCategory] = useState<string | null>(null);
const measures = useMemo(() => data.measures?.edges?.map(edge => edge.node) ?? [], [data.measures]); const measures = useMemo(
() => data.measures?.edges?.map(edge => edge.node) ?? [],
[data.measures],
);
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedMeasures?.map(m => m.id) ?? []); return new Set(props.linkedMeasures?.map(m => m.id) ?? []);
}, [props.linkedMeasures]); }, [props.linkedMeasures]);
@@ -94,12 +97,12 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search measures...")} placeholder={t("linkedMeasuresDialog.searchPlaceholder")}
onValueChange={setSearch} onValueChange={setSearch}
/> />
<Select <Select
value={category ?? ""} value={category ?? ""}
placeholder={__("All categories")} placeholder={t("linkedMeasuresDialog.allCategories")}
onValueChange={setCategory} onValueChange={setCategory}
className="max-w-[180px]" className="max-w-[180px]"
> >
@@ -141,7 +144,7 @@ type RowProps = {
}; };
function MeasureRow(props: RowProps) { function MeasureRow(props: RowProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.linkedMeasures.has(props.measure.id); const isLinked = props.linkedMeasures.has(props.measure.id);
const onClick = isLinked ? props.onUnlink : props.onLink; const onClick = isLinked ? props.onUnlink : props.onLink;
@@ -163,7 +166,9 @@ function MeasureRow(props: RowProps) {
<span> <span>
<IconComponent size={16} /> <IconComponent size={16} />
{" "} {" "}
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedMeasuresDialog.actions.unlink")
: t("linkedMeasuresDialog.actions.link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -21,9 +21,7 @@
import { import {
getObligationStatusLabel, getObligationStatusLabel,
getObligationStatusVariant, getObligationStatusVariant,
sprintf,
} from "@probo/helpers"; } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -41,6 +39,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -84,7 +83,7 @@ type Props<Params> = {
}; };
export function LinkedObligationsCard<Params>(props: Props<Params>) { export function LinkedObligationsCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [limit, setLimit] = useState<number | null>( const [limit, setLimit] = useState<number | null>(
props.variant === "card" ? 4 : null, props.variant === "card" ? 4 : null,
); );
@@ -126,7 +125,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
<Wrapper padded className="space-y-[10px]"> <Wrapper padded className="space-y-[10px]">
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold">{__("Obligations")}</div> <div className="text-lg font-semibold">
{t("linkedObligationsCard.title")}
</div>
{!props.readOnly && ( {!props.readOnly && (
<LinkedObligationDialog <LinkedObligationDialog
connectionId={props.connectionId} connectionId={props.connectionId}
@@ -136,7 +137,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<Button variant="tertiary" icon={IconPlusLarge}> <Button variant="tertiary" icon={IconPlusLarge}>
{__("Link obligation")} {t("linkedObligationsCard.actions.link")}
</Button> </Button>
</LinkedObligationDialog> </LinkedObligationDialog>
)} )}
@@ -145,10 +146,10 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Area")}</Th> <Th>{t("linkedObligationsCard.columns.area")}</Th>
<Th>{__("Source")}</Th> <Th>{t("linkedObligationsCard.columns.source")}</Th>
<Th>{__("Status")}</Th> <Th>{t("linkedObligationsCard.columns.status")}</Th>
<Th>{__("Owner")}</Th> <Th>{t("linkedObligationsCard.columns.owner")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -159,7 +160,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 4 : 5} colSpan={props.readOnly ? 4 : 5}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No obligations linked")} {t("linkedObligationsCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -180,7 +181,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={5} icon={IconPlusLarge}> <TrButton colspan={5} icon={IconPlusLarge}>
{__("Link obligation")} {t("linkedObligationsCard.actions.link")}
</TrButton> </TrButton>
</LinkedObligationDialog> </LinkedObligationDialog>
)} )}
@@ -193,7 +194,9 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
className="mt-3 mx-auto" className="mt-3 mx-auto"
icon={IconChevronDown} icon={IconChevronDown}
> >
{sprintf(__("Show %s more"), props.obligations.length - limit)} {t("linkedObligationsCard.actions.showMore", {
count: props.obligations.length - limit,
})}
</Button> </Button>
)} )}
</Wrapper> </Wrapper>
@@ -205,7 +208,7 @@ function ObligationRow(props: {
onClick: (obligationId: string) => void; onClick: (obligationId: string) => void;
readOnly?: boolean; readOnly?: boolean;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const obligation = useFragment(linkedObligationFragment, props.obligation); const obligation = useFragment(linkedObligationFragment, props.obligation);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -217,18 +220,20 @@ function ObligationRow(props: {
return ( return (
<Tr to={detailsUrl}> <Tr to={detailsUrl}>
<Td>{obligation.area || __("No area specified")}</Td> <Td>{obligation.area || t("linkedObligationsCard.noArea")}</Td>
<Td>{obligation.source || __("No source specified")}</Td> <Td>{obligation.source || t("linkedObligationsCard.noSource")}</Td>
<Td> <Td>
<Badge variant={getObligationStatusVariant(obligation.status)}> <Badge variant={getObligationStatusVariant(obligation.status)}>
{getObligationStatusLabel(obligation.status)} {getObligationStatusLabel(obligation.status)}
</Badge> </Badge>
</Td> </Td>
<Td>{obligation.owner?.fullName || __("Unassigned")}</Td> <Td>
{obligation.owner?.fullName || t("linkedObligationsCard.unassigned")}
</Td>
{!props.readOnly && ( {!props.readOnly && (
<Td noLink width={50} className="text-end"> <Td noLink width={50} className="text-end">
<Button variant="secondary" icon={IconTrashCan} onClick={onDetach}> <Button variant="secondary" icon={IconTrashCan} onClick={onDetach}>
{__("Unlink")} {t("linkedObligationsCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -22,7 +22,6 @@ import {
getObligationStatusLabel, getObligationStatusLabel,
getObligationStatusVariant, getObligationStatusVariant,
} from "@probo/helpers"; } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -37,6 +36,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, Suspense, useMemo, useState } from "react"; import { type ReactNode, Suspense, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay"; import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -102,16 +102,16 @@ type Props = {
}; };
export function LinkedObligationDialog({ children, ...props }: Props) { export function LinkedObligationDialog({ children, ...props }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog trigger={children} title={__("Link obligations")}> <Dialog trigger={children} title={t("linkedObligationsDialog.title")}>
<DialogContent> <DialogContent>
<Suspense fallback={<Spinner centered />}> <Suspense fallback={<Spinner centered />}>
<LinkedObligationsDialogContent {...props} /> <LinkedObligationsDialogContent {...props} />
</Suspense> </Suspense>
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")} /> <DialogFooter exitLabel={t("linkedObligationsDialog.actions.close")} />
</Dialog> </Dialog>
); );
} }
@@ -125,12 +125,14 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
}, },
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
); );
const { data, loadNext, hasNext, isLoadingNext } const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment<
= usePaginationFragment<LinkedObligationsDialogQuery_fragment, LinkedObligationsDialogFragment$key>( LinkedObligationsDialogQuery_fragment,
obligationsFragment, LinkedObligationsDialogFragment$key
query.organization as LinkedObligationsDialogFragment$key, >(
); obligationsFragment,
const { __ } = useTranslate(); query.organization as LinkedObligationsDialogFragment$key,
);
const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const obligations = useMemo( const obligations = useMemo(
() => data.obligations?.edges?.map(edge => edge.node) ?? [], () => data.obligations?.edges?.map(edge => edge.node) ?? [],
@@ -156,7 +158,7 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search obligations...")} placeholder={t("linkedObligationsDialog.searchPlaceholder")}
onValueChange={setSearch} onValueChange={setSearch}
/> />
</div> </div>
@@ -191,7 +193,7 @@ function ObligationRow(props: {
onUnlink: (obligationId: string) => void; onUnlink: (obligationId: string) => void;
disabled?: boolean; disabled?: boolean;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.linkedObligations.has(props.obligation.id); const isLinked = props.linkedObligations.has(props.obligation.id);
const onToggle = () => { const onToggle = () => {
@@ -208,11 +210,12 @@ function ObligationRow(props: {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-sm font-medium text-txt-primary truncate"> <div className="text-sm font-medium text-txt-primary truncate">
{props.obligation.area || __("No area specified")} {props.obligation.area || t("linkedObligationsDialog.noArea")}
{props.obligation.source || __("No source specified")} {props.obligation.source || t("linkedObligationsDialog.noSource")}
</div> </div>
<div className="text-xs text-txt-secondary"> <div className="text-xs text-txt-secondary">
{props.obligation.owner?.fullName || __("Unassigned")} {props.obligation.owner?.fullName
|| t("linkedObligationsDialog.unassigned")}
</div> </div>
</div> </div>
<Badge variant={getObligationStatusVariant(props.obligation.status)}> <Badge variant={getObligationStatusVariant(props.obligation.status)}>
@@ -227,7 +230,9 @@ function ObligationRow(props: {
disabled={props.disabled} disabled={props.disabled}
className="ml-6" className="ml-6"
> >
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedObligationsDialog.actions.unlink")
: t("linkedObligationsDialog.actions.link")}
</Button> </Button>
</div> </div>
); );

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Dialog, Dialog,
@@ -30,6 +28,7 @@ import {
useDialogRef, useDialogRef,
} from "@probo/ui"; } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
type DeleteOrganizationDialogProps = { type DeleteOrganizationDialogProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -44,7 +43,7 @@ export function DeleteOrganizationDialog({
onConfirm, onConfirm,
isDeleting = false, isDeleting = false,
}: DeleteOrganizationDialogProps) { }: DeleteOrganizationDialogProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const isConfirmDisabled = inputValue !== organizationName || isDeleting; const isConfirmDisabled = inputValue !== organizationName || isDeleting;
@@ -61,25 +60,21 @@ export function DeleteOrganizationDialog({
className="max-w-lg" className="max-w-lg"
ref={dialogRef} ref={dialogRef}
trigger={children} trigger={children}
title={__("Delete Organization")} title={t("deleteOrganizationDialog.title")}
> >
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm"> <p className="text-txt-secondary text-sm">
{sprintf( {t("deleteOrganizationDialog.description", { organizationName })}
__("This will permanently delete the organization %s and all its data."),
organizationName,
)}
</p> </p>
<p className="text-red-600 text-sm font-medium"> <p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")} {t("deleteOrganizationDialog.warning")}
</p> </p>
<Field <Field
label={sprintf( label={t("deleteOrganizationDialog.confirmationLabel", {
__("To confirm deletion, type \"%s\" below:"),
organizationName, organizationName,
)} })}
type="text" type="text"
value={inputValue} value={inputValue}
onChange={e => setInputValue(e.target.value)} onChange={e => setInputValue(e.target.value)}
@@ -96,7 +91,9 @@ export function DeleteOrganizationDialog({
onClick={handleConfirm} onClick={handleConfirm}
disabled={isConfirmDisabled} disabled={isConfirmDisabled}
> >
{isDeleting ? __("Deleting...") : __("Delete Organization")} {isDeleting
? t("deleteOrganizationDialog.actions.deleting")
: t("deleteOrganizationDialog.actions.delete")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
IconTrashCan, IconTrashCan,
@@ -31,6 +30,7 @@ import {
Tr, Tr,
TrButton, TrButton,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -77,7 +77,7 @@ type Props<Params> = {
* Reusable component that displays a list of linked risks * Reusable component that displays a list of linked risks
*/ */
export function LinkedRisksCard<Params>(props: Props<Params>) { export function LinkedRisksCard<Params>(props: Props<Params>) {
const { __ } = useTranslate(); const { t } = useTranslation();
const onAttach = (riskId: string) => { const onAttach = (riskId: string) => {
props.onAttach({ props.onAttach({
@@ -108,9 +108,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
<Table> <Table>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("linkedRisksCard.columns.name")}</Th>
<Th>{__("Initial Risk")}</Th> <Th>{t("linkedRisksCard.columns.initialRisk")}</Th>
<Th>{__("Residual Risk")}</Th> <Th>{t("linkedRisksCard.columns.residualRisk")}</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -121,7 +121,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
colSpan={props.readOnly ? 3 : 4} colSpan={props.readOnly ? 3 : 4}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No risks linked")} {t("linkedRisksCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -141,7 +141,9 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
onLink={onAttach} onLink={onAttach}
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={4}>{__("Link risk")}</TrButton> <TrButton colspan={4}>
{t("linkedRisksCard.actions.link")}
</TrButton>
</LinkedRisksDialog> </LinkedRisksDialog>
)} )}
</Tbody> </Tbody>
@@ -157,7 +159,7 @@ function RiskRow(props: {
}) { }) {
const risk = useFragment(linkedRiskFragment, props.risk); const risk = useFragment(linkedRiskFragment, props.risk);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}> <Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
@@ -175,7 +177,7 @@ function RiskRow(props: {
onClick={() => props.onClick(risk.id)} onClick={() => props.onClick(risk.id)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedRisksCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Badge, Badge,
Button, Button,
@@ -34,6 +33,7 @@ import {
Spinner, Spinner,
} from "@probo/ui"; } from "@probo/ui";
import { type ReactNode, Suspense, useMemo, useState } from "react"; import { type ReactNode, Suspense, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -70,16 +70,16 @@ type Props = {
}; };
export function LinkedRisksDialog({ children, ...props }: Props) { export function LinkedRisksDialog({ children, ...props }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog trigger={children} title={__("Link risks")}> <Dialog trigger={children} title={t("linkedRisksDialog.title")}>
<DialogContent> <DialogContent>
<Suspense fallback={<Spinner centered />}> <Suspense fallback={<Spinner centered />}>
<LinkedRisksDialogContent {...props} /> <LinkedRisksDialogContent {...props} />
</Suspense> </Suspense>
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")} /> <DialogFooter exitLabel={t("linkedRisksDialog.actions.close")} />
</Dialog> </Dialog>
); );
} }
@@ -89,7 +89,7 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, { const data = useLazyLoadQuery<LinkedRisksDialogQuery>(risksQuery, {
organizationId, organizationId,
}); });
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null); const [category, setCategory] = useState<string | null>(null);
const risks = useMemo( const risks = useMemo(
@@ -119,12 +119,12 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
<div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6"> <div className="flex items-center gap-2 sticky top-0 relative py-4 bg-linear-to-b from-50% from-level-2 to-level-2/0 px-6">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search risks...")} placeholder={t("linkedRisksDialog.searchPlaceholder")}
onValueChange={setSearch} onValueChange={setSearch}
/> />
<Select <Select
value={category ?? ""} value={category ?? ""}
placeholder={__("All categories")} placeholder={t("linkedRisksDialog.allCategories")}
onValueChange={setCategory} onValueChange={setCategory}
className="max-w-[180px]" className="max-w-[180px]"
> >
@@ -165,7 +165,7 @@ type RowProps = {
}; };
function RiskRow(props: RowProps) { function RiskRow(props: RowProps) {
const { __ } = useTranslate(); const { t } = useTranslation();
const isLinked = props.linkedRisks.has(props.risk.id); const isLinked = props.linkedRisks.has(props.risk.id);
const onClick = isLinked ? props.onUnlink : props.onLink; const onClick = isLinked ? props.onUnlink : props.onLink;
@@ -187,7 +187,9 @@ function RiskRow(props: RowProps) {
<span> <span>
<IconComponent size={16} /> <IconComponent size={16} />
{" "} {" "}
{isLinked ? __("Unlink") : __("Link")} {isLinked
? t("linkedRisksDialog.actions.unlink")
: t("linkedRisksDialog.actions.link")}
</span> </span>
</Button> </Button>
</button> </button>

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.
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Button, IconPencil, Skeleton } from "@probo/ui"; import { ActionDropdown, Button, IconPencil, Skeleton } from "@probo/ui";
import { useTranslation } from "react-i18next";
/** /**
* Skeleton state for the framework control panel * Skeleton state for the framework control panel
*/ */
export function ControlSkeleton() { export function ControlSkeleton() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex justify-between"> <div className="flex justify-between">
<Skeleton style={{ width: 72, height: 34 }} className="mb-3" /> <Skeleton style={{ width: 72, height: 34 }} className="mb-3" />
<div className="flex gap-2"> <div className="flex gap-2">
<Button icon={IconPencil} variant="secondary" disabled> <Button icon={IconPencil} variant="secondary" disabled>
{__("Edit control")} {t("controlSkeleton.actions.edit")}
</Button> </Button>
<ActionDropdown variant="secondary" /> <ActionDropdown variant="secondary" />
</div> </div>

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.
import { useTranslate } from "@probo/i18n";
import { Button, IconPlusLarge, PageHeader, Skeleton } from "@probo/ui"; import { Button, IconPlusLarge, PageHeader, Skeleton } from "@probo/ui";
import { useTranslation } from "react-i18next";
export function RisksPageSkeleton() { export function RisksPageSkeleton() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader title={__("Risks")}> <PageHeader title={t("risksPageSkeleton.title")}>
<Button icon={IconPlusLarge} disabled> <Button icon={IconPlusLarge} disabled>
{__("New Risk")} {t("risksPageSkeleton.actions.create")}
</Button> </Button>
</PageHeader> </PageHeader>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">

View File

@@ -18,8 +18,6 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -37,6 +35,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -46,18 +45,18 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
import { LinkedStatementsOfApplicabilityDialog } from "./LinkedStatementsOfApplicabilityDialog"; import { LinkedStatementsOfApplicabilityDialog } from "./LinkedStatementsOfApplicabilityDialog";
const linkedStatementOfApplicabilityFragment = graphql` const linkedStatementOfApplicabilityFragment = graphql`
fragment LinkedStatementsOfApplicabilityCardFragment on ApplicabilityStatement { fragment LinkedStatementsOfApplicabilityCardFragment on ApplicabilityStatement {
id id
statementOfApplicability { statementOfApplicability {
id id
name name
}
control {
id
}
applicability
justification
} }
control {
id
}
applicability
justification
}
`; `;
type AttachMutation<Params> = (p: { type AttachMutation<Params> = (p: {
@@ -94,8 +93,10 @@ type Props<Params> = {
readOnly?: boolean; readOnly?: boolean;
}; };
export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>) { export function LinkedStatementsOfApplicabilityCard<Params>(
const { __ } = useTranslate(); props: Props<Params>,
) {
const { t } = useTranslation();
const [limit, setLimit] = useState<number | null>( const [limit, setLimit] = useState<number | null>(
props.variant === "card" ? 4 : null, props.variant === "card" ? 4 : null,
@@ -171,7 +172,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
{variant === "card" && ( {variant === "card" && (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="text-lg font-semibold"> <div className="text-lg font-semibold">
{__("Statements of Applicability")} {t("linkedStatementsOfApplicabilityCard.title")}
</div> </div>
{!props.readOnly && ( {!props.readOnly && (
<LinkedStatementsOfApplicabilityDialog <LinkedStatementsOfApplicabilityDialog
@@ -182,7 +183,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
onUnlink={onDetach} onUnlink={onDetach}
> >
<Button variant="tertiary" icon={IconPlusLarge}> <Button variant="tertiary" icon={IconPlusLarge}>
{__("Link statement of applicability")} {t("linkedStatementsOfApplicabilityCard.actions.link")}
</Button> </Button>
</LinkedStatementsOfApplicabilityDialog> </LinkedStatementsOfApplicabilityDialog>
)} )}
@@ -191,9 +192,13 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
<Table className={clsx(variant === "card" && "bg-invert")}> <Table className={clsx(variant === "card" && "bg-invert")}>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("linkedStatementsOfApplicabilityCard.columns.name")}</Th>
<Th>{__("Applicability")}</Th> <Th>
<Th>{__("Justification")}</Th> {t("linkedStatementsOfApplicabilityCard.columns.applicability")}
</Th>
<Th>
{t("linkedStatementsOfApplicabilityCard.columns.justification")}
</Th>
{!props.readOnly && <Th></Th>} {!props.readOnly && <Th></Th>}
</Tr> </Tr>
</Thead> </Thead>
@@ -204,7 +209,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
colSpan={props.readOnly ? 3 : 4} colSpan={props.readOnly ? 3 : 4}
className="text-center text-txt-secondary" className="text-center text-txt-secondary"
> >
{__("No statements of applicability linked")} {t("linkedStatementsOfApplicabilityCard.empty")}
</Td> </Td>
</Tr> </Tr>
)} )}
@@ -225,7 +230,7 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
onUnlink={onDetach} onUnlink={onDetach}
> >
<TrButton colspan={4} icon={IconPlusLarge}> <TrButton colspan={4} icon={IconPlusLarge}>
{__("Link statement of applicability")} {t("linkedStatementsOfApplicabilityCard.actions.link")}
</TrButton> </TrButton>
</LinkedStatementsOfApplicabilityDialog> </LinkedStatementsOfApplicabilityDialog>
)} )}
@@ -237,10 +242,9 @@ export function LinkedStatementsOfApplicabilityCard<Params>(props: Props<Params>
icon={IconChevronDown} icon={IconChevronDown}
onClick={() => setLimit(null)} onClick={() => setLimit(null)}
> >
{sprintf( {t("linkedStatementsOfApplicabilityCard.actions.showMore", {
__("Show %d more"), count: props.statementsOfApplicability.length - limit,
props.statementsOfApplicability.length - limit, })}
)}
</Button> </Button>
)} )}
</Wrapper> </Wrapper>
@@ -256,10 +260,7 @@ function LinkedInfoExtractor(props: {
}) { }) {
const { onExtracted, fragment } = props; const { onExtracted, fragment } = props;
const data = useFragment( const data = useFragment(linkedStatementOfApplicabilityFragment, fragment);
linkedStatementOfApplicabilityFragment,
fragment,
);
useEffect(() => { useEffect(() => {
onExtracted({ onExtracted({
@@ -283,7 +284,7 @@ function StatementOfApplicabilityRow(props: {
props.statementOfApplicability, props.statementOfApplicability,
); );
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Tr <Tr
@@ -293,8 +294,8 @@ function StatementOfApplicabilityRow(props: {
<Td> <Td>
<Badge variant={soa.applicability ? "success" : "danger"}> <Badge variant={soa.applicability ? "success" : "danger"}>
{soa.applicability {soa.applicability
? __("Applicable") ? t("linkedStatementsOfApplicabilityCard.applicable")
: __("Not Applicable")} : t("linkedStatementsOfApplicabilityCard.notApplicable")}
</Badge> </Badge>
</Td> </Td>
<Td>{soa.justification || "-"}</Td> <Td>{soa.justification || "-"}</Td>
@@ -303,13 +304,10 @@ function StatementOfApplicabilityRow(props: {
<Button <Button
variant="secondary" variant="secondary"
onClick={() => onClick={() =>
props.onClick( props.onClick(soa.statementOfApplicability.id, soa.control.id)}
soa.statementOfApplicability.id,
soa.control.id,
)}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {t("linkedStatementsOfApplicabilityCard.actions.unlink")}
</Button> </Button>
</Td> </Td>
)} )}

View File

@@ -18,7 +18,6 @@
// 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 { import {
Badge, Badge,
Button, Button,
@@ -30,6 +29,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Suspense, useRef, useState } from "react"; import { Suspense, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -75,13 +75,14 @@ export function LinkedStatementsOfApplicabilityDialog({
children, children,
...props ...props
}: Props) { }: Props) {
const { t } = useTranslation();
const dialogRef = useRef<{ open: () => void; close: () => void }>(null); const dialogRef = useRef<{ open: () => void; close: () => void }>(null);
return ( return (
<Dialog <Dialog
ref={dialogRef} ref={dialogRef}
trigger={children} trigger={children}
title="Link Statement of Applicability" title={t("linkedStatementsOfApplicabilityDialog.title")}
> >
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<LinkedStatementsOfApplicabilityDialogContent <LinkedStatementsOfApplicabilityDialogContent
@@ -96,7 +97,7 @@ export function LinkedStatementsOfApplicabilityDialog({
function LinkedStatementsOfApplicabilityDialogContent( function LinkedStatementsOfApplicabilityDialogContent(
props: Omit<Props, "children"> & { onClose: () => void }, props: Omit<Props, "children"> & { onClose: () => void },
) { ) {
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const [selectedSOA, setSelectedSOA] = useState<{ const [selectedSOA, setSelectedSOA] = useState<{
id: string; id: string;
@@ -163,12 +164,10 @@ function LinkedStatementsOfApplicabilityDialogContent(
? ( ? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-txt-secondary text-base mb-2"> <div className="text-txt-secondary text-base mb-2">
{__("No statements of applicability available")} {t("linkedStatementsOfApplicabilityDialog.empty.title")}
</div> </div>
<div className="text-txt-tertiary text-sm"> <div className="text-txt-tertiary text-sm">
{__( {t("linkedStatementsOfApplicabilityDialog.empty.description")}
"Create a statement of applicability first to link it to this control",
)}
</div> </div>
</div> </div>
) )
@@ -176,7 +175,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
? ( ? (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium mb-2"> <div className="text-sm font-medium mb-2">
{__("Select a statement of applicability:")} {t("linkedStatementsOfApplicabilityDialog.select")}
</div> </div>
{statementsOfApplicability.map((soa) => { {statementsOfApplicability.map((soa) => {
const isLinked = linkedSOAIds.has(soa.id); const isLinked = linkedSOAIds.has(soa.id);
@@ -197,7 +196,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<Badge variant="success"> <Badge variant="success">
{__("Linked")} {t("linkedStatementsOfApplicabilityDialog.linked")}
</Badge> </Badge>
<Button <Button
variant="danger" variant="danger"
@@ -205,7 +204,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
handleUnlink(soa.id)} handleUnlink(soa.id)}
disabled={props.disabled} disabled={props.disabled}
> >
{__("Unlink")} {t("linkedStatementsOfApplicabilityDialog.actions.unlink")}
</Button> </Button>
</div> </div>
) )
@@ -220,7 +219,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<div className="text-sm text-txt-secondary mb-1"> <div className="text-sm text-txt-secondary mb-1">
{__("Selected:")} {t("linkedStatementsOfApplicabilityDialog.selected")}
</div> </div>
<div className="text-lg font-medium"> <div className="text-lg font-medium">
{selectedSOA.name} {selectedSOA.name}
@@ -230,7 +229,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
variant="tertiary" variant="tertiary"
onClick={() => setSelectedSOA(null)} onClick={() => setSelectedSOA(null)}
> >
{__("Change")} {t("linkedStatementsOfApplicabilityDialog.actions.change")}
</Button> </Button>
</div> </div>
@@ -242,16 +241,16 @@ function LinkedStatementsOfApplicabilityDialogContent(
setApplicability(checked)} setApplicability(checked)}
/> />
<span className="font-medium"> <span className="font-medium">
{__("Applicable")} {t("linkedStatementsOfApplicabilityDialog.applicable")}
</span> </span>
</label> </label>
<div> <div>
<label className="text-sm font-medium mb-1 block"> <label className="text-sm font-medium mb-1 block">
{__("Justification (optional)")} {t("linkedStatementsOfApplicabilityDialog.justification.label")}
</label> </label>
<Textarea <Textarea
placeholder={__("Add a justification...")} placeholder={t("linkedStatementsOfApplicabilityDialog.justification.placeholder")}
value={justification} value={justification}
onChange={e => onChange={e =>
setJustification(e.target.value)} setJustification(e.target.value)}
@@ -262,7 +261,7 @@ function LinkedStatementsOfApplicabilityDialogContent(
</div> </div>
)} )}
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")}> <DialogFooter exitLabel={t("linkedStatementsOfApplicabilityDialog.actions.close")}>
{selectedSOA {selectedSOA
? ( ? (
<> <>
@@ -270,14 +269,14 @@ function LinkedStatementsOfApplicabilityDialogContent(
variant="secondary" variant="secondary"
onClick={() => setSelectedSOA(null)} onClick={() => setSelectedSOA(null)}
> >
{__("Back")} {t("linkedStatementsOfApplicabilityDialog.actions.back")}
</Button> </Button>
<Button <Button
variant="primary" variant="primary"
onClick={handleLink} onClick={handleLink}
disabled={props.disabled} disabled={props.disabled}
> >
{__("Link")} {t("linkedStatementsOfApplicabilityDialog.actions.link")}
</Button> </Button>
</> </>
) )

View File

@@ -19,13 +19,13 @@
// SOFTWARE. // SOFTWARE.
import { useStateWithRef } from "@probo/hooks"; import { useStateWithRef } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { EditableCell, selectCell, SelectValue, Spinner } from "@probo/ui"; import { EditableCell, selectCell, SelectValue, Spinner } from "@probo/ui";
import { useEditableCellRef } from "@probo/ui/src/Molecules/Table/EditableCell"; import { useEditableCellRef } from "@probo/ui/src/Molecules/Table/EditableCell";
import { useEditableRowContext } from "@probo/ui/src/Molecules/Table/EditableRow"; import { useEditableRowContext } from "@probo/ui/src/Molecules/Table/EditableRow";
import { getKey } from "@probo/ui/src/Molecules/Table/utils"; import { getKey } from "@probo/ui/src/Molecules/Table/utils";
import { Command } from "cmdk"; import { Command } from "cmdk";
import { type ReactNode, Suspense } from "react"; import { type ReactNode, Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useLazyLoadQuery } from "react-relay"; import { useLazyLoadQuery } from "react-relay";
import type { import type {
GraphQLTaggedNode, GraphQLTaggedNode,
@@ -44,14 +44,23 @@ type Props<Q extends OperationType, T> = {
| { defaultValue: T[]; multiple: true } | { defaultValue: T[]; multiple: true }
); );
export function GraphQLCell<Q extends OperationType, T extends NonNullable<unknown>>(props: Props<Q, T>) { export function GraphQLCell<
Q extends OperationType,
T extends NonNullable<unknown>,
>(props: Props<Q, T>) {
const [value, setValue, valueRef] = useStateWithRef<T | T[] | undefined>( const [value, setValue, valueRef] = useStateWithRef<T | T[] | undefined>(
props.defaultValue, props.defaultValue,
); );
const cellRef = useEditableCellRef(); const cellRef = useEditableCellRef();
const { __ } = useTranslate(); const { t } = useTranslation();
const filteredValue = Array.isArray(value) ? value.filter(Boolean) : value ? [value] : []; const filteredValue = Array.isArray(value)
const usedKeys = new Set<string>(filteredValue.map(getKey).filter(Boolean) as string[]); ? value.filter(Boolean)
: value
? [value]
: [];
const usedKeys = new Set<string>(
filteredValue.map(getKey).filter(Boolean) as string[],
);
const { onUpdate } = useEditableRowContext(); const { onUpdate } = useEditableRowContext();
const onSelect = (item: T) => { const onSelect = (item: T) => {
@@ -104,7 +113,7 @@ export function GraphQLCell<Q extends OperationType, T extends NonNullable<unkno
{props.multiple && ( {props.multiple && (
<Command.Input <Command.Input
className={classNames.input()} className={classNames.input()}
placeholder={__("Search")} placeholder={t("graphQLCell.searchPlaceholder")}
/> />
)} )}
<Command.List> <Command.List>

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
CellHead, CellHead,
@@ -35,6 +34,7 @@ import {
useContext, useContext,
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import type { LoadMoreFn } from "react-relay"; import type { LoadMoreFn } from "react-relay";
import type { OperationType } from "relay-runtime"; import type { OperationType } from "relay-runtime";
@@ -75,7 +75,7 @@ export function SortableDataTable({
isLoadingNext?: boolean; isLoadingNext?: boolean;
pageSize?: number; pageSize?: number;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [order, setOrder] = useState(defaultOrder); const [order, setOrder] = useState(defaultOrder);
const onOrderChange = (o: Order) => { const onOrderChange = (o: Order) => {
startTransition(() => { startTransition(() => {
@@ -95,7 +95,7 @@ export function SortableDataTable({
disabled={isLoadingNext} disabled={isLoadingNext}
icon={isLoadingNext ? Spinner : IconChevronDown} icon={isLoadingNext ? Spinner : IconChevronDown}
> >
{__("Show more")} {t("sortableDataTable.actions.showMore")}
</Button> </Button>
)} )}
</div> </div>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { formatDatetime } from "@probo/helpers"; import { formatDatetime } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Dialog, Dialog,
@@ -40,6 +39,7 @@ import {
import { Breadcrumb } from "@probo/ui"; import { Breadcrumb } from "@probo/ui";
import { type ReactNode, useEffect } from "react"; import { type ReactNode, useEffect } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useFragment, useRelayEnvironment } from "react-relay"; import { useFragment, useRelayEnvironment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
@@ -143,7 +143,7 @@ type Props = {
export default function TaskFormDialog(props: Props) { export default function TaskFormDialog(props: Props) {
const { children, connection, ref, task: taskKey, measureId, onCompleted } = props; const { children, connection, ref, task: taskKey, measureId, onCompleted } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const newRef = useDialogRef(); const newRef = useDialogRef();
const dialogRef = ref ?? newRef; const dialogRef = ref ?? newRef;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
@@ -152,8 +152,8 @@ export default function TaskFormDialog(props: Props) {
const [mutate] = useMutationWithToasts( const [mutate] = useMutationWithToasts(
task ? taskUpdateMutation : taskCreateMutation, task ? taskUpdateMutation : taskCreateMutation,
{ {
successMessage: __(`Task ${task ? "updated" : "created"} successfully.`), successMessage: t(task ? "taskFormDialog.messages.updated" : "taskFormDialog.messages.created"),
errorMessage: __(`Failed to ${task ? "update" : "create"} task`), errorMessage: t(task ? "taskFormDialog.errors.update" : "taskFormDialog.errors.create"),
}, },
); );
@@ -246,7 +246,7 @@ export default function TaskFormDialog(props: Props) {
trigger={children} trigger={children}
title={( title={(
<Breadcrumb <Breadcrumb
items={[__("Tasks"), isUpdating ? __("Edit Task") : __("New Task")]} items={[t("taskFormDialog.breadcrumb.tasks"), isUpdating ? t("taskFormDialog.breadcrumb.edit") : t("taskFormDialog.breadcrumb.new")]}
/> />
)} )}
> >
@@ -257,23 +257,23 @@ export default function TaskFormDialog(props: Props) {
id="title" id="title"
required required
variant="title" variant="title"
placeholder={__("Task title")} placeholder={t("taskFormDialog.fields.title.placeholder")}
{...register("name")} {...register("name")}
/> />
<Textarea <Textarea
id="content" id="content"
variant="ghost" variant="ghost"
autogrow autogrow
placeholder={__("Add description")} placeholder={t("taskFormDialog.fields.description.placeholder")}
{...register("description")} {...register("description")}
/> />
</div> </div>
{/* Properties form */} {/* Properties form */}
<div className="py-5 px-6 bg-subtle"> <div className="py-5 px-6 bg-subtle">
<Label>{__("Properties")}</Label> <Label>{t("taskFormDialog.properties")}</Label>
{isUpdating && ( {isUpdating && (
<PropertyRow <PropertyRow
label={__("State")} label={t("taskFormDialog.fields.state.label")}
error={"state" in formState.errors ? formState.errors.state?.message : undefined} error={"state" in formState.errors ? formState.errors.state?.message : undefined}
> >
<Controller <Controller
@@ -287,19 +287,19 @@ export default function TaskFormDialog(props: Props) {
<Option value="TODO"> <Option value="TODO">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<TaskStateIcon state="TODO" /> <TaskStateIcon state="TODO" />
{__("To do")} {t("taskFormDialog.states.todo")}
</span> </span>
</Option> </Option>
<Option value="IN_PROGRESS"> <Option value="IN_PROGRESS">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<TaskStateIcon state="IN_PROGRESS" /> <TaskStateIcon state="IN_PROGRESS" />
{__("In progress")} {t("taskFormDialog.states.inProgress")}
</span> </span>
</Option> </Option>
<Option value="DONE"> <Option value="DONE">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<TaskStateIcon state="DONE" /> <TaskStateIcon state="DONE" />
{__("Done")} {t("taskFormDialog.states.done")}
</span> </span>
</Option> </Option>
</Select> </Select>
@@ -308,7 +308,7 @@ export default function TaskFormDialog(props: Props) {
</PropertyRow> </PropertyRow>
)} )}
<PropertyRow <PropertyRow
label={__("Priority")} label={t("taskFormDialog.fields.priority.label")}
error={formState.errors.priority?.message} error={formState.errors.priority?.message}
> >
<Controller <Controller
@@ -322,25 +322,25 @@ export default function TaskFormDialog(props: Props) {
<Option value="URGENT"> <Option value="URGENT">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<PriorityLevel level="URGENT" /> <PriorityLevel level="URGENT" />
{__("Urgent")} {t("taskFormDialog.priorities.urgent")}
</span> </span>
</Option> </Option>
<Option value="HIGH"> <Option value="HIGH">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<PriorityLevel level="HIGH" /> <PriorityLevel level="HIGH" />
{__("High")} {t("taskFormDialog.priorities.high")}
</span> </span>
</Option> </Option>
<Option value="MEDIUM"> <Option value="MEDIUM">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<PriorityLevel level="MEDIUM" /> <PriorityLevel level="MEDIUM" />
{__("Medium")} {t("taskFormDialog.priorities.medium")}
</span> </span>
</Option> </Option>
<Option value="LOW"> <Option value="LOW">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<PriorityLevel level="LOW" /> <PriorityLevel level="LOW" />
{__("Low")} {t("taskFormDialog.priorities.low")}
</span> </span>
</Option> </Option>
</Select> </Select>
@@ -348,7 +348,7 @@ export default function TaskFormDialog(props: Props) {
/> />
</PropertyRow> </PropertyRow>
<PropertyRow <PropertyRow
label={__("Assigned to")} label={t("taskFormDialog.fields.assignedTo.label")}
error={formState.errors.assignedToId?.message} error={formState.errors.assignedToId?.message}
> >
<PeopleSelectField <PeopleSelectField
@@ -360,7 +360,7 @@ export default function TaskFormDialog(props: Props) {
</PropertyRow> </PropertyRow>
{showMeasure && ( {showMeasure && (
<PropertyRow <PropertyRow
label={__("Measure")} label={t("taskFormDialog.fields.measure.label")}
error={formState.errors.measureId?.message} error={formState.errors.measureId?.message}
> >
<MeasureSelectField <MeasureSelectField
@@ -372,7 +372,7 @@ export default function TaskFormDialog(props: Props) {
</PropertyRow> </PropertyRow>
)} )}
<PropertyRow <PropertyRow
label={__("Time estimate")} label={t("taskFormDialog.fields.timeEstimate.label")}
error={formState.errors.timeEstimate?.message} error={formState.errors.timeEstimate?.message}
> >
<Controller <Controller
@@ -388,7 +388,7 @@ export default function TaskFormDialog(props: Props) {
/> />
</PropertyRow> </PropertyRow>
<PropertyRow <PropertyRow
label={__("Deadline")} label={t("taskFormDialog.fields.deadline.label")}
error={formState.errors.deadline?.message} error={formState.errors.deadline?.message}
> >
<Input id="deadline" type="date" {...register("deadline")} /> <Input id="deadline" type="date" {...register("deadline")} />
@@ -397,7 +397,7 @@ export default function TaskFormDialog(props: Props) {
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button type="submit"> <Button type="submit">
{isUpdating ? __("Update task") : __("Create task")} {isUpdating ? t("taskFormDialog.actions.update") : t("taskFormDialog.actions.create")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

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 { formatDate, formatDuration, formatError, promisifyMutation } from "@probo/helpers"; import { formatError, promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { dateFormat, formatDuration } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -38,6 +38,7 @@ import {
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { Fragment, type ReactNode, useRef, useState, useTransition } from "react"; import { Fragment, type ReactNode, useRef, useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { import {
graphql, graphql,
readInlineData, readInlineData,
@@ -185,7 +186,7 @@ const updateRankMutation = graphql`
`; `;
export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) { export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
const hash = useLocation().hash.replace("#", ""); const hash = useLocation().hash.replace("#", "");
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
@@ -205,13 +206,13 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
}; };
const stateHashes = [ const stateHashes = [
{ hash: "todo", label: __("To do"), state: "TODO" }, { hash: "todo", label: t("tasksCard.states.todo"), state: "TODO" },
{ hash: "in-progress", label: __("In progress"), state: "IN_PROGRESS" }, { hash: "in-progress", label: t("tasksCard.states.inProgress"), state: "IN_PROGRESS" },
{ hash: "done", label: __("Done"), state: "DONE" }, { hash: "done", label: t("tasksCard.states.done"), state: "DONE" },
] as const; ] as const;
const hashes = [ const hashes = [
{ hash: "", label: __("All"), state: null }, { hash: "", label: t("tasksCard.states.all"), state: null },
...stateHashes, ...stateHashes,
] as const; ] as const;
@@ -346,8 +347,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
onCompleted: (_, errors) => { onCompleted: (_, errors) => {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("tasksCard.error.title"),
description: formatError(__("Failed to reorder task."), errors), description: formatError(t("tasksCard.error.reorder"), errors),
variant: "error", variant: "error",
}); });
} }
@@ -365,7 +366,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
onError: () => { onError: () => {
droppedRef.current = false; droppedRef.current = false;
resetDragState(); resetDragState();
toast({ title: __("Error"), description: __("Failed to reorder task."), variant: "error" }); toast({ title: t("tasksCard.error.title"), description: t("tasksCard.error.reorder"), variant: "error" });
}, },
}); });
}; };
@@ -413,7 +414,7 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
<div className="space-y-6"> <div className="space-y-6">
{tasks.length === 0 {tasks.length === 0
? ( ? (
<p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p> <p className="text-center py-6 text-txt-secondary">{t("tasksCard.empty")}</p>
) )
: ( : (
<Card> <Card>
@@ -463,8 +464,8 @@ export function TasksCard({ tasks, connectionId, canReorder, refetch }: Props) {
{canDrag && filteredTasks.length > 1 && ( {canDrag && filteredTasks.length > 1 && (
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{hash === "" {hash === ""
? __("Drag and drop to reorder tasks or move them between states") ? t("tasksCard.dragInstructions.all")
: __("Drag and drop to reorder tasks")} : t("tasksCard.dragInstructions.state")}
</p> </p>
)} )}
</div> </div>
@@ -521,7 +522,7 @@ const deleteMutation = graphql`
function TaskRow(props: TaskRowProps) { function TaskRow(props: TaskRowProps) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { __ } = useTranslate(); const { t, i18n } = useTranslation();
const confirm = useConfirm(); const confirm = useConfirm();
const [deleteTask] = useMutation<TasksCardDeleteMutation>(deleteMutation); const [deleteTask] = useMutation<TasksCardDeleteMutation>(deleteMutation);
const params = useParams<{ measureId?: string }>(); const params = useParams<{ measureId?: string }>();
@@ -542,8 +543,8 @@ function TaskRow(props: TaskRowProps) {
icon: typeof IconCircleProgress; icon: typeof IconCircleProgress;
className: string; className: string;
}> = { }> = {
TODO: { state: "IN_PROGRESS", label: __("Move to In progress"), icon: IconCircleProgress, className: "text-txt-warning" }, TODO: { state: "IN_PROGRESS", label: t("tasksCard.actions.moveToInProgress"), icon: IconCircleProgress, className: "text-txt-warning" },
IN_PROGRESS: { state: "DONE", label: __("Move to Done"), icon: IconCircleCheck, className: "text-txt-accent" }, IN_PROGRESS: { state: "DONE", label: t("tasksCard.actions.moveToDone"), icon: IconCircleCheck, className: "text-txt-accent" },
}; };
const onAdvance = async () => { const onAdvance = async () => {
@@ -581,7 +582,7 @@ function TaskRow(props: TaskRowProps) {
}, },
}), }),
{ {
message: "Are you sure you want to delete this task?", message: t("tasksCard.deleteConfirmation"),
}, },
); );
}; };
@@ -642,11 +643,11 @@ function TaskRow(props: TaskRowProps) {
</span> </span>
)} )}
{task.timeEstimate && ( {task.timeEstimate && (
<span>{formatDuration(task.timeEstimate, __)}</span> <span>{formatDuration(task.timeEstimate, t)}</span>
)} )}
{task.deadline && ( {task.deadline && (
<time dateTime={task.deadline}> <time dateTime={task.deadline}>
{formatDate(task.deadline)} {dateFormat(i18n.language, task.deadline)}
</time> </time>
)} )}
</div> </div>
@@ -677,7 +678,7 @@ function TaskRow(props: TaskRowProps) {
<Button <Button
variant="secondary" variant="secondary"
icon={IconPencil} icon={IconPencil}
title={__("Edit")} title={t("tasksCard.actions.edit")}
onClick={() => dialogRef.current?.open()} onClick={() => dialogRef.current?.open()}
/> />
)} )}
@@ -685,7 +686,7 @@ function TaskRow(props: TaskRowProps) {
<Button <Button
variant="danger" variant="danger"
icon={IconTrashCan} icon={IconTrashCan}
title={__("Delete")} title={t("tasksCard.actions.delete")}
onClick={onDelete} onClick={onDelete}
/> />
)} )}

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 { useTranslate } from "@probo/i18n";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
@@ -88,11 +88,11 @@ const thirdPartyUpdateQuery = graphql`
export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key) { export function useThirdPartyForm(thirdPartyKey: useThirdPartyFormFragment$key) {
const thirdParty = useFragment(thirdPartyFormFragment, thirdPartyKey); const thirdParty = useFragment(thirdPartyFormFragment, thirdPartyKey);
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(thirdPartyUpdateQuery, { const [mutate] = useMutationWithToasts(thirdPartyUpdateQuery, {
successMessage: __("Third party updated successfully."), successMessage: t("thirdPartyForm.messages.updated"),
errorMessage: __("Failed to update third party"), errorMessage: t("thirdPartyForm.messages.updateError"),
}); });
const defaultValues = useMemo( const defaultValues = useMemo(

View File

@@ -18,9 +18,9 @@
// 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 { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -163,11 +163,11 @@ export const useDeleteAsset = (
) => { ) => {
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation); const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const { __ } = useTranslate(); const { t } = useTranslation();
return () => { return () => {
if (!asset.id || !asset.name) { if (!asset.id || !asset.name) {
return alert(__("Failed to delete asset: missing id or name")); return alert(t("assetGraph.errors.deleteMissingIdOrName"));
} }
confirm( confirm(
() => () =>
@@ -180,12 +180,7 @@ export const useDeleteAsset = (
}, },
}), }),
{ {
message: sprintf( message: t("assetGraph.deleteConfirmation", { name: asset.name }),
__(
"This will permanently delete \"%s\". This action cannot be undone.",
),
asset.name,
),
}, },
); );
}; };
@@ -193,7 +188,7 @@ export const useDeleteAsset = (
export const useCreateAsset = (connectionId: string) => { export const useCreateAsset = (connectionId: string) => {
const [mutate, isMutating] = useMutation<AssetGraphCreateMutation>(createAssetMutation); const [mutate, isMutating] = useMutation<AssetGraphCreateMutation>(createAssetMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return [ return [
(input: { (input: {
@@ -206,18 +201,16 @@ export const useCreateAsset = (connectionId: string) => {
dataTypesStored: string; dataTypesStored: string;
}) => { }) => {
if (!input.name?.trim()) { if (!input.name?.trim()) {
return alert(__("Failed to create asset: name is required")); return alert(t("assetGraph.errors.createNameRequired"));
} }
if (!input.ownerId) { if (!input.ownerId) {
return alert(__("Failed to create asset: owner is required")); return alert(t("assetGraph.errors.createOwnerRequired"));
} }
if (!input.organizationId) { if (!input.organizationId) {
return alert(__("Failed to create asset: organization is required")); return alert(t("assetGraph.errors.createOrganizationRequired"));
} }
if (!input.dataTypesStored) { if (!input.dataTypesStored) {
return alert( return alert(t("assetGraph.errors.createDataTypesStoredRequired"));
__("Failed to create asset: data types stored is required"),
);
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -240,7 +233,7 @@ export const useCreateAsset = (connectionId: string) => {
}; };
export const useUpdateAsset = () => { export const useUpdateAsset = () => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutation<AssetGraphUpdateMutation>(updateAssetMutation); const [mutate] = useMutation<AssetGraphUpdateMutation>(updateAssetMutation);
return (input: { return (input: {
@@ -253,7 +246,7 @@ export const useUpdateAsset = () => {
thirdPartyIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update asset: asset ID is required")); return alert(t("assetGraph.errors.updateIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({

View File

@@ -18,9 +18,9 @@
// 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 { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -148,10 +148,10 @@ export const useDeleteAudit = (
connectionId: string, connectionId: string,
onSuccess?: () => void, onSuccess?: () => void,
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(deleteAuditMutation, { const [mutate] = useMutationWithToasts(deleteAuditMutation, {
successMessage: __("Audit deleted successfully"), successMessage: t("auditGraph.messages.deleted"),
errorMessage: __("Failed to delete audit"), errorMessage: t("auditGraph.errors.delete"),
}); });
const confirm = useConfirm(); const confirm = useConfirm();
@@ -169,12 +169,7 @@ export const useDeleteAudit = (
onSuccess?.(); onSuccess?.();
}, },
{ {
message: sprintf( message: t("auditGraph.deleteConfirmation", { frameworkName: audit.framework?.name ?? "" }),
__(
"This will permanently delete the audit for %s. This action cannot be undone.",
),
audit.framework?.name ?? "",
),
}, },
); );
}; };
@@ -183,7 +178,7 @@ export const useDeleteAudit = (
export const useCreateAudit = (connectionId: string) => { export const useCreateAudit = (connectionId: string) => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createAuditMutation); const [mutate] = useMutation(createAuditMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
organizationId: string; organizationId: string;
@@ -196,10 +191,10 @@ export const useCreateAudit = (connectionId: string) => {
file?: File | null; file?: File | null;
}) => { }) => {
if (!input.organizationId) { if (!input.organizationId) {
return alert(__("Failed to create audit: organization is required")); return alert(t("auditGraph.errors.createOrganizationRequired"));
} }
if (!input.frameworkId) { if (!input.frameworkId) {
return alert(__("Failed to create audit: framework is required")); return alert(t("auditGraph.errors.createFrameworkRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -224,7 +219,7 @@ export const useCreateAudit = (connectionId: string) => {
export const useUpdateAudit = () => { export const useUpdateAudit = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateAuditMutation); const [mutate] = useMutation(updateAuditMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -234,7 +229,7 @@ export const useUpdateAudit = () => {
state?: string; state?: string;
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update audit: audit ID is required")); return alert(t("auditGraph.errors.updateIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -263,15 +258,15 @@ export const uploadAuditReportMutation = graphql`
`; `;
export const useUploadAuditReport = () => { export const useUploadAuditReport = () => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate, isLoading] = useMutationWithToasts(uploadAuditReportMutation, { const [mutate, isLoading] = useMutationWithToasts(uploadAuditReportMutation, {
successMessage: __("Audit report uploaded successfully"), successMessage: t("auditGraph.messages.reportUploaded"),
errorMessage: __("Failed to upload audit report"), errorMessage: t("auditGraph.errors.uploadReport"),
}); });
const uploadAuditReport = (input: { auditId: string; file: File }) => { const uploadAuditReport = (input: { auditId: string; file: File }) => {
if (!input.auditId) { if (!input.auditId) {
return alert(__("Failed to upload report: audit ID is required")); return alert(t("auditGraph.errors.uploadReportIdRequired"));
} }
return mutate({ return mutate({
@@ -308,10 +303,10 @@ export const deleteAuditReportMutation = graphql`
`; `;
export const useDeleteAuditReport = () => { export const useDeleteAuditReport = () => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(deleteAuditReportMutation, { const [mutate] = useMutationWithToasts(deleteAuditReportMutation, {
successMessage: __("Audit report deleted successfully"), successMessage: t("auditGraph.messages.reportDeleted"),
errorMessage: __("Failed to delete audit report"), errorMessage: t("auditGraph.errors.deleteReport"),
}); });
return (input: { auditId: string }) => { return (input: { auditId: string }) => {

View File

@@ -18,9 +18,9 @@
// 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 { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -154,11 +154,11 @@ export const useDeleteDatum = (
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(deleteDatumMutation); const [mutate] = useMutation(deleteDatumMutation);
const confirm = useConfirm(); const confirm = useConfirm();
const { __ } = useTranslate(); const { t } = useTranslation();
return () => { return () => {
if (!datum.id || !datum.name) { if (!datum.id || !datum.name) {
return alert(__("Failed to delete data: missing id or name")); return alert(t("datumGraph.errors.deleteMissingIdOrName"));
} }
confirm( confirm(
() => () =>
@@ -171,12 +171,7 @@ export const useDeleteDatum = (
}, },
}), }),
{ {
message: sprintf( message: t("datumGraph.deleteConfirmation", { name: datum.name }),
__(
"This will permanently delete \"%s\". This action cannot be undone.",
),
datum.name,
),
}, },
); );
}; };
@@ -185,7 +180,7 @@ export const useDeleteDatum = (
export const useCreateDatum = (connectionId: string) => { export const useCreateDatum = (connectionId: string) => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createDatumMutation); const [mutate] = useMutation(createDatumMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
name: string; name: string;
@@ -195,13 +190,13 @@ export const useCreateDatum = (connectionId: string) => {
thirdPartyIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.name?.trim()) { if (!input.name?.trim()) {
return alert(__("Failed to create data: name is required")); return alert(t("datumGraph.errors.createNameRequired"));
} }
if (!input.ownerId) { if (!input.ownerId) {
return alert(__("Failed to create data: owner is required")); return alert(t("datumGraph.errors.createOwnerRequired"));
} }
if (!input.organizationId) { if (!input.organizationId) {
return alert(__("Failed to create data: organization is required")); return alert(t("datumGraph.errors.createOrganizationRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -216,7 +211,7 @@ export const useCreateDatum = (connectionId: string) => {
export const useUpdateDatum = () => { export const useUpdateDatum = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateDatumMutation); const [mutate] = useMutation(updateDatumMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -226,7 +221,7 @@ export const useUpdateDatum = () => {
thirdPartyIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update data: missing id")); return alert(t("datumGraph.errors.updateMissingId"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({

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 { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { DocumentGraphBulkExportDocumentsMutation } from "#/__generated__/core/DocumentGraphBulkExportDocumentsMutation.graphql"; import type { DocumentGraphBulkExportDocumentsMutation } from "#/__generated__/core/DocumentGraphBulkExportDocumentsMutation.graphql";
@@ -40,13 +40,13 @@ const deleteDocumentMutation = graphql`
`; `;
export function useDeleteDocumentMutation() { export function useDeleteDocumentMutation() {
const { __ } = useTranslate(); const { t } = useTranslation();
return useMutationWithToasts<DocumentGraphDeleteMutation>( return useMutationWithToasts<DocumentGraphDeleteMutation>(
deleteDocumentMutation, deleteDocumentMutation,
{ {
successMessage: __("Document deleted successfully."), successMessage: t("documentGraph.messages.deleted"),
errorMessage: __("Failed to delete document"), errorMessage: t("documentGraph.errors.delete"),
}, },
); );
} }
@@ -62,11 +62,11 @@ const bulkDeleteDocumentsMutation = graphql`
`; `;
export function useBulkDeleteDocumentsMutation() { export function useBulkDeleteDocumentsMutation() {
const { __ } = useTranslate(); const { t } = useTranslation();
return useMutationWithToasts(bulkDeleteDocumentsMutation, { return useMutationWithToasts(bulkDeleteDocumentsMutation, {
successMessage: __("Documents deleted successfully."), successMessage: t("documentGraph.messages.bulkDeleted"),
errorMessage: __("Failed to delete documents"), errorMessage: t("documentGraph.errors.bulkDelete"),
}); });
} }
@@ -81,15 +81,13 @@ const bulkExportDocumentsMutation = graphql`
`; `;
export function useBulkExportDocumentsMutation() { export function useBulkExportDocumentsMutation() {
const { __ } = useTranslate(); const { t } = useTranslation();
return useMutationWithToasts<DocumentGraphBulkExportDocumentsMutation>( return useMutationWithToasts<DocumentGraphBulkExportDocumentsMutation>(
bulkExportDocumentsMutation, bulkExportDocumentsMutation,
{ {
successMessage: __( successMessage: t("documentGraph.messages.exportStarted"),
"Document export started successfully. You will receive an email when the export is ready.", errorMessage: t("documentGraph.errors.export"),
),
errorMessage: __("Failed to start document export"),
}, },
); );
} }

View File

@@ -18,10 +18,9 @@
// 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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "../useMutationWithToasts"; import { useMutationWithToasts } from "../useMutationWithToasts";
@@ -68,12 +67,12 @@ export const useDeleteFrameworkMutation = (
framework: { id: string; name: string }, framework: { id: string; name: string },
connectionId: string, connectionId: string,
) => { ) => {
const { t } = useTranslation();
const [commitDelete] = useMutationWithToasts(deleteFrameworkMutation, { const [commitDelete] = useMutationWithToasts(deleteFrameworkMutation, {
errorMessage: "Failed to delete framework", errorMessage: t("frameworkGraph.errors.delete"),
successMessage: "Framework deleted successfully", successMessage: t("frameworkGraph.messages.deleted"),
}); });
const confirm = useConfirm(); const confirm = useConfirm();
const { __ } = useTranslate();
return useCallback( return useCallback(
(options?: { onSuccess?: () => void }) => { (options?: { onSuccess?: () => void }) => {
@@ -90,16 +89,13 @@ export const useDeleteFrameworkMutation = (
}); });
}, },
{ {
message: sprintf( message: t("frameworkGraph.deleteConfirmation", {
__( name: framework.name,
"This will permanently delete framework \"%s\". This action cannot be undone.", }),
),
framework.name,
),
}, },
); );
}, },
[framework, connectionId, commitDelete, confirm, __], [framework, connectionId, commitDelete, confirm, t],
); );
}; };

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 { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { MeasureGraphDeleteMutation } from "#/__generated__/core/MeasureGraphDeleteMutation.graphql"; import type { MeasureGraphDeleteMutation } from "#/__generated__/core/MeasureGraphDeleteMutation.graphql";
@@ -39,13 +39,13 @@ const deleteMeasureMutation = graphql`
`; `;
export function useDeleteMeasureMutation() { export function useDeleteMeasureMutation() {
const { __ } = useTranslate(); const { t } = useTranslation();
return useMutationWithToasts<MeasureGraphDeleteMutation>( return useMutationWithToasts<MeasureGraphDeleteMutation>(
deleteMeasureMutation, deleteMeasureMutation,
{ {
successMessage: __("Measure deleted successfully."), successMessage: t("measureGraph.messages.deleted"),
errorMessage: __("Failed to delete measure"), errorMessage: t("measureGraph.errors.delete"),
}, },
); );
} }
@@ -61,10 +61,10 @@ const measureUpdateMutation = graphql`
`; `;
export const useUpdateMeasure = () => { export const useUpdateMeasure = () => {
const { __ } = useTranslate(); const { t } = useTranslation();
return useMutationWithToasts(measureUpdateMutation, { return useMutationWithToasts(measureUpdateMutation, {
successMessage: __("Measure updated successfully."), successMessage: t("measureGraph.messages.updated"),
errorMessage: __("Failed to update measure"), errorMessage: t("measureGraph.errors.update"),
}); });
}; };

View File

@@ -19,8 +19,8 @@
// SOFTWARE. // SOFTWARE.
import { promisifyMutation } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -149,10 +149,10 @@ export const useDeleteObligation = (
obligation: { id: string }, obligation: { id: string },
connectionId: string, connectionId: string,
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(deleteObligationMutation, { const [mutate] = useMutationWithToasts(deleteObligationMutation, {
successMessage: __("Obligation deleted successfully"), successMessage: t("obligationGraph.messages.deleted"),
errorMessage: __("Failed to delete obligation"), errorMessage: t("obligationGraph.errors.delete"),
}); });
const confirm = useConfirm(); const confirm = useConfirm();
@@ -168,9 +168,7 @@ export const useDeleteObligation = (
}, },
}), }),
{ {
message: __( message: t("obligationGraph.deleteConfirmation"),
"This will permanently delete this obligation. This action cannot be undone.",
),
}, },
); );
}; };
@@ -179,7 +177,7 @@ export const useDeleteObligation = (
export const useCreateObligation = (connectionId: string) => { export const useCreateObligation = (connectionId: string) => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createObligationMutation); const [mutate] = useMutation(createObligationMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
organizationId: string; organizationId: string;
@@ -195,10 +193,10 @@ export const useCreateObligation = (connectionId: string) => {
status: string; status: string;
}) => { }) => {
if (!input.organizationId) { if (!input.organizationId) {
return alert(__("Failed to create obligation: organization is required")); return alert(t("obligationGraph.errors.createOrganizationRequired"));
} }
if (!input.ownerId) { if (!input.ownerId) {
return alert(__("Failed to create obligation: owner is required")); return alert(t("obligationGraph.errors.createOwnerRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -225,7 +223,7 @@ export const useCreateObligation = (connectionId: string) => {
export const useUpdateObligation = () => { export const useUpdateObligation = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateObligationMutation); const [mutate] = useMutation(updateObligationMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -241,7 +239,7 @@ export const useUpdateObligation = () => {
status?: string; status?: string;
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update obligation: ID is required")); return alert(t("obligationGraph.errors.updateIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({

View File

@@ -18,9 +18,9 @@
// 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 { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -272,10 +272,10 @@ export const useDeleteProcessingActivity = (
processingActivity: { id: string; name: string }, processingActivity: { id: string; name: string },
connectionId: string, connectionId: string,
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(deleteProcessingActivityMutation, { const [mutate] = useMutationWithToasts(deleteProcessingActivityMutation, {
successMessage: __("Processing activity deleted successfully"), successMessage: t("processingActivityGraph.messages.deleted"),
errorMessage: __("Failed to delete processing activity"), errorMessage: t("processingActivityGraph.errors.delete"),
}); });
const confirm = useConfirm(); const confirm = useConfirm();
@@ -291,12 +291,9 @@ export const useDeleteProcessingActivity = (
}, },
}), }),
{ {
message: sprintf( message: t("processingActivityGraph.deleteConfirmation", {
__( name: processingActivity.name,
"This will permanently delete the processing activity %s. This action cannot be undone.", }),
),
processingActivity.name,
),
}, },
); );
}; };
@@ -305,7 +302,7 @@ export const useDeleteProcessingActivity = (
export const useCreateProcessingActivity = (connectionId?: string) => { export const useCreateProcessingActivity = (connectionId?: string) => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createProcessingActivityMutation); const [mutate] = useMutation(createProcessingActivityMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
organizationId: string; organizationId: string;
@@ -332,12 +329,12 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
}) => { }) => {
if (!input.organizationId) { if (!input.organizationId) {
return alert( return alert(
__("Failed to create processing activity: organization is required"), t("processingActivityGraph.errors.createOrganizationRequired"),
); );
} }
if (!input.name) { if (!input.name) {
return alert( return alert(
__("Failed to create processing activity: name is required"), t("processingActivityGraph.errors.createNameRequired"),
); );
} }
@@ -376,7 +373,7 @@ export const useCreateProcessingActivity = (connectionId?: string) => {
export const useUpdateProcessingActivity = () => { export const useUpdateProcessingActivity = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateProcessingActivityMutation); const [mutate] = useMutation(updateProcessingActivityMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -402,7 +399,7 @@ export const useUpdateProcessingActivity = () => {
thirdPartyIds?: string[]; thirdPartyIds?: string[];
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update processing activity: ID is required")); return alert(t("processingActivityGraph.errors.updateIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -489,7 +486,7 @@ export const deleteDataProtectionImpactAssessmentMutation = graphql`
export const useCreateDataProtectionImpactAssessment = () => { export const useCreateDataProtectionImpactAssessment = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createDataProtectionImpactAssessmentMutation); const [mutate] = useMutation(createDataProtectionImpactAssessmentMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
processingActivityId: string; processingActivityId: string;
@@ -501,7 +498,7 @@ export const useCreateDataProtectionImpactAssessment = () => {
}) => { }) => {
if (!input.processingActivityId) { if (!input.processingActivityId) {
return alert( return alert(
__("Failed to create DPIA: Processing Activity ID is required"), t("processingActivityGraph.errors.createDpiaProcessingActivityIdRequired"),
); );
} }
@@ -516,7 +513,7 @@ export const useCreateDataProtectionImpactAssessment = () => {
export const useUpdateDataProtectionImpactAssessment = () => { export const useUpdateDataProtectionImpactAssessment = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateDataProtectionImpactAssessmentMutation); const [mutate] = useMutation(updateDataProtectionImpactAssessmentMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -527,7 +524,7 @@ export const useUpdateDataProtectionImpactAssessment = () => {
residualRisk?: ProcessingActivityDPIAResidualRisk; residualRisk?: ProcessingActivityDPIAResidualRisk;
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update DPIA: ID is required")); return alert(t("processingActivityGraph.errors.updateDpiaIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -542,12 +539,12 @@ export const useDeleteDataProtectionImpactAssessment = (
dpia: { id: string }, dpia: { id: string },
options?: { onSuccess?: () => void }, options?: { onSuccess?: () => void },
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts( const [mutate] = useMutationWithToasts(
deleteDataProtectionImpactAssessmentMutation, deleteDataProtectionImpactAssessmentMutation,
{ {
successMessage: __("DPIA deleted successfully"), successMessage: t("processingActivityGraph.messages.dpiaDeleted"),
errorMessage: __("Failed to delete DPIA"), errorMessage: t("processingActivityGraph.errors.deleteDpia"),
}, },
); );
const confirm = useConfirm(); const confirm = useConfirm();
@@ -564,9 +561,7 @@ export const useDeleteDataProtectionImpactAssessment = (
onSuccess: options?.onSuccess, onSuccess: options?.onSuccess,
}), }),
{ {
message: __( message: t("processingActivityGraph.dpiaDeleteConfirmation"),
"This will permanently delete this Data Protection Impact Assessment. This action cannot be undone.",
),
}, },
); );
}; };
@@ -648,7 +643,7 @@ export const deleteTransferImpactAssessmentMutation = graphql`
export const useCreateTransferImpactAssessment = () => { export const useCreateTransferImpactAssessment = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createTransferImpactAssessmentMutation); const [mutate] = useMutation(createTransferImpactAssessmentMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
processingActivityId: string; processingActivityId: string;
@@ -660,7 +655,7 @@ export const useCreateTransferImpactAssessment = () => {
}) => { }) => {
if (!input.processingActivityId) { if (!input.processingActivityId) {
return alert( return alert(
__("Failed to create TIA: Processing Activity ID is required"), t("processingActivityGraph.errors.createTiaProcessingActivityIdRequired"),
); );
} }
@@ -675,7 +670,7 @@ export const useCreateTransferImpactAssessment = () => {
export const useUpdateTransferImpactAssessment = () => { export const useUpdateTransferImpactAssessment = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateTransferImpactAssessmentMutation); const [mutate] = useMutation(updateTransferImpactAssessmentMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -686,7 +681,7 @@ export const useUpdateTransferImpactAssessment = () => {
supplementaryMeasures?: string; supplementaryMeasures?: string;
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update TIA: ID is required")); return alert(t("processingActivityGraph.errors.updateTiaIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({
@@ -701,12 +696,12 @@ export const useDeleteTransferImpactAssessment = (
tia: { id: string }, tia: { id: string },
options?: { onSuccess?: () => void }, options?: { onSuccess?: () => void },
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts( const [mutate] = useMutationWithToasts(
deleteTransferImpactAssessmentMutation, deleteTransferImpactAssessmentMutation,
{ {
successMessage: __("TIA deleted successfully"), successMessage: t("processingActivityGraph.messages.tiaDeleted"),
errorMessage: __("Failed to delete TIA"), errorMessage: t("processingActivityGraph.errors.deleteTia"),
}, },
); );
const confirm = useConfirm(); const confirm = useConfirm();
@@ -723,9 +718,7 @@ export const useDeleteTransferImpactAssessment = (
onSuccess: options?.onSuccess, onSuccess: options?.onSuccess,
}), }),
{ {
message: __( message: t("processingActivityGraph.tiaDeleteConfirmation"),
"This will permanently delete this Transfer Impact Assessment. This action cannot be undone.",
),
}, },
); );
}; };

View File

@@ -18,9 +18,9 @@
// 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 { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useConfirm } from "@probo/ui"; import { useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -126,10 +126,10 @@ export const useDeleteRightsRequest = (
request: { id: string }, request: { id: string },
connectionId: string, connectionId: string,
) => { ) => {
const { __ } = useTranslate(); const { t } = useTranslation();
const [mutate] = useMutationWithToasts(deleteRightsRequestMutation, { const [mutate] = useMutationWithToasts(deleteRightsRequestMutation, {
successMessage: __("Rights request deleted successfully"), successMessage: t("rightsRequestGraph.messages.deleted"),
errorMessage: __("Failed to delete rights request"), errorMessage: t("rightsRequestGraph.errors.delete"),
}); });
const confirm = useConfirm(); const confirm = useConfirm();
@@ -145,11 +145,7 @@ export const useDeleteRightsRequest = (
}, },
}), }),
{ {
message: sprintf( message: t("rightsRequestGraph.deleteConfirmation"),
__(
"This will permanently delete the rights request. This action cannot be undone.",
),
),
}, },
); );
}; };
@@ -158,7 +154,7 @@ export const useDeleteRightsRequest = (
export const useCreateRightsRequest = (connectionId: string) => { export const useCreateRightsRequest = (connectionId: string) => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(createRightsRequestMutation); const [mutate] = useMutation(createRightsRequestMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
organizationId: string; organizationId: string;
@@ -172,17 +168,17 @@ export const useCreateRightsRequest = (connectionId: string) => {
}) => { }) => {
if (!input.organizationId) { if (!input.organizationId) {
return alert( return alert(
__("Failed to create rights request: organization is required"), t("rightsRequestGraph.errors.createOrganizationRequired"),
); );
} }
if (!input.requestType) { if (!input.requestType) {
return alert( return alert(
__("Failed to create rights request: request type is required"), t("rightsRequestGraph.errors.createRequestTypeRequired"),
); );
} }
if (!input.requestState) { if (!input.requestState) {
return alert( return alert(
__("Failed to create rights request: request state is required"), t("rightsRequestGraph.errors.createRequestStateRequired"),
); );
} }
@@ -207,7 +203,7 @@ export const useCreateRightsRequest = (connectionId: string) => {
export const useUpdateRightsRequest = () => { export const useUpdateRightsRequest = () => {
// eslint-disable-next-line relay/generated-typescript-types // eslint-disable-next-line relay/generated-typescript-types
const [mutate] = useMutation(updateRightsRequestMutation); const [mutate] = useMutation(updateRightsRequestMutation);
const { __ } = useTranslate(); const { t } = useTranslation();
return (input: { return (input: {
id: string; id: string;
@@ -220,7 +216,7 @@ export const useUpdateRightsRequest = () => {
actionTaken?: string; actionTaken?: string;
}) => { }) => {
if (!input.id) { if (!input.id) {
return alert(__("Failed to update rights request: ID is required")); return alert(t("rightsRequestGraph.errors.updateIdRequired"));
} }
return promisifyMutation(mutate)({ return promisifyMutation(mutate)({

View File

@@ -19,9 +19,9 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useToast } from "@probo/ui"; import { useToast } from "@probo/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { import {
useMutation, useMutation,
type UseMutationConfig, type UseMutationConfig,
@@ -55,7 +55,7 @@ export function useMutationWithIncrement<T extends MutationParameters>(
const [mutate, isLoading] = useMutation<T>(query); const [mutate, isLoading] = useMutation<T>(query);
const relayEnv = useRelayEnvironment(); const relayEnv = useRelayEnvironment();
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { t } = useTranslation();
const options = { ...defaultOptions, ...baseOptions }; const options = { ...defaultOptions, ...baseOptions };
const mutateAndIncrement = useCallback( const mutateAndIncrement = useCallback(
(queryOptions: UseMutationConfig<T>) => { (queryOptions: UseMutationConfig<T>) => {
@@ -63,9 +63,9 @@ export function useMutationWithIncrement<T extends MutationParameters>(
...queryOptions, ...queryOptions,
onCompleted: (response, error) => { onCompleted: (response, error) => {
if (error) { if (error) {
const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError(errorTitle, error), description: formatError(errorTitle, error),
variant: "error", variant: "error",
}); });
@@ -81,9 +81,9 @@ export function useMutationWithIncrement<T extends MutationParameters>(
queryOptions.onCompleted?.(response, error); queryOptions.onCompleted?.(response, error);
}, },
onError: (error) => { onError: (error) => {
const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError(errorTitle, error), description: formatError(errorTitle, error),
variant: "error", variant: "error",
}); });
@@ -91,7 +91,7 @@ export function useMutationWithIncrement<T extends MutationParameters>(
}, },
}); });
}, },
[mutate, options.id, options.node, options.field, options.value, options.errorMessage, relayEnv, toast, __], [mutate, options.id, options.node, options.field, options.value, options.errorMessage, relayEnv, toast, t],
); );
return [mutateAndIncrement, isLoading] as const; return [mutateAndIncrement, isLoading] as const;

View File

@@ -19,9 +19,9 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useToast } from "@probo/ui"; import { useToast } from "@probo/ui";
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, type UseMutationConfig } from "react-relay"; import { useMutation, type UseMutationConfig } from "react-relay";
import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime"; import type { GraphQLTaggedNode, MutationParameters } from "relay-runtime";
@@ -37,7 +37,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
) { ) {
const [mutate, isLoading] = useMutation<T>(query); const [mutate, isLoading] = useMutation<T>(query);
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { t } = useTranslation();
const mutateWithToast = useCallback( const mutateWithToast = useCallback(
( (
queryOptions: UseMutationConfig<T> & { queryOptions: UseMutationConfig<T> & {
@@ -53,9 +53,9 @@ export function useMutationWithToasts<T extends MutationParameters>(
onCompleted: (response, error) => { onCompleted: (response, error) => {
options.onCompleted?.(response, error); options.onCompleted?.(response, error);
if (error) { if (error) {
const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError(errorTitle, error), description: formatError(errorTitle, error),
variant: "error", variant: "error",
}); });
@@ -67,19 +67,19 @@ export function useMutationWithToasts<T extends MutationParameters>(
: options.successMessage; : options.successMessage;
toast({ toast({
title: __("Success"), title: t("common.success"),
description: description:
successMessage successMessage
?? __("Operation completed successfully"), ?? t("mutation.messages.completed"),
variant: "success", variant: "success",
}); });
options.onSuccess?.(); options.onSuccess?.();
resolve(); resolve();
}, },
onError: (error) => { onError: (error) => {
const errorTitle = options.errorMessage ?? __("Failed to commit this operation"); const errorTitle = options.errorMessage ?? t("mutation.errors.commit");
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError(errorTitle, error), description: formatError(errorTitle, error),
variant: "error", variant: "error",
}); });
@@ -88,7 +88,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
}), }),
); );
}, },
[mutate, toast, __, baseOptions], [mutate, toast, t, baseOptions],
); );
return [mutateWithToast, isLoading] as const; return [mutateWithToast, isLoading] as const;

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import type { BackendModule, ReadCallback, ResourceKey } from "i18next";
// The default namespace for app-wide chrome strings, sourced from src/_locales/.
export const DEFAULT_NAMESPACE = "app";
type CatalogModule = { default: ResourceKey };
// Vite turns each translation JSON into its own lazily-imported chunk. The keys
// are project-root-absolute paths, e.g.
// "/src/pages/organizations/measures/_locales/en-US.json".
const catalogs = import.meta.glob<CatalogModule>("/src/**/_locales/*.json");
const CATALOG_PATH = /^\/src\/(.*)_locales\/([^/]+)\.json$/;
// Map "<namespace>\u0000<language>" -> lazy importer for that catalog chunk.
function buildLookup(): Map<string, () => Promise<CatalogModule>> {
const lookup = new Map<string, () => Promise<CatalogModule>>();
for (const [path, importer] of Object.entries(catalogs)) {
const match = CATALOG_PATH.exec(path);
if (!match) {
continue;
}
const [, prefix, language] = match;
// prefix is the path between "src/" and "_locales/", e.g. "pages/foo/".
// Drop the leading "pages/" and trailing slash; an empty prefix (src/_locales)
// is the app-wide default namespace.
const namespace
= prefix.replace(/^pages\//, "").replace(/\/$/, "") || DEFAULT_NAMESPACE;
lookup.set(catalogKey(namespace, language), importer);
}
return lookup;
}
function catalogKey(namespace: string, language: string): string {
return `${namespace}\u0000${language}`;
}
const lookup = buildLookup();
// Custom i18next backend that resolves a (language, namespace) pair to its lazy
// JSON chunk. A missing catalog resolves to an empty resource so i18next falls
// through to fallbackLng rather than treating it as a hard load error.
export const globBackend: BackendModule = {
type: "backend",
init() {},
read(language: string, namespace: string, callback: ReadCallback) {
const importer = lookup.get(catalogKey(namespace, language));
if (!importer) {
callback(null, {});
return;
}
importer().then(
module => callback(null, module.default),
(error: unknown) =>
callback(error instanceof Error ? error : new Error(String(error)), null),
);
},
};

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { createInstance } from "i18next";
import { initReactI18next } from "react-i18next";
import { DEFAULT_NAMESPACE, globBackend } from "./backend";
import { resolveLanguage, SUPPORTED_LANGUAGES } from "./resolveLanguage";
// Build a dedicated instance rather than mutating i18next's global singleton.
// Initializing it through initReactI18next still registers it as the instance
// react-i18next's hooks read from, so no I18nextProvider is required.
const i18n = createInstance();
void i18n
.use(globBackend)
.use(initReactI18next)
.init({
lng: resolveLanguage(),
fallbackLng: "en-US",
supportedLngs: SUPPORTED_LANGUAGES,
load: "currentOnly",
defaultNS: DEFAULT_NAMESPACE,
fallbackNS: DEFAULT_NAMESPACE,
ns: [DEFAULT_NAMESPACE],
interpolation: { escapeValue: false },
react: { useSuspense: true },
});
export { i18n };

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export const SUPPORTED_LANGUAGES = [
"en-US",
"fr-FR",
"de-DE",
"es-ES",
"id-ID",
"it-IT",
"ja-JP",
"ko-KR",
"pl-PL",
"pt-PT",
"tr-TR",
"uk-UA",
"zh-CN",
] as const;
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
// Maps a two-letter language prefix (lowercased) to its canonical supported
// tag, e.g. any "de*" browser tag resolves to "de-DE".
const PREFIX_TO_LANGUAGE: Record<string, SupportedLanguage> = {
en: "en-US",
fr: "fr-FR",
de: "de-DE",
es: "es-ES",
id: "id-ID",
it: "it-IT",
ja: "ja-JP",
ko: "ko-KR",
pl: "pl-PL",
pt: "pt-PT",
tr: "tr-TR",
uk: "uk-UA",
zh: "zh-CN",
};
// Collapse the browser's preferred languages to one of our supported locales
// by matching the two-letter language prefix (e.g. any "fr*" tag maps to
// fr-FR). en-US is the ultimate fallback when nothing matches. Resolving to a
// canonical supported tag here means i18next is never asked to load an
// unsupported locale; fallbackLng only has to cover individual missing keys.
export function resolveLanguage(): SupportedLanguage {
const candidates = navigator.languages?.length
? navigator.languages
: [navigator.language];
for (const tag of candidates) {
const prefix = tag.toLowerCase().split("-")[0];
const language = PREFIX_TO_LANGUAGE[prefix];
if (language) {
return language;
}
}
return "en-US";
}

View File

@@ -13,10 +13,10 @@
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { createUseMutation, type MutationNotifier } from "@probo/relay"; import { createUseMutation, type MutationNotifier } from "@probo/relay";
import { useToast } from "@probo/ui"; import { useToast } from "@probo/ui";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
/** /**
* Binds the shared awaitable useMutation (`@probo/relay`) to this app's * Binds the shared awaitable useMutation (`@probo/relay`) to this app's
@@ -28,7 +28,7 @@ import { useMemo } from "react";
*/ */
function useMutationNotifier(): MutationNotifier { function useMutationNotifier(): MutationNotifier {
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { t } = useTranslation();
return useMemo<MutationNotifier>( return useMemo<MutationNotifier>(
() => ({ () => ({
@@ -36,7 +36,7 @@ function useMutationNotifier(): MutationNotifier {
toast({ title, description: "", variant: "success" }); toast({ title, description: "", variant: "success" });
}, },
notifyError: (error, title) => { notifyError: (error, title) => {
const finalTitle = title ?? __("Error"); const finalTitle = title ?? t("common.error");
toast({ toast({
title: finalTitle, title: finalTitle,
description: formatError(finalTitle, error), description: formatError(finalTitle, error),
@@ -44,7 +44,7 @@ function useMutationNotifier(): MutationNotifier {
}); });
}, },
}), }),
[toast, __], [toast, t],
); );
} }

View File

@@ -24,7 +24,7 @@ import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import { App } from "./App"; import { App } from "./App";
import { TranslatorProvider } from "./providers/TranslatorProvider"; import "./lib/i18n/i18n";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -36,8 +36,6 @@ const queryClient = new QueryClient({
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<TranslatorProvider> <App />
<App />
</TranslatorProvider>
</QueryClientProvider>, </QueryClientProvider>,
); );

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 { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { APIKeysPageQuery } from "#/__generated__/iam/APIKeysPageQuery.graphql"; import type { APIKeysPageQuery } from "#/__generated__/iam/APIKeysPageQuery.graphql";
@@ -37,13 +37,13 @@ export function APIKeysPage(props: {
queryRef: PreloadedQuery<APIKeysPageQuery>; queryRef: PreloadedQuery<APIKeysPageQuery>;
}) { }) {
const { queryRef } = props; const { queryRef } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const data = usePreloadedQuery<APIKeysPageQuery>(apiKeysPageQuery, queryRef); const data = usePreloadedQuery<APIKeysPageQuery>(apiKeysPageQuery, queryRef);
return ( return (
<div className="space-y-6 w-full py-6"> <div className="space-y-6 w-full py-6">
<h1 className="text-3xl font-bold text-center">{__("API Keys")}</h1> <h1 className="text-3xl font-bold text-center">{t("apiKeys.title")}</h1>
{data.viewer && <PersonalAPIKeyList fKey={data.viewer} />} {data.viewer && <PersonalAPIKeyList fKey={data.viewer} />}
</div> </div>
); );

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -37,6 +36,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { import {
ConnectionHandler, ConnectionHandler,
graphql, graphql,
@@ -120,7 +120,7 @@ export function PersonalAPIKeyList(props: {
fKey: PersonalAPIKeyListFragment$key; fKey: PersonalAPIKeyListFragment$key;
}) { }) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const createDialogRef = useDialogRef(); const createDialogRef = useDialogRef();
const tokenDialogRef = useDialogRef(); const tokenDialogRef = useDialogRef();
@@ -162,8 +162,8 @@ export function PersonalAPIKeyList(props: {
}, },
onCompleted: (response) => { onCompleted: (response) => {
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("API key created successfully."), description: t("personalApiKeyList.messages.created"),
variant: "success", variant: "success",
}); });
const newToken = response.createPersonalAPIKey?.token; const newToken = response.createPersonalAPIKey?.token;
@@ -176,8 +176,8 @@ export function PersonalAPIKeyList(props: {
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError(__("Failed to create API key."), error), description: formatError(t("personalApiKeyList.errors.create"), error),
variant: "error", variant: "error",
}); });
}, },
@@ -188,9 +188,9 @@ export function PersonalAPIKeyList(props: {
<> <>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<h2 className="text-base font-medium">{__("API Keys")}</h2> <h2 className="text-base font-medium">{t("apiKeys.title")}</h2>
<Button onClick={() => createDialogRef.current?.open()}> <Button onClick={() => createDialogRef.current?.open()}>
{__("Create API Key")} {t("personalApiKeyList.actions.create")}
</Button> </Button>
</div> </div>
@@ -199,10 +199,10 @@ export function PersonalAPIKeyList(props: {
<Card padded> <Card padded>
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
{__("No API keys")} {t("personalApiKeyList.empty.title")}
</h3> </h3>
<p className="text-gray-600 mb-6"> <p className="text-gray-600 mb-6">
{__("Create an API key to authenticate programmatic access.")} {t("personalApiKeyList.empty.description")}
</p> </p>
</div> </div>
</Card> </Card>
@@ -219,21 +219,21 @@ export function PersonalAPIKeyList(props: {
<Dialog <Dialog
ref={createDialogRef} ref={createDialogRef}
title={<Breadcrumb items={[__("API Keys"), __("Create")]} />} title={<Breadcrumb items={[t("apiKeys.title"), t("common.create")]} />}
onClose={() => reset()} onClose={() => reset()}
> >
<form onSubmit={e => void handleSubmit(handleCreate)(e)}> <form onSubmit={e => void handleSubmit(handleCreate)(e)}>
<DialogContent padded className="space-y-5"> <DialogContent padded className="space-y-5">
<Field error={formState.errors.name?.message}> <Field error={formState.errors.name?.message}>
<Label>{__("Name")}</Label> <Label>{t("personalApiKeyList.fields.name.label")}</Label>
<Input <Input
{...register("name")} {...register("name")}
placeholder={__("e.g., Production API Key")} placeholder={t("personalApiKeyList.fields.name.placeholder")}
/> />
</Field> </Field>
<Field error={formState.errors.expiresIn?.message}> <Field error={formState.errors.expiresIn?.message}>
<Label>{__("Expires In")}</Label> <Label>{t("personalApiKeyList.fields.expiresIn.label")}</Label>
<Controller <Controller
control={control} control={control}
name="expiresIn" name="expiresIn"
@@ -243,10 +243,18 @@ export function PersonalAPIKeyList(props: {
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
> >
<Option value="1month">{__("1 Month")}</Option> <Option value="1month">
<Option value="3months">{__("3 Months")}</Option> {t("personalApiKeyList.expirations.month", { count: 1 })}
<Option value="6months">{__("6 Months")}</Option> </Option>
<Option value="1year">{__("1 Year")}</Option> <Option value="3months">
{t("personalApiKeyList.expirations.month", { count: 3 })}
</Option>
<Option value="6months">
{t("personalApiKeyList.expirations.month", { count: 6 })}
</Option>
<Option value="1year">
{t("personalApiKeyList.expirations.year", { count: 1 })}
</Option>
</Select> </Select>
)} )}
/> />
@@ -254,7 +262,7 @@ export function PersonalAPIKeyList(props: {
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isCreating}> <Button type="submit" disabled={isCreating}>
{isCreating ? __("Creating...") : __("Create")} {isCreating ? t("personalApiKeyList.actions.creating") : t("common.create")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>

View File

@@ -18,11 +18,12 @@
// 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 { formatDate, formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { dateFormat } from "@probo/i18n";
import { Button, Spinner, Td, Tr, useConfirm, useToast } from "@probo/ui"; import { Button, Spinner, Td, Tr, useConfirm, useToast } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { Suspense } from "react"; import { Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useFragment, useMutation } from "react-relay"; import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -61,7 +62,7 @@ export function PersonalAPIKeyRow(props: {
connectionId: string; connectionId: string;
}) { }) {
const { fKey, connectionId } = props; const { fKey, connectionId } = props;
const { __ } = useTranslate(); const { t, i18n } = useTranslation();
const confirm = useConfirm(); const confirm = useConfirm();
const { toast } = useToast(); const { toast } = useToast();
const now = new Date(); const now = new Date();
@@ -84,28 +85,28 @@ export function PersonalAPIKeyRow(props: {
onCompleted: (_response, errors) => { onCompleted: (_response, errors) => {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to revoke API key."), t("personalApiKeyRow.errors.revoke"),
errors, errors,
), ),
variant: "error", variant: "error",
}); });
reject(new Error(errors[0]?.message ?? __("Failed to revoke API key."))); reject(new Error(errors[0]?.message ?? t("personalApiKeyRow.errors.revoke")));
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("API key revoked successfully."), description: t("personalApiKeyRow.messages.revoked"),
variant: "success", variant: "success",
}); });
resolve(); resolve();
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to revoke API key."), t("personalApiKeyRow.errors.revoke"),
error, error,
), ),
variant: "error", variant: "error",
@@ -116,11 +117,9 @@ export function PersonalAPIKeyRow(props: {
}); });
}, },
{ {
title: __("Revoke API Key"), title: t("personalApiKeyRow.revoke.title"),
message: __( message: t("personalApiKeyRow.revoke.confirmation", { name: key.name }),
`Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.`, label: t("personalApiKeyRow.actions.revoke"),
),
label: __("Revoke"),
variant: "danger", variant: "danger",
}, },
); );
@@ -131,22 +130,22 @@ export function PersonalAPIKeyRow(props: {
<Td> <Td>
<div className="font-medium text-txt-primary">{key.name}</div> <div className="font-medium text-txt-primary">{key.name}</div>
<div className="text-xs text-txt-tertiary"> <div className="text-xs text-txt-tertiary">
{expired ? __("Expired") : __("Active")} {expired ? t("personalApiKeyRow.status.expired") : t("personalApiKeyRow.status.active")}
</div> </div>
</Td> </Td>
<Td> <Td>
<span className="text-sm text-txt-secondary"> <span className="text-sm text-txt-secondary">
{key.lastUsedAt ? formatDate(key.lastUsedAt) : __("Never")} {key.lastUsedAt ? dateFormat(i18n.language, key.lastUsedAt) : t("personalApiKeyRow.never")}
</span> </span>
</Td> </Td>
<Td> <Td>
<span className="text-sm text-txt-secondary"> <span className="text-sm text-txt-secondary">
{formatDate(key.createdAt)} {dateFormat(i18n.language, key.createdAt)}
</span> </span>
</Td> </Td>
<Td> <Td>
<span className="text-sm text-txt-secondary"> <span className="text-sm text-txt-secondary">
{formatDate(key.expiresAt)} {dateFormat(i18n.language, key.expiresAt)}
</span> </span>
</Td> </Td>
<Td width={140} className="text-end"> <Td width={140} className="text-end">
@@ -155,7 +154,7 @@ export function PersonalAPIKeyRow(props: {
<PersonalAPIKeyTokenAction fKey={fKey} disabled={isRevoking} /> <PersonalAPIKeyTokenAction fKey={fKey} disabled={isRevoking} />
</Suspense> </Suspense>
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}> <Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
{__("Revoke")} {t("personalApiKeyRow.actions.revoke")}
</Button> </Button>
</div> </div>
</Td> </Td>

View File

@@ -19,8 +19,8 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Button, useDialogRef, useToast } from "@probo/ui"; import { Button, useDialogRef, useToast } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useRefetchableFragment } from "react-relay"; import { useRefetchableFragment } from "react-relay";
import type { PersonalAPIKeyRowFragment$key } from "#/__generated__/iam/PersonalAPIKeyRowFragment.graphql"; import type { PersonalAPIKeyRowFragment$key } from "#/__generated__/iam/PersonalAPIKeyRowFragment.graphql";
@@ -34,7 +34,7 @@ export function PersonalAPIKeyTokenAction(props: {
disabled?: boolean; disabled?: boolean;
}) { }) {
const { fKey, disabled } = props; const { fKey, disabled } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
@@ -53,9 +53,9 @@ export function PersonalAPIKeyTokenAction(props: {
onComplete: (error) => { onComplete: (error) => {
if (error) { if (error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to load API key token."), t("personalApiKeyTokenAction.errors.load"),
error, error,
), ),
variant: "error", variant: "error",
@@ -70,7 +70,7 @@ export function PersonalAPIKeyTokenAction(props: {
return ( return (
<> <>
<Button variant="secondary" onClick={handleShow} disabled={!!disabled}> <Button variant="secondary" onClick={handleShow} disabled={!!disabled}>
{__("Show")} {t("personalApiKeyTokenAction.actions.show")}
</Button> </Button>
<PersonalAPIKeyTokenDialog <PersonalAPIKeyTokenDialog

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { useCopy } from "@probo/hooks"; import { useCopy } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -27,6 +26,7 @@ import {
DialogContent, DialogContent,
DialogFooter, DialogFooter,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
export function PersonalAPIKeyTokenDialog(props: { export function PersonalAPIKeyTokenDialog(props: {
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>; dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
@@ -34,13 +34,13 @@ export function PersonalAPIKeyTokenDialog(props: {
onDone: () => void; onDone: () => void;
}) { }) {
const { dialogRef, token, onDone } = props; const { dialogRef, token, onDone } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const [isCopied, copy] = useCopy(); const [isCopied, copy] = useCopy();
return ( return (
<Dialog <Dialog
ref={dialogRef} ref={dialogRef}
title={<Breadcrumb items={[__("API Keys"), __("Token")]} />} title={<Breadcrumb items={[t("apiKeys.title"), t("personalApiKeyTokenDialog.title")]} />}
> >
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
<div className="bg-gray-100 p-4 rounded-lg flex items-center gap-2"> <div className="bg-gray-100 p-4 rounded-lg flex items-center gap-2">
@@ -50,12 +50,12 @@ export function PersonalAPIKeyTokenDialog(props: {
onClick={() => copy(token)} onClick={() => copy(token)}
disabled={!token} disabled={!token}
> >
{isCopied ? __("Copied") : __("Copy")} {isCopied ? t("personalApiKeyTokenDialog.actions.copied") : t("personalApiKeyTokenDialog.actions.copy")}
</Button> </Button>
</div> </div>
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button onClick={onDone}>{__("Done")}</Button> <Button onClick={onDone}>{t("personalApiKeyTokenDialog.actions.done")}</Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>
); );

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 { useTranslate } from "@probo/i18n";
import { Table, Tbody, Th, Thead, Tr } from "@probo/ui"; import { Table, Tbody, Th, Thead, Tr } from "@probo/ui";
import { useTranslation } from "react-i18next";
import type { PersonalAPIKeyListFragment$data } from "#/__generated__/iam/PersonalAPIKeyListFragment.graphql"; import type { PersonalAPIKeyListFragment$data } from "#/__generated__/iam/PersonalAPIKeyListFragment.graphql";
@@ -30,16 +30,16 @@ export function PersonalAPIKeysTable(props: {
connectionId: string; connectionId: string;
}) { }) {
const { edges, connectionId } = props; const { edges, connectionId } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Table> <Table>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("personalApiKeysTable.columns.name")}</Th>
<Th>{__("Last used")}</Th> <Th>{t("personalApiKeysTable.columns.lastUsed")}</Th>
<Th>{__("Created")}</Th> <Th>{t("personalApiKeysTable.columns.created")}</Th>
<Th>{__("Expires")}</Th> <Th>{t("personalApiKeysTable.columns.expires")}</Th>
<Th></Th> <Th></Th>
</Tr> </Tr>
</Thead> </Thead>

View File

@@ -20,9 +20,9 @@
import { formatError, type GraphQLError } from "@probo/helpers"; import { formatError, type GraphQLError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { useToast } from "@probo/ui"; import { useToast } from "@probo/ui";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link, useNavigate, useSearchParams } from "react-router"; import { Link, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -42,14 +42,14 @@ const activateAccountMutation = graphql`
`; `;
export default function ActivateAccountPage() { export default function ActivateAccountPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const submittedRef = useRef<boolean>(false); const submittedRef = useRef<boolean>(false);
const safeContinueUrl = useSafeContinueUrl(); const safeContinueUrl = useSafeContinueUrl();
usePageTitle(__("Activate Account")); usePageTitle(t("activateAccountPage.pageTitle"));
const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation); const [activateAccount] = useMutation<ActivateAccountPageMutation>(activateAccountMutation);
@@ -72,8 +72,8 @@ export default function ActivateAccountPage() {
} }
} }
toast({ toast({
title: __("Activation failed"), title: t("activateAccountPage.errors.activationFailed"),
description: formatError(__("Activation failed"), errors), description: formatError(t("activateAccountPage.errors.activationFailed"), errors),
variant: "error", variant: "error",
}); });
@@ -81,10 +81,8 @@ export default function ActivateAccountPage() {
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __( description: t("activateAccountPage.messages.activated"),
"Account activated successfully.",
),
variant: "success", variant: "success",
}); });
@@ -125,13 +123,13 @@ export default function ActivateAccountPage() {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Activation failed"), title: t("activateAccountPage.errors.activationFailed"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
}, },
}); });
}, [__, toast, activateAccount, navigate, safeContinueUrl]); }, [t, toast, activateAccount, navigate, safeContinueUrl]);
useEffect(() => { useEffect(() => {
const token = searchParams.get("token"); const token = searchParams.get("token");
@@ -144,9 +142,9 @@ export default function ActivateAccountPage() {
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Account Activation")}</h1> <h1 className="text-3xl font-bold">{t("activateAccountPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Activating your account…")} {t("activateAccountPage.activating")}
</p> </p>
</div> </div>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
@@ -154,7 +152,7 @@ export default function ActivateAccountPage() {
to="/auth/login" to="/auth/login"
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"
> >
{__("Go back")} {t("activateAccountPage.actions.goBack")}
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -20,7 +20,6 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
IconArrowsClockwise, IconArrowsClockwise,
@@ -34,6 +33,7 @@ import {
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -65,10 +65,10 @@ const approveConsentMutation = graphql`
`; `;
const scopeLabels: Record<string, string> = { const scopeLabels: Record<string, string> = {
openid: "Verify your identity", openid: "consentPage.scopes.openid",
email: "View your email address", email: "consentPage.scopes.email",
profile: "View your profile information", profile: "consentPage.scopes.profile",
offline_access: "Stay signed in and access your data while you're away", offline_access: "consentPage.scopes.offlineAccess",
}; };
const scopeIcons: Record<string, React.ReactNode> = { const scopeIcons: Record<string, React.ReactNode> = {
@@ -82,8 +82,9 @@ function scopeIcon(name: string): React.ReactNode {
return scopeIcons[name] ?? <IconKey size={18} className="shrink-0 text-txt-tertiary" />; return scopeIcons[name] ?? <IconKey size={18} className="shrink-0 text-txt-tertiary" />;
} }
function scopeLabel(name: string): string { function scopeLabel(name: string, translate: (key: string) => string): string {
return scopeLabels[name] ?? formatApiScopeLabel(name); const key = scopeLabels[name];
return key ? translate(key) : formatApiScopeLabel(name);
} }
function isApiScope(scope: string): boolean { function isApiScope(scope: string): boolean {
@@ -110,8 +111,7 @@ function ConsentScopeRow(props: {
translate: (label: string) => string; translate: (label: string) => string;
nested?: boolean; nested?: boolean;
}) { }) {
const label = scopeLabel(props.scope); const translated = scopeLabel(props.scope, props.translate);
const translated = label !== props.scope ? props.translate(label) : label;
return ( return (
<li <li
@@ -163,7 +163,7 @@ function ConsentApiScopesAccordion(props: {
export default function ConsentPage(props: { export default function ConsentPage(props: {
queryRef: PreloadedQuery<ConsentPageQuery>; queryRef: PreloadedQuery<ConsentPageQuery>;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null); const [deviceResult, setDeviceResult] = useState<"authorized" | "denied" | null>(null);
const [pendingAction, setPendingAction] = useState<"allow" | "deny" | null>(null); const [pendingAction, setPendingAction] = useState<"allow" | "deny" | null>(null);
@@ -173,7 +173,7 @@ export default function ConsentPage(props: {
} | null>(null); } | null>(null);
const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef); const data = usePreloadedQuery<ConsentPageQuery>(consentPageQuery, props.queryRef);
usePageTitle(__("Authorize Application")); usePageTitle(t("consentPage.pageTitle"));
const { node: consent } = data; const { node: consent } = data;
@@ -185,8 +185,8 @@ export default function ConsentPage(props: {
); );
const apiScopesSummary = useMemo( const apiScopesSummary = useMemo(
() => `${__("API access")} (${apiScopes.length})`, () => t("consentPage.apiAccess", { count: apiScopes.length }),
[__, apiScopes.length], [t, apiScopes.length],
); );
useEffect(() => { useEffect(() => {
@@ -212,9 +212,9 @@ export default function ConsentPage(props: {
if (errors) { if (errors) {
setPendingAction(null); setPendingAction(null);
toast({ toast({
title: __("Authorization failed"), title: t("consentPage.errors.authorizationFailed"),
description: formatError( description: formatError(
__("Something went wrong. Please try again."), t("consentPage.errors.generic"),
errors, errors,
), ),
variant: "error", variant: "error",
@@ -225,8 +225,8 @@ export default function ConsentPage(props: {
if (!response.approveConsent) { if (!response.approveConsent) {
setPendingAction(null); setPendingAction(null);
toast({ toast({
title: __("Authorization failed"), title: t("consentPage.errors.authorizationFailed"),
description: __("Something went wrong. Please try again."), description: t("consentPage.errors.generic"),
variant: "error", variant: "error",
}); });
return; return;
@@ -247,23 +247,23 @@ export default function ConsentPage(props: {
onError: (err) => { onError: (err) => {
setPendingAction(null); setPendingAction(null);
toast({ toast({
title: __("Error"), title: t("common.error"),
description: description:
err.message || __("Something went wrong. Please try again."), err.message || t("consentPage.errors.generic"),
variant: "error", variant: "error",
}); });
}, },
}); });
}, },
[consent, approveConsent, __, toast, pendingAction], [consent, approveConsent, t, toast, pendingAction],
); );
if (!consent.application || !consent.scopes) { if (!consent.application || !consent.scopes) {
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center"> <div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
<h1 className="text-2xl font-bold">{__("Invalid Request")}</h1> <h1 className="text-2xl font-bold">{t("consentPage.invalidRequest.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("This consent request is invalid or has expired.")} {t("consentPage.invalidRequest.description")}
</p> </p>
</div> </div>
); );
@@ -272,9 +272,9 @@ export default function ConsentPage(props: {
if (deviceResult === "authorized") { if (deviceResult === "authorized") {
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center"> <div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
<h1 className="text-2xl font-bold">{__("Device Authorized")}</h1> <h1 className="text-2xl font-bold">{t("consentPage.deviceAuthorized.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Your device has been successfully authorized. You can close this window and return to your device.")} {t("consentPage.deviceAuthorized.description")}
</p> </p>
</div> </div>
); );
@@ -283,9 +283,9 @@ export default function ConsentPage(props: {
if (deviceResult === "denied") { if (deviceResult === "denied") {
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center"> <div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
<h1 className="text-2xl font-bold">{__("Access Denied")}</h1> <h1 className="text-2xl font-bold">{t("consentPage.accessDenied.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("You have denied the authorization request. You can close this window.")} {t("consentPage.accessDenied.description")}
</p> </p>
</div> </div>
); );
@@ -297,10 +297,10 @@ export default function ConsentPage(props: {
<Spinner size={24} centered className="text-txt-tertiary" /> <Spinner size={24} centered className="text-txt-tertiary" />
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{redirectState.approved ? __("Authorization Complete") : __("Access Denied")} {redirectState.approved ? t("consentPage.authorizationComplete") : t("consentPage.accessDenied.title")}
</h1> </h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("You will be redirected to")} {t("consentPage.redirectingTo")}
{" "} {" "}
<span className="font-medium text-txt-secondary"> <span className="font-medium text-txt-secondary">
{consent.application.name} {consent.application.name}
@@ -321,14 +321,12 @@ export default function ConsentPage(props: {
</div> </div>
</div> </div>
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{__("Authorize")} {t("consentPage.authorize")}
{" "} {" "}
<span className="font-bold">{consent.application.name}</span> <span className="font-bold">{consent.application.name}</span>
</h1> </h1>
<p className="text-txt-tertiary text-sm"> <p className="text-txt-tertiary text-sm">
{__( {t("consentPage.description")}
"This application is requesting access to your account with the following permissions:",
)}
</p> </p>
</div> </div>
@@ -339,7 +337,7 @@ export default function ConsentPage(props: {
<ConsentScopeRow <ConsentScopeRow
key={scope} key={scope}
scope={scope} scope={scope}
translate={__} translate={t}
/> />
))} ))}
</ul> </ul>
@@ -347,7 +345,7 @@ export default function ConsentPage(props: {
<ConsentApiScopesAccordion <ConsentApiScopesAccordion
scopes={apiScopes} scopes={apiScopes}
translate={__} translate={t}
summaryLabel={apiScopesSummary} summaryLabel={apiScopesSummary}
/> />
</div> </div>
@@ -360,7 +358,7 @@ export default function ConsentPage(props: {
icon={pendingAction === "deny" ? Spinner : undefined} icon={pendingAction === "deny" ? Spinner : undefined}
onClick={() => handleAction(false)} onClick={() => handleAction(false)}
> >
{__("Deny")} {t("consentPage.actions.deny")}
</Button> </Button>
<Button <Button
className="flex-1 h-10" className="flex-1 h-10"
@@ -368,12 +366,12 @@ export default function ConsentPage(props: {
icon={pendingAction === "allow" ? Spinner : undefined} icon={pendingAction === "allow" ? Spinner : undefined}
onClick={() => handleAction(true)} onClick={() => handleAction(true)}
> >
{__("Allow")} {t("consentPage.actions.allow")}
</Button> </Button>
</div> </div>
<p className="text-center text-xs text-txt-tertiary"> <p className="text-center text-xs text-txt-tertiary">
{__("You can revoke access at any time from your account settings.")} {t("consentPage.revokeNotice")}
</p> </p>
</div> </div>
); );

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 { useTranslate } from "@probo/i18n";
import { Component, type ReactNode, useEffect } from "react"; import { Component, type ReactNode, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useQueryLoader } from "react-relay"; import { useQueryLoader } from "react-relay";
import { useSearchParams } from "react-router"; import { useSearchParams } from "react-router";
@@ -61,13 +61,13 @@ class ConsentErrorBoundary extends Component<
} }
function ConsentErrorFallback() { function ConsentErrorFallback() {
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center"> <div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
<h1 className="text-2xl font-bold">{__("Invalid Request")}</h1> <h1 className="text-2xl font-bold">{t("consentPage.invalidRequest.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("This consent request is invalid or has expired.")} {t("consentPage.invalidRequest.description")}
</p> </p>
</div> </div>
); );

View File

@@ -20,8 +20,8 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link, useNavigate, useSearchParams } from "react-router"; import { Link, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -43,12 +43,12 @@ const schema = z.object({
}); });
export default function CreatePasswordPage() { export default function CreatePasswordPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
usePageTitle(__("Create Password")); usePageTitle(t("createPasswordPage.pageTitle"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, { const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: { defaultValues: {
@@ -69,16 +69,16 @@ export default function CreatePasswordPage() {
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Password creation failed"), title: t("createPasswordPage.errors.creationFailed"),
description: formatError(__("Password creation failed"), e), description: formatError(t("createPasswordPage.errors.creationFailed"), e),
variant: "error", variant: "error",
}); });
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Account created successfully"), description: t("createPasswordPage.messages.created"),
variant: "success", variant: "success",
}); });
@@ -92,7 +92,7 @@ export default function CreatePasswordPage() {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Password creation failed"), title: t("createPasswordPage.errors.creationFailed"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -103,15 +103,15 @@ export default function CreatePasswordPage() {
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Create a password")}</h1> <h1 className="text-3xl font-bold">{t("createPasswordPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Set a password for your account, with at least 8 characters")} {t("createPasswordPage.description")}
</p> </p>
</div> </div>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field <Field
label={__("Password")} label={t("createPasswordPage.fields.password")}
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
{...register("password")} {...register("password")}
@@ -120,19 +120,19 @@ export default function CreatePasswordPage() {
/> />
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading || isCreatingPassword}> <Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading || isCreatingPassword}>
{__("Save")} {t("createPasswordPage.actions.save")}
</Button> </Button>
</form> </form>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Already have an account?")} {t("createPasswordPage.alreadyHaveAccount")}
{" "} {" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Log in here")} {t("createPasswordPage.actions.logIn")}
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -20,7 +20,6 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, useToast } from "@probo/ui"; import { Button, useToast } from "@probo/ui";
import { import {
type ClipboardEvent, type ClipboardEvent,
@@ -29,6 +28,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -56,13 +56,13 @@ const authorizeDeviceMutation = graphql`
export default function DeviceActivationPage(props: { export default function DeviceActivationPage(props: {
queryRef: PreloadedQuery<DeviceActivationPageQuery>; queryRef: PreloadedQuery<DeviceActivationPageQuery>;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
usePreloadedQuery<DeviceActivationPageQuery>(deviceActivationPageQuery, props.queryRef); usePreloadedQuery<DeviceActivationPageQuery>(deviceActivationPageQuery, props.queryRef);
usePageTitle(__("Device Activation")); usePageTitle(t("deviceActivationPage.pageTitle"));
const preset = (searchParams.get("user_code") ?? "").replace(/-/g, ""); const preset = (searchParams.get("user_code") ?? "").replace(/-/g, "");
const [values, setValues] = useState<string[]>(() => { const [values, setValues] = useState<string[]>(() => {
@@ -132,9 +132,9 @@ export default function DeviceActivationPage(props: {
onCompleted: (response, errors) => { onCompleted: (response, errors) => {
if (errors) { if (errors) {
toast({ toast({
title: __("Authorization failed"), title: t("deviceActivationPage.errors.authorizationFailed"),
description: formatError( description: formatError(
__("The code is invalid or has expired."), t("deviceActivationPage.errors.invalidCode"),
errors, errors,
), ),
variant: "error", variant: "error",
@@ -153,14 +153,14 @@ export default function DeviceActivationPage(props: {
}, },
onError: (err) => { onError: (err) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: err.message || __("Something went wrong. Please try again."), description: err.message || t("deviceActivationPage.errors.generic"),
variant: "error", variant: "error",
}); });
}, },
}); });
}, },
[values, authorizeDevice, __, toast, navigate], [values, authorizeDevice, t, toast, navigate],
); );
const isFilled = values.every(v => v.length === 1); const isFilled = values.every(v => v.length === 1);
@@ -168,9 +168,9 @@ export default function DeviceActivationPage(props: {
if (status === "success") { if (status === "success") {
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center"> <div className="w-full max-w-md mx-auto pt-8 space-y-6 text-center">
<h1 className="text-2xl font-bold">{__("Device Authorized")}</h1> <h1 className="text-2xl font-bold">{t("deviceActivationPage.authorized.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Your device has been successfully authorized. You can close this window and return to your device.")} {t("deviceActivationPage.authorized.description")}
</p> </p>
</div> </div>
); );
@@ -179,9 +179,9 @@ export default function DeviceActivationPage(props: {
return ( return (
<div className="w-full max-w-md mx-auto pt-8 space-y-6"> <div className="w-full max-w-md mx-auto pt-8 space-y-6">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-2xl font-bold">{__("Device Activation")}</h1> <h1 className="text-2xl font-bold">{t("deviceActivationPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Enter the code displayed on your device")} {t("deviceActivationPage.description")}
</p> </p>
</div> </div>
@@ -206,7 +206,7 @@ export default function DeviceActivationPage(props: {
autoCapitalize="characters" autoCapitalize="characters"
spellCheck={false} spellCheck={false}
autoFocus={idx === 0} autoFocus={idx === 0}
aria-label={`${__("Code character")} ${idx + 1}`} aria-label={t("deviceActivationPage.codeCharacter", { count: idx + 1 })}
className="w-11 h-13 text-center text-lg font-mono font-medium uppercase rounded-lg border border-border-mid bg-level-1 text-txt-primary outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20" className="w-11 h-13 text-center text-lg font-mono font-medium uppercase rounded-lg border border-border-mid bg-level-1 text-txt-primary outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20"
/> />
</div> </div>
@@ -218,12 +218,12 @@ export default function DeviceActivationPage(props: {
className="w-full h-10" className="w-full h-10"
disabled={!isFilled || isInFlight} disabled={!isFilled || isInFlight}
> >
{isInFlight ? __("Authorizing...") : __("Continue")} {isInFlight ? t("deviceActivationPage.actions.authorizing") : t("deviceActivationPage.actions.continue")}
</Button> </Button>
</form> </form>
<p className="text-center text-sm text-txt-tertiary"> <p className="text-center text-sm text-txt-tertiary">
{__("Make sure this code matches the one on your device.")} {t("deviceActivationPage.notice")}
</p> </p>
</div> </div>
); );

View File

@@ -20,9 +20,9 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link } from "react-router"; import { Link } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -45,9 +45,9 @@ const schema = z.object({
export default function ForgotPasswordPage() { export default function ForgotPasswordPage() {
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { t } = useTranslation();
usePageTitle(__("Forgot Password")); usePageTitle(t("forgotPasswordPage.pageTitle"));
const [instructionsSent, setInstructionsSent] = useState<boolean>(); const [instructionsSent, setInstructionsSent] = useState<boolean>();
const { register, handleSubmit, formState } = useFormWithSchema(schema, { const { register, handleSubmit, formState } = useFormWithSchema(schema, {
@@ -67,7 +67,7 @@ export default function ForgotPasswordPage() {
}, },
onError: (e: Error) => { onError: (e: Error) => {
toast({ toast({
title: __("Request failed"), title: t("forgotPasswordPage.errors.requestFailed"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -75,9 +75,9 @@ export default function ForgotPasswordPage() {
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Request failed"), title: t("forgotPasswordPage.errors.requestFailed"),
description: formatError( description: formatError(
__("Failed to send reset instructions"), t("forgotPasswordPage.errors.sendInstructions"),
e, e,
), ),
variant: "error", variant: "error",
@@ -86,8 +86,8 @@ export default function ForgotPasswordPage() {
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Password reset instructions sent to your email"), description: t("forgotPasswordPage.messages.instructionsSent"),
variant: "success", variant: "success",
}); });
setInstructionsSent(true); setInstructionsSent(true);
@@ -99,34 +99,34 @@ export default function ForgotPasswordPage() {
? ( ? (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Check your email")}</h1> <h1 className="text-3xl font-bold">{t("forgotPasswordPage.sent.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("We've sent password reset instructions to your email address")} {t("forgotPasswordPage.sent.description")}
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Didn't receive the email?")} {t("forgotPasswordPage.sent.didNotReceive")}
{" "} {" "}
<button <button
onClick={() => setInstructionsSent(false)} onClick={() => setInstructionsSent(false)}
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Try again")} {t("forgotPasswordPage.actions.tryAgain")}
</button> </button>
</p> </p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")} {t("forgotPasswordPage.rememberPassword")}
{" "} {" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Back to login")} {t("forgotPasswordPage.actions.backToLogin")}
</Link> </Link>
</p> </p>
</div> </div>
@@ -135,19 +135,17 @@ export default function ForgotPasswordPage() {
: ( : (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Forgot Password")}</h1> <h1 className="text-3xl font-bold">{t("forgotPasswordPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__( {t("forgotPasswordPage.description")}
"Enter your email address and we'll send you instructions to reset your password",
)}
</p> </p>
</div> </div>
<form onSubmit={e => void onSubmit(e)} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Field <Field
label={__("Email")} label={t("forgotPasswordPage.fields.email")}
type="email" type="email"
placeholder={__("name@example.com")} placeholder={t("forgotPasswordPage.fields.emailPlaceholder")}
{...register("email")} {...register("email")}
required required
error={formState.errors.email?.message} error={formState.errors.email?.message}
@@ -159,20 +157,20 @@ export default function ForgotPasswordPage() {
disabled={formState.isSubmitting} disabled={formState.isSubmitting}
> >
{formState.isSubmitting {formState.isSubmitting
? __("Sending instructions...") ? t("forgotPasswordPage.actions.sendingInstructions")
: __("Send reset instructions")} : t("forgotPasswordPage.actions.sendInstructions")}
</Button> </Button>
</form> </form>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")} {t("forgotPasswordPage.rememberPassword")}
{" "} {" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Back to login")} {t("forgotPasswordPage.actions.backToLogin")}
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -20,8 +20,8 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link, useNavigate, useSearchParams } from "react-router"; import { Link, useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -49,13 +49,13 @@ const schema = z
}); });
export default function ResetPasswordPage() { export default function ResetPasswordPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const token = searchParams.get("token"); const token = searchParams.get("token");
usePageTitle(__("Reset password")); usePageTitle(t("resetPasswordPage.pageTitle"));
const { register, handleSubmit, formState } = useFormWithSchema(schema, { const { register, handleSubmit, formState } = useFormWithSchema(schema, {
defaultValues: { defaultValues: {
@@ -71,8 +71,8 @@ export default function ResetPasswordPage() {
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
if (!token) { if (!token) {
toast({ toast({
title: __("Reset failed"), title: t("resetPasswordPage.errors.resetFailed"),
description: __("Invalid or missing reset token"), description: t("resetPasswordPage.errors.invalidToken"),
variant: "error", variant: "error",
}); });
return; return;
@@ -87,7 +87,7 @@ export default function ResetPasswordPage() {
}, },
onError: (e: Error) => { onError: (e: Error) => {
toast({ toast({
title: __("Reset failed"), title: t("resetPasswordPage.errors.resetFailed"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -95,9 +95,9 @@ export default function ResetPasswordPage() {
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Reset failed"), title: t("resetPasswordPage.errors.resetFailed"),
description: formatError( description: formatError(
__("Password reset failed"), t("resetPasswordPage.errors.reset"),
e, e,
), ),
variant: "error", variant: "error",
@@ -105,8 +105,8 @@ export default function ResetPasswordPage() {
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Password reset successfully"), description: t("resetPasswordPage.messages.reset"),
variant: "success", variant: "success",
}); });
void navigate("/auth/login", { replace: true }); void navigate("/auth/login", { replace: true });
@@ -117,15 +117,15 @@ export default function ResetPasswordPage() {
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Reset password")}</h1> <h1 className="text-3xl font-bold">{t("resetPasswordPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Enter your new password to reset your account")} {t("resetPasswordPage.description")}
</p> </p>
</div> </div>
<form onSubmit={e => void onSubmit(e)} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Field <Field
label={__("New Password")} label={t("resetPasswordPage.fields.newPassword")}
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
{...register("password")} {...register("password")}
@@ -134,7 +134,7 @@ export default function ResetPasswordPage() {
/> />
<Field <Field
label={__("Confirm Password")} label={t("resetPasswordPage.fields.confirmPassword")}
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
{...register("confirmPassword")} {...register("confirmPassword")}
@@ -144,20 +144,20 @@ export default function ResetPasswordPage() {
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}> <Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
{formState.isLoading {formState.isLoading
? __("Resetting password...") ? t("resetPasswordPage.actions.resetting")
: __("Reset password")} : t("resetPasswordPage.actions.reset")}
</Button> </Button>
</form> </form>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")} {t("resetPasswordPage.rememberPassword")}
{" "} {" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Log in here")} {t("resetPasswordPage.actions.logIn")}
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -20,9 +20,9 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, usePreloadedQuery, useQueryLoader } from "react-relay"; import { useMutation, usePreloadedQuery, useQueryLoader } from "react-relay";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -57,11 +57,11 @@ const schema = z.object({
type FormData = z.infer<typeof schema>; type FormData = z.infer<typeof schema>;
function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQueryLoader<SignUpPageQuery>>[0]> }) { function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQueryLoader<SignUpPageQuery>>[0]> }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
usePageTitle(__("Sign up")); usePageTitle(t("signUpPage.pageTitle"));
const data = usePreloadedQuery<SignUpPageQuery>(signUpPageQuery, props.queryRef); const data = usePreloadedQuery<SignUpPageQuery>(signUpPageQuery, props.queryRef);
@@ -87,23 +87,22 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Sign up failed"), title: t("signUpPage.errors.failed"),
description: formatError(__("Sign up failed"), e), description: formatError(t("signUpPage.errors.failed"), e),
variant: "error", variant: "error",
}); });
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"), description: t("signUpPage.messages.created"),
description: __("Account created successfully"),
variant: "success", variant: "success",
}); });
void navigate("/", { replace: true }); void navigate("/", { replace: true });
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Sign up failed"), title: t("signUpPage.errors.failed"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -115,9 +114,9 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8 text-center"> <div className="space-y-6 w-full max-w-md mx-auto pt-8 text-center">
<div className="space-y-2"> <div className="space-y-2">
<h1 className="text-3xl font-bold">{__("Registration unavailable")}</h1> <h1 className="text-3xl font-bold">{t("signUpPage.unavailable.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("New account registration is currently disabled. Please contact your administrator or reach out to Probo for assistance.")} {t("signUpPage.unavailable.description")}
</p> </p>
</div> </div>
@@ -127,7 +126,7 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
className="w-xs h-10 mx-auto" className="w-xs h-10 mx-auto"
to="/auth/login" to="/auth/login"
> >
{__("Back to login")} {t("signUpPage.actions.backToLogin")}
</Button> </Button>
</div> </div>
</div> </div>
@@ -137,33 +136,33 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Sign up")}</h1> <h1 className="text-3xl font-bold">{t("signUpPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Enter your information to create an account")} {t("signUpPage.description")}
</p> </p>
</div> </div>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field <Field
label={__("Full Name")} label={t("signUpPage.fields.fullName")}
type="text" type="text"
placeholder={__("John Doe")} placeholder={t("signUpPage.fields.fullNamePlaceholder")}
{...register("fullName")} {...register("fullName")}
required required
error={formState.errors.fullName?.message} error={formState.errors.fullName?.message}
/> />
<Field <Field
label={__("Email")} label={t("signUpPage.fields.email")}
type="email" type="email"
placeholder={__("name@example.com")} placeholder={t("signUpPage.fields.emailPlaceholder")}
{...register("email")} {...register("email")}
required required
error={formState.errors.email?.message} error={formState.errors.email?.message}
/> />
<Field <Field
label={__("Password")} label={t("signUpPage.fields.password")}
type="password" type="password"
placeholder="••••••••" placeholder="••••••••"
{...register("password")} {...register("password")}
@@ -173,20 +172,20 @@ function SignUpPageContent(props: { queryRef: NonNullable<ReturnType<typeof useQ
<Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}> <Button type="submit" className="w-xs h-10 mx-auto mt-6" disabled={formState.isLoading}>
{formState.isLoading {formState.isLoading
? __("Creating account...") ? t("signUpPage.actions.creating")
: __("Sign up with email")} : t("signUpPage.actions.signUpWithEmail")}
</Button> </Button>
</form> </form>
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Already have an account?")} {t("signUpPage.alreadyHaveAccount")}
{" "} {" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Log in here")} {t("signUpPage.actions.logIn")}
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -20,9 +20,9 @@
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link, useSearchParams } from "react-router"; import { Link, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -44,11 +44,11 @@ const confirmEmailSchema = z.object({
}); });
export default function VerifyEmailPage() { export default function VerifyEmailPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
usePageTitle(__("Confirm Email")); usePageTitle(t("verifyEmailPage.pageTitle"));
const [isConfirmed, setIsConfirmed] = useState(false); const [isConfirmed, setIsConfirmed] = useState(false);
@@ -71,8 +71,7 @@ export default function VerifyEmailPage() {
onCompleted: (_, errors) => { onCompleted: (_, errors) => {
if (errors) { if (errors) {
toast({ toast({
title: __("Error"), title: t("common.error"), description: formatError(t("verifyEmailPage.errors.confirm"), errors),
description: formatError(__("Failed to confirm email"), errors),
variant: "error", variant: "error",
}); });
return; return;
@@ -80,15 +79,13 @@ export default function VerifyEmailPage() {
setIsConfirmed(true); setIsConfirmed(true);
toast({ toast({
title: __("Success"), title: t("common.success"), description: t("verifyEmailPage.messages.confirmed"),
description: __("Your email has been confirmed successfully"),
variant: "success", variant: "success",
}); });
}, },
onError: (err) => { onError: (err) => {
toast({ toast({
title: __("Error"), title: t("common.error"), description: err.message || t("verifyEmailPage.errors.confirm"),
description: err.message || __("Failed to confirm email"),
variant: "error", variant: "error",
}); });
}, },
@@ -98,9 +95,9 @@ export default function VerifyEmailPage() {
return ( return (
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Email Confirmation")}</h1> <h1 className="text-3xl font-bold">{t("verifyEmailPage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Confirm your email address to complete registration")} {t("verifyEmailPage.description")}
</p> </p>
</div> </div>
@@ -108,25 +105,23 @@ export default function VerifyEmailPage() {
? ( ? (
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<p className="text-green-600 dark:text-green-400"> <p className="text-green-600 dark:text-green-400">
{__("Your email has been confirmed successfully!")} {t("verifyEmailPage.messages.confirmedWithExclamation")}
</p> </p>
<Button to="/auth/login" className="w-full"> <Button to="/auth/login" className="w-full">
{__("Proceed to Login")} {t("verifyEmailPage.actions.proceedToLogin")}
</Button> </Button>
</div> </div>
) )
: ( : (
<form onSubmit={e => void handleSubmit(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field <Field
label={__("Confirmation Token")} label={t("verifyEmailPage.fields.token")}
type="text" type="text"
placeholder={__("Enter your confirmation token")} placeholder={t("verifyEmailPage.fields.tokenPlaceholder")}
{...form.register("token")} {...form.register("token")}
error={form.formState.errors.token?.message} error={form.formState.errors.token?.message}
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
help={__( help={t("verifyEmailPage.fields.tokenHelp")}
"The token has been automatically filled from the URL if available",
)}
/> />
<Button <Button
@@ -135,8 +130,8 @@ export default function VerifyEmailPage() {
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
> >
{form.formState.isSubmitting {form.formState.isSubmitting
? __("Confirming...") ? t("verifyEmailPage.actions.confirming")
: __("Confirm Email")} : t("verifyEmailPage.actions.confirm")}
</Button> </Button>
</form> </form>
)} )}
@@ -148,7 +143,7 @@ export default function VerifyEmailPage() {
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
> >
{__("Back to Login")} {t("verifyEmailPage.actions.backToLogin")}
</Link> </Link>
</p> </p>
)} )}

View File

@@ -19,9 +19,9 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Button, Field, IconChevronLeft, useToast } from "@probo/ui"; import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
import type { FormEventHandler } from "react"; import type { FormEventHandler } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "react-relay"; import { useMutation } from "react-relay";
import { Link, matchPath, useLocation } from "react-router"; import { Link, matchPath, useLocation } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -43,7 +43,7 @@ export default function PasswordSignInPage() {
const location = useLocation(); const location = useLocation();
const postAuthRedirectUrl = usePostAuthRedirectUrl(); const postAuthRedirectUrl = usePostAuthRedirectUrl();
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [signIn, isSigningIn] const [signIn, isSigningIn]
@@ -74,9 +74,9 @@ export default function PasswordSignInPage() {
onCompleted: (_, error) => { onCompleted: (_, error) => {
if (error) { if (error) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to login"), t("passwordSignInPage.errors.login"),
error, error,
), ),
variant: "error", variant: "error",
@@ -88,7 +88,7 @@ export default function PasswordSignInPage() {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -103,55 +103,55 @@ export default function PasswordSignInPage() {
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4" className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
> >
<IconChevronLeft size={20} /> <IconChevronLeft size={20} />
<span className="text-sm">{__("Back")}</span> <span className="text-sm">{t("passwordSignInPage.actions.back")}</span>
</Link> </Link>
<h1 className="text-center text-2xl font-bold"> <h1 className="text-center text-2xl font-bold">
{__("Sign in with password")} {t("passwordSignInPage.title")}
</h1> </h1>
<p className="text-center text-txt-tertiary mt-1 mb-6"> <p className="text-center text-txt-tertiary mt-1 mb-6">
{__("Enter your email and password")} {t("passwordSignInPage.description")}
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
<Field <Field
required required
placeholder={__("Email")} placeholder={t("passwordSignInPage.fields.email")}
name="email" name="email"
type="email" type="email"
label={__("Email")} label={t("passwordSignInPage.fields.email")}
autoFocus autoFocus
/> />
<Field <Field
required required
placeholder={__("Password")} placeholder={t("passwordSignInPage.fields.password")}
name="password" name="password"
type="password" type="password"
label={__("Password")} label={t("passwordSignInPage.fields.password")}
/> />
</div> </div>
<Button className="w-xs h-10 mx-auto mt-6" disabled={isSigningIn}> <Button className="w-xs h-10 mx-auto mt-6" disabled={isSigningIn}>
{isSigningIn ? __("Logging in...") : __("Login")} {isSigningIn ? t("passwordSignInPage.actions.loggingIn") : t("passwordSignInPage.actions.login")}
</Button> </Button>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")} {t("passwordSignInPage.noAccount")}
{" "} {" "}
<Link to={{ pathname: "/auth/register", search: location.search }} className="underline hover:text-txt-primary"> <Link to={{ pathname: "/auth/register", search: location.search }} className="underline hover:text-txt-primary">
{__("Register")} {t("passwordSignInPage.actions.register")}
</Link> </Link>
</div> </div>
<div className="text-center text-sm text-txt-secondary"> <div className="text-center text-sm text-txt-secondary">
{__("Forgot password?")} {t("passwordSignInPage.forgotPassword")}
{" "} {" "}
<Link <Link
to="/auth/forgot-password" to="/auth/forgot-password"
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"
> >
{__("Reset password")} {t("passwordSignInPage.actions.resetPassword")}
</Link> </Link>
</div> </div>
</form> </form>

View File

@@ -18,9 +18,9 @@
// 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 { Button, Field, IconChevronLeft, useToast } from "@probo/ui"; import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
import { type FormEventHandler, useEffect, useState } from "react"; import { type FormEventHandler, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { import {
type PreloadedQuery, type PreloadedQuery,
usePreloadedQuery, usePreloadedQuery,
@@ -40,7 +40,7 @@ const ssoAvailabilityQuery = graphql`
export default function SSOSignInPage() { export default function SSOSignInPage() {
const location = useLocation(); const location = useLocation();
const { __ } = useTranslate(); const { t } = useTranslation();
const [queryRef, loadQuery] const [queryRef, loadQuery]
= useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery); = useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
@@ -65,37 +65,37 @@ export default function SSOSignInPage() {
className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4" className="flex items-center gap-2 text-txt-secondary hover:text-txt-primary transition-colors mb-4"
> >
<IconChevronLeft size={20} /> <IconChevronLeft size={20} />
<span className="text-sm">{__("Back")}</span> <span className="text-sm">{t("ssoSignInPage.actions.back")}</span>
</Link> </Link>
<h1 className="text-center text-2xl font-bold"> <h1 className="text-center text-2xl font-bold">
{__("Login with SSO")} {t("ssoSignInPage.title")}
</h1> </h1>
<p className="text-center text-txt-tertiary mt-1 mb-6"> <p className="text-center text-txt-tertiary mt-1 mb-6">
{__("Enter your work email to continue with SSO")} {t("ssoSignInPage.description")}
</p> </p>
<Field <Field
required required
placeholder={__("Work Email")} placeholder={t("ssoSignInPage.fields.workEmail")}
name="email" name="email"
type="email" type="email"
label={__("Work Email")} label={t("ssoSignInPage.fields.workEmail")}
autoFocus autoFocus
/> />
<Button className="w-xs h-10 mx-auto mt-6" disabled={checking}> <Button className="w-xs h-10 mx-auto mt-6" disabled={checking}>
{checking ? __("Checking...") : __("Continue with SSO")} {checking ? t("ssoSignInPage.actions.checking") : t("ssoSignInPage.actions.continue")}
</Button> </Button>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")} {t("ssoSignInPage.noAccount")}
{" "} {" "}
<Link <Link
to={{ pathname: "/auth/register", search: location.search }} to={{ pathname: "/auth/register", search: location.search }}
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"
> >
{__("Register")} {t("ssoSignInPage.actions.register")}
</Link> </Link>
</div> </div>
</form> </form>
@@ -118,7 +118,7 @@ function NavigateToSSOLoginURL(props: {
}) { }) {
const { queryRef, loginSearch } = props; const { queryRef, loginSearch } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -132,11 +132,11 @@ function NavigateToSSOLoginURL(props: {
useEffect(() => { useEffect(() => {
if (!ssoLoginURL.ok) { if (!ssoLoginURL.ok) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: description:
ssoLoginURL.errors[0] instanceof Error ssoLoginURL.errors[0] instanceof Error
? ssoLoginURL.errors[0].message ? ssoLoginURL.errors[0].message
: __("SSO not available for this email domain"), : t("ssoSignInPage.errors.unavailable"),
variant: "error", variant: "error",
}); });
@@ -146,8 +146,7 @@ function NavigateToSSOLoginURL(props: {
if (!ssoLoginURL.value) { if (!ssoLoginURL.value) {
toast({ toast({
title: __("Error"), title: t("common.error"), description: t("ssoSignInPage.errors.unavailable"),
description: __("SSO not available for this email domain"),
variant: "error", variant: "error",
}); });
return; return;
@@ -162,7 +161,7 @@ function NavigateToSSOLoginURL(props: {
} }
window.location.href = url.toString(); window.location.href = url.toString();
}, [__, loginSearch, navigate, postAuthRedirectUrl, searchParams, ssoLoginURL, toast]); }, [t, loginSearch, navigate, postAuthRedirectUrl, searchParams, ssoLoginURL, toast]);
return null; return null;
} }

View File

@@ -19,8 +19,8 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Button } from "@probo/ui"; import { Button } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Link, useLocation, useSearchParams } from "react-router"; import { Link, useLocation, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -52,7 +52,7 @@ type Props = {
}; };
export default function SignInPage(props: Props) { export default function SignInPage(props: Props) {
const { __ } = useTranslate(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const postAuthRedirectUrl = usePostAuthRedirectUrl(); const postAuthRedirectUrl = usePostAuthRedirectUrl();
@@ -64,15 +64,15 @@ export default function SignInPage(props: Props) {
const clientBranding = data.oauthClientBranding; const clientBranding = data.oauthClientBranding;
const authorizeHeading = clientBranding?.name const authorizeHeading = clientBranding?.name
? __("Sign in") ? t("auth.actions.signIn")
: __("Sign in to continue"); : t("signInPage.authorize.title");
usePageTitle( usePageTitle(
isAuthorizeFlow isAuthorizeFlow
? clientBranding?.name ? clientBranding?.name
? `${__("Sign in to")} ${clientBranding.name}` ? t("signInPage.authorize.titleWithClient", { name: clientBranding.name })
: authorizeHeading : authorizeHeading
: __("Sign in to your account"), : t("signInPage.title"),
); );
const oidcContinueURL = isAuthorizeFlow ? postAuthRedirectUrl : undefined; const oidcContinueURL = isAuthorizeFlow ? postAuthRedirectUrl : undefined;
@@ -94,11 +94,11 @@ export default function SignInPage(props: Props) {
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{isAuthorizeFlow {isAuthorizeFlow
? authorizeHeading ? authorizeHeading
: __("Sign in to your account")} : t("signInPage.title")}
</h1> </h1>
{isAuthorizeFlow && ( {isAuthorizeFlow && (
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Use your email or a connected account to continue")} {t("signInPage.authorize.description")}
</p> </p>
)} )}
</div> </div>
@@ -114,33 +114,35 @@ export default function SignInPage(props: Props) {
<MagicLinkForm /> <MagicLinkForm />
<Divider>{__("Or")}</Divider> <Divider>{t("signInPage.or")}</Divider>
<Button <Button
variant="secondary" variant="secondary"
className="w-full h-10" className="w-full h-10"
to={{ pathname: "/auth/sso-login", search: location.search }} to={{ pathname: "/auth/sso-login", search: location.search }}
> >
{__("Sign in with SSO")} {t("signInPage.actions.signInWithSso")}
</Button> </Button>
<Divider>{t("signInPage.or")}</Divider>
<Button <Button
variant="secondary" variant="secondary"
className="w-full h-10" className="w-full h-10"
to={{ pathname: "/auth/password-login", search: location.search }} to={{ pathname: "/auth/password-login", search: location.search }}
> >
{__("Sign in with password")} {t("signInPage.actions.signInWithEmail")}
</Button> </Button>
</div> </div>
<p className="text-center text-sm text-txt-secondary"> <p className="mt-8 text-center text-sm text-txt-secondary">
{__("New to Probo?")} {t("signInPage.newToProbo")}
{" "} {" "}
<Link <Link
to={{ pathname: "/auth/register", search: location.search }} to={{ pathname: "/auth/register", search: location.search }}
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"
> >
{__("Create account")} {t("signInPage.actions.createAccount")}
</Link> </Link>
</p> </p>
</div> </div>

View File

@@ -18,9 +18,9 @@
// 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 { Button, Field, useToast } from "@probo/ui"; import { Button, Field, useToast } from "@probo/ui";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
@@ -35,7 +35,7 @@ type FormData = z.infer<typeof schema>;
const timerDurationSeconds = 60; const timerDurationSeconds = 60;
export function MagicLinkForm() { export function MagicLinkForm() {
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const postAuthRedirectUrl = usePostAuthRedirectUrl(); const postAuthRedirectUrl = usePostAuthRedirectUrl();
@@ -83,8 +83,8 @@ export function MagicLinkForm() {
}); });
} catch { } catch {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: __("Cannot send magic link"), description: t("magicLinkForm.errors.send"),
variant: "error", variant: "error",
}); });
return; return;
@@ -92,16 +92,16 @@ export function MagicLinkForm() {
if (!response.ok) { if (!response.ok) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: __("Cannot send magic link"), description: t("magicLinkForm.errors.send"),
variant: "error", variant: "error",
}); });
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"),
description: __("Magic link sent!"), description: t("magicLinkForm.messages.sent"),
variant: "success", variant: "success",
}); });
setTimer(timerDurationSeconds); setTimer(timerDurationSeconds);
@@ -111,7 +111,7 @@ export function MagicLinkForm() {
return ( return (
<form onSubmit={e => void handleSubmit(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field <Field
label={__("Email")} label={t("magicLinkForm.fields.email")}
placeholder="john.doe@acme.com" placeholder="john.doe@acme.com"
{...register("email")} {...register("email")}
type="email" type="email"
@@ -121,9 +121,7 @@ export function MagicLinkForm() {
{magicLinkSent && ( {magicLinkSent && (
<p className="text-txt-primary text-sm"> <p className="text-txt-primary text-sm">
{__( {t("magicLinkForm.sentDescription")}
"Magic link sent! Check your email and use the link to continue.",
)}
</p> </p>
)} )}
@@ -134,9 +132,9 @@ export function MagicLinkForm() {
> >
{magicLinkSent {magicLinkSent
? timer === 0 ? timer === 0
? __("Resend Link") ? t("magicLinkForm.actions.resend")
: `${__("Resend Link in")} ${timer}s` : t("magicLinkForm.actions.resendIn", { count: timer })
: __("Send Magic Link")} : t("magicLinkForm.actions.send")}
</Button> </Button>
</form> </form>
); );

View File

@@ -18,9 +18,9 @@
// 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 { Button, Google, Microsoft } from "@probo/ui"; import { Button, Google, Microsoft } from "@probo/ui";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useSearchParams } from "react-router"; import { useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -50,7 +50,7 @@ export function OIDCButton({
providerRef: OIDCButtonFragment$key; providerRef: OIDCButtonFragment$key;
continueURL?: string; continueURL?: string;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const safeContinueUrl = useSafeContinueUrl(); const safeContinueUrl = useSafeContinueUrl();
const provider = useFragment(fragment, providerRef); const provider = useFragment(fragment, providerRef);
@@ -74,7 +74,9 @@ export function OIDCButton({
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{Icon && <Icon width={18} height={18} />} {Icon && <Icon width={18} height={18} />}
{__(`Sign in with ${provider.name.charAt(0).toUpperCase() + provider.name.slice(1)}`)} {t("oidcButton.signInWith", {
provider: provider.name.charAt(0).toUpperCase() + provider.name.slice(1),
})}
</span> </span>
</Button> </Button>
); );

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -28,6 +27,7 @@ import {
Input, Input,
} from "@probo/ui"; } from "@probo/ui";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { MembershipsPageQuery } from "#/__generated__/iam/MembershipsPageQuery.graphql"; import type { MembershipsPageQuery } from "#/__generated__/iam/MembershipsPageQuery.graphql";
@@ -67,10 +67,10 @@ export const membershipsPageQuery = graphql`
export function MembershipsPage(props: { export function MembershipsPage(props: {
queryRef: PreloadedQuery<MembershipsPageQuery>; queryRef: PreloadedQuery<MembershipsPageQuery>;
}) { }) {
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
usePageTitle(__("Select an organization")); usePageTitle(t("membershipsPage.pageTitle"));
const { queryRef } = props; const { queryRef } = props;
const { const {
@@ -93,13 +93,13 @@ export function MembershipsPage(props: {
<> <>
<div className="space-y-6 w-full py-6"> <div className="space-y-6 w-full py-6">
<h1 className="text-3xl font-bold text-center"> <h1 className="text-3xl font-bold text-center">
{__("Select an organization")} {t("membershipsPage.title")}
</h1> </h1>
<div className="space-y-4 w-full"> <div className="space-y-4 w-full">
{invitingOrganizations.length > 0 && ( {invitingOrganizations.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{__("Pending invitations")} {t("membershipsPage.pendingInvitations")}
</h2> </h2>
{invitingOrganizations.map(organization => ( {invitingOrganizations.map(organization => (
<InvitingOrganizationCard key={organization.id} fKey={organization} /> <InvitingOrganizationCard key={organization.id} fKey={organization} />
@@ -109,12 +109,12 @@ export function MembershipsPage(props: {
{initialProfiles.length > 0 && ( {initialProfiles.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{__("Your organizations")} {t("membershipsPage.yourOrganizations")}
</h2> </h2>
<div className="w-full"> <div className="w-full">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search organizations...")} placeholder={t("membershipsPage.searchPlaceholder")}
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
/> />
@@ -122,7 +122,7 @@ export function MembershipsPage(props: {
{profiles.length === 0 {profiles.length === 0
? ( ? (
<div className="text-center text-txt-secondary py-4"> <div className="text-center text-txt-secondary py-4">
{__("No organizations found")} {t("membershipsPage.empty")}
</div> </div>
) )
: ( : (
@@ -138,10 +138,10 @@ export function MembershipsPage(props: {
)} )}
<Card padded> <Card padded>
<h2 className="text-xl font-semibold mb-1"> <h2 className="text-xl font-semibold mb-1">
{__("Create an organization")} {t("membershipsPage.createOrganization.title")}
</h2> </h2>
<p className="text-txt-tertiary mb-4"> <p className="text-txt-tertiary mb-4">
{__("Add a new organization to your account")} {t("membershipsPage.createOrganization.description")}
</p> </p>
<Button <Button
to="/organizations/new" to="/organizations/new"
@@ -149,7 +149,7 @@ export function MembershipsPage(props: {
icon={IconPlusLarge} icon={IconPlusLarge}
className="w-full" className="w-full"
> >
{__("Create organization")} {t("membershipsPage.createOrganization.action")}
</Button> </Button>
</Card> </Card>
</div> </div>

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 { useTranslate } from "@probo/i18n";
import { Badge, Card, IconMail } from "@probo/ui"; import { Badge, Card, IconMail } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -37,7 +37,7 @@ interface InvitingOrganizationCardProps {
export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) { export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const organization = useFragment<InvitingOrganizationCardFragment$key>( const organization = useFragment<InvitingOrganizationCardFragment$key>(
fragment, fragment,
@@ -50,7 +50,7 @@ export function InvitingOrganizationCard(props: InvitingOrganizationCardProps) {
<h2 className="font-semibold text-xl">{organization.name}</h2> <h2 className="font-semibold text-xl">{organization.name}</h2>
<Badge variant="neutral" className="flex items-center gap-1"> <Badge variant="neutral" className="flex items-center gap-1">
<IconMail size={14} /> <IconMail size={14} />
{__("Check your email")} {t("invitingOrganizationCard.checkEmail")}
</Badge> </Badge>
</div> </div>
</Card> </Card>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { parseDate } from "@probo/helpers"; import { parseDate } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Avatar, Avatar,
Badge, Badge,
@@ -29,6 +28,7 @@ import {
IconClock, IconClock,
IconLock, IconLock,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { Link } from "react-router"; import { Link } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -65,7 +65,7 @@ interface MembershipCardProps {
export function MembershipCard(props: MembershipCardProps) { export function MembershipCard(props: MembershipCardProps) {
const { fKey, organizationFragmentRef } = props; const { fKey, organizationFragmentRef } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { membership, ...user } = useFragment<MembershipCardFragment$key>( const { membership, ...user } = useFragment<MembershipCardFragment$key>(
fragment, fragment,
@@ -84,21 +84,21 @@ export function MembershipCard(props: MembershipCardProps) {
return ( return (
<Badge variant="success" className="flex items-center gap-1"> <Badge variant="success" className="flex items-center gap-1">
<IconCheckmark1 size={14} /> <IconCheckmark1 size={14} />
{__("Authenticated")} {t("membershipCard.status.authenticated")}
</Badge> </Badge>
); );
} else if (isExpired) { } else if (isExpired) {
return ( return (
<Badge variant="warning" className="flex items-center gap-1"> <Badge variant="warning" className="flex items-center gap-1">
<IconClock size={14} /> <IconClock size={14} />
{__("Session expired")} {t("membershipCard.status.sessionExpired")}
</Badge> </Badge>
); );
} else { } else {
return ( return (
<Badge variant="neutral" className="flex items-center gap-1"> <Badge variant="neutral" className="flex items-center gap-1">
<IconLock size={14} /> <IconLock size={14} />
{__("Authentication required")} {t("membershipCard.status.authenticationRequired")}
</Badge> </Badge>
); );
} }
@@ -123,12 +123,12 @@ export function MembershipCard(props: MembershipCardProps) {
? ( ? (
<Link to={`/organizations/${organization.id}`}> <Link to={`/organizations/${organization.id}`}>
{isAssuming {isAssuming
? <Button variant="secondary">{__("Start")}</Button> ? <Button variant="secondary">{t("membershipCard.actions.start")}</Button>
: <Button>{__("Login")}</Button>} : <Button>{t("membershipCard.actions.login")}</Button>}
</Link> </Link>
) )
: ( : (
<Button variant="secondary" disabled>{__("Account deactivated")}</Button> <Button variant="secondary" disabled>{t("membershipCard.accountDeactivated")}</Button>
)} )}
</div> </div>
</div> </div>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
DropdownSeparator, DropdownSeparator,
IconArrowBoxLeft, IconArrowBoxLeft,
@@ -29,6 +28,7 @@ import {
UserDropdownItem, UserDropdownItem,
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment, useMutation } from "react-relay"; import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -55,7 +55,7 @@ const signOutMutation = graphql`
export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) { export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const { canListAPIKeys, canListOAuth2AccessTokens, email, fullName } const { canListAPIKeys, canListOAuth2AccessTokens, email, fullName }
@@ -70,8 +70,8 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Request failed"), title: t("viewerDropdown.errors.requestFailed"),
description: formatError(__("Cannot sign out"), e), description: formatError(t("viewerDropdown.errors.cannotSignOut"), e),
variant: "error", variant: "error",
}); });
return; return;
@@ -80,7 +80,7 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -94,27 +94,27 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
<UserDropdownItem <UserDropdownItem
to="/me/api-keys" to="/me/api-keys"
icon={IconKey} icon={IconKey}
label={__("API Keys")} label={t("apiKeys.title")}
/> />
)} )}
{canListOAuth2AccessTokens && ( {canListOAuth2AccessTokens && (
<UserDropdownItem <UserDropdownItem
to="/me/oauth-tokens" to="/me/oauth-tokens"
icon={IconKey} icon={IconKey}
label={__("OAuth tokens")} label={t("viewerDropdown.actions.oauthTokens")}
/> />
)} )}
<UserDropdownItem <UserDropdownItem
to="mailto:support@probo.com" to="mailto:support@probo.com"
icon={IconCircleQuestionmark} icon={IconCircleQuestionmark}
label={__("Help")} label={t("viewerDropdown.actions.help")}
/> />
<DropdownSeparator /> <DropdownSeparator />
<UserDropdownItem <UserDropdownItem
variant="danger" variant="danger"
to="/logout" to="/logout"
icon={IconArrowBoxLeft} icon={IconArrowBoxLeft}
label="Logout" label={t("viewerDropdown.actions.logout")}
onClick={handleLogout} onClick={handleLogout}
/> />
</UserDropdown> </UserDropdown>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -35,6 +34,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { ConnectionHandler, useLazyLoadQuery } from "react-relay"; import { ConnectionHandler, useLazyLoadQuery } from "react-relay";
import { Link, useNavigate } from "react-router"; import { Link, useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -106,12 +106,12 @@ function computeExpiresAt(expiresIn: CreateFormData["expiresIn"]) {
} }
export function NewOAuthTokenPage() { export function NewOAuthTokenPage() {
const { __ } = useTranslate(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const tokenDialogRef = useDialogRef(); const tokenDialogRef = useDialogRef();
const [token, setToken] = useState(""); const [token, setToken] = useState("");
usePageTitle(__("New OAuth token")); usePageTitle(t("newOAuthTokenPage.pageTitle"));
const data = useLazyLoadQuery<NewOAuthTokenPageQuery>(pageQuery, {}); const data = useLazyLoadQuery<NewOAuthTokenPageQuery>(pageQuery, {});
@@ -136,8 +136,8 @@ export function NewOAuthTokenPage() {
const [create, isCreating] = useMutationWithToasts<NewOAuthTokenPageCreateMutation>( const [create, isCreating] = useMutationWithToasts<NewOAuthTokenPageCreateMutation>(
createMutation, createMutation,
{ {
successMessage: "OAuth token created successfully.", successMessage: t("newOAuthTokenPage.messages.created"),
errorMessage: "Failed to create OAuth token", errorMessage: t("newOAuthTokenPage.errors.create"),
}, },
); );
@@ -202,25 +202,23 @@ export function NewOAuthTokenPage() {
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("OAuth tokens"), label: t("oauthTokensPage.title"),
to: "/me/oauth-tokens", to: "/me/oauth-tokens",
}, },
{ label: __("New token") }, { label: t("newOAuthTokenPage.breadcrumb") },
]} ]}
/> />
<PageHeader <PageHeader
title={__("New OAuth token")} title={t("newOAuthTokenPage.title")}
description={__( description={t("newOAuthTokenPage.description")}
"Create a bearer token with scoped access to the Probo API.",
)}
/> />
<Card padded> <Card padded>
<form className="space-y-6" onSubmit={e => void handleSubmit(handleCreate)(e)}> <form className="space-y-6" onSubmit={e => void handleSubmit(handleCreate)(e)}>
<div className="max-w-xl space-y-6"> <div className="max-w-xl space-y-6">
<Field> <Field>
<Label htmlFor="name">{__("Name")}</Label> <Label htmlFor="name">{t("newOAuthTokenPage.fields.name")}</Label>
<Input id="name" {...register("name")} /> <Input id="name" {...register("name")} />
{formState.errors.name && ( {formState.errors.name && (
<p className="text-sm text-danger mt-1"> <p className="text-sm text-danger mt-1">
@@ -230,7 +228,7 @@ export function NewOAuthTokenPage() {
</Field> </Field>
<Field> <Field>
<Label htmlFor="expiresIn">{__("Expiration")}</Label> <Label htmlFor="expiresIn">{t("newOAuthTokenPage.fields.expiration")}</Label>
<Controller <Controller
name="expiresIn" name="expiresIn"
control={control} control={control}
@@ -240,10 +238,10 @@ export function NewOAuthTokenPage() {
value={field.value} value={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
> >
<Option value="1month">{__("1 month")}</Option> <Option value="1month">{t("newOAuthTokenPage.expirations.month", { count: 1 })}</Option>
<Option value="3months">{__("3 months")}</Option> <Option value="3months">{t("newOAuthTokenPage.expirations.month", { count: 3 })}</Option>
<Option value="6months">{__("6 months")}</Option> <Option value="6months">{t("newOAuthTokenPage.expirations.month", { count: 6 })}</Option>
<Option value="1year">{__("1 year")}</Option> <Option value="1year">{t("newOAuthTokenPage.expirations.year", { count: 1 })}</Option>
</Select> </Select>
)} )}
/> />
@@ -251,7 +249,7 @@ export function NewOAuthTokenPage() {
</div> </div>
<Field> <Field>
<Label>{__("Scopes")}</Label> <Label>{t("newOAuthTokenPage.fields.scopes")}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mt-2"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 mt-2">
{supportedScopes.map(scope => ( {supportedScopes.map(scope => (
<label key={scope} className="flex items-start gap-2"> <label key={scope} className="flex items-start gap-2">
@@ -279,18 +277,18 @@ export function NewOAuthTokenPage() {
<div className="flex gap-3 max-w-xl"> <div className="flex gap-3 max-w-xl">
<Button type="submit" disabled={isCreating}> <Button type="submit" disabled={isCreating}>
{__("Create token")} {t("newOAuthTokenPage.actions.create")}
</Button> </Button>
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
onClick={toggleAllScopes} onClick={toggleAllScopes}
> >
{allScopesSelected ? __("Deselect all") : __("Select all")} {allScopesSelected ? t("newOAuthTokenPage.actions.deselectAll") : t("newOAuthTokenPage.actions.selectAll")}
</Button> </Button>
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<Link to="/me/oauth-tokens"> <Link to="/me/oauth-tokens">
{__("Cancel")} {t("newOAuthTokenPage.actions.cancel")}
</Link> </Link>
</Button> </Button>
</div> </div>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -32,6 +31,7 @@ import {
Thead, Thead,
Tr, Tr,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { import {
graphql, graphql,
type PreloadedQuery, type PreloadedQuery,
@@ -87,9 +87,9 @@ export function OAuthTokensPage(props: {
queryRef: PreloadedQuery<OAuthTokensPageQuery>; queryRef: PreloadedQuery<OAuthTokensPageQuery>;
}) { }) {
const { queryRef } = props; const { queryRef } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
usePageTitle(__("OAuth tokens")); usePageTitle(t("oauthTokensPage.pageTitle"));
const { viewer } = usePreloadedQuery<OAuthTokensPageQuery>( const { viewer } = usePreloadedQuery<OAuthTokensPageQuery>(
oauthTokensPageQuery, oauthTokensPageQuery,
@@ -106,15 +106,13 @@ export function OAuthTokensPage(props: {
return ( return (
<div className="space-y-6 w-full py-6"> <div className="space-y-6 w-full py-6">
<PageHeader <PageHeader
title={__("OAuth tokens")} title={t("oauthTokensPage.title")}
description={__( description={t("oauthTokensPage.description")}
"Create bearer tokens with scoped API access for your account.",
)}
> >
{viewer.canCreateOAuth2AccessToken && ( {viewer.canCreateOAuth2AccessToken && (
<Button asChild> <Button asChild>
<Link to="/me/oauth-tokens/new"> <Link to="/me/oauth-tokens/new">
{__("Create token")} {t("oauthTokensPage.actions.create")}
</Link> </Link>
</Button> </Button>
)} )}
@@ -125,12 +123,10 @@ export function OAuthTokensPage(props: {
<Card padded> <Card padded>
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
{__("No OAuth tokens")} {t("oauthTokensPage.empty.title")}
</h3> </h3>
<p className="text-gray-600"> <p className="text-gray-600">
{__( {t("oauthTokensPage.empty.description")}
"Create a token to authenticate API requests on your behalf.",
)}
</p> </p>
</div> </div>
</Card> </Card>
@@ -139,16 +135,16 @@ export function OAuthTokensPage(props: {
<Card padded className="space-y-4"> <Card padded className="space-y-4">
{totalCount > tokens.length && ( {totalCount > tokens.length && (
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{`${__("Showing")} ${tokens.length} ${__("of")} ${totalCount}`} {t("oauthTokensPage.showing", { shown: tokens.length, total: totalCount })}
</p> </p>
)} )}
<Table> <Table>
<Thead> <Thead>
<Tr> <Tr>
<Th>{__("Name")}</Th> <Th>{t("oauthTokensPage.columns.name")}</Th>
<Th>{__("Scopes")}</Th> <Th>{t("oauthTokensPage.columns.scopes")}</Th>
<Th>{__("Created")}</Th> <Th>{t("oauthTokensPage.columns.created")}</Th>
<Th>{__("Expires")}</Th> <Th>{t("oauthTokensPage.columns.expires")}</Th>
<Th className="w-0" /> <Th className="w-0" />
</Tr> </Tr>
</Thead> </Thead>
@@ -170,7 +166,7 @@ export function OAuthTokensPage(props: {
disabled={isLoadingNext} disabled={isLoadingNext}
icon={isLoadingNext ? Spinner : IconChevronDown} icon={isLoadingNext ? Spinner : IconChevronDown}
> >
{__("Show more")} {t("oauthTokensPage.actions.showMore")}
</Button> </Button>
)} )}
</Card> </Card>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { useCopy } from "@probo/hooks"; import { useCopy } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { import {
Breadcrumb, Breadcrumb,
Button, Button,
@@ -31,6 +30,7 @@ import {
IconWarning, IconWarning,
} from "@probo/ui"; } from "@probo/ui";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { useTranslation } from "react-i18next";
export function OAuthTokenCredentialsDialog(props: { export function OAuthTokenCredentialsDialog(props: {
dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>; dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>;
@@ -38,21 +38,19 @@ export function OAuthTokenCredentialsDialog(props: {
onDone: () => void; onDone: () => void;
}) { }) {
const { dialogRef, token, onDone } = props; const { dialogRef, token, onDone } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const [isCopied, copy] = useCopy(); const [isCopied, copy] = useCopy();
return ( return (
<Dialog <Dialog
ref={dialogRef} ref={dialogRef}
title={<Breadcrumb items={[__("OAuth tokens"), __("Token")]} />} title={<Breadcrumb items={[t("oauthTokensPage.title"), t("oauthTokenCredentialsDialog.title")]} />}
> >
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
<div className="flex items-start gap-2 rounded-lg border border-border-danger bg-danger px-4 py-3 text-sm text-txt-danger"> <div className="flex items-start gap-2 rounded-lg border border-border-danger bg-danger px-4 py-3 text-sm text-txt-danger">
<IconWarning size={16} className="shrink-0 mt-0.5" /> <IconWarning size={16} className="shrink-0 mt-0.5" />
<p> <p>
{__( {t("oauthTokenCredentialsDialog.description")}
"Copy this bearer token now. You will not be able to see it again.",
)}
</p> </p>
</div> </div>
<code className="flex items-start gap-2 rounded-lg bg-subtle p-4 font-mono text-sm"> <code className="flex items-start gap-2 rounded-lg bg-subtle p-4 font-mono text-sm">
@@ -65,8 +63,8 @@ export function OAuthTokenCredentialsDialog(props: {
)} )}
onClick={() => copy(token)} onClick={() => copy(token)}
disabled={!token} disabled={!token}
aria-label={isCopied ? __("Copied") : __("Copy")} aria-label={isCopied ? t("oauthTokenCredentialsDialog.actions.copied") : t("oauthTokenCredentialsDialog.actions.copy")}
title={isCopied ? __("Copied") : __("Copy")} title={isCopied ? t("oauthTokenCredentialsDialog.actions.copied") : t("oauthTokenCredentialsDialog.actions.copy")}
> >
{isCopied {isCopied
? <IconCheckmark1 size={16} /> ? <IconCheckmark1 size={16} />
@@ -75,7 +73,7 @@ export function OAuthTokenCredentialsDialog(props: {
</code> </code>
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button onClick={onDone}>{__("Done")}</Button> <Button onClick={onDone}>{t("oauthTokenCredentialsDialog.actions.done")}</Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>
); );

View File

@@ -18,8 +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 { formatError } from "@probo/helpers"; import { formatDate, formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Badge, Badge,
Button, Button,
@@ -31,6 +30,7 @@ import {
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import * as Popover from "@radix-ui/react-popover"; import * as Popover from "@radix-ui/react-popover";
import { useTranslation } from "react-i18next";
import { ConnectionHandler, graphql, useFragment, useMutation } from "react-relay"; import { ConnectionHandler, graphql, useFragment, useMutation } from "react-relay";
import type { OAuthTokenRowFragment$key } from "#/__generated__/iam/OAuthTokenRowFragment.graphql"; import type { OAuthTokenRowFragment$key } from "#/__generated__/iam/OAuthTokenRowFragment.graphql";
@@ -67,7 +67,7 @@ export function OAuthTokenRow(props: {
identityId: string; identityId: string;
}) { }) {
const { tokenKey, identityId } = props; const { tokenKey, identityId } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const token = useFragment(fragment, tokenKey); const token = useFragment(fragment, tokenKey);
@@ -90,9 +90,9 @@ export function OAuthTokenRow(props: {
onCompleted: (_response, errors) => { onCompleted: (_response, errors) => {
if (errors?.length) { if (errors?.length) {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: formatError( description: formatError(
__("Failed to revoke OAuth token."), t("oauthTokenRow.errors.revoke"),
errors, errors,
), ),
variant: "error", variant: "error",
@@ -100,15 +100,13 @@ export function OAuthTokenRow(props: {
return; return;
} }
toast({ toast({
title: __("Success"), title: t("common.success"), description: t("oauthTokenRow.messages.revoked"),
description: __("OAuth token revoked."),
variant: "success", variant: "success",
}); });
}, },
onError: (error) => { onError: (error) => {
toast({ toast({
title: __("Error"), title: t("common.error"), description: formatError(t("oauthTokenRow.errors.revoke"), error),
description: formatError(__("Failed to revoke OAuth token."), error),
variant: "error", variant: "error",
}); });
}, },
@@ -154,28 +152,26 @@ export function OAuthTokenRow(props: {
)} )}
</div> </div>
</Td> </Td>
<Td>{new Date(token.createdAt).toLocaleDateString()}</Td> <Td>{formatDate(token.createdAt)}</Td>
<Td>{new Date(token.expiresAt).toLocaleDateString()}</Td> <Td>{formatDate(token.expiresAt)}</Td>
<Td> <Td>
{token.canDelete && ( {token.canDelete && (
<Dialog <Dialog
title={__("Revoke OAuth token")} title={t("oauthTokenRow.revoke.title")}
trigger={( trigger={(
<Button variant="danger" disabled={isRevoking}> <Button variant="danger" disabled={isRevoking}>
{__("Revoke")} {t("oauthTokenRow.actions.revoke")}
</Button> </Button>
)} )}
> >
<DialogContent padded> <DialogContent padded>
<p> <p>
{__( {t("oauthTokenRow.revoke.description")}
"This token will stop working immediately. This action cannot be undone.",
)}
</p> </p>
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}> <Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
{__("Revoke token")} {t("oauthTokenRow.actions.revokeToken")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Dialog> </Dialog>

View File

@@ -18,9 +18,9 @@
// 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 { UnAuthenticatedError } from "@probo/relay"; import { UnAuthenticatedError } from "@probo/relay";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { useNavigate, useSearchParams } from "react-router"; import { useNavigate, useSearchParams } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -67,7 +67,7 @@ export function AssumePage(props: { queryRef: PreloadedQuery<AssumePageQuery> })
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { __ } = useTranslate(); const { t } = useTranslation();
const safeContinueUrl = useSafeContinueUrl(`/organizations/${organizationId}`); const safeContinueUrl = useSafeContinueUrl(`/organizations/${organizationId}`);
@@ -126,9 +126,9 @@ export function AssumePage(props: { queryRef: PreloadedQuery<AssumePageQuery> })
<AuthLayout> <AuthLayout>
<div className="space-y-6 w-full max-w-md mx-auto pt-8"> <div className="space-y-6 w-full max-w-md mx-auto pt-8">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Sign in Redirection")}</h1> <h1 className="text-3xl font-bold">{t("assumePage.title")}</h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__("Redirecting you to your authentication URL…")} {t("assumePage.description")}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
Button, Button,
Card, Card,
@@ -29,6 +28,7 @@ import {
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import type { FormEventHandler } from "react"; import type { FormEventHandler } from "react";
import { useTranslation } from "react-i18next";
import { graphql, useMutation } from "react-relay"; import { graphql, useMutation } from "react-relay";
import { Link, useLocation, useNavigate } from "react-router"; import { Link, useLocation, useNavigate } from "react-router";
@@ -50,7 +50,7 @@ function NewOrganizationPageInner() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { t } = useTranslation();
const [createOrganization, isCreating] const [createOrganization, isCreating]
= useMutation<NewOrganizationPageMutation>(createOrganizationMutation); = useMutation<NewOrganizationPageMutation>(createOrganizationMutation);
@@ -61,8 +61,7 @@ function NewOrganizationPageInner() {
const name = formData.get("name") ? (formData.get("name") as string).toString() : ""; const name = formData.get("name") ? (formData.get("name") as string).toString() : "";
if (!name) { if (!name) {
toast({ toast({
title: __("Error"), title: t("common.error"), description: t("newOrganizationPage.errors.nameRequired"),
description: __("Name is required"),
variant: "error", variant: "error",
}); });
return; return;
@@ -77,8 +76,7 @@ function NewOrganizationPageInner() {
onCompleted: (r, e) => { onCompleted: (r, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Error"), title: t("common.error"), description: formatError(t("newOrganizationPage.errors.create"), e),
description: formatError(__("Failed to create organization"), e),
variant: "error", variant: "error",
}); });
return; return;
@@ -87,14 +85,13 @@ function NewOrganizationPageInner() {
const org = r.createOrganization!.organization; const org = r.createOrganization!.organization;
void navigate(`/organizations/${org!.id}`); void navigate(`/organizations/${org!.id}`);
toast({ toast({
title: __("Success"), title: t("common.success"), description: t("newOrganizationPage.messages.created"),
description: __("Organization has been created successfully"),
variant: "success", variant: "success",
}); });
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -109,34 +106,30 @@ function NewOrganizationPageInner() {
className="mb-4 inline-flex gap-2 items-center" className="mb-4 inline-flex gap-2 items-center"
> >
<IconChevronLeft size={16} /> <IconChevronLeft size={16} />
{__("Back")} {t("newOrganizationPage.actions.back")}
</Link> </Link>
<PageHeader <PageHeader
title={__("Create Organization")} title={t("newOrganizationPage.title")}
description={__( description={t("newOrganizationPage.description")}
"Create a new organization to manage your compliance and security needs.",
)}
/> />
<Card padded asChild> <Card padded asChild>
<form onSubmit={e => void handleSubmit(e)} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<h2 className="text-xl font-semibold mb-1"> <h2 className="text-xl font-semibold mb-1">
{__("Organization Details")} {t("newOrganizationPage.details")}
</h2> </h2>
<p className="text-txt-tertiary text-sm mb-4"> <p className="text-txt-tertiary text-sm mb-4">
{__("Enter the basic information about your organization.")} {t("newOrganizationPage.detailsDescription")}
</p> </p>
<Field <Field
required required
name="name" name="name"
type="text" type="text"
placeholder={__("Organization name")} placeholder={t("newOrganizationPage.fields.name")}
label={__("Organization name")} label={t("newOrganizationPage.fields.name")}
help={__( help={t("newOrganizationPage.fields.nameHelp")}
"The name of your organization as it will appear throughout the platform.",
)}
/> />
<Button disabled={isCreating} type="submit" className="w-full"> <Button disabled={isCreating} type="submit" className="w-full">
{__("Create Organization")} {t("newOrganizationPage.actions.create")}
</Button> </Button>
</form> </form>
</Card> </Card>

View File

@@ -18,7 +18,6 @@
// 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 { import {
Button, Button,
Dropdown, Dropdown,
@@ -30,6 +29,7 @@ import {
Input, Input,
} from "@probo/ui"; } from "@probo/ui";
import { Suspense, useCallback, useState } from "react"; import { Suspense, useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { useFragment, useQueryLoader } from "react-relay"; import { useFragment, useQueryLoader } from "react-relay";
import { Link, useLocation } from "react-router"; import { Link, useLocation } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -54,7 +54,7 @@ export function MembershipsDropdown(props: {
const { organizationFKey } = props; const { organizationFKey } = props;
const location = useLocation(); const location = useLocation();
const { __ } = useTranslate(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const currentOrganization const currentOrganization
@@ -90,7 +90,7 @@ export function MembershipsDropdown(props: {
<div className="px-3 py-2"> <div className="px-3 py-2">
<Input <Input
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
placeholder={__("Search organizations...")} placeholder={t("membershipsDropdown.searchPlaceholder")}
value={search} value={search}
onValueChange={setSearch} onValueChange={setSearch}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -104,7 +104,7 @@ export function MembershipsDropdown(props: {
<Suspense <Suspense
fallback={( fallback={(
<div className="px-3 py-2 text-gray-500"> <div className="px-3 py-2 text-gray-500">
{__("Loading organizations...")} {t("membershipsDropdown.loading")}
</div> </div>
)} )}
> >
@@ -116,7 +116,7 @@ export function MembershipsDropdown(props: {
<DropdownItem asChild> <DropdownItem asChild>
<Link to="/organizations/new" state={{ from: location.pathname }}> <Link to="/organizations/new" state={{ from: location.pathname }}>
<IconPlusLarge size={16} /> <IconPlusLarge size={16} />
{__("Add organization")} {t("membershipsDropdown.addOrganization")}
</Link> </Link>
</DropdownItem> </DropdownItem>
</Dropdown> </Dropdown>

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 { useTranslate } from "@probo/i18n";
import { IconMail } from "@probo/ui"; import { IconMail } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -35,7 +35,7 @@ export function MembershipsDropdownInvitingItem(props: {
fKey: MembershipsDropdownInvitingItemFragment$key; fKey: MembershipsDropdownInvitingItemFragment$key;
}) { }) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const organization = useFragment<MembershipsDropdownInvitingItemFragment$key>( const organization = useFragment<MembershipsDropdownInvitingItemFragment$key>(
fragment, fragment,
@@ -45,7 +45,7 @@ export function MembershipsDropdownInvitingItem(props: {
return ( return (
<div <div
className="text-txt-primary flex items-center gap-2 p-2 cursor-default" className="text-txt-primary flex items-center gap-2 p-2 cursor-default"
title={__("Check your email to accept the invitation")} title={t("membershipsDropdownInvitingItem.title")}
> >
<div className="bg-border-mid text-txt-invert! rounded-full size-6 flex items-center justify-center flex-none"> <div className="bg-border-mid text-txt-invert! rounded-full size-6 flex items-center justify-center flex-none">
<IconMail size={16} /> <IconMail size={16} />

View File

@@ -18,9 +18,9 @@
// 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 { DropdownSeparator } from "@probo/ui"; import { DropdownSeparator } from "@probo/ui";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { graphql, type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { MembershipsDropdownMenuQuery } from "#/__generated__/iam/MembershipsDropdownMenuQuery.graphql"; import type { MembershipsDropdownMenuQuery } from "#/__generated__/iam/MembershipsDropdownMenuQuery.graphql";
@@ -65,7 +65,7 @@ interface MembershipsDropdownMenuProps {
export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) { export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
const { queryRef, search } = props; const { queryRef, search } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const { const {
viewer: { viewer: {
@@ -102,7 +102,7 @@ export function MembershipsDropdownMenu(props: MembershipsDropdownMenuProps) {
{invitingOrganizations.length > 0 && ( {invitingOrganizations.length > 0 && (
<> <>
<div className="px-3 py-1 text-xs text-txt-tertiary uppercase"> <div className="px-3 py-1 text-xs text-txt-tertiary uppercase">
{__("Pending invitations")} {t("membershipsDropdownMenu.pendingInvitations")}
</div> </div>
{invitingOrganizations.map(organization => ( {invitingOrganizations.map(organization => (
<MembershipsDropdownInvitingItem key={organization.id} fKey={organization} /> <MembershipsDropdownInvitingItem key={organization.id} fKey={organization} />

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { CookieIcon, LaptopIcon } from "@phosphor-icons/react"; import { CookieIcon, LaptopIcon } from "@phosphor-icons/react";
import { useTranslate } from "@probo/i18n";
import { import {
IconBank, IconBank,
IconBook, IconBook,
@@ -42,6 +41,7 @@ import {
IconTodo, IconTodo,
SidebarItem, SidebarItem,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -84,7 +84,7 @@ const fragment = graphql`
export function Sidebar(props: { fKey: SidebarFragment$key }) { export function Sidebar(props: { fKey: SidebarFragment$key }) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const organization = useFragment<SidebarFragment$key>(fragment, fKey); const organization = useFragment<SidebarFragment$key>(fragment, fKey);
@@ -95,28 +95,28 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
<ul className="space-y-[2px]"> <ul className="space-y-[2px]">
{organization.canGetContext && ( {organization.canGetContext && (
<SidebarItem <SidebarItem
label={__("Context")} label={t("sidebar.context")}
icon={IconPageTextSolid} icon={IconPageTextSolid}
to={`${prefix}/context`} to={`${prefix}/context`}
/> />
)} )}
{organization.canListTasks && ( {organization.canListTasks && (
<SidebarItem <SidebarItem
label={__("Tasks")} label={t("sidebar.tasks")}
icon={IconInboxEmpty} icon={IconInboxEmpty}
to={`${prefix}/tasks`} to={`${prefix}/tasks`}
/> />
)} )}
{organization.canListMeasures && ( {organization.canListMeasures && (
<SidebarItem <SidebarItem
label={__("Measures")} label={t("sidebar.measures")}
icon={IconTodo} icon={IconTodo}
to={`${prefix}/measures`} to={`${prefix}/measures`}
/> />
)} )}
{organization.canListRisks && ( {organization.canListRisks && (
<SidebarItem <SidebarItem
label={__("Risks")} label={t("sidebar.risks")}
icon={IconFire3} icon={IconFire3}
to={`${prefix}/risks`} to={`${prefix}/risks`}
/> />
@@ -124,119 +124,119 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
{organization.canListFrameworks && ( {organization.canListFrameworks && (
<SidebarItem <SidebarItem
label={__("Frameworks")} label={t("sidebar.frameworks")}
icon={IconBank} icon={IconBank}
to={`${prefix}/frameworks`} to={`${prefix}/frameworks`}
/> />
)} )}
{organization.canListMembers && ( {organization.canListMembers && (
<SidebarItem <SidebarItem
label={__("People")} label={t("sidebar.people")}
icon={IconGroup1} icon={IconGroup1}
to={`${prefix}/people`} to={`${prefix}/people`}
/> />
)} )}
{organization.canListThirdParties && ( {organization.canListThirdParties && (
<SidebarItem <SidebarItem
label={__("Third parties")} label={t("sidebar.thirdParties")}
icon={IconStore} icon={IconStore}
to={`${prefix}/third-parties`} to={`${prefix}/third-parties`}
/> />
)} )}
{organization.canListDocuments && ( {organization.canListDocuments && (
<SidebarItem <SidebarItem
label={__("Documents")} label={t("sidebar.documents")}
icon={IconPageTextLine} icon={IconPageTextLine}
to={`${prefix}/documents`} to={`${prefix}/documents`}
/> />
)} )}
{organization.canListAssets && ( {organization.canListAssets && (
<SidebarItem <SidebarItem
label={__("Assets")} label={t("sidebar.assets")}
icon={IconBox} icon={IconBox}
to={`${prefix}/assets`} to={`${prefix}/assets`}
/> />
)} )}
{organization.canListDevices && ( {organization.canListDevices && (
<SidebarItem <SidebarItem
label={__("Devices")} label={t("sidebar.devices")}
icon={LaptopIcon} icon={LaptopIcon}
to={`${prefix}/devices`} to={`${prefix}/devices`}
/> />
)} )}
{organization.canListData && ( {organization.canListData && (
<SidebarItem <SidebarItem
label={__("Data")} label={t("sidebar.data")}
icon={IconListStack} icon={IconListStack}
to={`${prefix}/data`} to={`${prefix}/data`}
/> />
)} )}
{organization.canListAudits && ( {organization.canListAudits && (
<SidebarItem <SidebarItem
label={__("Audits")} label={t("sidebar.audits")}
icon={IconMedal} icon={IconMedal}
to={`${prefix}/audits`} to={`${prefix}/audits`}
/> />
)} )}
{organization.canListFindings && ( {organization.canListFindings && (
<SidebarItem <SidebarItem
label={__("Findings")} label={t("sidebar.findings")}
icon={IconMagnifyingGlass} icon={IconMagnifyingGlass}
to={`${prefix}/findings`} to={`${prefix}/findings`}
/> />
)} )}
{organization.canListObligations && ( {organization.canListObligations && (
<SidebarItem <SidebarItem
label={__("Obligations")} label={t("sidebar.obligations")}
icon={IconBook} icon={IconBook}
to={`${prefix}/obligations`} to={`${prefix}/obligations`}
/> />
)} )}
{organization.canListProcessingActivities && ( {organization.canListProcessingActivities && (
<SidebarItem <SidebarItem
label={__("Processing Activities")} label={t("sidebar.processingActivities")}
icon={IconCircleProgress} icon={IconCircleProgress}
to={`${prefix}/processing-activities`} to={`${prefix}/processing-activities`}
/> />
)} )}
{organization.canListStatementsOfApplicability && ( {organization.canListStatementsOfApplicability && (
<SidebarItem <SidebarItem
label={__("Statements of Applicability")} label={t("sidebar.statementsOfApplicability")}
icon={IconPageCheck} icon={IconPageCheck}
to={`${prefix}/statements-of-applicability`} to={`${prefix}/statements-of-applicability`}
/> />
)} )}
{organization.canListRightsRequests && ( {organization.canListRightsRequests && (
<SidebarItem <SidebarItem
label={__("Rights Requests")} label={t("sidebar.rightsRequests")}
icon={IconLock} icon={IconLock}
to={`${prefix}/rights-requests`} to={`${prefix}/rights-requests`}
/> />
)} )}
{organization.canListAccessReviewCampaigns && ( {organization.canListAccessReviewCampaigns && (
<SidebarItem <SidebarItem
label={__("Access Reviews")} label={t("sidebar.accessReviews")}
icon={IconKey} icon={IconKey}
to={`${prefix}/access-reviews`} to={`${prefix}/access-reviews`}
/> />
)} )}
{organization.canGetCompliancePage && ( {organization.canGetCompliancePage && (
<SidebarItem <SidebarItem
label={__("Compliance Page")} label={t("sidebar.compliancePage")}
icon={IconShield} icon={IconShield}
to={`${prefix}/compliance-page`} to={`${prefix}/compliance-page`}
/> />
)} )}
{organization.canListCookieBanners && ( {organization.canListCookieBanners && (
<SidebarItem <SidebarItem
label={__("Cookie Banners")} label={t("sidebar.cookieBanners")}
icon={CookieIcon} icon={CookieIcon}
to={`${prefix}/cookie-banners`} to={`${prefix}/cookie-banners`}
/> />
)} )}
{organization.canUpdateOrganization && ( {organization.canUpdateOrganization && (
<SidebarItem <SidebarItem
label={__("Settings")} label={t("sidebar.settings")}
icon={IconSettingsGear2} icon={IconSettingsGear2}
to={`${prefix}/settings`} to={`${prefix}/settings`}
/> />

View File

@@ -19,7 +19,6 @@
// SOFTWARE. // SOFTWARE.
import { formatError } from "@probo/helpers"; import { formatError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { import {
DropdownSeparator, DropdownSeparator,
IconArrowBoxLeft, IconArrowBoxLeft,
@@ -30,6 +29,7 @@ import {
UserDropdownItem, UserDropdownItem,
useToast, useToast,
} from "@probo/ui"; } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { useFragment, useMutation } from "react-relay"; import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -65,7 +65,7 @@ export function ViewerMembershipDropdown(props: {
}) { }) {
const { fKey } = props; const { fKey } = props;
const { __ } = useTranslate(); const { t } = useTranslation();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { toast } = useToast(); const { toast } = useToast();
@@ -85,8 +85,8 @@ export function ViewerMembershipDropdown(props: {
onCompleted: (_, e) => { onCompleted: (_, e) => {
if (e) { if (e) {
toast({ toast({
title: __("Request failed"), title: t("viewerMembershipDropdown.errors.requestFailed"),
description: formatError(__("Cannot sign out"), e), description: formatError(t("viewerMembershipDropdown.errors.cannotSignOut"), e),
variant: "error", variant: "error",
}); });
return; return;
@@ -95,7 +95,7 @@ export function ViewerMembershipDropdown(props: {
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
title: __("Error"), title: t("common.error"),
description: e.message, description: e.message,
variant: "error", variant: "error",
}); });
@@ -109,32 +109,32 @@ export function ViewerMembershipDropdown(props: {
<UserDropdownItem <UserDropdownItem
to="/me/api-keys" to="/me/api-keys"
icon={IconKey} icon={IconKey}
label={__("API Keys")} label={t("viewerMembershipDropdown.actions.apiKeys")}
/> />
)} )}
{canListOAuth2AccessTokens && ( {canListOAuth2AccessTokens && (
<UserDropdownItem <UserDropdownItem
to="/me/oauth-tokens" to="/me/oauth-tokens"
icon={IconKey} icon={IconKey}
label={__("OAuth tokens")} label={t("viewerMembershipDropdown.actions.oauthTokens")}
/> />
)} )}
<UserDropdownItem <UserDropdownItem
to={`/organizations/${organizationId}/employee`} to={`/organizations/${organizationId}/employee`}
icon={IconPageTextLine} icon={IconPageTextLine}
label={__("Employee Portal")} label={t("viewerMembershipDropdown.actions.employeePortal")}
/> />
<UserDropdownItem <UserDropdownItem
to="mailto:support@probo.com" to="mailto:support@probo.com"
icon={IconCircleQuestionmark} icon={IconCircleQuestionmark}
label={__("Help")} label={t("viewerMembershipDropdown.actions.help")}
/> />
<DropdownSeparator /> <DropdownSeparator />
<UserDropdownItem <UserDropdownItem
variant="danger" variant="danger"
to="/logout" to="/logout"
icon={IconArrowBoxLeft} icon={IconArrowBoxLeft}
label="Logout" label={t("viewerMembershipDropdown.actions.logout")}
onClick={handleLogout} onClick={handleLogout}
/> />
</UserDropdown> </UserDropdown>

View File

@@ -18,9 +18,9 @@
// 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 { Button, PageHeader } from "@probo/ui"; import { Button, PageHeader } from "@probo/ui";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { ConnectionHandler, type DataID, graphql } from "relay-runtime"; import { ConnectionHandler, type DataID, graphql } from "relay-runtime";
@@ -49,7 +49,7 @@ export function PeoplePage(props: {
const { queryRef } = props; const { queryRef } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
const [connectionId, setConnectionId] = useState<DataID>( const [connectionId, setConnectionId] = useState<DataID>(
ConnectionHandler.getConnectionID( ConnectionHandler.getConnectionID(
@@ -69,11 +69,11 @@ export function PeoplePage(props: {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader title={__("People")}> <PageHeader title={t("peoplePage.title")}>
{organization.canCreateUser {organization.canCreateUser
&& ( && (
<AddPersonDialog connectionId={connectionId}> <AddPersonDialog connectionId={connectionId}>
<Button variant="secondary">{__("Add Person")}</Button> <Button variant="secondary">{t("peoplePage.actions.add")}</Button>
</AddPersonDialog> </AddPersonDialog>
)} )}
</PageHeader> </PageHeader>

View File

@@ -18,9 +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 { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconArchive, IconTrashCan, useConfirm } from "@probo/ui"; import { ActionDropdown, Avatar, Badge, Breadcrumb, Card, DropdownItem, IconArchive, IconTrashCan, useConfirm } from "@probo/ui";
import { useTranslation } from "react-i18next";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
@@ -73,7 +72,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
const { queryRef } = props; const { queryRef } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
const confirm = useConfirm(); const confirm = useConfirm();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -85,15 +84,15 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
const [archiveUser, isArchiving] = useMutationWithToasts( const [archiveUser, isArchiving] = useMutationWithToasts(
archiveUserMutation, archiveUserMutation,
{ {
successMessage: __("Person archived successfully"), successMessage: t("personPage.messages.archived"),
errorMessage: __("Failed to archive person"), errorMessage: t("personPage.errors.archive"),
}, },
); );
const [removeUser, isRemoving] = useMutationWithToasts( const [removeUser, isRemoving] = useMutationWithToasts(
removeUserMutation, removeUserMutation,
{ {
successMessage: __("Person removed successfully"), successMessage: t("personPage.messages.removed"),
errorMessage: __("Failed to remove person"), errorMessage: t("personPage.errors.remove"),
}, },
); );
const isMutating = isArchiving || isRemoving; const isMutating = isArchiving || isRemoving;
@@ -114,10 +113,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
}); });
}, },
{ {
message: sprintf( message: t("personPage.confirmations.archive", { name: person.fullName }),
__("Are you sure you want to archive %s?"),
person.fullName,
),
}, },
); );
}; };
@@ -138,10 +134,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
}); });
}, },
{ {
message: sprintf( message: t("personPage.confirmations.remove", { name: person.fullName }),
__("Are you sure you want to remove %s?"),
person.fullName,
),
}, },
); );
}; };
@@ -154,7 +147,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: __("People"), label: t("personPage.breadcrumb.people"),
to: `/organizations/${organizationId}/people`, to: `/organizations/${organizationId}/people`,
}, },
{ {
@@ -181,7 +174,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
onClick={handleArchive} onClick={handleArchive}
disabled={isMutating} disabled={isMutating}
> >
{__("Archive")} {t("personPage.actions.archive")}
</DropdownItem> </DropdownItem>
)} )}
{canRemove && ( {canRemove && (
@@ -191,7 +184,7 @@ export function PersonPage(props: { queryRef: PreloadedQuery<PersonPageQuery> })
onClick={handleRemove} onClick={handleRemove}
disabled={isMutating} disabled={isMutating}
> >
{__("Remove")} {t("personPage.actions.remove")}
</DropdownItem> </DropdownItem>
)} )}
</ActionDropdown> </ActionDropdown>

View File

@@ -18,9 +18,9 @@
// 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 { Dialog, DialogContent, useDialogRef } from "@probo/ui"; import { Dialog, DialogContent, useDialogRef } from "@probo/ui";
import { type PropsWithChildren } from "react"; import { type PropsWithChildren } from "react";
import { useTranslation } from "react-i18next";
import type { DataID } from "relay-runtime"; import type { DataID } from "relay-runtime";
import { PersonForm } from "./PersonForm"; import { PersonForm } from "./PersonForm";
@@ -30,11 +30,11 @@ export function AddPersonDialog(props: PropsWithChildren<{
}>) { }>) {
const { children, connectionId } = props; const { children, connectionId } = props;
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { __ } = useTranslate(); const { t } = useTranslation();
return ( return (
<Dialog <Dialog
title={__("Add Person")} title={t("addPersonDialog.title")}
trigger={children} trigger={children}
className="max-w-xl" className="max-w-xl"
ref={dialogRef} ref={dialogRef}

View File

@@ -19,10 +19,10 @@
// SOFTWARE. // SOFTWARE.
import { getAssignableRoles } from "@probo/helpers"; import { getAssignableRoles } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Tbody, Td, Th, Thead, Tr } from "@probo/ui"; import { Tbody, Td, Th, Thead, Tr } from "@probo/ui";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { use } from "react"; import { use } from "react";
import { useTranslation } from "react-i18next";
import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay"; import { ConnectionHandler, graphql, usePaginationFragment } from "react-relay";
import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql"; import type { PeopleListFragment$key } from "#/__generated__/iam/PeopleListFragment.graphql";
@@ -72,7 +72,7 @@ export function PeopleList(props: {
const { fKey, onConnectionIdChange } = props; const { fKey, onConnectionIdChange } = props;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { t } = useTranslation();
const { role } = use(CurrentUser); const { role } = use(CurrentUser);
const canManageRoles = getAssignableRoles(role).length > 0; const canManageRoles = getAssignableRoles(role).length > 0;
@@ -107,11 +107,11 @@ export function PeopleList(props: {
> >
<Thead> <Thead>
<Tr> <Tr>
<SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{__("Name")}</SortableTh> <SortableTh field="FULL_NAME" onOrderChange={handleOrderChange}>{t("peopleList.columns.name")}</SortableTh>
<SortableTh field="STATE">{__("Status")}</SortableTh> <SortableTh field="STATE">{t("peopleList.columns.status")}</SortableTh>
<SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{__("Email")}</SortableTh> <SortableTh field="EMAIL_ADDRESS" onOrderChange={handleOrderChange}>{t("peopleList.columns.email")}</SortableTh>
{canManageRoles && <Th>{__("Role")}</Th>} {canManageRoles && <Th>{t("peopleList.columns.role")}</Th>}
<SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{__("Created on")}</SortableTh> <SortableTh field="CREATED_AT" onOrderChange={handleOrderChange}>{t("peopleList.columns.createdOn")}</SortableTh>
<Th></Th> <Th></Th>
</Tr> </Tr>
</Thead> </Thead>
@@ -120,7 +120,7 @@ export function PeopleList(props: {
? ( ? (
<Tr> <Tr>
<Td colSpan={7} className="text-center text-txt-secondary"> <Td colSpan={7} className="text-center text-txt-secondary">
{__("No people")} {t("peopleList.empty")}
</Td> </Td>
</Tr> </Tr>
) )

Some files were not shown because too many files have changed in this diff Show More