complete share link i18n

This commit is contained in:
Lokowitz
2025-05-05 16:10:08 +00:00
parent 29375385c0
commit fa1997adc1
8 changed files with 109 additions and 82 deletions

View File

@@ -77,6 +77,15 @@
"siteRunsInDocker": "Runs in Docker", "siteRunsInDocker": "Runs in Docker",
"siteRunsInShell": "Runs in shell on macOS, Linux, and Windows", "siteRunsInShell": "Runs in shell on macOS, Linux, and Windows",
"siteErrorDelete": "Error deleting site", "siteErrorDelete": "Error deleting site",
"siteErrorUpdate": "Failed to update site",
"siteErrorUpdateDescription": "An error occurred while updating the site.",
"siteUpdated": "Site updated",
"siteUpdatedDescription": "The site has been updated.",
"siteGeneralDescription": "Configure the general settings for this site",
"siteSettingDescription": "Configure the settings on your site",
"siteSetting": "{siteName} Settings",
"siteInfo": "Site Information",
"status": "Status",
"shareTitle": "Manage Share Links", "shareTitle": "Manage Share Links",
"shareDescription": "Create shareable links to grant temporary or permanent access to your resources", "shareDescription": "Create shareable links to grant temporary or permanent access to your resources",
"shareSearch": "Search share links...", "shareSearch": "Search share links...",
@@ -85,6 +94,31 @@
"shareErrorDeleteMessage": "An error occurred deleting link", "shareErrorDeleteMessage": "An error occurred deleting link",
"shareDeleted": "Link deleted", "shareDeleted": "Link deleted",
"shareDeletedDesciption": "The link has been deleted", "shareDeletedDesciption": "The link has been deleted",
"shareTokenDescription": "Your access token can be passed in two ways: as a query parameter or in the request headers. These must be passed from the client on every request for authenticated access.",
"accessToken": "Access Token",
"usageExamples": "Usage Examples",
"tokenId": "Token ID",
"requestHeades": "Request Headers",
"queryParameter": "Query Parameter",
"importantNote": "Important Note",
"shareImportantDescription": "For security reasons, using headers is recommended over query parameters when possible, as query parameters may be logged in server logs or browser history.",
"token": "Token",
"shareTokenSecurety": "Keep your access token secure. Do not share it in publicly accessible areas or client-side code.",
"shareErrorFetchResource": "Failed to fetch resources",
"shareErrorFetchResourceDescription": "An error occurred while fetching the resources",
"shareErrorCreate": "Failed to create share link",
"shareErrorCreateDescription": "An error occurred while creating the share link",
"shareCreateDescription": "Anyone with this link can access the resource",
"shareTitleOptional": "Title (optional)",
"expireIn": "Expire In",
"neverExpire": "Never expire",
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
"shareSeeOnce": "You will only be able to see this linkonce. Make sure to copy it.",
"shareAccessHint": "Anyone with this link can access the resource. Share it with care.",
"shareTokenUsage": "See Access Token Usage",
"createLink": "Create Link",
"resourceNotFound": "No resources found",
"resourceSearch": "Search resources",
"openMenu": "Open menu", "openMenu": "Open menu",
"resource": "Resource", "resource": "Resource",
"title": "Title", "title": "Title",
@@ -94,7 +128,7 @@
"shareErrorSelectResource": "Please select a resource", "shareErrorSelectResource": "Please select a resource",
"resourceTitle": "Manage Resources", "resourceTitle": "Manage Resources",
"resourceDescription": "Create secure proxies to your private applications", "resourceDescription": "Create secure proxies to your private applications",
"resourceSearch": "Search resources...", "resourcesSearch": "Search resources...",
"resourceAdd": "Add Resource", "resourceAdd": "Add Resource",
"resourceErrorDelte": "Error deleting resource", "resourceErrorDelte": "Error deleting resource",
"authentication": "Authentication", "authentication": "Authentication",
@@ -142,6 +176,7 @@
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"general": "General", "general": "General",
"generalSettings": "General Settings",
"proxy": "Proxy", "proxy": "Proxy",
"rules": "Rules", "rules": "Rules",
"resourceSettingDescription": "Configure the settings on your resource", "resourceSettingDescription": "Configure the settings on your resource",
@@ -151,7 +186,7 @@
"orgSettingsDescription": "Configure your organization's general settings", "orgSettingsDescription": "Configure your organization's general settings",
"orgGeneralSettings": "Organization Settings", "orgGeneralSettings": "Organization Settings",
"orgGeneralSettingsDescription": "Manage your organization details and configuration", "orgGeneralSettingsDescription": "Manage your organization details and configuration",
"orgGeneralSave": "Save General Settings", "saveGeneralSettings": "Save General Settings",
"orgDangerZone": "Danger Zone", "orgDangerZone": "Danger Zone",
"orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.", "orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.",
"orgDelete": "Delete Organization", "orgDelete": "Delete Organization",
@@ -186,5 +221,11 @@
"description": "Description", "description": "Description",
"inviteTitle": "Open Invitations", "inviteTitle": "Open Invitations",
"inviteDescription": "Manage your invitations to other users", "inviteDescription": "Manage your invitations to other users",
"inviteSearch": "Search invitations..." "inviteSearch": "Search invitations...",
"minutes": "Minutes",
"hours": "Hours",
"days": "Days",
"weeks": "Weeks",
"months": "Months",
"years": "Years"
} }

