- Are you sure you want to remove the invitation for{" "} - {selectedInvitation?.email}? + {t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}
- Once removed, this invitation will no longer be - valid. You can always re-invite the user later. + {t('inviteMessageRemove')}
- To confirm, please type the email address of the - invitation below. + {t('inviteMessageConfirm')}
- Are you sure you want to regenerate the - invitation for {invitation?.email}? This - will revoke the previous invitation. + {t('inviteQuestionRegenerate', {email: invitation?.email || ""})}
- Are you sure you want to delete the organization{" "} - {org?.org.name}? + {t('orgQuestionRemove', {selectedOrg: org?.org.name})}
- This action is irreversible and will delete all - associated data. + {t('orgMessageRemove')}
- To confirm, type the name of the organization below. + {t('orgMessageConfirm')}
- Resources are proxies to applications running on your private network. Create a resource for any HTTP/HTTPS or raw TCP/UDP service on your private network. - Each resource must be connected to a site to enable private, secure connectivity through an encrypted WireGuard tunnel. + {t('resourcesDescription')}
- Are you sure you want to remove the resource{" "} - - {selectedResource?.name || - selectedResource?.id} - {" "} - from the organization? + {t('resourceQuestionRemove', {selectedResource: selectedResource?.name || selectedResource?.id})}
- Once removed, the resource will no longer be - accessible. All targets attached to the resource - will be removed. + {t('resourceMessageRemove')}
- To confirm, please type the name of the resource - below. + {t('resourceMessageConfirm')}
- Advanced TLS - Settings + {t('targetTlsSettingsAdvanced')}
- Rules allow you to control access to your resource - based on a set of criteria. You can create rules to - allow or deny access based on IP address or URL - path. + {t('rulesAboutDescription')}
- 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')}
- 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')}
- You will only be able to see this link - once. Make sure to copy it. + {t('shareSeeOnce')}
- Anyone with this link can access the - resource. Share it with care. + {t('shareAccessHint')}
- Create shareable links to your resources. Links provide - temporary or unlimited access to your resource. You can - configure the expiration duration of the link when you - create one. + {t('shareDescription2')}
Loading WireGuard configuration...
+{t('siteLoadWGConfig')}
) : form.watch("method") === "newt" && siteDefaults ? ( <>- For the best user experience, use Newt. It uses - WireGuard under the hood and allows you to address your - private resources by their LAN address on your private - network from within the Pangolin dashboard. + {t('siteNewtDescription')}
- Use any WireGuard client to connect. You will have to - address your internal resources using the peer IP. + {t('siteWgAnyClients')}
- Are you sure you want to remove the site{" "} - {selectedSite?.name || selectedSite?.id}{" "} - from the organization? + {t('siteQuestionRemove', {selectedSite: selectedSite?.name || selectedSite?.id})} +
+ ++ {t('siteMessageRemove')}
- Once removed, the site will no longer be - accessible.{" "} - - All resources and targets associated with - the site will also be removed. - -
- -- To confirm, please type the name of the site - below. + {t('siteMessageConfirm')}
- Operating System + {t('operatingSystem')}
- Commands + {t('commands')}
- Are you sure you want to remove the API key{" "} - {selected?.name || selected?.id}? + {t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
- Once removed, the API key will no longer be - able to be used. + {t('apiKeysMessageRemove')}
- To confirm, please type the name of the API key - below. + {t('apiKeysMessageConfirm')}
- Are you sure you want to permanently delete the - identity provider {selectedIdp.name}? + {t('idpQuestionRemove', {name: selectedIdp.name})}
- This will remove the identity provider and - all associated configurations. Users who - authenticate through this provider will no - longer be able to log in. + {t('idpMessageRemove')}
- To confirm, please type the name of the identity - provider below. + {t('idpMessageConfirm')}
- For the most up-to-date pricing and discounts, - please visit the{" "} + {t('licensePricingPage')} - pricing page + {t('pricingPage')} .
@@ -120,10 +119,10 @@ export function SitePriceCalculator({- Are you sure you want to delete the license key{" "} - - {obfuscateLicenseKey( - selectedLicenseKey.licenseKey - )} - - ? + {t('licenseQuestionRemove', {selectedKey: obfuscateLicenseKey(selectedLicenseKey.licenseKey)})}
- This will remove the license key and all - associated permissions granted by it. + {t('licenseMessageRemove')}
- To confirm, please type the license key below. + {t('licenseMessageConfirm')}
- There is no limit on the number of sites - using an unlicensed host. + {t('licenseNoSiteLimit')}
)} {licenseStatus?.maxSites && (- Are you sure you want to permanently delete{" "} - - {selected?.email || - selected?.name || - selected?.username} - {" "} - from the server? + {t('userQuestionRemove', {selectedUser: selected?.email || selected?.name || selected?.username})}
- The user will be removed from all - organizations and be completely removed from - the server. + {t('userMessageRemove')}
- To confirm, please type the name of the user - below. + {t('userMessageConfirm')}
- Don't have an account?{" "} + {t('authNoAccount')}{" "} - Sign up + {t('signup')}
)} diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 7ddac325..8262c738 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -44,27 +44,12 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { passwordSchema } from "@server/auth/passwordSchema"; import { cleanRedirect } from "@app/lib/cleanRedirect"; +import { useTranslations } from "next-intl"; const requestSchema = z.object({ email: z.string().email() }); -const formSchema = z - .object({ - email: z.string().email({ message: "Invalid email address" }), - token: z.string().min(8, { message: "Invalid token" }), - password: passwordSchema, - confirmPassword: passwordSchema - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: "Passwords do not match" - }); - -const mfaSchema = z.object({ - code: z.string().length(6, { message: "Invalid code" }) -}); - export type ResetPasswordFormProps = { emailParam?: string; tokenParam?: string; @@ -81,6 +66,7 @@ export default function ResetPasswordForm({ const [error, setError] = useState- Create an account to get started + {t('authCreateAccount')}
- To accept the invite, you must log in or create an - account. + {t('inviteAlreadyDescription')}
- Already have an account?{" "} + {t('signupQuestion')}{" "} - Log in + {t('login')}
> diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index 7d68263e..cbe1e5fb 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -37,13 +37,7 @@ import { formatAxiosError } from "@app/lib/api";; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { cleanRedirect } from "@app/lib/cleanRedirect"; - -const FormSchema = z.object({ - email: z.string().email({ message: "Invalid email address" }), - pin: z.string().min(8, { - message: "Your verification code must be 8 characters.", - }), -}); +import { useTranslations } from "next-intl"; export type VerifyEmailFormProps = { email: string; @@ -55,6 +49,7 @@ export default function VerifyEmailForm({ redirect, }: VerifyEmailFormProps) { const router = useRouter(); + const t = useTranslations(); const [error, setError] = useState- Invalid or expired license keys detected. Follow license - terms to continue using all features. + {t('componentsInvalidKey')}
- License Violation: This server is using{" "} - {licenseStatus.usedSites} sites which exceeds its - licensed limit of {licenseStatus.maxSites} sites. Follow - license terms to continue using all features. + {t('componentsLicenseViolation', {usedSites: licenseStatus.usedSites, maxSites: licenseStatus.maxSites})}
- You are not currently a member of any organizations. + t('componentsErrorNoMember')
) : ( @@ -64,7 +66,7 @@ export default function OrganizationLanding({ size="lg" >- We're sorry, but it looks like the invite you're trying - to access has not been accepted or is no longer valid. + {t('inviteErrorNotValid')}
- We're sorry, but it looks like the invite you're trying - to access is not for this user. + {t('inviteErrorUser')}
- Please make sure you're logged in as the correct user. + {t('inviteLoginUser')}
- We're sorry, but it looks like the invite you're trying - to access is not for a user that exists. + {t('inviteErrorNoUser')}
- Please create an account first. + {t('inviteCreateUser')}
The invite link is invalid.
+{t('inviteInvalidDescription')}
> ); } @@ -52,15 +54,13 @@ export default async function InvitePage(props: { } function cardType() { - if (error.includes("Invite is not for this user")) { + if (error.includes(t('inviteErrorWrongUser'))) { return "wrong_user"; } else if ( - error.includes( - "User does not exist. Please create an account first." - ) + error.includes(t('inviteErrorUserNotExists')) ) { return "user_does_not_exist"; - } else if (error.includes("You must be logged in to accept an invite")) { + } else if (error.includes(t('inviteErrorLoginRequired'))) { return "not_logged_in"; } else { return "rejected"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 22b478be..dd02c489 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,6 +13,8 @@ import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; import { GetLicenseStatusResponse } from "@server/routers/license"; import LicenseViolation from "./components/LicenseViolation"; import { cache } from "react"; +import { NextIntlClientProvider } from "next-intl"; +import { getLocale } from "next-intl/server"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -30,6 +32,7 @@ export default async function RootLayout({ children: React.ReactNode; }>) { const env = pullEnv(); + const locale = await getLocale(); let supporterData = { visible: true @@ -50,31 +53,33 @@ export default async function RootLayout({ const licenseStatus = licenseStatusRes.data.data; return ( - + -- Oops! The page you're looking for doesn't exist. + {t('pageNotFoundDescription')}
- Two-Factor Authentication Disabled + {t('otpRemoveSuccess')}
- Two-factor authentication has been disabled for - your account. You can enable it again at any - time. + {t('otpRemoveSuccessMessage')}
- Scan this QR code with your authenticator app or - enter the secret key manually: + {t('otpSetupScanQr')}
- Two-Factor Authentication Enabled + {t('otpSetupSuccess')}
- Your account is now more secure. Don't forget to - save your backup codes. + {t('otpSetupSuccessStoreBackupCodes')}
- Enter the code from your authenticator app or one of - your single-use backup codes. + {t('otpAuthDescription')}
- This feature is only available in the Professional - Edition. + {t('licenseTierProfessionalRequiredDescription')}
- Signed in as + {t('signingAs')}
{user.email || user.name || user.username} @@ -100,11 +105,11 @@ export default function ProfileIcon() {
- Server Admin + {t('serverAdmin')}
) : (- {user.idpName || "Internal"} + {user.idpName || t('idpNameInternal')}
)}- Purchase a supporter key to help us continue - developing Pangolin for the community. Your - contribution allows us to commit more time to - maintain and add new features to the application for - everyone. We will never use this to paywall - features. This is separate from any Commercial - Edition. + {t('supportKeyDescription')}
- You will also get to adopt and meet your very own - pet Pangolin! + {t('supportKeyPet')}
- Payments are processed via GitHub. Afterward, you - can retrieve your key on{" "} + {t('supportKeyPurchase')}{" "} - our website + {t('supportKeyPurchaseLink')} {" "} - and redeem it here.{" "} + {t('supportKeyPurchase2')}{" "} - Learn more. + {t('supportKeyLearnMore')}
- Please select the option that best suits you. + {t('supportKeyOptions')}
$95
@@ -239,19 +232,19 @@ export default function SupporterStatus() {$25
@@ -282,19 +275,19 @@ export default function SupporterStatus() {- These are the tags you've entered. + {t('tagsEnteredDescription')}