add admin/license i18n

This commit is contained in:
Lokowitz 2025-05-06 09:41:44 +00:00
parent 4dd9f4736d
commit 1e72b0f854
4 changed files with 124 additions and 98 deletions

View file

@ -297,5 +297,58 @@
"userDeleteServer": "Delete User from Server", "userDeleteServer": "Delete User from Server",
"userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.", "userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.",
"userMessageConfirm": "To confirm, please type the name of the user below.", "userMessageConfirm": "To confirm, please type the name of the user below.",
"userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?" "userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?",
"licenseKey": "License Key",
"valid": "Valid",
"numberOfSites": "Number of Sites",
"licenseKeySearch": "Search license keys...",
"licenseKeyAdd": "Add License Key",
"type": "Type",
"licenseKeyRequired": "License key is required",
"licenseTermsAgree": "You must agree to the license terms",
"licenseErrorKeyLoad": "Failed to load license keys",
"licenseErrorKeyLoadDescription": "An error occurred loading license keys.",
"licenseErrorKeyDelete": "Failed to delete license key",
"licenseErrorKeyDeleteDescription": "An error occurred deleting license key.",
"licenseKeyDeleted": "License key deleted",
"licenseKeyDeletedDescription": "The license key has been deleted.",
"licenseErrorKeyActivate": "Failed to activate license key",
"licenseErrorKeyActivateDescription": "An error occurred while activating the license key.",
"licenseKeyActivated": "License key activated",
"licenseKeyActivatedDescription": "The license key has been successfully activated.",
"licenseErrorKeyRecheck": "Failed to recheck license keys",
"licenseErrorKeyRecheckDescription": "An error occurred rechecking license keys.",
"licenseErrorKeyRechecked": "License keys rechecked",
"licenseErrorKeyRecheckedDescription": "All license keys have been rechecked",
"licenseActivateKey": "Activate License Key",
"licenseActivateKeyDescription": "Enter a license key to activate it.",
"licenseActivate": "Activate License",
"licenseAgreement": "By checking this box, you confirm that you have read and agree to the license terms corresponding to the tier associated with your license key.",
"fossorialLicense": "View Fossorial Commercial License & Subscription Terms",
"licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.",
"licenseMessageConfirm": "To confirm, please type the license key below.",
"licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?",
"licenseKeyDelete": "Delete License Key",
"licenseKeyDeleteConfirm": "Confirm Delete License Key",
"licenseTitle": "Manage License Status",
"licenseTitleDescription": "View and manage license keys in the system",
"licenseHost": "Host License",
"licenseHostDescription": "Manage the main license key for the host.",
"notLicensed": "Not Licensed",
"hostId": "Host ID",
"licenseReckeckAll": "Recheck All Keys",
"licenseSiteUsage": "Sites Usage",
"licenseSiteUsageDecsription": "View the number of sites using this license.",
"licenseNoSiteLimit": "There is no limit on the number of sites using an unlicensed host.",
"licensePurchase": "Purchase License",
"licensePurchaseSites": "Purchase Additional Sites",
"licenseSitesUsedMax": "{usedSites} of {maxSites} sites used",
"licenseSitesUsed": "{count, plural, =0 {# sites} =1 {# site} other {# sites}} in system.",
"licensePurchaseDescription": "Choose how many sites you want to {selectedMode, select, license {purchase a license for. You can always add more sites later.} other {add to your existing license.}}",
"licenseFee": "License fee",
"licensePriceSite": "Price per site",
"total": "Total",
"licenseContinuePayment": "Continue to Payment",
"pricingPage": "pricing page",
"licensePricingPage": "For the most up-to-date pricing and discounts, please visit the "
} }

View file