View File

@@ -225,7 +225,7 @@ export default function GeneralPage() {
loading={loadingSave} loading={loadingSave}
disabled={loadingSave} disabled={loadingSave}
> >
{t('orgGeneralSave')} {t('saveGeneralSettings')}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>

View File

@@ -25,7 +25,7 @@ export function ResourcesDataTable<TData, TValue>({
columns={columns} columns={columns}
data={data} data={data}
title="Resources" title="Resources"
searchPlaceholder={t('resourceSearch')} searchPlaceholder={t('resourcesSearch')}
searchColumn="name" searchColumn="name"
onAdd={createResource} onAdd={createResource}
addButtonText={t('resourceAdd')} addButtonText={t('resourceAdd')}

View File

@@ -15,6 +15,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import CopyTextBox from "@app/components/CopyTextBox"; import CopyTextBox from "@app/components/CopyTextBox";
import { useTranslations } from 'next-intl';
interface AccessTokenSectionProps { interface AccessTokenSectionProps {
token: string; token: string;
@@ -37,37 +38,37 @@ export default function AccessTokenSection({
setTimeout(() => setCopied(null), 2000); setTimeout(() => setCopied(null), 2000);
}; };
const t = useTranslations();
return ( return (
<> <>
<div className="flex items-start space-x-2"> <div className="flex items-start space-x-2">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Your access token can be passed in two ways: as a query {t('shareTokenDescription')}
parameter or in the request headers. These must be passed
from the client on every request for authenticated access.
</p> </p>
</div> </div>
<Tabs defaultValue="token" className="w-full mt-4"> <Tabs defaultValue="token" className="w-full mt-4">
<TabsList className="grid grid-cols-2"> <TabsList className="grid grid-cols-2">
<TabsTrigger value="token">Access Token</TabsTrigger> <TabsTrigger value="token">{t('accessToken')}</TabsTrigger>
<TabsTrigger value="usage">Usage Examples</TabsTrigger> <TabsTrigger value="usage">{t('usageExamples')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="token" className="space-y-4"> <TabsContent value="token" className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<div className="font-bold">Token ID</div> <div className="font-bold">{t('tokenId')}</div>
<CopyToClipboard text={tokenId} isLink={false} /> <CopyToClipboard text={tokenId} isLink={false} />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="font-bold">Token</div> <div className="font-bold">{t('token')}</div>
<CopyToClipboard text={token} isLink={false} /> <CopyToClipboard text={token} isLink={false} />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="usage" className="space-y-4"> <TabsContent value="usage" className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-medium">Request Headers</h3> <h3 className="text-sm font-medium">{t('requestHeades')}</h3>
<CopyTextBox <CopyTextBox
text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId} text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId}
${env.server.resourceAccessTokenHeadersToken}: ${token}`} ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
@@ -75,7 +76,7 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-sm font-medium">Query Parameter</h3> <h3 className="text-sm font-medium">{t('queryParameter')}</h3>
<CopyTextBox <CopyTextBox
text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`} text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`}
/> />
@@ -84,21 +85,17 @@ ${env.server.resourceAccessTokenHeadersToken}: ${token}`}
<Alert variant="neutral"> <Alert variant="neutral">
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold"> <AlertTitle className="font-semibold">
Important Note {t('importantNote')}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
For security reasons, using headers is recommended {t('shareImportantDescription')}
over query parameters when possible, as query
parameters may be logged in server logs or browser
history.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<div className="text-sm text-muted-foreground mt-4"> <div className="text-sm text-muted-foreground mt-4">
Keep your access token secure. Do not share it in publicly {t('shareTokenSecurety')}
accessible areas or client-side code.
</div> </div>
</> </>
); );

View File

@@ -66,6 +66,7 @@ import {
CollapsibleTrigger CollapsibleTrigger
} from "@app/components/ui/collapsible"; } from "@app/components/ui/collapsible";
import AccessTokenSection from "./AccessTokenUsage"; import AccessTokenSection from "./AccessTokenUsage";
import { useTranslations } from 'next-intl';
type FormProps = { type FormProps = {
open: boolean; open: boolean;
@@ -91,6 +92,7 @@ export default function CreateShareLinkForm({
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
const t = useTranslations();
const [link, setLink] = useState<string | null>(null); const [link, setLink] = useState<string | null>(null);
const [accessTokenId, setAccessTokenId] = useState<string | null>(null); const [accessTokenId, setAccessTokenId] = useState<string | null>(null);
@@ -110,12 +112,12 @@ export default function CreateShareLinkForm({
>([]); >([]);
const timeUnits = [ const timeUnits = [
{ unit: "minutes", name: "Minutes" }, { unit: "minutes", name: t('minutes') },
{ unit: "hours", name: "Hours" }, { unit: "hours", name: t('hours') },
{ unit: "days", name: "Days" }, { unit: "days", name: t('days') },
{ unit: "weeks", name: "Weeks" }, { unit: "weeks", name: t('weeks') },
{ unit: "months", name: "Months" }, { unit: "months", name: t('months') },
{ unit: "years", name: "Years" } { unit: "years", name: t('years') }
]; ];
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -141,11 +143,8 @@ export default function CreateShareLinkForm({
console.error(e); console.error(e);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to fetch resources", title: t('shareErrorFetchResource'),
description: formatAxiosError( description: formatAxiosError(e, t('shareErrorFetchResourceDescription'))
e,
"An error occurred while fetching the resources"
)
}); });
}); });
@@ -208,11 +207,8 @@ export default function CreateShareLinkForm({
console.error(e); console.error(e);
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to create share link", title: t('shareErrorCreate'),
description: formatAxiosError( description: formatAxiosError(e, t('shareErrorCreateDescription'))
e,
"An error occurred while creating the share link"
)
}); });
}); });
@@ -260,9 +256,9 @@ export default function CreateShareLinkForm({
> >
<CredenzaContent> <CredenzaContent>
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle>Create Shareable Link</CredenzaTitle> <CredenzaTitle>{t('shareCreate')}</CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
Anyone with this link can access the resource {t('shareCreateDescription')}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
@@ -280,7 +276,7 @@ export default function CreateShareLinkForm({
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-col"> <FormItem className="flex flex-col">
<FormLabel> <FormLabel>
Resource {t('resource')}
</FormLabel> </FormLabel>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -305,12 +301,10 @@ export default function CreateShareLinkForm({
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0"> <PopoverContent className="p-0">
<Command> <Command>
<CommandInput placeholder="Search resources" /> <CommandInput placeholder={t('resourceSearch')} />
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
No {t('resourceNotFound')}
resources
found
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{resources.map( {resources.map(
@@ -366,7 +360,7 @@ export default function CreateShareLinkForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
Title (optional) {t('shareTitleOptional')}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
@@ -378,7 +372,7 @@ export default function CreateShareLinkForm({
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<FormLabel>Expire In</FormLabel> <FormLabel>{t('expireIn')}</FormLabel>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -455,18 +449,12 @@ export default function CreateShareLinkForm({
htmlFor="terms" htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
Never expire {t('neverExpire')}
</label> </label>
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Expiration time is how long the {t('shareExpireDescription')}
link will be usable and provide
access to the resource. After
this time, the link will no
longer work, and users who used
this link will lose access to
the resource.
</p> </p>
</div> </div>
</form> </form>
@@ -475,12 +463,10 @@ export default function CreateShareLinkForm({
{link && ( {link && (
<div className="max-w-md space-y-4"> <div className="max-w-md space-y-4">
<p> <p>
You will only be able to see this link {t('shareSeeOnce')}
once. Make sure to copy it.
</p> </p>
<p> <p>
Anyone with this link can access the {t('shareAccessHint')}
resource. Share it with care.
</p> </p>
<div className="h-[250px] w-full mx-auto flex items-center justify-center"> <div className="h-[250px] w-full mx-auto flex items-center justify-center">
@@ -506,7 +492,7 @@ export default function CreateShareLinkForm({
className="p-0 flex items-center justify-between w-full" className="p-0 flex items-center justify-between w-full"
> >
<h4 className="text-sm font-semibold"> <h4 className="text-sm font-semibold">
See Access Token Usage {t('shareTokenUsage')}
</h4> </h4>
<div> <div>
<ChevronsUpDown className="h-4 w-4" /> <ChevronsUpDown className="h-4 w-4" />
@@ -549,7 +535,7 @@ export default function CreateShareLinkForm({
loading={loading} loading={loading}
disabled={link !== null || loading} disabled={link !== null || loading}
> >
Create Link {t('createLink')}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>

View File

@@ -9,11 +9,13 @@ import {
InfoSections, InfoSections,
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import { useTranslations } from 'next-intl';
type SiteInfoCardProps = {}; type SiteInfoCardProps = {};
export default function SiteInfoCard({}: SiteInfoCardProps) { export default function SiteInfoCard({}: SiteInfoCardProps) {
const { site, updateSite } = useSiteContext(); const { site, updateSite } = useSiteContext();
const t = useTranslations();
const getConnectionTypeString = (type: string) => { const getConnectionTypeString = (type: string) => {
if (type === "newt") { if (type === "newt") {
@@ -21,7 +23,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
} else if (type === "wireguard") { } else if (type === "wireguard") {
return "WireGuard"; return "WireGuard";
} else if (type === "local") { } else if (type === "local") {
return "Local"; return t('local');
} else { } else {
return "Unknown"; return "Unknown";
} }
@@ -30,23 +32,23 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
return ( return (
<Alert> <Alert>
<InfoIcon className="h-4 w-4" /> <InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">Site Information</AlertTitle> <AlertTitle className="font-semibold">{t('siteInfo')}</AlertTitle>
<AlertDescription className="mt-4"> <AlertDescription className="mt-4">
<InfoSections cols={2}> <InfoSections cols={2}>
{(site.type == "newt" || site.type == "wireguard") && ( {(site.type == "newt" || site.type == "wireguard") && (
<> <>
<InfoSection> <InfoSection>
<InfoSectionTitle>Status</InfoSectionTitle> <InfoSectionTitle>{t('status')}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{site.online ? ( {site.online ? (
<div className="text-green-500 flex items-center space-x-2"> <div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div> <div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span> <span>{t('online')}</span>
</div> </div>
) : ( ) : (
<div className="text-neutral-500 flex items-center space-x-2"> <div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div> <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span> <span>{t('offline')}</span>
</div> </div>
)} )}
</InfoSectionContent> </InfoSectionContent>
@@ -54,7 +56,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
</> </>
)} )}
<InfoSection> <InfoSection>
<InfoSectionTitle>Connection Type</InfoSectionTitle> <InfoSectionTitle>{t('connectionType')}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{getConnectionTypeString(site.type)} {getConnectionTypeString(site.type)}
</InfoSectionContent> </InfoSectionContent>

View File

@@ -31,6 +31,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState } from "react"; import { useState } from "react";
import { useTranslations } from 'next-intl';
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required") name: z.string().nonempty("Name is required")
@@ -46,6 +47,7 @@ export default function GeneralPage() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations();
const form = useForm<GeneralFormValues>({ const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
@@ -65,19 +67,16 @@ export default function GeneralPage() {
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to update site", title: t('siteErrorUpdate'),
description: formatAxiosError( description: formatAxiosError(e,t('siteErrorUpdateDescription'))
e,
"An error occurred while updating the site."
)
}); });
}); });
updateSite({ name: data.name }); updateSite({ name: data.name });
toast({ toast({
title: "Site updated", title: t('siteUpdated'),
description: "The site has been updated." description: t('siteUpdatedDescription')
}); });
setLoading(false); setLoading(false);
@@ -90,10 +89,10 @@ export default function GeneralPage() {
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
General Settings {t('generalSettings')}
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Configure the general settings for this site {t('siteGeneralDescription')}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
@@ -110,14 +109,13 @@ export default function GeneralPage() {
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>{t('name')}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
<FormDescription> <FormDescription>
This is the display name of the {t('siteNameDescription')}
site.
</FormDescription> </FormDescription>
</FormItem> </FormItem>
)} )}
@@ -134,7 +132,7 @@ export default function GeneralPage() {
loading={loading} loading={loading}
disabled={loading} disabled={loading}
> >
Save General Settings {t('saveGeneralSettings')}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>

View File

@@ -16,6 +16,7 @@ import {
BreadcrumbSeparator BreadcrumbSeparator
} from "@app/components/ui/breadcrumb"; } from "@app/components/ui/breadcrumb";
import SiteInfoCard from "./SiteInfoCard"; import SiteInfoCard from "./SiteInfoCard";
import { getTranslations } from 'next-intl/server';
interface SettingsLayoutProps { interface SettingsLayoutProps {
children: React.ReactNode; children: React.ReactNode;
@@ -38,9 +39,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
redirect(`/${params.orgId}/settings/sites`); redirect(`/${params.orgId}/settings/sites`);
} }
const t = await getTranslations();
const navItems = [ const navItems = [
{ {
title: "General", title: t('general'),
href: "/{orgId}/settings/sites/{niceId}/general" href: "/{orgId}/settings/sites/{niceId}/general"
} }
]; ];
@@ -48,8 +51,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
title={`${site?.name} Settings`} title={t('siteSetting', {siteName: site?.name})}
description="Configure the settings on your site" description={t('siteSettingDescription')}
/> />
<SiteProvider site={site}> <SiteProvider site={site}>