Translate the member page

This commit is contained in:
Owen
2026-05-06 10:26:20 -07:00
parent fab53ba26a
commit 780feba19c
2 changed files with 154 additions and 61 deletions

View File

@@ -3208,5 +3208,48 @@
"domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.", "domainPickerWildcardCertWarning": "Wildcard resources may require additional configuration to work properly.",
"domainPickerWildcardCertWarningLink": "Learn more", "domainPickerWildcardCertWarningLink": "Learn more",
"health": "Health", "health": "Health",
"domainPendingErrorTitle": "Verification Issue" "domainPendingErrorTitle": "Verification Issue",
"memberPortalTitle": "Resources",
"memberPortalDescription": "Resources you have access to in this organization",
"memberPortalSortBy": "Sort by...",
"memberPortalSortNameAsc": "Name A-Z",
"memberPortalSortNameDesc": "Name Z-A",
"memberPortalSortDomainAsc": "Domain A-Z",
"memberPortalSortDomainDesc": "Domain Z-A",
"memberPortalSortEnabledFirst": "Enabled First",
"memberPortalSortDisabledFirst": "Disabled First",
"memberPortalRefresh": "Refresh",
"memberPortalRefreshResources": "Refresh Resources",
"memberPortalFailedToLoad": "Failed to load resources",
"memberPortalFailedToLoadDescription": "Failed to load resources. Please check your connection and try again.",
"memberPortalUnableToLoad": "Unable to Load Resources",
"memberPortalTryAgain": "Try Again",
"memberPortalNoResourcesFound": "No Resources Found",
"memberPortalNoResourcesAvailable": "No Resources Available",
"memberPortalNoResourcesMatchSearch": "No resources match \"{query}\". Try adjusting your search terms or clearing the search to see all resources.",
"memberPortalNoResourcesAccess": "You don't have access to any resources yet. Contact your administrator to get access to resources you need.",
"memberPortalClearSearch": "Clear Search",
"memberPortalPublicResources": "Public Resources",
"memberPortalPublicResourcesDescription": "Web applications and services accessible via browser",
"memberPortalCopiedToClipboard": "Copied to clipboard",
"memberPortalCopiedUrlDescription": "Resource URL has been copied to your clipboard.",
"memberPortalOpenResource": "Open Resource",
"memberPortalPrivateResources": "Private Resources",
"memberPortalPrivateResourcesDescription": "Internal network resources accessible via client",
"memberPortalResourceDetails": "Resource Details",
"memberPortalMode": "Mode",
"memberPortalDestination": "Destination",
"memberPortalAlias": "Alias",
"memberPortalCopiedAliasDescription": "Resource alias has been copied to your clipboard.",
"memberPortalCopiedDestinationDescription": "Resource destination has been copied to your clipboard.",
"memberPortalRequiresClientConnection": "Requires Client Connection",
"memberPortalAuthMethods": "Authentication Methods",
"memberPortalSso": "Single Sign-On (SSO)",
"memberPortalPasswordProtected": "Password Protected",
"memberPortalPinCode": "PIN Code",
"memberPortalEmailWhitelist": "Email Whitelist",
"memberPortalResourceDisabled": "Resource Disabled",
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
"memberPortalPrevious": "Previous",
"memberPortalNext": "Next"
} }

View File

