Compare commits

..

90 Commits

Author SHA1 Message Date
Owen Schwartz 68ab3b1474 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-22 11:32:14 -04:00
Owen Schwartz f1af569743 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-22 11:32:12 -04:00
Owen Schwartz 9381b49017 New translations en-us.json (Turkish)
[ci skip]
2026-09-22 11:32:10 -04:00
Owen Schwartz 8831818994 New translations en-us.json (Russian)
[ci skip]
2026-09-22 11:32:07 -04:00
Owen Schwartz 07d5ac3de9 New translations en-us.json (Portuguese)
[ci skip]
2026-09-22 11:32:05 -04:00
Owen Schwartz e08181612f New translations en-us.json (Polish)
[ci skip]
2026-09-22 11:32:03 -04:00
Owen Schwartz 6c6d3aa589 New translations en-us.json (Dutch)
[ci skip]
2026-09-22 11:32:00 -04:00
Owen Schwartz d0b8e1c01b New translations en-us.json (Korean)
[ci skip]
2026-09-22 11:31:58 -04:00
Owen Schwartz 410dab6ea3 New translations en-us.json (Italian)
[ci skip]
2026-09-22 11:31:56 -04:00
Owen Schwartz f247317bf6 New translations en-us.json (German)
[ci skip]
2026-09-22 11:31:53 -04:00
Owen Schwartz a88f596b3e New translations en-us.json (Danish)
[ci skip]
2026-09-22 11:31:51 -04:00
Owen Schwartz 255d9097cc New translations en-us.json (Czech)
[ci skip]
2026-09-22 11:31:49 -04:00
Owen Schwartz b0ae77b690 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-22 11:31:46 -04:00
Owen Schwartz 72e5bb4483 New translations en-us.json (Spanish)
[ci skip]
2026-09-22 11:31:44 -04:00
Owen Schwartz 73a7284e1d New translations en-us.json (French)
[ci skip]
2026-09-22 11:31:41 -04:00
Owen Schwartz 3f679bbf33 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-22 09:59:40 -04:00
Owen Schwartz bd94f23924 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-22 09:59:37 -04:00
Owen Schwartz 0f629913ef New translations en-us.json (Turkish)
[ci skip]
2026-09-22 09:59:34 -04:00
Owen Schwartz a36066d565 New translations en-us.json (Russian)
[ci skip]
2026-09-22 09:59:32 -04:00
Owen Schwartz 506e69a9a7 New translations en-us.json (Portuguese)
[ci skip]
2026-09-22 09:59:29 -04:00
Owen Schwartz ca2c5a5eb4 New translations en-us.json (Polish)
[ci skip]
2026-09-22 09:59:26 -04:00
Owen Schwartz b0599c0aa0 New translations en-us.json (Dutch)
[ci skip]
2026-09-22 09:59:24 -04:00
Owen Schwartz 6e16625f33 New translations en-us.json (Korean)
[ci skip]
2026-09-22 09:59:22 -04:00
Owen Schwartz 5d81c19e3b New translations en-us.json (Italian)
[ci skip]
2026-09-22 09:59:19 -04:00
Owen Schwartz 8dec4cd9d3 New translations en-us.json (German)
[ci skip]
2026-09-22 09:59:17 -04:00
Owen Schwartz b00b355c96 New translations en-us.json (Danish)
[ci skip]
2026-09-22 09:59:14 -04:00
Owen Schwartz 7a29bd5305 New translations en-us.json (Czech)
[ci skip]
2026-09-22 09:59:11 -04:00
Owen Schwartz 37cc1a7a44 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-22 09:59:08 -04:00
Owen Schwartz 406ca09d98 New translations en-us.json (Spanish)
[ci skip]
2026-09-22 09:59:06 -04:00
Owen Schwartz a1f71b3284 New translations en-us.json (French)
[ci skip]
2026-09-22 09:59:04 -04:00
Owen Schwartz 8f6fde4f74 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-21 15:18:59 -04:00
Owen Schwartz 502383b72d New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-21 15:18:55 -04:00
Owen Schwartz 8ac4d32c23 New translations en-us.json (Turkish)
[ci skip]
2026-09-21 15:18:53 -04:00
Owen Schwartz bea8e987e1 New translations en-us.json (Russian)
[ci skip]
2026-09-21 15:18:51 -04:00
Owen Schwartz abbee0ccf6 New translations en-us.json (Portuguese)
[ci skip]
2026-09-21 15:18:48 -04:00
Owen Schwartz c41aeaa9ff New translations en-us.json (Polish)
[ci skip]
2026-09-21 15:18:46 -04:00
Owen Schwartz 39c0bb9f81 New translations en-us.json (Dutch)
[ci skip]
2026-09-21 15:18:44 -04:00
Owen Schwartz e5ed8852ca New translations en-us.json (Korean)
[ci skip]
2026-09-21 15:18:42 -04:00
Owen Schwartz b67ae66ea0 New translations en-us.json (Italian)
[ci skip]
2026-09-21 15:18:39 -04:00
Owen Schwartz 4e0979e56f New translations en-us.json (German)
[ci skip]
2026-09-21 15:18:37 -04:00
Owen Schwartz 198d10e9dc New translations en-us.json (Danish)
[ci skip]
2026-09-21 15:18:35 -04:00
Owen Schwartz 3d76959b45 New translations en-us.json (Czech)
[ci skip]
2026-09-21 15:18:33 -04:00
Owen Schwartz fc20f13d0e New translations en-us.json (Bulgarian)
[ci skip]
2026-09-21 15:18:30 -04:00
Owen Schwartz e74af63f10 New translations en-us.json (Spanish)
[ci skip]
2026-09-21 15:18:28 -04:00
Owen Schwartz 878ce1be56 New translations en-us.json (French)
[ci skip]
2026-09-21 15:18:26 -04:00
Owen Schwartz d8a84c78aa New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-21 12:21:34 -04:00
Owen Schwartz 4d133fecf7 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-21 12:21:31 -04:00
Owen Schwartz c903fd7e25 New translations en-us.json (Turkish)
[ci skip]
2026-09-21 12:21:29 -04:00
Owen Schwartz 48cc9bde40 New translations en-us.json (Russian)
[ci skip]
2026-09-21 12:21:27 -04:00
Owen Schwartz 17cf6b7df3 New translations en-us.json (Portuguese)
[ci skip]
2026-09-21 12:21:24 -04:00
Owen Schwartz 2cb6587693 New translations en-us.json (Polish)
[ci skip]
2026-09-21 12:21:22 -04:00
Owen Schwartz 3efa290354 New translations en-us.json (Dutch)
[ci skip]
2026-09-21 12:21:20 -04:00
Owen Schwartz 0a43a39a20 New translations en-us.json (Korean)
[ci skip]
2026-09-21 12:21:17 -04:00
Owen Schwartz 2bd1f2f830 New translations en-us.json (Italian)
[ci skip]
2026-09-21 12:21:15 -04:00
Owen Schwartz d9066b9f1e New translations en-us.json (German)
[ci skip]
2026-09-21 12:21:12 -04:00
Owen Schwartz 9388f963dd New translations en-us.json (Danish)
[ci skip]
2026-09-21 12:21:10 -04:00
Owen Schwartz e4ba12ff7f New translations en-us.json (Czech)
[ci skip]
2026-09-21 12:21:08 -04:00
Owen Schwartz 8483e85991 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-21 12:21:05 -04:00
Owen Schwartz fe7919163f New translations en-us.json (Spanish)
[ci skip]
2026-09-21 12:21:03 -04:00
Owen Schwartz 0fe1529af7 New translations en-us.json (French)
[ci skip]
2026-09-21 12:21:00 -04:00
Owen Schwartz b1cd252ea6 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-17 13:56:08 -04:00
Owen Schwartz 0801c71cf2 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-17 13:56:06 -04:00
Owen Schwartz aea0a2779c New translations en-us.json (Turkish)
[ci skip]
2026-09-17 13:56:03 -04:00
Owen Schwartz 89909f3b35 New translations en-us.json (Russian)
[ci skip]
2026-09-17 13:56:01 -04:00
Owen Schwartz e54df0c592 New translations en-us.json (Portuguese)
[ci skip]
2026-09-17 13:55:58 -04:00
Owen Schwartz 8c579b69e2 New translations en-us.json (Polish)
[ci skip]
2026-09-17 13:55:56 -04:00
Owen Schwartz 1ab28781e3 New translations en-us.json (Dutch)
[ci skip]
2026-09-17 13:55:53 -04:00
Owen Schwartz 8c973e5455 New translations en-us.json (Korean)
[ci skip]
2026-09-17 13:55:51 -04:00
Owen Schwartz b1c26b700b New translations en-us.json (Italian)
[ci skip]
2026-09-17 13:55:49 -04:00
Owen Schwartz aada83e57f New translations en-us.json (German)
[ci skip]
2026-09-17 13:55:46 -04:00
Owen Schwartz 5fc03bc2e5 New translations en-us.json (Danish)
[ci skip]
2026-09-17 13:55:44 -04:00
Owen Schwartz a75cc9b723 New translations en-us.json (Czech)
[ci skip]
2026-09-17 13:55:42 -04:00
Owen Schwartz 6f1b3266c0 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-17 13:55:39 -04:00
Owen Schwartz 5681ec06cd New translations en-us.json (Spanish)
[ci skip]
2026-09-17 13:55:36 -04:00
Owen Schwartz f5b2058112 New translations en-us.json (French)
[ci skip]
2026-09-17 13:55:34 -04:00
Owen Schwartz 7f9fe3c67b New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-16 13:35:12 -04:00
Owen Schwartz b617670fd8 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-16 13:35:10 -04:00
Owen Schwartz 53d4433d87 New translations en-us.json (Turkish)
[ci skip]
2026-09-16 13:35:08 -04:00
Owen Schwartz 36bbb652cc New translations en-us.json (Russian)
[ci skip]
2026-09-16 13:35:06 -04:00
Owen Schwartz 0394289aab New translations en-us.json (Portuguese)
[ci skip]
2026-09-16 13:35:03 -04:00
Owen Schwartz 52e9a8d43f New translations en-us.json (Polish)
[ci skip]
2026-09-16 13:35:01 -04:00
Owen Schwartz 72cfe81727 New translations en-us.json (Dutch)
[ci skip]
2026-09-16 13:34:59 -04:00
Owen Schwartz ed1a7c2723 New translations en-us.json (Korean)
[ci skip]
2026-09-16 13:34:57 -04:00
Owen Schwartz 8a05885192 New translations en-us.json (Italian)
[ci skip]
2026-09-16 13:34:55 -04:00
Owen Schwartz fed9d4b4b5 New translations en-us.json (German)
[ci skip]
2026-09-16 13:34:52 -04:00
Owen Schwartz b0d338456f New translations en-us.json (Danish)
[ci skip]
2026-09-16 13:34:50 -04:00
Owen Schwartz 9442ad2214 New translations en-us.json (Czech)
[ci skip]
2026-09-16 13:34:48 -04:00
Owen Schwartz 3fbe8d0618 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-16 13:34:46 -04:00
Owen Schwartz 86a9eadcff New translations en-us.json (Spanish)
[ci skip]
2026-09-16 13:34:43 -04:00
Owen Schwartz 6a7be975ed New translations en-us.json (French)
[ci skip]
2026-09-16 13:34:41 -04:00
56 changed files with 680 additions and 1216 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# FROM node:24.18.1-slim AS base
FROM public.ecr.aws/docker/library/node:26.9.0-slim AS base
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS base
WORKDIR /app
@@ -33,7 +33,7 @@ FROM base AS builder
RUN npm ci --omit=dev
# FROM node:24.18.1-slim AS runner
FROM public.ecr.aws/docker/library/node:26.9.0-slim AS runner
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
WORKDIR /app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:26.9.0-alpine
FROM node:24.18.1-alpine
WORKDIR /app
-1
View File
@@ -142,7 +142,6 @@ Give users a landing page to quickly find and open the resources they can access
* Create reusable views for common access patterns
<img src="public/screenshots/resource-launcher.png" alt="Resource Launcher" width="100%" />
<img src="public/screenshots/resource-launcher-expanded.png" alt="Resource Launcher Details Panel" width="100%" />
## Download Clients
+2 -2
View File
@@ -2,8 +2,8 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { setAdminCredentials } from "./commands/setAdminCredentials";
import { resetUserSecurityKeys } from "./commands/resetUserSecurityKeys";
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
+1 -1
View File
@@ -1,7 +1,7 @@
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:ee-postgresql-latest
image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin
restart: unless-stopped
volumes:
+1 -1
View File
@@ -1,7 +1,7 @@
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:ee-postgresql-latest
image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin
restart: unless-stopped
volumes:
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Сигурни ли сте, че искате да рестартирате WireGuard тунела за <b>{name}</b>? Сайтът ще изгуби връзка за кратко.",
"siteRestartWarning": "Сайтът ще се изключи за кратко, докато тунелът се рестартира.",
"siteRestarted": "Сайтът е рестартиран",
"siteRestartedDescription": "WireGuard тунелът е рестартиран.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Неуспешно рестартиране на сайта",
"siteErrorRestartDescription": "Възникна грешка при рестартирането на сайта.",
"siteSettingDescription": "Конфигурирайте настройките на сайта",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Създайте и управлявайте ресурси, които са достъпни само чрез свързан клиент.",
"privateResourcesBannerTitle": "Достъп до частни ресурси с нулево доверие.",
"privateResourcesBannerDescription": "Частните ресурси използват сигурност с нулево доверие, осигурявайки че потребителите и машините могат да имат само достъп до ресурси, които вие изрично предоставяте. Свържете потребителските устройства или машинните клиенти, за да получите достъп до тези ресурси чрез сигурна виртуална частна мрежа.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Търсене на ресурси...",
"resourceAdd": "Добавете ресурс",
"resourceErrorDelte": "Грешка при изтриване на ресурс",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Отключете корпоративните функции за вашият хостинг на Pangolin. Закупете лицензионен ключ, за да активирате премиум възможности, след това го добавете по-долу.",
"licenseBannerGetLicense": "Вземете лиценз",
"licenseBannerViewDocs": "Преглед на документацията",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Комюнити издание",
"licenseAboutDescription": "Това е за бизнес и корпоративни потребители, които използват Pangolin в търговска среда. Ако използвате Pangolin за лична употреба, можете да игнорирате този раздел.",
"licenseKeyActivated": "Лицензионният ключ е активиран",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Сигурни ли сте, че искате да премахнете лицензионния ключ?",
"licenseKeyDelete": "Изтриване на лицензионен ключ",
"licenseKeyDeleteConfirm": "Потвърдете изтриването на лицензионен ключ",
"licenseTitle": "Управление на лицензионния статус",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Преглед и управление на лицензионни ключове в системата",
"licenseHost": "Лиценз за хост",
"licenseHostDescription": "Управление на главния лицензионен ключ за хоста.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Не е лицензиран",
"hostId": "Идентификатор на хост",
"licenseReckeckAll": "Повторно проверяване на всички ключове",
@@ -586,7 +593,16 @@
"licensePurchase": "Закупуване на лиценз",
"licensePurchaseSites": "Закупуване на допълнителни сайтове",
"licenseSitesUsedMax": "{usedSites} от {maxSites} сайтове използвани",
"licenseSitesUsed": "{count, plural, =0 {# сайта} one {# сайт} other {# сайта}} в системата.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Изберете колко сайтове искате да {selectedMode, select, license {закупите лиценз за. Можете винаги да добавите повече сайтове по-късно.} other {добавите към съществуващия си лиценз.}}",
"licenseFee": "Такса за лиценз",
"licensePriceSite": "Цена на сайт",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Всяко правило трябва да има уникален номер на приоритет.",
"rulesErrorValidation": "Невалидни правила",
"rulesErrorValidationRuleDescription": "Правило {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Изберете валиден тип съвпадение (път, IP, CIDR, държава, регион или ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Въведете стойност за това правило.",
"rulesErrorInvalidCountry": "Невалидна държава",
"rulesErrorInvalidCountryDescription": "Изберете валидна държава.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Опити за повторно",
"expectedResponseCodes": "Очаквани кодове за отговор",
"expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.",
"customHeaders": "Персонализирани заглавия",
"customHeadersDescription": "Add custom headers to be sent when proxying requests. One per line in the format Header-Name: value",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Заглавията трябва да бъдат във формат: Име на заглавието: стойност.",
"saveHealthCheck": "Запазване на проверка на здравето",
"healthCheckSaved": "Проверка на здравето е запазена",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Показване на лентата с инструменти",
"sessionToolbarHide": "Скриване на лентата с инструменти",
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта",
"check": "Проверка"
"check": "Проверка",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Opravdu chcete restartovat WireGuard tunel pro <b>{name}</b>? Lokalita krátce ztratí konektivitu.",
"siteRestartWarning": "Lokalita bude krátce odpojena, zatímco se tunel restartuje.",
"siteRestarted": "Lokalita restartována",
"siteRestartedDescription": "Tunel WireGuard byl restartován.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Nepodařilo se restartovat lokalitu",
"siteErrorRestartDescription": "Při restartování lokality došlo k chybě.",
"siteSettingDescription": "Konfigurace nastavení na webu",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Vytváření a správa zdrojů, které jsou přístupné pouze prostřednictvím připojeného klienta",
"privateResourcesBannerTitle": "Zero-Trust soukromý přístup",
"privateResourcesBannerDescription": "Soukromé prostředky používají zero-trust zabezpečení, což zajišťuje, že uživatelé a zařízení mohou získat přístup pouze k prostředkům, k nimž máte explicitní práva. Připojte zařízení uživatele nebo klientské stroje, abyste získali přístup k těmto prostředkům přes zabezpečenou virtuální soukromou síť.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Prohledat zdroje...",
"resourceAdd": "Přidat zdroj",
"resourceErrorDelte": "Chyba při odstraňování zdroje",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Odemkněte firemní funkce pro vaši samohostovanou instanci Pangolin. Zakupte si licenční klíč pro aktivaci prémiových možností a poté jej přidejte níže.",
"licenseBannerGetLicense": "Zakoupit licenci",
"licenseBannerViewDocs": "Zobrazit dokumentaci",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Komunitní edice",
"licenseAboutDescription": "To je pro obchodní a podnikové uživatele, kteří používají Pangolin v komerčním prostředí. Pokud používáte Pangolin pro osobní použití, můžete tuto sekci ignorovat.",
"licenseKeyActivated": "Licenční klíč aktivován",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Jste si jisti, že chcete odstranit licenční klíč?",
"licenseKeyDelete": "Odstranit licenční klíč",
"licenseKeyDeleteConfirm": "Potvrdit odstranění licenčního klíče",
"licenseTitle": "Správa stavu licence",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Zobrazit a spravovat licenční klíče v systému",
"licenseHost": "Licence hostitele",
"licenseHostDescription": "Správa hlavního licenčního klíče pro hostitele.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Bez licence",
"hostId": "ID hostitele",
"licenseReckeckAll": "Znovu zobrazit všechny klíče",
@@ -586,7 +593,16 @@
"licensePurchase": "Zakoupit licenci",
"licensePurchaseSites": "Zakoupit další stránky",
"licenseSitesUsedMax": "{usedSites} použitých stránek {maxSites}",
"licenseSitesUsed": "{count, plural, =0 {# stránek} one {# stránky} other {# stránek}}",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Vyberte kolik stránek chcete {selectedMode, select, license {Zakupte si licenci. Vždy můžete přidat více webů později.} other {Přidejte k vaší existující licenci.}}",
"licenseFee": "Licenční poplatek",
"licensePriceSite": "Cena za stránku",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Každé pravidlo musí mít unikátní číslo priority.",
"rulesErrorValidation": "Neplatná pravidla",
"rulesErrorValidationRuleDescription": "Pravidlo {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Vyberte platný typ shody (cesta, IP, CIDR, země, oblast nebo ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Zadejte hodnotu pro toto pravidlo.",
"rulesErrorInvalidCountry": "Neplatná země",
"rulesErrorInvalidCountryDescription": "Vyberte platnou zemi.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Opakovat pokusy",
"expectedResponseCodes": "Očekávané kódy odezvy",
"expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.",
"customHeaders": "Vlastní záhlaví",
"customHeadersDescription": "Záhlaví oddělená nová řádka: hodnota",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Headers must be in the format: Header-Name: value.",
"saveHealthCheck": "Uložit kontrolu stavu",
"healthCheckSaved": "Kontrola stavu uložena",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Zobrazit panel nástrojů",
"sessionToolbarHide": "Skrýt panel nástrojů",
"actionUpdateSiteApprovals": "Aktualizovat schválení webu",
"check": "Zkontrolovat"
"check": "Zkontrolovat",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Er du sikker på, at du vil genstarte WireGuard-tunnelen for <b>{name}</b>? Sitet vil kortvarigt miste forbindelsen.",
"siteRestartWarning": "Sitet vil kortvarigt afbryde forbindelse, mens tunnelen genstarter.",
"siteRestarted": "Site genstartet",
"siteRestartedDescription": "WireGuard-tunnelen er blevet genstartet.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Kunne ikke genstarte site",
"siteErrorRestartDescription": "En fejl opstod, mens sitet blev genstartet.",
"siteSettingDescription": "Konfigurer indstillingerne for sitet",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Oprette og administrer ressourcer som kun er tilgængelige via en tilsluttet klient",
"privateResourcesBannerTitle": "Zero-Trust privat adgang",
"privateResourcesBannerDescription": "Private ressourcer bruger Zero-Trust-sikkerhed og sikrer, at brugere og maskiner kun kan få adgang til ressourcer, du eksplicit giver tilladelse til. Tilslut brugerenheder eller maskinklienter for at få adgang til disse ressourcer via et sikkert virtuelt privat netværk.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Søg i ressourcer...",
"resourceAdd": "Tilføj ressource",
"resourceErrorDelte": "Fejl ved sletning af ressource",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Lås op for virksomhedsfunktioner på din selvhostede Pangolin-instans. Køb en licensnøgle for at aktivere premium-funktioner, og indtast den nedenfor.",
"licenseBannerGetLicense": "Få en licens",
"licenseBannerViewDocs": "Vis dokumentation",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Community-udgave",
"licenseAboutDescription": "Dette er for virksomheds- og enterprise-brugere som bruger Pangolin i et kommercielt miljø. Hvis du bruger Pangolin til personlig brug, kan du ignorere denne sektionen.",
"licenseKeyActivated": "Licensnøgle aktiveret",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Er du sikker på at du vil slette licensnøglen?",
"licenseKeyDelete": "Slet Licensnøgle",
"licenseKeyDeleteConfirm": "Bekræft sletning af licensnøgle",
"licenseTitle": "Administrer licensstatus",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Se og administrer licensnøgler i systemet",
"licenseHost": "Hostlicens",
"licenseHostDescription": "Administrer hovedlicensnøglen for verten.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Ikke licenseret",
"hostId": "Host-ID",
"licenseReckeckAll": "Bekræft alle nøgler",
@@ -586,7 +593,16 @@
"licensePurchase": "Køb licens",
"licensePurchaseSites": "Køb flere sites",
"licenseSitesUsedMax": "{usedSites} af {maxSites} sites brugt",
"licenseSitesUsed": "{count, plural, =0 {ingen sites} one {ét site} other {# sites}} i systemet.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Vælg hvor mange sites du vil {selectedMode, select, license {købe en licens for. Du kan altid tilføje flere sites senere.} other {tilføje din eksisterende licens.}}",
"licenseFee": "Licensafgift",
"licensePriceSite": "Pris per site",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Hver regel skal have et unikt prioritetstall.",
"rulesErrorValidation": "Ugyldige regler",
"rulesErrorValidationRuleDescription": "Regel {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Vælg en gyldig matchtype (sti, IP, CIDR, land, region eller ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Indtast en værdi for denne reglen.",
"rulesErrorInvalidCountry": "Ugyldig land",
"rulesErrorInvalidCountryDescription": "Vælg et gyldig land.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Forsøg igen",
"expectedResponseCodes": "Forventede svarkoder",
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sund status. Hvis den bliver stående tom, regnes 200-300 som sund.",
"customHeaders": "Brugerdefinerede headers",
"customHeadersDescription": "Headers som er adskilt med linje: Overskriftsnavn: værdi",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Header skal være i formatet: header-navn: værdi.",
"saveHealthCheck": "Gem Sundhedstjek",
"healthCheckSaved": "Sundhedstjek Gemt",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Vis værktøjslinje",
"sessionToolbarHide": "Skjul værktøjslinje",
"actionUpdateSiteApprovals": "Opdater godkendelser på site",
"check": "Tjek"
"check": "Tjek",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Sind Sie sicher, dass Sie den WireGuard-Tunnel für <b>{name}</b> neu starten möchten? Der Standort wird kurzzeitig die Konnektivität verlieren.",
"siteRestartWarning": "Der Standort wird kurzzeitig getrennt, während der Tunnel neu gestartet wird.",
"siteRestarted": "Standort neu gestartet",
"siteRestartedDescription": "Der WireGuard-Tunnel wurde neu gestartet.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Fehler beim Neustart des Standorts",
"siteErrorRestartDescription": "Ein Fehler ist aufgetreten, während der Standort neu gestartet wurde.",
"siteSettingDescription": "Standorteinstellungen konfigurieren",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Erstelle und verwalte Ressourcen, die nur über einen verbundenen Client zugänglich sind",
"privateResourcesBannerTitle": "Zero-Trust-Zugriff auf private Ressourcen",
"privateResourcesBannerDescription": "Private Ressourcen nutzen Zero-Trust und stellen sicher, dass Benutzer und Maschinen nur auf Ressourcen zugreifen können, die Sie explizit gewähren. Verbinden Sie Benutzergeräte oder Maschinen-Clients, um auf diese Ressourcen über ein sicheres virtuelles privates Netzwerk zuzugreifen.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Suche Ressourcen...",
"resourceAdd": "Ressource hinzufügen",
"resourceErrorDelte": "Fehler beim Löschen der Ressource",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Schalten Sie Unternehmensfunktionen für Ihre selbstgehostete Pangolin-Instanz frei. Kaufen Sie einen Lizenzschlüssel, um Premium-Funktionen zu aktivieren, und fügen Sie ihn dann unten hinzu.",
"licenseBannerGetLicense": "Lizenz erhalten",
"licenseBannerViewDocs": "Dokumentation anzeigen",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Community-Edition",
"licenseAboutDescription": "Dies ist für Geschäfts- und Unternehmensanwender, die Pangolin in einem kommerziellen Umfeld einsetzen. Wenn Sie Pangolin für den persönlichen Gebrauch verwenden, können Sie diesen Abschnitt ignorieren.",
"licenseKeyActivated": "Lizenzschlüssel aktiviert",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Sind Sie sicher, dass Sie den Lizenzschlüssel löschen möchten?",
"licenseKeyDelete": "Lizenzschlüssel löschen",
"licenseKeyDeleteConfirm": "Lizenzschlüssel löschen bestätigen",
"licenseTitle": "Lizenzstatus verwalten",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Lizenzschlüssel im System anzeigen und verwalten",
"licenseHost": "Hostlizenz",
"licenseHostDescription": "Verwalten Sie den Haupt-Lizenzschlüssel für den Host.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Nicht lizenziert",
"hostId": "Host-ID",
"licenseReckeckAll": "Überprüfe alle Schlüssel",
@@ -586,7 +593,16 @@
"licensePurchase": "Lizenz kaufen",
"licensePurchaseSites": "Zusätzliche Standorte kaufen\n",
"licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet",
"licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Wähle aus, für wie viele Standorte du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Standorte hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}",
"licenseFee": "Lizenzgebühr",
"licensePriceSite": "Preis pro Standort",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Jede Regel muss eine eindeutige Prioritätsnummer haben.",
"rulesErrorValidation": "Ungültige Regeln",
"rulesErrorValidationRuleDescription": "Regel {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Wählen Sie einen gültigen Vergleichstyp (Pfad, IP, CIDR, Land, Region oder ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Geben Sie einen Wert für diese Regel ein.",
"rulesErrorInvalidCountry": "Ungültiges Land",
"rulesErrorInvalidCountryDescription": "Wählen Sie ein gültiges Land aus.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Wiederholungsversuche",
"expectedResponseCodes": "Erwartete Antwortcodes",
"expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.",
"customHeaders": "Eigene Kopfzeilen",
"customHeadersDescription": "Header neue Zeile getrennt: Header-Name: Wert",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Header müssen im Format Header-Name: Wert sein.",
"saveHealthCheck": "Gesundheits-Check speichern",
"healthCheckSaved": "Gesundheits-Check gespeichert",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Werkzeugleiste zeigen",
"sessionToolbarHide": "Werkzeugleiste ausblenden",
"actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren",
"check": "Prüfen"
"check": "Prüfen",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
-3
View File
@@ -234,9 +234,6 @@
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access",
"privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Search resources...",
"resourceAdd": "Add Resource",
"resourceErrorDelte": "Error deleting resource",
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "¿Estás seguro de que deseas reiniciar el túnel WireGuard para <b>{name}</b>? El sitio perderá conectividad brevemente.",
"siteRestartWarning": "El sitio se desconectará brevemente mientras se reinicia el túnel.",
"siteRestarted": "Sitio reiniciado",
"siteRestartedDescription": "El túnel WireGuard ha sido reiniciado.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Error al reiniciar el sitio",
"siteErrorRestartDescription": "Se ha producido un error al reiniciar el sitio.",
"siteSettingDescription": "Configurar los ajustes en el sitio",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Crear y administrar recursos que sólo son accesibles a través de un cliente conectado",
"privateResourcesBannerTitle": "Acceso privado de confianza cero",
"privateResourcesBannerDescription": "Los recursos privados usan seguridad de confianza cero, asegurando que usuarios y máquinas solo puedan acceder a los recursos que usted conceda explícitamente. Conecte dispositivos de usuario o clientes de máquinas para acceder a estos recursos a través de una red privada virtual segura.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Buscar recursos...",
"resourceAdd": "Añadir Recurso",
"resourceErrorDelte": "Error al eliminar el recurso",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Desbloquea funciones empresariales para tu instancia autohospedada de Pangolin. Compra una clave de licencia para activar capacidades premium, luego agréguela a continuación.",
"licenseBannerGetLicense": "Obtener una Licencia",
"licenseBannerViewDocs": "Ver Documentación",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Edición comunitaria",
"licenseAboutDescription": "Esto es para usuarios empresariales y empresariales que utilizan Pangolin en un entorno comercial. Si estás usando Pangolin para uso personal, puedes ignorar esta sección.",
"licenseKeyActivated": "Clave de licencia activada",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "¿Está seguro que desea eliminar la clave de licencia?",
"licenseKeyDelete": "Eliminar clave de licencia",
"licenseKeyDeleteConfirm": "Confirmar eliminar clave de licencia",
"licenseTitle": "Administrar estado de licencia",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Ver y administrar claves de licencia en el sistema",
"licenseHost": "Licencia de host",
"licenseHostDescription": "Administrar la clave de licencia principal para el host.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Sin licencia",
"hostId": "ID del Host",
"licenseReckeckAll": "Revisar todas las claves",
@@ -586,7 +593,16 @@
"licensePurchase": "Comprar Licencia",
"licensePurchaseSites": "Comprar sitios adicionales",
"licenseSitesUsedMax": "{usedSites} de {maxSites} sitios usados",
"licenseSitesUsed": "{count, plural, =0 {# sitios} one {# sitio} other {# sitios}} en el sistema.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Elige cuántos sitios quieres {selectedMode, select, license {compra una licencia para. Siempre puedes añadir más sitios más tarde.} other {añadir a tu licencia existente.}}",
"licenseFee": "Tarifa de licencia",
"licensePriceSite": "Precio por sitio",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Cada regla debe tener un número de prioridad único.",
"rulesErrorValidation": "Reglas no válidas",
"rulesErrorValidationRuleDescription": "Regla {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Selecciona un tipo de coincidencia válido (ruta, IP, CIDR, país, región o ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Introduce un valor para esta regla.",
"rulesErrorInvalidCountry": "País no válido",
"rulesErrorInvalidCountryDescription": "Selecciona un país válido.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Intentos de Reintento",
"expectedResponseCodes": "Códigos de respuesta esperados",
"expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.",
"customHeaders": "Cabeceras personalizadas",
"customHeadersDescription": "Nueva línea de cabeceras separada: Nombre de cabecera: valor",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Los encabezados deben estar en el formato: Nombre de cabecera: valor.",
"saveHealthCheck": "Guardar Chequeo de Salud",
"healthCheckSaved": "Chequeo de Salud Guardado",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Mostrar barra de herramientas",
"sessionToolbarHide": "Ocultar barra de herramientas",
"actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio",
"check": "Comprobar"
"check": "Comprobar",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Êtes-vous sûr de vouloir redémarrer le tunnel WireGuard pour <b>{name}</b>? Le site perdra brièvement sa connectivité.",
"siteRestartWarning": "Le site sera brièvement déconnecté pendant le redémarrage du tunnel.",
"siteRestarted": "Site redémarré",
"siteRestartedDescription": "Le tunnel WireGuard a été redémarré.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Échec du redémarrage du site",
"siteErrorRestartDescription": "Une erreur s'est produite lors du redémarrage du site.",
"siteSettingDescription": "Configurer les paramètres du site",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Créer et gérer des ressources qui ne sont accessibles que via un client connecté",
"privateResourcesBannerTitle": "Accès privé sans confiance",
"privateResourcesBannerDescription": "Les ressources privées utilisent la sécurité sans confiance, garantissant que les utilisateurs et les machines ne peuvent accéder qu'aux ressources que vous accordez explicitement. Connectez les appareils utilisateur ou les clients machines à ces ressources via un réseau privé virtuel sécurisé.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Chercher des ressources...",
"resourceAdd": "Ajouter une ressource",
"resourceErrorDelte": "Erreur lors de la de suppression de la ressource",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Débloquez les fonctionnalités d'entreprise pour votre instance autohébergée de Pangolin. Achetez une clé de licence pour activer les capacités premium, puis ajoutez-la ci-dessous.",
"licenseBannerGetLicense": "Obtenez une Licence",
"licenseBannerViewDocs": "Afficher la Documentation",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Edition Communautaire",
"licenseAboutDescription": "Ceci est destiné aux entreprises qui utilisent Pangolin dans un environnement commercial. Si vous utilisez Pangolin pour un usage personnel, vous pouvez ignorer cette section.",
"licenseKeyActivated": "Clé de licence activée",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Êtes-vous sûr de vouloir supprimer la clé de licence ?",
"licenseKeyDelete": "Supprimer la clé de licence",
"licenseKeyDeleteConfirm": "Confirmer la suppression de la clé de licence",
"licenseTitle": "Gérer le statut de la licence",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Voir et gérer les clés de licence dans le système",
"licenseHost": "Licence Hôte",
"licenseHostDescription": "Gérer la clé de licence principale de l'hôte.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Pas de licence",
"hostId": "ID de l'hôte",
"licenseReckeckAll": "Revérifier toutes les clés",
@@ -586,7 +593,16 @@
"licensePurchase": "Acheter une licence",
"licensePurchaseSites": "Acheter des sites supplémentaires",
"licenseSitesUsedMax": "{usedSites} des {maxSites} sites utilisés",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} dans le système.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}",
"licenseFee": "Frais de licence",
"licensePriceSite": "Prix par site",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Chaque règle doit avoir un numéro de priorité unique.",
"rulesErrorValidation": "Règles invalides",
"rulesErrorValidationRuleDescription": "Règle {ruleNumber} : {message}",
"rulesErrorInvalidMatchTypeDescription": "Sélectionnez un type de correspondance valide (chemin, IP, CIDR, pays, région ou ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Entrez une valeur pour cette règle.",
"rulesErrorInvalidCountry": "Pays invalide",
"rulesErrorInvalidCountryDescription": "Sélectionnez un pays valide.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Tentatives de réessai",
"expectedResponseCodes": "Codes de réponse attendus",
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
"customHeaders": "En-têtes personnalisés",
"customHeadersDescription": "En-têtes séparés par une nouvelle ligne: En-nom: valeur",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Les entêtes doivent être au format : Header-Name: valeur.",
"saveHealthCheck": "Sauvegarder la vérification de l'état de santé",
"healthCheckSaved": "Vérification de l'état de santé enregistrée",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Afficher la barre d'outils",
"sessionToolbarHide": "Masquer la barre d'outils",
"actionUpdateSiteApprovals": "Mettre à jour les approbations de site",
"check": "Vérifier"
"check": "Vérifier",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Sei sicuro di voler riavviare il tunnel WireGuard per <b>{name}</b>? Il sito perderà brevemente la connettività.",
"siteRestartWarning": "Il sito si disconnette brevemente mentre il tunnel si riavvia.",
"siteRestarted": "Sito riavviato",
"siteRestartedDescription": "Il tunnel WireGuard è stato riavviato.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Impossibile riavviare il sito",
"siteErrorRestartDescription": "Si è verificato un errore durante il riavvio del sito.",
"siteSettingDescription": "Configura le impostazioni del sito",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Crea e gestisci risorse accessibili solo tramite un client connesso",
"privateResourcesBannerTitle": "Accesso Privato Zero-Trust",
"privateResourcesBannerDescription": "Le risorse private utilizzano la sicurezza zero-trust, assicurandosi che gli utenti e le macchine possano accedere solo alle risorse a cui hai concesso esplicitamente l'accesso. Collega i dispositivi utente o i client macchina per accedere a queste risorse tramite una rete privata virtuale sicura.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Cerca risorse...",
"resourceAdd": "Aggiungi Risorsa",
"resourceErrorDelte": "Errore nell'eliminare la risorsa",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Sblocca le funzionalità enterprise per la tua istanza Pangolin auto-ospitata. Acquista una chiave di licenza per attivare le capacità premium e poi aggiungila qui sotto.",
"licenseBannerGetLicense": "Ottieni una Licenza",
"licenseBannerViewDocs": "Visualizza Documentazione",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Edizione Community",
"licenseAboutDescription": "Questa sezione è per gli utenti aziendali e aziendali che utilizzano Pangolin in un ambiente commerciale. Se stai usando Pangolin per uso personale, puoi ignorare questa sezione.",
"licenseKeyActivated": "Chiave di licenza attivata",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Sei sicuro di voler eliminare la chiave di licenza?",
"licenseKeyDelete": "Elimina Chiave Di Licenza",
"licenseKeyDeleteConfirm": "Conferma Elimina Chiave Di Licenza",
"licenseTitle": "Gestisci Stato Licenza",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Visualizza e gestisci le chiavi di licenza nel sistema",
"licenseHost": "Licenza Host",
"licenseHostDescription": "Gestisci la chiave di licenza principale per l'host.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Non Licenziato",
"hostId": "ID Host",
"licenseReckeckAll": "Ricontrolla Tutte le chiavi",
@@ -586,7 +593,16 @@
"licensePurchase": "Acquista Licenza",
"licensePurchaseSites": "Acquista Siti Aggiuntivi",
"licenseSitesUsedMax": "{usedSites} di {maxSites} siti utilizzati",
"licenseSitesUsed": "{count, plural, =0 {# siti} one {# sito} other {# siti}} nel sistema.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Scegli quanti siti vuoi {selectedMode, select, license {acquista una licenza. Puoi sempre aggiungere altri siti più tardi.} other {aggiungi alla tua licenza esistente.}}",
"licenseFee": "Costo della licenza",
"licensePriceSite": "Prezzo per sito",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Ogni regola deve avere un numero di priorità univoco.",
"rulesErrorValidation": "Regole non valide",
"rulesErrorValidationRuleDescription": "Regola {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Seleziona un tipo di corrispondenza valido (percorso, IP, CIDR, paese, regione o ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Inserisci un valore per questa regola.",
"rulesErrorInvalidCountry": "Nazione non valida",
"rulesErrorInvalidCountryDescription": "Seleziona un paese valido.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Tentativi di Riprova",
"expectedResponseCodes": "Codici di Risposta Attesi",
"expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.",
"customHeaders": "Intestazioni Personalizzate",
"customHeadersDescription": "Intestazioni nuova riga separate: Intestazione-Nome: valore",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Le intestazioni devono essere nel formato: Intestazione-Nome: valore.",
"saveHealthCheck": "Salva Controllo Salute",
"healthCheckSaved": "Controllo Salute Salvato",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Mostra barra degli strumenti",
"sessionToolbarHide": "Nascondi barra degli strumenti",
"actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito",
"check": "Controlla"
"check": "Controlla",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "<b>{name}</b>의 WireGuard 터널을 재시작하시겠습니까? 이 작업으로 인해 사이트의 연결이 일시적으로 중단될 수 있습니다.",
"siteRestartWarning": "터널을 재시작하는 동안 사이트가 일시적으로 연결이 끊깁니다.",
"siteRestarted": "사이트가 재시작되었습니다",
"siteRestartedDescription": "WireGuard 터널이 재시작되었습니다.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "사이트 재시작 실패",
"siteErrorRestartDescription": "사이트를 재시작하는 중 오류가 발생했습니다.",
"siteSettingDescription": "사이트에서 설정을 구성하세요.",
@@ -234,6 +234,9 @@
"clientResourceDescription": "연결된 클라이언트를 통해서만 접근할 수 있는 리소스를 생성하고 관리하세요.",
"privateResourcesBannerTitle": "제로 트러스트 개인 접근",
"privateResourcesBannerDescription": "개인 리소스는 제로 트러스트 보안을 사용하여, 사용자와 장치가 명시적으로 허용된 리소스에만 접근할 수 있도록 보장합니다. 이러한 리소스에 접근하려면 안전한 가상 사설 네트워크를 통해 사용자 장치 또는 머신 클라이언트를 연결하십시오.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "리소스 검색...",
"resourceAdd": "리소스 추가",
"resourceErrorDelte": "리소스 삭제 중 오류 발생",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "자체 호스팅된 Pangolin 인스턴스에서 기업 기능을 잠금 해제하십시오. 라이선스 키를 구입하여 프리미엄 기능을 활성화하고 아래에 추가하십시오.",
"licenseBannerGetLicense": "라이선스 획득",
"licenseBannerViewDocs": "문서 보기",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "커뮤니티 에디션",
"licenseAboutDescription": "이것은 상업적 환경에서 Pangolin을 사용하는 비즈니스 및 기업 사용자용입니다. 개인 용도로 Pangolin을 사용하는 경우 이 섹션을 무시할 수 있습니다.",
"licenseKeyActivated": "라이센스 키가 활성화되었습니다",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "라이선스 키를 삭제하시겠습니까?",
"licenseKeyDelete": "라이센스 키 삭제",
"licenseKeyDeleteConfirm": "라이센스 키 삭제 확인",
"licenseTitle": "라이선스 상태 관리",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "시스템에서 라이센스 키를 보고 관리합니다.",
"licenseHost": "호스트 라이센스",
"licenseHostDescription": "호스트의 주요 라이센스 키를 관리합니다.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "라이센스 없음",
"hostId": "호스트 ID",
"licenseReckeckAll": "모든 키 재확인",
@@ -586,7 +593,16 @@
"licensePurchase": "라이센스 구매",
"licensePurchaseSites": "추가 사이트 구매",
"licenseSitesUsedMax": "{maxSites}개의 사이트 중 {usedSites}개 사용 중",
"licenseSitesUsed": "시스템에 {count, plural, =0 {# 사이트} one {# 사이트} other {# 사이트}}가 있습니다.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "구매할 사이트 수를 선택하세요 {selectedMode, select, license {라이센스를 구매합니다. 나중에 더 많은 사이트를 추가할 수 있습니다.} other {기존 라이센스에 추가합니다.}}",
"licenseFee": "라이선스 요금",
"licensePriceSite": "사이트당 가격",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "각 규칙은 고유한 우선순위 번호를 가져야 합니다.",
"rulesErrorValidation": "유효하지 않은 규칙",
"rulesErrorValidationRuleDescription": "규칙 {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "유효한 매칭 유형을 선택하세요 (경로, IP, CIDR, 국가, 지역, 또는 ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "이 규칙에 대한 값을 입력하세요.",
"rulesErrorInvalidCountry": "유효하지 않은 국가",
"rulesErrorInvalidCountryDescription": "유효한 국가를 선택하세요.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "재시도 횟수",
"expectedResponseCodes": "예상 응답 코드",
"expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.",
"customHeaders": "사용자 정의 헤더",
"customHeadersDescription": "헤더는 새 줄로 구분됨: Header-Name: value",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "헤더는 형식이어야 합니다: 헤더명: 값.",
"saveHealthCheck": "상태 확인 저장",
"healthCheckSaved": "상태 확인이 저장되었습니다.",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "툴바 보기",
"sessionToolbarHide": "툴바 숨기기",
"actionUpdateSiteApprovals": "사이트 승인 업데이트",
"check": "확인"
"check": "확인",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Er du sikker på at du vil starte WireGuard-tunnelen for <b>{name}</b> på nytt? Området vil midlertidig miste tilkoblingen.",
"siteRestartWarning": "Området vil kobles kort fra mens tunnelen starter om.",
"siteRestarted": "Område startet på nytt",
"siteRestartedDescription": "WireGuard-tunnelen er startet på nytt.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Kan ikke starte område på nytt",
"siteErrorRestartDescription": "En feil oppstod ved omstart av området.",
"siteSettingDescription": "Konfigurere innstillingene på nettstedet",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Opprette og administrere ressurser som bare er tilgjengelige via en tilkoblet klient",
"privateResourcesBannerTitle": "Zero-Trust privat tilgang",
"privateResourcesBannerDescription": "Private ressurser bruker Zero-Trust-sikkerhet, og sikrer at brukere og maskiner kun kan få tilgang til ressurser du eksplisitt gir tillatelse til. Koble bruker-enheter eller maskinklienter for å få tilgang til disse ressursene via et sikkert virtuelt privat nettverk.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Søk i ressurser...",
"resourceAdd": "Legg til ressurs",
"resourceErrorDelte": "Feil ved sletting av ressurs",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Lås opp bedriftsfunksjoner for din egenvertede Pangolin-instans. Kjøp en lisensnøkkel for å aktivere premium-funksjoner og legg den inn nedenfor.",
"licenseBannerGetLicense": "Få en lisens",
"licenseBannerViewDocs": "Vis dokumentasjon",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Fellesskapsutgave",
"licenseAboutDescription": "Dette er for bedrifts- og foretaksbrukere som bruker Pangolin i et kommersielt miljø. Hvis du bruker Pangolin til personlig bruk, kan du ignorere denne seksjonen.",
"licenseKeyActivated": "Lisensnøkkel aktivert",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Er du sikker på at du vil slette lisensnøkkelen?",
"licenseKeyDelete": "Slett Lisensnøkkel",
"licenseKeyDeleteConfirm": "Bekreft sletting av lisensnøkkel",
"licenseTitle": "Behandle lisensstatus",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Se og administrer lisensnøkler i systemet",
"licenseHost": "Vertslisens",
"licenseHostDescription": "Behandle hovedlisensnøkkelen for verten.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Ikke lisensiert",
"hostId": "Verts-ID",
"licenseReckeckAll": "Verifiser alle nøkler",
@@ -586,7 +593,16 @@
"licensePurchase": "Kjøp lisens",
"licensePurchaseSites": "Kjøp flere områder",
"licenseSitesUsedMax": "{usedSites} av {maxSites} områder brukt",
"licenseSitesUsed": "{count, plural, =0 {ingen områder} one {ett område} other {# områder}} i systemet.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}",
"licenseFee": "Lisensavgift",
"licensePriceSite": "Pris per område",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Hver regel må ha et unikt prioritetstall.",
"rulesErrorValidation": "Ugyldige regler",
"rulesErrorValidationRuleDescription": "Regel {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Velg en gyldig samsvarstype (sti, IP, CIDR, land, region eller ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Skriv inn en verdi for denne regelen.",
"rulesErrorInvalidCountry": "Ugyldig land",
"rulesErrorInvalidCountryDescription": "Velg et gyldig land.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Forsøk på nytt",
"expectedResponseCodes": "Forventede svarkoder",
"expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.",
"customHeaders": "Egendefinerte topptekster",
"customHeadersDescription": "Overskrifter som er adskilt med linje: Overskriftsnavn: verdi",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Topptekst må være i formatet: header-navn: verdi.",
"saveHealthCheck": "Lagre Helsekontroll",
"healthCheckSaved": "Helsekontroll Lagret",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Vis verktøylinje",
"sessionToolbarHide": "Skjul verktøylinje",
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger",
"check": "Sjekk"
"check": "Sjekk",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+34 -13
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Weet u zeker dat u de WireGuard-tunnel voor <b>{name}</b> wilt herstarten? De site zal tijdelijk geen connectiviteit hebben.",
"siteRestartWarning": "De site zal kort worden losgekoppeld terwijl de tunnel opnieuw wordt gestart.",
"siteRestarted": "Site herstart",
"siteRestartedDescription": "De WireGuard-tunnel is opnieuw gestart.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Site herstarten mislukt",
"siteErrorRestartDescription": "Er is een fout opgetreden tijdens het herstarten van de site.",
"siteSettingDescription": "Configureer de instellingen van de site",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Creëer en beheer bronnen die alleen toegankelijk zijn via een verbonden client",
"privateResourcesBannerTitle": "Privébronnen via Zero Trust",
"privateResourcesBannerDescription": "Privébronnen maken gebruik van zero trust beveiliging, wat ervoor zorgt dat gebruikers en machines alleen toegang kunnen krijgen tot resources die je specifiek toestaat. Verbind gebruikersapparaten of machines om deze middelen te benaderen via een veilig, virtueel privénetwerk.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Zoek bronnen...",
"resourceAdd": "Bron toevoegen",
"resourceErrorDelte": "Fout bij verwijderen document",
@@ -459,13 +462,13 @@
"searchApiKeys": "API sleutels zoeken...",
"apiKeysAdd": "API sleutel genereren",
"apiKeysErrorDelete": "Fout bij verwijderen API sleutel",
"apiKeysErrorDeleteMessage": "Fout bij verwijderen API- leutel",
"apiKeysErrorDeleteMessage": "Fout bij verwijderen API sleutel",
"apiKeysQuestionRemove": "Weet u zeker dat u de API sleutel van de organisatie wilt verwijderen?",
"apiKeysMessageRemove": "Eenmaal verwijderd, kan de APIsleutel niet meer worden gebruikt.",
"apiKeysDeleteConfirm": "Bevestig verwijderen API sleutel",
"apiKeysMessageRemove": "Eenmaal verwijderd, kan de API sleutel niet meer worden gebruikt.",
"apiKeysDeleteConfirm": "Bevestig Verwijderen API sleutel",
"apiKeysDelete": "API sleutel verwijderen",
"apiKeysManage": "API sleutels beheren",
"apiKeysDescription": "API sleutels worden gebruikt om te verifiëren met de integratie-API",
"apiKeysDescription": "API sleutels worden gebruikt om toegang te verifiëren met de integratie API ",
"orgsManage": "Organisaties Beheren",
"orgsDescription": "Bekijk en beheer alle organisaties op dit systeem",
"provisioningKeysTitle": "Vertrekkende sleutel",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Ontgrendel enterprise-functies voor uw zelf-gehoste Pangolin-instantie. Koop een licentiesleutel om premium mogelijkheden te activeren, voeg deze vervolgens hieronder toe.",
"licenseBannerGetLicense": "Koop een Licentie",
"licenseBannerViewDocs": "Bekijk Documentatie",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Community editie",
"licenseAboutDescription": "Dit geldt voor gebruikers van bedrijven en ondernemingen die Pangolin in gebruiken in een commerciële omgeving. Als u Pangolin gebruikt voor persoonlijk gebruik, kunt u dit gedeelte negeren.",
"licenseKeyActivated": "Licentiesleutel geactiveerd",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Weet u zeker dat u de licentiesleutel wilt verwijderen?",
"licenseKeyDelete": "Licentiesleutel verwijderen",
"licenseKeyDeleteConfirm": "Bevestig verwijderen licentiesleutel",
"licenseTitle": "Licentiestatus beheren",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Bekijk en beheer licentiesleutels in het systeem",
"licenseHost": "Host Licentie",
"licenseHostDescription": "Beheer de belangrijkste licentiesleutel voor de host.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Niet gelicentieerd",
"hostId": "Host-ID",
"licenseReckeckAll": "Alle sleutels opnieuw selecteren",
@@ -586,7 +593,16 @@
"licensePurchase": "Licentie kopen",
"licensePurchaseSites": "Extra sites kopen",
"licenseSitesUsedMax": "{usedSites} van {maxSites} sites gebruikt",
"licenseSitesUsed": "{count, plural, =0 {# locaties} one {# locatie} other {# locaties}} in het systeem.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Kies hoeveel sites je wilt {selectedMode, select, license {Koop een licentie. Je kunt later altijd meer sites toevoegen.} other {Voeg je bestaande licentie toe}}",
"licenseFee": "Licentie vergoeding",
"licensePriceSite": "Prijs per site",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Elke regel moet een uniek prioriteitsnummer hebben.",
"rulesErrorValidation": "Ongeldige regels",
"rulesErrorValidationRuleDescription": "Regel {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Selecteer een geldig matchtype (pad, IP, CIDR, land, regio of ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Voer een waarde in voor deze regel.",
"rulesErrorInvalidCountry": "Ongeldig land",
"rulesErrorInvalidCountryDescription": "Selecteer een geldig land.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Herhaal Pogingen",
"expectedResponseCodes": "Verwachte Reactiecodes",
"expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.",
"customHeaders": "Aangepaste headers",
"customHeadersDescription": "Kopregeleinde: Header-Naam: waarde",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Headers moeten in het formaat zijn: Header-Naam: waarde.",
"saveHealthCheck": "Opslaan Gezondheidscontrole",
"healthCheckSaved": "Gezondheidscontrole Opgeslagen",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Toon werkbalk",
"sessionToolbarHide": "Verberg werkbalk",
"actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken",
"check": "Controleren"
"check": "Controleren",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Czy na pewno chcesz uruchomić ponownie tunel WireGuard dla <b>{name}</b>? Strona tymczasowo straci łączność.",
"siteRestartWarning": "Strona tymczasowo rozłączy się podczas ponownego uruchamiania tunelu.",
"siteRestarted": "Strona zrestartowana",
"siteRestartedDescription": "Tunel WireGuard został ponownie uruchomiony.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Nie udało się zrestartować strony",
"siteErrorRestartDescription": "Wystąpił błąd podczas ponownego uruchamiania strony.",
"siteSettingDescription": "Skonfiguruj ustawienia na stronie",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Twórz i zarządzaj zasobami, które są dostępne tylko za pośrednictwem połączonego klienta",
"privateResourcesBannerTitle": "Zero zaufania do prywatnego dostępu",
"privateResourcesBannerDescription": "Zasoby prywatne korzystają z zabezpieczeń zero-trust, zapewniając dostęp do zasobów użytkownikom i maszynom, którym wyraźnie udzielasz dostępu. Połącz urządzenia użytkowników lub klientów maszyn z tymi zasobami przez bezpieczną prywatną sieć wirtualną.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Szukaj zasobów...",
"resourceAdd": "Dodaj zasób",
"resourceErrorDelte": "Błąd podczas usuwania zasobu",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Odblokuj funkcje korporacyjne dla swojego autonomicznego wdrożenia Pangolin. Kup klucz licencyjny, aby aktywować możliwości premium, a następnie wprowadź go poniżej.",
"licenseBannerGetLicense": "Uzyskaj licencję",
"licenseBannerViewDocs": "Zobacz dokumentację",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Edycja Społecznościowa",
"licenseAboutDescription": "Dotyczy to przedsiębiorstw i przedsiębiorstw, którzy stosują Pangolin w środowisku handlowym. Jeśli używasz Pangolin do użytku osobistego, możesz zignorować tę sekcję.",
"licenseKeyActivated": "Klucz licencyjny aktywowany",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Czy na pewno chcesz usunąć klucz licencyjny?",
"licenseKeyDelete": "Usuń klucz licencyjny",
"licenseKeyDeleteConfirm": "Potwierdź usunięcie klucza licencyjnego",
"licenseTitle": "Zarządzaj statusem licencji",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Wyświetl i zarządzaj kluczami licencyjnymi w systemie",
"licenseHost": "Licencja hosta",
"licenseHostDescription": "Zarządzaj głównym kluczem licencyjnym hosta.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Brak licencji",
"hostId": "ID hosta",
"licenseReckeckAll": "Sprawdź ponownie wszystkie klucze",
@@ -586,7 +593,16 @@
"licensePurchase": "Kup licencję",
"licensePurchaseSites": "Kup dodatkowe witryny",
"licenseSitesUsedMax": "Użyte strony {usedSites} z {maxSites}",
"licenseSitesUsed": "{count, plural, =0 {# witryn} one {# witryna} few {# witryny} many {# witryn} other {# witryn}} w systemie.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Wybierz ile witryn chcesz {selectedMode, select, license {kupić licencję. Zawsze możesz dodać więcej witryn później.} other {dodaj do swojej istniejącej licencji.}}",
"licenseFee": "Opłata licencyjna",
"licensePriceSite": "Cena za witrynę",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Każda reguła musi mieć unikalny numer priorytetu.",
"rulesErrorValidation": "Nieprawidłowe reguły",
"rulesErrorValidationRuleDescription": "Reguła {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Wybierz poprawny typ dopasowania (ścieżka, IP, CIDR, kraj, region lub ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Wprowadź wartość dla tej reguły.",
"rulesErrorInvalidCountry": "Nieprawidłowy kraj",
"rulesErrorInvalidCountryDescription": "Wybierz poprawny kraj.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Próby Ponowienia",
"expectedResponseCodes": "Oczekiwane Kody Odpowiedzi",
"expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.",
"customHeaders": "Niestandardowe nagłówki",
"customHeadersDescription": "Nagłówki oddzielone: Nazwa nagłówka: wartość",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Nagłówki muszą być w formacie: Nazwa nagłówka: wartość.",
"saveHealthCheck": "Zapisz Kontrolę Zdrowia",
"healthCheckSaved": "Kontrola Zdrowia Zapisana",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Pokaż pasek narzędzi",
"sessionToolbarHide": "Ukryj pasek narzędzi",
"actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę",
"check": "Sprawdź"
"check": "Sprawdź",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Tem certeza de que deseja reiniciar o túnel WireGuard para <b>{name}</b>? O site perderá brevemente a conectividade.",
"siteRestartWarning": "O site será desconectado brevemente enquanto o túnel reinicia.",
"siteRestarted": "Site reiniciado",
"siteRestartedDescription": "O túnel WireGuard foi reiniciado.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Falha ao reiniciar o site",
"siteErrorRestartDescription": "Ocorreu um erro ao reiniciar o site.",
"siteSettingDescription": "Configurar as configurações no site",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Criar e gerenciar recursos que só são acessíveis por meio de um cliente conectado",
"privateResourcesBannerTitle": "Acesso Privado com Confiança Zero",
"privateResourcesBannerDescription": "Os recursos privados usam segurança de zero confiança, garantindo que usuários e máquinas possam acessar apenas os recursos que você concede explicitamente. Conecte dispositivos de usuários ou clientes de máquinas para acessar esses recursos por meio de uma rede privada virtual segura.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Procurar recursos...",
"resourceAdd": "Adicionar Recurso",
"resourceErrorDelte": "Erro ao apagar recurso",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Desbloqueie recursos empresariais para sua instância de Pangolin autohospedada. Compre uma chave de licença para ativar recursos premium e adicione-a abaixo.",
"licenseBannerGetLicense": "Obter Licença",
"licenseBannerViewDocs": "Ver Documentação",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Edição da Comunidade",
"licenseAboutDescription": "Isto destina-se aos utilizadores empresariais e empresariais que estão a usar o Pangolin num ambiente comercial. Se você estiver usando o Pangolin para uso pessoal, você pode ignorar esta seção.",
"licenseKeyActivated": "Chave de licença ativada",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Tem certeza que deseja excluir a chave de licença?",
"licenseKeyDelete": "Excluir Chave de Licença",
"licenseKeyDeleteConfirm": "Confirmar que pretende apagar a chave de licença",
"licenseTitle": "Gerir Status da Licença",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Visualizar e gerir chaves de licença no sistema",
"licenseHost": "Licença do host",
"licenseHostDescription": "Gerir a chave de licença principal do host.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Não Licenciado",
"hostId": "ID do host",
"licenseReckeckAll": "Verifique novamente todas as chaves",
@@ -586,7 +593,16 @@
"licensePurchase": "Comprar Licença",
"licensePurchaseSites": "Comprar Sites Adicionais",
"licenseSitesUsedMax": "{usedSites} de {maxSites} utilizados",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} no sistema.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Escolha quantos sites você quer {selectedMode, select, license {Compre uma licença. Você sempre pode adicionar mais sites depois.} other {adicione à sua licença existente.}}",
"licenseFee": "Taxa de licença",
"licensePriceSite": "Preço por site",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Cada regra deve ter um número de prioridade único.",
"rulesErrorValidation": "Regras inválidas",
"rulesErrorValidationRuleDescription": "Regra {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Selecione um tipo de correspondência válido (caminho, IP, CIDR, país, região ou ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Digite um valor para esta regra.",
"rulesErrorInvalidCountry": "País inválido",
"rulesErrorInvalidCountryDescription": "Selecione um país válido.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Tentativas de Repetição",
"expectedResponseCodes": "Códigos de Resposta Esperados",
"expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.",
"customHeaders": "Cabeçalhos Personalizados",
"customHeadersDescription": "Separados por cabeçalhos da nova linha: Nome do Cabeçalho: valor",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Cabeçalhos devem estar no formato: Nome do Cabeçalho: valor.",
"saveHealthCheck": "Salvar Verificação de Saúde",
"healthCheckSaved": "Verificação de Saúde Salva",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Mostrar barra de ferramentas",
"sessionToolbarHide": "Ocultar barra de ferramentas",
"actionUpdateSiteApprovals": "Atualizar Aprovações do Site",
"check": "Verificar"
"check": "Verificar",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "Вы уверены, что хотите перезапустить туннель WireGuard для <b>{name}</b>? Сайт кратковременно потеряет соединение.",
"siteRestartWarning": "Сайт кратковременно отключится во время перезапуска туннеля.",
"siteRestarted": "Сайт перезапущен",
"siteRestartedDescription": "Туннель WireGuard был перезапущен.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Не удалось перезапустить сайт",
"siteErrorRestartDescription": "Произошла ошибка во время перезапуска сайта.",
"siteSettingDescription": "Настройка параметров на сайте",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Создание и управление ресурсами, которые доступны только через подключенный клиент",
"privateResourcesBannerTitle": "Частный доступ с нулевым доверием",
"privateResourcesBannerDescription": "Частные ресурсы используют безопасность с нулевым доверием, обеспечивая доступ пользователей и устройств только к ресурсам, к которым вы явно предоставили доступ. Подключите пользовательские устройства или машинных клиентов, чтобы получить доступ к этим ресурсам через безопасную виртуальную частную сеть.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Поиск ресурсов...",
"resourceAdd": "Добавить ресурс",
"resourceErrorDelte": "Ошибка при удалении ресурса",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Откройте доступ к корпоративным функциям для вашей локально размещаемой версии Pangolin. Приобретите лицензионный ключ, чтобы активировать премиум-функции, затем добавьте его ниже.",
"licenseBannerGetLicense": "Получить лицензию",
"licenseBannerViewDocs": "Посмотреть документацию",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Издание для сообщества",
"licenseAboutDescription": "Это для бизнес и корпоративных пользователей, использующих Pangolin в коммерческой среде. Если вы используете Pangolin для личного использования, вы можете игнорировать этот раздел.",
"licenseKeyActivated": "Лицензионный ключ активирован",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Вы уверены, что хотите удалить лицензионный ключ?",
"licenseKeyDelete": "Удалить лицензионный ключ",
"licenseKeyDeleteConfirm": "Подтвердить удаление лицензионного ключа",
"licenseTitle": "Управление статусом лицензии",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Просмотр и управление лицензионными ключами в системе",
"licenseHost": "Лицензия хоста",
"licenseHostDescription": "Управление основным лицензионным ключом для хоста.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Не лицензировано",
"hostId": "ID хоста",
"licenseReckeckAll": "Перепроверить все ключи",
@@ -586,7 +593,16 @@
"licensePurchase": "Приобрести лицензию",
"licensePurchaseSites": "Приобрести дополнительные сайты",
"licenseSitesUsedMax": "Использовано сайтов: {usedSites} из {maxSites}",
"licenseSitesUsed": "{count, plural, =0 {0 сайтов} one {# сайт} few {# сайта} many {# сайтов} other {# сайтов}} в системе.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "Выберите, для скольких сайтов вы хотите {selectedMode, select, license {приобрести лицензию. Вы всегда можете добавить больше сайтов позже.} other {добавить к существующей лицензии.}}",
"licenseFee": "Лицензионный сбор",
"licensePriceSite": "Цена за сайт",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Каждое правило должно иметь уникальный номер приоритета.",
"rulesErrorValidation": "Неверные правила",
"rulesErrorValidationRuleDescription": "Правило {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Выберите действительный тип совпадения (путь, IP, CIDR, страна, регион или ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Введите значение для этого правила.",
"rulesErrorInvalidCountry": "Недействительная страна",
"rulesErrorInvalidCountryDescription": "Выберите правильную страну.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Количество попыток повторного запроса",
"expectedResponseCodes": "Ожидаемые коды ответов",
"expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.",
"customHeaders": "Пользовательские заголовки",
"customHeadersDescription": "Заголовки новой строки, разделённые: название заголовка: значение",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Заголовки должны быть в формате: Название заголовка: значение.",
"saveHealthCheck": "Сохранить проверку здоровья",
"healthCheckSaved": "Проверка здоровья сохранена",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Показать панель инструментов",
"sessionToolbarHide": "Скрыть панель инструментов",
"actionUpdateSiteApprovals": "Обновить утверждения сайта",
"check": "Проверить"
"check": "Проверить",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "<b>{name}</b> için WireGuard tünelini yeniden başlatmak istediğinizden emin misiniz? Site kısa süreliğine bağlantıyı kaybedecektir.",
"siteRestartWarning": "Tünel yeniden başlatılırken site kısa süreliğine kesintiye uğrar.",
"siteRestarted": "Site yeniden başlatıldı",
"siteRestartedDescription": "WireGuard tüneli yeniden başlatıldı.",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "Sitenin yeniden başlatılması başarısız oldu",
"siteErrorRestartDescription": "Site yeniden başlatılırken bir hata oluştu.",
"siteSettingDescription": "Sitenizdeki ayarları yapılandırın",
@@ -234,6 +234,9 @@
"clientResourceDescription": "Sadece bağlı bir istemci aracılığıyla erişilebilen kaynakları oluşturun ve yönetin",
"privateResourcesBannerTitle": "Sıfır Güven Özel Erişim",
"privateResourcesBannerDescription": "Özel kaynaklar sıfır güven güvenliği kullanır, kullanıcılar ve makinelerin yalnızca açıkça izin verdiğiniz kaynaklara erişmesini sağlar. Bu kaynaklara güvenli bir sanal özel ağ üzerinden erişmek için kullanıcı cihazlarını veya makine müşterilerini bağlayın.",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "Kaynakları ara...",
"resourceAdd": "Kaynak Ekle",
"resourceErrorDelte": "Kaynak silinirken hata",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "Kendi barındırdığınız Pangolin örneğiniz için kurumsal özelliklerin kilidini açın. Premium yetenekleri etkinleştirmek için bir lisans anahtarı satın alın, ardından aşağıya ekleyin.",
"licenseBannerGetLicense": "Lisans Alın",
"licenseBannerViewDocs": "Dokümantasyonu Görüntüleyin",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "Topluluk Sürümü",
"licenseAboutDescription": "Bu, Pangolin'i ticari bir ortamda kullanan işletme ve kurumsal kullanıcılar içindir. Pangolin'i kişisel kullanım için kullanıyorsanız, bu bölümü görmezden gelebilirsiniz.",
"licenseKeyActivated": "Lisans anahtarı etkinleştirildi",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "Lisans anahtarını silmek istediğinizden emin misiniz?",
"licenseKeyDelete": "Lisans Anahtarını Sil",
"licenseKeyDeleteConfirm": "Lisans Anahtarının Silinmesini Onaylayın",
"licenseTitle": "Lisans Durumunu Yönet",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "Sistemdeki lisans anahtarlarını görüntüleyin ve yönetin",
"licenseHost": "Ana Lisans",
"licenseHostDescription": "Ana bilgisayar için ana lisans anahtarını yönetin.",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "Lisanssız",
"hostId": "Ana Bilgisayar Kimliği",
"licenseReckeckAll": "Tüm Anahtarları Yeniden Kontrol Et",
@@ -586,7 +593,16 @@
"licensePurchase": "Lisans Satın Al",
"licensePurchaseSites": "Ek Siteler Satın Al",
"licenseSitesUsedMax": "{usedSites} / {maxSites} siteleri kullanıldı",
"licenseSitesUsed": "{count, plural, =0 {# site} one {# site} other {# site}} sistemde bulunmaktadır.",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "{selectedMode, select, license {Lisans satın almak için kaç site istediğinizi seçin. Daha sonra daha fazla site ekleyebilirsiniz.} other {mevcut lisansınıza kaç site ekleneceğini seçin.}}",
"licenseFee": "Lisans ücreti",
"licensePriceSite": "Site başına fiyat",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "Her kuralın benzersiz bir öncelik numarası olmalıdır.",
"rulesErrorValidation": "Geçersiz kurallar",
"rulesErrorValidationRuleDescription": "Kural {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "Geçerli bir eşleşme türünü seçin (yol, IP, CIDR, ülke, bölge veya ASN).",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "Bu kural için bir değer girin.",
"rulesErrorInvalidCountry": "Geçersiz ülke",
"rulesErrorInvalidCountryDescription": "Geçerli bir ülke seçin.",
@@ -2826,8 +2842,10 @@
"retryAttempts": "Tekrar Deneme Girişimleri",
"expectedResponseCodes": "Beklenen Yanıt Kodları",
"expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.",
"customHeaders": "Özel Başlıklar",
"customHeadersDescription": "Başlıklar yeni satırla ayrılmış: Başlık-Adı: değer",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "Başlıklar şu formatta olmalıdır: Başlık-Adı: değer.",
"saveHealthCheck": "Sağlık Kontrolünü Kaydet",
"healthCheckSaved": "Sağlık Kontrolü Kaydedildi",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "Araç çubuğunu göster",
"sessionToolbarHide": "Araç çubuğunu gizle",
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle",
"check": "Kontrol Et"
"check": "Kontrol Et",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
+30 -9
View File
@@ -132,7 +132,7 @@
"siteRestartDialogMessage": "确定要重启<b>{name}</b>的WireGuard隧道吗?站点将暂时断开连接。",
"siteRestartWarning": "隧道重启时,站点将暂时断开连接。",
"siteRestarted": "站点已重启",
"siteRestartedDescription": "WireGuard隧道已重启。",
"siteRestartedDescription": "The site has been restarted.",
"siteErrorRestart": "重启站点失败",
"siteErrorRestartDescription": "重启站点时发生错误。",
"siteSettingDescription": "配置站点设置",
@@ -234,6 +234,9 @@
"clientResourceDescription": "创建和管理只能通过连接客户端访问的资源",
"privateResourcesBannerTitle": "零信任私有访问",
"privateResourcesBannerDescription": "私有资源采用零信任安全机制,确保只有获得明确授权的用户和机器才能访问。用户设备或机器客户端连接后,即可通过安全的虚拟专用网络访问这些资源。",
"licenseBillingBannerTitle": "Manage License Billing",
"licenseBillingBannerDescription": "To manage billing for your license keys, including payment methods and invoices, visit the billing page. You can generate additional licenses below to increase server capacity or deploy more instances.",
"licenseBillingBannerButton": "Go to Billing",
"resourcesSearch": "搜索资源...",
"resourceAdd": "添加资源",
"resourceErrorDelte": "删除资源时出错",
@@ -555,6 +558,10 @@
"licenseBannerDescription": "为您自行托管的Pangolin实例解锁企业功能。购买许可证密钥以激活高级功能,然后在下方添加。",
"licenseBannerGetLicense": "获取许可证",
"licenseBannerViewDocs": "查看文档",
"licenseUpgradeBannerTitle": "Need More Capacity?",
"licenseUpgradeBannerDescription": "If you need to increase your site or user capacity, you can buy more licenses from the app.pangolin.net portal and add them below to upgrade your instance.",
"licenseUpgradeBannerButton": "Buy More Licenses",
"licenseMultipleKeysDescription": "You have multiple licenses activated on this server. The highest tier is used.",
"communityEdition": "社区版",
"licenseAboutDescription": "这是针对商业环境中使用Pangolin的商业和企业用户。 如果您正在使用 Pangolin 供个人使用,您可以忽略此部分。",
"licenseKeyActivated": "授权密钥已激活",
@@ -573,10 +580,10 @@
"licenseQuestionRemove": "您确定要删除许可证密钥?",
"licenseKeyDelete": "删除许可证密钥",
"licenseKeyDeleteConfirm": "确认删除许可证密钥",
"licenseTitle": "管理许可证状态",
"licenseTitle": "Manage Licenses",
"licenseTitleDescription": "查看和管理系统中的许可证密钥",
"licenseHost": "主机许可证",
"licenseHostDescription": "管理主机的主许可证密钥。",
"licenseHost": "License",
"licenseHostDescription": "See the license tier and host information",
"licensedNot": "未授权",
"hostId": "主机 ID",
"licenseReckeckAll": "重新检查所有密钥",
@@ -586,7 +593,16 @@
"licensePurchase": "购买许可证",
"licensePurchaseSites": "购买更多站点",
"licenseSitesUsedMax": "使用了 {usedSites}/{maxSites} 个站点",
"licenseSitesUsed": "{count, plural, =0 {# 站点} one {# 站点} other {# 站点}}",
"licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}}",
"licenseUsage": "Usage",
"licenseUsageDescription": "View the number of users and sites licensed for this host",
"licenseUsageSites": "Sites",
"licenseUsageUsers": "Users",
"licenseNoUserLimit": "There is no limit on the number of users using an unlicensed hot",
"licenseUsersUsedMax": "{usedUsers} of {maxUsers} users used",
"licenseUsersUsed": "{count, plural, =0 {# users} one {# user} other {# users}}",
"licenseUnlimited": "Unlimited",
"licenseTierLabel": "Tier",
"licensePurchaseDescription": "请选择您希望 {selectedMode, select, license {直接购买许可证,您可以随时增加更多站点。} other {为现有许可证购买更多站点}}",
"licenseFee": "许可证费用",
"licensePriceSite": "每个站点的价格",
@@ -831,7 +847,7 @@
"rulesErrorDuplicatePriorityDescription": "每条规则必须拥有唯一的优先级号。",
"rulesErrorValidation": "规则无效",
"rulesErrorValidationRuleDescription": "规则 {ruleNumber}: {message}",
"rulesErrorInvalidMatchTypeDescription": "选择有效的匹配类型(路径、IP、CIDR、国家、地区或 ASN)。",
"rulesErrorInvalidMatchTypeDescription": "Select a valid match type (path, IP, CIDR, country, region, ASN, or method).",
"rulesErrorValueRequired": "为此规则输入一个值。",
"rulesErrorInvalidCountry": "国家无效",
"rulesErrorInvalidCountryDescription": "选择一个有效的国家。",
@@ -2826,8 +2842,10 @@
"retryAttempts": "重试次数",
"expectedResponseCodes": "期望响应代码",
"expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。",
"customHeaders": "自定义标题",
"customHeadersDescription": "头部新行分隔:头部名称:值",
"customRequestHeaders": "Custom Request Headers",
"customRequestHeadersDescription": "Request headers sent to the downstream targets. One per line: Header-Name: value",
"customResponseHeaders": "Custom Response Headers",
"customResponseHeadersDescription": "Response headers sent back to the client. One per line: Header-Name: value",
"headersValidationError": "头部必须是格式:头部名称:值。",
"saveHealthCheck": "保存健康检查",
"healthCheckSaved": "健康检查已保存",
@@ -4397,5 +4415,8 @@
"sessionToolbarShow": "显示工具栏",
"sessionToolbarHide": "隐藏工具栏",
"actionUpdateSiteApprovals": "更新站点审批",
"check": "检查"
"check": "检查",
"rulesErrorInvalidMethod": "Invalid HTTP method",
"rulesErrorInvalidMethodDescription": "Select at least one HTTP method.",
"rulesSelectMethods": "Select methods"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 620 KiB

