Explorar o código

feat(i18n): translate the log levels, access events and calendar labels (#6226)

* feat(i18n): translate the log levels, access events and calendar labels

The log-level selector, the access-log event tags, the Sub Formats sidebar
entry and the calendar choices were hardcoded English, so a fully translated
locale still showed them in English on core screens.

Add eleven keys across the 13 locales and reference them. Russian and
Ukrainian are translated; the remaining locales carry the English string, the
same convention the existing files already use for untranslated entries.

Two module-level constants had to move: the calendar list and the access-event
map were built outside the component, where t is not in scope. The event map
now stores keys and resolves them at render.

* fix(i18n): keep the log export language-independent and fit the translations

Three follow-ups from review. The downloaded x-ui.log had started carrying the
translated event text, so its contents depended on the panel language and the
Russian value for PROXY contains a space in a field format whose other values
are single tokens. The export keeps DIRECT/BLOCKED/PROXY; only the on-screen
tag is translated.

The log-level select had a fixed 95px width sized for "Warning", which clips
"Предупреждение"; it now grows with its content.

The three access filters stayed English while the tags they filter became
translated, so they use the same keys.

---------

Co-authored-by: n0ctal <[email protected]>
n0ctal hai 20 horas
pai
achega
5c9268c431

+ 21 - 9
frontend/public/mockServiceWorker.js

@@ -7,8 +7,8 @@
  * - Please do NOT modify this file.
  */
 
-const PACKAGE_VERSION = '2.14.7'
-const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
+const PACKAGE_VERSION = '2.15.0'
+const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
 const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
 const activeClientIds = new Set()
 
@@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
   if (client && activeClientIds.has(client.id)) {
     const serializedRequest = await serializeRequest(requestCloneForEvents)
 
+    // Omit the body of server-sent event stream responses.
+    // Cloning such responses would prevent client-side stream cancelations
+    // from reaching the original stream (a teed stream only cancels its
+    // source once both of its branches cancel) and would buffer the
+    // entire stream into the unconsumed clone indefinitely.
+    const isEventStreamResponse = response.headers
+      .get('content-type')
+      ?.toLowerCase()
+      .startsWith('text/event-stream')
+
     // Clone the response so both the client and the library could consume it.
-    const responseClone = response.clone()
+    const responseClone = isEventStreamResponse ? null : response.clone()
 
     sendToClient(
       client,
@@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
             ...serializedRequest,
           },
           response: {
-            type: responseClone.type,
-            status: responseClone.status,
-            statusText: responseClone.statusText,
-            headers: Object.fromEntries(responseClone.headers.entries()),
-            body: responseClone.body,
+            type: response.type,
+            status: response.status,
+            statusText: response.statusText,
+            headers: Object.fromEntries(response.headers.entries()),
+            body: responseClone ? responseClone.body : null,
           },
         },
       },
-      responseClone.body ? [serializedRequest.body, responseClone.body] : [],
+      responseClone && responseClone.body
+        ? [serializedRequest.body, responseClone.body]
+        : [],
     )
   }
 

+ 1 - 1
frontend/src/layouts/AppSidebar.tsx

@@ -219,7 +219,7 @@ export default function AppSidebar() {
       { key: '/settings#subscription', icon: <CloudServerOutlined />, label: t('pages.settings.subSettings') },
     ];
     if (showSubFormats) {
-      children.push({ key: '/settings#subscription-formats', icon: <CodeOutlined />, label: 'Sub Formats' });
+      children.push({ key: '/settings#subscription-formats', icon: <CodeOutlined />, label: t('menu.subFormats') });
     }
     return children;
   }, [t, showSubFormats]);

+ 1 - 1
frontend/src/pages/index/IndexPage.tsx

