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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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