@@ -93,8 +93,9 @@ export const EnterpriseEditionKeyGenerated = ({
</EmailSection>
<EmailText>
For any questions or concerns regarding your license
or billing, please reach out to support at{" "}
If you need to purchase additional license keys or
modify your existing license, please reach out to
our support team at{" "}
<a
href="mailto:support@pangolin.net"
className="text-primary font-medium"
-138
View File
@@ -1,138 +0,0 @@
import { formatBackupFileName, formatBackupTimestamp } from "./backupFileName";
import { assertEquals } from "@test/assert";
// Local-time constructors are used throughout, matching formatBackupTimestamp,
// so these cases do not depend on the machine's timezone.
function testMonthIsOneIndexed() {
console.log("Running month indexing tests...");
// The case from the report: a backup taken on 12 September 2026 was named
// db_2026-8-12_... because Date#getMonth is zero-indexed.
{
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 35, 56));
assertEquals(
result,
"2026-09-12_20-35-56",
"September must render as 09, not 8"
);
}
// The other reported name, db_2026-0-23_..., was a January backup.
{
const result = formatBackupTimestamp(new Date(2026, 0, 23, 20, 25, 49));
assertEquals(
result,
"2026-01-23_20-25-49",
"January must render as 01, not 0"
);
}
{
const result = formatBackupTimestamp(
new Date(2026, 11, 31, 23, 59, 59)
);
assertEquals(
result,
"2026-12-31_23-59-59",
"December must render as 12"
);
}
}
function testEveryFieldIsZeroPadded() {
console.log("Running zero padding tests...");
// db_2026-8-12_20-36-2 in the report: a single-digit second was not padded.
{
const result = formatBackupTimestamp(new Date(2026, 8, 12, 20, 36, 2));
assertEquals(
result,
"2026-09-12_20-36-02",
"Single-digit seconds must be padded"
);
}
{
const result = formatBackupTimestamp(new Date(2026, 0, 1, 0, 0, 0));
assertEquals(
result,
"2026-01-01_00-00-00",
"Midnight on the first of the month must pad every field"
);
}
}
function testNamesSortChronologically() {
console.log("Running sort order tests...");
// Zero padding means a plain lexicographic sort of the backups directory
// lists the backups in the order they were taken.
const taken = [
new Date(2026, 8, 12, 20, 36, 2),
new Date(2026, 0, 23, 20, 25, 49),
new Date(2026, 8, 12, 20, 35, 56),
new Date(2026, 11, 31, 23, 59, 59)
];
const sorted = taken.map((date) => formatBackupTimestamp(date)).sort();
assertEquals(
sorted.join(","),
[
"2026-01-23_20-25-49",
"2026-09-12_20-35-56",
"2026-09-12_20-36-02",
"2026-12-31_23-59-59"
].join(","),
"Backup names must sort into the order the backups were taken"
);
}
function testFormatBackupFileName() {
console.log("Running backup file name formatting tests...");
const date = new Date(2026, 8, 12, 20, 35, 56);
// With semver version string without leading 'v'
assertEquals(
formatBackupFileName("1.22.0", date),
"db_2026-09-12_20-35-56_v1.22.0.sqlite",
"Filename must include timestamp and prefixed version tag"
);
// With version string already containing 'v'
assertEquals(
formatBackupFileName("v1.22.0", date),
"db_2026-09-12_20-35-56_v1.22.0.sqlite",
"Filename must not duplicate 'v' prefix if already present"
);
// Without version (fallback/default)
assertEquals(
formatBackupFileName(undefined, date),
"db_2026-09-12_20-35-56.sqlite",
"Filename without version must match default timestamped format"
);
// Distinct versions within the exact same second do not collide
const sameSecondFile1 = formatBackupFileName("1.21.0", date);
const sameSecondFile2 = formatBackupFileName("1.22.0", date);
if (sameSecondFile1 === sameSecondFile2) {
throw new Error(
"Backup file names for different versions in the same second must not collide"
);
}
}
// Run all tests
try {
testMonthIsOneIndexed();
testEveryFieldIsZeroPadded();
testNamesSortChronologically();
testFormatBackupFileName();
console.log("All tests passed successfully!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
-51
View File
@@ -1,51 +0,0 @@
/**
* Builds the timestamp segment of a database backup file name.
*
* `Date#getMonth` is zero-indexed, so building this inline produced names like
* `db_2026-8-12_...` for a backup taken on 12 September 2026. Every field is
* also zero-padded, which keeps the names unambiguous and makes them sort
* lexicographically in the order they were taken.
*
* @param date The moment the backup is being taken. Defaults to now.
* @returns A timestamp of the form `YYYY-MM-DD_HH-MM-SS`.
*/
export function formatBackupTimestamp(date: Date = new Date()): string {
const pad = (value: number): string => String(value).padStart(2, "0");
const datePart = [
date.getFullYear(),
pad(date.getMonth() + 1),
pad(date.getDate())
].join("-");
const timePart = [
pad(date.getHours()),
pad(date.getMinutes()),
pad(date.getSeconds())
].join("-");
return `${datePart}_${timePart}`;
}
/**
* Builds the full database backup file name, including timestamp and optional version tag.
*
* When a migration version is provided, the filename includes `_v<version>`,
* preventing collisions between multiple migrations running in the same second and making it easy
* to identify the migration state contained in the backup.
*
* @param version Optional migration version being run.
* @param date The moment the backup is being taken. Defaults to now.
* @returns A filename of the form `db_YYYY-MM-DD_HH-MM-SS_v<version>.sqlite` or `db_YYYY-MM-DD_HH-MM-SS.sqlite`.
*/
export function formatBackupFileName(
version?: string,
date: Date = new Date()
): string {
const timestamp = formatBackupTimestamp(date);
if (version) {
const versionTag = version.startsWith("v") ? version : `v${version}`;
return `db_${timestamp}_${versionTag}.sqlite`;
}
return `db_${timestamp}.sqlite`;
}
+1 -6
View File
@@ -20,11 +20,6 @@ function getSegmentRegex(patternPart: string): RegExp {
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment
// or a wildcard-swallowed sequence under `/public/*`.
//
// Applied to both the request path and the rule pattern: the pattern
// validator only accepts spaces / non-ASCII in percent-encoded form, so a
// rule like `/my%20docs/*` must be compared against the decoded segment
// `my docs`, not the literal text `my%20docs`.
function decodeAndResolvePath(p: string): string[] {
const rawParts = p.split("/").filter(Boolean);
@@ -53,7 +48,7 @@ function decodeAndResolvePath(p: string): string[] {
}
export function isPathAllowed(pattern: string, path: string): boolean {
const patternParts = decodeAndResolvePath(pattern);
const patternParts = pattern.split("/").filter(Boolean);
const pathParts = decodeAndResolvePath(path);
function matchSegments(
+8 -3
View File
@@ -70,8 +70,13 @@ export async function verifyApiKeyAccess(
);
}
if (!apiKey.apiKeyOrg?.orgId) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `API key with ID ${apiKeyId} does not have an organization ID`));
if (!apiKeyOrg.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`API key with ID ${apiKeyId} does not have an organization ID`
)
);
}
if (!req.userOrg) {
@@ -81,7 +86,7 @@ export async function verifyApiKeyAccess(
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, apiKey.apiKeyOrg.orgId)
eq(userOrgs.orgId, apiKeyOrg.orgId)
)
)
.limit(1);
@@ -97,7 +97,7 @@ export async function exportConnectionAuditLogs(
const baseQuery = queryConnection(data);
const log = await baseQuery.limit(MAX_EXPORT_LIMIT);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const csvData = generateCSV(log);
@@ -243,14 +243,6 @@ export async function handleSubscriptionCreated(
`License type determined: ${numUsers} users, ${numSites} sites for subscription ${subscription.id}`
);
// Grace period of 5 days added on top of the current billing
// period end (usually ~1 year out) before the license expires
const currentPeriodEnd =
fullSubscription.items.data[0]?.current_period_end;
const expiresAt =
(currentPeriodEnd ?? subscription.created) +
5 * 24 * 60 * 60;
const response = await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/paid-for`,
{
@@ -266,8 +258,7 @@ export async function handleSubscriptionCreated(
paidFor: true,
users: numUsers,
sites: numSites,
tier: tier,
expiresAt: expiresAt
tier: tier
})
}
);
@@ -276,13 +267,6 @@ export async function handleSubscriptionCreated(
logger.debug(`Fossorial API response: ${JSON.stringify(data)}`);
if (!response.ok || !data.success) {
logger.error(
`Fossorial API returned ${response.status} when setting paid-for for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${JSON.stringify(data)}`
);
return;
}
if (customer.email) {
logger.debug(
`Sending license key email to ${customer.email} for subscription ${subscription.id}`
@@ -125,7 +125,9 @@ export async function handleSubscriptionDeleted(
`Handling license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}`
);
try {
const invalidateResponse = await fetch(
// WARNING:
// this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId
await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`,
{
method: "POST",
@@ -137,18 +139,9 @@ export async function handleSubscriptionDeleted(
},
body: JSON.stringify({
orgId: customer.orgId,
licenseKeyId: parseInt(
subscription.metadata.licenseKeyId
)
})
}
);
if (!invalidateResponse.ok) {
logger.error(
`Fossorial API returned ${invalidateResponse.status} when invalidating license for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${await invalidateResponse.text()}`
);
}
} catch (error) {
logger.error(
`Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`,
@@ -23,10 +23,7 @@ import {
} from "@server/db";
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
import {
getFeatureIdByMetricId,
getFeatureIdByPriceId
} from "@server/lib/billing/features";
import { getFeatureIdByMetricId, getFeatureIdByPriceId } from "@server/lib/billing/features";
import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
import { getSubType, SubscriptionType } from "./getSubType";
@@ -69,8 +66,7 @@ export async function handleSubscriptionUpdated(
.limit(1);
const type = getSubType(fullSubscription);
const previousType =
existingSubscription.type as SubscriptionType | null;
const previousType = existingSubscription.type as SubscriptionType | null;
// If the subscription has been manually overridden, we lock the
// status down so Stripe webhooks can no longer change it.
@@ -104,11 +100,7 @@ export async function handleSubscriptionUpdated(
logger.info(
`Tier change detected for org ${customer.orgId}: ${previousType} -> ${type}`
);
await handleTierChange(
customer.orgId,
type,
previousType ?? undefined
);
await handleTierChange(customer.orgId, type, previousType ?? undefined);
}
// Upsert subscription items
@@ -121,8 +113,7 @@ export async function handleSubscriptionUpdated(
const itemsToUpsert = fullSubscription.items.data.map((item) => {
// Try to get featureId from price
let featureId: string | null =
getFeatureIdByPriceId(item.price.id) || null;
let featureId: string | null = getFeatureIdByPriceId(item.price.id) || null;
// If no match, try to preserve existing featureId
if (!featureId) {
@@ -311,20 +302,14 @@ export async function handleSubscriptionUpdated(
logger.info(
`Subscription ${subscription.id} for org ${customer.orgId} is ${effectiveStatus}, disabling paid features`
);
await handleTierChange(
customer.orgId,
null,
previousType ?? undefined
);
await handleTierChange(customer.orgId, null, previousType ?? undefined);
}
} else if (type === "license") {
if (
effectiveStatus === "canceled" ||
effectiveStatus == "unpaid" ||
effectiveStatus == "incomplete_expired"
) {
if (effectiveStatus === "canceled" || effectiveStatus == "unpaid" || effectiveStatus == "incomplete_expired") {
try {
const invalidateResponse = await fetch(
// WARNING:
// this invalidates ALL OF THE ENTERPRISE LICENSES for this orgId
await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/invalidate`,
{
method: "POST",
@@ -335,95 +320,16 @@ export async function handleSubscriptionUpdated(
"Content-Type": "application/json"
},
body: JSON.stringify({
orgId: customer.orgId,
licenseKeyId: parseInt(
subscription.metadata.licenseKeyId
)
orgId: customer.orgId
})
}
);
if (!invalidateResponse.ok) {
logger.error(
`Fossorial API returned ${invalidateResponse.status} when invalidating license for orgId ${customer.orgId} and subscription ID ${subscription.id}: ${await invalidateResponse.text()}`
);
}
} catch (error) {
logger.error(
`Error notifying Fossorial API of license subscription deletion for orgId ${customer.orgId} and subscription ID ${subscription.id}:`,
error
);
}
} else if (effectiveStatus === "active" && previousAttributes) {
// Detect a successful renewal: the billing period rolled
// forward (the invoice was paid and the new period began
// right where the previous one ended).
const currentItem = fullSubscription.items.data[0];
const prevItems = previousAttributes.items?.data;
const prevItem = Array.isArray(prevItems)
? prevItems.find(
(pi: any) => pi.id === currentItem?.id
)
: undefined;
const renewed =
currentItem &&
prevItem?.current_period_end &&
currentItem.current_period_start ===
prevItem.current_period_end &&
currentItem.current_period_start >
prevItem.current_period_start;
if (renewed) {
const licenseKeyId =
subscription.metadata.licenseKeyId;
if (!licenseKeyId) {
logger.error(
`No licenseKeyId in metadata for subscription ${subscription.id}, cannot extend license.`
);
} else {
// Grace period of 5 days added on top of the new
// billing period end (usually ~1 year out)
const expiresAt =
currentItem.current_period_end +
5 * 24 * 60 * 60;
try {
const extendResponse = await fetch(
`${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/extend`,
{
method: "POST",
headers: {
"api-key":
privateConfig.getRawPrivateConfig()
.server.fossorial_api_key!,
"Content-Type": "application/json"
},
body: JSON.stringify({
licenseId: parseInt(licenseKeyId),
expiresAt: expiresAt
})
}
);
if (!extendResponse.ok) {
logger.error(
`Fossorial API returned ${extendResponse.status} when extending license ${licenseKeyId} for subscription ${subscription.id}: ${await extendResponse.text()}`
);
} else {
logger.info(
`Extended license ${licenseKeyId} for subscription ${subscription.id} to expire at ${expiresAt}.`
);
}
} catch (error) {
logger.error(
`Error notifying Fossorial API of license renewal for subscription ${subscription.id}:`,
error
);
}
}
}
}
}
}
@@ -96,8 +96,6 @@ export async function generateNewEnterpriseLicense(
);
}
const licenseKeyValue = apiResponse?.data?.licenseKey?.licenseKey;
// check if we already have a customer for this org
const [customer] = await db
.select()
@@ -131,16 +129,6 @@ export async function generateNewEnterpriseLicense(
], // Start with the standard feature set that matches the free limits
customer: customer.customerId,
mode: "subscription",
subscription_data: {
description: licenseKeyValue
? `License ${licenseKeyValue}`
: `License key ID ${keyId}`,
metadata: {
licenseKeyId: keyId.toString(),
licenseKey: licenseKeyValue ?? "",
tier: licenseData.tier
}
},
allow_promotion_codes: true,
success_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?success=true&session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${config.getRawConfig().app.dashboard_url}/${orgId}/settings/license?canceled=true`
@@ -386,38 +386,6 @@ function runSpecialCharacterTests() {
console.log("All special character tests passed!");
}
function runEncodedPatternTests() {
console.log("\nRunning percent-encoded pattern tests...");
// isValidUrlGlobPattern accepts percent-encoded sequences and rejects
// raw spaces / non-ASCII, so `%20` and `%C3%A9` are the only way to write
// a PATH rule for such a path. Badger sends the request path already
// decoded (Go's req.URL.Path), and isPathAllowed decodes it again, so the
// rule pattern must be decoded the same way or it can never match.
assertEquals(
isPathAllowed("/my%20docs/*", "/my docs/report.pdf"),
true,
"Percent-encoded space in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my%20docs/report.pdf"),
true,
"Percent-encoded space in pattern should match raw-encoded request path"
);
assertEquals(
isPathAllowed("/caf%C3%A9", "/café"),
true,
"Percent-encoded UTF-8 in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my-docs/report.pdf"),
false,
"Decoded pattern must still reject a different path"
);
console.log("All percent-encoded pattern tests passed!");
}
function runRegionTests() {
console.log("\nRunning isIpInRegion tests...");
@@ -478,7 +446,6 @@ function runRegionTests() {
try {
runTests();
runSpecialCharacterTests();
runEncodedPatternTests();
runRegionTests();
console.log("\n✅ All tests passed!");
} catch (error) {
+1 -1
View File
@@ -31,7 +31,7 @@ export async function addPeer(
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
throw new Error(`Newt not found for site ${siteId}`);
throw new Error(`Site found for site ${siteId}`);
}
newtId = newt.newtId;
}
+1 -2
View File
@@ -500,8 +500,7 @@ async function updateHttpResource(
}
// catch when the resource policy changes or gets cleared
if (updateData.resourcePolicyId !== undefined &&
resource.resourcePolicyId !== updateData.resourcePolicyId) {
if (resource.resourcePolicyId != updateData.resourcePolicyId) {
await clearResourceSpecificSettings(
resource.resourceId,
resource.orgId,
+1 -1
View File
@@ -263,7 +263,7 @@ export async function createSite(
const { value: newClientAddress, release } =
await getNextAvailableClientSubnet(orgId);
releaseSubnetLock = release;
updatedAddress = `${newClientAddress.split("/")[0]}/${org.subnet ? org.subnet.split("/")[1] : "32"}`;
updatedAddress = newClientAddress.split("/")[0];
}
let newSite: Site | undefined;
+1 -1
View File
@@ -113,7 +113,7 @@ export async function updateSite(
.where(
and(
eq(sites.niceId, updateData.niceId),
eq(sites.orgId, existingSite.orgId),
eq(sites.orgId, sites.orgId),
ne(sites.siteId, siteId)
)
)
@@ -1,58 +0,0 @@
import { assertEquals } from "@test/assert";
import { getSiteResourceParamsSchema } from "./getSiteResource";
function testSiteResourceIdOnlyParams() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "42"
});
assertEquals(
result.success,
true,
"siteResourceId-only integration routes should pass validation"
);
if (result.success) {
assertEquals(
result.data.siteResourceId,
42,
"siteResourceId should be parsed as a number"
);
assertEquals(
result.data.orgId,
undefined,
"orgId should remain optional"
);
}
}
function testOrgScopedParamsRemainSupported() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "42",
orgId: "org-id"
});
assertEquals(
result.success,
true,
"org-scoped routes should continue to pass validation"
);
}
function testInvalidSiteResourceId() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "not-a-number"
});
assertEquals(
result.success,
false,
"non-numeric siteResourceIds should fail validation"
);
}
testSiteResourceIdOnlyParams();
testOrgScopedParamsRemainSupported();
testInvalidSiteResourceId();
console.log("All getSiteResource parameter validation tests passed.");
@@ -10,7 +10,7 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
export const getSiteResourceParamsSchema = z.strictObject({
const getSiteResourceParamsSchema = z.strictObject({
siteResourceId: z
.string()
.optional()
@@ -22,17 +22,15 @@ export const getSiteResourceParamsSchema = z.strictObject({
});
async function query(siteResourceId?: number, niceId?: string, orgId?: string) {
if (siteResourceId) {
if (siteResourceId && orgId) {
const [siteResource] = await db
.select()
.from(siteResources)
.where(
orgId
? and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.orgId, orgId)
)
: eq(siteResources.siteResourceId, siteResourceId)
and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.orgId, orgId)
)
)
.limit(1);
return siteResource;
@@ -1,342 +0,0 @@
import { execFileSync } from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import Database from "better-sqlite3";
import { assertEquals } from "@test/assert";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "..", "..");
const migrationsScript = path.join(here, "migrationsSqlite.ts");
const SEED_STATEMENTS = [
`CREATE TABLE versionMigrations (version TEXT PRIMARY KEY, executedAt INTEGER NOT NULL)`,
`INSERT INTO versionMigrations (version, executedAt) VALUES ('1.21.0', 1750000000000)`,
`CREATE TABLE sites (siteId INTEGER PRIMARY KEY AUTOINCREMENT, subnet TEXT)`,
`INSERT INTO sites (subnet) VALUES ('10.0.0.0/24')`,
`CREATE TABLE roles (roleId INTEGER PRIMARY KEY AUTOINCREMENT, orgId TEXT, isAdmin INTEGER DEFAULT 0, sshSudoMode TEXT DEFAULT 'none')`,
`INSERT INTO roles (orgId, isAdmin, sshSudoMode) VALUES ('org1', 0, 'none')`,
`CREATE TABLE licenseKey (licenseKeyId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE targets (targetId INTEGER PRIMARY KEY AUTOINCREMENT, resourceId INTEGER, siteId INTEGER NOT NULL, ip TEXT NOT NULL, method TEXT, port INTEGER NOT NULL, internalPort INTEGER, enabled INTEGER DEFAULT 1, path TEXT, pathMatchType TEXT, rewritePath TEXT, rewritePathType TEXT, priority INTEGER DEFAULT 100, mode TEXT DEFAULT 'http', authToken TEXT)`,
`CREATE TABLE subscriptions (subscriptionId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE clients (clientId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE orgs (orgId TEXT PRIMARY KEY)`,
`INSERT INTO orgs (orgId) VALUES ('org1')`,
`CREATE TABLE siteResources (siteResourceId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE eventStreamingDestinations (destinationId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE roleActions (roleId INTEGER, actionId TEXT, orgId TEXT)`,
`CREATE TABLE newt (newtId INTEGER PRIMARY KEY AUTOINCREMENT)`
];
function seedDatabase(dbPath: string) {
const db = new Database(dbPath);
try {
for (const statement of SEED_STATEMENTS) {
db.exec(statement);
}
} finally {
db.close();
}
}
function tableColumns(dbPath: string, tableName: string): string[] {
const db = new Database(dbPath, { readonly: true });
try {
return (
db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
name: unknown;
}>
).map((row) => String(row.name));
} finally {
db.close();
}
}
function executedMigrationVersions(dbPath: string): string[] {
const db = new Database(dbPath, { readonly: true });
try {
return (
db.prepare(`SELECT version FROM versionMigrations`).all() as Array<{
version: unknown;
}>
).map((row) => String(row.version));
} finally {
db.close();
}
}
function runMigrations(
workdir: string,
env: Record<string, string> = {}
): {
exitCode: number;
output: string;
} {
const tsconfig = ["tsconfig.json", "tsconfig.oss.json"]
.map((file) => path.join(repoRoot, file))
.find((file) => fs.existsSync(file));
if (!tsconfig) {
throw new Error("No tsconfig found for @server path aliases");
}
const tsxCli = path.join(
repoRoot,
"node_modules",
"tsx",
"dist",
"cli.mjs"
);
if (!fs.existsSync(tsxCli)) {
throw new Error("tsx is not installed; run npm ci first");
}
try {
const output = execFileSync(
process.execPath,
[tsxCli, "--tsconfig", tsconfig, migrationsScript],
{
cwd: workdir,
timeout: 120000,
encoding: "utf8",
env: { ...process.env, NODE_ENV: "test", ...env }
}
);
return { exitCode: 0, output };
} catch (error) {
const output =
error instanceof Error
? (error as Error & { stdout?: unknown }).stdout
: "";
return { exitCode: 1, output: String(output ?? "") };
}
}
function createTestEnvironment(): string {
for (const generated of ["server/build.ts", "server/db/index.ts"]) {
if (!fs.existsSync(path.join(repoRoot, generated))) {
throw new Error(
`Missing ${generated}; run npm run set:oss && npm run set:sqlite first`
);
}
}
const workdir = fs.mkdtempSync(
path.join(os.tmpdir(), "pangolin-backup-test-")
);
fs.mkdirSync(path.join(workdir, "config", "db"), { recursive: true });
fs.copyFileSync(
path.join(repoRoot, "config", "config.example.yml"),
path.join(workdir, "config", "config.yml")
);
const traefikSrc = path.join(repoRoot, "config", "traefik");
if (fs.existsSync(traefikSrc)) {
fs.cpSync(traefikSrc, path.join(workdir, "config", "traefik"), {
recursive: true
});
}
fs.symlinkSync(
path.join(repoRoot, "server"),
path.join(workdir, "server"),
process.platform === "win32" ? "junction" : "dir"
);
return workdir;
}
function testMultipleSequentialMigrations() {
console.log("Running multiple sequential migrations test...");
const workdir = createTestEnvironment();
try {
seedDatabase(path.join(workdir, "config", "db", "db.sqlite"));
const result = runMigrations(workdir);
assertEquals(result.exitCode, 0, "Seeded migrations must run cleanly");
if (!result.output.includes("All migrations completed successfully")) {
throw new Error(
"Seeded migrations did not complete; the backup assertions below would be vacuous"
);
}
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
// Upgrading from 1.21.0 runs 1.22.0 and 1.23.0 -> produces 2 distinct backups
assertEquals(
backups.length,
2,
"Each migration must have its own distinct backup snapshot"
);
const v122Backup = backups.find((file) =>
file.includes("_v1.22.0.sqlite")
);
const v123Backup = backups.find((file) =>
file.includes("_v1.23.0.sqlite")
);
if (!v122Backup || !v123Backup) {
throw new Error(
`Expected backups for v1.22.0 and v1.23.0, found: ${backups.join(", ")}`
);
}
// Verify pre-1.22.0 snapshot state: sites has 'subnet' (not exitNodeSubnet), versions = [1.21.0]
const v122Columns = tableColumns(
path.join(backupsDir, v122Backup),
"sites"
);
assertEquals(
v122Columns.includes("subnet") &&
!v122Columns.includes("exitNodeSubnet"),
true,
"Backup before 1.22.0 must retain pre-1.22.0 schema (sites.subnet)"
);
const v122Versions = executedMigrationVersions(
path.join(backupsDir, v122Backup)
);
assertEquals(
v122Versions.includes("1.21.0") && !v122Versions.includes("1.22.0"),
true,
"Backup before 1.22.0 must only record version 1.21.0"
);
// Verify pre-1.23.0 snapshot state: sites has 'exitNodeSubnet' (1.22.0 applied), newt has no agent
const v123Columns = tableColumns(
path.join(backupsDir, v123Backup),
"sites"
);
assertEquals(
v123Columns.includes("exitNodeSubnet"),
true,
"Backup before 1.23.0 must contain successfully applied 1.22.0 schema (sites.exitNodeSubnet)"
);
const v123NewtCols = tableColumns(
path.join(backupsDir, v123Backup),
"newt"
);
assertEquals(
!v123NewtCols.includes("agent"),
true,
"Backup before 1.23.0 must not contain 1.23.0 schema changes yet"
);
const v123Versions = executedMigrationVersions(
path.join(backupsDir, v123Backup)
);
assertEquals(
v123Versions.includes("1.21.0") && v123Versions.includes("1.22.0"),
true,
"Backup before 1.23.0 must record both 1.21.0 and 1.22.0"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
function testFailureInLaterMigrationPreservesRestorePoints() {
console.log("Running failure in later migration test...");
const workdir = createTestEnvironment();
try {
const dbPath = path.join(workdir, "config", "db", "db.sqlite");
seedDatabase(dbPath);
// Intentionally drop table 'newt' so migration 1.23.0 fails on ALTER TABLE newt ADD COLUMN agent
const db = new Database(dbPath);
db.exec("DROP TABLE newt;");
db.close();
const result = runMigrations(workdir);
assertEquals(
result.exitCode,
1,
"Migration suite must fail when 1.23.0 errors"
);
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
// Both pre-1.22.0 and pre-1.23.0 backups must exist
assertEquals(
backups.length,
2,
"Backups for earlier successful migration and the failed migration must both exist"
);
const v122Backup = backups.find((file) =>
file.includes("_v1.22.0.sqlite")
);
const v123Backup = backups.find((file) =>
file.includes("_v1.23.0.sqlite")
);
if (!v122Backup || !v123Backup) {
throw new Error(
`Expected restore points for v1.22.0 and v1.23.0, found: ${backups.join(", ")}`
);
}
// Verify pre-1.23.0 backup is a valid restore point with 1.22.0 changes applied
const v123SitesCols = tableColumns(
path.join(backupsDir, v123Backup),
"sites"
);
assertEquals(
v123SitesCols.includes("exitNodeSubnet"),
true,
"Pre-failure restore point must have 1.22.0 changes intact"
);
const v123Versions = executedMigrationVersions(
path.join(backupsDir, v123Backup)
);
assertEquals(
v123Versions.includes("1.22.0"),
true,
"Pre-failure restore point must record successful 1.22.0 migration"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
function testDisableBackupOnMigration() {
console.log("Running DISABLE_BACKUP_ON_MIGRATION test...");
const workdir = createTestEnvironment();
try {
seedDatabase(path.join(workdir, "config", "db", "db.sqlite"));
const result = runMigrations(workdir, {
DISABLE_BACKUP_ON_MIGRATION: "1"
});
assertEquals(
result.exitCode,
0,
"Migrations must succeed with backups disabled"
);
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
assertEquals(
backups.length,
0,
"No backup files should be created when DISABLE_BACKUP_ON_MIGRATION is set"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
try {
testMultipleSequentialMigrations();
testFailureInLaterMigrationPreservesRestorePoints();
testDisableBackupOnMigration();
console.log("All backup migration regression tests passed successfully!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
+6 -12
View File
@@ -5,7 +5,6 @@ import path from "path";
import semver from "semver";
import { versionMigrations } from "../db/sqlite";
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
import { formatBackupFileName } from "@server/lib/backupFileName";
import { SqliteError } from "better-sqlite3";
import fs from "fs";
import { build } from "@server/build";
@@ -107,7 +106,7 @@ async function run() {
await runMigrations();
}
function backupDb(version?: string) {
function backupDb() {
// make dir config/db/backups
const appPath = APP_PATH;
const dbDir = path.join(appPath, "db");
@@ -120,10 +119,11 @@ function backupDb(version?: string) {
}
// copy the db.sqlite file to backups
// add the date and migration version to the filename
const fileName = formatBackupFileName(version);
// add the date to the filename
const date = new Date();
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
const dbPath = path.join(dbDir, "db.sqlite");
const backupPath = path.join(backupsDir, fileName);
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
fs.copyFileSync(dbPath, backupPath);
}
@@ -162,12 +162,6 @@ export async function runMigrations() {
}
} catch (e) {
console.error("Error running migrations:", e);
if (
process.env.NODE_ENV === "test" ||
process.env.ENVIRONMENT === "test"
) {
throw e;
}
await new Promise((resolve) =>
setTimeout(resolve, 1000 * 60 * 60 * 24 * 1)
);
@@ -202,7 +196,7 @@ async function executeScripts() {
try {
if (!process.env.DISABLE_BACKUP_ON_MIGRATION) {
// Backup the database before running the migration
backupDb(migration.version);
backupDb();
}
await migration.run();
+4 -2
View File
@@ -3,6 +3,8 @@ import { sql } from "drizzle-orm";
const version = "1.23.0";
await migration();
export default async function migration() {
console.log(`Running setup script ${version}...`);
@@ -10,11 +12,11 @@ export default async function migration() {
await db.execute(sql`BEGIN`);
await db.execute(sql`
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agent" varchar;
ALTER TABLE "newt" ADD COLUMN "agent" varchar;
`);
await db.execute(sql`
ALTER TABLE "newt" ADD COLUMN IF NOT EXISTS "agentVersion" varchar;
ALTER TABLE "newt" ADD COLUMN "agentVersion" varchar;
`);
await db.execute(sql`COMMIT`);
+19
View File
@@ -20,6 +20,25 @@ import { fromZodError } from "zod-validation-error";
export default async function migration() {
console.log("Running setup script 1.0.0-beta.9...");
// make dir config/db/backups
const appPath = APP_PATH;
const dbDir = path.join(appPath, "db");
const backupsDir = path.join(dbDir, "backups");
// check if the backups directory exists and create it if it doesn't
if (!fs.existsSync(backupsDir)) {
fs.mkdirSync(backupsDir, { recursive: true });
}
// copy the db.sqlite file to backups
// add the date to the filename
const date = new Date();
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
const dbPath = path.join(dbDir, "db.sqlite");
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
fs.copyFileSync(dbPath, backupPath);
await db.transaction(async (trx) => {
try {
// Determine which config file exists
+2 -2
View File
@@ -16,13 +16,13 @@ export default async function migration() {
db.transaction(() => {
db.prepare(
`
ALTER TABLE 'newt' ADD COLUMN 'agent' text;
ALTER TABLE 'newt' ADD 'agent' text;
`
).run();
db.prepare(
`
ALTER TABLE 'newt' ADD COLUMN 'agentVersion' text;
ALTER TABLE 'newt' ADD 'agentVersion' text;
`
).run();
})();
@@ -749,6 +749,12 @@ export default function BillingPage() {
return 0;
};
// Get license key count
const getLicenseKeyCount = (): number => {
if (!licenseSubscription?.items) return 0;
return licenseSubscription.items.length;
};
// Check if downgrading to a tier would violate current usage limits
const checkLimitViolations = (
targetTier: Tier | "basic"
@@ -1539,7 +1545,7 @@ export default function BillingPage() {
</SettingsSection>
{/* Paid License Keys Section */}
{licenseSubscription && (
{(licenseSubscription || getLicenseKeyCount() > 0) && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -1555,6 +1561,22 @@ export default function BillingPage() {
<SettingsFormGrid>
<SettingsFormCell span="full">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 border rounded-lg p-4">
<div>
<div className="text-sm text-muted-foreground mb-1">
{t("billingCurrentKeys") ||
"Current Keys"}
</div>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-semibold">
{getLicenseKeyCount()}
</span>
<span className="text-lg">
{getLicenseKeyCount() === 1
? "key"
: "keys"}
</span>
</div>
</div>
<Button
variant="outline"
onClick={handleModifySubscription}
@@ -1,5 +1,4 @@
import GenerateLicenseKeysTable from "@app/components/GenerateLicenseKeysTable";
import LicenseBillingBanner from "@app/components/LicenseBillingBanner";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types";
@@ -27,16 +26,5 @@ export default async function Page({ params }: Props) {
licenseKeys = data.data.data;
} catch {}
const hasNonPersonalLicenseKey = licenseKeys.some(
(key) => key.tier !== "personal"
);
return (
<>
{hasNonPersonalLicenseKey && (
<LicenseBillingBanner orgId={orgId} />
)}
<GenerateLicenseKeysTable licenseKeys={licenseKeys} orgId={orgId} />
</>
);
return <GenerateLicenseKeysTable licenseKeys={licenseKeys} orgId={orgId} />;
}
@@ -76,13 +76,11 @@ export default function PrivateResourceInferencePage() {
})
),
httpConfigSubdomain: z.string().nullish(),
httpConfigDomainId: z
.string()
.min(1, { message: t("domainRequired") }),
httpConfigDomainId: z.string().nullish(),
httpConfigFullDomain: z.string().nullish(),
ssl: z.boolean().optional()
}),
[t]
[]
);
type FormValues = z.infer<typeof formSchema>;
@@ -105,7 +103,7 @@ export default function PrivateResourceInferencePage() {
defaultValues: {
providers: [],
httpConfigSubdomain: siteResource.subdomain ?? null,
httpConfigDomainId: siteResource.domainId ?? "",
httpConfigDomainId: siteResource.domainId ?? null,
httpConfigFullDomain: siteResource.fullDomain ?? null,
ssl: siteResource.ssl ?? false
}
@@ -291,74 +289,50 @@ export default function PrivateResourceInferencePage() {
</SettingsSubsectionHeader>
</SettingsFormCell>
<SettingsFormCell span="full">
<FormField
control={form.control}
name="httpConfigDomainId"
render={() => (
<FormItem>
<DomainPicker
key={`inference-domain-${siteResource.id}`}
orgId={
siteResource.orgId
}
cols={2}
hideFreeDomain
defaultSubdomain={
httpConfigSubdomain ??
undefined
}
defaultDomainId={
httpConfigDomainId ??
undefined
}
defaultFullDomain={
httpConfigFullDomain ??
undefined
}
onDomainChange={(
res
) => {
if (res === null) {
form.setValue(
"httpConfigSubdomain",
null
);
form.setValue(
"httpConfigDomainId",
"",
{
shouldValidate:
true
}
);
form.setValue(
"httpConfigFullDomain",
null
);
return;
}
form.setValue(
"httpConfigSubdomain",
res.subdomain ??
null
);
form.setValue(
"httpConfigDomainId",
res.domainId,
{
shouldValidate:
true
}
);
form.setValue(
"httpConfigFullDomain",
res.fullDomain
);
}}
/>
<FormMessage />
</FormItem>
)}
<DomainPicker
key={`inference-domain-${siteResource.id}`}
orgId={siteResource.orgId}
cols={2}
hideFreeDomain
defaultSubdomain={
httpConfigSubdomain ?? undefined
}
defaultDomainId={
httpConfigDomainId ?? undefined
}
defaultFullDomain={
httpConfigFullDomain ??
undefined
}
onDomainChange={(res) => {
if (res === null) {
form.setValue(
"httpConfigSubdomain",
null
);
form.setValue(
"httpConfigDomainId",
null
);
form.setValue(
"httpConfigFullDomain",
null
);
return;
}
form.setValue(
"httpConfigSubdomain",
res.subdomain ?? null
);
form.setValue(
"httpConfigDomainId",
res.domainId
);
form.setValue(
"httpConfigFullDomain",
res.fullDomain
);
}}
/>
</SettingsFormCell>
<SettingsFormCell span="half">
@@ -139,22 +139,6 @@ export default function GeneralForm() {
: "Port number should not be set for HTTP resources",
path: ["proxyPort"]
}
)
.refine(
(data) => {
if (
["http", "ssh", "rdp", "vnc", "inference"].includes(
resource.mode
)
) {
return !!data.domainId;
}
return true;
},
{
message: t("domainRequired"),
path: ["domainId"]
}
);
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@@ -450,87 +434,63 @@ export default function GeneralForm() {
resource.mode
) && (
<SettingsFormCell span="full">
<FormField
control={form.control}
name="domainId"
render={() => (
<FormItem>
<div id="resource-domain-picker">
<DomainPicker
allowWildcard={
resource.mode !==
"inference"
}
key={
resource.resourceId
}
orgId={
orgId as string
}
cols={2}
defaultSubdomain={
form.watch(
"subdomain"
) ??
undefined
}
defaultDomainId={
form.watch(
"domainId"
) ??
undefined
}
defaultFullDomain={
resourceFullDomainName ||
undefined
}
onDomainChange={(
res
) => {
if (
res ===
null
) {
form.setValue(
"domainId",
undefined,
{
shouldValidate:
true
}
);
form.setValue(
"subdomain",
undefined
);
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://`
);
return;
}
form.setValue(
"domainId",
res.domainId,
{
shouldValidate:
true
}
);
form.setValue(
"subdomain",
res.subdomain ??
undefined
);
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
);
}}
/>
</div>
<FormMessage />
</FormItem>
)}
/>
<div id="resource-domain-picker">
<DomainPicker
allowWildcard={
resource.mode !==
"inference"
}
key={
resource.resourceId
}
orgId={orgId as string}
cols={2}
defaultSubdomain={
form.watch(
"subdomain"
) ?? undefined
}
defaultDomainId={
form.watch(
"domainId"
) ?? undefined
}
defaultFullDomain={
resourceFullDomainName ||
undefined
}
onDomainChange={(
res
) => {
if (res === null) {
form.setValue(
"domainId",
undefined
);
form.setValue(
"subdomain",
undefined
);
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://`
);
return;
}
form.setValue(
"domainId",
res.domainId
);
form.setValue(
"subdomain",
res.subdomain ??
undefined
);
setResourceFullDomain(
`${resource.ssl ? "https" : "http"}://${toUnicode(res.fullDomain)}`
);
}}
/>
</div>
</SettingsFormCell>
)}
{!["tcp", "udp", "inference"].includes(
-39
View File
@@ -1,39 +0,0 @@
"use client";
import { Globe, CreditCard, ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { Button } from "@app/components/ui/button";
import DismissableBanner from "./DismissableBanner";
type LicenseBillingBannerProps = {
orgId: string;
};
export const LicenseBillingBanner = ({ orgId }: LicenseBillingBannerProps) => {
const t = useTranslations();
return (
<DismissableBanner
storageKey="license-billing-banner-dismissed"
version={1}
title={t("licenseBillingBannerTitle")}
titleIcon={<Globe className="w-5 h-5 text-primary" />}
description={t("licenseBillingBannerDescription")}
>
<Link href={`/${orgId}/settings/billing`}>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
<CreditCard className="w-4 h-4" />
{t("licenseBillingBannerButton")}
<ArrowRight className="w-4 h-4" />
</Button>
</Link>
</DismissableBanner>
);
};
export default LicenseBillingBanner;
+1 -1
View File
@@ -330,7 +330,7 @@ export default function NewPricingLicenseForm({
cols={2}
/>
<a
href="https://pangolin.net/pricing#Self-Hosted"
href="https://pangolin.net/pricing"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:underline"
+7 -12
View File
@@ -63,12 +63,6 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
) : null;
if (site.type === "newt") {
// agent and agentVersion were added after newtVersion, so a
// site still running an older Newt reports only newtVersion.
// Without these fallbacks the badge renders with no label and
// no version at all.
const agentLabel = site.agent == "cli" ? "Pangolin CLI" : "Newt";
const agentVersion = site.agentVersion ?? site.newtVersion;
return (
<Alert>
<AlertDescription>
@@ -85,12 +79,13 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
<InfoSection>
<InfoSectionTitle>{t("agent")}</InfoSectionTitle>
<InfoSectionContent>
<div className="flex items-center space-x-1">
<span>{agentLabel}</span>
{agentVersion && (
<span>v{agentVersion}</span>
)}
</div>
{site.agent == "newt" ? "Newt" : null}
{site.agent == "cli"
? "Pangolin CLI"
: null}{" "}
{site.agentVersion
? `v${site.agentVersion}`
: "-"}
</InfoSectionContent>
</InfoSection>
{endpointSection}
+14 -16
View File
@@ -373,7 +373,7 @@ export default function SitesTable({
accessorKey: "type",
friendlyName: t("agent"),
header: () => {
return <span className="p-3">{t("agent")}</span>;
return <span className="p-3">{t("type")}</span>;
},
cell: ({ row }) => {
const originalRow = row.original;
@@ -386,27 +386,25 @@ export default function SitesTable({
);
if (originalRow.type === "newt") {
if (!originalRow.agent && !originalRow.newtVersion) {
// it has not checked in yet
if (!originalRow.agent) {
return <span>-</span>;
}
// agent and agentVersion were added after newtVersion, so a
// site still running an older Newt reports only newtVersion.
// Without these fallbacks the badge renders with no label and
// no version at all.
const agentLabel =
originalRow.agent == "cli"
? "Pangolin CLI"
: "Newt";
const agentVersion =
originalRow.agentVersion ?? originalRow.newtVersion;
return (
<div className="flex items-center space-x-1">
<Badge variant="secondary">
<div className="flex items-center space-x-1">
<span>{agentLabel}</span>
{agentVersion && (
<span>v{agentVersion}</span>
<span>
{originalRow.agent == "newt"
? "Newt"
: null}
{originalRow.agent == "cli"
? "Pangolin CLI"
: null}
</span>
{originalRow.agentVersion && (
<span>
v{originalRow.agentVersion}
</span>
)}
</div>
</Badge>
+1 -1
View File
@@ -150,7 +150,7 @@ Type=simple
User=root
Group=root
EnvironmentFile=/etc/pangolin/pangolin-site.env
ExecStart=/usr/local/bin/pangolin up site
ExecStart=/home/owen/fossorial/cli/bin/pangolin up site
Restart=always
RestartSec=2
UMask=0077
-7
View File
@@ -523,13 +523,6 @@ export function createCreateFormSchema(t: TranslateFn) {
});
}
}
if (data.mode === "inference" && !data.httpConfigDomainId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("domainRequired"),
path: ["httpConfigDomainId"]
});
}
});
}