@ -13,6 +13,7 @@ import { LicenseKeyCache } from "@server/license/license";
import { ArrowUpDown } from "lucide-react"; import { ArrowUpDown } from "lucide-react";
import moment from "moment"; import moment from "moment";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import { useTranslations } from 'next-intl';
type LicenseKeysDataTableProps = { type LicenseKeysDataTableProps = {
licenseKeys: LicenseKeyCache[]; licenseKeys: LicenseKeyCache[];
@ -32,6 +33,9 @@ export function LicenseKeysDataTable({
onDelete, onDelete,
onCreate onCreate
}: LicenseKeysDataTableProps) { }: LicenseKeysDataTableProps) {
const t = useTranslations();
const columns: ColumnDef<LicenseKeyCache>[] = [ const columns: ColumnDef<LicenseKeyCache>[] = [
{ {
accessorKey: "licenseKey", accessorKey: "licenseKey",
@ -43,7 +47,7 @@ export function LicenseKeysDataTable({
column.toggleSorting(column.getIsSorted() === "asc") column.toggleSorting(column.getIsSorted() === "asc")
} }
> >
License Key {t('licenseKey')}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
@ -68,7 +72,7 @@ export function LicenseKeysDataTable({
column.toggleSorting(column.getIsSorted() === "asc") column.toggleSorting(column.getIsSorted() === "asc")
} }
> >
Valid {t('valid')}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
@ -87,7 +91,7 @@ export function LicenseKeysDataTable({
column.toggleSorting(column.getIsSorted() === "asc") column.toggleSorting(column.getIsSorted() === "asc")
} }
> >
Type {t('type')}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
@ -112,7 +116,7 @@ export function LicenseKeysDataTable({
column.toggleSorting(column.getIsSorted() === "asc") column.toggleSorting(column.getIsSorted() === "asc")
} }
> >
Number of Sites {t('numberOfSites')}
<ArrowUpDown className="ml-2 h-4 w-4" /> <ArrowUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
); );
@ -126,7 +130,7 @@ export function LicenseKeysDataTable({
variant="outlinePrimary" variant="outlinePrimary"
onClick={() => onDelete(row.original)} onClick={() => onDelete(row.original)}
> >
Delete {t('delete')}
</Button> </Button>
</div> </div>
) )
@ -138,10 +142,10 @@ export function LicenseKeysDataTable({
columns={columns} columns={columns}
data={licenseKeys} data={licenseKeys}
title="License Keys" title="License Keys"
searchPlaceholder="Search license keys..." searchPlaceholder={t('licenseKeySearch')}
searchColumn="licenseKey" searchColumn="licenseKey"
onAdd={onCreate} onAdd={onCreate}
addButtonText="Add License Key" addButtonText={t('licenseKeyAdd')}
/> />
); );
} }

View file

@ -16,6 +16,7 @@ import {
CredenzaHeader, CredenzaHeader,
CredenzaTitle CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { useTranslations } from 'next-intl';
type SitePriceCalculatorProps = { type SitePriceCalculatorProps = {
isOpen: boolean; isOpen: boolean;
@ -60,27 +61,26 @@ export function SitePriceCalculator({
? licenseFlatRate + siteCount * pricePerSite ? licenseFlatRate + siteCount * pricePerSite
: siteCount * pricePerSite; : siteCount * pricePerSite;
const t = useTranslations();
return ( return (
<Credenza open={isOpen} onOpenChange={onOpenChange}> <Credenza open={isOpen} onOpenChange={onOpenChange}>
<CredenzaContent> <CredenzaContent>
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle> <CredenzaTitle>
{mode === "license" {mode === "license"
? "Purchase License" ? t('licensePurchase')
: "Purchase Additional Sites"} : t('licensePurchaseSites')}
</CredenzaTitle> </CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
Choose how many sites you want to{" "} {t('licensePurchaseDescription', {selectedMode: mode})}
{mode === "license"
? "purchase a license for. You can always add more sites later."
: "add to your existing license."}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<div className="text-sm font-medium text-muted-foreground"> <div className="text-sm font-medium text-muted-foreground">
Number of Sites {t('numberOfSites')}
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Button <Button
@ -110,7 +110,7 @@ export function SitePriceCalculator({
{mode === "license" && ( {mode === "license" && (
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
License fee: {t('licenseFee')}:
</span> </span>
<span className="font-medium"> <span className="font-medium">
${licenseFlatRate.toFixed(2)} ${licenseFlatRate.toFixed(2)}
@ -119,7 +119,7 @@ export function SitePriceCalculator({
)} )}
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-2">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Price per site: {t('licensePriceSite')}:
</span> </span>
<span className="font-medium"> <span className="font-medium">
${pricePerSite.toFixed(2)} ${pricePerSite.toFixed(2)}
@ -127,25 +127,24 @@ export function SitePriceCalculator({
</div> </div>
<div className="flex justify-between items-center mt-2"> <div className="flex justify-between items-center mt-2">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
Number of sites: {t('numberOfSites')}:
</span> </span>
<span className="font-medium">{siteCount}</span> <span className="font-medium">{siteCount}</span>
</div> </div>
<div className="flex justify-between items-center mt-4 text-lg font-bold"> <div className="flex justify-between items-center mt-4 text-lg font-bold">
<span>Total:</span> <span>{t('total')}:</span>
<span>${totalCost.toFixed(2)} / mo</span> <span>${totalCost.toFixed(2)} / mo</span>
</div> </div>
<p className="text-muted-foreground text-sm mt-2 text-center"> <p className="text-muted-foreground text-sm mt-2 text-center">
For the most up-to-date pricing and discounts, {t('licensePricingPage')}
please visit the{" "}
<a <a
href="https://docs.fossorial.io/pricing" href="https://docs.fossorial.io/pricing"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="underline" className="underline"
> >
pricing page {t('pricingPage')}
</a> </a>
. .
</p> </p>
@ -154,10 +153,10 @@ export function SitePriceCalculator({
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline">Cancel</Button> <Button variant="outline">{t('cancel')}</Button>
</CredenzaClose> </CredenzaClose>
<Button onClick={continueToPayment}> <Button onClick={continueToPayment}>
Continue to Payment {t('licenseContinuePayment')}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>

View file

@ -57,6 +57,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { SitePriceCalculator } from "./components/SitePriceCalculator"; import { SitePriceCalculator } from "./components/SitePriceCalculator";
import Link from "next/link"; import Link from "next/link";
import { Checkbox } from "@app/components/ui/checkbox"; import { Checkbox } from "@app/components/ui/checkbox";
import { useTranslations } from 'next-intl';
const formSchema = z.object({ const formSchema = z.object({
licenseKey: z licenseKey: z
@ -77,6 +78,7 @@ function obfuscateLicenseKey(key: string): string {
export default function LicensePage() { export default function LicensePage() {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations();
const [rows, setRows] = useState<LicenseKeyCache[]>([]); const [rows, setRows] = useState<LicenseKeyCache[]>([]);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -129,11 +131,8 @@ export default function LicensePage() {
} }
} catch (e) { } catch (e) {
toast({ toast({
title: "Failed to load license keys", title: t('licenseErrorKeyLoad'),
description: formatAxiosError( description: formatAxiosError(e, t('licenseErrorKeyLoadDescription'))
e,
"An error occurred loading license keys"
)
}); });
} }
} }
@ -148,17 +147,14 @@ export default function LicensePage() {
} }
await loadLicenseKeys(); await loadLicenseKeys();
toast({ toast({
title: "License key deleted", title: t('licenseKeyDeleted'),
description: "The license key has been deleted" description: t('licenseKeyDeletedDescription')
}); });
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
} catch (e) { } catch (e) {
toast({ toast({
title: "Failed to delete license key", title: t('licenseErrorKeyDelete'),
description: formatAxiosError( description: formatAxiosError(e, t('licenseErrorKeyDeleteDescription'))
e,
"An error occurred deleting license key"
)
}); });
} finally { } finally {
setIsDeletingLicense(false); setIsDeletingLicense(false);
@ -174,16 +170,13 @@ export default function LicensePage() {
} }
await loadLicenseKeys(); await loadLicenseKeys();
toast({ toast({
title: "License keys rechecked", title: t('licenseErrorKeyRechecked'),
description: "All license keys have been rechecked" description: t('licenseErrorKeyRecheckedDescription')
}); });
} catch (e) { } catch (e) {
toast({ toast({
title: "Failed to recheck license keys", title: t('licenseErrorKeyRecheck'),
description: formatAxiosError( description: formatAxiosError(e, t('licenseErrorKeyRecheckDescription'))
e,
"An error occurred rechecking license keys"
)
}); });
} finally { } finally {
setIsRecheckingLicense(false); setIsRecheckingLicense(false);
@ -201,8 +194,8 @@ export default function LicensePage() {
} }
toast({ toast({
title: "License key activated", title: t('licenseKeyActivated'),
description: "The license key has been successfully activated." description: t('licenseKeyActivatedDescription')
}); });
setIsCreateModalOpen(false); setIsCreateModalOpen(false);
@ -211,11 +204,8 @@ export default function LicensePage() {
} catch (e) { } catch (e) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Failed to activate license key", title: t('licenseErrorKeyActivate'),
description: formatAxiosError( description: formatAxiosError(e, t('licenseErrorKeyActivateDescription'))
e,
"An error occurred while activating the license key."
)
}); });
} finally { } finally {
setIsActivatingLicense(false); setIsActivatingLicense(false);
@ -245,9 +235,9 @@ export default function LicensePage() {
> >
<CredenzaContent> <CredenzaContent>
<CredenzaHeader> <CredenzaHeader>
<CredenzaTitle>Activate License Key</CredenzaTitle> <CredenzaTitle>{t('licenseActivateKey')}</CredenzaTitle>
<CredenzaDescription> <CredenzaDescription>
Enter a license key to activate it. {t('licenseActivateKeyDescription')}
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody> <CredenzaBody>
@ -262,7 +252,7 @@ export default function LicensePage() {
name="licenseKey" name="licenseKey"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>License Key</FormLabel> <FormLabel>{t('licenseKey')}</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
@ -285,12 +275,7 @@ export default function LicensePage() {
</FormControl> </FormControl>
<div className="space-y-1 leading-none"> <div className="space-y-1 leading-none">
<FormLabel> <FormLabel>
By checking this box, you {t('licenseAgreement')}
confirm that you have read
and agree to the license
terms corresponding to the
tier associated with your
license key.
<br /> <br />
<Link <Link
href="https://fossorial.io/license.html" href="https://fossorial.io/license.html"
@ -298,9 +283,7 @@ export default function LicensePage() {
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-primary hover:underline" className="text-primary hover:underline"
> >
View Fossorial {t('fossorialLicense')}
Commercial License &
Subscription Terms
</Link> </Link>
</FormLabel> </FormLabel>
<FormMessage /> <FormMessage />
@ -313,7 +296,7 @@ export default function LicensePage() {
</CredenzaBody> </CredenzaBody>
<CredenzaFooter> <CredenzaFooter>
<CredenzaClose asChild> <CredenzaClose asChild>
<Button variant="outline">Close</Button> <Button variant="outline">{t('close')}</Button>
</CredenzaClose> </CredenzaClose>
<Button <Button
type="submit" type="submit"
@ -321,7 +304,7 @@ export default function LicensePage() {
loading={isActivatingLicense} loading={isActivatingLicense}
disabled={isActivatingLicense} disabled={isActivatingLicense}
> >
Activate License {t('licenseActivate')}
</Button> </Button>
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
@ -336,47 +319,40 @@ export default function LicensePage() {
}} }}
dialog={ dialog={
<div className="space-y-4"> <div className="space-y-4">
<p> <p>
Are you sure you want to delete the license key{" "} {t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})}
<b>
{obfuscateLicenseKey(
selectedLicenseKey.licenseKey
)}
</b>
?
</p> </p>
<p> <p>
<b> <b>
This will remove the license key and all {t('licenseMessageRemove')}
associated permissions granted by it.
</b> </b>
</p> </p>
<p> <p>
To confirm, please type the license key below. {t('licenseMessageConfirm')}
</p> </p>
</div> </div>
} }
buttonText="Confirm Delete License Key" buttonText={t('licenseKeyDeleteConfirm')}
onConfirm={async () => onConfirm={async () =>
deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted)
} }
string={selectedLicenseKey.licenseKey} string={selectedLicenseKey.licenseKey}
title="Delete License Key" title={t('licenseKeyDelete')}
/> />
)} )}
<SettingsSectionTitle <SettingsSectionTitle
title="Manage License Status" title={t('licenseTitle')}
description="View and manage license keys in the system" description={t('licenseTitleDescription')}
/> />
<SettingsContainer> <SettingsContainer>
<SettingsSectionGrid cols={2}> <SettingsSectionGrid cols={2}>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SSTitle>Host License</SSTitle> <SSTitle>{t('licenseHost')}</SSTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Manage the main license key for the host. {t('licenseHostDescription')}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<div className="space-y-4"> <div className="space-y-4">
@ -397,7 +373,7 @@ export default function LicensePage() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-2xl"> <div className="text-2xl">
Not Licensed {t('notLicensed')}
</div> </div>
</div> </div>
)} )}
@ -405,7 +381,7 @@ export default function LicensePage() {
{licenseStatus?.hostId && ( {licenseStatus?.hostId && (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium"> <div className="text-sm font-medium">
Host ID {t('hostId')}
</div> </div>
<CopyTextBox text={licenseStatus.hostId} /> <CopyTextBox text={licenseStatus.hostId} />
</div> </div>
@ -413,7 +389,7 @@ export default function LicensePage() {
{hostLicense && ( {hostLicense && (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium"> <div className="text-sm font-medium">
License Key {t('licenseKey')}
</div> </div>
<CopyTextBox <CopyTextBox
text={hostLicense} text={hostLicense}
@ -431,39 +407,33 @@ export default function LicensePage() {
disabled={isRecheckingLicense} disabled={isRecheckingLicense}
loading={isRecheckingLicense} loading={isRecheckingLicense}
> >
Recheck All Keys {t('licenseReckeckAll')}
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SSTitle>Sites Usage</SSTitle> <SSTitle>{t('licenseSiteUsage')}</SSTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
View the number of sites using this license. {t('licenseSiteUsageDecsription')}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="text-2xl"> <div className="text-2xl">
{licenseStatus?.usedSites || 0}{" "} {t('licenseSitesUsed', {count: licenseStatus?.usedSites || 0})}
{licenseStatus?.usedSites === 1
? "site"
: "sites"}{" "}
in system
</div> </div>
</div> </div>
{!licenseStatus?.isHostLicensed && ( {!licenseStatus?.isHostLicensed && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
There is no limit on the number of sites {t('licenseNoSiteLimit')}
using an unlicensed host.
</p> </p>
)} )}
{licenseStatus?.maxSites && ( {licenseStatus?.maxSites && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{licenseStatus.usedSites || 0} of{" "} {t('licenseSitesUsedMax', {usedSites: licenseStatus.usedSites || 0, maxSites: licenseStatus.maxSites})}
{licenseStatus.maxSites} sites used
</span> </span>
<span className="text-muted-foreground"> <span className="text-muted-foreground">
{Math.round( {Math.round(
@ -495,7 +465,7 @@ export default function LicensePage() {
setIsPurchaseModalOpen(true); setIsPurchaseModalOpen(true);
}} }}
> >
Purchase License {t('licensePurchase')}
</Button> </Button>
</> </>
) : ( ) : (
@ -507,7 +477,7 @@ export default function LicensePage() {
setIsPurchaseModalOpen(true); setIsPurchaseModalOpen(true);
}} }}
> >
Purchase Additional Sites {t('licensePurchaseSites')}
</Button> </Button>
</> </>
)} )}