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 <n0ctal@users.noreply.github.com>
This commit is contained in:
n0ctal
2026-08-18 14:43:59 +05:00
committed by GitHub
parent 2b1fe1fd02
commit 5c9268c431
19 changed files with 234 additions and 66 deletions
+21 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
</>