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