@@ -127,7 +127,7 @@ export default function IndexPage() {
 
   async function copyConfig() {
     const ok = await ClipboardManager.copyText(configText || '');
-    if (ok) messageApi.success('Copied');
+    if (ok) messageApi.success(t('copied'));
   }
 
   function downloadConfig() {

+ 6 - 6
frontend/src/pages/index/LogModal.tsx

@@ -105,14 +105,14 @@ export default function LogModal({ open, onClose }: LogModalProps) {
             <Select
               value={level}
               size="small"
-              style={{ width: 95 }}
+              style={{ minWidth: 95 }}
               onChange={setLevel}
               options={[
-                { value: 'debug', label: 'Debug' },
-                { value: 'info', label: 'Info' },
-                { value: 'notice', label: 'Notice' },
-                { value: 'warning', label: 'Warning' },
-                { value: 'err', label: 'Error' },
+                { value: 'debug', label: t('pages.index.logLevelDebug') },
+                { value: 'info', label: t('pages.index.logLevelInfo') },
+                { value: 'notice', label: t('pages.index.logLevelNotice') },
+                { value: 'warning', label: t('pages.index.logLevelWarning') },
+                { value: 'err', label: t('pages.index.logLevelError') },
               ]}
             />
           </Space.Compact>

+ 19 - 5
frontend/src/pages/index/XrayLogModal.tsx

@@ -1,4 +1,5 @@
 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import type { TFunction } from 'i18next';
 import { useTranslation } from 'react-i18next';
 import { Button, Checkbox, Form, Input, Modal, Select, Tag } from 'antd';
 import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
@@ -24,11 +25,24 @@ interface XrayLogEntry {
   Event?: number;
 }
 
-const EVENT_LABELS: Record<number, string> = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
+// The downloaded log is a data format people grep, so it keeps the stable
+// tokens; only what is rendered on screen follows the panel language.
+const EVENT_TOKENS: Record<number, string> = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
+
+const EVENT_KEYS: Record<number, string> = {
+  0: 'pages.index.accessDirect',
+  1: 'pages.index.accessBlocked',
+  2: 'pages.index.accessProxy',
+};
 const EVENT_COLORS: Record<number, string> = { 0: 'green', 1: 'red', 2: 'blue' };
 
-function eventLabel(ev?: number): string {
-  return EVENT_LABELS[ev ?? -1] ?? String(ev ?? '');
+function eventToken(ev?: number): string {
+  return EVENT_TOKENS[ev ?? -1] ?? String(ev ?? '');
+}
+
+function eventLabel(t: TFunction, ev?: number): string {
+  const key = EVENT_KEYS[ev ?? -1];
+  return key ? t(key) : String(ev ?? '');
 }
 
 function eventColor(ev?: number): string {
@@ -112,7 +126,7 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
       try {
         const dt = l.DateTime ? new Date(l.DateTime) : null;
         const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
-        const eventText = eventLabel(l.Event);
+        const eventText = eventToken(l.Event);
         const emailPart = l.Email ? ` Email=${l.Email}` : '';
         return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
       } catch {
@@ -193,7 +207,7 @@ export default function XrayLogModal({ open, onClose }: XrayLogModalProps) {
                   {shortTime(log.DateTime)}
                 </span>
                 <Tag color={eventColor(log.Event)} className="log-event-tag">
-                  {eventLabel(log.Event)}
+                  {eventLabel(t, log.Event)}
                 </Tag>
               </div>
               <div className="log-route">

+ 4 - 5
frontend/src/pages/settings/GeneralTab.tsx

@@ -34,10 +34,6 @@ interface GeneralTabProps {
   updateSetting: (patch: Partial<AllSetting>) => void;
 }
 
-const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [
-  { name: 'Gregorian (Standard)', value: 'gregorian' },
-  { name: 'Jalalian (شمسی)', value: 'jalalian' },
-];
 
 export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProps) {
   const { t } = useTranslation();
@@ -290,7 +286,10 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
                 value={allSetting.datepicker || 'gregorian'}
                 onChange={(v) => updateSetting({ datepicker: v as 'gregorian' | 'jalalian' })}
                 style={{ width: '100%' }}
-                options={DATEPICKER_LIST.map((d) => ({ value: d.value, label: d.name }))}
+                options={[
+                  { value: 'gregorian', label: t('pages.settings.calendarGregorian') },
+                  { value: 'jalalian', label: t('pages.settings.calendarJalalian') },
+                ]}
               />
             </SettingListItem>
           </>

+ 14 - 3
internal/web/translation/ar-EG.json

@@ -112,7 +112,8 @@
     "docs": "التوثيق",
     "openMenu": "فتح القائمة",
     "pinSidebar": "تثبيت الشريط الجانبي",
-    "unpinSidebar": "إلغاء تثبيت الشريط الجانبي"
+    "unpinSidebar": "إلغاء تثبيت الشريط الجانبي",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — حرج",
       "panel": "اللوحة",
       "threads": "الخيوط",
-      "uptime": "مدة التشغيل"
+      "uptime": "مدة التشغيل",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "إجمالي المرسل/المستقبل",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "يجب أن يبدأ المسار بالرمز /"
       },
       "secretClear": "مسح",
-      "secretClearUndo": "تراجع عن المسح"
+      "secretClearUndo": "تراجع عن المسح",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "احفظ",

+ 14 - 3
internal/web/translation/en-US.json

@@ -112,7 +112,8 @@
     "docs": "Documentation",
     "openMenu": "Open menu",
     "pinSidebar": "Pin sidebar",
-    "unpinSidebar": "Unpin sidebar"
+    "unpinSidebar": "Unpin sidebar",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — critical",
       "panel": "Panel",
       "threads": "Threads",
-      "uptime": "Uptime"
+      "uptime": "Uptime",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Total Sent/Received",
@@ -1469,7 +1478,9 @@
         "pathLeadingSlash": "Path must start with /"
       },
       "secretClear": "Clear",
-      "secretClearUndo": "Undo clear"
+      "secretClearUndo": "Undo clear",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "Save",

+ 14 - 3
internal/web/translation/es-ES.json

@@ -112,7 +112,8 @@
     "docs": "Documentación",
     "openMenu": "Abrir menú",
     "pinSidebar": "Fijar barra lateral",
-    "unpinSidebar": "Desfijar barra lateral"
+    "unpinSidebar": "Desfijar barra lateral",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — crítico",
       "panel": "Panel",
       "threads": "Hilos",
-      "uptime": "Tiempo activo"
+      "uptime": "Tiempo activo",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Subidas/Descargas Totales",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "La ruta debe comenzar con /"
       },
       "secretClear": "Borrar",
-      "secretClearUndo": "Deshacer borrado"
+      "secretClearUndo": "Deshacer borrado",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "Guardar configuración",

+ 14 - 3
internal/web/translation/fa-IR.json

@@ -112,7 +112,8 @@
     "docs": "مستندات",
     "openMenu": "باز کردن منو",
     "pinSidebar": "ثابت کردن نوار کناری",
-    "unpinSidebar": "برداشتن تثبیت نوار کناری"
+    "unpinSidebar": "برداشتن تثبیت نوار کناری",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — بحرانی",
       "panel": "پنل",
       "threads": "نخ‌ها",
-      "uptime": "مدت کارکرد"
+      "uptime": "مدت کارکرد",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "دریافت/ارسال کل",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "مسیر باید با / شروع شود"
       },
       "secretClear": "پاک کردن",
-      "secretClearUndo": "لغو پاک کردن"
+      "secretClearUndo": "لغو پاک کردن",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "ذخیره",

+ 14 - 3
internal/web/translation/id-ID.json

@@ -112,7 +112,8 @@
     "docs": "Dokumentasi",
     "openMenu": "Buka menu",
     "pinSidebar": "Sematkan bilah sisi",
-    "unpinSidebar": "Lepas sematan bilah sisi"
+    "unpinSidebar": "Lepas sematan bilah sisi",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — kritis",
       "panel": "Panel",
       "threads": "Thread",
-      "uptime": "Waktu aktif"
+      "uptime": "Waktu aktif",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Total Terkirim/Diterima",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "Path harus diawali dengan /"
       },
       "secretClear": "Hapus",
-      "secretClearUndo": "Batalkan hapus"
+      "secretClearUndo": "Batalkan hapus",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "Simpan",

+ 14 - 3
internal/web/translation/ja-JP.json

@@ -112,7 +112,8 @@
     "docs": "ドキュメント",
     "openMenu": "メニューを開く",
     "pinSidebar": "サイドバーを固定",
-    "unpinSidebar": "サイドバーの固定を解除"
+    "unpinSidebar": "サイドバーの固定を解除",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — 危険水準",
       "panel": "パネル",
       "threads": "スレッド",
-      "uptime": "稼働時間"
+      "uptime": "稼働時間",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "総アップロード / ダウンロード",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "パスは / で始まる必要があります"
       },
       "secretClear": "クリア",
-      "secretClearUndo": "クリアを取り消す"
+      "secretClearUndo": "クリアを取り消す",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "importRules": "ルールをインポート",

+ 14 - 3
internal/web/translation/pt-BR.json

@@ -112,7 +112,8 @@
     "docs": "Documentação",
     "openMenu": "Abrir menu",
     "pinSidebar": "Fixar barra lateral",
-    "unpinSidebar": "Desafixar barra lateral"
+    "unpinSidebar": "Desafixar barra lateral",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — crítico",
       "panel": "Painel",
       "threads": "Threads",
-      "uptime": "Tempo ativo"
+      "uptime": "Tempo ativo",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Total Enviado/Recebido",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "O caminho deve começar com /"
       },
       "secretClear": "Limpar",
