mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-02 00:55:48 +02:00
add admin/license i18n
This commit is contained in:
parent
4dd9f4736d
commit
1e72b0f854
4 changed files with 124 additions and 98 deletions
|
@ -297,5 +297,58 @@
|
|||
"userDeleteServer": "Delete User from 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.",
|
||||
"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 "
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { LicenseKeyCache } from "@server/license/license";
|
|||
import { ArrowUpDown } from "lucide-react";
|
||||
import moment from "moment";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type LicenseKeysDataTableProps = {
|
||||
licenseKeys: LicenseKeyCache[];
|
||||
|
@ -32,6 +33,9 @@ export function LicenseKeysDataTable({
|
|||
onDelete,
|
||||
onCreate
|
||||
}: LicenseKeysDataTableProps) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const columns: ColumnDef<LicenseKeyCache>[] = [
|
||||
{
|
||||
accessorKey: "licenseKey",
|
||||
|
@ -43,7 +47,7 @@ export function LicenseKeysDataTable({
|
|||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
License Key
|
||||
{t('licenseKey')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
@ -68,7 +72,7 @@ export function LicenseKeysDataTable({
|
|||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Valid
|
||||
{t('valid')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
@ -87,7 +91,7 @@ export function LicenseKeysDataTable({
|
|||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Type
|
||||
{t('type')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
@ -112,7 +116,7 @@ export function LicenseKeysDataTable({
|
|||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Number of Sites
|
||||
{t('numberOfSites')}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
|
@ -126,7 +130,7 @@ export function LicenseKeysDataTable({
|
|||
variant="outlinePrimary"
|
||||
onClick={() => onDelete(row.original)}
|
||||
>
|
||||
Delete
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
@ -138,10 +142,10 @@ export function LicenseKeysDataTable({
|
|||
columns={columns}
|
||||
data={licenseKeys}
|
||||
title="License Keys"
|
||||
searchPlaceholder="Search license keys..."
|
||||
searchPlaceholder={t('licenseKeySearch')}
|
||||
searchColumn="licenseKey"
|
||||
onAdd={onCreate}
|
||||
addButtonText="Add License Key"
|
||||
addButtonText={t('licenseKeyAdd')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type SitePriceCalculatorProps = {
|
||||
isOpen: boolean;
|
||||
|
@ -60,27 +61,26 @@ export function SitePriceCalculator({
|
|||
? licenseFlatRate + siteCount * pricePerSite
|
||||
: siteCount * pricePerSite;
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Credenza open={isOpen} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{mode === "license"
|
||||
? "Purchase License"
|
||||
: "Purchase Additional Sites"}
|
||||
? t('licensePurchase')
|
||||
: t('licensePurchaseSites')}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Choose how many sites you want to{" "}
|
||||
{mode === "license"
|
||||
? "purchase a license for. You can always add more sites later."
|
||||
: "add to your existing license."}
|
||||
{t('licensePurchaseDescription', {selectedMode: mode})}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
Number of Sites
|
||||
{t('numberOfSites')}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
|
@ -110,7 +110,7 @@ export function SitePriceCalculator({
|
|||
{mode === "license" && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
License fee:
|
||||
{t('licenseFee')}:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
${licenseFlatRate.toFixed(2)}
|
||||
|
@ -119,7 +119,7 @@ export function SitePriceCalculator({
|
|||
)}
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-sm font-medium">
|
||||
Price per site:
|
||||
{t('licensePriceSite')}:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
${pricePerSite.toFixed(2)}
|
||||
|
@ -127,25 +127,24 @@ export function SitePriceCalculator({
|
|||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-sm font-medium">
|
||||
Number of sites:
|
||||
{t('numberOfSites')}:
|
||||
</span>
|
||||
<span className="font-medium">{siteCount}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-sm mt-2 text-center">
|
||||
For the most up-to-date pricing and discounts,
|
||||
please visit the{" "}
|
||||
{t('licensePricingPage')}
|
||||
<a
|
||||
href="https://docs.fossorial.io/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
pricing page
|
||||
{t('pricingPage')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
@ -154,10 +153,10 @@ export function SitePriceCalculator({
|
|||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button variant="outline">{t('cancel')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button onClick={continueToPayment}>
|
||||
Continue to Payment
|
||||
{t('licenseContinuePayment')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
|
|
@ -57,6 +57,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|||
import { SitePriceCalculator } from "./components/SitePriceCalculator";
|
||||
import Link from "next/link";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
const formSchema = z.object({
|
||||
licenseKey: z
|
||||
|
@ -77,6 +78,7 @@ function obfuscateLicenseKey(key: string): string {
|
|||
|
||||
export default function LicensePage() {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const [rows, setRows] = useState<LicenseKeyCache[]>([]);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
@ -129,11 +131,8 @@ export default function LicensePage() {
|
|||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to load license keys",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred loading license keys"
|
||||
)
|
||||
title: t('licenseErrorKeyLoad'),
|
||||
description: formatAxiosError(e, t('licenseErrorKeyLoadDescription'))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -148,17 +147,14 @@ export default function LicensePage() {
|
|||
}
|
||||
await loadLicenseKeys();
|
||||
toast({
|
||||
title: "License key deleted",
|
||||
description: "The license key has been deleted"
|
||||
title: t('licenseKeyDeleted'),
|
||||
description: t('licenseKeyDeletedDescription')
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to delete license key",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred deleting license key"
|
||||
)
|
||||
title: t('licenseErrorKeyDelete'),
|
||||
description: formatAxiosError(e, t('licenseErrorKeyDeleteDescription'))
|
||||
});
|
||||
} finally {
|
||||
setIsDeletingLicense(false);
|
||||
|
@ -174,16 +170,13 @@ export default function LicensePage() {
|
|||
}
|
||||
await loadLicenseKeys();
|
||||
toast({
|
||||
title: "License keys rechecked",
|
||||
description: "All license keys have been rechecked"
|
||||
title: t('licenseErrorKeyRechecked'),
|
||||
description: t('licenseErrorKeyRecheckedDescription')
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: "Failed to recheck license keys",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred rechecking license keys"
|
||||
)
|
||||
title: t('licenseErrorKeyRecheck'),
|
||||
description: formatAxiosError(e, t('licenseErrorKeyRecheckDescription'))
|
||||
});
|
||||
} finally {
|
||||
setIsRecheckingLicense(false);
|
||||
|
@ -201,8 +194,8 @@ export default function LicensePage() {
|
|||
}
|
||||
|
||||
toast({
|
||||
title: "License key activated",
|
||||
description: "The license key has been successfully activated."
|
||||
title: t('licenseKeyActivated'),
|
||||
description: t('licenseKeyActivatedDescription')
|
||||
});
|
||||
|
||||
setIsCreateModalOpen(false);
|
||||
|
@ -211,11 +204,8 @@ export default function LicensePage() {
|
|||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to activate license key",
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
"An error occurred while activating the license key."
|
||||
)
|
||||
title: t('licenseErrorKeyActivate'),
|
||||
description: formatAxiosError(e, t('licenseErrorKeyActivateDescription'))
|
||||
});
|
||||
} finally {
|
||||
setIsActivatingLicense(false);
|
||||
|
@ -245,9 +235,9 @@ export default function LicensePage() {
|
|||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>Activate License Key</CredenzaTitle>
|
||||
<CredenzaTitle>{t('licenseActivateKey')}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
Enter a license key to activate it.
|
||||
{t('licenseActivateKeyDescription')}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
|
@ -262,7 +252,7 @@ export default function LicensePage() {
|
|||
name="licenseKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>License Key</FormLabel>
|
||||
<FormLabel>{t('licenseKey')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
@ -285,12 +275,7 @@ export default function LicensePage() {
|
|||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
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.
|
||||
{t('licenseAgreement')}
|
||||
<br />
|
||||
<Link
|
||||
href="https://fossorial.io/license.html"
|
||||
|
@ -298,9 +283,7 @@ export default function LicensePage() {
|
|||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
View Fossorial
|
||||
Commercial License &
|
||||
Subscription Terms
|
||||
{t('fossorialLicense')}
|
||||
</Link>
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
|
@ -313,7 +296,7 @@ export default function LicensePage() {
|
|||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
<Button variant="outline">{t('close')}</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -321,7 +304,7 @@ export default function LicensePage() {
|
|||
loading={isActivatingLicense}
|
||||
disabled={isActivatingLicense}
|
||||
>
|
||||
Activate License
|
||||
{t('licenseActivate')}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
|
@ -336,47 +319,40 @@ export default function LicensePage() {
|
|||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
Are you sure you want to delete the license key{" "}
|
||||
<b>
|
||||
{obfuscateLicenseKey(
|
||||
selectedLicenseKey.licenseKey
|
||||
)}
|
||||
</b>
|
||||
?
|
||||
<p>
|
||||
{t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})}
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
This will remove the license key and all
|
||||
associated permissions granted by it.
|
||||
{t('licenseMessageRemove')}
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
To confirm, please type the license key below.
|
||||
{t('licenseMessageConfirm')}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete License Key"
|
||||
buttonText={t('licenseKeyDeleteConfirm')}
|
||||
onConfirm={async () =>
|
||||
deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted)
|
||||
}
|
||||
string={selectedLicenseKey.licenseKey}
|
||||
title="Delete License Key"
|
||||
title={t('licenseKeyDelete')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title="Manage License Status"
|
||||
description="View and manage license keys in the system"
|
||||
title={t('licenseTitle')}
|
||||
description={t('licenseTitleDescription')}
|
||||
/>
|
||||
|
||||
<SettingsContainer>
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>Host License</SSTitle>
|
||||
<SSTitle>{t('licenseHost')}</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
Manage the main license key for the host.
|
||||
{t('licenseHostDescription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
|
@ -397,7 +373,7 @@ export default function LicensePage() {
|
|||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl">
|
||||
Not Licensed
|
||||
{t('notLicensed')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -405,7 +381,7 @@ export default function LicensePage() {
|
|||
{licenseStatus?.hostId && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
Host ID
|
||||
{t('hostId')}
|
||||
</div>
|
||||
<CopyTextBox text={licenseStatus.hostId} />
|
||||
</div>
|
||||
|
@ -413,7 +389,7 @@ export default function LicensePage() {
|
|||
{hostLicense && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
License Key
|
||||
{t('licenseKey')}
|
||||
</div>
|
||||
<CopyTextBox
|
||||
text={hostLicense}
|
||||
|
@ -431,39 +407,33 @@ export default function LicensePage() {
|
|||
disabled={isRecheckingLicense}
|
||||
loading={isRecheckingLicense}
|
||||
>
|
||||
Recheck All Keys
|
||||
{t('licenseReckeckAll')}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SSTitle>Sites Usage</SSTitle>
|
||||
<SSTitle>{t('licenseSiteUsage')}</SSTitle>
|
||||
<SettingsSectionDescription>
|
||||
View the number of sites using this license.
|
||||
{t('licenseSiteUsageDecsription')}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-2xl">
|
||||
{licenseStatus?.usedSites || 0}{" "}
|
||||
{licenseStatus?.usedSites === 1
|
||||
? "site"
|
||||
: "sites"}{" "}
|
||||
in system
|
||||
{t('licenseSitesUsed', {count: licenseStatus?.usedSites || 0})}
|
||||
</div>
|
||||
</div>
|
||||
{!licenseStatus?.isHostLicensed && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
There is no limit on the number of sites
|
||||
using an unlicensed host.
|
||||
{t('licenseNoSiteLimit')}
|
||||
</p>
|
||||
)}
|
||||
{licenseStatus?.maxSites && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{licenseStatus.usedSites || 0} of{" "}
|
||||
{licenseStatus.maxSites} sites used
|
||||
{t('licenseSitesUsedMax', {usedSites: licenseStatus.usedSites || 0, maxSites: licenseStatus.maxSites})}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(
|
||||
|
@ -495,7 +465,7 @@ export default function LicensePage() {
|
|||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase License
|
||||
{t('licensePurchase')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
|
@ -507,7 +477,7 @@ export default function LicensePage() {
|
|||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase Additional Sites
|
||||
{t('licensePurchaseSites')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue