diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs
old mode 100755
new mode 100644
index 163cd5b95..251b19c4b
--- a/frontend/check-locales.cjs
+++ b/frontend/check-locales.cjs
@@ -31,6 +31,7 @@ const allLocales = [
["no", "no-NO"],
["uk", "uk-UA"],
["az", "az-AZ"],
+ ["fa", "fa-IR"],
];
const ignoreUnused = [/^.*$/];
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b6f0bba70..dfd0c7fe4 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4,7 +4,7 @@ import EasyModal from "ez-modal-react";
import { RawIntlProvider } from "react-intl";
import { ToastContainer } from "react-toastify";
import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context";
-import { intl } from "src/locale";
+import { getLocale, intl, isRTLLocale } from "src/locale";
import Router from "src/Router.tsx";
// Create a client
@@ -26,7 +26,7 @@ function App() {
hideProgressBar={true}
newestOnTop={true}
closeOnClick={true}
- rtl={false}
+ rtl={isRTLLocale(getLocale())}
closeButton={false}
/>
diff --git a/frontend/src/locale/IntlProvider.tsx b/frontend/src/locale/IntlProvider.tsx
old mode 100755
new mode 100644
index 5ba36aa84..671ef8ed5
--- a/frontend/src/locale/IntlProvider.tsx
+++ b/frontend/src/locale/IntlProvider.tsx
@@ -23,6 +23,7 @@ import langHu from "./lang/hu.json";
import langNo from "./lang/no.json";
import langUk from "./lang/uk.json";
import langAz from "./lang/az.json";
+import langFa from "./lang/fa.json";
import langList from "./lang/lang-list.json";
// first item of each array should be the language code,
@@ -53,6 +54,7 @@ const localeOptions = [
["no", "no-NO", langNo],
["uk", "uk-UA", langUk],
["az", "az-AZ", langAz],
+ ["fa", "fa-IR", langFa],
];
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
@@ -79,6 +81,7 @@ const getFlagCodeForLocale = (locale?: string) => {
ga: "ie", // Ireland (Irish)
et: "ee", // Estonia (ISO 3166-1). "et" as a country code would be Ethiopia.
uk: "ua", // Ukraine
+ fa: "ir", // Iran (Persian)
};
if (specialCases[thisLocale]) {
@@ -87,6 +90,15 @@ const getFlagCodeForLocale = (locale?: string) => {
return thisLocale.toUpperCase();
};
+const rtlLocales = ["fa"];
+
+const isRTLLocale = (locale?: string) => rtlLocales.includes((locale || "en").slice(0, 2));
+
+const applyDocumentLocale = (locale: string): void => {
+ document.documentElement.lang = locale;
+ document.documentElement.dir = isRTLLocale(locale) ? "rtl" : "ltr";
+};
+
const getLocale = (short = false) => {
let loc = window.localStorage.getItem("locale");
if (!loc) {
@@ -105,13 +117,14 @@ const getLocale = (short = false) => {
const cache = createIntlCache();
const initialMessages = loadMessages(getLocale());
+applyDocumentLocale(getLocale());
let intl = createIntl({ locale: getLocale(), messages: initialMessages }, cache);
const changeLocale = (locale: string): void => {
const messages = loadMessages(locale);
intl = createIntl({ locale, messages }, cache);
window.localStorage.setItem("locale", locale);
- document.documentElement.lang = locale;
+ applyDocumentLocale(locale);
};
// This is a translation component that wraps the translation in a span with a data
@@ -147,4 +160,4 @@ const T = ({
//console.log("L:", localeOptions);
-export { localeOptions, getFlagCodeForLocale, getLocale, createIntl, changeLocale, intl, T };
+export { localeOptions, getFlagCodeForLocale, getLocale, isRTLLocale, createIntl, changeLocale, intl, T };
diff --git a/frontend/src/locale/src/HelpDoc/fa/AccessLists.md b/frontend/src/locale/src/HelpDoc/fa/AccessLists.md
new file mode 100644
index 000000000..d9d5fe53d
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/AccessLists.md
@@ -0,0 +1,7 @@
+## فهرست دسترسی چیست؟
+
+فهرست دسترسی (Access List) به شما اجازه میدهد دسترسی آدرسهای IP مشخص را مجاز یا مسدود کنید و در صورت نیاز با استفاده از Basic HTTP Authentication از Proxy Hostها با نام کاربری و رمز عبور محافظت کنید.
+
+در هر فهرست دسترسی میتوانید چندین قانون برای کلاینتها و چندین نام کاربری و رمز عبور تعریف کنید و سپس همان فهرست را به یک یا چند _میزبان پراکسی (Proxy Host)_ اختصاص دهید.
+
+این قابلیت برای سرویسهای وبی که خودشان مکانیزم احراز هویت ندارند، یا زمانی که میخواهید دسترسی کلاینتهای ناشناس را محدود کنید، بسیار کاربردی است.
diff --git a/frontend/src/locale/src/HelpDoc/fa/Certificates.md b/frontend/src/locale/src/HelpDoc/fa/Certificates.md
new file mode 100644
index 000000000..f65f96956
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/Certificates.md
@@ -0,0 +1,21 @@
+## راهنمای گواهیها
+
+### گواهی HTTP
+
+در اعتبارسنجی از طریق HTTP، سرورهای Let's Encrypt تلاش میکنند از طریق HTTP (نه HTTPS) به دامنهٔ شما دسترسی پیدا کنند. اگر اعتبارسنجی موفق باشد، گواهی صادر میشود.
+
+برای این روش باید از قبل یک _میزبان پراکسی (Proxy Host)_ برای دامنهٔ موردنظر ایجاد کرده باشید که از طریق HTTP در دسترس باشد و به همین نصب Nginx Proxy Manager اشاره کند. پس از صدور گواهی میتوانید Proxy Host را طوری تنظیم کنید که برای اتصالهای HTTPS نیز از آن استفاده کند. با این حال، برای تمدید خودکار گواهی، دسترسی HTTP باید همچنان برقرار بماند.
+
+این روش از دامنههای Wildcard پشتیبانی _نمیکند_.
+
+### گواهی DNS
+
+در اعتبارسنجی از طریق DNS باید از افزونهٔ ارائهدهندهٔ DNS (DNS Provider) استفاده کنید. افزونه بهطور موقت رکوردهایی در DNS دامنه ایجاد میکند و Let's Encrypt با بررسی آنها مالکیت دامنه را تأیید میکند. در صورت موفقیت، گواهی صادر میشود.
+
+برای درخواست این نوع گواهی لازم نیست از قبل Proxy Host ساخته باشید و Proxy Host نیز نیازی به دسترسی HTTP ندارد.
+
+این روش از دامنههای Wildcard پشتیبانی _میکند_.
+
+### گواهی سفارشی
+
+از این گزینه برای بارگذاری گواهی SSL/TLS خود که توسط مرجع صدور گواهی (CA) موردنظر شما صادر شده است استفاده کنید.
diff --git a/frontend/src/locale/src/HelpDoc/fa/DeadHosts.md b/frontend/src/locale/src/HelpDoc/fa/DeadHosts.md
new file mode 100644
index 000000000..42db654c1
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/DeadHosts.md
@@ -0,0 +1,7 @@
+## میزبان 404 چیست؟
+
+میزبان 404 (404 Host) میزبانی است که برای درخواستهای ورودی یک صفحهٔ 404 نمایش میدهد.
+
+این قابلیت زمانی مفید است که دامنهٔ شما هنوز در موتورهای جستوجو فهرست شده است و میخواهید صفحهٔ خطای مناسبتری نمایش دهید، یا به خزندههای موتورهای جستوجو اعلام کنید که صفحات آن دامنه دیگر وجود ندارند.
+
+همچنین میتوانید لاگ درخواستهای ورودی به این Host و آدرسهای ارجاعدهنده (Referrer) را بررسی کنید.
diff --git a/frontend/src/locale/src/HelpDoc/fa/ProxyHosts.md b/frontend/src/locale/src/HelpDoc/fa/ProxyHosts.md
new file mode 100644
index 000000000..2ef0fcf5b
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/ProxyHosts.md
@@ -0,0 +1,7 @@
+## میزبان پراکسی چیست؟
+
+میزبان پراکسی (Proxy Host) نقطهٔ ورودی برای یک سرویس وب است که میخواهید درخواستها را به آن فوروارد کنید.
+
+Proxy Host میتواند SSL/TLS Termination را نیز انجام دهد؛ بنابراین حتی اگر سرویس مقصد خودش SSL نداشته باشد، میتوانید دسترسی HTTPS را در Nginx Proxy Manager فراهم کنید.
+
+Proxy Hostها رایجترین قابلیت مورد استفاده در Nginx Proxy Manager هستند.
diff --git a/frontend/src/locale/src/HelpDoc/fa/RedirectionHosts.md b/frontend/src/locale/src/HelpDoc/fa/RedirectionHosts.md
new file mode 100644
index 000000000..65f09f96a
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/RedirectionHosts.md
@@ -0,0 +1,5 @@
+## میزبان تغییر مسیر چیست؟
+
+میزبان تغییر مسیر (Redirection Host) درخواستهای دامنهٔ ورودی را به دامنهٔ دیگری Redirect میکند.
+
+کاربرد رایج آن زمانی است که دامنهٔ وبسایت تغییر کرده، اما نتایج موتورهای جستوجو یا لینکهای ارجاعدهنده هنوز به دامنهٔ قدیمی اشاره میکنند.
diff --git a/frontend/src/locale/src/HelpDoc/fa/Streams.md b/frontend/src/locale/src/HelpDoc/fa/Streams.md
new file mode 100644
index 000000000..9896e2c00
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/Streams.md
@@ -0,0 +1,5 @@
+## استریم چیست؟
+
+Stream در Nginx برای فوروارد کردن مستقیم ترافیک TCP/UDP به یک میزبان دیگر در شبکه استفاده میشود.
+
+این قابلیت برای سرویسهایی مانند سرورهای بازی، FTP، SSH و سایر سرویسهای مبتنی بر TCP یا UDP کاربرد دارد.
diff --git a/frontend/src/locale/src/HelpDoc/fa/index.ts b/frontend/src/locale/src/HelpDoc/fa/index.ts
new file mode 100644
index 000000000..a9bb46ba7
--- /dev/null
+++ b/frontend/src/locale/src/HelpDoc/fa/index.ts
@@ -0,0 +1,6 @@
+export * as AccessLists from "./AccessLists.md";
+export * as Certificates from "./Certificates.md";
+export * as DeadHosts from "./DeadHosts.md";
+export * as ProxyHosts from "./ProxyHosts.md";
+export * as RedirectionHosts from "./RedirectionHosts.md";
+export * as Streams from "./Streams.md";
diff --git a/frontend/src/locale/src/HelpDoc/index.ts b/frontend/src/locale/src/HelpDoc/index.ts
index 79c7ae7af..ceddf4c78 100644
--- a/frontend/src/locale/src/HelpDoc/index.ts
+++ b/frontend/src/locale/src/HelpDoc/index.ts
@@ -21,8 +21,9 @@ import * as tr from "./tr/index";
import * as hu from "./hu/index";
import * as uk from "./uk/index";
import * as az from "./az/index";
+import * as fa from "./fa/index";
-const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu, uk, az };
+const items: any = { en, de, pt, es, et, ja, sk, cs, zh, pl, ru, it, vi, nl, bg, ko, ga, id, fr, tr, hu, uk, az, fa };
const fallbackLang = "en";
diff --git a/frontend/src/locale/src/fa.json b/frontend/src/locale/src/fa.json
new file mode 100644
index 000000000..2819bbc95
--- /dev/null
+++ b/frontend/src/locale/src/fa.json
@@ -0,0 +1,785 @@
+{
+ "2fa.backup-codes-remaining": {
+ "defaultMessage": "کدهای پشتیبان باقیمانده: {count}"
+ },
+ "2fa.backup-warning": {
+ "defaultMessage": "این کدهای پشتیبان را در مکانی امن ذخیره کنید. هر کد فقط یکبار قابل استفاده است."
+ },
+ "2fa.disable": {
+ "defaultMessage": "غیرفعال کردن احراز هویت دومرحلهای"
+ },
+ "2fa.disable-confirm": {
+ "defaultMessage": "غیرفعال کردن 2FA"
+ },
+ "2fa.disable-warning": {
+ "defaultMessage": "غیرفعال کردن احراز هویت دومرحلهای امنیت حساب شما را کاهش میدهد."
+ },
+ "2fa.disabled": {
+ "defaultMessage": "غیرفعال"
+ },
+ "2fa.done": {
+ "defaultMessage": "کدهای پشتیبان را ذخیره کردهام"
+ },
+ "2fa.enable": {
+ "defaultMessage": "فعال کردن احراز هویت دومرحلهای"
+ },
+ "2fa.enabled": {
+ "defaultMessage": "فعال"
+ },
+ "2fa.enter-code": {
+ "defaultMessage": "کد تأیید را وارد کنید"
+ },
+ "2fa.enter-code-disable": {
+ "defaultMessage": "برای غیرفعالسازی، کد تأیید را وارد کنید"
+ },
+ "2fa.regenerate": {
+ "defaultMessage": "تولید مجدد"
+ },
+ "2fa.regenerate-backup": {
+ "defaultMessage": "تولید مجدد کدهای پشتیبان"
+ },
+ "2fa.regenerate-instructions": {
+ "defaultMessage": "برای تولید کدهای پشتیبان جدید، یک کد تأیید وارد کنید. کدهای قبلی شما باطل خواهند شد."
+ },
+ "2fa.secret-key": {
+ "defaultMessage": "کلید مخفی"
+ },
+ "2fa.setup-instructions": {
+ "defaultMessage": "این QR Code را با برنامهٔ احراز هویت خود اسکن کنید یا کلید مخفی را بهصورت دستی وارد کنید."
+ },
+ "2fa.status": {
+ "defaultMessage": "وضعیت"
+ },
+ "2fa.title": {
+ "defaultMessage": "احراز هویت دومرحلهای"
+ },
+ "2fa.verify-enable": {
+ "defaultMessage": "تأیید و فعالسازی"
+ },
+ "access-list": {
+ "defaultMessage": "فهرست دسترسی"
+ },
+ "access-list.access-count": {
+ "defaultMessage": "{count} {count, plural, one {قانون دسترسی} other {قانون دسترسی}}"
+ },
+ "access-list.auth-count": {
+ "defaultMessage": "{count} {count, plural, one {کاربر} other {کاربر}}"
+ },
+ "access-list.help-rules-last": {
+ "defaultMessage": "اگر حداقل یک قانون وجود داشته باشد، قانون «رد همه» (Deny All) در انتهای فهرست اضافه میشود."
+ },
+ "access-list.help.rules-order": {
+ "defaultMessage": "توجه کنید که دستورهای اجازه (Allow) و رد (Deny) به همان ترتیبی که تعریف شدهاند اعمال میشوند."
+ },
+ "access-list.pass-auth": {
+ "defaultMessage": "ارسال اطلاعات احراز هویت به سرور بالادستی (Upstream)"
+ },
+ "access-list.public": {
+ "defaultMessage": "قابل دسترسی عمومی"
+ },
+ "access-list.public.subtitle": {
+ "defaultMessage": "بدون نیاز به احراز هویت پایه (Basic Auth)"
+ },
+ "access-list.rule-source.placeholder": {
+ "defaultMessage": "192.168.1.100 یا 192.168.1.0/24 یا 2001:0db8::/32"
+ },
+ "access-list.satisfy-any": {
+ "defaultMessage": "کافی بودن یکی از شرایط"
+ },
+ "access-list.subtitle": {
+ "defaultMessage": "{users} {users, plural, one {کاربر} other {کاربر}}، {rules} {rules, plural, one {قانون دسترسی} other {قانون دسترسی}} — ایجاد: {date}"
+ },
+ "access-lists": {
+ "defaultMessage": "فهرستهای دسترسی"
+ },
+ "action.add": {
+ "defaultMessage": "افزودن"
+ },
+ "action.add-location": {
+ "defaultMessage": "افزودن مسیر (Location)"
+ },
+ "action.allow": {
+ "defaultMessage": "اجازه"
+ },
+ "action.clear": {
+ "defaultMessage": "پاک کردن"
+ },
+ "action.close": {
+ "defaultMessage": "بستن"
+ },
+ "action.delete": {
+ "defaultMessage": "حذف"
+ },
+ "action.deny": {
+ "defaultMessage": "رد"
+ },
+ "action.disable": {
+ "defaultMessage": "غیرفعالسازی"
+ },
+ "action.download": {
+ "defaultMessage": "دانلود"
+ },
+ "action.edit": {
+ "defaultMessage": "ویرایش"
+ },
+ "action.enable": {
+ "defaultMessage": "فعالسازی"
+ },
+ "action.permissions": {
+ "defaultMessage": "دسترسیها"
+ },
+ "action.renew": {
+ "defaultMessage": "تمدید"
+ },
+ "action.view-details": {
+ "defaultMessage": "مشاهده جزئیات"
+ },
+ "auditlogs": {
+ "defaultMessage": "گزارشهای ممیزی (Audit Logs)"
+ },
+ "auto": {
+ "defaultMessage": "خودکار"
+ },
+ "cancel": {
+ "defaultMessage": "لغو"
+ },
+ "certificate": {
+ "defaultMessage": "گواهی"
+ },
+ "certificate.custom-certificate": {
+ "defaultMessage": "گواهی"
+ },
+ "certificate.custom-certificate-key": {
+ "defaultMessage": "کلید خصوصی گواهی"
+ },
+ "certificate.custom-intermediate": {
+ "defaultMessage": "گواهی میانی (Intermediate)"
+ },
+ "certificate.in-use": {
+ "defaultMessage": "در حال استفاده"
+ },
+ "certificate.none.subtitle": {
+ "defaultMessage": "هیچ گواهیای اختصاص داده نشده"
+ },
+ "certificate.none.subtitle.for-http": {
+ "defaultMessage": "این میزبان از HTTPS استفاده نخواهد کرد"
+ },
+ "certificate.none.title": {
+ "defaultMessage": "هیچکدام"
+ },
+ "certificate.not-in-use": {
+ "defaultMessage": "استفاده نشده"
+ },
+ "certificate.renew": {
+ "defaultMessage": "تمدید گواهی"
+ },
+ "certificates": {
+ "defaultMessage": "گواهیها"
+ },
+ "certificates.custom": {
+ "defaultMessage": "گواهی سفارشی"
+ },
+ "certificates.custom.warning": {
+ "defaultMessage": "فایلهای کلید خصوصیِ محافظتشده با رمز عبور پشتیبانی نمیشوند."
+ },
+ "certificates.dns.credentials": {
+ "defaultMessage": "محتوای فایل اعتبارنامهها"
+ },
+ "certificates.dns.credentials-note": {
+ "defaultMessage": "این افزونه به یک فایل پیکربندی شامل API Token یا سایر اعتبارنامههای ارائهدهندهٔ شما نیاز دارد."
+ },
+ "certificates.dns.credentials-warning": {
+ "defaultMessage": "این اطلاعات بهصورت متن ساده (Plain Text) در پایگاه داده و یک فایل ذخیره میشوند!"
+ },
+ "certificates.dns.propagation-seconds": {
+ "defaultMessage": "زمان انتشار DNS (ثانیه)"
+ },
+ "certificates.dns.propagation-seconds-note": {
+ "defaultMessage": "برای استفاده از مقدار پیشفرض افزونه، این فیلد را خالی بگذارید. این مقدار مدت انتظار برای انتشار DNS را برحسب ثانیه مشخص میکند."
+ },
+ "certificates.dns.provider": {
+ "defaultMessage": "ارائهدهندهٔ DNS"
+ },
+ "certificates.dns.provider.placeholder": {
+ "defaultMessage": "یک ارائهدهندهٔ DNS انتخاب کنید..."
+ },
+ "certificates.dns.warning": {
+ "defaultMessage": "این بخش نیازمند آشنایی با Certbot و افزونههای DNS آن است. لطفاً مستندات افزونهٔ مربوطه را بررسی کنید."
+ },
+ "certificates.http.reachability-404": {
+ "defaultMessage": "در این دامنه یک سرور پیدا شد، اما به نظر نمیرسد Nginx Proxy Manager باشد. مطمئن شوید دامنه به IP سروری اشاره میکند که NPM روی آن اجرا میشود."
+ },
+ "certificates.http.reachability-failed-to-check": {
+ "defaultMessage": "بررسی دسترسیپذیری بهدلیل خطای ارتباط با site24x7.com انجام نشد."
+ },
+ "certificates.http.reachability-not-resolved": {
+ "defaultMessage": "هیچ سروری در این دامنه در دسترس نیست. مطمئن شوید دامنه وجود دارد، به IP سروری اشاره میکند که NPM روی آن اجرا میشود و در صورت نیاز پورت 80 روی روتر به این سرور فوروارد شده است."
+ },
+ "certificates.http.reachability-ok": {
+ "defaultMessage": "سرور شما در دسترس است و صدور گواهی باید امکانپذیر باشد."
+ },
+ "certificates.http.reachability-other": {
+ "defaultMessage": "در این دامنه یک سرور پیدا شد، اما کد وضعیت غیرمنتظرهٔ {code} را برگرداند. آیا این سرور NPM است؟ مطمئن شوید دامنه به IP سروری اشاره میکند که NPM روی آن اجرا میشود."
+ },
+ "certificates.http.reachability-wrong-data": {
+ "defaultMessage": "در این دامنه یک سرور پیدا شد، اما دادهٔ غیرمنتظرهای برگرداند. آیا این سرور NPM است؟ مطمئن شوید دامنه به IP سروری اشاره میکند که NPM روی آن اجرا میشود."
+ },
+ "certificates.http.test-results": {
+ "defaultMessage": "نتایج تست"
+ },
+ "certificates.http.warning": {
+ "defaultMessage": "این دامنهها باید از قبل طوری تنظیم شده باشند که به این Nginx Proxy Manager اشاره کنند."
+ },
+ "certificates.key-type": {
+ "defaultMessage": "نوع کلید"
+ },
+ "certificates.key-type-description": {
+ "defaultMessage": "RSA سازگاری گستردهتری دارد؛ ECDSA سریعتر و امنتر است، اما ممکن است در سیستمهای قدیمی پشتیبانی نشود."
+ },
+ "certificates.key-type-ecdsa": {
+ "defaultMessage": "ECDSA 256"
+ },
+ "certificates.key-type-rsa": {
+ "defaultMessage": "RSA 2048"
+ },
+ "certificates.request.subtitle": {
+ "defaultMessage": "از طریق Let's Encrypt"
+ },
+ "certificates.request.title": {
+ "defaultMessage": "درخواست گواهی جدید"
+ },
+ "column.access": {
+ "defaultMessage": "دسترسی"
+ },
+ "column.authorization": {
+ "defaultMessage": "احراز هویت"
+ },
+ "column.authorizations": {
+ "defaultMessage": "کاربران احراز هویت"
+ },
+ "column.custom-locations": {
+ "defaultMessage": "مسیرهای سفارشی (Location)"
+ },
+ "column.destination": {
+ "defaultMessage": "مقصد"
+ },
+ "column.details": {
+ "defaultMessage": "جزئیات"
+ },
+ "column.email": {
+ "defaultMessage": "ایمیل"
+ },
+ "column.event": {
+ "defaultMessage": "رویداد"
+ },
+ "column.expires": {
+ "defaultMessage": "انقضا"
+ },
+ "column.http-code": {
+ "defaultMessage": "کد HTTP"
+ },
+ "column.incoming-port": {
+ "defaultMessage": "پورت ورودی"
+ },
+ "column.name": {
+ "defaultMessage": "نام"
+ },
+ "column.protocol": {
+ "defaultMessage": "پروتکل"
+ },
+ "column.provider": {
+ "defaultMessage": "ارائهدهنده"
+ },
+ "column.roles": {
+ "defaultMessage": "نقشها"
+ },
+ "column.rules": {
+ "defaultMessage": "قوانین"
+ },
+ "column.satisfy": {
+ "defaultMessage": "شرط دسترسی"
+ },
+ "column.satisfy-all": {
+ "defaultMessage": "همهٔ شرایط"
+ },
+ "column.satisfy-any": {
+ "defaultMessage": "یکی از شرایط"
+ },
+ "column.scheme": {
+ "defaultMessage": "پروتکل (Scheme)"
+ },
+ "column.source": {
+ "defaultMessage": "منبع"
+ },
+ "column.ssl": {
+ "defaultMessage": "SSL"
+ },
+ "column.status": {
+ "defaultMessage": "وضعیت"
+ },
+ "created-on": {
+ "defaultMessage": "ایجاد: {date}"
+ },
+ "dashboard": {
+ "defaultMessage": "داشبورد"
+ },
+ "dead-host": {
+ "defaultMessage": "میزبان 404"
+ },
+ "dead-hosts": {
+ "defaultMessage": "میزبانهای 404"
+ },
+ "dead-hosts.count": {
+ "defaultMessage": "{count} {count, plural, one {میزبان 404} other {میزبان 404}}"
+ },
+ "disabled": {
+ "defaultMessage": "غیرفعال"
+ },
+ "domain-names": {
+ "defaultMessage": "نامهای دامنه"
+ },
+ "domain-names.max": {
+ "defaultMessage": "حداکثر {count} نام دامنه"
+ },
+ "domain-names.placeholder": {
+ "defaultMessage": "برای افزودن دامنه شروع به تایپ کنید..."
+ },
+ "domain-names.wildcards-not-permitted": {
+ "defaultMessage": "دامنهٔ Wildcard برای این نوع مجاز نیست."
+ },
+ "domain-names.wildcards-not-supported": {
+ "defaultMessage": "این مرجع صدور گواهی (CA) از دامنههای Wildcard پشتیبانی نمیکند."
+ },
+ "domains.advanced": {
+ "defaultMessage": "پیشرفته"
+ },
+ "domains.force-ssl": {
+ "defaultMessage": "اجبار به HTTPS"
+ },
+ "domains.hsts-enabled": {
+ "defaultMessage": "فعالسازی HSTS"
+ },
+ "domains.hsts-subdomains": {
+ "defaultMessage": "اعمال HSTS به زیردامنهها"
+ },
+ "domains.http2-support": {
+ "defaultMessage": "پشتیبانی از HTTP/2"
+ },
+ "domains.trust-forwarded-proto": {
+ "defaultMessage": "اعتماد به هدرهای Forwarded Proto از سرور بالادستی (Upstream)"
+ },
+ "domains.use-dns": {
+ "defaultMessage": "استفاده از چالش DNS (DNS Challenge)"
+ },
+ "email-address": {
+ "defaultMessage": "آدرس ایمیل"
+ },
+ "empty-search": {
+ "defaultMessage": "نتیجهای یافت نشد"
+ },
+ "empty-subtitle": {
+ "defaultMessage": "چرا یکی ایجاد نمیکنید؟"
+ },
+ "enabled": {
+ "defaultMessage": "فعال"
+ },
+ "error.access.at-least-one": {
+ "defaultMessage": "حداقل یک کاربر احراز هویت یا یک قانون دسترسی لازم است."
+ },
+ "error.access.duplicate-usernames": {
+ "defaultMessage": "نامهای کاربری بخش احراز هویت باید یکتا باشند."
+ },
+ "error.invalid-auth": {
+ "defaultMessage": "ایمیل یا رمز عبور نامعتبر است"
+ },
+ "error.invalid-domain": {
+ "defaultMessage": "دامنه نامعتبر: {domain}"
+ },
+ "error.invalid-email": {
+ "defaultMessage": "آدرس ایمیل نامعتبر است"
+ },
+ "error.max-character-length": {
+ "defaultMessage": "حداکثر طول {max} کاراکتر است"
+ },
+ "error.max-domains": {
+ "defaultMessage": "تعداد دامنهها بیش از حد مجاز است؛ حداکثر {max} دامنه."
+ },
+ "error.maximum": {
+ "defaultMessage": "حداکثر {max} است"
+ },
+ "error.min-character-length": {
+ "defaultMessage": "حداقل طول {min} کاراکتر است"
+ },
+ "error.minimum": {
+ "defaultMessage": "حداقل {min} است"
+ },
+ "error.passwords-must-match": {
+ "defaultMessage": "رمزهای عبور باید مطابقت داشته باشند"
+ },
+ "error.required": {
+ "defaultMessage": "این فیلد الزامی است"
+ },
+ "expires.on": {
+ "defaultMessage": "انقضا: {date}"
+ },
+ "footer.github-fork": {
+ "defaultMessage": "Fork در GitHub"
+ },
+ "host.flags.block-exploits": {
+ "defaultMessage": "مسدودسازی اکسپلویتهای رایج"
+ },
+ "host.flags.cache-assets": {
+ "defaultMessage": "کشکردن فایلهای استاتیک"
+ },
+ "host.flags.preserve-path": {
+ "defaultMessage": "حفظ مسیر URL"
+ },
+ "host.flags.protocols": {
+ "defaultMessage": "پروتکلها"
+ },
+ "host.flags.websockets-upgrade": {
+ "defaultMessage": "پشتیبانی از WebSocket"
+ },
+ "host.forward-port": {
+ "defaultMessage": "پورت مقصد"
+ },
+ "host.forward-scheme": {
+ "defaultMessage": "پروتکل مقصد (Scheme)"
+ },
+ "hosts": {
+ "defaultMessage": "میزبانها"
+ },
+ "http-only": {
+ "defaultMessage": "فقط HTTP"
+ },
+ "lets-encrypt": {
+ "defaultMessage": "Let's Encrypt"
+ },
+ "lets-encrypt-via-dns": {
+ "defaultMessage": "Let's Encrypt از طریق DNS"
+ },
+ "lets-encrypt-via-http": {
+ "defaultMessage": "Let's Encrypt از طریق HTTP"
+ },
+ "loading": {
+ "defaultMessage": "در حال بارگذاری…"
+ },
+ "location.advanced-config": {
+ "defaultMessage": "دارای پیکربندی سفارشی Nginx"
+ },
+ "location.filter": {
+ "defaultMessage": "فیلتر بر اساس مسیر یا مقصد"
+ },
+ "login.2fa-code": {
+ "defaultMessage": "کد تأیید"
+ },
+ "login.2fa-code-placeholder": {
+ "defaultMessage": "کد را وارد کنید"
+ },
+ "login.2fa-description": {
+ "defaultMessage": "کد را از اپلیکیشن احراز هویت خود وارد کنید"
+ },
+ "login.2fa-title": {
+ "defaultMessage": "احراز هویت دومرحلهای"
+ },
+ "login.2fa-verify": {
+ "defaultMessage": "تأیید"
+ },
+ "login.title": {
+ "defaultMessage": "ورود به حساب کاربری"
+ },
+ "nginx-config.label": {
+ "defaultMessage": "پیکربندی سفارشی Nginx"
+ },
+ "nginx-config.placeholder": {
+ "defaultMessage": "# پیکربندی سفارشی Nginx را با مسئولیت خودتان اینجا وارد کنید!"
+ },
+ "no-permission-error": {
+ "defaultMessage": "شما دسترسی لازم برای مشاهده این مورد را ندارید."
+ },
+ "notfound.action": {
+ "defaultMessage": "بازگشت به صفحه اصلی"
+ },
+ "notfound.content": {
+ "defaultMessage": "متأسفیم، صفحهای که به دنبال آن هستید یافت نشد"
+ },
+ "notfound.title": {
+ "defaultMessage": "اوه… شما به یک صفحه خطا رسیدید"
+ },
+ "notification.error": {
+ "defaultMessage": "خطا"
+ },
+ "notification.object-deleted": {
+ "defaultMessage": "{object} حذف شد"
+ },
+ "notification.object-disabled": {
+ "defaultMessage": "{object} غیرفعال شد"
+ },
+ "notification.object-enabled": {
+ "defaultMessage": "{object} فعال شد"
+ },
+ "notification.object-renewed": {
+ "defaultMessage": "{object} تمدید شد"
+ },
+ "notification.object-saved": {
+ "defaultMessage": "{object} ذخیره شد"
+ },
+ "notification.success": {
+ "defaultMessage": "موفقیت"
+ },
+ "object.actions-title": {
+ "defaultMessage": "{object} #{id}"
+ },
+ "object.add": {
+ "defaultMessage": "افزودن {object}"
+ },
+ "object.delete": {
+ "defaultMessage": "حذف {object}"
+ },
+ "object.delete.content": {
+ "defaultMessage": "آیا مطمئن هستید که میخواهید این {object} را حذف کنید؟"
+ },
+ "object.edit": {
+ "defaultMessage": "ویرایش {object}"
+ },
+ "object.empty": {
+ "defaultMessage": "هیچ {objects} وجود ندارد"
+ },
+ "object.event.created": {
+ "defaultMessage": "{object} ایجاد شد"
+ },
+ "object.event.deleted": {
+ "defaultMessage": "{object} حذف شد"
+ },
+ "object.event.disabled": {
+ "defaultMessage": "{object} غیرفعال شد"
+ },
+ "object.event.enabled": {
+ "defaultMessage": "{object} فعال شد"
+ },
+ "object.event.renewed": {
+ "defaultMessage": "{object} تمدید شد"
+ },
+ "object.event.updated": {
+ "defaultMessage": "{object} بهروزرسانی شد"
+ },
+ "offline": {
+ "defaultMessage": "آفلاین"
+ },
+ "online": {
+ "defaultMessage": "آنلاین"
+ },
+ "options": {
+ "defaultMessage": "گزینهها"
+ },
+ "password": {
+ "defaultMessage": "رمز عبور"
+ },
+ "password.generate": {
+ "defaultMessage": "تولید رمز عبور تصادفی"
+ },
+ "password.hide": {
+ "defaultMessage": "پنهان کردن رمز عبور"
+ },
+ "password.show": {
+ "defaultMessage": "نمایش رمز عبور"
+ },
+ "permissions.hidden": {
+ "defaultMessage": "پنهان"
+ },
+ "permissions.manage": {
+ "defaultMessage": "مدیریت"
+ },
+ "permissions.view": {
+ "defaultMessage": "فقط مشاهده"
+ },
+ "permissions.visibility.all": {
+ "defaultMessage": "همه موارد"
+ },
+ "permissions.visibility.title": {
+ "defaultMessage": "محدودهٔ نمایش موارد"
+ },
+ "permissions.visibility.user": {
+ "defaultMessage": "فقط مواردی که خودتان ایجاد کردهاید"
+ },
+ "proxy-host": {
+ "defaultMessage": "میزبان پراکسی"
+ },
+ "proxy-host.forward-host": {
+ "defaultMessage": "نام میزبان / IP مقصد"
+ },
+ "proxy-hosts": {
+ "defaultMessage": "میزبانهای پراکسی"
+ },
+ "proxy-hosts.count": {
+ "defaultMessage": "{count} {count, plural, one {میزبان پراکسی} other {میزبان پراکسی}}"
+ },
+ "public": {
+ "defaultMessage": "عمومی"
+ },
+ "redirection-host": {
+ "defaultMessage": "میزبان تغییر مسیر"
+ },
+ "redirection-host.forward-domain": {
+ "defaultMessage": "دامنهٔ مقصد"
+ },
+ "redirection-host.forward-http-code": {
+ "defaultMessage": "کد HTTP"
+ },
+ "redirection-hosts": {
+ "defaultMessage": "میزبانهای تغییر مسیر"
+ },
+ "redirection-hosts.count": {
+ "defaultMessage": "{count} {count, plural, one {میزبان تغییر مسیر} other {میزبان تغییر مسیر}}"
+ },
+ "redirection-hosts.http-code.300": {
+ "defaultMessage": "300 انتخابهای متعدد"
+ },
+ "redirection-hosts.http-code.301": {
+ "defaultMessage": "301 انتقال دائمی"
+ },
+ "redirection-hosts.http-code.302": {
+ "defaultMessage": "302 انتقال موقت"
+ },
+ "redirection-hosts.http-code.303": {
+ "defaultMessage": "303 مشاهده آدرس دیگر"
+ },
+ "redirection-hosts.http-code.307": {
+ "defaultMessage": "307 تغییر مسیر موقت"
+ },
+ "redirection-hosts.http-code.308": {
+ "defaultMessage": "308 تغییر مسیر دائمی"
+ },
+ "role.admin": {
+ "defaultMessage": "مدیر"
+ },
+ "role.standard-user": {
+ "defaultMessage": "کاربر عادی"
+ },
+ "save": {
+ "defaultMessage": "ذخیره"
+ },
+ "setting": {
+ "defaultMessage": "تنظیم"
+ },
+ "settings": {
+ "defaultMessage": "تنظیمات"
+ },
+ "settings.default-site": {
+ "defaultMessage": "سایت پیشفرض"
+ },
+ "settings.default-site.404": {
+ "defaultMessage": "صفحه 404"
+ },
+ "settings.default-site.444": {
+ "defaultMessage": "بدون پاسخ (444)"
+ },
+ "settings.default-site.congratulations": {
+ "defaultMessage": "صفحه تبریک"
+ },
+ "settings.default-site.description": {
+ "defaultMessage": "مشخص میکند وقتی Nginx درخواستی با Host ناشناخته دریافت میکند چه چیزی نمایش داده شود."
+ },
+ "settings.default-site.html": {
+ "defaultMessage": "HTML سفارشی"
+ },
+ "settings.default-site.html.placeholder": {
+ "defaultMessage": ""
+ },
+ "settings.default-site.redirect": {
+ "defaultMessage": "تغییر مسیر"
+ },
+ "setup.preamble": {
+ "defaultMessage": "با ساختن حساب مدیر خود شروع کنید."
+ },
+ "setup.title": {
+ "defaultMessage": "خوش آمدید!"
+ },
+ "sign-in": {
+ "defaultMessage": "ورود"
+ },
+ "ssl-certificate": {
+ "defaultMessage": "گواهی SSL"
+ },
+ "stream": {
+ "defaultMessage": "استریم"
+ },
+ "stream.forward-host": {
+ "defaultMessage": "نام میزبان مقصد"
+ },
+ "stream.forward-host.placeholder": {
+ "defaultMessage": "example.com یا 10.0.0.1 یا 2001:db8:3333:4444:5555:6666:7777:8888"
+ },
+ "stream.incoming-port": {
+ "defaultMessage": "پورت ورودی"
+ },
+ "streams": {
+ "defaultMessage": "استریمها"
+ },
+ "streams.count": {
+ "defaultMessage": "{count} {count, plural, one {استریم} other {استریم}}"
+ },
+ "streams.tcp": {
+ "defaultMessage": "TCP"
+ },
+ "streams.udp": {
+ "defaultMessage": "UDP"
+ },
+ "test": {
+ "defaultMessage": "تست"
+ },
+ "update-available": {
+ "defaultMessage": "بهروزرسانی موجود است: {latestVersion}"
+ },
+ "user": {
+ "defaultMessage": "کاربر"
+ },
+ "user.change-password": {
+ "defaultMessage": "تغییر رمز عبور"
+ },
+ "user.confirm-password": {
+ "defaultMessage": "تأیید رمز عبور"
+ },
+ "user.current-password": {
+ "defaultMessage": "رمز عبور فعلی"
+ },
+ "user.edit-profile": {
+ "defaultMessage": "ویرایش پروفایل"
+ },
+ "user.full-name": {
+ "defaultMessage": "نام کامل"
+ },
+ "user.login-as": {
+ "defaultMessage": "ورود بهعنوان {name}"
+ },
+ "user.logout": {
+ "defaultMessage": "خروج"
+ },
+ "user.new-password": {
+ "defaultMessage": "رمز عبور جدید"
+ },
+ "user.nickname": {
+ "defaultMessage": "نام مستعار"
+ },
+ "user.set-password": {
+ "defaultMessage": "تنظیم رمز عبور"
+ },
+ "user.set-permissions": {
+ "defaultMessage": "تنظیم دسترسیها برای {name}"
+ },
+ "user.switch-dark": {
+ "defaultMessage": "تغییر به حالت تیره"
+ },
+ "user.switch-light": {
+ "defaultMessage": "تغییر به حالت روشن"
+ },
+ "user.two-factor": {
+ "defaultMessage": "احراز هویت دومرحلهای"
+ },
+ "username": {
+ "defaultMessage": "نام کاربری"
+ },
+ "users": {
+ "defaultMessage": "کاربران"
+ }
+}
diff --git a/frontend/src/locale/src/lang-list.json b/frontend/src/locale/src/lang-list.json
index da9232753..9ba12915a 100644
--- a/frontend/src/locale/src/lang-list.json
+++ b/frontend/src/locale/src/lang-list.json
@@ -70,5 +70,8 @@
},
"locale-az-AZ": {
"defaultMessage": "Azərbaycanca"
+ },
+ "locale-fa-IR": {
+ "defaultMessage": "فارسی"
}
}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 62c1c4810..f448d547c 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,13 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "src/App.tsx";
+import { getLocale, isRTLLocale } from "src/locale";
-import "@tabler/core/dist/css/tabler.min.css";
import "@tabler/core/dist/js/tabler.min.js";
import "./App.css";
-ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
-
-
- ,
-);
+const renderApp = () => {
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+ ,
+ );
+};
+
+const tablerStyles = isRTLLocale(getLocale())
+ ? import("@tabler/core/dist/css/tabler.rtl.min.css")
+ : import("@tabler/core/dist/css/tabler.min.css");
+
+void tablerStyles.then(renderApp);