-      "secretClearUndo": "Desfazer limpeza"
+      "secretClearUndo": "Desfazer limpeza",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "importRules": "Importar regras",

+ 14 - 3
internal/web/translation/ru-RU.json

@@ -112,7 +112,8 @@
     "docs": "Документация",
     "openMenu": "Открыть меню",
     "pinSidebar": "Закрепить боковую панель",
-    "unpinSidebar": "Открепить боковую панель"
+    "unpinSidebar": "Открепить боковую панель",
+    "subFormats": "Форматы подписки"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — критический уровень",
       "panel": "Панель",
       "threads": "Потоки",
-      "uptime": "Время работы"
+      "uptime": "Время работы",
+      "logLevelDebug": "Отладка",
+      "logLevelInfo": "Информация",
+      "logLevelNotice": "Уведомление",
+      "logLevelWarning": "Предупреждение",
+      "logLevelError": "Ошибка",
+      "accessDirect": "НАПРЯМУЮ",
+      "accessBlocked": "ЗАБЛОКИРОВАНО",
+      "accessProxy": "ЧЕРЕЗ ПРОКСИ"
     },
     "inbounds": {
       "totalDownUp": "Отправлено/получено",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "Путь должен начинаться с /"
       },
       "secretClear": "Очистить",
-      "secretClearUndo": "Отменить очистку"
+      "secretClearUndo": "Отменить очистку",
+      "calendarGregorian": "Григорианский (обычный)",
+      "calendarJalalian": "Джалали (شمسی)"
     },
     "xray": {
       "importRules": "Импорт правил",

+ 14 - 3
internal/web/translation/tr-TR.json

@@ -112,7 +112,8 @@
     "docs": "Belgeler",
     "openMenu": "Menüyü aç",
     "pinSidebar": "Kenar çubuğunu sabitle",
-    "unpinSidebar": "Kenar çubuğu sabitlemesini kaldır"
+    "unpinSidebar": "Kenar çubuğu sabitlemesini kaldır",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — kritik",
       "panel": "Panel",
       "threads": "İş parçacıkları",
-      "uptime": "Çalışma süresi"
+      "uptime": "Çalışma süresi",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Toplam Gönderilen/Alınan",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "Yol / ile başlamalıdır"
       },
       "secretClear": "Temizle",