@@ -123,6 +123,7 @@ const ResourceFavicon = ({
// Resource Info component // Resource Info component
const ResourceInfo = ({ resource }: { resource: Resource }) => { const ResourceInfo = ({ resource }: { resource: Resource }) => {
const t = useTranslations();
const hasAuthMethods = const hasAuthMethods =
resource.sso || resource.sso ||
resource.password || resource.password ||
@@ -141,7 +142,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
{/* Site Information */} {/* Site Information */}
{resource.siteName && ( {resource.siteName && (
<div> <div>
<div className="text-xs font-medium mb-1.5">Site</div> <div className="text-xs font-medium mb-1.5">
{t("site")}
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Combine className="h-4 w-4 text-foreground shrink-0" /> <Combine className="h-4 w-4 text-foreground shrink-0" />
<span className="text-sm">{resource.siteName}</span> <span className="text-sm">{resource.siteName}</span>
@@ -157,7 +160,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
} }
> >
<div className="text-xs font-medium mb-1.5"> <div className="text-xs font-medium mb-1.5">
Authentication Methods {t("memberPortalAuthMethods")}
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
{resource.sso && ( {resource.sso && (
@@ -166,7 +169,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" /> <Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
</div> </div>
<span className="text-sm"> <span className="text-sm">
Single Sign-On (SSO) {t("memberPortalSso")}
</span> </span>
</div> </div>
)} )}
@@ -176,7 +179,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" /> <KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
</div> </div>
<span className="text-sm"> <span className="text-sm">
Password Protected {t("memberPortalPasswordProtected")}
</span> </span>
</div> </div>
)} )}
@@ -185,7 +188,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50"> <div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" /> <Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
</div> </div>
<span className="text-sm">PIN Code</span> <span className="text-sm">
{t("memberPortalPinCode")}
</span>
</div> </div>
)} )}
{resource.whitelist && ( {resource.whitelist && (
@@ -193,7 +198,9 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50"> <div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" /> <AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
</div> </div>
<span className="text-sm">Email Whitelist</span> <span className="text-sm">
{t("memberPortalEmailWhitelist")}
</span>
</div> </div>
)} )}
</div> </div>
@@ -208,7 +215,7 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-destructive shrink-0" /> <AlertCircle className="h-4 w-4 text-destructive shrink-0" />
<span className="text-sm text-destructive"> <span className="text-sm text-destructive">
Resource Disabled {t("memberPortalResourceDisabled")}
</span> </span>
</div> </div>
</div> </div>
@@ -233,6 +240,7 @@ const PaginationControls = ({
totalItems: number; totalItems: number;
itemsPerPage: number; itemsPerPage: number;
}) => { }) => {
const t = useTranslations();
const startItem = (currentPage - 1) * itemsPerPage + 1; const startItem = (currentPage - 1) * itemsPerPage + 1;
const endItem = Math.min(currentPage * itemsPerPage, totalItems); const endItem = Math.min(currentPage * itemsPerPage, totalItems);
@@ -241,7 +249,11 @@ const PaginationControls = ({
return ( return (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8"> <div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-8">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
Showing {startItem}-{endItem} of {totalItems} resources {t("memberPortalShowingResources", {
start: startItem,
end: endItem,
total: totalItems
})}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -253,7 +265,7 @@ const PaginationControls = ({
className="gap-1" className="gap-1"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
Previous {t("memberPortalPrevious")}
</Button> </Button>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -309,7 +321,7 @@ const PaginationControls = ({
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
className="gap-1" className="gap-1"
> >
Next {t("memberPortalNext")}
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Button> </Button>
</div> </div>
@@ -389,13 +401,11 @@ export default function MemberResourcesPortal({
response.data.data.siteResources || [] response.data.data.siteResources || []
); );
} else { } else {
setError("Failed to load resources"); setError(t("memberPortalFailedToLoad"));
} }
} catch (err) { } catch (err) {
console.error("Error fetching user resources:", err); console.error("Error fetching user resources:", err);
setError( setError(t("memberPortalFailedToLoadDescription"));
"Failed to load resources. Please check your connection and try again."
);
} finally { } finally {
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
@@ -526,8 +536,8 @@ export default function MemberResourcesPortal({
return ( return (
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
<SettingsSectionTitle <SettingsSectionTitle
title="Resources" title={t("memberPortalTitle")}
description="Resources you have access to in this organization" description={t("memberPortalDescription")}
/> />
{/* Search and Sort Controls - Skeleton */} {/* Search and Sort Controls - Skeleton */}
@@ -554,8 +564,8 @@ export default function MemberResourcesPortal({
return ( return (
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
<SettingsSectionTitle <SettingsSectionTitle
title="Resources" title={t("memberPortalTitle")}
description="Resources you have access to in this organization" description={t("memberPortalDescription")}
/> />
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-20 text-center"> <CardContent className="flex flex-col items-center justify-center py-20 text-center">
@@ -563,7 +573,7 @@ export default function MemberResourcesPortal({
<AlertCircle className="h-16 w-16 text-destructive/60" /> <AlertCircle className="h-16 w-16 text-destructive/60" />
</div> </div>
<h3 className="text-xl font-semibold text-foreground mb-3"> <h3 className="text-xl font-semibold text-foreground mb-3">
Unable to Load Resources {t("memberPortalUnableToLoad")}
</h3> </h3>
<p className="text-muted-foreground max-w-lg text-base mb-6"> <p className="text-muted-foreground max-w-lg text-base mb-6">
{error} {error}
@@ -574,7 +584,7 @@ export default function MemberResourcesPortal({
className="gap-2" className="gap-2"
> >
<RefreshCw className="h-4 w-4" /> <RefreshCw className="h-4 w-4" />
Try Again {t("memberPortalTryAgain")}
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
@@ -585,8 +595,8 @@ export default function MemberResourcesPortal({
return ( return (
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
<SettingsSectionTitle <SettingsSectionTitle
title="Resources" title={t("memberPortalTitle")}
description="Resources you have access to in this organization" description={t("memberPortalDescription")}
/> />
{/* Search and Sort Controls with Refresh */} {/* Search and Sort Controls with Refresh */}
@@ -595,7 +605,7 @@ export default function MemberResourcesPortal({
{/* Search */} {/* Search */}
<div className="relative w-full sm:w-80"> <div className="relative w-full sm:w-80">
<Input <Input
placeholder="Search resources..." placeholder={t("resourcesSearch")}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-8 bg-card" className="w-full pl-8 bg-card"
@@ -607,26 +617,28 @@ export default function MemberResourcesPortal({
<div className="w-full sm:w-36"> <div className="w-full sm:w-36">
<Select value={sortBy} onValueChange={setSortBy}> <Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="bg-card"> <SelectTrigger className="bg-card">
<SelectValue placeholder="Sort by..." /> <SelectValue
placeholder={t("memberPortalSortBy")}
/>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="name-asc"> <SelectItem value="name-asc">
Name A-Z {t("memberPortalSortNameAsc")}
</SelectItem> </SelectItem>
<SelectItem value="name-desc"> <SelectItem value="name-desc">
Name Z-A {t("memberPortalSortNameDesc")}
</SelectItem> </SelectItem>
<SelectItem value="domain-asc"> <SelectItem value="domain-asc">
Domain A-Z {t("memberPortalSortDomainAsc")}
</SelectItem> </SelectItem>
<SelectItem value="domain-desc"> <SelectItem value="domain-desc">
Domain Z-A {t("memberPortalSortDomainDesc")}
</SelectItem> </SelectItem>
<SelectItem value="status-enabled"> <SelectItem value="status-enabled">
Enabled First {t("memberPortalSortEnabledFirst")}
</SelectItem> </SelectItem>
<SelectItem value="status-disabled"> <SelectItem value="status-disabled">
Disabled First {t("memberPortalSortDisabledFirst")}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -644,7 +656,7 @@ export default function MemberResourcesPortal({
<RefreshCw <RefreshCw
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
/> />
Refresh {t("memberPortalRefresh")}
</Button> </Button>
</div> </div>
@@ -663,13 +675,15 @@ export default function MemberResourcesPortal({
</div> </div>
<h3 className="text-2xl font-semibold text-foreground mb-3"> <h3 className="text-2xl font-semibold text-foreground mb-3">
{searchQuery {searchQuery
? "No Resources Found" ? t("memberPortalNoResourcesFound")
: "No Resources Available"} : t("memberPortalNoResourcesAvailable")}
</h3> </h3>
<p className="text-muted-foreground max-w-lg text-base mb-6"> <p className="text-muted-foreground max-w-lg text-base mb-6">
{searchQuery {searchQuery
? `No resources match "${searchQuery}". Try adjusting your search terms or clearing the search to see all resources.` ? t("memberPortalNoResourcesMatchSearch", {
: "You don't have access to any resources yet. Contact your administrator to get access to resources you need."} query: searchQuery
})
: t("memberPortalNoResourcesAccess")}
</p> </p>
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-3">
{searchQuery ? ( {searchQuery ? (
@@ -678,7 +692,7 @@ export default function MemberResourcesPortal({
variant="outline" variant="outline"
className="gap-2" className="gap-2"
> >
Clear Search {t("memberPortalClearSearch")}
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -690,7 +704,7 @@ export default function MemberResourcesPortal({
<RefreshCw <RefreshCw
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
/> />
Refresh Resources {t("memberPortalRefreshResources")}
</Button> </Button>
)} )}
</div> </div>
@@ -704,11 +718,12 @@ export default function MemberResourcesPortal({
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2"> <h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
<Globe className="h-5 w-5" /> <Globe className="h-5 w-5" />
Public Resources {t("memberPortalPublicResources")}
</h3> </h3>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Web applications and services accessible via {t(
browser "memberPortalPublicResourcesDescription"
)}
</p> </p>
</div> </div>
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8"> <div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
@@ -768,9 +783,12 @@ export default function MemberResourcesPortal({
resource.domain resource.domain
); );
toast({ toast({
title: "Copied to clipboard", title: t(
description: "memberPortalCopiedToClipboard"
"Resource URL has been copied to your clipboard.", ),
description: t(
"memberPortalCopiedUrlDescription"
),
duration: 2000 duration: 2000
}); });
}} }}
@@ -791,7 +809,7 @@ export default function MemberResourcesPortal({
disabled={!resource.enabled} disabled={!resource.enabled}
> >
<ExternalLink className="h-3.5 w-3.5 mr-2" /> <ExternalLink className="h-3.5 w-3.5 mr-2" />
Open Resource {t("memberPortalOpenResource")}
</Button> </Button>
</div> </div>
</Card> </Card>
@@ -806,11 +824,12 @@ export default function MemberResourcesPortal({
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2"> <h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
<Combine className="h-5 w-5" /> <Combine className="h-5 w-5" />
Private Resources {t("memberPortalPrivateResources")}
</h3> </h3>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Internal network resources accessible via {t(
client "memberPortalPrivateResourcesDescription"
)}
</p> </p>
</div> </div>
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8"> <div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr mb-8">
@@ -843,11 +862,16 @@ export default function MemberResourcesPortal({
<InfoPopup> <InfoPopup>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="text-xs font-medium mb-1.5"> <div className="text-xs font-medium mb-1.5">
Resource Details {t(
"memberPortalResourceDetails"
)}
</div> </div>
<div> <div>
<span className="font-medium"> <span className="font-medium">
Mode: {t(
"memberPortalMode"
)}
:
</span> </span>
<span className="ml-2 text-muted-foreground capitalize"> <span className="ml-2 text-muted-foreground capitalize">
{ {
@@ -858,7 +882,10 @@ export default function MemberResourcesPortal({
{siteResource.protocol && ( {siteResource.protocol && (
<div> <div>
<span className="font-medium"> <span className="font-medium">
Protocol: {t(
"protocol"
)}
:
</span> </span>
<span className="ml-2 text-muted-foreground uppercase"> <span className="ml-2 text-muted-foreground uppercase">
{ {
@@ -869,7 +896,10 @@ export default function MemberResourcesPortal({
)} )}
<div> <div>
<span className="font-medium"> <span className="font-medium">
Destination: {t(
"memberPortalDestination"
)}
:
</span> </span>
<span className="ml-2 text-muted-foreground"> <span className="ml-2 text-muted-foreground">
{ {
@@ -880,7 +910,10 @@ export default function MemberResourcesPortal({
{siteResource.alias && ( {siteResource.alias && (
<div> <div>
<span className="font-medium"> <span className="font-medium">
Alias: {t(
"memberPortalAlias"
)}
:
</span> </span>
<span className="ml-2 text-muted-foreground"> <span className="ml-2 text-muted-foreground">
{ {
@@ -891,14 +924,21 @@ export default function MemberResourcesPortal({
)} )}
<div> <div>
<span className="font-medium"> <span className="font-medium">
Status: {t(
"status"
)}
:
</span> </span>
<span <span
className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`} className={`ml-2 ${siteResource.enabled ? "text-green-600" : "text-red-600"}`}
> >
{siteResource.enabled {siteResource.enabled
? "Enabled" ? t(
: "Disabled"} "enabled"
)
: t(
"disabled"
)}
</span> </span>
</div> </div>
</div> </div>
@@ -925,9 +965,13 @@ export default function MemberResourcesPortal({
siteResource.alias! siteResource.alias!
); );
toast({ toast({
title: "Copied to clipboard", title: t(
"memberPortalCopiedToClipboard"
),
description: description:
"Resource alias has been copied to your clipboard.", t(
"memberPortalCopiedAliasDescription"
),
duration: 2000 duration: 2000
}); });
}} }}
@@ -959,9 +1003,13 @@ export default function MemberResourcesPortal({
siteResource.destination siteResource.destination
); );
toast({ toast({
title: "Copied to clipboard", title: t(
"memberPortalCopiedToClipboard"
),
description: description:
"Resource destination has been copied to your clipboard.", t(
"memberPortalCopiedDestinationDescription"
),
duration: 2000 duration: 2000
}); });
}} }}
@@ -976,7 +1024,9 @@ export default function MemberResourcesPortal({
<div className="p-6 pt-0 mt-auto"> <div className="p-6 pt-0 mt-auto">
<div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground"> <div className="flex items-center justify-center py-2 px-4 bg-muted/50 rounded text-sm text-muted-foreground">
<Combine className="h-3.5 w-3.5 mr-2" /> <Combine className="h-3.5 w-3.5 mr-2" />
Requires Client Connection {t(
"memberPortalRequiresClientConnection"
)}
</div> </div>
</div> </div>
</Card> </Card>