-      "secretClearUndo": "Temizlemeyi geri al"
+      "secretClearUndo": "Temizlemeyi geri al",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "Kaydet",

+ 14 - 3
internal/web/translation/uk-UA.json

@@ -112,7 +112,8 @@
     "docs": "Документація",
     "openMenu": "Відкрити меню",
     "pinSidebar": "Закріпити бічну панель",
-    "unpinSidebar": "Відкріпити бічну панель"
+    "unpinSidebar": "Відкріпити бічну панель",
+    "subFormats": "Формати підписки"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — критичний рівень",
       "panel": "Панель",
       "threads": "Потоки",
-      "uptime": "Час роботи"
+      "uptime": "Час роботи",
+      "logLevelDebug": "Налагодження",
+      "logLevelInfo": "Інформація",
+      "logLevelNotice": "Сповіщення",
+      "logLevelWarning": "Попередження",
+      "logLevelError": "Помилка",
+      "accessDirect": "НАПРЯМУ",
+      "accessBlocked": "ЗАБЛОКОВАНО",
+      "accessProxy": "ЧЕРЕЗ ПРОКСІ"
     },
     "inbounds": {
       "totalDownUp": "Всього надісланих/отриманих",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "Шлях має починатися з /"
       },
       "secretClear": "Очистити",
-      "secretClearUndo": "Скасувати очищення"
+      "secretClearUndo": "Скасувати очищення",
+      "calendarGregorian": "Григоріанський (звичайний)",
+      "calendarJalalian": "Джалалі (شمسی)"
     },
     "xray": {
       "save": "Зберегти",

+ 14 - 3
internal/web/translation/vi-VN.json

@@ -112,7 +112,8 @@
     "docs": "Tài liệu",
     "openMenu": "Mở menu",
     "pinSidebar": "Ghim thanh bên",
-    "unpinSidebar": "Bỏ ghim thanh bên"
+    "unpinSidebar": "Bỏ ghim thanh bên",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — nguy cấp",
       "panel": "Panel",
       "threads": "Luồng",
-      "uptime": "Thời gian chạy"
+      "uptime": "Thời gian chạy",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "Tổng tải lên/tải xuống",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "Đường dẫn phải bắt đầu bằng /"
       },
       "secretClear": "Xóa",
-      "secretClearUndo": "Hoàn tác xóa"
+      "secretClearUndo": "Hoàn tác xóa",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "importRules": "Nhập quy tắc",

+ 14 - 3
internal/web/translation/zh-CN.json

@@ -112,7 +112,8 @@
     "docs": "文档",
     "openMenu": "打开菜单",
     "pinSidebar": "固定侧边栏",
-    "unpinSidebar": "取消固定侧边栏"
+    "unpinSidebar": "取消固定侧边栏",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — 危险",
       "panel": "面板",
       "threads": "线程",
-      "uptime": "运行时间"
+      "uptime": "运行时间",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "总上传 / 下载",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "路径必须以 / 开头"
       },
       "secretClear": "清除",
-      "secretClearUndo": "撤销清除"
+      "secretClearUndo": "撤销清除",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "importRules": "导入规则",

+ 14 - 3
internal/web/translation/zh-TW.json

@@ -112,7 +112,8 @@
     "docs": "文件",
     "openMenu": "開啟選單",
     "pinSidebar": "固定側邊欄",
-    "unpinSidebar": "取消固定側邊欄"
+    "unpinSidebar": "取消固定側邊欄",
+    "subFormats": "Sub Formats"
   },
   "pages": {
     "login": {
@@ -257,7 +258,15 @@
       "healthCritical": "{list} — 危險",
       "panel": "面板",
       "threads": "執行緒",
-      "uptime": "執行時間"
+      "uptime": "執行時間",
+      "logLevelDebug": "Debug",
+      "logLevelInfo": "Info",
+      "logLevelNotice": "Notice",
+      "logLevelWarning": "Warning",
+      "logLevelError": "Error",
+      "accessDirect": "DIRECT",
+      "accessBlocked": "BLOCKED",
+      "accessProxy": "PROXY"
     },
     "inbounds": {
       "totalDownUp": "總上傳 / 下載",
@@ -1352,7 +1361,9 @@
         "pathLeadingSlash": "路徑必須以 / 開頭"
       },
       "secretClear": "清除",
-      "secretClearUndo": "復原清除"
+      "secretClearUndo": "復原清除",
+      "calendarGregorian": "Gregorian (Standard)",
+      "calendarJalalian": "Jalalian (شمسی)"
     },
     "xray": {
       "save": "儲存",