Compare commits
2
Commits
main
...
v3.7.0-agent.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17be1ad5ae | ||
|
|
a654a96c38 |
@@ -5042,44 +5042,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/getXrayVersion": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "List Xray binary versions available for install on this host.",
|
||||
"operationId": "get_panel_api_server_getXrayVersion",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": [
|
||||
"v25.10.31",
|
||||
"v25.9.15",
|
||||
"v25.8.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/getPanelUpdateInfo": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -5613,47 +5575,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/installXray/{version}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "Download and install the specified Xray version. Pass \"latest\" for the newest release.",
|
||||
"operationId": "post_panel_api_server_installXray_version",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "version",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Xray tag (e.g. v25.10.31) or \"latest\".",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/updatePanel": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -5730,87 +5651,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/updateGeofile": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "Refresh the default GeoIP / GeoSite data files. Body can include a fileName, or use the /:fileName variant.",
|
||||
"operationId": "post_panel_api_server_updateGeofile",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/updateGeofile/{fileName}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).",
|
||||
"operationId": "post_panel_api_server_updateGeofile_fileName",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "fileName",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Filename of the data file to refresh.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/logs/{count}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
||||
@@ -12,16 +12,13 @@ import {
|
||||
ClusterOutlined,
|
||||
CodeOutlined,
|
||||
DashboardOutlined,
|
||||
DatabaseOutlined,
|
||||
ExportOutlined,
|
||||
GithubOutlined,
|
||||
GlobalOutlined,
|
||||
HeartOutlined,
|
||||
ImportOutlined,
|
||||
LogoutOutlined,
|
||||
MailOutlined,
|
||||
MenuOutlined,
|
||||
MessageOutlined,
|
||||
MoonFilled,
|
||||
MoonOutlined,
|
||||
PushpinFilled,
|
||||
@@ -245,12 +242,6 @@ export default function AppSidebar() {
|
||||
icon: <SafetyOutlined />,
|
||||
label: t('pages.settings.securitySettings'),
|
||||
},
|
||||
{
|
||||
key: '/settings#telegram',
|
||||
icon: <MessageOutlined />,
|
||||
label: t('pages.settings.TGBotSettings'),
|
||||
},
|
||||
{ key: '/settings#email', icon: <MailOutlined />, label: t('pages.settings.emailSettings') },
|
||||
{
|
||||
key: '/settings#subscription',
|
||||
icon: <CloudServerOutlined />,
|
||||
@@ -275,12 +266,7 @@ export default function AppSidebar() {
|
||||
}, [t, showSubFormats, showSubBalancers]);
|
||||
|
||||
const xrayChildren = useMemo<NonNullable<MenuProps['items']>>(
|
||||
() => [
|
||||
{ key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') },
|
||||
{ key: '/xray#balancer', icon: <ClusterOutlined />, label: t('pages.xray.Balancers') },
|
||||
{ key: '/xray#dns', icon: <DatabaseOutlined />, label: 'DNS' },
|
||||
{ key: '/xray#advanced', icon: <CodeOutlined />, label: t('pages.xray.advancedTemplate') },
|
||||
],
|
||||
() => [{ key: '/xray#basic', icon: <SettingOutlined />, label: t('pages.xray.basicTemplate') }],
|
||||
[t],
|
||||
);
|
||||
|
||||
|
||||
@@ -396,12 +396,6 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/getXrayVersion',
|
||||
summary: 'List Xray binary versions available for install on this host.',
|
||||
response: '{\n "success": true,\n "obj": ["v25.10.31", "v25.9.15", "v25.8.1"]\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/server/getPanelUpdateInfo',
|
||||
@@ -499,20 +493,6 @@ export const sections: readonly Section[] = [
|
||||
'Reload Xray with the current config. Typically required after structural inbound or routing changes.',
|
||||
errorResponse: '{\n "success": false,\n "msg": "Xray config is invalid: ..."\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/installXray/:version',
|
||||
summary:
|
||||
'Download and install the specified Xray version. Pass "latest" for the newest release.',
|
||||
params: [
|
||||
{
|
||||
name: 'version',
|
||||
in: 'path',
|
||||
type: 'string',
|
||||
desc: 'Xray tag (e.g. v25.10.31) or "latest".',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/updatePanel',
|
||||
@@ -534,34 +514,6 @@ export const sections: readonly Section[] = [
|
||||
],
|
||||
body: 'dev=true',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/updateGeofile',
|
||||
summary:
|
||||
'Refresh the default GeoIP / GeoSite data files. Body can include a fileName, or use the /:fileName variant.',
|
||||
params: [
|
||||
{
|
||||
name: 'fileName',
|
||||
in: 'body (form)',
|
||||
type: 'string',
|
||||
desc: 'Filename to update (e.g. geoip.dat, geosite.dat). Omit to update all defaults.',
|
||||
},
|
||||
],
|
||||
body: 'fileName=geoip.dat',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/updateGeofile/:fileName',
|
||||
summary: 'Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).',
|
||||
params: [
|
||||
{
|
||||
name: 'fileName',
|
||||
in: 'path',
|
||||
type: 'string',
|
||||
desc: 'Filename of the data file to refresh.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/server/logs/:count',
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Form, Input, Modal, Select, Space, Spin, Typography, message } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
|
||||
interface GeodataAssetRow {
|
||||
url: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
interface GeodataSectionProps {
|
||||
active: boolean;
|
||||
onBusy: (e: { busy: boolean; tip?: string }) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_CRON = '0 4 * * *';
|
||||
// Xray resolves `file` inside its asset directory; plain file names only.
|
||||
const FILE_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
|
||||
|
||||
function fileNameFromUrl(url: string): string {
|
||||
try {
|
||||
const seg = new URL(url).pathname.split('/').filter(Boolean).pop() || '';
|
||||
return FILE_NAME_PATTERN.test(seg) ? seg : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default function GeodataSection({ active, onBusy, onClose }: GeodataSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cron, setCron] = useState(DEFAULT_CRON);
|
||||
const [outbound, setOutbound] = useState<string | undefined>(undefined);
|
||||
const [rows, setRows] = useState<GeodataAssetRow[]>([]);
|
||||
const [outboundTags, setOutboundTags] = useState<string[]>([]);
|
||||
const [template, setTemplate] = useState<Record<string, unknown> | null>(null);
|
||||
const outboundTestUrlRef = useRef('');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true });
|
||||
if (!msg?.success || typeof msg.obj !== 'string') return;
|
||||
const payload = JSON.parse(msg.obj) as Record<string, unknown>;
|
||||
const next = (payload.xraySetting || {}) as Record<string, unknown>;
|
||||
setTemplate(next);
|
||||
outboundTestUrlRef.current =
|
||||
typeof payload.outboundTestUrl === 'string' ? payload.outboundTestUrl : '';
|
||||
|
||||
const geodata = (next.geodata || {}) as Record<string, unknown>;
|
||||
const assets = Array.isArray(geodata.assets) ? geodata.assets : [];
|
||||
setRows(
|
||||
assets
|
||||
.filter((a): a is Record<string, unknown> => !!a && typeof a === 'object')
|
||||
.map((a) => ({ url: String(a.url ?? ''), file: String(a.file ?? '') })),
|
||||
);
|
||||
setCron(typeof geodata.cron === 'string' && geodata.cron ? geodata.cron : DEFAULT_CRON);
|
||||
setOutbound(
|
||||
typeof geodata.outbound === 'string' && geodata.outbound ? geodata.outbound : undefined,
|
||||
);
|
||||
|
||||
// Download outbound candidates: template outbounds + subscription outbounds.
|
||||
// Skip blackhole outbounds — routing a download through one just drops it.
|
||||
const tags = new Set<string>();
|
||||
const outbounds = Array.isArray(next.outbounds) ? next.outbounds : [];
|
||||
for (const o of outbounds) {
|
||||
if (!o || typeof o !== 'object') continue;
|
||||
const rec = o as Record<string, unknown>;
|
||||
if (rec.protocol === 'blackhole') continue;
|
||||
const tag = rec.tag;
|
||||
if (typeof tag === 'string' && tag) tags.add(tag);
|
||||
}
|
||||
const subTags = Array.isArray(payload.subscriptionOutboundTags)
|
||||
? payload.subscriptionOutboundTags
|
||||
: [];
|
||||
for (const tag of subTags) {
|
||||
if (typeof tag === 'string' && tag) tags.add(tag);
|
||||
}
|
||||
setOutboundTags([...tags]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [wasActive, setWasActive] = useState(false);
|
||||
if (active !== wasActive) {
|
||||
setWasActive(active);
|
||||
if (active) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (active) void load();
|
||||
}, [active, load]);
|
||||
|
||||
function setRow(index: number, patch: Partial<GeodataAssetRow>) {
|
||||
setRows((prev) => prev.map((r, i) => (i === index ? { ...r, ...patch } : r)));
|
||||
}
|
||||
|
||||
function onUrlBlur(index: number) {
|
||||
setRows((prev) =>
|
||||
prev.map((r, i) => (i === index && !r.file ? { ...r, file: fileNameFromUrl(r.url) } : r)),
|
||||
);
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!template) return;
|
||||
const assets = rows
|
||||
.map((r) => ({ url: r.url.trim(), file: r.file.trim() }))
|
||||
.filter((r) => r.url || r.file);
|
||||
for (const a of assets) {
|
||||
// Xray's geodata downloader accepts HTTPS URLs only.
|
||||
if (!/^https:\/\/\S+$/i.test(a.url)) {
|
||||
messageApi.error(t('pages.index.geodataInvalidUrl'));
|
||||
return;
|
||||
}
|
||||
if (!FILE_NAME_PATTERN.test(a.file)) {
|
||||
messageApi.error(t('pages.index.geodataInvalidFile'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const cronValue = cron.trim();
|
||||
if (assets.length > 0 && cronValue && cronValue.split(/\s+/).length !== 5) {
|
||||
messageApi.error(t('pages.index.geodataInvalidCron'));
|
||||
return;
|
||||
}
|
||||
|
||||
modal.confirm({
|
||||
title: t('pages.index.geodataConfirmTitle'),
|
||||
content: t('pages.index.geodataConfirmContent'),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const next: Record<string, unknown> = { ...template };
|
||||
if (assets.length === 0) {
|
||||
delete next.geodata;
|
||||
} else {
|
||||
const geodata: Record<string, unknown> = { assets };
|
||||
if (cronValue) geodata.cron = cronValue;
|
||||
if (outbound) geodata.outbound = outbound;
|
||||
next.geodata = geodata;
|
||||
}
|
||||
onClose();
|
||||
onBusy({ busy: true, tip: t('pages.index.dontRefresh') });
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/api/xray/update', {
|
||||
xraySetting: JSON.stringify(next, null, 2),
|
||||
outboundTestUrl: outboundTestUrlRef.current,
|
||||
});
|
||||
if (msg?.success) {
|
||||
await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||
}
|
||||
} finally {
|
||||
onBusy({ busy: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{modalContextHolder}
|
||||
{messageContextHolder}
|
||||
<Spin spinning={loading}>
|
||||
<Alert type="info" className="mb-12" title={t('pages.index.geodataHint')} showIcon />
|
||||
<Form layout="vertical">
|
||||
<Form.Item label={t('pages.index.geodataCron')} style={{ marginBottom: 8 }}>
|
||||
<Input
|
||||
value={cron}
|
||||
placeholder={DEFAULT_CRON}
|
||||
onChange={(e) => setCron(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.index.geodataOutbound')} style={{ marginBottom: 8 }}>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
allowClear
|
||||
value={outbound}
|
||||
onChange={(v) => setOutbound(v)}
|
||||
options={outboundTags.map((tag) => ({ label: tag, value: tag }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Space orientation="vertical" style={{ width: '100%' }} size={8}>
|
||||
{rows.length === 0 && (
|
||||
<Typography.Text type="secondary">{t('pages.index.geodataEmpty')}</Typography.Text>
|
||||
)}
|
||||
{rows.map((row, i) => (
|
||||
<Space.Compact key={i} style={{ width: '100%' }}>
|
||||
<Input
|
||||
style={{ width: '60%' }}
|
||||
placeholder="https://example.com/geosite_custom.dat"
|
||||
value={row.url}
|
||||
onChange={(e) => setRow(i, { url: e.target.value })}
|
||||
onBlur={() => onUrlBlur(i)}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: '40%' }}
|
||||
placeholder={t('pages.index.geodataFile')}
|
||||
value={row.file}
|
||||
onChange={(e) => setRow(i, { file: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('delete')}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => setRows((p) => p.filter((_, j) => j !== i))}
|
||||
/>
|
||||
</Space.Compact>
|
||||
))}
|
||||
<div className="actions-row">
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setRows((p) => [...p, { url: '', file: '' }])}
|
||||
>
|
||||
{t('pages.index.geodataAddFile')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={save} disabled={loading || !template}>
|
||||
{t('pages.index.geodataSaveRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
</Space>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +38,6 @@ const SystemHistoryModal = lazy(() => import('./SystemHistoryModal'));
|
||||
const XrayMetricsModal = lazy(() => import('./XrayMetricsModal'));
|
||||
const XrayLogModal = lazy(() => import('./XrayLogModal'));
|
||||
const AmneziaWGLogModal = lazy(() => import('./AmneziaWGLogModal'));
|
||||
const VersionModal = lazy(() => import('./VersionModal'));
|
||||
import './IndexPage.css';
|
||||
|
||||
export default function IndexPage() {
|
||||
@@ -69,7 +68,6 @@ export default function IndexPage() {
|
||||
const [xrayMetricsOpen, setXrayMetricsOpen] = useState(false);
|
||||
const [xrayLogsOpen, setXrayLogsOpen] = useState(false);
|
||||
const [amneziawgLogsOpen, setAmneziawgLogsOpen] = useState(false);
|
||||
const [versionOpen, setVersionOpen] = useState(false);
|
||||
const [configTextOpen, setConfigTextOpen] = useState(false);
|
||||
const [configText, setConfigText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -210,7 +208,6 @@ export default function IndexPage() {
|
||||
onOpenSystemHistory={() => setSysHistoryOpen(true)}
|
||||
onOpenXrayMetrics={() => setXrayMetricsOpen(true)}
|
||||
onOpenPanelUpdate={() => setPanelUpdateOpen(true)}
|
||||
onOpenVersionSwitch={() => setVersionOpen(true)}
|
||||
/>
|
||||
|
||||
{health && (
|
||||
@@ -334,15 +331,6 @@ export default function IndexPage() {
|
||||
<LazyMount when={amneziawgLogsOpen}>
|
||||
<AmneziaWGLogModal open={amneziawgLogsOpen} onClose={() => setAmneziawgLogsOpen(false)} />
|
||||
</LazyMount>
|
||||
<LazyMount when={versionOpen}>
|
||||
<VersionModal
|
||||
open={versionOpen}
|
||||
status={status}
|
||||
onClose={() => setVersionOpen(false)}
|
||||
onBusy={setBusy}
|
||||
/>
|
||||
</LazyMount>
|
||||
|
||||
<LazyMount when={configTextOpen}>
|
||||
<Modal
|
||||
open={configTextOpen}
|
||||
|
||||
@@ -35,7 +35,6 @@ interface OverviewActionBarProps {
|
||||
onOpenSystemHistory: () => void;
|
||||
onOpenXrayMetrics: () => void;
|
||||
onOpenPanelUpdate: () => void;
|
||||
onOpenVersionSwitch: () => void;
|
||||
}
|
||||
|
||||
interface BarAction {
|
||||
@@ -69,7 +68,6 @@ export default function OverviewActionBar({
|
||||
onOpenSystemHistory,
|
||||
onOpenXrayMetrics,
|
||||
onOpenPanelUpdate,
|
||||
onOpenVersionSwitch,
|
||||
}: OverviewActionBarProps) {
|
||||
const { t } = useTranslation();
|
||||
const stateText = t(XRAY_STATE_KEYS[status.xray.state] ?? 'pages.index.xrayStatusUnknown');
|
||||
@@ -147,13 +145,7 @@ export default function OverviewActionBar({
|
||||
<span className="ov-state" data-state={status.xray.state}>
|
||||
<span className="ov-state-dot" style={{ color: status.xray.color }} />
|
||||
<span>{`${t('pages.index.xrayStatus')} · ${stateText}`}</span>
|
||||
{hasVersion && (
|
||||
<Tooltip title={t('pages.index.xraySwitch')}>
|
||||
<button type="button" className="ov-state-version" onClick={onOpenVersionSwitch}>
|
||||
{`v${status.xray.version}`}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{hasVersion && <span className="ov-state-version">{`v${status.xray.version}`}</span>}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
.version-list {
|
||||
width: 100%;
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.version-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
.version-list-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.reload-icon {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Collapse, Modal, Radio, Spin, Tag, Tooltip } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import type { Status } from '@/models/status';
|
||||
import GeodataSection from './GeodataSection';
|
||||
import './VersionModal.css';
|
||||
|
||||
interface BusyEvent {
|
||||
busy: boolean;
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
interface VersionModalProps {
|
||||
open: boolean;
|
||||
status: Status;
|
||||
onClose: () => void;
|
||||
onBusy: (e: BusyEvent) => void;
|
||||
}
|
||||
|
||||
const GEOFILES = [
|
||||
'geosite.dat',
|
||||
'geoip.dat',
|
||||
'geosite_IR.dat',
|
||||
'geoip_IR.dat',
|
||||
'geosite_RU.dat',
|
||||
'geoip_RU.dat',
|
||||
];
|
||||
|
||||
export default function VersionModal({ open, status, onClose, onBusy }: VersionModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [activeKey, setActiveKey] = useState<string | string[]>('1');
|
||||
const [versions, setVersions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchVersions = useCallback(async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.get<string[]>('/panel/api/server/getXrayVersion');
|
||||
if (msg?.success) setVersions(msg.obj || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setLoading(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) void fetchVersions();
|
||||
}, [open, fetchVersions]);
|
||||
|
||||
function switchXrayVersion(version: string) {
|
||||
modal.confirm({
|
||||
title: t('pages.index.xraySwitchVersionDialog'),
|
||||
content: t('pages.index.xraySwitchVersionDialogDesc').replace('#version#', version),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
onClose();
|
||||
onBusy({ busy: true, tip: t('pages.index.dontRefresh') });
|
||||
try {
|
||||
await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
||||
} finally {
|
||||
onBusy({ busy: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function updateGeofile(fileName: string) {
|
||||
const isSingle = !!fileName;
|
||||
modal.confirm({
|
||||
title: t('pages.index.geofileUpdateDialog'),
|
||||
content: isSingle
|
||||
? t('pages.index.geofileUpdateDialogDesc').replace('#filename#', fileName)
|
||||
: t('pages.index.geofilesUpdateDialogDesc'),
|
||||
okText: t('confirm'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
onClose();
|
||||
onBusy({ busy: true, tip: t('pages.index.dontRefresh') });
|
||||
const url = isSingle
|
||||
? `/panel/api/server/updateGeofile/${fileName}`
|
||||
: '/panel/api/server/updateGeofile';
|
||||
try {
|
||||
await HttpUtil.post(url);
|
||||
} finally {
|
||||
onBusy({ busy: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const activeKeyStr = Array.isArray(activeKey) ? activeKey[0] : activeKey;
|
||||
|
||||
return (
|
||||
<Modal open={open} title={t('pages.index.xrayUpdates')} footer={null} onCancel={onClose}>
|
||||
{modalContextHolder}
|
||||
<Spin spinning={loading}>
|
||||
<Collapse
|
||||
accordion
|
||||
activeKey={activeKey}
|
||||
onChange={setActiveKey}
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'Xray',
|
||||
children: (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
className="mb-12"
|
||||
title={t('pages.index.xraySwitchClickDesk')}
|
||||
showIcon
|
||||
/>
|
||||
<div className="version-list">
|
||||
{versions.map((version, index) => (
|
||||
<div key={version} className="version-list-item">
|
||||
<Tag color={index % 2 === 0 ? 'purple' : 'green'}>{version}</Tag>
|
||||
<Radio
|
||||
checked={version === `v${status?.xray?.version}`}
|
||||
onClick={() => switchXrayVersion(version)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Geofiles',
|
||||
children: (
|
||||
<>
|
||||
<div className="version-list">
|
||||
{GEOFILES.map((file, index) => (
|
||||
<div key={file} className="version-list-item">
|
||||
<Tag color={index % 2 === 0 ? 'purple' : 'green'}>{file}</Tag>
|
||||
<Tooltip title={t('update')}>
|
||||
<ReloadOutlined
|
||||
className="reload-icon"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('update')}
|
||||
onClick={() => updateGeofile(file)}
|
||||
onKeyDown={activateOnKey(() => updateGeofile(file))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="actions-row">
|
||||
<Button onClick={() => updateGeofile('')}>
|
||||
{t('pages.index.geofilesUpdateAll')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: t('pages.index.geodataTitle'),
|
||||
children: (
|
||||
<GeodataSection active={activeKeyStr === '3'} onBusy={onBusy} onClose={onClose} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Spin>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from 'antd';
|
||||
import { MailOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { DefaultSettingTag, SettingListItem } from '@/components/ui';
|
||||
import { EmailNotifications } from '@/components/ui/notifications/EmailNotifications';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import SecretInput from './SecretInput';
|
||||
|
||||
interface EmailTabProps {
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
interface SmtpTestResult {
|
||||
success: boolean;
|
||||
stage?: string;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [testResult, setTestResult] = useState<SmtpTestResult | null>(null);
|
||||
|
||||
const stageLabel: Record<string, string> = {
|
||||
connect: t('pages.settings.smtpStageConnect'),
|
||||
auth: t('pages.settings.smtpStageAuth'),
|
||||
send: t('pages.settings.smtpStageSend'),
|
||||
};
|
||||
|
||||
async function handleTestSmtp() {
|
||||
setTestLoading(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const res = (await HttpUtil.post('/panel/api/setting/testSmtp')) as SmtpTestResult;
|
||||
setTestResult(res);
|
||||
} catch (e: unknown) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
msg: e instanceof Error ? e.message : t('pages.settings.requestFailed'),
|
||||
});
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.smtpSettings'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpEnable')}
|
||||
description={t('pages.settings.smtpEnableDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.smtpEnable}
|
||||
onChange={(v) => updateSetting({ smtpEnable: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpHost')}
|
||||
description={t('pages.settings.smtpHostDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.smtpHost}
|
||||
placeholder="smtp.gmail.com"
|
||||
onChange={(e) => updateSetting({ smtpHost: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpPort')}
|
||||
badge={<DefaultSettingTag settingKey="smtpPort" value={allSetting.smtpPort} />}
|
||||
description={t('pages.settings.smtpPortDesc')}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.smtpPort}
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ smtpPort: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpUsername')}
|
||||
description={t('pages.settings.smtpUsernameDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.smtpUsername}
|
||||
placeholder="user@gmail.com"
|
||||
onChange={(e) => updateSetting({ smtpUsername: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpPassword')}
|
||||
description={
|
||||
allSetting.hasSmtpPassword && !allSetting.clearSmtpPassword
|
||||
? t('pages.settings.smtpPasswordConfigured')
|
||||
: t('pages.settings.smtpPasswordDesc')
|
||||
}
|
||||
>
|
||||
<SecretInput
|
||||
value={allSetting.smtpPassword}
|
||||
configured={allSetting.hasSmtpPassword}
|
||||
clearArmed={allSetting.clearSmtpPassword}
|
||||
placeholder={t('pages.settings.smtpPasswordPlaceholder')}
|
||||
onChange={(v) => updateSetting({ smtpPassword: v })}
|
||||
onClearArmedChange={(armed) => updateSetting({ clearSmtpPassword: armed })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpFrom')}
|
||||
description={t('pages.settings.smtpFromDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.smtpFrom}
|
||||
placeholder="user@gmail.com"
|
||||
onChange={(e) => updateSetting({ smtpFrom: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpFromName')}
|
||||
description={t('pages.settings.smtpFromNameDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.smtpFromName}
|
||||
placeholder="3x-ui"
|
||||
onChange={(e) => updateSetting({ smtpFromName: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpTo')}
|
||||
description={t('pages.settings.smtpToDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.smtpTo}
|
||||
placeholder="admin@example.com, ops@example.com"
|
||||
onChange={(e) => updateSetting({ smtpTo: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpEncryption')}
|
||||
description={t('pages.settings.smtpEncryptionDesc')}
|
||||
>
|
||||
<Select
|
||||
value={allSetting.smtpEncryptionType}
|
||||
onChange={(v) => updateSetting({ smtpEncryptionType: v })}
|
||||
options={[
|
||||
{ value: 'none', label: t('pages.settings.smtpEncryptionNone') },
|
||||
{ value: 'starttls', label: t('pages.settings.smtpEncryptionStartTLS') },
|
||||
{ value: 'tls', label: t('pages.settings.smtpEncryptionTLS') },
|
||||
]}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={testLoading}
|
||||
onClick={handleTestSmtp}
|
||||
>
|
||||
{t('pages.settings.testSmtp')}
|
||||
</Button>
|
||||
{testResult && (
|
||||
<Alert
|
||||
type={testResult.success ? 'success' : 'error'}
|
||||
title={
|
||||
testResult.success ? (
|
||||
t('pages.settings.' + testResult.msg)
|
||||
) : (
|
||||
<span>
|
||||
<b>{stageLabel[testResult.stage || ''] || testResult.stage}:</b>{' '}
|
||||
{t('pages.settings.' + testResult.msg)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
showIcon
|
||||
closable={{ onClose: () => setTestResult(null) }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: catTabLabel(<MailOutlined />, t('pages.settings.emailNotifications'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.smtpEventBusNotify')}
|
||||
description={t('pages.settings.smtpEventBusNotifyDesc')}
|
||||
>
|
||||
<EmailNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input, InputNumber, Select, Switch, Tabs } from 'antd';
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
BellOutlined,
|
||||
ClockCircleOutlined,
|
||||
GlobalOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ClockCircleOutlined, SafetyCertificateOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { HttpUtil, LanguageManager } from '@/utils';
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
@@ -16,7 +9,6 @@ import { DefaultSettingTag, SettingListItem } from '@/components/ui';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import { sanitizePath } from './uriPath';
|
||||
import SecretInput from './SecretInput';
|
||||
|
||||
interface ApiMsg<T = unknown> {
|
||||
success?: boolean;
|
||||
@@ -33,39 +25,9 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
const { isMobile } = useMediaQuery();
|
||||
|
||||
const [lang, setLang] = useState<string>(() => LanguageManager.getLanguage());
|
||||
const [inboundOptions, setInboundOptions] = useState<{ label: string; value: string }[]>([]);
|
||||
const [outboundTagList, setOutboundTagList] = useState<string[]>([]);
|
||||
const [balancerTagList, setBalancerTagList] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
// /options is the slim picker-shaped endpoint — it skips the heavy
|
||||
// per-client settings and clientStats payloads that /list ships.
|
||||
const msg = (await HttpUtil.get('/panel/api/inbounds/options')) as ApiMsg<
|
||||
{
|
||||
tag: string;
|
||||
protocol: string;
|
||||
port: number;
|
||||
}[]
|
||||
>;
|
||||
if (cancelled) return;
|
||||
if (msg?.success && Array.isArray(msg.obj)) {
|
||||
setInboundOptions(
|
||||
msg.obj.map((ib) => ({
|
||||
label: `${ib.tag} (${ib.protocol}@${ib.port})`,
|
||||
value: ib.tag,
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setInboundOptions([]);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
@@ -136,20 +98,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
];
|
||||
}, [outboundTagList, balancerTagList, t]);
|
||||
|
||||
const ldapInboundTagList = useMemo(() => {
|
||||
const csv = allSetting.ldapInboundTags || '';
|
||||
return csv.length
|
||||
? csv
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
}, [allSetting.ldapInboundTags]);
|
||||
|
||||
function setLdapInboundTagList(list: string[]) {
|
||||
updateSetting({ ldapInboundTags: Array.isArray(list) ? list.join(',') : '' });
|
||||
}
|
||||
|
||||
function onLangChange(value: string) {
|
||||
setLang(value);
|
||||
LanguageManager.setLanguage(value);
|
||||
@@ -327,43 +275,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.expireTimeDiff')}
|
||||
badge={<DefaultSettingTag settingKey="expireDiff" value={allSetting.expireDiff} />}
|
||||
description={t('pages.settings.expireTimeDiffDesc')}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.expireDiff}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ expireDiff: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.trafficDiff')}
|
||||
badge={
|
||||
<DefaultSettingTag settingKey="trafficDiff" value={allSetting.trafficDiff} />
|
||||
}
|
||||
description={t('pages.settings.trafficDiffDesc')}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.trafficDiff}
|
||||
min={0}
|
||||
max={100}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ trafficDiff: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
|
||||
@@ -392,35 +303,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
label: catTabLabel(<GlobalOutlined />, t('pages.settings.externalTraffic'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.externalTrafficInformEnable')}
|
||||
description={t('pages.settings.externalTrafficInformEnableDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.externalTrafficInformEnable}
|
||||
onChange={(v) => updateSetting({ externalTrafficInformEnable: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.externalTrafficInformURI')}
|
||||
description={t('pages.settings.externalTrafficInformURIDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.externalTrafficInformURI}
|
||||
placeholder="(http|https)://domain[:port]/path/"
|
||||
onChange={(e) => updateSetting({ externalTrafficInformURI: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
label: catTabLabel(<ClockCircleOutlined />, t('pages.settings.dateAndTime'), isMobile),
|
||||
@@ -454,225 +336,6 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '6',
|
||||
label: catTabLabel(<ApartmentOutlined />, 'LDAP', isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
|
||||
<Switch
|
||||
checked={allSetting.ldapEnable}
|
||||
onChange={(v) => updateSetting({ ldapEnable: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.host')}>
|
||||
<Input
|
||||
value={allSetting.ldapHost}
|
||||
onChange={(e) => updateSetting({ ldapHost: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.port')}
|
||||
badge={<DefaultSettingTag settingKey="ldapPort" value={allSetting.ldapPort} />}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.ldapPort}
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ ldapPort: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.useTls')}>
|
||||
<Switch
|
||||
checked={allSetting.ldapUseTLS}
|
||||
onChange={(v) => updateSetting({ ldapUseTLS: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.skipTlsVerify')}
|
||||
description={t('pages.settings.ldap.skipTlsVerifyDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.ldapInsecureSkipVerify}
|
||||
disabled={!allSetting.ldapUseTLS}
|
||||
onChange={(v) => updateSetting({ ldapInsecureSkipVerify: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.bindDn')}>
|
||||
<Input
|
||||
value={allSetting.ldapBindDN}
|
||||
onChange={(e) => updateSetting({ ldapBindDN: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('password')}
|
||||
description={
|
||||
allSetting.hasLdapPassword && !allSetting.clearLdapPassword
|
||||
? t('pages.settings.ldap.passwordConfigured')
|
||||
: t('pages.settings.ldap.passwordUnconfigured')
|
||||
}
|
||||
>
|
||||
<SecretInput
|
||||
value={allSetting.ldapPassword}
|
||||
configured={allSetting.hasLdapPassword}
|
||||
clearArmed={allSetting.clearLdapPassword}
|
||||
placeholder={t('pages.settings.ldap.passwordPlaceholder')}
|
||||
onChange={(v) => updateSetting({ ldapPassword: v })}
|
||||
onClearArmedChange={(armed) => updateSetting({ clearLdapPassword: armed })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.baseDn')}>
|
||||
<Input
|
||||
value={allSetting.ldapBaseDN}
|
||||
onChange={(e) => updateSetting({ ldapBaseDN: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.userFilter')}>
|
||||
<Input
|
||||
value={allSetting.ldapUserFilter}
|
||||
onChange={(e) => updateSetting({ ldapUserFilter: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.userAttr')}>
|
||||
<Input
|
||||
value={allSetting.ldapUserAttr}
|
||||
onChange={(e) => updateSetting({ ldapUserAttr: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.vlessField')}>
|
||||
<Input
|
||||
value={allSetting.ldapVlessField}
|
||||
onChange={(e) => updateSetting({ ldapVlessField: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.flagField')}
|
||||
description={t('pages.settings.ldap.flagFieldDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.ldapFlagField}
|
||||
onChange={(e) => updateSetting({ ldapFlagField: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.truthyValues')}
|
||||
description={t('pages.settings.ldap.truthyValuesDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.ldapTruthyValues}
|
||||
onChange={(e) => updateSetting({ ldapTruthyValues: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.invertFlag')}
|
||||
description={t('pages.settings.ldap.invertFlagDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.ldapInvertFlag}
|
||||
onChange={(v) => updateSetting({ ldapInvertFlag: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.syncSchedule')}
|
||||
description={t('pages.settings.ldap.syncScheduleDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.ldapSyncCron}
|
||||
onChange={(e) => updateSetting({ ldapSyncCron: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.inboundTags')}
|
||||
description={t('pages.settings.ldap.inboundTagsDesc')}
|
||||
>
|
||||
<>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={ldapInboundTagList}
|
||||
onChange={setLdapInboundTagList}
|
||||
style={{ width: '100%' }}
|
||||
options={inboundOptions}
|
||||
/>
|
||||
{inboundOptions.length === 0 && (
|
||||
<div className="ldap-no-inbounds">{t('pages.settings.ldap.noInbounds')}</div>
|
||||
)}
|
||||
</>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoCreate')}>
|
||||
<Switch
|
||||
checked={allSetting.ldapAutoCreate}
|
||||
onChange={(v) => updateSetting({ ldapAutoCreate: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoDelete')}>
|
||||
<Switch
|
||||
checked={allSetting.ldapAutoDelete}
|
||||
onChange={(v) => updateSetting({ ldapAutoDelete: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.defaultTotalGb')}
|
||||
badge={
|
||||
<DefaultSettingTag
|
||||
settingKey="ldapDefaultTotalGB"
|
||||
value={allSetting.ldapDefaultTotalGB}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.ldapDefaultTotalGB}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ ldapDefaultTotalGB: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.defaultExpiryDays')}
|
||||
badge={
|
||||
<DefaultSettingTag
|
||||
settingKey="ldapDefaultExpiryDays"
|
||||
value={allSetting.ldapDefaultExpiryDays}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.ldapDefaultExpiryDays}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ ldapDefaultExpiryDays: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.ldap.defaultIpLimit')}
|
||||
badge={
|
||||
<DefaultSettingTag
|
||||
settingKey="ldapDefaultLimitIP"
|
||||
value={allSetting.ldapDefaultLimitIP}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
value={allSetting.ldapDefaultLimitIP}
|
||||
min={0}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => updateSetting({ ldapDefaultLimitIP: v }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Button, Input, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SecretInputProps {
|
||||
value: string;
|
||||
configured: boolean;
|
||||
clearArmed: boolean;
|
||||
placeholder: string;
|
||||
onChange: (value: string) => void;
|
||||
onClearArmedChange: (armed: boolean) => void;
|
||||
}
|
||||
|
||||
export default function SecretInput({
|
||||
value,
|
||||
configured,
|
||||
clearArmed,
|
||||
placeholder,
|
||||
onChange,
|
||||
onClearArmedChange,
|
||||
}: SecretInputProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input.Password
|
||||
value={value}
|
||||
placeholder={configured && !clearArmed ? placeholder : ''}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
if (clearArmed) onClearArmedChange(false);
|
||||
}}
|
||||
/>
|
||||
{configured && (
|
||||
<Button
|
||||
danger={clearArmed}
|
||||
onClick={() => {
|
||||
onChange('');
|
||||
onClearArmedChange(!clearArmed);
|
||||
}}
|
||||
>
|
||||
{clearArmed ? t('pages.settings.secretClearUndo') : t('pages.settings.secretClear')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Empty, Form, Input, Modal, Space, Spin, Switch, Tabs, message } from 'antd';
|
||||
import { ApiOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { ClipboardManager, HttpUtil, IntlUtil, RandomUtil } from '@/utils';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { ApiOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { ClipboardManager, HttpUtil, IntlUtil } from '@/utils';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import TwoFactorModal from './TwoFactorModal';
|
||||
import './SecurityTab.css';
|
||||
|
||||
interface ApiMsg<T = unknown> {
|
||||
@@ -25,45 +23,18 @@ interface ApiTokenRow {
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface SecurityTabProps {
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
saveSetting: (payload: Partial<AllSetting> & Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
|
||||
const UNIX_MILLISECONDS_THRESHOLD = 100_000_000_000;
|
||||
|
||||
function apiTokenCreatedAtMilliseconds(createdAt: number): number {
|
||||
return createdAt < UNIX_MILLISECONDS_THRESHOLD ? createdAt * 1000 : createdAt;
|
||||
}
|
||||
|
||||
type TfaType = 'set' | 'confirm';
|
||||
|
||||
interface TfaState {
|
||||
open: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
token: string;
|
||||
type: TfaType;
|
||||
onConfirm: (success: boolean, code?: string) => void;
|
||||
}
|
||||
|
||||
const TFA_INITIAL: TfaState = {
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
token: '',
|
||||
type: 'set',
|
||||
onConfirm: () => {},
|
||||
};
|
||||
|
||||
export default function SecurityTab({ allSetting, updateSetting, saveSetting }: SecurityTabProps) {
|
||||
export default function SecurityTab() {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
|
||||
const [tfa, setTfa] = useState<TfaState>(TFA_INITIAL);
|
||||
const [user, setUser] = useState({
|
||||
oldUsername: '',
|
||||
oldPassword: '',
|
||||
@@ -79,17 +50,6 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [createdToken, setCreatedToken] = useState<{ name: string; token: string } | null>(null);
|
||||
|
||||
const openTfa = useCallback((opts: Omit<TfaState, 'open'>) => {
|
||||
setTfa({ ...opts, open: true });
|
||||
}, []);
|
||||
|
||||
const onTfaConfirm = useCallback(
|
||||
(success: boolean, code?: string) => {
|
||||
tfa.onConfirm(success, code);
|
||||
},
|
||||
[tfa],
|
||||
);
|
||||
|
||||
function updateUserField<K extends keyof typeof user>(key: K, value: string) {
|
||||
setUser((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
@@ -115,19 +75,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
||||
);
|
||||
|
||||
function onUpdateUserClick() {
|
||||
if (allSetting.twoFactorEnable) {
|
||||
openTfa({
|
||||
title: t('pages.settings.security.twoFactorModalChangeCredentialsTitle'),
|
||||
description: t('pages.settings.security.twoFactorModalChangeCredentialsStep'),
|
||||
token: '',
|
||||
type: 'confirm',
|
||||
onConfirm: (ok: boolean, code?: string) => {
|
||||
if (ok) sendUpdateUser(code || '');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
sendUpdateUser();
|
||||
}
|
||||
sendUpdateUser();
|
||||
}
|
||||
|
||||
const fetchApiTokens = useCallback(async () => {
|
||||
@@ -217,47 +165,6 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
||||
return IntlUtil.formatDate(apiTokenCreatedAtMilliseconds(ts));
|
||||
}
|
||||
|
||||
function toggleTwoFactor() {
|
||||
if (!allSetting.twoFactorEnable) {
|
||||
const newToken = RandomUtil.randomBase32String();
|
||||
openTfa({
|
||||
title: t('pages.settings.security.twoFactorModalSetTitle'),
|
||||
description: '',
|
||||
token: newToken,
|
||||
type: 'set',
|
||||
onConfirm: (ok: boolean) => {
|
||||
if (ok) {
|
||||
messageApi.success(t('pages.settings.security.twoFactorModalSetSuccess'));
|
||||
updateSetting({ twoFactorToken: newToken, twoFactorEnable: true });
|
||||
} else {
|
||||
updateSetting({ twoFactorEnable: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openTfa({
|
||||
title: t('pages.settings.security.twoFactorModalDeleteTitle'),
|
||||
description: t('pages.settings.security.twoFactorModalRemoveStep'),
|
||||
token: '',
|
||||
type: 'confirm',
|
||||
onConfirm: async (ok: boolean, code?: string) => {
|
||||
if (!ok) return;
|
||||
const next = {
|
||||
...allSetting,
|
||||
twoFactorEnable: false,
|
||||
twoFactorToken: '',
|
||||
twoFactorCode: code || '',
|
||||
};
|
||||
const msg = (await saveSetting(next)) as ApiMsg;
|
||||
if (msg?.success) {
|
||||
messageApi.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
|
||||
updateSetting({ twoFactorEnable: false, twoFactorToken: '', hasTwoFactorToken: false });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{messageContextHolder}
|
||||
@@ -307,23 +214,6 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: catTabLabel(
|
||||
<SafetyOutlined />,
|
||||
t('pages.settings.security.twoFactor'),
|
||||
isMobile,
|
||||
),
|
||||
children: (
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.security.twoFactorEnable')}
|
||||
description={t('pages.settings.security.twoFactorEnableDesc')}
|
||||
>
|
||||
<Switch checked={allSetting.twoFactorEnable} onClick={toggleTwoFactor} />
|
||||
</SettingListItem>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: catTabLabel(<ApiOutlined />, t('pages.nodes.apiToken'), isMobile),
|
||||
@@ -422,16 +312,6 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<TwoFactorModal
|
||||
open={tfa.open}
|
||||
title={tfa.title}
|
||||
description={tfa.description}
|
||||
token={tfa.token}
|
||||
type={tfa.type}
|
||||
onConfirm={onTfaConfirm}
|
||||
onOpenChange={(open) => setTfa((prev) => ({ ...prev, open }))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import { AllSettingSchema } from '@/schemas/setting';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
import GeneralTab from './GeneralTab';
|
||||
import SecurityTab from './SecurityTab';
|
||||
import TelegramTab from './TelegramTab';
|
||||
import EmailTab from './EmailTab';
|
||||
import SubscriptionGeneralTab from './SubscriptionGeneralTab';
|
||||
import SubscriptionFormatsTab from './SubscriptionFormatsTab';
|
||||
import SubscriptionBalancersTab from './SubscriptionBalancersTab';
|
||||
@@ -39,8 +37,6 @@ interface ApiMsg {
|
||||
const tabSlugs = [
|
||||
'general',
|
||||
'security',
|
||||
'telegram',
|
||||
'email',
|
||||
'subscription',
|
||||
'subscription-formats',
|
||||
'subscription-balancers',
|
||||
@@ -77,16 +73,8 @@ export default function SettingsPage() {
|
||||
setMessageInstance(messageApi);
|
||||
}, [messageApi]);
|
||||
|
||||
const {
|
||||
allSetting,
|
||||
updateSetting,
|
||||
fetched,
|
||||
spinning,
|
||||
setSpinning,
|
||||
saveDisabled,
|
||||
saveAll,
|
||||
savePayload,
|
||||
} = useAllSettings();
|
||||
const { allSetting, updateSetting, fetched, spinning, setSpinning, saveDisabled, saveAll } =
|
||||
useAllSettings();
|
||||
|
||||
const [entryHost] = useState(() => window.location.hostname);
|
||||
const [entryPort] = useState(() => window.location.port);
|
||||
@@ -206,17 +194,7 @@ export default function SettingsPage() {
|
||||
const categoryBody = useMemo(() => {
|
||||
switch (activeSlug) {
|
||||
case 'security':
|
||||
return (
|
||||
<SecurityTab
|
||||
allSetting={allSetting}
|
||||
updateSetting={updateSetting}
|
||||
saveSetting={savePayload}
|
||||
/>
|
||||
);
|
||||
case 'telegram':
|
||||
return <TelegramTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
case 'email':
|
||||
return <EmailTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
return <SecurityTab />;
|
||||
case 'subscription':
|
||||
return <SubscriptionGeneralTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
case 'subscription-formats':
|
||||
@@ -226,7 +204,7 @@ export default function SettingsPage() {
|
||||
default:
|
||||
return <GeneralTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
}
|
||||
}, [activeSlug, allSetting, updateSetting, savePayload]);
|
||||
}, [activeSlug, allSetting, updateSetting]);
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={antdThemeConfig}>
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Input, InputNumber, Select, Space, Switch, Tabs } from 'antd';
|
||||
import { BellOutlined, SendOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { LanguageManager } from '@/utils';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { TelegramNotifications } from '@/components/ui/notifications/TelegramNotifications';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from './catTabLabel';
|
||||
import SecretInput from './SecretInput';
|
||||
|
||||
interface TelegramTabProps {
|
||||
allSetting: AllSetting;
|
||||
updateSetting: (patch: Partial<AllSetting>) => void;
|
||||
}
|
||||
|
||||
// The notification schedule is fed straight to robfig/cron's AddJob (see
|
||||
// web.go startTask), which accepts @every <duration>, the @hourly/@daily/...
|
||||
// macros, and full crontab expressions. This builder covers the common cases
|
||||
// with dropdowns so users don't have to memorise the syntax, while "Custom"
|
||||
// preserves the raw crontab escape hatch.
|
||||
type Unit = 's' | 'm' | 'h';
|
||||
type Macro = '@hourly' | '@daily' | '@weekly' | '@monthly';
|
||||
type Mode = 'every' | Macro | 'custom';
|
||||
const MACROS: Macro[] = ['@hourly', '@daily', '@weekly', '@monthly'];
|
||||
const EVERY_RE = /^@every\s+(\d+)\s*([smh])$/i;
|
||||
|
||||
interface RunTime {
|
||||
mode: Mode;
|
||||
num: number;
|
||||
unit: Unit;
|
||||
custom: string;
|
||||
}
|
||||
|
||||
function parseRunTime(raw: string): RunTime {
|
||||
const v = (raw ?? '').trim();
|
||||
const m = v.match(EVERY_RE);
|
||||
if (m) {
|
||||
return {
|
||||
mode: 'every',
|
||||
num: Math.max(1, Number(m[1]) || 1),
|
||||
unit: m[2].toLowerCase() as Unit,
|
||||
custom: '',
|
||||
};
|
||||
}
|
||||
if ((MACROS as string[]).includes(v)) {
|
||||
return { mode: v as Macro, num: 1, unit: 'h', custom: '' };
|
||||
}
|
||||
return { mode: 'custom', num: 1, unit: 'h', custom: v };
|
||||
}
|
||||
|
||||
function composeRunTime(s: RunTime): string {
|
||||
if (s.mode === 'every') return `@every ${Math.max(1, s.num || 1)}${s.unit}`;
|
||||
if (s.mode === 'custom') return s.custom;
|
||||
return s.mode;
|
||||
}
|
||||
|
||||
// The panel's cron runs with seconds enabled (cron.WithSeconds() in web.go), so
|
||||
// crontab expressions are 6-field: "second minute hour day month weekday". When
|
||||
// the user drops into Custom we seed the box with the crontab equivalent of the
|
||||
// current selection rather than a bare @macro, so they get a real expression to
|
||||
// edit (and one that the 6-field parser accepts).
|
||||
function toCrontab(s: RunTime): string {
|
||||
switch (s.mode) {
|
||||
case '@hourly':
|
||||
return '0 0 * * * *';
|
||||
case '@daily':
|
||||
return '0 0 0 * * *';
|
||||
case '@weekly':
|
||||
return '0 0 0 * * 0';
|
||||
case '@monthly':
|
||||
return '0 0 0 1 * *';
|
||||
case 'every': {
|
||||
const n = Math.max(1, s.num || 1);
|
||||
if (s.unit === 's') return `*/${n} * * * * *`;
|
||||
if (s.unit === 'm') return `0 */${n} * * * *`;
|
||||
return `0 0 */${n} * * *`;
|
||||
}
|
||||
default:
|
||||
return s.custom;
|
||||
}
|
||||
}
|
||||
|
||||
function NotifyTimeField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
const { t } = useTranslation();
|
||||
// Init once: the Settings tabs only mount after settings are fetched, so the
|
||||
// incoming value is already the persisted one.
|
||||
const [state, setState] = useState<RunTime>(() => parseRunTime(value));
|
||||
|
||||
function update(patch: Partial<RunTime>) {
|
||||
const next = { ...state, ...patch };
|
||||
setState(next);
|
||||
onChange(composeRunTime(next));
|
||||
}
|
||||
|
||||
function onModeChange(mode: Mode) {
|
||||
// Seed Custom with the crontab equivalent of the current selection so the
|
||||
// box starts from a real expression (e.g. "0 0 0 * * *", not "@daily").
|
||||
if (mode === 'custom' && !state.custom.trim()) {
|
||||
update({ mode, custom: toCrontab(state) });
|
||||
} else {
|
||||
update({ mode });
|
||||
}
|
||||
}
|
||||
|
||||
const modeOptions = [
|
||||
{ value: 'every', label: t('pages.settings.notifyTime.every') },
|
||||
{ value: '@hourly', label: t('pages.settings.notifyTime.hourly') },
|
||||
{ value: '@daily', label: t('pages.settings.notifyTime.daily') },
|
||||
{ value: '@weekly', label: t('pages.settings.notifyTime.weekly') },
|
||||
{ value: '@monthly', label: t('pages.settings.notifyTime.monthly') },
|
||||
{ value: 'custom', label: t('pages.settings.notifyTime.custom') },
|
||||
];
|
||||
const unitOptions = [
|
||||
{ value: 's', label: t('pages.settings.notifyTime.seconds') },
|
||||
{ value: 'm', label: t('pages.settings.notifyTime.minutes') },
|
||||
{ value: 'h', label: t('pages.settings.notifyTime.hours') },
|
||||
];
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="small" style={{ width: '100%' }}>
|
||||
<Select<Mode>
|
||||
style={{ width: '100%' }}
|
||||
value={state.mode}
|
||||
options={modeOptions}
|
||||
onChange={onModeChange}
|
||||
aria-label={t('pages.settings.telegramNotifyTime')}
|
||||
/>
|
||||
{state.mode === 'every' && (
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
precision={0}
|
||||
style={{ width: '50%' }}
|
||||
value={state.num}
|
||||
onChange={onNumber((v) => update({ num: Math.max(1, v) }))}
|
||||
aria-label={t('pages.settings.notifyTime.interval')}
|
||||
/>
|
||||
<Select<Unit>
|
||||
style={{ width: '50%' }}
|
||||
value={state.unit}
|
||||
options={unitOptions}
|
||||
onChange={(unit) => update({ unit })}
|
||||
aria-label={t('pages.settings.notifyTime.unit')}
|
||||
/>
|
||||
</Space.Compact>
|
||||
)}
|
||||
{state.mode === 'custom' && (
|
||||
<Input
|
||||
value={state.custom}
|
||||
placeholder="0 30 8 * * *"
|
||||
onChange={(e) => update({ custom: e.target.value })}
|
||||
aria-label={t('pages.settings.notifyTime.custom')}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TelegramTab({ allSetting, updateSetting }: TelegramTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [testLoading, setTestLoading] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; msg: string } | null>(null);
|
||||
|
||||
async function handleTestTgBot() {
|
||||
setTestLoading(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
const res = (await HttpUtil.post('/panel/api/setting/testTgBot')) as {
|
||||
success?: boolean;
|
||||
msg?: string;
|
||||
};
|
||||
setTestResult({ success: !!res.success, msg: res.msg || '' });
|
||||
} catch (e: unknown) {
|
||||
setTestResult({
|
||||
success: false,
|
||||
msg: e instanceof Error ? e.message : t('pages.settings.requestFailed'),
|
||||
});
|
||||
} finally {
|
||||
setTestLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const langOptions = useMemo(
|
||||
() =>
|
||||
LanguageManager.supportedLanguages.map(
|
||||
(l: { value: string; name: string; icon: string }) => ({
|
||||
value: l.value,
|
||||
label: (
|
||||
<>
|
||||
<span role="img" aria-label={l.name}>
|
||||
{l.icon}
|
||||
</span>
|
||||
<span>{l.name}</span>
|
||||
</>
|
||||
),
|
||||
}),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.telegramBotEnable')}
|
||||
description={t('pages.settings.telegramBotEnableDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.tgBotEnable}
|
||||
onChange={(v) => updateSetting({ tgBotEnable: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.telegramToken')}
|
||||
description={
|
||||
allSetting.hasTgBotToken && !allSetting.clearTgBotToken
|
||||
? t('pages.settings.telegramTokenConfigured')
|
||||
: t('pages.settings.telegramTokenDesc')
|
||||
}
|
||||
>
|
||||
<SecretInput
|
||||
value={allSetting.tgBotToken}
|
||||
configured={allSetting.hasTgBotToken}
|
||||
clearArmed={allSetting.clearTgBotToken}
|
||||
placeholder={t('pages.settings.telegramTokenPlaceholder')}
|
||||
onChange={(v) => updateSetting({ tgBotToken: v })}
|
||||
onClearArmedChange={(armed) => updateSetting({ clearTgBotToken: armed })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.telegramChatId')}
|
||||
description={t('pages.settings.telegramChatIdDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.tgBotChatId}
|
||||
onChange={(e) => updateSetting({ tgBotChatId: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem paddings="small" title={t('pages.settings.telegramBotLanguage')}>
|
||||
<Select
|
||||
value={allSetting.tgLang}
|
||||
onChange={(v) => updateSetting({ tgLang: v })}
|
||||
style={{ width: '100%' }}
|
||||
options={langOptions}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.telegramAPIServer')}
|
||||
description={t('pages.settings.telegramAPIServerDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.tgBotAPIServer}
|
||||
placeholder="https://api.example.com"
|
||||
onChange={(e) => updateSetting({ tgBotAPIServer: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<Space orientation="vertical" size={8} style={{ width: '100%', marginTop: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
loading={testLoading}
|
||||
onClick={handleTestTgBot}
|
||||
>
|
||||
{t('pages.settings.testTgBot')}
|
||||
</Button>
|
||||
{testResult && (
|
||||
<Alert
|
||||
type={testResult.success ? 'success' : 'error'}
|
||||
title={testResult.msg}
|
||||
showIcon
|
||||
closable={{ onClose: () => setTestResult(null) }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.telegramNotifyTime')}
|
||||
description={t('pages.settings.telegramNotifyTimeDesc')}
|
||||
>
|
||||
<NotifyTimeField
|
||||
value={allSetting.tgRunTime}
|
||||
onChange={(v) => updateSetting({ tgRunTime: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.tgNotifyBackup')}
|
||||
description={t('pages.settings.tgNotifyBackupDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={allSetting.tgBotBackup}
|
||||
onChange={(v) => updateSetting({ tgBotBackup: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.settings.tgEventBusNotify')}
|
||||
description={t('pages.settings.tgEventBusNotifyDesc')}
|
||||
>
|
||||
<TelegramNotifications allSetting={allSetting} updateSetting={updateSetting} />
|
||||
</SettingListItem>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
.qr-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qr-token {
|
||||
font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Input, Modal, QRCode, message } from 'antd';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
|
||||
import { ClipboardManager } from '@/utils';
|
||||
import { activateOnKey } from '@/utils/a11y';
|
||||
import { TotpCodeSchema } from '@/schemas/login';
|
||||
import './TwoFactorModal.css';
|
||||
|
||||
type Type = 'set' | 'confirm';
|
||||
|
||||
interface TwoFactorModalProps {
|
||||
open: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
token?: string;
|
||||
type?: Type;
|
||||
onConfirm: (success: boolean, code?: string) => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function TwoFactorModal({
|
||||
open,
|
||||
title = '',
|
||||
description = '',
|
||||
token = '',
|
||||
type = 'set',
|
||||
onConfirm,
|
||||
onOpenChange,
|
||||
}: TwoFactorModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [enteredCode, setEnteredCode] = useState('');
|
||||
|
||||
const totp = useMemo(() => {
|
||||
if (!open || !token) return null;
|
||||
return new OTPAuth.TOTP({
|
||||
issuer: '3x-ui',
|
||||
label: 'Administrator',
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: token,
|
||||
});
|
||||
}, [open, token]);
|
||||
const qrValue = totp ? totp.toString() : '';
|
||||
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setEnteredCode('');
|
||||
}
|
||||
|
||||
function close(success: boolean, code = '') {
|
||||
onConfirm(success, code);
|
||||
onOpenChange(false);
|
||||
setEnteredCode('');
|
||||
}
|
||||
|
||||
function onOk() {
|
||||
const codeOk = TotpCodeSchema.safeParse(enteredCode);
|
||||
if (!codeOk.success) {
|
||||
messageApi.error(
|
||||
t(codeOk.error.issues[0]?.message ?? 'pages.settings.security.twoFactorModalError'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (type === 'confirm' && !token) {
|
||||
close(true, codeOk.data);
|
||||
return;
|
||||
}
|
||||
if (!totp) return;
|
||||
if (totp.generate() === codeOk.data) {
|
||||
close(true);
|
||||
} else {
|
||||
messageApi.error(t('pages.settings.security.twoFactorModalError'));
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
close(false);
|
||||
}
|
||||
|
||||
async function copyToken() {
|
||||
const ok = await ClipboardManager.copyText(token);
|
||||
if (ok) messageApi.success(t('copied'));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{messageContextHolder}
|
||||
<Modal
|
||||
open={open}
|
||||
title={title}
|
||||
closable
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={onCancel}>
|
||||
{t('cancel')}
|
||||
</Button>,
|
||||
<Button
|
||||
key="ok"
|
||||
type="primary"
|
||||
disabled={!TotpCodeSchema.safeParse(enteredCode).success}
|
||||
onClick={onOk}
|
||||
>
|
||||
{t('confirm')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{type === 'set' ? (
|
||||
<>
|
||||
<p>{t('pages.settings.security.twoFactorModalSteps')}</p>
|
||||
<Divider />
|
||||
<p>{t('pages.settings.security.twoFactorModalFirstStep')}</p>
|
||||
<div
|
||||
className="qr-wrap"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t('copy')}
|
||||
onClick={copyToken}
|
||||
onKeyDown={activateOnKey(copyToken)}
|
||||
>
|
||||
<QRCode
|
||||
className="qr-code"
|
||||
value={qrValue}
|
||||
size={180}
|
||||
type="svg"
|
||||
bordered={false}
|
||||
color="#000000"
|
||||
bgColor="#ffffff"
|
||||
errorLevel="L"
|
||||
title={t('copy')}
|
||||
/>
|
||||
<span className="qr-token">{token}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<p>{t('pages.settings.security.twoFactorModalSecondStep')}</p>
|
||||
<Input
|
||||
value={enteredCode}
|
||||
onChange={(e) => setEnteredCode(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
aria-label={t('twoFactorCode')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>{description}</p>
|
||||
<Input
|
||||
value={enteredCode}
|
||||
onChange={(e) => setEnteredCode(e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
aria-label={t('twoFactorCode')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import { useLocation } from 'react-router';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
FloatButton,
|
||||
Layout,
|
||||
message,
|
||||
Radio,
|
||||
Result,
|
||||
Row,
|
||||
Space,
|
||||
@@ -22,26 +21,21 @@ import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { useXraySetting } from '@/hooks/useXraySetting';
|
||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import AppSidebar from '@/layouts/AppSidebar';
|
||||
import { JsonEditor } from '@/components/form';
|
||||
import { setMessageInstance } from '@/utils/messageBus';
|
||||
|
||||
import { BasicsTab } from './basics';
|
||||
import { propagateOutboundTagRename } from './basics/helpers';
|
||||
import { RoutingTab } from './routing';
|
||||
import { OutboundsTab } from './outbounds';
|
||||
import { BalancersTab } from './balancers';
|
||||
import {
|
||||
cleanupOrphanedBalancerLoopbacks,
|
||||
ensureMissingBalancerLoopbacks,
|
||||
detectBalancerCycles,
|
||||
} from './balancers/balancer-loopback';
|
||||
import { DnsTab } from './dns';
|
||||
import { WarpModal, NordModal, PiaModal } from './overrides';
|
||||
import './XrayPage.css';
|
||||
|
||||
const SECTION_SLUGS = ['basic', 'routing', 'outbound', 'balancer', 'dns', 'advanced'];
|
||||
|
||||
type AdvKey = 'xraySetting' | 'inboundSettings' | 'outboundSettings' | 'routingRuleSettings';
|
||||
const SECTION_SLUGS = ['basic', 'routing', 'outbound'];
|
||||
|
||||
export default function XrayPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -83,9 +77,7 @@ export default function XrayPage() {
|
||||
const [warpOpen, setWarpOpen] = useState(false);
|
||||
const [nordOpen, setNordOpen] = useState(false);
|
||||
const [piaOpen, setPiaOpen] = useState(false);
|
||||
const [advSettings, setAdvSettings] = useState<AdvKey>('xraySetting');
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const pathSection =
|
||||
location.pathname === '/outbound'
|
||||
? 'outbound'
|
||||
@@ -157,59 +149,11 @@ export default function XrayPage() {
|
||||
});
|
||||
}
|
||||
|
||||
const advancedText = useMemo(() => {
|
||||
if (advSettings === 'xraySetting') return xraySetting;
|
||||
const tpl = templateSettings;
|
||||
if (!tpl) return '';
|
||||
try {
|
||||
switch (advSettings) {
|
||||
case 'inboundSettings':
|
||||
return JSON.stringify(tpl.inbounds || [], null, 2);
|
||||
case 'outboundSettings':
|
||||
return JSON.stringify(tpl.outbounds || [], null, 2);
|
||||
case 'routingRuleSettings':
|
||||
return JSON.stringify(tpl.routing?.rules || [], null, 2);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}, [advSettings, xraySetting, templateSettings]);
|
||||
|
||||
function onAdvancedTextChange(next: string) {
|
||||
if (advSettings === 'xraySetting') {
|
||||
setXraySetting(next);
|
||||
return;
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(next);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
mutate((tt) => {
|
||||
switch (advSettings) {
|
||||
case 'inboundSettings':
|
||||
tt.inbounds = parsed;
|
||||
break;
|
||||
case 'outboundSettings':
|
||||
tt.outbounds = parsed;
|
||||
break;
|
||||
case 'routingRuleSettings':
|
||||
if (!tt.routing) tt.routing = {};
|
||||
tt.routing.rules = parsed;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onSaveAll() {
|
||||
try {
|
||||
JSON.parse(xraySetting);
|
||||
} catch (e) {
|
||||
messageApi.error(`Advanced JSON: ${(e as Error).message}`);
|
||||
navigate('/xray#advanced');
|
||||
messageApi.error(`Template JSON: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
if (templateSettings) {
|
||||
@@ -269,47 +213,6 @@ export default function XrayPage() {
|
||||
onRefreshXrayData={fetchAll}
|
||||
/>
|
||||
);
|
||||
case 'balancer':
|
||||
return (
|
||||
<BalancersTab
|
||||
templateSettings={templateSettings}
|
||||
setTemplateSettings={setTemplateSettings}
|
||||
clientReverseTags={clientReverseTags}
|
||||
subscriptionOutboundTags={subscriptionOutboundTags}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
);
|
||||
case 'dns':
|
||||
return (
|
||||
<DnsTab templateSettings={templateSettings} setTemplateSettings={setTemplateSettings} />
|
||||
);
|
||||
case 'advanced':
|
||||
return (
|
||||
<>
|
||||
<div className="advanced-meta">
|
||||
<h4>{t('pages.xray.Template')}</h4>
|
||||
<p>{t('pages.xray.TemplateDesc')}</p>
|
||||
</div>
|
||||
<Radio.Group
|
||||
value={advSettings}
|
||||
buttonStyle="solid"
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ margin: '12px 0' }}
|
||||
onChange={(e) => setAdvSettings(e.target.value)}
|
||||
>
|
||||
<Radio.Button value="xraySetting">{t('pages.xray.completeTemplate')}</Radio.Button>
|
||||
<Radio.Button value="inboundSettings">{t('pages.xray.Inbounds')}</Radio.Button>
|
||||
<Radio.Button value="outboundSettings">{t('pages.xray.Outbounds')}</Radio.Button>
|
||||
<Radio.Button value="routingRuleSettings">{t('pages.xray.Routings')}</Radio.Button>
|
||||
</Radio.Group>
|
||||
<JsonEditor
|
||||
value={advancedText}
|
||||
onChange={onAdvancedTextChange}
|
||||
minHeight="420px"
|
||||
maxHeight="720px"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<BasicsTab
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag } from 'antd';
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
import type { Path } from 'react-hook-form';
|
||||
|
||||
import { InputAddon } from '@/components/ui';
|
||||
import { FormField } from '@/components/form/rhf';
|
||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { BalancerFormSchema, type BalancerFormValues } from '@/schemas/xray';
|
||||
import {
|
||||
BalancerStrategyTypeSchema,
|
||||
type BalancerStrategyType,
|
||||
type BalancerObject,
|
||||
} from '@/schemas/routing';
|
||||
import { isBalancerLoopbackTag } from './balancer-loopback';
|
||||
|
||||
export type BalancerFormValue = BalancerFormValues;
|
||||
|
||||
interface BalancerFormModalProps {
|
||||
open: boolean;
|
||||
balancer: BalancerFormValue | null;
|
||||
outboundTags: string[];
|
||||
balancerTags: string[];
|
||||
balancers: BalancerObject[];
|
||||
templateSettings: XraySettingsValue | null;
|
||||
otherTags: string[];
|
||||
onClose: () => void;
|
||||
onConfirm: (value: BalancerFormValue) => void;
|
||||
}
|
||||
|
||||
const STRATEGY_LABELS: Record<string, string> = {
|
||||
random: 'Random',
|
||||
roundRobin: 'Round robin',
|
||||
leastLoad: 'Least load',
|
||||
leastPing: 'Least ping',
|
||||
};
|
||||
|
||||
const STRATEGIES = BalancerStrategyTypeSchema.options.map((value) => ({
|
||||
value,
|
||||
label: STRATEGY_LABELS[value] ?? value,
|
||||
}));
|
||||
|
||||
function initialState(balancer: BalancerFormValue | null): BalancerFormValues {
|
||||
if (!balancer) {
|
||||
return { tag: '', strategy: 'random', selector: [], fallbackTag: '' };
|
||||
}
|
||||
return {
|
||||
tag: balancer.tag ?? '',
|
||||
strategy: (balancer.strategy ?? 'random') as BalancerStrategyType,
|
||||
selector: [...(balancer.selector ?? [])],
|
||||
fallbackTag: balancer.fallbackTag ?? '',
|
||||
settings: balancer.settings,
|
||||
};
|
||||
}
|
||||
|
||||
export default function BalancerFormModal({
|
||||
open,
|
||||
balancer,
|
||||
outboundTags,
|
||||
balancerTags,
|
||||
balancers,
|
||||
templateSettings,
|
||||
otherTags,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: BalancerFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const methods = useForm<BalancerFormValues>({ defaultValues: initialState(balancer) });
|
||||
const [submitAttempted, setSubmitAttempted] = useState(false);
|
||||
const isEdit = balancer != null;
|
||||
|
||||
const openBalancer = open ? (balancer ?? null) : undefined;
|
||||
const [syncedBalancer, setSyncedBalancer] = useState<typeof openBalancer>(undefined);
|
||||
if (openBalancer !== syncedBalancer) {
|
||||
setSyncedBalancer(openBalancer);
|
||||
if (open) {
|
||||
methods.reset(initialState(balancer));
|
||||
setSubmitAttempted(false);
|
||||
}
|
||||
}
|
||||
|
||||
const strategy = useWatch({ control: methods.control, name: 'strategy' });
|
||||
const baselines = useWatch({ control: methods.control, name: 'settings.baselines' }) ?? [];
|
||||
const costs = useWatch({ control: methods.control, name: 'settings.costs' }) ?? [];
|
||||
const tagValue = useWatch({ control: methods.control, name: 'tag' }) ?? '';
|
||||
const fallbackTag = useWatch({ control: methods.control, name: 'fallbackTag' }) ?? '';
|
||||
const currentTag = tagValue.trim();
|
||||
|
||||
const availableBalancerTags = useMemo(
|
||||
() => balancerTags.filter((tg) => tg !== currentTag),
|
||||
[balancerTags, currentTag],
|
||||
);
|
||||
|
||||
const cycleInfo = useMemo(() => {
|
||||
const rules = (templateSettings?.routing?.rules || []) as Array<{
|
||||
inboundTag?: string[];
|
||||
balancerTag?: string;
|
||||
}>;
|
||||
const resolveLoopback = (tag: string): string | null => {
|
||||
for (const r of rules) {
|
||||
if (Array.isArray(r.inboundTag) && r.inboundTag.includes(tag) && r.balancerTag) {
|
||||
return r.balancerTag;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const fallbackOf: Record<string, string> = {};
|
||||
for (const b of balancers) {
|
||||
if (!b.tag || !b.fallbackTag || b.tag === currentTag) continue;
|
||||
const target = isBalancerLoopbackTag(b.fallbackTag)
|
||||
? resolveLoopback(b.fallbackTag)
|
||||
: b.fallbackTag;
|
||||
if (target) fallbackOf[b.tag] = target;
|
||||
}
|
||||
|
||||
const result: Record<string, string[]> = {};
|
||||
for (const tg of availableBalancerTags) {
|
||||
const visited = new Set<string>();
|
||||
let cursor = tg;
|
||||
const path = [tg];
|
||||
while (cursor && !visited.has(cursor)) {
|
||||
if (cursor === currentTag) {
|
||||
result[tg] = path;
|
||||
break;
|
||||
}
|
||||
visited.add(cursor);
|
||||
cursor = fallbackOf[cursor] || '';
|
||||
if (cursor) path.push(cursor);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [currentTag, balancers, availableBalancerTags, templateSettings?.routing?.rules]);
|
||||
|
||||
const wouldCreateCycle = !!cycleInfo[fallbackTag];
|
||||
|
||||
const fallbackOptions = useMemo(() => {
|
||||
const options: Array<{ value: string; label: ReactNode; disabled?: boolean; title?: string }> =
|
||||
[{ value: '', label: `(${t('none')})` }];
|
||||
for (const tg of outboundTags) {
|
||||
options.push({ value: tg, label: tg });
|
||||
}
|
||||
for (const tg of availableBalancerTags) {
|
||||
const cycle = cycleInfo[tg];
|
||||
options.push({
|
||||
value: tg,
|
||||
disabled: !!cycle,
|
||||
title: cycle
|
||||
? t('pages.xray.balancer.cycleTooltip', { path: cycle.join(' → '), start: currentTag })
|
||||
: undefined,
|
||||
label: (
|
||||
<span>
|
||||
<Tag color="blue" style={{ marginRight: 4 }}>
|
||||
{t('pages.xray.rules.balancer')}
|
||||
</Tag>
|
||||
{tg}
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}, [outboundTags, availableBalancerTags, cycleInfo, currentTag, t]);
|
||||
|
||||
const isFallbackBalancer = useMemo(
|
||||
() => balancerTags.includes(fallbackTag),
|
||||
[balancerTags, fallbackTag],
|
||||
);
|
||||
|
||||
function submit() {
|
||||
const values = methods.getValues();
|
||||
const parsed = BalancerFormSchema.safeParse(values);
|
||||
const trimmedTag = (values.tag ?? '').trim();
|
||||
const duplicateTag = !!trimmedTag && otherTags.includes(trimmedTag);
|
||||
methods.clearErrors();
|
||||
if (!parsed.success) {
|
||||
const seen = new Set<string>();
|
||||
for (const issue of parsed.error.issues) {
|
||||
const key = String(issue.path[0] ?? '');
|
||||
if (key && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
methods.setError(key as Path<BalancerFormValues>, { message: issue.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!parsed.success || duplicateTag || wouldCreateCycle) {
|
||||
setSubmitAttempted(true);
|
||||
return;
|
||||
}
|
||||
const result: BalancerFormValues = { ...parsed.data };
|
||||
if (result.strategy !== 'leastLoad') delete result.settings;
|
||||
onConfirm(result);
|
||||
}
|
||||
|
||||
const title = isEdit
|
||||
? `${t('edit')} ${t('pages.xray.Balancers')}`
|
||||
: `+ ${t('pages.xray.Balancers')}`;
|
||||
const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={title}
|
||||
okText={okText}
|
||||
cancelText={t('close')}
|
||||
okButtonProps={{ disabled: wouldCreateCycle }}
|
||||
mask={{ closable: false }}
|
||||
onOk={submit}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
|
||||
<Controller
|
||||
control={methods.control}
|
||||
name="tag"
|
||||
render={({ field, fieldState }) => {
|
||||
const trimmed = (field.value ?? '').trim();
|
||||
const duplicate = !!trimmed && otherTags.includes(trimmed);
|
||||
const errorMessage = fieldState.error?.message
|
||||
? t(fieldState.error.message, { defaultValue: fieldState.error.message })
|
||||
: '';
|
||||
const showDuplicate =
|
||||
!errorMessage && (submitAttempted || fieldState.isTouched) && duplicate;
|
||||
return (
|
||||
<Form.Item
|
||||
label={t('pages.xray.balancer.tag')}
|
||||
required
|
||||
validateStatus={errorMessage ? 'error' : showDuplicate ? 'warning' : ''}
|
||||
help={
|
||||
errorMessage || (showDuplicate ? t('pages.xray.balancer.tagDuplicate') : '')
|
||||
}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
onBlur={field.onBlur}
|
||||
ref={field.ref}
|
||||
placeholder={t('pages.xray.balancer.tagPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormField name="strategy" label={t('pages.xray.balancer.balancerStrategy')}>
|
||||
<Select options={STRATEGIES} />
|
||||
</FormField>
|
||||
<FormField name="selector" label={t('pages.xray.balancer.selector')} required>
|
||||
<Select
|
||||
mode="tags"
|
||||
tokenSeparators={[',']}
|
||||
options={outboundTags.map((tg) => ({ value: tg, label: tg }))}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
name="fallbackTag"
|
||||
label={t('pages.xray.balancer.fallback')}
|
||||
extra={t('pages.xray.balancer.fallbackBalancerHint')}
|
||||
transform={{ output: (v) => v ?? '' }}
|
||||
>
|
||||
<Select allowClear options={fallbackOptions} />
|
||||
</FormField>
|
||||
{isFallbackBalancer && !wouldCreateCycle && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
title={t('pages.xray.balancer.balancerFallbackInfo')}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
{wouldCreateCycle && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
title={t('pages.xray.balancer.balancerFallbackCycle')}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{strategy === 'leastLoad' && (
|
||||
<>
|
||||
<FormField
|
||||
name={['settings', 'expected']}
|
||||
label={t('pages.xray.balancer.expected')}
|
||||
transform={{ output: (v) => (typeof v === 'number' ? v : undefined) }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder={t('pages.xray.balancer.expectedPlaceholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'maxRTT']}
|
||||
label={t('pages.xray.balancer.maxRtt')}
|
||||
transform={{
|
||||
input: (v) => v ?? '',
|
||||
output: (v) => (typeof v === 'string' && v ? v : undefined),
|
||||
}}
|
||||
>
|
||||
<Input placeholder="e.g. 1s" />
|
||||
</FormField>
|
||||
<FormField
|
||||
name={['settings', 'tolerance']}
|
||||
label={t('pages.xray.balancer.tolerance')}
|
||||
transform={{ output: (v) => (typeof v === 'number' ? v : undefined) }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
placeholder="0.01 = 1%"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormField>
|
||||
<Form.Item label={t('pages.xray.balancer.baselines')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => methods.setValue('settings.baselines', [...baselines, ''])}
|
||||
/>
|
||||
{baselines.map((b, idx) => (
|
||||
<Space.Compact key={idx} block style={{ marginTop: 4 }}>
|
||||
<Input
|
||||
value={b}
|
||||
aria-label={t('pages.xray.balancer.baselines')}
|
||||
placeholder="e.g. 1s"
|
||||
onChange={(e) =>
|
||||
methods.setValue(
|
||||
'settings.baselines',
|
||||
baselines.map((x, i) => (i === idx ? e.target.value : x)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<InputAddon
|
||||
ariaLabel={t('remove')}
|
||||
onClick={() =>
|
||||
methods.setValue(
|
||||
'settings.baselines',
|
||||
baselines.filter((_, i) => i !== idx),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form.Item label={t('pages.xray.balancer.costs')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() =>
|
||||
methods.setValue('settings.costs', [
|
||||
...costs,
|
||||
{ regexp: false, match: '', value: 1 },
|
||||
])
|
||||
}
|
||||
/>
|
||||
{costs.map((c, idx) => (
|
||||
<Space.Compact key={idx} block style={{ marginTop: 4 }}>
|
||||
<Switch
|
||||
checked={c.regexp}
|
||||
aria-label={t('pages.xray.balancer.costRegexp')}
|
||||
checkedChildren="re"
|
||||
unCheckedChildren="lit"
|
||||
onChange={(v) =>
|
||||
methods.setValue(
|
||||
'settings.costs',
|
||||
costs.map((x, i) => (i === idx ? { ...x, regexp: v } : x)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={c.match}
|
||||
aria-label={t('pages.xray.balancer.costMatch')}
|
||||
placeholder="tag pattern"
|
||||
onChange={(e) =>
|
||||
methods.setValue(
|
||||
'settings.costs',
|
||||
costs.map((x, i) => (i === idx ? { ...x, match: e.target.value } : x)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<InputNumber
|
||||
value={c.value}
|
||||
aria-label={t('pages.xray.balancer.costValue')}
|
||||
placeholder="weight"
|
||||
style={{ width: 100 }}
|
||||
onChange={(v) =>
|
||||
methods.setValue(
|
||||
'settings.costs',
|
||||
costs.map((x, i) =>
|
||||
i === idx ? { ...x, value: typeof v === 'number' ? v : 0 } : x,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<InputAddon
|
||||
ariaLabel={t('remove')}
|
||||
onClick={() =>
|
||||
methods.setValue(
|
||||
'settings.costs',
|
||||
costs.filter((_, i) => i !== idx),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,549 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
MoreOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
SyncOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
RadarChartOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import BalancerFormModal from './BalancerFormModal';
|
||||
import type { BalancerFormValue } from './BalancerFormModal';
|
||||
import { syncObservatories } from './balancer-helpers';
|
||||
import {
|
||||
isBalancerLoopbackTag,
|
||||
loopbackTagFor,
|
||||
resolveLoopbackFallback,
|
||||
ensureBalancerLoopback,
|
||||
removeBalancerLoopback,
|
||||
removeBalancerLoopbackIfOrphaned,
|
||||
propagateBalancerTagRename,
|
||||
} from './balancer-loopback';
|
||||
import { planBalancerDeletion, applyBalancerDeletion } from '../reference-cleanup';
|
||||
import DeletionImpactList from '../DeletionImpactList';
|
||||
import ObservatorySettingsTab from './ObservatorySettingsTab';
|
||||
import { catTabLabel } from '@/pages/settings/catTabLabel';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
|
||||
import type {
|
||||
BalancerObject,
|
||||
BalancerStrategySettings,
|
||||
BalancerStrategyType,
|
||||
} from '@/schemas/routing';
|
||||
|
||||
// Live state of one balancer inside the running core, as reported by the
|
||||
// panel's /xray/balancerStatus endpoint (RoutingService.GetBalancerInfo).
|
||||
interface BalancerLiveStatus {
|
||||
tag: string;
|
||||
running: boolean;
|
||||
override: string;
|
||||
selected: string[];
|
||||
}
|
||||
|
||||
interface BalancersTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
clientReverseTags: string[];
|
||||
subscriptionOutboundTags?: string[];
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
type BalancerRecord = BalancerObject;
|
||||
|
||||
interface BalancerRow {
|
||||
key: number;
|
||||
tag: string;
|
||||
strategy: BalancerStrategyType;
|
||||
selector: string[];
|
||||
fallbackTag: string;
|
||||
displayFallbackTag: string;
|
||||
settings?: BalancerStrategySettings;
|
||||
}
|
||||
|
||||
const STRATEGY_LABELS: Record<string, string> = {
|
||||
random: 'Random',
|
||||
roundRobin: 'Round robin',
|
||||
leastLoad: 'Least load',
|
||||
leastPing: 'Least ping',
|
||||
};
|
||||
|
||||
export default function BalancersTab({
|
||||
templateSettings,
|
||||
setTemplateSettings,
|
||||
clientReverseTags,
|
||||
subscriptionOutboundTags,
|
||||
isMobile,
|
||||
}: BalancersTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editingBalancer, setEditingBalancer] = useState<BalancerFormValue | null>(null);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
|
||||
const balancerObjects = useMemo(
|
||||
() => (templateSettings?.routing?.balancers || []) as BalancerObject[],
|
||||
[templateSettings?.routing?.balancers],
|
||||
);
|
||||
|
||||
const rows: BalancerRow[] = useMemo(() => {
|
||||
const list = balancerObjects;
|
||||
return list.map((b, idx) => ({
|
||||
key: idx,
|
||||
tag: b.tag || '',
|
||||
strategy: (b.strategy?.type ?? 'random') as BalancerStrategyType,
|
||||
selector: b.selector || [],
|
||||
fallbackTag: b.fallbackTag || '',
|
||||
displayFallbackTag: resolveLoopbackFallback(templateSettings!, b.fallbackTag || ''),
|
||||
settings: b.strategy?.settings,
|
||||
}));
|
||||
}, [balancerObjects, templateSettings]);
|
||||
|
||||
const outboundTags = useMemo(() => {
|
||||
const tags = new Set<string>();
|
||||
for (const o of templateSettings?.outbounds || []) {
|
||||
if (o?.tag && !isBalancerLoopbackTag(o.tag)) tags.add(o.tag);
|
||||
}
|
||||
for (const tag of clientReverseTags || []) {
|
||||
if (tag) tags.add(tag);
|
||||
}
|
||||
for (const tag of subscriptionOutboundTags || []) {
|
||||
if (tag) tags.add(tag);
|
||||
}
|
||||
return [...tags];
|
||||
}, [templateSettings?.outbounds, clientReverseTags, subscriptionOutboundTags]);
|
||||
|
||||
const otherTags = useMemo(() => {
|
||||
if (editingIndex == null) return rows.map((b) => b.tag).filter(Boolean);
|
||||
return rows
|
||||
.filter((b) => b.key !== editingIndex)
|
||||
.map((b) => b.tag)
|
||||
.filter(Boolean);
|
||||
}, [rows, editingIndex]);
|
||||
|
||||
const balancerTags = useMemo(() => {
|
||||
return otherTags.filter((tg) => !isBalancerLoopbackTag(tg));
|
||||
}, [otherTags]);
|
||||
|
||||
const overrideOptions: Array<{ value: string; label: React.ReactNode }> = useMemo(() => {
|
||||
return outboundTags.map((tag) => ({ value: tag, label: tag }));
|
||||
}, [outboundTags]);
|
||||
|
||||
const mutate = useCallback(
|
||||
(mutator: (next: XraySettingsValue) => void) => {
|
||||
setTemplateSettings((prev) => {
|
||||
if (!prev) return prev;
|
||||
const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue;
|
||||
mutator(clone);
|
||||
return clone;
|
||||
});
|
||||
},
|
||||
[setTemplateSettings],
|
||||
);
|
||||
|
||||
const [liveStatus, setLiveStatus] = useState<Record<string, BalancerLiveStatus>>({});
|
||||
const [liveLoading, setLiveLoading] = useState(false);
|
||||
const liveTags = useMemo(
|
||||
() =>
|
||||
rows
|
||||
.map((r) => r.tag)
|
||||
.filter(Boolean)
|
||||
.join(','),
|
||||
[rows],
|
||||
);
|
||||
|
||||
const refreshLive = useCallback(async () => {
|
||||
if (!liveTags) {
|
||||
setLiveStatus({});
|
||||
return;
|
||||
}
|
||||
setLiveLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post(
|
||||
'/panel/api/xray/balancerStatus',
|
||||
{ tags: liveTags },
|
||||
{ silent: true },
|
||||
);
|
||||
if (msg?.success && msg.obj && typeof msg.obj === 'object') {
|
||||
setLiveStatus(msg.obj as Record<string, BalancerLiveStatus>);
|
||||
}
|
||||
} finally {
|
||||
setLiveLoading(false);
|
||||
}
|
||||
}, [liveTags]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
await refreshLive();
|
||||
if (cancelled) return;
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [refreshLive]);
|
||||
|
||||
async function setOverride(tag: string, target: string) {
|
||||
const msg = await HttpUtil.post('/panel/api/xray/balancerOverride', { tag, target });
|
||||
if (msg?.success) await refreshLive();
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
setEditingBalancer(null);
|
||||
setEditingIndex(null);
|
||||
setModalOpen(true);
|
||||
}
|
||||
function openEdit(idx: number) {
|
||||
const row = rows[idx];
|
||||
const resolved: BalancerFormValue = {
|
||||
...row,
|
||||
fallbackTag: resolveLoopbackFallback(templateSettings!, row.fallbackTag),
|
||||
};
|
||||
setEditingBalancer(resolved);
|
||||
setEditingIndex(idx);
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function onConfirm(form: BalancerFormValue) {
|
||||
mutate((tt) => {
|
||||
if (!tt.routing) tt.routing = { rules: [], balancers: [] };
|
||||
if (!Array.isArray(tt.routing.balancers)) tt.routing.balancers = [];
|
||||
const list = tt.routing.balancers as BalancerRecord[];
|
||||
|
||||
const wire: BalancerRecord = {
|
||||
tag: form.tag,
|
||||
selector: [...form.selector],
|
||||
fallbackTag: '',
|
||||
};
|
||||
if (form.strategy && form.strategy !== 'random') {
|
||||
wire.strategy = { type: form.strategy };
|
||||
if (form.strategy === 'leastLoad' && form.settings) {
|
||||
wire.strategy.settings = form.settings;
|
||||
}
|
||||
}
|
||||
|
||||
const isFallbackABalancer = form.fallbackTag && balancerTags.includes(form.fallbackTag);
|
||||
|
||||
if (isFallbackABalancer) {
|
||||
wire.fallbackTag = loopbackTagFor(form.fallbackTag);
|
||||
} else {
|
||||
wire.fallbackTag = form.fallbackTag || '';
|
||||
}
|
||||
|
||||
if (editingIndex == null) {
|
||||
list.push(wire);
|
||||
if (isFallbackABalancer) {
|
||||
ensureBalancerLoopback(tt, form.fallbackTag);
|
||||
}
|
||||
} else {
|
||||
const oldTag = list[editingIndex]?.tag;
|
||||
const oldFallback = list[editingIndex]?.fallbackTag || '';
|
||||
list[editingIndex] = wire;
|
||||
|
||||
if (oldTag && oldTag !== wire.tag) {
|
||||
const rules = tt.routing.rules || [];
|
||||
for (const rule of rules) {
|
||||
if (rule?.balancerTag === oldTag) rule.balancerTag = wire.tag;
|
||||
}
|
||||
propagateBalancerTagRename(tt, oldTag, wire.tag);
|
||||
}
|
||||
|
||||
const oldTarget = isBalancerLoopbackTag(oldFallback) ? oldFallback.slice(4) : null;
|
||||
|
||||
if (oldTarget && oldTarget !== form.fallbackTag) {
|
||||
removeBalancerLoopbackIfOrphaned(tt, oldTarget);
|
||||
}
|
||||
if (isFallbackABalancer) {
|
||||
ensureBalancerLoopback(tt, form.fallbackTag);
|
||||
}
|
||||
}
|
||||
syncObservatories(tt);
|
||||
});
|
||||
setModalOpen(false);
|
||||
}
|
||||
|
||||
function confirmDelete(idx: number) {
|
||||
const deletedTag = rows[idx]?.tag;
|
||||
const lbTag = loopbackTagFor(deletedTag);
|
||||
const dependents = (templateSettings?.routing?.balancers || [])
|
||||
.filter((b) => b.tag !== deletedTag && b.fallbackTag === lbTag)
|
||||
.map((b) => b.tag);
|
||||
if (dependents.length > 0) {
|
||||
messageApi.error(
|
||||
t('pages.xray.balancer.balancerDeleteInUse', { names: dependents.join(', ') }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const impact = templateSettings
|
||||
? planBalancerDeletion(templateSettings, idx)
|
||||
: { rules: [], balancers: [], observatory: false, burst: false };
|
||||
modal.confirm({
|
||||
title: `${t('delete')} ${t('pages.xray.Balancers')} #${idx + 1}?`,
|
||||
content: <DeletionImpactList impact={impact} />,
|
||||
okText: t('delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('cancel'),
|
||||
onOk: () =>
|
||||
mutate((tt) => {
|
||||
const tag = tt.routing?.balancers?.[idx]?.tag ?? '';
|
||||
removeBalancerLoopback(tt, tag);
|
||||
applyBalancerDeletion(tt, idx);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const columns: ColumnsType<BalancerRow> = [
|
||||
{
|
||||
title: '#',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: (_v, _record, index) => (
|
||||
<div className="action-cell">
|
||||
<span className="row-index">{index + 1}</span>
|
||||
<div className={!isMobile ? 'action-buttons' : ''}>
|
||||
{!isMobile && (
|
||||
<Button
|
||||
aria-label={t('edit')}
|
||||
shape="circle"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openEdit(index)}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: [
|
||||
...(isMobile
|
||||
? [
|
||||
{
|
||||
key: 'edit',
|
||||
label: (
|
||||
<>
|
||||
<EditOutlined /> {t('edit')}
|
||||
</>
|
||||
),
|
||||
onClick: () => openEdit(index),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'del',
|
||||
danger: true,
|
||||
label: (
|
||||
<>
|
||||
<DeleteOutlined /> {t('delete')}
|
||||
</>
|
||||
),
|
||||
onClick: () => confirmDelete(index),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button aria-label={t('more')} shape="circle" size="small" icon={<MoreOutlined />} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: 'Tag', dataIndex: 'tag', key: 'tag', align: 'center', width: 160 },
|
||||
{
|
||||
title: 'Strategy',
|
||||
key: 'strategy',
|
||||
align: 'center',
|
||||
width: 140,
|
||||
render: (_v, record) => (
|
||||
<Tag color={record.strategy === 'random' ? 'purple' : 'green'}>
|
||||
{STRATEGY_LABELS[record.strategy] || record.strategy}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Selector',
|
||||
key: 'selector',
|
||||
align: 'center',
|
||||
render: (_v, record) =>
|
||||
(record.selector || []).map((sel) => (
|
||||
<Tag key={sel} className="info-large-tag" style={{ margin: 0, marginRight: 4 }}>
|
||||
{sel}
|
||||
</Tag>
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: 'Fallback',
|
||||
dataIndex: 'displayFallbackTag',
|
||||
key: 'displayFallbackTag',
|
||||
align: 'center',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.balancerLive'),
|
||||
key: 'live',
|
||||
align: 'center',
|
||||
width: 170,
|
||||
render: (_v, record) => {
|
||||
const live = liveStatus[record.tag];
|
||||
if (!live?.running) {
|
||||
return (
|
||||
<Tooltip title={t('pages.xray.balancerNotRunning')}>
|
||||
<Tag>—</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const resolve = (tag: string) =>
|
||||
isBalancerLoopbackTag(tag) ? resolveLoopbackFallback(templateSettings!, tag) : tag;
|
||||
const picked = live.override
|
||||
? resolve(live.override)
|
||||
: live.selected?.[0]
|
||||
? resolve(live.selected[0])
|
||||
: record.displayFallbackTag;
|
||||
const tooltipText = live.override
|
||||
? resolve(live.override)
|
||||
: (live.selected || []).map(resolve).join(', ');
|
||||
return (
|
||||
<Tooltip title={tooltipText || undefined}>
|
||||
<Tag color={live.override ? 'orange' : 'blue'}>{picked || '—'}</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.balancerOverride'),
|
||||
key: 'overrideTarget',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
render: (_v, record) => {
|
||||
const live = liveStatus[record.tag];
|
||||
const resolvedFB = record.displayFallbackTag;
|
||||
let options = overrideOptions;
|
||||
if (resolvedFB && !outboundTags.includes(resolvedFB)) {
|
||||
options = [
|
||||
...overrideOptions,
|
||||
{
|
||||
value: resolvedFB,
|
||||
label: (
|
||||
<span>
|
||||
<Tag color="blue" style={{ marginRight: 4 }}>
|
||||
{t('pages.xray.rules.balancer')}
|
||||
</Tag>
|
||||
{resolvedFB}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
const rawOverride = live?.override || undefined;
|
||||
const resolvedOverride =
|
||||
rawOverride && isBalancerLoopbackTag(rawOverride)
|
||||
? resolveLoopbackFallback(templateSettings!, rawOverride)
|
||||
: rawOverride;
|
||||
return (
|
||||
<Select
|
||||
size="small"
|
||||
style={{ width: 170 }}
|
||||
placeholder={t('pages.xray.balancerOverridePh')}
|
||||
allowClear
|
||||
disabled={!live?.running}
|
||||
value={resolvedOverride}
|
||||
options={options}
|
||||
onChange={(v) => setOverride(record.tag, (v as string | undefined) || '')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const balancerSettingsTab = (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{rows.length === 0 ? (
|
||||
<Empty description={t('emptyBalancersDesc')}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
|
||||
{t('pages.xray.Balancers')}
|
||||
</Button>
|
||||
</Empty>
|
||||
) : (
|
||||
<>
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
|
||||
{t('pages.xray.Balancers')}
|
||||
</Button>
|
||||
<Tooltip title={t('pages.xray.balancerLiveRefresh')}>
|
||||
<Button
|
||||
aria-label={t('pages.xray.balancerLiveRefresh')}
|
||||
icon={<SyncOutlined spin={liveLoading} />}
|
||||
onClick={refreshLive}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey={(r) => r.key}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 700 }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalContextHolder}
|
||||
{messageContextHolder}
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'balancers',
|
||||
label: catTabLabel(
|
||||
<DeploymentUnitOutlined />,
|
||||
t('pages.xray.tabBalancerSettings'),
|
||||
isMobile,
|
||||
),
|
||||
children: balancerSettingsTab,
|
||||
},
|
||||
{
|
||||
key: 'observatory',
|
||||
label: catTabLabel(<RadarChartOutlined />, t('pages.xray.tabObservatory'), isMobile),
|
||||
children: (
|
||||
<ObservatorySettingsTab templateSettings={templateSettings} mutate={mutate} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<BalancerFormModal
|
||||
key={modalOpen ? `${editingIndex ?? 'new'}-${editingBalancer?.tag ?? ''}` : 'closed'}
|
||||
open={modalOpen}
|
||||
balancer={editingBalancer}
|
||||
outboundTags={outboundTags}
|
||||
balancerTags={balancerTags}
|
||||
balancers={balancerObjects}
|
||||
templateSettings={templateSettings}
|
||||
otherTags={otherTags}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Empty, Input, InputNumber, Select, Space, Switch, Tag } from 'antd';
|
||||
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import {
|
||||
BurstObservatorySchema,
|
||||
ObservatoryHttpMethodSchema,
|
||||
ObservatorySchema,
|
||||
type BurstObservatoryObject,
|
||||
type ObservatoryHttpMethod,
|
||||
type ObservatoryObject,
|
||||
type PingConfigObject,
|
||||
} from '@/schemas/observatory';
|
||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { settingsRequireBurstObservatory } from './balancer-helpers';
|
||||
|
||||
interface ObservatorySettingsTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
mutate: (mutator: (next: XraySettingsValue) => void) => void;
|
||||
}
|
||||
|
||||
const OBSERVATORY_DEFAULTS = ObservatorySchema.parse({});
|
||||
const BURST_DEFAULTS = BurstObservatorySchema.parse({});
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function SelectorTags({ tags }: { tags: string[] }) {
|
||||
if (!tags || tags.length === 0) return <Tag>—</Tag>;
|
||||
return (
|
||||
<>
|
||||
{tags.map((sel) => (
|
||||
<Tag
|
||||
key={sel}
|
||||
className="info-large-tag"
|
||||
style={{ margin: 0, marginRight: 4, marginBottom: 4 }}
|
||||
>
|
||||
{sel}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ObservatorySettingsTab({
|
||||
templateSettings,
|
||||
mutate,
|
||||
}: ObservatorySettingsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const observatory = useMemo<ObservatoryObject | null>(() => {
|
||||
const raw = templateSettings?.observatory;
|
||||
if (raw == null) return null;
|
||||
return { ...OBSERVATORY_DEFAULTS, ...asObject(raw) } as ObservatoryObject;
|
||||
}, [templateSettings?.observatory]);
|
||||
|
||||
const burst = useMemo<BurstObservatoryObject | null>(() => {
|
||||
const raw = templateSettings?.burstObservatory;
|
||||
if (raw == null) return null;
|
||||
const merged = { ...BURST_DEFAULTS, ...asObject(raw) } as BurstObservatoryObject;
|
||||
merged.pingConfig = {
|
||||
...BURST_DEFAULTS.pingConfig,
|
||||
...asObject(merged.pingConfig),
|
||||
} as PingConfigObject;
|
||||
return merged;
|
||||
}, [templateSettings?.burstObservatory]);
|
||||
|
||||
const hasObservatory = observatory != null;
|
||||
const hasBurst = burst != null;
|
||||
const hasMixedObservers = hasObservatory && hasBurst;
|
||||
const activeView =
|
||||
hasBurst && (!hasObservatory || settingsRequireBurstObservatory(templateSettings))
|
||||
? 'burstObservatory'
|
||||
: 'observatory';
|
||||
|
||||
function patchObservatory(patch: Partial<ObservatoryObject>) {
|
||||
mutate((tt) => {
|
||||
tt.observatory = { ...OBSERVATORY_DEFAULTS, ...asObject(tt.observatory), ...patch };
|
||||
});
|
||||
}
|
||||
|
||||
function patchPingConfig(patch: Partial<PingConfigObject>) {
|
||||
mutate((tt) => {
|
||||
const current = asObject(tt.burstObservatory);
|
||||
const currentPing = asObject(current.pingConfig);
|
||||
tt.burstObservatory = {
|
||||
...BURST_DEFAULTS,
|
||||
...current,
|
||||
pingConfig: { ...BURST_DEFAULTS.pingConfig, ...currentPing, ...patch },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasObservatory && !hasBurst) {
|
||||
return <Empty description={t('pages.xray.observatory.emptyHint')} />;
|
||||
}
|
||||
|
||||
const observatorySection = observatory && (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.subjectSelector')}
|
||||
description={t('pages.xray.observatory.subjectSelectorDesc')}
|
||||
>
|
||||
<SelectorTags tags={observatory.subjectSelector} />
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.probeURL')}
|
||||
description={t('pages.xray.observatory.probeURLDesc')}
|
||||
>
|
||||
<Input
|
||||
value={observatory.probeURL}
|
||||
onChange={(e) => patchObservatory({ probeURL: e.target.value })}
|
||||
placeholder="https://www.google.com/generate_204"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.probeInterval')}
|
||||
description={t('pages.xray.observatory.probeIntervalDesc')}
|
||||
>
|
||||
<Input
|
||||
value={observatory.probeInterval}
|
||||
onChange={(e) => patchObservatory({ probeInterval: e.target.value })}
|
||||
placeholder="1m"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.enableConcurrency')}
|
||||
description={t('pages.xray.observatory.enableConcurrencyDesc')}
|
||||
>
|
||||
<Switch
|
||||
checked={observatory.enableConcurrency}
|
||||
onChange={(v) => patchObservatory({ enableConcurrency: v })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const burstSection = burst && (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.subjectSelector')}
|
||||
description={t('pages.xray.observatory.subjectSelectorDesc')}
|
||||
>
|
||||
<SelectorTags tags={burst.subjectSelector} />
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.destination')}
|
||||
description={t('pages.xray.observatory.destinationDesc')}
|
||||
>
|
||||
<Input
|
||||
value={burst.pingConfig.destination}
|
||||
onChange={(e) => patchPingConfig({ destination: e.target.value })}
|
||||
placeholder="https://www.google.com/generate_204"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.connectivity')}
|
||||
description={t('pages.xray.observatory.connectivityDesc')}
|
||||
>
|
||||
<Input
|
||||
value={burst.pingConfig.connectivity}
|
||||
allowClear
|
||||
onChange={(e) => patchPingConfig({ connectivity: e.target.value })}
|
||||
placeholder="http://connectivitycheck.platform.hicloud.com/generate_204"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.interval')}
|
||||
description={t('pages.xray.observatory.intervalDesc')}
|
||||
>
|
||||
<Input
|
||||
value={burst.pingConfig.interval}
|
||||
onChange={(e) => patchPingConfig({ interval: e.target.value })}
|
||||
placeholder="1m"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.timeout')}
|
||||
description={t('pages.xray.observatory.timeoutDesc')}
|
||||
>
|
||||
<Input
|
||||
value={burst.pingConfig.timeout}
|
||||
onChange={(e) => patchPingConfig({ timeout: e.target.value })}
|
||||
placeholder="5s"
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.sampling')}
|
||||
description={t('pages.xray.observatory.samplingDesc')}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
value={burst.pingConfig.sampling}
|
||||
onChange={onNumber((v) => patchPingConfig({ sampling: v }))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.observatory.httpMethod')}
|
||||
description={t('pages.xray.observatory.httpMethodDesc')}
|
||||
>
|
||||
<Select<ObservatoryHttpMethod>
|
||||
value={burst.pingConfig.httpMethod}
|
||||
onChange={(v) => patchPingConfig({ httpMethod: v })}
|
||||
options={ObservatoryHttpMethodSchema.options.map((m) => ({ value: m, label: m }))}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</SettingListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Alert type="info" showIcon title={t('pages.xray.observatory.autoManaged')} />
|
||||
{hasMixedObservers && (
|
||||
<Alert type="warning" showIcon title={t('pages.xray.observatory.mixedLegacy')} />
|
||||
)}
|
||||
<div>{activeView === 'observatory' ? observatorySection : burstSection}</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as BalancersTab } from './BalancersTab';
|
||||
export { default as BalancerFormModal } from './BalancerFormModal';
|
||||
@@ -1,26 +0,0 @@
|
||||
.preset-warning {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
border: 1px solid var(--ant-color-border-secondary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preset-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--ant-color-border-secondary);
|
||||
}
|
||||
|
||||
.preset-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.preset-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, Button, Modal, Space, Tag } from 'antd';
|
||||
import './DnsPresetsModal.css';
|
||||
|
||||
interface DnsPresetsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onInstall: (servers: string[]) => void;
|
||||
}
|
||||
|
||||
export const PRESETS: { name: string; tags: string[]; data: string[] }[] = [
|
||||
{
|
||||
name: 'Google DNS',
|
||||
tags: ['UDP'],
|
||||
data: ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844'],
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare DNS',
|
||||
tags: ['UDP'],
|
||||
data: ['1.1.1.1', '1.0.0.1', '2606:4700:4700::1111', '2606:4700:4700::1001'],
|
||||
},
|
||||
{
|
||||
name: 'AdGuard DNS',
|
||||
tags: ['UDP'],
|
||||
data: ['94.140.14.14', '94.140.15.15', '2a10:50c0::ad1:ff', '2a10:50c0::ad2:ff'],
|
||||
},
|
||||
{
|
||||
name: 'AdGuard Family DNS',
|
||||
tags: ['UDP', 'Family'],
|
||||
data: ['94.140.14.15', '94.140.15.16', '2a10:50c0::bad1:ff', '2a10:50c0::bad2:ff'],
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare Family DNS',
|
||||
tags: ['UDP', 'Family'],
|
||||
data: ['1.1.1.3', '1.0.0.3', '2606:4700:4700::1113', '2606:4700:4700::1003'],
|
||||
},
|
||||
{
|
||||
name: 'Cloudflare DoH',
|
||||
tags: ['DoH'],
|
||||
data: ['https://cloudflare-dns.com/dns-query'],
|
||||
},
|
||||
{
|
||||
name: 'Google DoH',
|
||||
tags: ['DoH'],
|
||||
data: ['https://dns.google/dns-query'],
|
||||
},
|
||||
{
|
||||
name: 'Quad9 Secure DoH',
|
||||
tags: ['DoH', 'Malware'],
|
||||
data: ['https://dns.quad9.net/dns-query'],
|
||||
},
|
||||
{
|
||||
name: 'AdGuard DoH + DoQ',
|
||||
tags: ['DoH', 'DoQ', 'Ads'],
|
||||
data: ['https://dns.adguard-dns.com/dns-query', 'quic+local://dns.adguard-dns.com'],
|
||||
},
|
||||
{
|
||||
name: 'Control D Ads DoH + DoQ',
|
||||
tags: ['DoH', 'DoQ', 'Ads'],
|
||||
data: ['https://freedns.controld.com/p2', 'quic+local://p2.freedns.controld.com'],
|
||||
},
|
||||
{
|
||||
name: 'Control D Family DoH + DoQ',
|
||||
tags: ['DoH', 'DoQ', 'Family'],
|
||||
data: ['https://freedns.controld.com/p4', 'quic+local://p4.freedns.controld.com'],
|
||||
},
|
||||
];
|
||||
|
||||
function tagLabel(tag: string, t: (key: string) => string): string {
|
||||
if (tag === 'Family') return t('pages.xray.dns.dnsPresetFamily');
|
||||
return tag;
|
||||
}
|
||||
|
||||
export default function DnsPresetsModal({ open, onClose, onInstall }: DnsPresetsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={t('pages.xray.dns.dnsPresetTitle')}
|
||||
footer={null}
|
||||
mask={{ closable: false }}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
className="preset-warning"
|
||||
title={t('pages.xray.dns.dnsLeakWarning')}
|
||||
/>
|
||||
<div className="preset-list">
|
||||
{PRESETS.map((preset) => (
|
||||
<div key={preset.name} className="preset-row">
|
||||
<Space size="small" align="center">
|
||||
{preset.tags.map((tag) => (
|
||||
<Tag
|
||||
key={tag}
|
||||
color={tag === 'Family' ? 'purple' : tag === 'UDP' ? 'orange' : 'green'}
|
||||
>
|
||||
{tagLabel(tag, t)}
|
||||
</Tag>
|
||||
))}
|
||||
<span className="preset-name">{preset.name}</span>
|
||||
</Space>
|
||||
<Button type="primary" size="small" onClick={() => onInstall([...preset.data])}>
|
||||
{t('install')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Form, Input, InputNumber, Modal, Select, Space, Switch } from 'antd';
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { InputAddon } from '@/components/ui';
|
||||
import { FormField, rhfZodValidate } from '@/components/form/rhf';
|
||||
import {
|
||||
DnsQueryStrategySchema,
|
||||
DnsServerObjectInnerSchema,
|
||||
DnsServerObjectSchema,
|
||||
isEncryptedDnsAddress,
|
||||
type DnsServerObject,
|
||||
} from '@/schemas/dns';
|
||||
|
||||
export type DnsServerValue =
|
||||
| string
|
||||
| (DnsServerObject & {
|
||||
expectIPs?: string[];
|
||||
[key: string]: unknown;
|
||||
});
|
||||
|
||||
interface DnsServerModalProps {
|
||||
open: boolean;
|
||||
server: DnsServerValue | null;
|
||||
isEdit: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (value: DnsServerValue) => void;
|
||||
}
|
||||
|
||||
const STRATEGIES = DnsQueryStrategySchema.options;
|
||||
|
||||
type DnsServerForm = {
|
||||
address: string;
|
||||
port: number;
|
||||
domains: string[];
|
||||
expectedIPs: string[];
|
||||
unexpectedIPs: string[];
|
||||
queryStrategy: string;
|
||||
skipFallback: boolean;
|
||||
disableCache: boolean;
|
||||
finalQuery: boolean;
|
||||
tag: string;
|
||||
clientIP: string;
|
||||
serveStale: boolean;
|
||||
serveExpiredTTL: number;
|
||||
timeoutMs: number;
|
||||
};
|
||||
|
||||
function defaultFormValues(): DnsServerForm {
|
||||
return {
|
||||
address: 'localhost',
|
||||
port: 53,
|
||||
domains: [],
|
||||
expectedIPs: [],
|
||||
unexpectedIPs: [],
|
||||
queryStrategy: 'UseIP',
|
||||
skipFallback: false,
|
||||
disableCache: false,
|
||||
finalQuery: false,
|
||||
tag: '',
|
||||
clientIP: '',
|
||||
serveStale: false,
|
||||
serveExpiredTTL: 0,
|
||||
timeoutMs: 4000,
|
||||
};
|
||||
}
|
||||
|
||||
function valuesFromServer(server: DnsServerValue | null): DnsServerForm {
|
||||
if (server == null) return defaultFormValues();
|
||||
if (typeof server === 'string') return { ...defaultFormValues(), address: server };
|
||||
const parsed = DnsServerObjectSchema.safeParse(server);
|
||||
const data = parsed.success ? parsed.data : null;
|
||||
return {
|
||||
...defaultFormValues(),
|
||||
...(data ?? {}),
|
||||
address: (data?.address ?? server.address) || 'localhost',
|
||||
domains: data?.domains ?? server.domains ?? [],
|
||||
expectedIPs: data?.expectedIPs ?? server.expectedIPs ?? server.expectIPs ?? [],
|
||||
unexpectedIPs: data?.unexpectedIPs ?? server.unexpectedIPs ?? [],
|
||||
queryStrategy: data?.queryStrategy ?? server.queryStrategy ?? 'UseIP',
|
||||
skipFallback: data?.skipFallback ?? server.skipFallback ?? false,
|
||||
disableCache: data?.disableCache ?? server.disableCache ?? false,
|
||||
finalQuery: data?.finalQuery ?? server.finalQuery ?? false,
|
||||
tag: data?.tag ?? server.tag ?? '',
|
||||
clientIP: data?.clientIP ?? server.clientIP ?? '',
|
||||
serveStale: data?.serveStale ?? server.serveStale ?? false,
|
||||
serveExpiredTTL: data?.serveExpiredTTL ?? server.serveExpiredTTL ?? 0,
|
||||
timeoutMs: data?.timeoutMs ?? server.timeoutMs ?? 4000,
|
||||
};
|
||||
}
|
||||
|
||||
function valuesToWire(values: DnsServerForm): DnsServerValue {
|
||||
const isPlain =
|
||||
values.domains.length === 0 &&
|
||||
values.expectedIPs.length === 0 &&
|
||||
values.unexpectedIPs.length === 0 &&
|
||||
values.port === 53 &&
|
||||
values.queryStrategy === 'UseIP' &&
|
||||
values.skipFallback === false &&
|
||||
values.disableCache === false &&
|
||||
values.finalQuery === false &&
|
||||
!values.tag &&
|
||||
!values.clientIP &&
|
||||
values.serveStale === false &&
|
||||
values.serveExpiredTTL === 0 &&
|
||||
values.timeoutMs === 4000;
|
||||
if (isPlain) return values.address;
|
||||
|
||||
const out: Record<string, unknown> = {
|
||||
address: values.address,
|
||||
domains: values.domains.filter(Boolean),
|
||||
expectedIPs: values.expectedIPs.filter(Boolean),
|
||||
unexpectedIPs: values.unexpectedIPs.filter(Boolean),
|
||||
queryStrategy: values.queryStrategy,
|
||||
skipFallback: values.skipFallback,
|
||||
disableCache: values.disableCache,
|
||||
finalQuery: values.finalQuery,
|
||||
serveStale: values.serveStale,
|
||||
serveExpiredTTL: values.serveExpiredTTL,
|
||||
timeoutMs: values.timeoutMs,
|
||||
};
|
||||
if (!isEncryptedDnsAddress(values.address)) out.port = values.port;
|
||||
if (values.tag) out.tag = values.tag;
|
||||
if (values.clientIP) out.clientIP = values.clientIP;
|
||||
return out as DnsServerValue;
|
||||
}
|
||||
|
||||
const shape = DnsServerObjectInnerSchema.shape;
|
||||
|
||||
export default function DnsServerModal({
|
||||
open,
|
||||
server,
|
||||
isEdit,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: DnsServerModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const methods = useForm<DnsServerForm>({ defaultValues: defaultFormValues() });
|
||||
const domains = useWatch({ control: methods.control, name: 'domains' }) ?? [];
|
||||
const expectedIPs = useWatch({ control: methods.control, name: 'expectedIPs' }) ?? [];
|
||||
const unexpectedIPs = useWatch({ control: methods.control, name: 'unexpectedIPs' }) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
methods.reset(valuesFromServer(server));
|
||||
}, [open, server, methods]);
|
||||
|
||||
const title = isEdit ? t('pages.xray.dns.edit') : t('pages.xray.dns.add');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={title}
|
||||
okText={t('confirm')}
|
||||
cancelText={t('close')}
|
||||
mask={{ closable: false }}
|
||||
onOk={methods.handleSubmit((values) => onConfirm(valuesToWire(values)))}
|
||||
onCancel={onClose}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
|
||||
<FormField
|
||||
label={t('pages.inbounds.address')}
|
||||
name="address"
|
||||
rules={{ validate: rhfZodValidate(shape.address) }}
|
||||
>
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.inbounds.port')}
|
||||
name="port"
|
||||
rules={{ validate: rhfZodValidate(shape.port) }}
|
||||
>
|
||||
<InputNumber min={1} max={65535} />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.tag')} name="tag">
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.clientIp')} name="clientIP">
|
||||
<Input />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.strategy')} name="queryStrategy">
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
options={STRATEGIES.map((s) => ({ value: s, label: s }))}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.xray.dns.timeoutMs')}
|
||||
name="timeoutMs"
|
||||
rules={{ validate: rhfZodValidate(shape.timeoutMs) }}
|
||||
>
|
||||
<InputNumber min={0} step={500} />
|
||||
</FormField>
|
||||
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<Form.Item label={t('pages.xray.dns.domains')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => methods.setValue('domains', [...domains, ''])}
|
||||
/>
|
||||
{domains.map((_, i) => (
|
||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||
<FormField name={`domains.${i}`} noStyle>
|
||||
<Input />
|
||||
</FormField>
|
||||
<InputAddon
|
||||
ariaLabel={t('remove')}
|
||||
onClick={() =>
|
||||
methods.setValue(
|
||||
'domains',
|
||||
domains.filter((__, idx) => idx !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.xray.dns.expectIPs')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => methods.setValue('expectedIPs', [...expectedIPs, ''])}
|
||||
/>
|
||||
{expectedIPs.map((_, i) => (
|
||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||
<FormField name={`expectedIPs.${i}`} noStyle>
|
||||
<Input />
|
||||
</FormField>
|
||||
<InputAddon
|
||||
ariaLabel={t('remove')}
|
||||
onClick={() =>
|
||||
methods.setValue(
|
||||
'expectedIPs',
|
||||
expectedIPs.filter((__, idx) => idx !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t('pages.xray.dns.unexpectIPs')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
aria-label={t('add')}
|
||||
onClick={() => methods.setValue('unexpectedIPs', [...unexpectedIPs, ''])}
|
||||
/>
|
||||
{unexpectedIPs.map((_, i) => (
|
||||
<Space.Compact key={i} block style={{ marginTop: 4 }}>
|
||||
<FormField name={`unexpectedIPs.${i}`} noStyle>
|
||||
<Input />
|
||||
</FormField>
|
||||
<InputAddon
|
||||
ariaLabel={t('remove')}
|
||||
onClick={() =>
|
||||
methods.setValue(
|
||||
'unexpectedIPs',
|
||||
unexpectedIPs.filter((__, idx) => idx !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusOutlined />
|
||||
</InputAddon>
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
|
||||
<Divider style={{ margin: '5px 0' }} />
|
||||
|
||||
<FormField
|
||||
label={t('pages.xray.dns.skipFallback')}
|
||||
name="skipFallback"
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.finalQuery')} name="finalQuery" valueProp="checked">
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField
|
||||
label={t('pages.xray.dns.disableCache')}
|
||||
name="disableCache"
|
||||
valueProp="checked"
|
||||
>
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.serveStale')} name="serveStale" valueProp="checked">
|
||||
<Switch />
|
||||
</FormField>
|
||||
<FormField label={t('pages.xray.dns.serveExpiredTTL')} name="serveExpiredTTL">
|
||||
<InputNumber min={0} step={60} />
|
||||
</FormField>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
.row-index {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.muted {
|
||||
opacity: 0.7;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.hosts-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
DeleteOutlined,
|
||||
ExperimentOutlined,
|
||||
MenuOutlined,
|
||||
PlusOutlined,
|
||||
ProfileOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
import { SettingListItem } from '@/components/ui';
|
||||
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
||||
import { catTabLabel } from '@/pages/settings/catTabLabel';
|
||||
import DnsServerModal from './DnsServerModal';
|
||||
import type { DnsServerValue } from './DnsServerModal';
|
||||
import DnsPresetsModal from './DnsPresetsModal';
|
||||
import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
|
||||
import './DnsTab.css';
|
||||
|
||||
import { STRATEGIES, DEFAULT_FAKEDNS } from './helpers';
|
||||
import type { DnsConfig, HostRow, FakednsRow } from './types';
|
||||
import { useDnsServerColumns, useFakednsColumns } from './useDnsColumns';
|
||||
|
||||
interface DnsTabProps {
|
||||
templateSettings: XraySettingsValue | null;
|
||||
setTemplateSettings: SetTemplate;
|
||||
}
|
||||
|
||||
export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = useMediaQuery();
|
||||
const [modal, modalContextHolder] = Modal.useModal();
|
||||
const [hostsList, setHostsList] = useState<HostRow[]>([]);
|
||||
const [serverModalOpen, setServerModalOpen] = useState(false);
|
||||
const [editingServer, setEditingServer] = useState<DnsServerValue | null>(null);
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
||||
const [presetsModalOpen, setPresetsModalOpen] = useState(false);
|
||||
|
||||
const dns = (templateSettings?.dns as DnsConfig | undefined) ?? null;
|
||||
const dnsEnabled = !!dns;
|
||||
const sourceHosts = dns?.hosts;
|
||||
const incomingHosts = JSON.stringify(sourceHosts ?? {});
|
||||
const lastWrittenHostsRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dnsEnabled) {
|
||||
lastWrittenHostsRef.current = '{}';
|
||||
setHostsList([]);
|
||||
return;
|
||||
}
|
||||
if (incomingHosts === lastWrittenHostsRef.current) return;
|
||||
lastWrittenHostsRef.current = incomingHosts;
|
||||
setHostsList(
|
||||
Object.entries(sourceHosts ?? {}).map(([domain, values]) => ({
|
||||
domain,
|
||||
values: Array.isArray(values) ? [...values] : [String(values)],
|
||||
})),
|
||||
);
|
||||
}, [dnsEnabled, incomingHosts, sourceHosts]);
|
||||
|
||||
const mutate = useCallback(
|
||||
(mutator: (next: XraySettingsValue) => void) => {
|
||||
setTemplateSettings((prev) => {
|
||||
if (!prev) return prev;
|
||||
const clone = JSON.parse(JSON.stringify(prev)) as XraySettingsValue;
|
||||
mutator(clone);
|
||||
return clone;
|
||||
});
|
||||
},
|
||||
[setTemplateSettings],
|
||||
);
|
||||
|
||||
function toggleDNS(enabled: boolean) {
|
||||
mutate((next) => {
|
||||
if (enabled) {
|
||||
(next as { dns?: DnsConfig }).dns = {
|
||||
tag: 'dns_inbound',
|
||||
queryStrategy: 'UseIP',
|
||||
disableCache: false,
|
||||
disableFallback: false,
|
||||
disableFallbackIfMatch: false,
|
||||
useSystemHosts: false,
|
||||
enableParallelQuery: false,
|
||||
serveStale: false,
|
||||
serveExpiredTTL: 0,
|
||||
hosts: {},
|
||||
servers: [],
|
||||
};
|
||||
next.fakedns = null;
|
||||
} else {
|
||||
delete next.dns;
|
||||
delete next.fakedns;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function syncHosts(next: HostRow[]) {
|
||||
const obj: Record<string, string | string[]> = {};
|
||||
for (const row of next) {
|
||||
if (!row.domain) continue;
|
||||
const vals = (row.values || []).filter(Boolean);
|
||||
if (vals.length === 0) continue;
|
||||
obj[row.domain] = vals.length === 1 ? vals[0] : vals;
|
||||
}
|
||||
lastWrittenHostsRef.current = JSON.stringify(obj);
|
||||
setHostsList(next);
|
||||
mutate((tt) => {
|
||||
if (!tt.dns) return;
|
||||
if (Object.keys(obj).length > 0) {
|
||||
(tt.dns as DnsConfig).hosts = obj;
|
||||
} else if ('hosts' in (tt.dns as DnsConfig)) {
|
||||
delete (tt.dns as DnsConfig).hosts;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setDnsField<K extends keyof DnsConfig>(key: K, value: DnsConfig[K], omit = false) {
|
||||
mutate((tt) => {
|
||||
if (!tt.dns) return;
|
||||
if (omit && (value == null || (typeof value === 'string' && value.trim() === ''))) {
|
||||
delete (tt.dns as Record<string, unknown>)[key as string];
|
||||
} else {
|
||||
(tt.dns as Record<string, unknown>)[key as string] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const dnsServers = useMemo(() => {
|
||||
const list = dns?.servers || [];
|
||||
return list.map((server, idx) => ({ key: idx, server }));
|
||||
}, [dns?.servers]);
|
||||
|
||||
// Stable callbacks: the column definitions in useDnsServerColumns are
|
||||
// memoized, so they must be able to depend on these (see issue #5155)
|
||||
const openEditServer = useCallback(
|
||||
(idx: number) => {
|
||||
setEditingServer((dns?.servers || [])[idx] || null);
|
||||
setEditingIndex(idx);
|
||||
setServerModalOpen(true);
|
||||
},
|
||||
[dns?.servers],
|
||||
);
|
||||
const deleteServer = useCallback(
|
||||
(idx: number) => {
|
||||
mutate((tt) => {
|
||||
const cfg = tt.dns as DnsConfig | undefined;
|
||||
if (cfg?.servers) cfg.servers.splice(idx, 1);
|
||||
});
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
|
||||
const dnsColumns = useDnsServerColumns({ openEditServer, deleteServer });
|
||||
|
||||
function openAddServer() {
|
||||
setEditingServer(null);
|
||||
setEditingIndex(null);
|
||||
setServerModalOpen(true);
|
||||
}
|
||||
function onServerConfirm(value: DnsServerValue) {
|
||||
mutate((tt) => {
|
||||
if (!tt.dns) return;
|
||||
const cfg = tt.dns as DnsConfig;
|
||||
if (!Array.isArray(cfg.servers)) cfg.servers = [];
|
||||
if (editingIndex == null) cfg.servers.push(value);
|
||||
else cfg.servers[editingIndex] = value;
|
||||
});
|
||||
setServerModalOpen(false);
|
||||
}
|
||||
function clearAllServers() {
|
||||
modal.confirm({
|
||||
title: t('pages.xray.dns.clearAllTitle'),
|
||||
content: t('pages.xray.dns.clearAllConfirm'),
|
||||
okText: t('delete'),
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: t('cancel'),
|
||||
onOk: () =>
|
||||
mutate((tt) => {
|
||||
if (tt.dns) (tt.dns as DnsConfig).servers = [];
|
||||
}),
|
||||
});
|
||||
}
|
||||
function onPresetInstall(servers: string[]) {
|
||||
mutate((tt) => {
|
||||
if (tt.dns) (tt.dns as DnsConfig).servers = servers;
|
||||
});
|
||||
setPresetsModalOpen(false);
|
||||
}
|
||||
|
||||
const fakeDnsList = useMemo<{ key: number; ipPool: string; poolSize: number }[]>(() => {
|
||||
const list = Array.isArray(templateSettings?.fakedns)
|
||||
? (templateSettings?.fakedns as FakednsRow[])
|
||||
: [];
|
||||
return list.map((entry, idx) => ({ key: idx, ...entry }));
|
||||
}, [templateSettings?.fakedns]);
|
||||
|
||||
const deleteFakedns = useCallback(
|
||||
(idx: number) => {
|
||||
mutate((tt) => {
|
||||
const list = tt.fakedns as FakednsRow[] | undefined;
|
||||
if (!list) return;
|
||||
list.splice(idx, 1);
|
||||
if (list.length === 0) tt.fakedns = null;
|
||||
});
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
const updateFakednsField = useCallback(
|
||||
(idx: number, field: 'ipPool' | 'poolSize', value: string | number) => {
|
||||
mutate((tt) => {
|
||||
const list = tt.fakedns as FakednsRow[] | undefined;
|
||||
if (!list?.[idx]) return;
|
||||
(list[idx] as unknown as Record<string, unknown>)[field] = value;
|
||||
});
|
||||
},
|
||||
[mutate],
|
||||
);
|
||||
|
||||
const fakednsColumns = useFakednsColumns({ deleteFakedns, updateFakednsField });
|
||||
|
||||
function addFakedns() {
|
||||
mutate((tt) => {
|
||||
if (!Array.isArray(tt.fakedns)) tt.fakedns = [];
|
||||
(tt.fakedns as FakednsRow[]).push(DEFAULT_FAKEDNS());
|
||||
});
|
||||
}
|
||||
|
||||
const items = useMemo(() => {
|
||||
const out = [
|
||||
{
|
||||
key: '1',
|
||||
label: catTabLabel(<SettingOutlined />, t('pages.xray.generalConfigs'), isMobile),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.enable')}
|
||||
description={t('pages.xray.dns.enableDesc')}
|
||||
control={<Switch checked={dnsEnabled} onChange={toggleDNS} />}
|
||||
/>
|
||||
{dnsEnabled && (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title={t('pages.xray.dns.dnsLeakWarning')}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.tag')}
|
||||
description={t('pages.xray.dns.tagDesc')}
|
||||
control={
|
||||
<Input
|
||||
value={dns?.tag ?? 'dns_inbound'}
|
||||
onChange={(e) => setDnsField('tag', e.target.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.clientIp')}
|
||||
description={t('pages.xray.dns.clientIpDesc')}
|
||||
control={
|
||||
<Input
|
||||
value={dns?.clientIp ?? ''}
|
||||
onChange={(e) => setDnsField('clientIp', e.target.value, true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.strategy')}
|
||||
description={t('pages.xray.dns.strategyDesc')}
|
||||
control={
|
||||
<Select
|
||||
value={dns?.queryStrategy ?? 'UseIP'}
|
||||
style={{ width: '100%' }}
|
||||
options={STRATEGIES.map((s) => ({ value: s, label: s }))}
|
||||
onChange={(v) => setDnsField('queryStrategy', v)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{(
|
||||
[
|
||||
[
|
||||
'disableCache',
|
||||
'pages.xray.dns.disableCache',
|
||||
'pages.xray.dns.disableCacheDesc',
|
||||
],
|
||||
[
|
||||
'disableFallback',
|
||||
'pages.xray.dns.disableFallback',
|
||||
'pages.xray.dns.disableFallbackDesc',
|
||||
],
|
||||
[
|
||||
'disableFallbackIfMatch',
|
||||
'pages.xray.dns.disableFallbackIfMatch',
|
||||
'pages.xray.dns.disableFallbackIfMatchDesc',
|
||||
],
|
||||
[
|
||||
'enableParallelQuery',
|
||||
'pages.xray.dns.enableParallelQuery',
|
||||
'pages.xray.dns.enableParallelQueryDesc',
|
||||
],
|
||||
[
|
||||
'useSystemHosts',
|
||||
'pages.xray.dns.useSystemHosts',
|
||||
'pages.xray.dns.useSystemHostsDesc',
|
||||
],
|
||||
['serveStale', 'pages.xray.dns.serveStale', 'pages.xray.dns.serveStaleDesc'],
|
||||
] as const
|
||||
).map(([field, titleKey, descKey]) => (
|
||||
<SettingListItem
|
||||
key={field}
|
||||
paddings="small"
|
||||
title={t(titleKey)}
|
||||
description={t(descKey)}
|
||||
control={
|
||||
<Switch
|
||||
checked={!!dns?.[field]}
|
||||
onChange={(v) => setDnsField(field as keyof DnsConfig, v as never)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title={t('pages.xray.dns.serveExpiredTTL')}
|
||||
description={t('pages.xray.dns.serveExpiredTTLDesc')}
|
||||
control={
|
||||
<InputNumber
|
||||
value={dns?.serveExpiredTTL ?? 0}
|
||||
min={0}
|
||||
step={60}
|
||||
style={{ width: '100%' }}
|
||||
onChange={onNumber((v) => setDnsField('serveExpiredTTL', v))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (dnsEnabled) {
|
||||
out.push({
|
||||
key: 'hosts',
|
||||
label: catTabLabel(<ProfileOutlined />, t('pages.xray.dns.hosts'), isMobile),
|
||||
children:
|
||||
hostsList.length === 0 ? (
|
||||
<Empty description={t('pages.xray.dns.hostsEmpty')}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
||||
>
|
||||
{t('pages.xray.dns.hostsAdd')}
|
||||
</Button>
|
||||
</Empty>
|
||||
) : (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => syncHosts([...hostsList, { domain: '', values: [] }])}
|
||||
>
|
||||
{t('pages.xray.dns.hostsAdd')}
|
||||
</Button>
|
||||
{hostsList.map((row, idx) => (
|
||||
<div key={`h${idx}`} className="hosts-row">
|
||||
<Input
|
||||
value={row.domain}
|
||||
aria-label={t('pages.xray.dns.hostsDomain')}
|
||||
placeholder={t('pages.xray.dns.hostsDomain')}
|
||||
style={{ flex: '1 1 220px' }}
|
||||
onChange={(e) => {
|
||||
const next = hostsList.map((r, i) =>
|
||||
i === idx ? { ...r, domain: e.target.value } : r,
|
||||
);
|
||||
syncHosts(next);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={row.values}
|
||||
aria-label={t('pages.xray.dns.hostsValues')}
|
||||
placeholder={t('pages.xray.dns.hostsValues')}
|
||||
style={{ flex: '2 1 320px' }}
|
||||
tokenSeparators={[',', ' ']}
|
||||
onChange={(values) => {
|
||||
const next = hostsList.map((r, i) => (i === idx ? { ...r, values } : r));
|
||||
syncHosts(next);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
danger
|
||||
aria-label={t('delete')}
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => syncHosts(hostsList.filter((_, i) => i !== idx))}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
|
||||
out.push({
|
||||
key: '2',
|
||||
label: catTabLabel(<DatabaseOutlined />, 'DNS', isMobile),
|
||||
children:
|
||||
dnsServers.length === 0 ? (
|
||||
<Empty description={t('emptyDnsDesc')}>
|
||||
<Space>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddServer}>
|
||||
{t('pages.xray.dns.add')}
|
||||
</Button>
|
||||
<Button icon={<MenuOutlined />} onClick={() => setPresetsModalOpen(true)}>
|
||||
{t('pages.xray.dns.usePreset')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Empty>
|
||||
) : (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space wrap>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openAddServer}>
|
||||
{t('pages.xray.dns.add')}
|
||||
</Button>
|
||||
<Button icon={<MenuOutlined />} onClick={() => setPresetsModalOpen(true)}>
|
||||
{t('pages.xray.dns.usePreset')}
|
||||
</Button>
|
||||
<Button danger icon={<DeleteOutlined />} onClick={clearAllServers}>
|
||||
{t('pages.xray.dns.clearAll')}
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
columns={dnsColumns}
|
||||
dataSource={dnsServers}
|
||||
rowKey={(r) => r.key}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
|
||||
out.push({
|
||||
key: '3',
|
||||
label: catTabLabel(<ExperimentOutlined />, 'Fake DNS', isMobile),
|
||||
children:
|
||||
fakeDnsList.length === 0 ? (
|
||||
<Empty description={t('emptyFakeDnsDesc')}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={addFakedns}>
|
||||
{t('pages.xray.fakedns.add')}
|
||||
</Button>
|
||||
</Empty>
|
||||
) : (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={addFakedns}>
|
||||
{t('pages.xray.fakedns.add')}
|
||||
</Button>
|
||||
<Table
|
||||
columns={fakednsColumns}
|
||||
dataSource={fakeDnsList}
|
||||
rowKey={(r) => r.key}
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [t, isMobile, dnsEnabled, dns, hostsList, dnsServers, fakeDnsList]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalContextHolder}
|
||||
<Tabs defaultActiveKey="1" items={items} />
|
||||
<DnsServerModal
|
||||
open={serverModalOpen}
|
||||
server={editingServer}
|
||||
isEdit={editingIndex != null}
|
||||
onClose={() => setServerModalOpen(false)}
|
||||
onConfirm={onServerConfirm}
|
||||
/>
|
||||
<DnsPresetsModal
|
||||
open={presetsModalOpen}
|
||||
onClose={() => setPresetsModalOpen(false)}
|
||||
onInstall={onPresetInstall}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { DnsQueryStrategySchema } from '@/schemas/dns';
|
||||
import type { DnsServerValue } from './DnsServerModal';
|
||||
|
||||
export const STRATEGIES = DnsQueryStrategySchema.options;
|
||||
export const DEFAULT_FAKEDNS = () => ({ ipPool: '198.18.0.0/15', poolSize: 65535 });
|
||||
|
||||
export function addrFor(server: DnsServerValue): string {
|
||||
return typeof server === 'string' ? server : server?.address || '';
|
||||
}
|
||||
|
||||
export function domainsFor(server: DnsServerValue): string {
|
||||
return typeof server === 'object' && server !== null ? (server.domains || []).join(',') : '';
|
||||
}
|
||||
|
||||
export function expectedIPsFor(server: DnsServerValue): string {
|
||||
if (typeof server !== 'object' || !server) return '';
|
||||
const list = server.expectedIPs || server.expectIPs || [];
|
||||
return Array.isArray(list) ? list.join(',') : '';
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as DnsTab } from './DnsTab';
|
||||
export { default as DnsServerModal } from './DnsServerModal';
|
||||
export { default as DnsPresetsModal } from './DnsPresetsModal';
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { DnsObject } from '@/schemas/dns';
|
||||
import type { DnsServerValue } from './DnsServerModal';
|
||||
|
||||
export type DnsConfig = Omit<DnsObject, 'servers'> & { servers?: DnsServerValue[] };
|
||||
|
||||
export interface HostRow {
|
||||
domain: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface FakednsRow {
|
||||
ipPool: string;
|
||||
poolSize: number;
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Dropdown, Input, InputNumber, Space } from 'antd';
|
||||
import { MoreOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
import { onNumber } from '@/utils/onNumber';
|
||||
|
||||
import { addrFor, domainsFor, expectedIPsFor } from './helpers';
|
||||
import type { DnsServerValue } from './DnsServerModal';
|
||||
|
||||
export type DnsServerRow = { key: number; server: DnsServerValue };
|
||||
export type FakednsTableRow = { key: number; ipPool: string; poolSize: number };
|
||||
|
||||
export function useDnsServerColumns({
|
||||
openEditServer,
|
||||
deleteServer,
|
||||
}: {
|
||||
openEditServer: (idx: number) => void;
|
||||
deleteServer: (idx: number) => void;
|
||||
}): ColumnsType<DnsServerRow> {
|
||||
const { t } = useTranslation();
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '#',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
render: (_v, _record, index) => (
|
||||
<Space size={6}>
|
||||
<span className="row-index">{index + 1}</span>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
label: (
|
||||
<>
|
||||
<EditOutlined /> {t('edit')}
|
||||
</>
|
||||
),
|
||||
onClick: () => openEditServer(index),
|
||||
},
|
||||
{
|
||||
key: 'del',
|
||||
danger: true,
|
||||
label: (
|
||||
<>
|
||||
<DeleteOutlined /> {t('delete')}
|
||||
</>
|
||||
),
|
||||
onClick: () => deleteServer(index),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button aria-label={t('more')} shape="circle" size="small" icon={<MoreOutlined />} />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('pages.inbounds.address'),
|
||||
key: 'address',
|
||||
align: 'left',
|
||||
render: (_v, record) => addrFor(record.server),
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.dns.domains'),
|
||||
key: 'domains',
|
||||
align: 'left',
|
||||
render: (_v, record) => <span className="muted">{domainsFor(record.server)}</span>,
|
||||
},
|
||||
{
|
||||
title: t('pages.xray.dns.expectIPs'),
|
||||
key: 'expectedIPs',
|
||||
align: 'left',
|
||||
render: (_v, record) => <span className="muted">{expectedIPsFor(record.server)}</span>,
|
||||
},
|
||||
],
|
||||
[t, openEditServer, deleteServer],
|
||||
);
|
||||
}
|
||||
|
||||
export function useFakednsColumns({
|
||||
deleteFakedns,
|
||||
updateFakednsField,
|
||||
}: {
|
||||
deleteFakedns: (idx: number) => void;
|
||||
updateFakednsField: (idx: number, field: 'ipPool' | 'poolSize', value: string | number) => void;
|
||||
}): ColumnsType<FakednsTableRow> {
|
||||
const { t } = useTranslation();
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
title: '#',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
render: (_v, _record, index) => (
|
||||
<Space size={6}>
|
||||
<span className="row-index">{index + 1}</span>
|
||||
<Button
|
||||
aria-label={t('delete')}
|
||||
shape="circle"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => deleteFakedns(index)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'IP pool',
|
||||
dataIndex: 'ipPool',
|
||||
key: 'ipPool',
|
||||
align: 'left',
|
||||
render: (_v, record, index) => (
|
||||
<Input
|
||||
value={record.ipPool}
|
||||
aria-label={t('pages.xray.fakedns.ipPool')}
|
||||
size="small"
|
||||
onChange={(e) => updateFakednsField(index, 'ipPool', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Pool size',
|
||||
dataIndex: 'poolSize',
|
||||
key: 'poolSize',
|
||||
align: 'right',
|
||||
width: 120,
|
||||
render: (_v, record, index) => (
|
||||
<InputNumber
|
||||
value={record.poolSize}
|
||||
aria-label={t('pages.xray.fakedns.poolSize')}
|
||||
min={1}
|
||||
size="small"
|
||||
onChange={onNumber((v) => updateFakednsField(index, 'poolSize', v))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, deleteFakedns, updateFakednsField],
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { AllSetting } from '@/models/setting';
|
||||
import SecurityTab from '@/pages/settings/SecurityTab';
|
||||
import { HttpUtil } from '@/utils';
|
||||
|
||||
@@ -26,9 +25,7 @@ describe('API token creation date', () => {
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<SecurityTab allSetting={{} as AllSetting} updateSetting={vi.fn()} saveSetting={vi.fn()} />,
|
||||
);
|
||||
render(<SecurityTab />);
|
||||
fireEvent.click(screen.getByRole('tab', { name: /API Token/ }));
|
||||
|
||||
expect(await screen.findByText('seconds-token')).toBeTruthy();
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
import BalancerFormModal from '@/pages/xray/balancers/BalancerFormModal';
|
||||
import type { BalancerFormValue } from '@/pages/xray/balancers/BalancerFormModal';
|
||||
import type { BalancerObject } from '@/schemas/routing';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
function renderModal(onConfirm = vi.fn()) {
|
||||
renderWithProviders(
|
||||
<BalancerFormModal
|
||||
open
|
||||
balancer={null}
|
||||
outboundTags={['proxy', 'direct']}
|
||||
balancerTags={[]}
|
||||
balancers={[]}
|
||||
templateSettings={null}
|
||||
otherTags={['existing']}
|
||||
onClose={() => {}}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
return { onConfirm };
|
||||
}
|
||||
|
||||
function erroredItemCount(): number {
|
||||
return document.querySelectorAll('.ant-form-item-has-error').length;
|
||||
}
|
||||
|
||||
function explainText(): string {
|
||||
return Array.from(document.querySelectorAll('.ant-form-item-explain'))
|
||||
.map((el) => (el.textContent ?? '').trim())
|
||||
.join(' | ');
|
||||
}
|
||||
|
||||
function primaryButton(): HTMLElement {
|
||||
const btn = document.querySelector('.ant-modal-footer .ant-btn-primary');
|
||||
if (!btn) throw new Error('Primary button not found');
|
||||
return btn as HTMLElement;
|
||||
}
|
||||
|
||||
describe('BalancerFormModal', () => {
|
||||
it('shows no validation errors when freshly opened in add mode', () => {
|
||||
renderModal();
|
||||
expect(document.querySelector('.ant-modal')).toBeTruthy();
|
||||
expect(erroredItemCount()).toBe(0);
|
||||
expect(explainText()).not.toContain('Tag is required');
|
||||
expect(explainText()).not.toContain('Pick at least one outbound');
|
||||
expect(primaryButton().hasAttribute('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('reveals required-field errors only after a save attempt, without confirming', () => {
|
||||
const { onConfirm } = renderModal();
|
||||
expect(erroredItemCount()).toBe(0);
|
||||
|
||||
fireEvent.click(primaryButton());
|
||||
|
||||
expect(erroredItemCount()).toBe(2);
|
||||
expect(explainText()).toContain('Tag is required');
|
||||
expect(explainText()).toContain('Pick at least one outbound');
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables save and warns when the chosen fallback would create a balancer cycle', () => {
|
||||
const editing: BalancerFormValue = {
|
||||
tag: 'A',
|
||||
strategy: 'random',
|
||||
selector: ['proxy'],
|
||||
fallbackTag: 'B',
|
||||
};
|
||||
const others: BalancerObject[] = [{ tag: 'B', selector: ['direct'], fallbackTag: 'A' }];
|
||||
const onConfirm = vi.fn();
|
||||
renderWithProviders(
|
||||
<BalancerFormModal
|
||||
open
|
||||
balancer={editing}
|
||||
outboundTags={['proxy', 'direct']}
|
||||
balancerTags={['A', 'B']}
|
||||
balancers={others}
|
||||
templateSettings={null}
|
||||
otherTags={['B']}
|
||||
onClose={() => {}}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('.ant-alert-error')).toBeTruthy();
|
||||
expect(primaryButton().hasAttribute('disabled')).toBe(true);
|
||||
|
||||
fireEvent.click(primaryButton());
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { PRESETS } from '@/pages/xray/dns/DnsPresetsModal';
|
||||
import { DnsObjectSchema, DnsServerObjectSchema } from '@/schemas/dns';
|
||||
|
||||
describe('DNS presets', () => {
|
||||
it('include encrypted presets accepted by xray DNS config', () => {
|
||||
const servers = PRESETS.flatMap((preset) => preset.data);
|
||||
|
||||
expect(servers).toContain('https://freedns.controld.com/p2');
|
||||
expect(servers).toContain('quic+local://p2.freedns.controld.com');
|
||||
expect(servers).toContain('https://dns.google/dns-query');
|
||||
expect(servers.every((server) => !server.startsWith('tls://'))).toBe(true);
|
||||
expect(DnsObjectSchema.parse({ servers }).servers).toEqual(servers);
|
||||
for (const server of servers) {
|
||||
expect(DnsServerObjectSchema.parse({ address: server }).address).toBe(server);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,130 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
|
||||
import DnsTab from '@/pages/xray/dns/DnsTab';
|
||||
import type { SetTemplate, XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
function withHosts(hosts: Record<string, string>): XraySettingsValue {
|
||||
return {
|
||||
dns: {
|
||||
hosts,
|
||||
servers: [],
|
||||
},
|
||||
} as unknown as XraySettingsValue;
|
||||
}
|
||||
|
||||
describe('DnsTab', () => {
|
||||
it('keeps an empty row after adding a host', () => {
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
|
||||
withHosts({ 'first.example': '1.1.1.1' }),
|
||||
);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
|
||||
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add Host$/ }));
|
||||
|
||||
expect(screen.getAllByLabelText('Domain (e.g. domain:example.com)')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('keeps a row visible while its domain is incomplete', () => {
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
|
||||
withHosts({ 'first.example': '1.1.1.1' }),
|
||||
);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
|
||||
return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
|
||||
fireEvent.change(screen.getByLabelText('Domain (e.g. domain:example.com)'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
|
||||
expect(
|
||||
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('shows hosts from an externally refreshed configuration', () => {
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
|
||||
withHosts({ 'first.example': '1.1.1.1' }),
|
||||
);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTemplateSettings(withHosts({ 'second.example': '2.2.2.2' }))}
|
||||
>
|
||||
Refresh hosts
|
||||
</button>
|
||||
<DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
|
||||
expect(
|
||||
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
|
||||
).toBe('first.example');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Refresh hosts' }));
|
||||
expect(
|
||||
(screen.getByLabelText('Domain (e.g. domain:example.com)') as HTMLInputElement).value,
|
||||
).toBe('second.example');
|
||||
});
|
||||
|
||||
it('clears an incomplete host draft when DNS is disabled', () => {
|
||||
function Harness() {
|
||||
const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
|
||||
withHosts({ 'first.example': '1.1.1.1' }),
|
||||
);
|
||||
const updateTemplate: SetTemplate = (next) => {
|
||||
setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setTemplateSettings({})}>
|
||||
Disable DNS
|
||||
</button>
|
||||
<button type="button" onClick={() => setTemplateSettings(withHosts({}))}>
|
||||
Enable DNS
|
||||
</button>
|
||||
<DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
|
||||
fireEvent.change(screen.getByLabelText('Domain (e.g. domain:example.com)'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Disable DNS' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Enable DNS' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: /Hosts$/ }));
|
||||
|
||||
expect(screen.queryByLabelText('Domain (e.g. domain:example.com)')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import ObservatorySettingsTab from '@/pages/xray/balancers/ObservatorySettingsTab';
|
||||
import type { XraySettingsValue } from '@/hooks/useXraySetting';
|
||||
import { renderWithProviders } from './test-utils';
|
||||
|
||||
function renderTab(templateSettings: XraySettingsValue) {
|
||||
renderWithProviders(
|
||||
<ObservatorySettingsTab templateSettings={templateSettings} mutate={vi.fn()} />,
|
||||
);
|
||||
}
|
||||
|
||||
describe('ObservatorySettingsTab', () => {
|
||||
it('shows one burst settings panel and warns for legacy mixed observers', () => {
|
||||
renderTab({
|
||||
routing: {
|
||||
balancers: [{ tag: 'll', selector: ['proxy-a'], strategy: { type: 'leastLoad' } }],
|
||||
},
|
||||
observatory: { subjectSelector: ['stale-regular'] },
|
||||
burstObservatory: { subjectSelector: ['proxy-a'] },
|
||||
} as unknown as XraySettingsValue);
|
||||
|
||||
expect(
|
||||
screen.getByText(/This config contains both Observatory and Burst Observatory/),
|
||||
).toBeTruthy();
|
||||
expect(document.querySelector('.ant-segmented')).toBeFalsy();
|
||||
expect(screen.getByText('Probe Destination')).toBeTruthy();
|
||||
expect(screen.queryByText('Probe URL')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows regular observatory settings for mixed legacy configs that normalize back to leastPing only', () => {
|
||||
renderTab({
|
||||
routing: {
|
||||
balancers: [{ tag: 'lp', selector: ['proxy-b'], strategy: { type: 'leastPing' } }],
|
||||
},
|
||||
observatory: { subjectSelector: ['proxy-b'] },
|
||||
burstObservatory: { subjectSelector: ['stale-burst'] },
|
||||
} as unknown as XraySettingsValue);
|
||||
|
||||
expect(
|
||||
screen.getByText(/This config contains both Observatory and Burst Observatory/),
|
||||
).toBeTruthy();
|
||||
expect(document.querySelector('.ant-segmented')).toBeFalsy();
|
||||
expect(screen.getByText('Probe URL')).toBeTruthy();
|
||||
expect(screen.queryByText('Probe Destination')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
+143
-22
@@ -43,6 +43,12 @@ type Config struct {
|
||||
// config. Needed when agent and master share a host (e.g. testing),
|
||||
// otherwise both xrays fight over the default 62789. 0 = use master's.
|
||||
ApiPort int `json:"api_port"`
|
||||
// BaselinePath is where the last master-acknowledged traffic counters
|
||||
// are persisted, so an agent restart keeps diffing against them
|
||||
// instead of swallowing unreported traffic into a fresh baseline.
|
||||
// Empty: main derives it next to the agent config file; the agent
|
||||
// falls back to the bin folder.
|
||||
BaselinePath string `json:"baseline_path,omitempty"`
|
||||
}
|
||||
|
||||
// Agent represents the node agent
|
||||
@@ -54,6 +60,7 @@ type Agent struct {
|
||||
xrayProc *xray.Process
|
||||
xrayAPI xray.XrayAPI
|
||||
apiPort int
|
||||
baselineLoaded bool
|
||||
}
|
||||
|
||||
// New creates a new agent instance
|
||||
@@ -331,36 +338,73 @@ type userEntry struct {
|
||||
Down int64 `json:"down"`
|
||||
}
|
||||
|
||||
// collectAndReportTraffic queries the local xray stats API for per-poll
|
||||
// deltas and posts them to the master. The XrayAPI instance is kept across
|
||||
// polls so delta baselines survive; xray restarts are handled (counters
|
||||
// resetting to zero are treated as fresh).
|
||||
func (a *Agent) collectAndReportTraffic() error {
|
||||
if a.xrayProc == nil || !a.xrayProc.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
if a.apiPort <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := a.xrayAPI.Init(a.apiPort); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.xrayAPI.Close()
|
||||
// statsClient is the slice of the xray stats API the traffic reporter
|
||||
// uses. The snapshot/commit pair is the loss-proofing contract: snapshots
|
||||
// never advance the baseline, commits do.
|
||||
type statsClient interface {
|
||||
GetTrafficSnapshot() (*xray.TrafficSnapshot, error)
|
||||
CommitTrafficSnapshot(*xray.TrafficSnapshot)
|
||||
RestoreTrafficBaseline(map[string]int64)
|
||||
GetOnlineUsers() ([]xray.OnlineUser, error)
|
||||
}
|
||||
|
||||
traffics, clientTraffics, err := a.xrayAPI.GetTraffic()
|
||||
// trafficBaselineFile is the on-disk form of the last committed baseline.
|
||||
type trafficBaselineFile struct {
|
||||
Version int `json:"version"`
|
||||
Values map[string]int64 `json:"values"`
|
||||
}
|
||||
|
||||
// baselinePath resolves where the acknowledged traffic baseline lives.
|
||||
func (a *Agent) baselinePath() string {
|
||||
if a.config.BaselinePath != "" {
|
||||
return a.config.BaselinePath
|
||||
}
|
||||
return filepath.Join(config.GetBinFolderPath(), "agent-traffic-baseline.json")
|
||||
}
|
||||
|
||||
// loadTrafficBaseline reads a persisted baseline. A missing file is a
|
||||
// normal first run; a corrupt one is reported so the caller can log it —
|
||||
// both yield no values and the reporter falls back to a baseline-only
|
||||
// first snapshot rather than crashing the agent.
|
||||
func loadTrafficBaseline(path string) (map[string]int64, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var f trafficBaselineFile
|
||||
if err := json.Unmarshal(data, &f); err != nil {
|
||||
return nil, fmt.Errorf("parse traffic baseline: %w", err)
|
||||
}
|
||||
return f.Values, nil
|
||||
}
|
||||
|
||||
// saveTrafficBaseline persists the baseline atomically (write-then-rename)
|
||||
// so a crash mid-write can never leave a torn file behind.
|
||||
func saveTrafficBaseline(path string, values map[string]int64) error {
|
||||
data, err := json.Marshal(trafficBaselineFile{Version: 1, Values: values})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
onlineUsers, _ := a.xrayAPI.GetOnlineUsers()
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
// buildTrafficReport shapes a snapshot plus the freshly polled online
|
||||
// users into the wire report, dropping zero-traffic entries. Onlines are
|
||||
// point-in-time data: they ride along with each report and are never part
|
||||
// of the baseline bookkeeping.
|
||||
func buildTrafficReport(snap *xray.TrafficSnapshot, onlineUsers []xray.OnlineUser) trafficReport {
|
||||
report := trafficReport{}
|
||||
for _, t := range traffics {
|
||||
for _, t := range snap.Traffics {
|
||||
if t == nil || !t.IsInbound || t.Up+t.Down <= 0 {
|
||||
continue
|
||||
}
|
||||
report.Inbounds = append(report.Inbounds, trafficEntry{Tag: t.Tag, Up: t.Up, Down: t.Down})
|
||||
}
|
||||
for _, ct := range clientTraffics {
|
||||
for _, ct := range snap.ClientTraffics {
|
||||
if ct == nil || ct.Email == "" || ct.Up+ct.Down <= 0 {
|
||||
continue
|
||||
}
|
||||
@@ -369,10 +413,16 @@ func (a *Agent) collectAndReportTraffic() error {
|
||||
for _, u := range onlineUsers {
|
||||
report.Onlines = append(report.Onlines, u.Email)
|
||||
}
|
||||
if len(report.Inbounds) == 0 && len(report.Users) == 0 && len(report.Onlines) == 0 {
|
||||
return nil
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func (r trafficReport) empty() bool {
|
||||
return len(r.Inbounds) == 0 && len(r.Users) == 0 && len(r.Onlines) == 0
|
||||
}
|
||||
|
||||
// postTrafficReport sends one report to the master; only an HTTP 200
|
||||
// counts as acknowledged.
|
||||
func (a *Agent) postTrafficReport(report trafficReport) error {
|
||||
url := fmt.Sprintf("%s/panel/api/agent/traffic", a.config.MasterURL)
|
||||
body, _ := json.Marshal(report)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
@@ -393,6 +443,77 @@ func (a *Agent) collectAndReportTraffic() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportTrafficWith runs one snapshot → report → commit cycle.
|
||||
//
|
||||
// The committed baseline advances ONLY after the master acknowledges the
|
||||
// report (HTTP 200). Core counters are cumulative, so when a report fails
|
||||
// nothing is lost: the next snapshot diffs against the same baseline and
|
||||
// re-includes everything unreported, exactly once. The acknowledged
|
||||
// baseline is persisted after every commit, and restored before the first
|
||||
// snapshot of a restarted agent, so an agent restart cannot swallow the
|
||||
// unreported tail into a fresh baseline either.
|
||||
//
|
||||
// A poll with nothing to send still commits: it establishes the very
|
||||
// first baseline, and keeps the persisted copy fresh.
|
||||
func (a *Agent) reportTrafficWith(sc statsClient, post func(trafficReport) error) error {
|
||||
if !a.baselineLoaded {
|
||||
a.baselineLoaded = true
|
||||
if values, err := loadTrafficBaseline(a.baselinePath()); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Warningf("Traffic baseline at %s unusable (%v); starting from a fresh baseline", a.baselinePath(), err)
|
||||
}
|
||||
} else if len(values) > 0 {
|
||||
sc.RestoreTrafficBaseline(values)
|
||||
}
|
||||
}
|
||||
|
||||
snap, err := sc.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
onlineUsers, _ := sc.GetOnlineUsers()
|
||||
|
||||
report := buildTrafficReport(snap, onlineUsers)
|
||||
if !report.empty() {
|
||||
if err := post(report); err != nil {
|
||||
// No commit: the baseline stays where the master last
|
||||
// acknowledged it, and these deltas are reported again
|
||||
// (grown) on the next poll.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sc.CommitTrafficSnapshot(snap)
|
||||
if err := saveTrafficBaseline(a.baselinePath(), snap.BaselineValues()); err != nil {
|
||||
// The in-memory commit stands — rolling it back would re-report
|
||||
// traffic the master already counted. The stale file self-heals
|
||||
// on the next successful poll; only a crash before that could
|
||||
// re-report a single interval.
|
||||
logger.Warningf("Failed to persist traffic baseline: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectAndReportTraffic snapshots the local xray counters and reports
|
||||
// the deltas since the last master-acknowledged baseline. The XrayAPI
|
||||
// instance is kept across polls so baselines survive; xray restarts are
|
||||
// handled (counters resetting to zero are treated as fresh).
|
||||
func (a *Agent) collectAndReportTraffic() error {
|
||||
if a.xrayProc == nil || !a.xrayProc.IsRunning() {
|
||||
return nil
|
||||
}
|
||||
if a.apiPort <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := a.xrayAPI.Init(a.apiPort); err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.xrayAPI.Close()
|
||||
|
||||
return a.reportTrafficWith(&a.xrayAPI, a.postTrafficReport)
|
||||
}
|
||||
|
||||
// LoadConfig loads agent config from file
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// fakeStatsClient implements the snapshot/commit contract over scripted
|
||||
// cumulative counters: snapshots diff against the committed baseline with
|
||||
// the same per-stat rule as the real client (unseen or moved-backwards
|
||||
// counters count from zero), and only Commit moves the baseline. The
|
||||
// delta math itself is proven against the wire-level fake in the xray
|
||||
// package; here the fake exists so the agent's orchestration — commit
|
||||
// only after a 200, persist, restore after restart — can be driven
|
||||
// exactly.
|
||||
type fakeStatsClient struct {
|
||||
counters map[string]int64
|
||||
committed map[string]int64
|
||||
ready bool
|
||||
}
|
||||
|
||||
func newFakeStatsClient() *fakeStatsClient {
|
||||
return &fakeStatsClient{counters: map[string]int64{}}
|
||||
}
|
||||
|
||||
func (f *fakeStatsClient) GetTrafficSnapshot() (*xray.TrafficSnapshot, error) {
|
||||
snap := &xray.TrafficSnapshot{Values: map[string]int64{}}
|
||||
clients := map[string]*xray.ClientTraffic{}
|
||||
for name, value := range f.counters {
|
||||
snap.Values[name] = value
|
||||
if !f.ready {
|
||||
continue
|
||||
}
|
||||
last, ok := f.committed[name]
|
||||
if !ok || value < last {
|
||||
last = 0
|
||||
}
|
||||
delta := value - last
|
||||
parts := strings.Split(name, ">>>")
|
||||
if len(parts) != 4 || parts[0] != "user" {
|
||||
continue
|
||||
}
|
||||
ct, ok := clients[parts[1]]
|
||||
if !ok {
|
||||
ct = &xray.ClientTraffic{Email: parts[1]}
|
||||
clients[parts[1]] = ct
|
||||
}
|
||||
if parts[3] == "downlink" {
|
||||
ct.Down = delta
|
||||
} else {
|
||||
ct.Up = delta
|
||||
}
|
||||
}
|
||||
for _, ct := range clients {
|
||||
snap.ClientTraffics = append(snap.ClientTraffics, ct)
|
||||
}
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func (f *fakeStatsClient) CommitTrafficSnapshot(s *xray.TrafficSnapshot) {
|
||||
f.committed = s.BaselineValues()
|
||||
f.ready = true
|
||||
}
|
||||
|
||||
func (f *fakeStatsClient) RestoreTrafficBaseline(values map[string]int64) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
f.committed = map[string]int64{}
|
||||
for k, v := range values {
|
||||
f.committed[k] = v
|
||||
}
|
||||
f.ready = true
|
||||
}
|
||||
|
||||
func (f *fakeStatsClient) GetOnlineUsers() ([]xray.OnlineUser, error) { return nil, nil }
|
||||
|
||||
// fakeMaster is an HTTP master whose /panel/api/agent/traffic endpoint
|
||||
// fails (500) for the first failTimes requests, then acknowledges and
|
||||
// records everything it receives.
|
||||
type fakeMaster struct {
|
||||
srv *httptest.Server
|
||||
mu sync.Mutex
|
||||
failTimes int
|
||||
posts int
|
||||
userUp map[string]int64
|
||||
userDown map[string]int64
|
||||
}
|
||||
|
||||
func newFakeMaster(t *testing.T, failTimes int) *fakeMaster {
|
||||
t.Helper()
|
||||
m := &fakeMaster{failTimes: failTimes, userUp: map[string]int64{}, userDown: map[string]int64{}}
|
||||
m.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.posts++
|
||||
if m.posts <= m.failTimes {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var report trafficReport
|
||||
if err := json.NewDecoder(r.Body).Decode(&report); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for _, u := range report.Users {
|
||||
m.userUp[u.Email] += u.Up
|
||||
m.userDown[u.Email] += u.Down
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(m.srv.Close)
|
||||
return m
|
||||
}
|
||||
|
||||
func newTestAgent(t *testing.T, masterURL string) *Agent {
|
||||
t.Helper()
|
||||
return New(&Config{
|
||||
MasterURL: masterURL,
|
||||
Token: "test-token",
|
||||
NodeName: "test-node",
|
||||
BaselinePath: filepath.Join(t.TempDir(), "agent-traffic-baseline.json"),
|
||||
})
|
||||
}
|
||||
|
||||
const aliceUp = "user>>>alice>>>traffic>>>uplink"
|
||||
|
||||
// TestReportOutageThenRecoveryConservesTotals is the acceptance scenario
|
||||
// for the fix: the master is down for several polls while traffic accrues,
|
||||
// then recovers. Every byte the core counted must reach the master exactly
|
||||
// once — the recovery report carries the whole outage window, and later
|
||||
// polls carry only new traffic.
|
||||
func TestReportOutageThenRecoveryConservesTotals(t *testing.T) {
|
||||
master := newFakeMaster(t, 2) // first two posts fail with 500
|
||||
a := newTestAgent(t, master.srv.URL)
|
||||
fake := newFakeStatsClient()
|
||||
fake.counters[aliceUp] = 1000 // pre-existing, unattributable
|
||||
|
||||
// Poll 1 establishes the baseline: nothing is posted, the baseline
|
||||
// file appears with the raw counter.
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("baseline poll: %v", err)
|
||||
}
|
||||
if master.posts != 0 {
|
||||
t.Fatalf("baseline poll posted %d times, want 0", master.posts)
|
||||
}
|
||||
values, err := loadTrafficBaseline(a.baselinePath())
|
||||
if err != nil || values[aliceUp] != 1000 {
|
||||
t.Fatalf("persisted baseline = %v (err %v), want alice at 1000", values, err)
|
||||
}
|
||||
|
||||
// Outage: two polls accrue traffic but the master 500s.
|
||||
fake.counters[aliceUp] = 1400
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err == nil {
|
||||
t.Fatal("poll during outage returned nil, want the post error")
|
||||
}
|
||||
fake.counters[aliceUp] = 2000
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err == nil {
|
||||
t.Fatal("second outage poll returned nil, want the post error")
|
||||
}
|
||||
// The failures advanced nothing: the persisted baseline still holds
|
||||
// the last acknowledged counters.
|
||||
values, err = loadTrafficBaseline(a.baselinePath())
|
||||
if err != nil || values[aliceUp] != 1000 {
|
||||
t.Fatalf("baseline after failed polls = %v (err %v), want still 1000", values, err)
|
||||
}
|
||||
|
||||
// Recovery: one post carries the entire outage window (2000-1000).
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("recovery poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 1000 {
|
||||
t.Fatalf("master received %d for alice after recovery, want exactly 1000", got)
|
||||
}
|
||||
|
||||
// Steady state resumes: only fresh growth is reported.
|
||||
fake.counters[aliceUp] = 2100
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("steady poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 1100 {
|
||||
t.Fatalf("master total = %d, want 1100 (= 2100 counted - 1000 baseline)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAgentRestartRestoresBaselineFromDisk: a new agent process (fresh
|
||||
// in-memory state everywhere) over the same baseline file must pick up
|
||||
// diffing exactly where the old one stopped — the traffic that accrued
|
||||
// while it was down is reported by its FIRST poll, and nothing is counted
|
||||
// twice.
|
||||
func TestAgentRestartRestoresBaselineFromDisk(t *testing.T) {
|
||||
master := newFakeMaster(t, 0)
|
||||
baselinePath := filepath.Join(t.TempDir(), "agent-traffic-baseline.json")
|
||||
newAgent := func() *Agent {
|
||||
return New(&Config{MasterURL: master.srv.URL, Token: "t", NodeName: "n", BaselinePath: baselinePath})
|
||||
}
|
||||
|
||||
a1 := newAgent()
|
||||
fake := newFakeStatsClient()
|
||||
fake.counters[aliceUp] = 1000
|
||||
if err := a1.reportTrafficWith(fake, a1.postTrafficReport); err != nil {
|
||||
t.Fatalf("a1 baseline poll: %v", err)
|
||||
}
|
||||
fake.counters[aliceUp] = 1500
|
||||
if err := a1.reportTrafficWith(fake, a1.postTrafficReport); err != nil {
|
||||
t.Fatalf("a1 report poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 500 {
|
||||
t.Fatalf("master received %d before restart, want 500", got)
|
||||
}
|
||||
|
||||
// "Restart": brand-new agent AND a brand-new stats client (its
|
||||
// in-memory committed baseline is gone); only the file survives. The
|
||||
// core kept counting meanwhile.
|
||||
a2 := newAgent()
|
||||
fake2 := newFakeStatsClient()
|
||||
fake2.counters[aliceUp] = 1900
|
||||
if err := a2.reportTrafficWith(fake2, a2.postTrafficReport); err != nil {
|
||||
t.Fatalf("a2 first poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 900 {
|
||||
t.Fatalf("master total = %d, want 900 (500 + the 400 accrued across the restart)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCounterResetAfterCoreRestart: the core restarts while the agent
|
||||
// keeps running; its counters fall below the committed baseline. The
|
||||
// report must carry the new counter as fresh traffic — never a negative
|
||||
// delta — and then resume normal diffing in the new epoch.
|
||||
func TestCounterResetAfterCoreRestart(t *testing.T) {
|
||||
master := newFakeMaster(t, 0)
|
||||
a := newTestAgent(t, master.srv.URL)
|
||||
fake := newFakeStatsClient()
|
||||
fake.counters[aliceUp] = 5000
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("baseline poll: %v", err)
|
||||
}
|
||||
|
||||
fake.counters[aliceUp] = 300 // core restarted; counters began again
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("post-reset poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 300 {
|
||||
t.Fatalf("master received %d after core restart, want the fresh 300", got)
|
||||
}
|
||||
|
||||
fake.counters[aliceUp] = 450
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("new-epoch poll: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 450 {
|
||||
t.Fatalf("master total = %d, want 450 (300 + 150 of new-epoch growth)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCorruptBaselineFileTolerated: a torn or foreign baseline file must
|
||||
// not wedge the agent — it logs and falls back to a baseline-only first
|
||||
// snapshot, then reports normally.
|
||||
func TestCorruptBaselineFileTolerated(t *testing.T) {
|
||||
master := newFakeMaster(t, 0)
|
||||
a := newTestAgent(t, master.srv.URL)
|
||||
if err := os.WriteFile(a.baselinePath(), []byte("{not json"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fake := newFakeStatsClient()
|
||||
fake.counters[aliceUp] = 800
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("poll with corrupt baseline: %v", err)
|
||||
}
|
||||
if master.posts != 0 {
|
||||
t.Fatalf("corrupt-baseline first poll posted %d times, want baseline-only (0)", master.posts)
|
||||
}
|
||||
fake.counters[aliceUp] = 900
|
||||
if err := a.reportTrafficWith(fake, a.postTrafficReport); err != nil {
|
||||
t.Fatalf("poll after corrupt baseline: %v", err)
|
||||
}
|
||||
if got := master.userUp["alice"]; got != 100 {
|
||||
t.Fatalf("master received %d, want only the post-baseline 100", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveLoadBaselineRoundTrip pins the persistence helpers: atomic save,
|
||||
// version field, exact values back.
|
||||
func TestSaveLoadBaselineRoundTrip(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "baseline.json")
|
||||
want := map[string]int64{aliceUp: 42, "inbound>>>in-443>>>traffic>>>downlink": 7}
|
||||
if err := saveTrafficBaseline(path, want); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
got, err := loadTrafficBaseline(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if len(got) != len(want) || got[aliceUp] != 42 || got["inbound>>>in-443>>>traffic>>>downlink"] != 7 {
|
||||
t.Fatalf("round trip = %v, want %v", got, want)
|
||||
}
|
||||
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Fatal("temp file left behind after atomic save")
|
||||
}
|
||||
if _, err := loadTrafficBaseline(filepath.Join(t.TempDir(), "missing.json")); !os.IsNotExist(err) {
|
||||
t.Fatalf("missing file error = %v, want a not-exist error", err)
|
||||
}
|
||||
var parseErr error
|
||||
_, parseErr = loadTrafficBaseline(path)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("valid file failed to load: %v", parseErr)
|
||||
}
|
||||
bad := filepath.Join(t.TempDir(), "bad.json")
|
||||
_ = os.WriteFile(bad, []byte("[]"), 0o600)
|
||||
if _, err := loadTrafficBaseline(bad); err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("corrupt file error = %v, want a parse error", err)
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,6 @@ var monitorScopeAllow = map[string]struct{}{
|
||||
"/server/xrayMetricsHistory/:metric/:bucket": {},
|
||||
"/server/xrayObservatory": {},
|
||||
"/server/xrayObservatoryHistory/:tag/:bucket": {},
|
||||
"/server/getXrayVersion": {},
|
||||
"/server/getPanelUpdateInfo": {},
|
||||
"/nodes/history/:id/:metric/:bucket": {},
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/xrayMetricsHistory/:metric/:bucket", a.getXrayMetricsHistoryBucket)
|
||||
g.GET("/xrayObservatory", a.getXrayObservatory)
|
||||
g.GET("/xrayObservatoryHistory/:tag/:bucket", a.getXrayObservatoryHistoryBucket)
|
||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/getPanelUpdateInfo", a.getPanelUpdateInfo)
|
||||
g.GET("/getUpdateStatus", a.getUpdateStatus)
|
||||
g.GET("/getConfigJson", a.getConfigJson)
|
||||
@@ -67,11 +66,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
g.POST("/updatePanel", a.updatePanel)
|
||||
g.POST("/setUpdateChannel", a.setUpdateChannel)
|
||||
g.POST("/updateGeofile", a.updateGeofile)
|
||||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||
g.POST("/amneziawglogs/:count", a.getAmneziaWGLogs)
|
||||
@@ -182,15 +178,6 @@ func (a *ServerController) getXrayObservatoryHistoryBucket(c *gin.Context) {
|
||||
jsonObj(c, a.xrayMetricsService.AggregateObservatory(tag, bucket, 60), nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
versions, err := a.serverService.GetXrayVersionsCached()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "getVersion"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, versions, nil)
|
||||
}
|
||||
|
||||
// getPanelUpdateInfo retrieves the current and latest panel version.
|
||||
func (a *ServerController) getPanelUpdateInfo(c *gin.Context) {
|
||||
info, err := a.panelService.GetUpdateInfo()
|
||||
@@ -202,13 +189,6 @@ func (a *ServerController) getPanelUpdateInfo(c *gin.Context) {
|
||||
jsonObj(c, info, nil)
|
||||
}
|
||||
|
||||
// installXray installs or updates Xray to the specified version.
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("version")
|
||||
err := a.serverService.UpdateXray(version)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.xraySwitchVersionPopover"), err)
|
||||
}
|
||||
|
||||
// updatePanel starts a panel self-update. With no "dev" form value it follows
|
||||
// this panel's own channel setting; an explicit "dev" (sent by the master node
|
||||
// updater) overrides it for this run. The response's runId identifies this
|
||||
@@ -252,20 +232,6 @@ func (a *ServerController) setUpdateChannel(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.updateChannelChanged"), err)
|
||||
}
|
||||
|
||||
// updateGeofile updates the specified geo file for Xray.
|
||||
func (a *ServerController) updateGeofile(c *gin.Context) {
|
||||
fileName := c.Param("fileName")
|
||||
|
||||
if fileName != "" && !a.serverService.IsValidGeofileName(fileName) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"),
|
||||
fmt.Errorf("invalid filename: contains unsafe characters or path traversal patterns"))
|
||||
return
|
||||
}
|
||||
|
||||
err := a.serverService.UpdateGeofile(fileName)
|
||||
jsonMsg(c, I18nWeb(c, "pages.index.geofileUpdatePopover"), err)
|
||||
}
|
||||
|
||||
// stopXrayService stops the Xray service.
|
||||
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||
err := a.serverService.StopXrayService()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
@@ -161,23 +160,12 @@ type ServerService struct {
|
||||
lastStatus *Status
|
||||
|
||||
versionsCacheMu sync.Mutex
|
||||
versionsCache *cachedXrayVersions
|
||||
|
||||
fail2banMu sync.Mutex
|
||||
fail2banInstalled bool
|
||||
fail2banCheckedAt time.Time
|
||||
}
|
||||
|
||||
type cachedXrayVersions struct {
|
||||
versions []string
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
// xrayVersionsCacheTTL bounds how often /getXrayVersion hits GitHub. The list
|
||||
// is purely informational (rendered in the "switch Xray version" picker) so a
|
||||
// quarter-hour staleness window is fine and saves the API budget.
|
||||
const xrayVersionsCacheTTL = 15 * time.Minute
|
||||
|
||||
// allowedHistoryBuckets is the bucket-second whitelist for time-series
|
||||
// aggregation endpoints (server + node metrics). Restricting it prevents
|
||||
// callers from triggering arbitrary aggregation work and keeps the
|
||||
@@ -272,31 +260,6 @@ func (s *ServerService) RefreshStatus() *Status {
|
||||
return next
|
||||
}
|
||||
|
||||
// GetXrayVersionsCached wraps GetXrayVersions with a TTL cache. On fetch
|
||||
// failure we serve the last successful list (if any) so the UI doesn't go
|
||||
// blank during a GitHub API hiccup; if there's no cache at all the underlying
|
||||
// error is surfaced.
|
||||
func (s *ServerService) GetXrayVersionsCached() ([]string, error) {
|
||||
s.versionsCacheMu.Lock()
|
||||
cache := s.versionsCache
|
||||
s.versionsCacheMu.Unlock()
|
||||
if cache != nil && time.Since(cache.fetchedAt) <= xrayVersionsCacheTTL {
|
||||
return cache.versions, nil
|
||||
}
|
||||
versions, err := s.GetXrayVersions()
|
||||
if err != nil {
|
||||
if cache != nil {
|
||||
logger.Warning("GetXrayVersionsCached: serving stale list:", err)
|
||||
return cache.versions, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
s.versionsCacheMu.Lock()
|
||||
s.versionsCache = &cachedXrayVersions{versions: versions, fetchedAt: time.Now()}
|
||||
s.versionsCacheMu.Unlock()
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// GetDefaultLogOutboundTags scans the default Xray config for freedom and
|
||||
// blackhole outbound tags so /getXrayLogs can colour-code log lines without
|
||||
// the controller re-doing the JSON walk. Falls back to the historical
|
||||
@@ -789,75 +752,6 @@ func (s *ServerService) sampleCPUUtilization() (float64, error) {
|
||||
return s.emaCPU, nil
|
||||
}
|
||||
|
||||
const (
|
||||
maxXrayArchiveBytes = 200 << 20
|
||||
maxXrayBinaryBytes = 200 << 20
|
||||
// maxXrayDigestBytes caps the .dgst checksum sidecar read; it is a few
|
||||
// hundred bytes in practice.
|
||||
maxXrayDigestBytes = 64 << 10
|
||||
)
|
||||
|
||||
func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
const (
|
||||
XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
|
||||
bufferSize = 8192
|
||||
)
|
||||
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, XrayURL, nil)
|
||||
if reqErr != nil {
|
||||
return nil, reqErr
|
||||
}
|
||||
resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check HTTP status code - GitHub API returns object instead of array on error
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
var errorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
|
||||
return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||
buffer.Reset()
|
||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var releases []Release
|
||||
if err := json.Unmarshal(buffer.Bytes(), &releases); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var versions []string
|
||||
for _, release := range releases {
|
||||
tagVersion := strings.TrimPrefix(release.TagName, "v")
|
||||
tagParts := strings.Split(tagVersion, ".")
|
||||
if len(tagParts) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
major, err1 := strconv.Atoi(tagParts[0])
|
||||
minor, err2 := strconv.Atoi(tagParts[1])
|
||||
patch, err3 := strconv.Atoi(tagParts[2])
|
||||
if err1 != nil || err2 != nil || err3 != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if major > 26 || (major == 26 && minor > 6) || (major == 26 && minor == 6 && patch >= 27) {
|
||||
versions = append(versions, release.TagName)
|
||||
}
|
||||
}
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) StopXrayService() error {
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
@@ -876,240 +770,6 @@ func (s *ServerService) RestartXrayService() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
osName := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
|
||||
switch osName {
|
||||
case "darwin":
|
||||
osName = "macos"
|
||||
case "windows":
|
||||
osName = "windows"
|
||||
}
|
||||
|
||||
switch arch {
|
||||
case "amd64":
|
||||
arch = "64"
|
||||
case "arm64":
|
||||
arch = "arm64-v8a"
|
||||
case "armv7":
|
||||
arch = "arm32-v7a"
|
||||
case "armv6":
|
||||
arch = "arm32-v6"
|
||||
case "armv5":
|
||||
arch = "arm32-v5"
|
||||
case "386":
|
||||
arch = "32"
|
||||
case "s390x":
|
||||
arch = "s390x"
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
|
||||
url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
|
||||
client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if reqErr != nil {
|
||||
return "", reqErr
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download xray: unexpected HTTP %d", resp.StatusCode)
|
||||
}
|
||||
if resp.ContentLength > maxXrayArchiveBytes {
|
||||
return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
|
||||
}
|
||||
|
||||
file, err := os.CreateTemp("", "xray-*.zip")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := file.Name()
|
||||
ok := false
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(path)
|
||||
}
|
||||
}()
|
||||
|
||||
n, err := io.Copy(file, io.LimitReader(resp.Body, maxXrayArchiveBytes+1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n > maxXrayArchiveBytes {
|
||||
return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
|
||||
}
|
||||
|
||||
// Verify the archive against the SHA2-256 published in the release's .dgst
|
||||
// sidecar before installing it. TLS protects the transport, not the artifact;
|
||||
// a corrupted or tampered asset must not be installed and run as xray.
|
||||
want, err := s.fetchXrayDigestSHA256(client, url+".dgst")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if got := hex.EncodeToString(hasher.Sum(nil)); !strings.EqualFold(got, want) {
|
||||
// User-facing warning: the archive's SHA-256 does not match the official
|
||||
// release checksum, so the download is corrupted or has been tampered
|
||||
// with. Abort the install so a bad binary is never run, and tell the user
|
||||
// to retry/re-download rather than proceed with a mismatched image.
|
||||
return "", fmt.Errorf("Xray update aborted: the downloaded archive does not match the official SHA-256 checksum, so the image is corrupted or differs from the official release. Please exit and re-download the official image, then try again (expected %s, got %s)", want, got)
|
||||
}
|
||||
|
||||
ok = true
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each
|
||||
// release asset and returns the SHA2-256 hex digest it lists.
|
||||
func (s *ServerService) fetchXrayDigestSHA256(client *http.Client, dgstURL string) (string, error) {
|
||||
req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, dgstURL, nil)
|
||||
if reqErr != nil {
|
||||
return "", fmt.Errorf("download xray checksum: %w", reqErr)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download xray checksum: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download xray checksum: unexpected HTTP %d", resp.StatusCode)
|
||||
}
|
||||
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxXrayDigestBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download xray checksum: %w", err)
|
||||
}
|
||||
return parseXrayDigestSHA256(raw)
|
||||
}
|
||||
|
||||
// parseXrayDigestSHA256 extracts the lowercase SHA2-256 hex from an XTLS .dgst
|
||||
// file, whose lines are "ALGO= <hex>" (the relevant one being "SHA2-256= ...").
|
||||
func parseXrayDigestSHA256(dgst []byte) (string, error) {
|
||||
for line := range strings.SplitSeq(string(dgst), "\n") {
|
||||
rest, ok := strings.CutPrefix(strings.TrimSpace(line), "SHA2-256=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
h := strings.ToLower(strings.TrimSpace(rest))
|
||||
if len(h) != 64 {
|
||||
return "", fmt.Errorf("xray checksum: malformed SHA2-256 entry in digest")
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
return "", fmt.Errorf("xray checksum: no SHA2-256 entry in digest")
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateXray(version string) error {
|
||||
versions, err := s.GetXrayVersions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !slices.Contains(versions, version) {
|
||||
return fmt.Errorf("xray version %q is not in the fetched release list", version)
|
||||
}
|
||||
|
||||
// 1. Stop xray before doing anything
|
||||
if err := s.StopXrayService(); err != nil {
|
||||
logger.Warning("failed to stop xray before update:", err)
|
||||
}
|
||||
|
||||
// 2. Download the zip
|
||||
zipFileName, err := s.downloadXRay(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(zipFileName)
|
||||
|
||||
zipFile, err := os.Open(zipFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
stat, err := zipFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reader, err := zip.NewReader(zipFile, stat.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. Helper to extract files
|
||||
copyZipFile := func(zipName string, fileName string) error {
|
||||
zipFile, err := reader.Open(zipName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
if err := os.MkdirAll(filepath.Dir(fileName), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
ok := false
|
||||
defer func() {
|
||||
_ = tmpFile.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(tmpPath)
|
||||
}
|
||||
}()
|
||||
n, err := io.Copy(tmpFile, io.LimitReader(zipFile, maxXrayBinaryBytes+1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n > maxXrayBinaryBytes {
|
||||
return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
|
||||
}
|
||||
if err := tmpFile.Chmod(0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
_ = os.Remove(fileName)
|
||||
}
|
||||
if err := os.Rename(tmpPath, fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. Extract correct binary
|
||||
if runtime.GOOS == "windows" {
|
||||
targetBinary := filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
|
||||
err = copyZipFile("xray.exe", targetBinary)
|
||||
} else {
|
||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Restart xray
|
||||
if err := s.xrayService.RestartXray(true); err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
|
||||
c, _ := strconv.Atoi(count)
|
||||
var lines []string
|
||||
@@ -2141,155 +1801,6 @@ func stageSQLiteUpload(file multipart.File, kind int, tempPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValidGeofileName validates that the filename is safe for geofile operations.
|
||||
// It checks for path traversal attempts and ensures the filename contains only safe characters.
|
||||
func (s *ServerService) IsValidGeofileName(filename string) bool {
|
||||
if filename == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if strings.Contains(filename, "..") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for path separators (both forward and backward slash)
|
||||
if strings.ContainsAny(filename, `/\`) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for absolute path indicators
|
||||
if filepath.IsAbs(filename) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional security: only allow alphanumeric, dots, underscores, and hyphens
|
||||
// This is stricter than the general filename regex
|
||||
validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
|
||||
matched, _ := regexp.MatchString(validGeofilePattern, filename)
|
||||
return matched
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateGeofile(fileName string) error {
|
||||
type geofileEntry struct {
|
||||
URL string
|
||||
FileName string
|
||||
}
|
||||
geofileAllowlist := map[string]geofileEntry{
|
||||
"geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
|
||||
"geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
|
||||
"geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
|
||||
"geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
|
||||
"geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"},
|
||||
"geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
|
||||
}
|
||||
|
||||
// Strict allowlist check to avoid writing uncontrolled files
|
||||
if fileName != "" {
|
||||
if _, ok := geofileAllowlist[fileName]; !ok {
|
||||
return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName)
|
||||
}
|
||||
}
|
||||
|
||||
client := s.settingService.NewProxiedHTTPClient(0)
|
||||
|
||||
downloadFile := func(url, destPath string) error {
|
||||
var req *http.Request
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
|
||||
}
|
||||
|
||||
var localFileModTime time.Time
|
||||
if fileInfo, err := os.Stat(destPath); err == nil {
|
||||
localFileModTime = fileInfo.ModTime()
|
||||
if !localFileModTime.IsZero() {
|
||||
req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Parse Last-Modified header from server
|
||||
var serverModTime time.Time
|
||||
serverModTimeStr := resp.Header.Get("Last-Modified")
|
||||
if serverModTimeStr != "" {
|
||||
parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
|
||||
} else {
|
||||
serverModTime = parsedTime
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update local file's modification time
|
||||
updateFileModTime := func() {
|
||||
if !serverModTime.IsZero() {
|
||||
if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
|
||||
logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 304 Not Modified
|
||||
if resp.StatusCode == http.StatusNotModified {
|
||||
updateFileModTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
file, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
|
||||
}
|
||||
|
||||
updateFileModTime()
|
||||
return nil
|
||||
}
|
||||
|
||||
var errorMessages []string
|
||||
|
||||
if fileName == "" {
|
||||
// Download all geofiles
|
||||
for _, entry := range geofileAllowlist {
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entry := geofileAllowlist[fileName]
|
||||
destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
|
||||
if err := downloadFile(entry.URL, destPath); err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
|
||||
}
|
||||
}
|
||||
|
||||
err := s.RestartXrayService()
|
||||
if err != nil {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
|
||||
}
|
||||
|
||||
if len(errorMessages) > 0 {
|
||||
return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseXrayKeyPairOutput reads the two-line "Label: value" output that xray's
|
||||
// key-generation subcommands (x25519, mldsa65, mlkem768) print and returns the
|
||||
// two values. Short or label-less output yields an error instead of panicking
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A real XTLS .dgst sidecar (Xray-linux-64.zip.dgst, v26.3.27): lines are
|
||||
// "ALGO= <hex>", and the algorithm label is "SHA2-256", not "SHA256".
|
||||
const sampleXrayDgst = `# Hash Values
|
||||
|
||||
MD5= ee4e2ff74948a9b464624b1cabc44409
|
||||
SHA1= b55b06e74e89083b9cedfdecf0d68b579cd2af72
|
||||
SHA2-256= 23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae
|
||||
SHA2-512= e8bc40a0687cac184bbe4b5c1f047e69064ccedc489fb25e208889ae287bbf8736dff16b108d68fc00dc33edc8bb53502e47a9698a277f4f51b67b83d899e518
|
||||
`
|
||||
|
||||
const wantSHA = "23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae"
|
||||
|
||||
func TestParseXrayDigestSHA256(t *testing.T) {
|
||||
got, err := parseXrayDigestSHA256([]byte(sampleXrayDgst))
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if got != wantSHA {
|
||||
t.Fatalf("sha = %q, want %q", got, wantSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXrayDigestSHA256_Errors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in string
|
||||
}{
|
||||
{"no-sha256-line", "MD5= abc\nSHA1= def\n"},
|
||||
{"malformed-short", "SHA2-256= deadbeef\n"},
|
||||
{"empty", ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := parseXrayDigestSHA256([]byte(tc.in)); err == nil {
|
||||
t.Fatalf("%s: expected an error", tc.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchXrayDigestSHA256(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(sampleXrayDgst))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
got, err := (&ServerService{}).fetchXrayDigestSHA256(srv.Client(), srv.URL+"/Xray-linux-64.zip.dgst")
|
||||
if err != nil {
|
||||
t.Fatalf("fetch: %v", err)
|
||||
}
|
||||
if got != wantSHA {
|
||||
t.Fatalf("sha = %q, want %q", got, wantSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchXrayDigestSHA256_HTTPError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "nope", http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
if _, err := (&ServerService{}).fetchXrayDigestSHA256(srv.Client(), srv.URL+"/missing.dgst"); err == nil {
|
||||
t.Fatal("expected an error on HTTP 404")
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,27 @@ var (
|
||||
isNeedXrayRestart atomic.Bool // Indicates that restart was requested for Xray
|
||||
isManuallyStopped atomic.Bool // Indicates that Xray was stopped manually from the panel
|
||||
xrayState xrayLifecycle
|
||||
|
||||
// localCoreDisabledOnce ensures the "local core disabled" notice is
|
||||
// logged exactly once per panel run, no matter how many legacy callers
|
||||
// still ask for a local restart.
|
||||
localCoreDisabledOnce sync.Once
|
||||
)
|
||||
|
||||
// logLocalCoreDisabled records, once, that local-xray lifecycle requests are
|
||||
// being ignored. The lite master is a pure control plane: it never runs a
|
||||
// local xray process (all inbounds live on agent nodes), so restarts are
|
||||
// no-ops instead of spawn attempts that would fail every few seconds on
|
||||
// hosts where bin/xray is absent or deliberately disabled.
|
||||
func logLocalCoreDisabled() {
|
||||
localCoreDisabledOnce.Do(func() {
|
||||
// Warning level on purpose: the panel's default log level hides
|
||||
// Info, and this one-time notice is the only sign that legacy
|
||||
// restart requests are being absorbed.
|
||||
logger.Warning("local xray core is disabled in this build (control-plane only); local restart requests are ignored")
|
||||
})
|
||||
}
|
||||
|
||||
type xrayLifecycle struct {
|
||||
mu sync.RWMutex
|
||||
process *xray.Process
|
||||
@@ -1421,6 +1440,18 @@ func (s *XrayService) TestRoute(req xray.RouteTestRequest) (*xray.RouteTestResul
|
||||
// routing rules/balancers are hot-reloadable); only changes the core cannot
|
||||
// take at runtime — or a force request — stop and restart the process.
|
||||
func (s *XrayService) RestartXray(isForce bool) error {
|
||||
// Lite control-plane build: the local xray process is never started.
|
||||
// Every historical caller (startup, crash watchdog, pending-restart
|
||||
// ticker, settings-save hooks, server endpoints) funnels through here,
|
||||
// so degrading this single point retires the whole local lifecycle
|
||||
// without touching agent-node flows (the agent runs its own core via
|
||||
// internal/agent, not via this service).
|
||||
_ = isForce
|
||||
logLocalCoreDisabled()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *XrayService) restartXrayLegacy(isForce bool) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
logger.Debug("restart Xray, force:", isForce)
|
||||
@@ -1600,7 +1631,10 @@ func (s *XrayService) StopXray() error {
|
||||
if process != nil && process.IsRunning() {
|
||||
return process.Stop()
|
||||
}
|
||||
return errors.New("xray is not running")
|
||||
// Lite control-plane build: no local process ever runs, so stopping is
|
||||
// an idempotent no-op rather than an error.
|
||||
logLocalCoreDisabled()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetToNeedRestart marks that Xray needs to be restarted.
|
||||
@@ -1625,17 +1659,14 @@ func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||
return isNeedXrayRestart.CompareAndSwap(true, false)
|
||||
}
|
||||
|
||||
// ApplyPendingRestart consumes the need-restart flag and restarts Xray. If the
|
||||
// restart fails (for example GetXrayConfig hits a transient DB error and leaves
|
||||
// the old process running), it re-arms the flag so the next tick retries instead
|
||||
// of silently dropping the pending config change.
|
||||
// ApplyPendingRestart used to consume the need-restart flag and restart
|
||||
// the local core, re-arming the flag on failure so the next tick retried.
|
||||
// The local core never runs in this build, so a pending restart is simply
|
||||
// dropped: clear the flag and do nothing. (Its only caller, the 30s cron
|
||||
// tick in web.go, has been removed as well.)
|
||||
func (s *XrayService) ApplyPendingRestart() {
|
||||
if !s.IsNeedRestartAndSetFalse() {
|
||||
return
|
||||
}
|
||||
if err := s.RestartXray(false); err != nil {
|
||||
logger.Error("restart xray failed:", err)
|
||||
s.SetToNeedRestart()
|
||||
if s.IsNeedRestartAndSetFalse() {
|
||||
logLocalCoreDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,14 @@ type rawObsEntry struct {
|
||||
}
|
||||
|
||||
func (s *XrayMetricsService) Sample(t time.Time) {
|
||||
// The local xray core never runs in this build (control-plane only),
|
||||
// so there is no /debug/vars endpoint to sample. Report the metrics
|
||||
// feed as disabled and stay silent instead of probing a permanently
|
||||
// dead endpoint on every 2s status tick.
|
||||
s.setStateQuiet(xrayMetricsState{Enabled: false, Reason: "local xray core disabled in this build"})
|
||||
}
|
||||
|
||||
func (s *XrayMetricsService) sampleLocalXrayLegacy(t time.Time) {
|
||||
listen, err := s.discoverListen()
|
||||
if err != nil {
|
||||
s.setState(xrayMetricsState{Reason: err.Error()})
|
||||
@@ -312,3 +320,11 @@ func (s *XrayMetricsService) setState(st xrayMetricsState) {
|
||||
logger.Debugf("xray metrics unavailable: %s", st.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
// setStateQuiet updates the metrics state without the Debug log line, for
|
||||
// states that are permanent by design rather than a transient failure.
|
||||
func (s *XrayMetricsService) setStateQuiet(st xrayMetricsState) {
|
||||
s.mu.Lock()
|
||||
s.state = st
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ func TestRestartXrayRespectsManualStop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPendingRestartReArmsFlagOnFailure(t *testing.T) {
|
||||
// The local core never runs in this build, so ApplyPendingRestart no
|
||||
// longer restarts anything and must not re-arm the flag for a retry loop:
|
||||
// it consumes the pending flag and drops it.
|
||||
func TestApplyPendingRestartDropsFlagWithoutLocalCore(t *testing.T) {
|
||||
setupSettingTestDB(t)
|
||||
if err := (&SettingService{}).saveSetting("xrayTemplateConfig", "{ not valid json"); err != nil {
|
||||
t.Fatalf("seed template: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
isManuallyStopped.Store(false)
|
||||
isNeedXrayRestart.Store(false)
|
||||
@@ -34,7 +34,7 @@ func TestApplyPendingRestartReArmsFlagOnFailure(t *testing.T) {
|
||||
svc.SetToNeedRestart()
|
||||
svc.ApplyPendingRestart()
|
||||
|
||||
if !isNeedXrayRestart.Load() {
|
||||
t.Fatal("a failed restart must re-arm the need-restart flag so the pending config change is retried")
|
||||
if isNeedXrayRestart.Load() {
|
||||
t.Fatal("pending restart must be consumed and dropped; there is no local core to retry against")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "تم بنجاح",
|
||||
"lastOnline": "آخر متصل",
|
||||
"lastSubFetch": "آخر جلب للاشتراك",
|
||||
"getVersion": "جيب النسخة",
|
||||
"install": "تثبيت",
|
||||
"clients": "عملاء",
|
||||
"usage": "استخدام",
|
||||
"twoFactorCode": "الكود",
|
||||
"remained": "المتبقي",
|
||||
"security": "أمان",
|
||||
"emptyDnsDesc": "مفيش سيرفر DNS مضاف.",
|
||||
"emptyFakeDnsDesc": "مفيش سيرفر Fake DNS مضاف.",
|
||||
"emptyBalancersDesc": "مفيش موازن تحميل مضاف.",
|
||||
"somethingWentWrong": "حدث خطأ ما",
|
||||
"subscription": {
|
||||
"title": "معلومات الاشتراك",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "الرفع",
|
||||
"expiry": "تاريخ الانتهاء",
|
||||
"totalQuota": "الحصة الإجمالية",
|
||||
"individualLinks": "روابط فردية",
|
||||
"active": "نشط",
|
||||
"inactive": "غير نشط",
|
||||
"unlimited": "غير محدود",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "إيقاف",
|
||||
"restartXray": "إعادة تشغيل",
|
||||
"xraySwitch": "النسخة",
|
||||
"xrayUpdates": "تحديثات Xray",
|
||||
"xraySwitchClickDesk": "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية.",
|
||||
"updatePanel": "تحديث البانل",
|
||||
"panelUpdateDesc": "ده هيحدث 3X-UI لآخر إصدار وهيعيد تشغيل خدمة البانل.",
|
||||
"currentPanelVersion": "إصدار البانل الحالي",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "تنزيل",
|
||||
"sent": "مرسل",
|
||||
"received": "مستقبل",
|
||||
"xraySwitchVersionDialog": "هل تريد حقًا تغيير إصدار Xray؟",
|
||||
"xraySwitchVersionDialogDesc": "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#.",
|
||||
"xraySwitchVersionPopover": "تم تحديث Xray بنجاح",
|
||||
"panelUpdateDialog": "هل فعلاً عايز تحدث البانل؟",
|
||||
"panelUpdateDialogDesc": "ده هيحدث 3X-UI للإصدار #version# وهيعيد تشغيل البانل.",
|
||||
"panelUpdateStartedPopover": "بدأ تحديث البانل",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "لم يكتمل التحديث بنجاح. تحقق من سجلات الخادم، أو نفّذ الأمر «x-ui update» من سطر الأوامر.",
|
||||
"panelUpdateUnknownTitle": "تعذّر التأكد من اكتمال التحديث",
|
||||
"panelUpdateUnknownDesc": "لم تُبلغ اللوحة بنتيجة في الوقت المحدد. أعد تحميل الصفحة للتحقق من الإصدار الحالي، أو تحقق من سجلات الخادم.",
|
||||
"geofileUpdateDialog": "هل تريد حقًا تحديث ملف الجغرافيا؟",
|
||||
"geofileUpdateDialogDesc": "سيؤدي هذا إلى تحديث ملف #filename#.",
|
||||
"geofilesUpdateDialogDesc": "سيؤدي هذا إلى تحديث كافة الملفات.",
|
||||
"geofilesUpdateAll": "تحديث الكل",
|
||||
"geofileUpdatePopover": "تم تحديث ملف الجغرافيا بنجاح",
|
||||
"geodataTitle": "التحديث التلقائي لـ Geodata",
|
||||
"geodataHint": "يقوم Xray بتنزيل هذه الملفات حسب الجدول الزمني وإعادة تحميلها دون إعادة تشغيل. يجب أن تكون الروابط HTTPS. يجب أن يكون كل ملف موجوداً مسبقاً في مجلد bin حتى يتمكن Xray من تحديثه.",
|
||||
"geodataCron": "الجدولة (cron)",
|
||||
"geodataOutbound": "التنزيل عبر outbound (اختياري)",
|
||||
"geodataFile": "اسم الملف",
|
||||
"geodataAddFile": "إضافة ملف",
|
||||
"geodataSaveRestart": "حفظ وإعادة تشغيل Xray",
|
||||
"geodataConfirmTitle": "حفظ إعدادات geodata؟",
|
||||
"geodataConfirmContent": "سيتم تحديث قالب إعدادات Xray وإعادة تشغيل Xray.",
|
||||
"geodataInvalidUrl": "كل ملف يحتاج إلى رابط HTTPS.",
|
||||
"geodataInvalidFile": "يجب أن يكون اسم الملف اسماً بسيطاً مثل geosite_custom.dat (بدون مسارات).",
|
||||
"geodataInvalidCron": "يجب أن يتكون cron من 5 حقول، مثل 0 4 * * *",
|
||||
"geodataEmpty": "لا توجد ملفات مهيأة. في قواعد التوجيه يُشار إلى الملفات بالشكل ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "التثبيت شغال، متعملش Refresh للصفحة",
|
||||
"logs": "السجلات",
|
||||
"accessLogs": "سجلات الوصول",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "المسار الأساسي الافتراضي \"/\" معروف — غيّره إلى مسار عشوائي.",
|
||||
"warnDefaultSubPath": "مسار الاشتراك الافتراضي \"/sub/\" معروف — قم بتغييره.",
|
||||
"warnDefaultJsonPath": "مسار اشتراك JSON الافتراضي \"/json/\" معروف — قم بتغييره.",
|
||||
"TGBotSettings": "بوت تيليجرام",
|
||||
"panelListeningIP": "IP الاستماع",
|
||||
"panelListeningIPDesc": "عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)",
|
||||
"panelListeningDomain": "دومين الاستماع",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "الباسورد الحالي",
|
||||
"newUsername": "اسم المستخدم الجديد",
|
||||
"newPassword": "الباسورد الجديد",
|
||||
"telegramBotEnable": "تفعيل بوت Telegram",
|
||||
"telegramBotEnableDesc": "يفعل بوت Telegram.",
|
||||
"telegramToken": "رمز تيليجرام",
|
||||
"telegramTokenDesc": "توكن البوت اللي جبت من '{'@'}BotFather'.",
|
||||
"telegramProxy": "وكيل SOCKS",
|
||||
"telegramProxyDesc": "يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)",
|
||||
"telegramAPIServer": "خادم API لتيليجرام",
|
||||
"telegramAPIServerDesc": "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي.",
|
||||
"telegramChatId": "ID شات الأدمن",
|
||||
"telegramChatIdDesc": "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من {'@'}userinfobot) أو (استخدم '/id' في البوت)",
|
||||
"telegramNotifyTime": "وقت الإشعار",
|
||||
"telegramNotifyTimeDesc": "عدد مرات إرسال البوت للتقارير الدورية. اختر فترة جاهزة، أو اختر «مخصص» لإدخال تعبير crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — التكرار ضمن فترة",
|
||||
"hourly": "@hourly — كل ساعة",
|
||||
"daily": "@daily — كل يوم الساعة 00:00",
|
||||
"weekly": "@weekly — كل أسبوع",
|
||||
"monthly": "@monthly — كل شهر",
|
||||
"custom": "مخصص (crontab)",
|
||||
"seconds": "ثوانٍ",
|
||||
"minutes": "دقائق",
|
||||
"hours": "ساعات",
|
||||
"interval": "الفاصل الزمني",
|
||||
"unit": "الوحدة"
|
||||
},
|
||||
"tgNotifyBackup": "نسخة احتياطية لقاعدة البيانات",
|
||||
"tgNotifyBackupDesc": "ابعت ملف النسخة الاحتياطية لقاعدة البيانات مع التقرير.",
|
||||
"tgNotifyLogin": "إشعار بتسجيل الدخول",
|
||||
"tgNotifyLoginDesc": "استقبل إشعار بكل محاولة تسجيل دخول للبانل مع اسم المستخدم، الـ IP، والوقت.",
|
||||
"sessionMaxAge": "مدة الجلسة",
|
||||
"sessionMaxAgeDesc": "المدة اللي تفضل فيها مسجل دخول. (الوحدة: دقيقة)",
|
||||
"expireTimeDiff": "تنبيه بتاريخ الانتهاء",
|
||||
"expireTimeDiffDesc": "استقبل تنبيه قبل ما توصل لتاريخ الانتهاء بالمدة المحددة. (الوحدة: يوم)",
|
||||
"trafficDiff": "تنبيه حد الترافيك",
|
||||
"trafficDiffDesc": "استقبل تنبيه عند وصول الترافيك للحد المحدد. (الوحدة: جيجابايت)",
|
||||
"tgNotifyCpu": "تنبيه حمل المعالج",
|
||||
"tgNotifyCpuDesc": "استقبل تنبيه لو حمل المعالج عدى الحد المحدد. (الوحدة: %)",
|
||||
"timeZone": "المنطقة الزمنية",
|
||||
"timeZoneDesc": "المهام المجدولة هتشتغل بناءً على المنطقة الزمنية دي.",
|
||||
"subSettings": "الاشتراك",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64.",
|
||||
"subURI": "مسار البروكسي العكسي",
|
||||
"subURIDesc": "الرابط الأساسي الكامل (scheme://domain[:port]/path/) لرابط الاشتراك وكود الـQR، بيتستخدم بدل دومين الاستماع/بورت الاستماع. املا الحقل ده لو الاشتراك بيتفتح من ورا بروكسي عكسي أو على دومين/بورت مختلف عن اللي فوق.",
|
||||
"externalTrafficInformEnable": "تنبيه الترافيك الخارجي",
|
||||
"externalTrafficInformEnableDesc": "إخطار واجهة API خارجية بكل تحديث لحركة المرور.",
|
||||
"externalTrafficInformURI": "مسار تنبيه الترافيك الخارجي",
|
||||
"externalTrafficInformURIDesc": "تحديثات الترافيك هتتبعت للمسار ده.",
|
||||
"restartXrayOnClientDisable": "إعادة تشغيل Xray بعد التعطيل التلقائي",
|
||||
"restartXrayOnClientDisableDesc": "عند تعطيل العميل تلقائيا بسبب انتهاء الصلاحية أو حد حركة المرور، أعد تشغيل Xray.",
|
||||
"fragment": "تجزئة",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "إعدادات التشويش",
|
||||
"trustedProxyCidrs": "CIDR وكلاء موثوقين",
|
||||
"trustedProxyCidrsDesc": "IPs/CIDRs مفصولة بفواصل يُسمح لها بتعيين ترويسات host، proto و client IP المعاد توجيهها.",
|
||||
"ldap": {
|
||||
"enable": "تفعيل مزامنة LDAP",
|
||||
"host": "مضيف LDAP",
|
||||
"port": "منفذ LDAP",
|
||||
"useTls": "استخدام TLS (LDAPS)",
|
||||
"skipTlsVerify": "تخطي التحقق من شهادة TLS",
|
||||
"skipTlsVerifyDesc": "غير آمن — يعطل التحقق من شهادة الخادم. استخدم فقط مع CA الداخلية/غير الموثوقة.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "مهيأة؛ اترك فارغاً للاحتفاظ بكلمة المرور الحالية.",
|
||||
"passwordUnconfigured": "غير مهيأة.",
|
||||
"passwordPlaceholder": "مهيأة — أدخل قيمة جديدة لاستبدالها",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "مرشح المستخدم",
|
||||
"userAttr": "خاصية المستخدم (username/email)",
|
||||
"vlessField": "خاصية VLESS flag",
|
||||
"flagField": "خاصية flag عامة (اختياري)",
|
||||
"flagFieldDesc": "إذا تم تعيينها، تتجاوز VLESS flag — مثل shadowInactive.",
|
||||
"truthyValues": "قيم Truthy",
|
||||
"truthyValuesDesc": "مفصولة بفواصل؛ الافتراضي: true,1,yes,on",
|
||||
"invertFlag": "عكس flag",
|
||||
"invertFlagDesc": "فعّل عندما تعني الخاصية «معطل» (مثل shadowInactive).",
|
||||
"syncSchedule": "جدول المزامنة",
|
||||
"syncScheduleDesc": "سلسلة شبيهة بـ cron، مثل @every 1m",
|
||||
"inboundTags": "وسوم الواردات",
|
||||
"inboundTagsDesc": "الواردات التي يمكن لمزامنة LDAP إنشاء/حذف العملاء فيها تلقائياً.",
|
||||
"noInbounds": "لم يتم العثور على واردات. أنشئ واحداً في الواردات أولاً.",
|
||||
"autoCreate": "إنشاء عملاء تلقائياً",
|
||||
"autoDelete": "حذف عملاء تلقائياً",
|
||||
"defaultTotalGb": "الإجمالي الافتراضي (GB)",
|
||||
"defaultExpiryDays": "الانتهاء الافتراضي (أيام)",
|
||||
"defaultIpLimit": "حد IP الافتراضي"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "يضيف أقنعة TCP/UDP الخاصة بـ Xray finalmask ومعلمات QUIC إلى كل ملف Xray JSON يتم إنشاؤه. يتطلب تطبيقًا يدعم اشتراكات Xray JSON وإصدارًا حديثًا من نواة Xray.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "إعدادات MUX",
|
||||
"direct": "اتصال مباشر",
|
||||
"directDesc": "ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة.",
|
||||
"notifications": "الإشعارات",
|
||||
"certs": "الشهادات",
|
||||
"externalTraffic": "الترافيك الخارجي",
|
||||
"dateAndTime": "التاريخ والوقت",
|
||||
"proxyAndServer": "البروكسي والسيرفر",
|
||||
"intervals": "الفترات",
|
||||
"information": "المعلومات",
|
||||
"profile": "الملف الشخصي",
|
||||
"language": "اللغة",
|
||||
"telegramBotLanguage": "لغة بوت Telegram",
|
||||
"security": {
|
||||
"admin": "بيانات الأدمن",
|
||||
"twoFactor": "المصادقة الثنائية",
|
||||
"twoFactorEnable": "تفعيل المصادقة الثنائية",
|
||||
"twoFactorEnableDesc": "يضيف طبقة إضافية من المصادقة لتعزيز الأمان.",
|
||||
"twoFactorModalSetTitle": "تفعيل المصادقة الثنائية",
|
||||
"twoFactorModalDeleteTitle": "تعطيل المصادقة الثنائية",
|
||||
"twoFactorModalSteps": "لإعداد المصادقة الثنائية، قم ببعض الخطوات:",
|
||||
"twoFactorModalFirstStep": "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق",
|
||||
"twoFactorModalSecondStep": "2. أدخل الرمز من التطبيق",
|
||||
"twoFactorModalRemoveStep": "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية.",
|
||||
"twoFactorModalChangeCredentialsTitle": "تغيير بيانات الاعتماد",
|
||||
"twoFactorModalChangeCredentialsStep": "أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول.",
|
||||
"twoFactorModalSetSuccess": "تم إنشاء المصادقة الثنائية بنجاح",
|
||||
"twoFactorModalDeleteSuccess": "تم حذف المصادقة الثنائية بنجاح",
|
||||
"twoFactorModalError": "رمز خاطئ",
|
||||
"show": "إظهار",
|
||||
"hide": "إخفاء",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "خطأ في الحصول على حركات المرور الصادرة",
|
||||
"resetOutboundTrafficError": "خطأ في إعادة تعيين حركات المرور الصادرة"
|
||||
},
|
||||
"smtpSettings": "إعدادات SMTP",
|
||||
"smtpEnable": "تفعيل إشعارات البريد الإلكتروني",
|
||||
"smtpEnableDesc": "تفعيل إشعارات البريد الإلكتروني عبر SMTP",
|
||||
"smtpHost": "خادم SMTP",
|
||||
"smtpHostDesc": "اسم مضيف خادم SMTP (مثال: smtp.gmail.com)",
|
||||
"smtpPort": "منفذ SMTP",
|
||||
"smtpPortDesc": "منفذ خادم SMTP (الافتراضي: 587)",
|
||||
"smtpUsername": "اسم مستخدم SMTP",
|
||||
"smtpUsernameDesc": "اسم المستخدم للمصادقة على SMTP",
|
||||
"smtpFrom": "عنوان المُرسِل (From)",
|
||||
"smtpFromDesc": "العنوان المستخدم في ترويسة From للبريد. اتركه فارغًا لاستخدام اسم المستخدم.",
|
||||
"smtpFromName": "اسم المُرسِل (From)",
|
||||
"smtpFromNameDesc": "اسم عرض اختياري يظهر قبل العنوان في ترويسة From.",
|
||||
"smtpPassword": "كلمة مرور SMTP",
|
||||
"smtpPasswordDesc": "كلمة المرور للمصادقة على SMTP",
|
||||
"smtpTo": "المستلمون",
|
||||
"smtpToDesc": "عناوين البريد الإلكتروني للمستلمين مفصولة بفواصل",
|
||||
"emailSettings": "البريد الإلكتروني",
|
||||
"emailNotifications": "الإشعارات",
|
||||
"smtpEventBusNotify": "إشعارات الأحداث بالبريد الإلكتروني",
|
||||
"smtpEventBusNotifyDesc": "اختر الأحداث التي تُطلق إشعارات البريد الإلكتروني",
|
||||
"tgEventBusNotify": "إشعارات الأحداث عبر Telegram",
|
||||
"tgEventBusNotifyDesc": "اختر الأحداث التي تُطلق إشعارات Telegram",
|
||||
"testSmtp": "إرسال بريد تجريبي",
|
||||
"testTgBot": "إرسال رسالة تجريبية",
|
||||
"eventGroupOutbound": "الصادر",
|
||||
"eventGroupXray": "نواة Xray",
|
||||
"eventGroupSystem": "النظام",
|
||||
"eventGroupSecurity": "الأمان",
|
||||
"eventGroupNode": "العقد",
|
||||
"eventOutboundDown": "غير متصل",
|
||||
"eventOutboundUp": "متصل",
|
||||
"eventXrayCrash": "تعطّل",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "متصلة",
|
||||
"eventCPUHigh": "ارتفاع استخدام المعالج (%)",
|
||||
"requestFailed": "فشل الطلب",
|
||||
"smtpEncryption": "التشفير",
|
||||
"smtpEncryptionDesc": "طريقة تشفير اتصال SMTP",
|
||||
"smtpEncryptionNone": "بدون (نص عادي)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (ضمني)",
|
||||
"smtpStageConnect": "الاتصال",
|
||||
"smtpStageAuth": "المصادقة",
|
||||
"smtpStageSend": "الإرسال",
|
||||
"smtpTestSuccess": "تم إرسال البريد التجريبي بنجاح",
|
||||
"smtpHostNotConfigured": "خادم SMTP غير مهيأ",
|
||||
"smtpNoRecipients": "لا يوجد مستلمون مهيؤون",
|
||||
"smtpFromNotConfigured": "عنوان مرسل SMTP غير مُهيأ",
|
||||
"eventLoginAttempt": "محاولة تسجيل دخول",
|
||||
"telegramTokenConfigured": "مهيأ؛ اتركه فارغاً للاحتفاظ بالتوكن الحالي.",
|
||||
"telegramTokenPlaceholder": "مهيأ — أدخل توكن جديد لاستبداله",
|
||||
"smtpPasswordConfigured": "مهيأة؛ اتركها فارغة للاحتفاظ بكلمة المرور الحالية.",
|
||||
"smtpPasswordPlaceholder": "مهيأة — أدخل كلمة مرور جديدة لاستبدالها",
|
||||
"smtpNotInitialized": "لم تتم تهيئة SMTP",
|
||||
"tgBotNotEnabled": "بوت Telegram غير مفعّل",
|
||||
"tgTestFailed": "فشل اختبار Telegram",
|
||||
"tgTestSuccess": "تم إرسال رسالة تجريبية إلى Telegram",
|
||||
"tgBotNotRunning": "بوت Telegram لا يعمل",
|
||||
"smtpErrorAuth": "فشلت المصادقة — تحقق من اسم المستخدم وكلمة المرور",
|
||||
"smtpErrorStarttls": "الخادم يتطلب STARTTLS — غيّر نوع التشفير",
|
||||
"smtpErrorTls": "الخادم يتطلب TLS — غيّر نوع التشفير",
|
||||
"smtpErrorRefused": "تم رفض الاتصال — تحقق من الخادم والمنفذ",
|
||||
"smtpErrorTimeout": "انتهت مهلة الاتصال — تعذّر الوصول إلى الخادم",
|
||||
"smtpErrorRelay": "الخادم يرفض الإرسال من هذا العنوان",
|
||||
"smtpErrorEof": "تم إغلاق الاتصال من قبل الخادم",
|
||||
"smtpErrorUnknown": "خطأ SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "ارتفاع استخدام الذاكرة (%)",
|
||||
"remarkTemplate": "قالب الملاحظة",
|
||||
"remarkTemplateDesc": "عند تعيينه، يحل هذا محل نموذج الملاحظة لكل رابط اشتراك — اكتب صيغتك الخاصة باستخدام رموز المتغيرات (استخدم الزر لإدراجها). اتركه فارغاً لاستخدام النموذج أعلاه.",
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "اتصالات مباشرة للدومينات",
|
||||
"ipv4Routing": "توجيه IPv4",
|
||||
"ipv4RoutingDesc": "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4.",
|
||||
"Template": "قالب إعدادات Xray المتقدم",
|
||||
"TemplateDesc": "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده.",
|
||||
"FreedomStrategy": "استراتيجية بروتوكول الحرية",
|
||||
"FreedomStrategyDesc": "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "موازنات التحميل",
|
||||
"balancerTagRequired": "الوسم مطلوب",
|
||||
"balancerSelectorRequired": "اختر صادراً واحداً على الأقل",
|
||||
"balancerLive": "الهدف الحالي",
|
||||
"balancerOverride": "تجاوز الاختيار",
|
||||
"balancerOverridePh": "تلقائي (الاستراتيجية)",
|
||||
"balancerLiveRefresh": "تحديث حالة موازن التحميل",
|
||||
"balancerNotRunning": "موازن التحميل ده مش نشط في Xray الشغال — احفظ التغييرات أو ابدأ Xray الأول",
|
||||
"routeTester": "اختبار المسار",
|
||||
"routeTesterDesc": "اسأل Xray الشغال أي صادر هيتعامل مع الاتصال ده. مفيش ترافيك بيتبعت — القرار بييجي مباشرة من محرك التوجيه الحي.",
|
||||
"routeTesterDest": "نطاق أو IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "عبر موازن التحميل",
|
||||
"routeTesterDefaultOutbound": "ما في قاعدة توجيه اتطابقت — الترافيك رايح للصادر الافتراضي (الأول).",
|
||||
"Routings": "قواعد التوجيه",
|
||||
"completeTemplate": "الكل",
|
||||
"logLevel": "مستوى السجلات",
|
||||
"logLevelDesc": "مستوى السجل الخاص بالأخطاء، اللي بيوضح المعلومات المطلوبة للتسجيل.",
|
||||
"accessLog": "سجل الوصول",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "المصدر",
|
||||
"dest": "الوجهة",
|
||||
"inbound": "إدخال",
|
||||
"balancer": "موازن",
|
||||
"useComma": "عناصر مفصولة بفواصل"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "هذا الخادم موجود بالفعل في قائمة الصادر. استخدم {reset} لتجديد المفتاح.",
|
||||
"provisionFailed": "تعذر إنشاء مسار PIA الصادر. حاول مرة أخرى."
|
||||
},
|
||||
"tabBalancerSettings": "إعدادات الموازن",
|
||||
"tabObservatory": "المرصد",
|
||||
"observatory": {
|
||||
"autoManaged": "تتم إدارة المراصد تلقائيًا من الموازنات لديك. اضبط طريقة الفحص بالأسفل؛ تتبع الوجهات الصادرة المراقَبة محدِّدات الموازن.",
|
||||
"emptyHint": "لا يوجد مرصد اتصال نشط. تتم إضافة واحد تلقائيًا عند إنشاء موازن Least Ping أو Least Load — أو موازن Random / Round-robin مع fallback — حتى تتمكن الموازنات المعتمدة على المرصد من فحص صحة الوجهات الصادرة قبل اختيار الهدف.",
|
||||
"mixedLegacy": "يحتوي هذا الإعداد على Observatory و Burst Observatory معًا. يستخدم Xray مرصدًا عالميًا واحدًا، لذلك هذه الحالة القديمة المختلطة غير مدعومة؛ حفظ الموازنات سيحوّلها إلى مرصد واحد.",
|
||||
"subjectSelector": "الوجهات المراقَبة",
|
||||
"subjectSelectorDesc": "وسوم الوجهات الصادرة التي يفحصها هذا المرصد. تتم إدارتها تلقائيًا من الموازنات لديك.",
|
||||
"probeURL": "رابط الفحص (URL)",
|
||||
"probeURLDesc": "الرابط الذي يُطلب لقياس كل وجهة صادرة. يجب أن يُعيد HTTP 204.",
|
||||
"probeInterval": "فترة الفحص",
|
||||
"probeIntervalDesc": "كم مرة يتم فحص كل وجهة صادرة، مثل 30s أو 1m أو 2h45m.",
|
||||
"enableConcurrency": "فحص متزامن",
|
||||
"enableConcurrencyDesc": "افحص كل الوجهات المراقَبة دفعة واحدة بدلًا من واحدة تلو الأخرى. أسرع، لكنه أكثر وضوحًا على الشبكة.",
|
||||
"destination": "وجهة الفحص",
|
||||
"destinationDesc": "الرابط الذي يُطلب لقياس كل وجهة صادرة. يجب أن يُعيد HTTP 204.",
|
||||
"connectivity": "فحص الاتصال",
|
||||
"connectivityDesc": "رابط اختياري لفحص الشبكة المحلية، يُجرَّب فقط بعد فشل الوجهة. اتركه فارغًا للتخطي.",
|
||||
"interval": "فترة الفحص",
|
||||
"intervalDesc": "متوسط الوقت بين عمليات الفحص لكل وجهة صادرة، مثل 1m. الحد الأدنى 10s.",
|
||||
"timeout": "مهلة الفحص",
|
||||
"timeoutDesc": "كم من الوقت يُنتظر استجابة الفحص قبل اعتباره فاشلًا، مثل 5s.",
|
||||
"sampling": "عدد العينات",
|
||||
"samplingDesc": "عدد نتائج الفحص الأخيرة المحفوظة لتقييم كل وجهة صادرة.",
|
||||
"httpMethod": "طريقة HTTP",
|
||||
"httpMethodDesc": "طريقة HTTP المستخدمة في عمليات الفحص.",
|
||||
"deleteAlsoObservatory": "هذا آخر موازن يستخدم Observatory، لذلك ستتم إزالته أيضًا.",
|
||||
"deleteAlsoBurst": "هذا آخر موازن يستخدم Burst Observatory، لذلك ستتم إزالته أيضًا."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "استراتيجية الموازن",
|
||||
"tag": "الوسم",
|
||||
"tagDuplicate": "الوسم مستخدم بالفعل من قبل موازن آخر",
|
||||
"tagPlaceholder": "وسم موازن فريد",
|
||||
"selector": "المحدد",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "حلقة: {path} → (العودة إلى {start})",
|
||||
"expected": "المتوقع",
|
||||
"expectedPlaceholder": "العدد الأمثل للعقد",
|
||||
"maxRtt": "أقصى RTT",
|
||||
"tolerance": "التحمل",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "نمط الوسم",
|
||||
"costValue": "الوزن",
|
||||
"costRegexp": "مطابقة تعبير نمطي",
|
||||
"balancerDeleteInUse": "لا يمكن حذف هذا الموزان — يتم استخدامه كبديل لـ: {names}",
|
||||
"balancerFallbackCycle": "لا يمكن تعيين هذا الموزان كبديل — سيؤدي ذلك إلى إنشاء تبعية دائرية.",
|
||||
"balancerFallbackInfo": "سيتم توجيه حركة المرور عبر: الموزان → Loopback → الخادم → الموزان المستهدف → الاتصال الخارجي. سيؤدي هذا إلى إضافة قفزة إضافية عبر الخادم، مما قد يسبب تأخيرات طفيفة.",
|
||||
"fallbackBalancerHint": "اختر موزان أحمال آخر كبديل",
|
||||
"reservedPrefix": "البادئة _bl_ محجوزة لكائنات loopback الداخلية للموازنات"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "الاستخدام",
|
||||
"addOutbound": "إضافة صادر"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "فعل DNS",
|
||||
"enableDesc": "فعل سيرفر DNS المدمج",
|
||||
"tag": "تاج إدخال DNS",
|
||||
"tagDesc": "التاج ده هيبقى متاح كإدخال في قواعد التوجيه.",
|
||||
"clientIp": "IP العميل",
|
||||
"clientIpDesc": "بيحدد موقع العميل خلال استعلامات DNS",
|
||||
"disableCache": "تعطيل الكاش",
|
||||
"disableCacheDesc": "بيعطل تخزين نتائج DNS مؤقتاً",
|
||||
"disableFallback": "تعطيل النسخ الاحتياطي",
|
||||
"disableFallbackDesc": "بيعطل استعلامات DNS الاحتياطية",
|
||||
"disableFallbackIfMatch": "تعطيل النسخ الاحتياطي عند التطابق",
|
||||
"disableFallbackIfMatchDesc": "بيعطل استعلامات DNS الاحتياطية لما يتحقق تطابق مع قائمة الدومينات",
|
||||
"enableParallelQuery": "تفعيل الاستعلام المتوازي",
|
||||
"enableParallelQueryDesc": "تفعيل استعلامات DNS المتوازية لعدة خوادم لحل أسرع",
|
||||
"strategy": "استراتيجية الاستعلام",
|
||||
"strategyDesc": "الاستراتيجية العامة لحل أسماء الدومين",
|
||||
"add": "أضف سيرفر",
|
||||
"edit": "عدل السيرفر",
|
||||
"domains": "النطاقات",
|
||||
"expectIPs": "العناوين المتوقعة",
|
||||
"unexpectIPs": "عناوين IP غير متوقعة",
|
||||
"useSystemHosts": "استخدام ملف Hosts الخاص بالنظام",
|
||||
"useSystemHostsDesc": "استخدام ملف hosts من نظام مثبت",
|
||||
"serveStale": "تقديم النتائج المنتهية",
|
||||
"serveStaleDesc": "إرجاع نتائج الكاش المنتهية الصلاحية أثناء التحديث في الخلفية",
|
||||
"serveExpiredTTL": "مدة صلاحية النتائج المنتهية",
|
||||
"serveExpiredTTLDesc": "مدة صلاحية إدخالات الكاش المنتهية بالثواني؛ 0 = لا تنتهي أبدًا",
|
||||
"timeoutMs": "المهلة (مللي ثانية)",
|
||||
"skipFallback": "تخطي الاحتياطي",
|
||||
"finalQuery": "الاستعلام النهائي",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "إضافة Host",
|
||||
"hostsEmpty": "لم يتم تعريف أي Host",
|
||||
"hostsDomain": "النطاق (مثل domain:example.com)",
|
||||
"hostsValues": "عنوان IP أو نطاق — اكتب واضغط Enter",
|
||||
"usePreset": "استخدام النموذج",
|
||||
"dnsPresetTitle": "قوالب DNS",
|
||||
"dnsPresetFamily": "العائلي",
|
||||
"clearAll": "حذف الكل",
|
||||
"clearAllTitle": "حذف جميع خوادم DNS؟",
|
||||
"clearAllConfirm": "سيؤدي هذا إلى إزالة جميع خوادم DNS من القائمة. لا يمكن التراجع عن هذا الإجراء.",
|
||||
"dnsLeakWarning": "قد يتسرب DNS عبر localhost أو UDP/TCP غير المشفر أو وضع DoH/DoQ المحلي أو استعلامات الرجوع أو EDNS client IP. استخدم DoH عبر التوجيه، وثبت أسماء المحللات في hosts، وعطل الرجوع عندما تكون الخصوصية مهمة."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "أضف Fake DNS",
|
||||
"ipPool": "نطاق IP Pool",
|
||||
"poolSize": "حجم المجموعة"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "الصادر الافتراضي",
|
||||
"defaultOutboundDesc": "الحركة التي لا تطابق أي قاعدة توجيه تستخدم هذا الصادر (الأول في القائمة)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ لوحة المفاتيح مغلقة!",
|
||||
"noResult": "❗ لا يوجد نتائج!",
|
||||
"noQuery": "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!",
|
||||
"wentWrong": "❌ حدث خطأ ما!",
|
||||
"noIpRecord": "❗ لا يوجد سجل IP!",
|
||||
"noInbounds": "❗ لم يتم العثور على أي وارد!",
|
||||
"unlimited": "♾ غير محدود (إعادة تعيين)",
|
||||
"add": "إضافة",
|
||||
"month": "شهر",
|
||||
"months": "أشهر",
|
||||
"days": "أيام",
|
||||
"hours": "ساعات",
|
||||
"minutes": "دقائق",
|
||||
"unknown": "غير معروف",
|
||||
"inbounds": "الواردات",
|
||||
"clients": "العملاء",
|
||||
"offline": "🔴 غير متصل",
|
||||
"online": "🟢 متصل",
|
||||
"commands": {
|
||||
"unknown": "❗ أمر مش معروف.",
|
||||
"pleaseChoose": "👇 من فضلك اختار:\r\n",
|
||||
"help": "🤖 أهلا بيك في البوت! البوت ده معمول عشان يديك بيانات معينة من البانل ويسمحلك بالتعديلات.",
|
||||
"start": "👋 أهلا <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 أهلا بيك في بوت إدارة <b>{{ .Hostname }}</b>.\r\n",
|
||||
"status": "✅ البوت شغال!",
|
||||
"usage": "❗ من فضلك ادخل نص للتبحث عنه!",
|
||||
"getID": "🆔 الـ ID بتاعك: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "عشان تعيد تشغيل Xray Core:\r\n<code>/restart</code>\r\n\r\nعشان تدور على إيميل عميل:\r\n<code>/usage [Email]</code>\r\n\r\nعشان تدور على إدخالات (مع إحصائيات العملاء):\r\n<code>/inbound [Remark]</code>\r\n\r\nID شات Telegram:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "عشان تدور على الإحصائيات، استخدم الأمر ده:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nID شات Telegram:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ العملية نجحت!",
|
||||
"restartFailed": "❗ حصل خطأ في العملية.\r\n\r\n<code>Error: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core مش شغال.",
|
||||
"startDesc": "عرض القائمة الرئيسية",
|
||||
"helpDesc": "مساعدة البوت",
|
||||
"statusDesc": "التحقق من حالة البوت",
|
||||
"idDesc": "عرض معرف Telegram الخاص بك",
|
||||
"usageDesc": "عرض استهلاك العميل: /usage البريد",
|
||||
"inboundDesc": "البحث في الواردات: /inbound الاسم (مشرف)",
|
||||
"restartDesc": "إعادة تشغيل نواة Xray (مشرف)",
|
||||
"clearallDesc": "تصفير استهلاك جميع العملاء (مشرف)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)",
|
||||
"selectUserFailed": "❌ حصل خطأ في اختيار المستخدم!",
|
||||
"userSaved": "✅ حفظت بيانات مستخدم Telegram.",
|
||||
"loginSuccess": "✅ تسجيل الدخول للبانل تم بنجاح.\r\n",
|
||||
"loginFailed": "❗️فشل محاولة تسجيل الدخول للبانل.\r\n",
|
||||
"report": "🕰 التقارير المجدولة: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ التاريخ والوقت: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 المضيف: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 نسخة 3X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 نسخة Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ الحالة: {{ .State }}\r\n",
|
||||
"username": "👤 اسم المستخدم: {{ .Username }}\r\n",
|
||||
"reason": "❗️ السبب: {{ .Reason }}\r\n",
|
||||
"time": "⏰ الوقت: {{ .Time }}\r\n",
|
||||
"inbound": "📍 الوارد: {{ .Remark }}\r\n",
|
||||
"port": "🔌 المنفذ: {{ .Port }}\r\n",
|
||||
"expire": "📅 تاريخ الانتهاء: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 هيخلص بعد: {{ .Time }}\r\n",
|
||||
"active": "💡 مفعل: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 مفعل: {{ .Enable }}\r\n",
|
||||
"online": "🌐 حالة الاتصال: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 آخر متصل: {{ .Time }}\r\n",
|
||||
"email": "📧 البريد: {{ .Email }}\r\n",
|
||||
"upload": "🔼 الرفع: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 التنزيل: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 مستخدم Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 عدد النفاذ لـ {{ .Type }}:\r\n",
|
||||
"onlinesCount": "🌐 العملاء الأونلاين: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 معطل: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 هينتهي قريب: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 وقت النسخة الاحتياطية: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ أيوه",
|
||||
"no": "❌ لا",
|
||||
"received_email": "📧📥 الإيميل اتحدث.",
|
||||
"received_comment": "💬📥 التعليق اتحدث.",
|
||||
"email_prompt": "📧 الإيميل الافتراضي: {{ .ClientEmail }}\n\nادخل الإيميل بتاعك.",
|
||||
"comment_prompt": "💬 التعليق الافتراضي: {{ .ClientComment }}\n\nادخل تعليقك.",
|
||||
"cancel": "❌ العملية اتلغت! \n\nممكن تبدأ من /start في أي وقت. 🔄",
|
||||
"error_add_client": "⚠️ خطأ:\n\n {{ .error }}",
|
||||
"using_default_value": "تمام، هشيل على القيمة الافتراضية. 😊",
|
||||
"incorrect_input": "المدخلات مش صحيحة.\nالكلمات لازم تكون متصلة من غير فراغات.\nمثال صحيح: aaaaaa\nمثال غلط: aaa aaa 🚫",
|
||||
"AreYouSure": "إنت متأكد؟ 🤔",
|
||||
"SuccessResetTraffic": "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ✅ تم بنجاح",
|
||||
"FailedResetTraffic": "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠️ الخطأ: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء.",
|
||||
"eventOutboundDown": "الصادر {{ .Tag }} غير متصل",
|
||||
"eventOutboundUp": "الصادر {{ .Tag }} متصل",
|
||||
"eventErrorDetail": "الخطأ: {{ .Error }}",
|
||||
"eventDelayDetail": "التأخير: {{ .Delay }} مللي ثانية",
|
||||
"eventXrayCrash": "تعطّل Xray",
|
||||
"eventXrayCrashError": "الخطأ: {{ .Error }}",
|
||||
"eventNodeDown": "العقدة {{ .Name }} غير متصلة",
|
||||
"eventNodeUp": "العقدة {{ .Name }} متصلة",
|
||||
"eventLoginFallback": "فشل تسجيل الدخول من {{ .Source }}",
|
||||
"memoryThreshold": "استخدام الذاكرة {{ .Percent }}% يتجاوز الحد {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ اقفل الكيبورد",
|
||||
"cancel": "❌ إلغاء",
|
||||
"cancelReset": "❌ إلغاء إعادة الضبط",
|
||||
"cancelIpLimit": "❌ إلغاء حد الـ IP",
|
||||
"confirmResetTraffic": "✅ تأكيد إعادة ضبط الترافيك؟",
|
||||
"confirmClearIps": "✅ تأكيد مسح الـ IPs؟",
|
||||
"confirmRemoveTGUser": "✅ تأكيد حذف مستخدم Telegram؟",
|
||||
"confirmToggle": "✅ تأكيد تفعيل/تعطيل المستخدم؟",
|
||||
"dbBackup": "احصل على نسخة DB",
|
||||
"serverUsage": "استخدام السيرفر",
|
||||
"getInbounds": "احصل على الإدخالات",
|
||||
"depleteSoon": "هينتهي قريب",
|
||||
"clientUsage": "استخدام العميل",
|
||||
"onlines": "العملاء الأونلاين",
|
||||
"commands": "الأوامر",
|
||||
"refresh": "🔄 تجديد",
|
||||
"clearIPs": "❌ مسح الـ IPs",
|
||||
"removeTGUser": "❌ حذف مستخدم Telegram",
|
||||
"selectTGUser": "👤 اختار مستخدم Telegram",
|
||||
"selectOneTGUser": "👤 اختار مستخدم Telegram:",
|
||||
"resetTraffic": "📈 إعادة ضبط الترافيك",
|
||||
"resetExpire": "📅 تغيير تاريخ الانتهاء",
|
||||
"ipLog": "🔢 سجل الـ IP",
|
||||
"ipLimit": "🔢 حد الـ IP",
|
||||
"setTGUser": "👤 ضبط مستخدم Telegram",
|
||||
"toggle": "🔘 تفعيل / تعطيل",
|
||||
"custom": "🔢 مخصص",
|
||||
"confirmNumber": "✅ تأكيد: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ تأكيد إضافة: {{ .Num }}",
|
||||
"limitTraffic": "🚧 حد الترافيك",
|
||||
"getBanLogs": "احصل على سجلات الحظر",
|
||||
"allClients": "كل العملاء",
|
||||
"addClient": "إضافة عميل",
|
||||
"submitDisable": "إرسال كمعطّل ☑️",
|
||||
"submitEnable": "إرسال كمفعّل ✅",
|
||||
"use_default": "🏷️ استخدام الإعدادات الافتراضية",
|
||||
"change_email": "⚙️📧 البريد",
|
||||
"change_comment": "⚙️💬 تعليق",
|
||||
"ResetAllTraffics": "إعادة ضبط جميع الترافيك",
|
||||
"SortedTrafficUsageReport": "تقرير استخدام الترافيك المرتب"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ العملية نجحت!",
|
||||
"errorOperation": "❗ حصل خطأ في العملية.",
|
||||
"getInboundsFailed": "❌ فشل الحصول على الإدخالات.",
|
||||
"getClientsFailed": "❌ فشل الحصول على العملاء.",
|
||||
"canceled": "❌ {{ .Email }}: العملية اتلغت.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: العميل اتحدث بنجاح.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: الـ IPs اتحدثت بنجاح.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: مستخدم Telegram اتحدث بنجاح.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: الترافيك اتظبط بنجاح.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: حد الترافيك اتسجل بنجاح.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: أيام الانتهاء اتظبطت بنجاح.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: حد الـ IP ({{ .Count }}) اتسجل بنجاح.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: الـ IPs اتمسحت بنجاح.",
|
||||
"getIpLog": "✅ {{ .Email }}: سجل الـ IP اتجاب.",
|
||||
"getUserInfo": "✅ {{ .Email }}: بيانات مستخدم Telegram اتجاب.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: مستخدم Telegram اتحذف بنجاح.",
|
||||
"enableSuccess": "✅ {{ .Email }}: اتفعل بنجاح.",
|
||||
"disableSuccess": "✅ {{ .Email }}: اتعطل بنجاح.",
|
||||
"askToAddUserId": "مافيش إعدادات ليك!\r\nاطلب من الأدمن يضيف الـ Telegram ChatID الخاص بيك في إعداداتك.\r\n\r\nالـ ChatID بتاعك: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "اختار عميل للإدخال {{ .Inbound }}",
|
||||
"chooseInbound": "اختار الإدخال"
|
||||
}
|
||||
"noIpRecord": "❗ لا يوجد سجل IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "الحالة",
|
||||
"labelOutbound": "الصادر",
|
||||
"labelNode": "العقدة",
|
||||
"labelError": "الخطأ",
|
||||
"labelDelay": "التأخير",
|
||||
"labelUsername": "اسم المستخدم",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "السبب",
|
||||
"labelSource": "المصدر",
|
||||
"statusCrashed": "متعطّل",
|
||||
"statusHigh": "مرتفع",
|
||||
"statusSuccess": "نجاح",
|
||||
"statusFailed": "فشل",
|
||||
"statusDown": "غير متصل",
|
||||
"statusUp": "متصل"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Success",
|
||||
"lastOnline": "Last Online",
|
||||
"lastSubFetch": "Last Subscription Fetch",
|
||||
"getVersion": "Get Version",
|
||||
"install": "Install",
|
||||
"clients": "Clients",
|
||||
"usage": "Usage",
|
||||
"twoFactorCode": "Code",
|
||||
"remained": "Remaining",
|
||||
"security": "Security",
|
||||
"emptyDnsDesc": "No added DNS servers.",
|
||||
"emptyFakeDnsDesc": "No added Fake DNS servers.",
|
||||
"emptyBalancersDesc": "No added balancers.",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"subscription": {
|
||||
"title": "Subscription info",
|
||||
@@ -87,7 +83,6 @@
|
||||
"uploaded": "Uploaded",
|
||||
"expiry": "Expiry",
|
||||
"totalQuota": "Total quota",
|
||||
"individualLinks": "Individual links",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"unlimited": "Unlimited",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Stop",
|
||||
"restartXray": "Restart",
|
||||
"xraySwitch": "Version",
|
||||
"xrayUpdates": "Xray Updates",
|
||||
"xraySwitchClickDesk": "Choose carefully, as older versions may not be compatible with current configurations.",
|
||||
"updatePanel": "Update Panel",
|
||||
"panelUpdateDesc": "This will update 3X-UI itself to the latest release and restart the panel service.",
|
||||
"currentPanelVersion": "Current panel version",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Download",
|
||||
"sent": "Sent",
|
||||
"received": "Received",
|
||||
"xraySwitchVersionDialog": "Do you really want to change the Xray version?",
|
||||
"xraySwitchVersionDialogDesc": "This will change the Xray version to #version#.",
|
||||
"xraySwitchVersionPopover": "Xray updated successfully",
|
||||
"panelUpdateDialog": "Do you really want to update the panel?",
|
||||
"panelUpdateDialogDesc": "This will update 3X-UI to #version# and restart the panel service.",
|
||||
"panelUpdateStartedPopover": "Panel update started",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "The update did not finish successfully. Check the server logs, or run 'x-ui update' from the command line.",
|
||||
"panelUpdateUnknownTitle": "Couldn't confirm the update finished",
|
||||
"panelUpdateUnknownDesc": "The panel didn't report a result in time. Reload to check the current version, or check the server logs.",
|
||||
"geofileUpdateDialog": "Do you really want to update the geofile?",
|
||||
"geofileUpdateDialogDesc": "This will update the #filename# file.",
|
||||
"geofilesUpdateDialogDesc": "This will update all geofiles.",
|
||||
"geofilesUpdateAll": "Update all",
|
||||
"geofileUpdatePopover": "Geofile updated successfully",
|
||||
"geodataTitle": "Geodata Auto-Update",
|
||||
"geodataHint": "Xray downloads these files on schedule and hot-reloads them without a restart. URLs must be HTTPS. Each file must already exist in the bin folder once before Xray can update it.",
|
||||
"geodataCron": "Schedule (cron)",
|
||||
"geodataOutbound": "Download through outbound (optional)",
|
||||
"geodataFile": "File name",
|
||||
"geodataAddFile": "Add file",
|
||||
"geodataSaveRestart": "Save & Restart Xray",
|
||||
"geodataConfirmTitle": "Save geodata settings?",
|
||||
"geodataConfirmContent": "This updates the Xray config template and restarts Xray.",
|
||||
"geodataInvalidUrl": "Each file needs an HTTPS URL.",
|
||||
"geodataInvalidFile": "File names must be plain names like geosite_custom.dat (no paths).",
|
||||
"geodataInvalidCron": "Cron must have 5 fields, e.g. 0 4 * * *",
|
||||
"geodataEmpty": "No files configured. Reference files in routing rules as ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Installation is in progress, please do not refresh this page",
|
||||
"logs": "Logs",
|
||||
"accessLogs": "Access Logs",
|
||||
@@ -1179,19 +1150,7 @@
|
||||
"pinnedCertPlaceholder": "base64 or hex SHA-256",
|
||||
"fetchPin": "Fetch",
|
||||
"pinFetched": "Fetched the node's current certificate",
|
||||
"pinFetchFailed": "Could not fetch the certificate",
|
||||
"nodeType": "Node Type",
|
||||
"nodeTypeTraditional": "Traditional Panel Node",
|
||||
"nodeTypeAgent": "Agent Lightweight Node",
|
||||
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
|
||||
"agentTokenTitle": "Agent Node Setup",
|
||||
"agentTokenCreated": "Node {{name}} created successfully",
|
||||
"agentTokenHint": "Copy the command below and run it on the node machine. This token is shown only once!",
|
||||
"agentTokenInstructions": "The command downloads the installer, configures the master URL and token. The node comes online in ~30s.",
|
||||
"agentTokenCopied": "Install command copied",
|
||||
"agentTokenCopyFailed": "Copy failed, please copy manually",
|
||||
"copyToken": "Copy Install Command",
|
||||
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):"
|
||||
"pinFetchFailed": "Could not fetch the certificate"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "Default",
|
||||
@@ -1212,7 +1171,6 @@
|
||||
"warnDefaultBasePath": "Default base path \"/\" is well-known — change it to a random path.",
|
||||
"warnDefaultSubPath": "Default subscription path \"/sub/\" is well-known — change it.",
|
||||
"warnDefaultJsonPath": "Default JSON subscription path \"/json/\" is well-known — change it.",
|
||||
"TGBotSettings": "Telegram Bot",
|
||||
"panelListeningIP": "Listen IP",
|
||||
"panelListeningIPDesc": "The IP address for the web panel. (leave blank to listen on all IPs)",
|
||||
"panelListeningDomain": "Listen Domain",
|
||||
@@ -1241,43 +1199,12 @@
|
||||
"currentPassword": "Current Password",
|
||||
"newUsername": "New Username",
|
||||
"newPassword": "New Password",
|
||||
"telegramBotEnable": "Enable Telegram Bot",
|
||||
"telegramBotEnableDesc": "Enables the Telegram bot.",
|
||||
"telegramToken": "Telegram Token",
|
||||
"telegramTokenDesc": "The Telegram bot token obtained from '{'@'}BotFather'.",
|
||||
"telegramProxy": "SOCKS Proxy",
|
||||
"telegramProxyDesc": "Enables SOCKS5 proxy for connecting to Telegram. (adjust settings as per guide)",
|
||||
"telegramAPIServer": "Telegram API Server",
|
||||
"telegramAPIServerDesc": "The Telegram API server to use. Leave blank to use the default server.",
|
||||
"telegramChatId": "Admin Chat ID",
|
||||
"telegramChatIdDesc": "The Telegram Admin Chat ID(s). (comma-separated)(get it here {'@'}userinfobot) or (use '/id' command in the bot)",
|
||||
"telegramNotifyTime": "Notification Time",
|
||||
"telegramNotifyTimeDesc": "How often the Telegram bot sends periodic reports. Pick a preset interval, or choose Custom to enter a raw crontab expression.",
|
||||
"notifyTime": {
|
||||
"every": "@every — repeat at an interval",
|
||||
"hourly": "@hourly — every hour",
|
||||
"daily": "@daily — every day at 00:00",
|
||||
"weekly": "@weekly — every week",
|
||||
"monthly": "@monthly — every month",
|
||||
"custom": "Custom (crontab)",
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"interval": "Interval",
|
||||
"unit": "Unit"
|
||||
},
|
||||
"tgNotifyBackup": "Database Backup",
|
||||
"tgNotifyBackupDesc": "Send a database backup file with a report.",
|
||||
"tgNotifyLogin": "Login Notification",
|
||||
"tgNotifyLoginDesc": "Get notified about the username, IP address, and time whenever someone attempts to log into your web panel.",
|
||||
"sessionMaxAge": "Session Duration",
|
||||
"sessionMaxAgeDesc": "The duration for which you can stay logged in. (unit: minute)",
|
||||
"expireTimeDiff": "Expiration Date Notification",
|
||||
"expireTimeDiffDesc": "Get notified about expiration date when reaching this threshold. (unit: day)",
|
||||
"trafficDiff": "Traffic Cap Notification",
|
||||
"trafficDiffDesc": "Get notified about traffic cap when reaching this threshold. (unit: GB)",
|
||||
"tgNotifyCpu": "CPU Load Notification",
|
||||
"tgNotifyCpuDesc": "Get notified if CPU load exceeds this threshold. (unit: %)",
|
||||
"timeZone": "Time Zone",
|
||||
"timeZoneDesc": "Scheduled tasks will run based on this time zone.",
|
||||
"subSettings": "Subscription",
|
||||
@@ -1342,10 +1269,6 @@
|
||||
"subEncryptDesc": "The returned content of subscription service will be Base64 encoded.",
|
||||
"subURI": "Reverse Proxy URI",
|
||||
"subURIDesc": "The full base URL (scheme://domain[:port]/path/) for the subscription link and QR code, used instead of Listen Domain/Listen Port. Set this whenever the subscription is reached through a reverse proxy or a domain/port different from the ones above.",
|
||||
"externalTrafficInformEnable": "External Traffic Inform",
|
||||
"externalTrafficInformEnableDesc": "Inform external API on every traffic update.",
|
||||
"externalTrafficInformURI": "External Traffic Inform URI",
|
||||
"externalTrafficInformURIDesc": "Traffic updates are sent to this URI.",
|
||||
"restartXrayOnClientDisable": "Restart Xray After Auto Disable",
|
||||
"restartXrayOnClientDisableDesc": "When a client is automatically disabled due to expiration or traffic limit, restart Xray.",
|
||||
"fragment": "Fragmentation",
|
||||
@@ -1355,38 +1278,6 @@
|
||||
"noisesSett": "Noises Settings",
|
||||
"trustedProxyCidrs": "Trusted proxy CIDRs",
|
||||
"trustedProxyCidrsDesc": "Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers.",
|
||||
"ldap": {
|
||||
"enable": "Enable LDAP sync",
|
||||
"host": "LDAP host",
|
||||
"port": "LDAP port",
|
||||
"useTls": "Use TLS (LDAPS)",
|
||||
"skipTlsVerify": "Skip TLS certificate verification",
|
||||
"skipTlsVerifyDesc": "Insecure — disables server certificate validation. Use only with internal/untrusted CAs.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Configured; leave blank to keep current password.",
|
||||
"passwordUnconfigured": "Not configured.",
|
||||
"passwordPlaceholder": "Configured - enter a new value to replace",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "User filter",
|
||||
"userAttr": "User attribute (username/email)",
|
||||
"vlessField": "VLESS flag attribute",
|
||||
"flagField": "Generic flag attribute (optional)",
|
||||
"flagFieldDesc": "If set, overrides VLESS flag — e.g. shadowInactive.",
|
||||
"truthyValues": "Truthy values",
|
||||
"truthyValuesDesc": "Comma-separated; default: true,1,yes,on",
|
||||
"invertFlag": "Invert flag",
|
||||
"invertFlagDesc": "Enable when the attribute means disabled (e.g. shadowInactive).",
|
||||
"syncSchedule": "Sync schedule",
|
||||
"syncScheduleDesc": "Cron-like string, e.g. @every 1m",
|
||||
"inboundTags": "Inbound tags",
|
||||
"inboundTagsDesc": "Inbounds that LDAP sync may auto-create or auto-delete clients on.",
|
||||
"noInbounds": "No inbounds found. Create one in Inbounds first.",
|
||||
"autoCreate": "Auto create clients",
|
||||
"autoDelete": "Auto delete clients",
|
||||
"defaultTotalGb": "Default total (GB)",
|
||||
"defaultExpiryDays": "Default expiry (days)",
|
||||
"defaultIpLimit": "Default IP limit"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Inject Xray finalmask TCP/UDP masks and QUIC parameters into every generated Xray JSON profile. Requires a client app that supports Xray JSON subscriptions and a recent Xray core.",
|
||||
@@ -1410,31 +1301,15 @@
|
||||
"muxSett": "Mux Settings",
|
||||
"direct": "Direct Connection",
|
||||
"directDesc": "Directly establishes connections with domains or IP ranges of a specific country.",
|
||||
"notifications": "Notifications",
|
||||
"certs": "Certificates",
|
||||
"externalTraffic": "External Traffic",
|
||||
"dateAndTime": "Date and Time",
|
||||
"proxyAndServer": "Proxy and Server",
|
||||
"intervals": "Intervals",
|
||||
"information": "Information",
|
||||
"profile": "Profile",
|
||||
"language": "Language",
|
||||
"telegramBotLanguage": "Telegram Bot Language",
|
||||
"security": {
|
||||
"admin": "Admin credentials",
|
||||
"twoFactor": "Two-factor authentication",
|
||||
"twoFactorEnable": "Enable 2FA",
|
||||
"twoFactorEnableDesc": "Adds an additional layer of authentication to provide more security.",
|
||||
"twoFactorModalSetTitle": "Enable two-factor authentication",
|
||||
"twoFactorModalDeleteTitle": "Disable two-factor authentication",
|
||||
"twoFactorModalSteps": "To set up two-factor authentication, perform a few steps:",
|
||||
"twoFactorModalFirstStep": "1. Scan this QR code in the app for authentication or copy the token near the QR code and paste it into the app",
|
||||
"twoFactorModalSecondStep": "2. Enter the code from the app",
|
||||
"twoFactorModalRemoveStep": "Enter the code from the application to remove two-factor authentication.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Change credentials",
|
||||
"twoFactorModalChangeCredentialsStep": "Enter the code from the application to change administrator credentials.",
|
||||
"twoFactorModalSetSuccess": "Two-factor authentication has been successfully established",
|
||||
"twoFactorModalDeleteSuccess": "Two-factor authentication has been successfully deleted",
|
||||
"twoFactorModalError": "Wrong code",
|
||||
"show": "Show",
|
||||
"hide": "Hide",
|
||||
@@ -1457,36 +1332,6 @@
|
||||
"getOutboundTrafficError": "Error getting traffic",
|
||||
"resetOutboundTrafficError": "Error resetting outbound traffic"
|
||||
},
|
||||
"smtpSettings": "SMTP Settings",
|
||||
"smtpEnable": "Enable Email Notifications",
|
||||
"smtpEnableDesc": "Enable email notifications via SMTP",
|
||||
"smtpHost": "SMTP Host",
|
||||
"smtpHostDesc": "SMTP server hostname (e.g. smtp.gmail.com)",
|
||||
"smtpPort": "SMTP Port",
|
||||
"smtpPortDesc": "SMTP server port (default: 587)",
|
||||
"smtpUsername": "SMTP Username",
|
||||
"smtpUsernameDesc": "SMTP authentication username",
|
||||
"smtpFrom": "SMTP From Address",
|
||||
"smtpFromDesc": "Sender address used in the email From header. Leave empty to use the username.",
|
||||
"smtpFromName": "SMTP Sender Name",
|
||||
"smtpFromNameDesc": "Optional display name shown before the sender address in the From header.",
|
||||
"smtpPassword": "SMTP Password",
|
||||
"smtpPasswordDesc": "SMTP authentication password",
|
||||
"smtpTo": "Recipients",
|
||||
"smtpToDesc": "Comma-separated recipient email addresses",
|
||||
"emailSettings": "Email",
|
||||
"emailNotifications": "Notifications",
|
||||
"smtpEventBusNotify": "Email Event Notifications",
|
||||
"smtpEventBusNotifyDesc": "Select which events trigger email notifications",
|
||||
"tgEventBusNotify": "Telegram Event Notifications",
|
||||
"tgEventBusNotifyDesc": "Select which events trigger Telegram notifications",
|
||||
"testSmtp": "Send Test Email",
|
||||
"testTgBot": "Send Test Message",
|
||||
"eventGroupOutbound": "Outbound",
|
||||
"eventGroupXray": "Xray Core",
|
||||
"eventGroupSystem": "System",
|
||||
"eventGroupSecurity": "Security",
|
||||
"eventGroupNode": "Nodes",
|
||||
"eventOutboundDown": "Down",
|
||||
"eventOutboundUp": "Up",
|
||||
"eventXrayCrash": "Crash",
|
||||
@@ -1494,36 +1339,7 @@
|
||||
"eventNodeUp": "Up",
|
||||
"eventCPUHigh": "CPU high (%)",
|
||||
"requestFailed": "Request failed",
|
||||
"smtpEncryption": "Encryption",
|
||||
"smtpEncryptionDesc": "SMTP connection encryption method",
|
||||
"smtpEncryptionNone": "None (plain text)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (implicit)",
|
||||
"smtpStageConnect": "Connection",
|
||||
"smtpStageAuth": "Authentication",
|
||||
"smtpStageSend": "Send",
|
||||
"smtpTestSuccess": "Test email sent successfully",
|
||||
"smtpHostNotConfigured": "SMTP host not configured",
|
||||
"smtpNoRecipients": "No recipients configured",
|
||||
"smtpFromNotConfigured": "SMTP sender address not configured",
|
||||
"eventLoginAttempt": "Login attempt",
|
||||
"telegramTokenConfigured": "Configured; leave blank to keep current token.",
|
||||
"telegramTokenPlaceholder": "Configured - enter a new token to replace",
|
||||
"smtpPasswordConfigured": "Configured; leave blank to keep current password.",
|
||||
"smtpPasswordPlaceholder": "Configured - enter a new password to replace",
|
||||
"smtpNotInitialized": "SMTP not initialized",
|
||||
"tgBotNotEnabled": "Telegram bot is not enabled",
|
||||
"tgTestFailed": "Telegram test failed",
|
||||
"tgTestSuccess": "Test message sent to Telegram",
|
||||
"tgBotNotRunning": "Telegram bot not running",
|
||||
"smtpErrorAuth": "Authentication failed — check username and password",
|
||||
"smtpErrorStarttls": "Server requires STARTTLS — change encryption type",
|
||||
"smtpErrorTls": "Server requires TLS — change encryption type",
|
||||
"smtpErrorRefused": "Connection refused — check host and port",
|
||||
"smtpErrorTimeout": "Connection timeout — host unreachable",
|
||||
"smtpErrorRelay": "Server rejects sending from this address",
|
||||
"smtpErrorEof": "Connection closed by server",
|
||||
"smtpErrorUnknown": "SMTP error: {{ .Error }}",
|
||||
"eventMemoryHigh": "Memory high (%)",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "Path must start with /"
|
||||
@@ -1605,8 +1421,6 @@
|
||||
"directdomains": "Direct Domains",
|
||||
"ipv4Routing": "IPv4 Routing",
|
||||
"ipv4RoutingDesc": "These options will route traffic based on a specific destination via IPv4.",
|
||||
"Template": "Advanced Xray Configuration Template",
|
||||
"TemplateDesc": "The final Xray config file will be generated based on this template.",
|
||||
"FreedomStrategy": "Freedom Protocol Strategy",
|
||||
"FreedomStrategyDesc": "Set the output strategy for the network in the Freedom Protocol.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1622,11 +1436,6 @@
|
||||
"Balancers": "Balancers",
|
||||
"balancerTagRequired": "Tag is required",
|
||||
"balancerSelectorRequired": "Pick at least one outbound",
|
||||
"balancerLive": "Live Target",
|
||||
"balancerOverride": "Override",
|
||||
"balancerOverridePh": "Auto (strategy)",
|
||||
"balancerLiveRefresh": "Refresh live balancer state",
|
||||
"balancerNotRunning": "This balancer is not active in the running Xray — save your changes or start Xray first",
|
||||
"routeTester": "Route Tester",
|
||||
"routeTesterDesc": "Ask the running Xray which outbound would handle a connection. No traffic is sent — the decision comes straight from the live routing engine.",
|
||||
"routeTesterDest": "Domain or IP",
|
||||
@@ -1643,7 +1452,6 @@
|
||||
"importOutbounds": "Import Outbounds",
|
||||
"exportOutbounds": "Export Outbounds",
|
||||
"importInvalidJson": "Invalid JSON — expected an array or an object with a matching key.",
|
||||
"completeTemplate": "All",
|
||||
"logLevel": "Log Level",
|
||||
"logLevelDesc": "The log level for error logs, indicating the information that needs to be recorded.",
|
||||
"accessLog": "Access Log",
|
||||
@@ -1674,7 +1482,6 @@
|
||||
"source": "Source",
|
||||
"dest": "Destination",
|
||||
"inbound": "Inbound",
|
||||
"balancer": "Balancer",
|
||||
"useComma": "Comma-separated list"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1855,32 +1662,7 @@
|
||||
"alreadyAdded": "This server is already in the outbound list. Use {reset} to renew its key.",
|
||||
"provisionFailed": "Could not build the PIA outbound. Try again."
|
||||
},
|
||||
"tabBalancerSettings": "Balancer Settings",
|
||||
"tabObservatory": "Observatory",
|
||||
"observatory": {
|
||||
"autoManaged": "Observers are managed automatically from your balancers. Tune how they probe below — the watched outbounds follow your balancer selectors.",
|
||||
"emptyHint": "No connection observer is active. One is added automatically when you create a Least Ping or Least Load balancer — or a Random / Round-robin balancer with a fallback — so observer-backed balancers can check outbound health before choosing a target.",
|
||||
"mixedLegacy": "This config contains both Observatory and Burst Observatory. Xray uses one global observer, so this mixed legacy state is not supported; saving balancers will normalize it to one observer.",
|
||||
"subjectSelector": "Watched Outbounds",
|
||||
"subjectSelectorDesc": "Outbound tags this observer probes. Managed automatically from your balancers.",
|
||||
"probeURL": "Probe URL",
|
||||
"probeURLDesc": "URL fetched to measure each outbound. Should return HTTP 204.",
|
||||
"probeInterval": "Probe Interval",
|
||||
"probeIntervalDesc": "How often to probe each outbound, e.g. 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Concurrent Probing",
|
||||
"enableConcurrencyDesc": "Probe all watched outbounds at once instead of one-by-one. Faster, but more visible on the network.",
|
||||
"destination": "Probe Destination",
|
||||
"destinationDesc": "URL fetched to measure each outbound. Should return HTTP 204.",
|
||||
"connectivity": "Connectivity Check",
|
||||
"connectivityDesc": "Optional local-network check URL, tried only after the destination fails. Leave empty to skip.",
|
||||
"interval": "Probe Interval",
|
||||
"intervalDesc": "Average time between probes per outbound, e.g. 1m. Minimum 10s.",
|
||||
"timeout": "Probe Timeout",
|
||||
"timeoutDesc": "How long to wait for a probe before it counts as failed, e.g. 5s.",
|
||||
"sampling": "Sampling Count",
|
||||
"samplingDesc": "Number of recent probe results kept to score each outbound.",
|
||||
"httpMethod": "HTTP Method",
|
||||
"httpMethodDesc": "HTTP method used for probes.",
|
||||
"deleteAlsoObservatory": "This is the last balancer using the Observatory, so it will be removed too.",
|
||||
"deleteAlsoBurst": "This is the last balancer using the Burst Observatory, so it will be removed too."
|
||||
},
|
||||
@@ -1892,26 +1674,8 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Strategy",
|
||||
"tag": "Tag",
|
||||
"tagDuplicate": "Tag already used by another balancer",
|
||||
"tagPlaceholder": "unique balancer tag",
|
||||
"selector": "Selector",
|
||||
"fallback": "Fallback",
|
||||
"fallbackBalancerHint": "Select another balancer as fallback",
|
||||
"balancerFallbackInfo": "Traffic will be routed through: Balancer → Loopback → Server → Target Balancer → Outbound. This adds an extra hop through the server, which may introduce slight delays.",
|
||||
"balancerFallbackCycle": "Cannot set this balancer as fallback — it would create a circular dependency.",
|
||||
"balancerDeleteInUse": "Cannot delete this balancer — it is used as fallback by: {names}",
|
||||
"reservedPrefix": "_bl_ prefix is reserved for internal balancer loopback objects",
|
||||
"cycleTooltip": "Cycle: {path} → (back to {start})",
|
||||
"expected": "Expected",
|
||||
"expectedPlaceholder": "optimal node count",
|
||||
"maxRtt": "Max RTT",
|
||||
"tolerance": "Tolerance",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Tag pattern",
|
||||
"costValue": "Weight",
|
||||
"costRegexp": "Regular expression match"
|
||||
"reservedPrefix": "_bl_ prefix is reserved for internal balancer loopback objects"
|
||||
},
|
||||
"wireguard": {
|
||||
"secretKey": "Secret Key",
|
||||
@@ -2013,252 +1777,14 @@
|
||||
"usage": "Usage",
|
||||
"addOutbound": "Add outbound"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Enable DNS",
|
||||
"enableDesc": "Enable built-in DNS server",
|
||||
"tag": "DNS Inbound Tag",
|
||||
"tagDesc": "This tag will be available as an Inbound tag in routing rules.",
|
||||
"clientIp": "Client IP",
|
||||
"clientIpDesc": "Used to notify the server of the specified IP location during DNS queries",
|
||||
"disableCache": "Disable cache",
|
||||
"disableCacheDesc": "Disables DNS caching",
|
||||
"disableFallback": "Disable Fallback",
|
||||
"disableFallbackDesc": "Disables fallback DNS queries",
|
||||
"disableFallbackIfMatch": "Disable Fallback If Match",
|
||||
"disableFallbackIfMatchDesc": "Disables fallback DNS queries when the matching domain list of the DNS server is hit",
|
||||
"enableParallelQuery": "Enable Parallel Query",
|
||||
"enableParallelQueryDesc": "Enable parallel DNS queries to multiple servers for faster resolution",
|
||||
"strategy": "Query Strategy",
|
||||
"strategyDesc": "Overall strategy to resolve domain names",
|
||||
"add": "Add Server",
|
||||
"edit": "Edit Server",
|
||||
"domains": "Domains",
|
||||
"expectIPs": "Expect IPs",
|
||||
"unexpectIPs": "Unexpected IPs",
|
||||
"useSystemHosts": "Use System Hosts",
|
||||
"useSystemHostsDesc": "Use the operating system's hosts file",
|
||||
"serveStale": "Serve Stale",
|
||||
"serveStaleDesc": "Return expired cached results while refreshing in the background",
|
||||
"serveExpiredTTL": "Serve Expired TTL",
|
||||
"serveExpiredTTLDesc": "Validity (seconds) of stale cache entries; 0 = never expire",
|
||||
"timeoutMs": "Timeout (ms)",
|
||||
"skipFallback": "Skip Fallback",
|
||||
"finalQuery": "Final Query",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Add Host",
|
||||
"hostsEmpty": "No host overrides defined",
|
||||
"hostsDomain": "Domain (e.g. domain:example.com)",
|
||||
"hostsValues": "IP or domain — type and press Enter",
|
||||
"usePreset": "Use Preset",
|
||||
"dnsPresetTitle": "DNS Presets",
|
||||
"dnsPresetFamily": "Family",
|
||||
"clearAll": "Delete All",
|
||||
"clearAllTitle": "Delete all DNS servers?",
|
||||
"clearAllConfirm": "This removes every DNS server from the list. This cannot be undone.",
|
||||
"dnsLeakWarning": "DNS can leak through localhost, plain UDP/TCP, local-mode DoH/DoQ, fallback queries, or EDNS client IP. Use routed DoH, hosts pins, and disable fallback when privacy matters."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Add Fake DNS",
|
||||
"ipPool": "IP Pool Subnet",
|
||||
"poolSize": "Pool Size"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Default Outbound",
|
||||
"defaultOutboundDesc": "Traffic that does not match any routing rule uses this outbound (Xray uses the first outbound in the list)."
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Custom keyboard closed!",
|
||||
"noResult": "❗ No result!",
|
||||
"noQuery": "❌ Query not found! Please use the command again!",
|
||||
"wentWrong": "❌ Something went wrong!",
|
||||
"noIpRecord": "❗ No IP Record!",
|
||||
"noInbounds": "❗ No inbound found!",
|
||||
"unlimited": "♾ Unlimited(Reset)",
|
||||
"add": "Add",
|
||||
"month": "Month",
|
||||
"months": "Months",
|
||||
"days": "Days",
|
||||
"hours": "Hours",
|
||||
"minutes": "Minutes",
|
||||
"unknown": "Unknown",
|
||||
"inbounds": "Inbounds",
|
||||
"clients": "Clients",
|
||||
"offline": "🔴 Offline",
|
||||
"online": "🟢 Online",
|
||||
"commands": {
|
||||
"unknown": "❗ Unknown command.",
|
||||
"pleaseChoose": "👇 Please choose:\r\n",
|
||||
"help": "🤖 Welcome to this bot! It's designed to offer specific data from the web panel and allows you to make modifications as needed.\r\n\r\n",
|
||||
"start": "👋 Hello <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n",
|
||||
"status": "✅ Bot is OK!",
|
||||
"usage": "❗ Please provide a text to search!",
|
||||
"getID": "🆔 Your ID: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "To restart Xray Core:\r\n<code>/restart</code>\r\n\r\nTo search for a client email:\r\n<code>/usage [Email]</code>\r\n\r\nTo search for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "To search for statistics, use the following command:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Operation successful!",
|
||||
"restartFailed": "❗ Error in operation.\r\n\r\n<code>Error: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core is not running.",
|
||||
"startDesc": "Show the main menu",
|
||||
"helpDesc": "Bot help",
|
||||
"statusDesc": "Check bot status",
|
||||
"idDesc": "Show your Telegram ID",
|
||||
"usageDesc": "Show client usage: /usage email",
|
||||
"inboundDesc": "Search inbounds: /inbound remark (admin)",
|
||||
"restartDesc": "Restart Xray core (admin)",
|
||||
"clearallDesc": "Reset all clients' traffic (admin)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "CPU Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Error in user selection!",
|
||||
"userSaved": "✅ Telegram User saved.",
|
||||
"loginSuccess": "✅ Logged in to the panel successfully.\r\n",
|
||||
"loginFailed": "❗️Login attempt to the panel failed.\r\n",
|
||||
"report": "🕰 Scheduled Reports: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Date&Time: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Host: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 3X-UI Version: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray Version: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Uptime: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 System Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Status: {{ .State }}\r\n",
|
||||
"username": "👤 Username: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Reason: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Time: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Inbound: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Port: {{ .Port }}\r\n",
|
||||
"expire": "📅 Expire Date: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Expire In: {{ .Time }}\r\n",
|
||||
"active": "💡 Active: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Enabled: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Connection status: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Last online: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Upload: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Download: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Telegram User: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Exhausted {{ .Type }} count:\r\n",
|
||||
"onlinesCount": "🌐 Online Clients: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Disabled: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Deplete Soon: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Backup Time: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Refreshed On: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Yes",
|
||||
"no": "❌ No",
|
||||
"received_email": "📧📥 Email updated.",
|
||||
"received_comment": "💬📥 Comment updated.",
|
||||
"email_prompt": "📧 Default Email: {{ .ClientEmail }}\n\nEnter your email.",
|
||||
"comment_prompt": "💬 Default Comment: {{ .ClientComment }}\n\nEnter your comment.",
|
||||
"cancel": "❌ Process Canceled! \n\nYou can /start again anytime. 🔄",
|
||||
"error_add_client": "⚠️ Error:\n\n {{ .error }}",
|
||||
"using_default_value": "Okay, I'll stick with the default value. 😊",
|
||||
"incorrect_input": "Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫",
|
||||
"AreYouSure": "Are you sure? 🤔",
|
||||
"SuccessResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success",
|
||||
"FailedResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠️ Error: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Traffic reset process finished for all clients.",
|
||||
"eventOutboundDown": "Outbound {{ .Tag }} is DOWN",
|
||||
"eventOutboundUp": "Outbound {{ .Tag }} is UP",
|
||||
"eventErrorDetail": "Error: {{ .Error }}",
|
||||
"eventDelayDetail": "Delay: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xray CRASHED",
|
||||
"eventXrayCrashError": "Error: {{ .Error }}",
|
||||
"eventNodeDown": "Node {{ .Name }} is DOWN",
|
||||
"eventNodeUp": "Node {{ .Name }} is UP",
|
||||
"eventLoginFallback": "Login failed from {{ .Source }}",
|
||||
"memoryThreshold": "Memory Load {{ .Percent }}% exceeds the threshold of {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Close Keyboard",
|
||||
"cancel": "❌ Cancel",
|
||||
"cancelReset": "❌ Cancel Reset",
|
||||
"cancelIpLimit": "❌ Cancel IP Limit",
|
||||
"confirmResetTraffic": "✅ Confirm Reset Traffic?",
|
||||
"confirmClearIps": "✅ Confirm Clear IPs?",
|
||||
"confirmRemoveTGUser": "✅ Confirm Remove Telegram User?",
|
||||
"confirmToggle": "✅ Confirm Enable/Disable User?",
|
||||
"dbBackup": "Get DB Backup",
|
||||
"serverUsage": "Server Usage",
|
||||
"getInbounds": "Get Inbounds",
|
||||
"depleteSoon": "Deplete Soon",
|
||||
"clientUsage": "Get Usage",
|
||||
"onlines": "Online Clients",
|
||||
"commands": "Commands",
|
||||
"refresh": "🔄 Refresh",
|
||||
"clearIPs": "❌ Clear IPs",
|
||||
"removeTGUser": "❌ Remove Telegram User",
|
||||
"selectTGUser": "👤 Select Telegram User",
|
||||
"selectOneTGUser": "👤 Select a Telegram User:",
|
||||
"resetTraffic": "📈 Reset Traffic",
|
||||
"resetExpire": "📅 Change Expiry Date",
|
||||
"ipLog": "🔢 IP Log",
|
||||
"ipLimit": "🔢 IP Limit",
|
||||
"setTGUser": "👤 Set Telegram User",
|
||||
"toggle": "🔘 Enable / Disable",
|
||||
"custom": "🔢 Custom",
|
||||
"confirmNumber": "✅ Confirm: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Confirm adding: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Traffic Limit",
|
||||
"getBanLogs": "Get Ban Logs",
|
||||
"allClients": "All Clients",
|
||||
"addClient": "Add Client",
|
||||
"submitDisable": "Submit As Disable ☑️",
|
||||
"submitEnable": "Submit As Enable ✅",
|
||||
"use_default": "🏷️ Use default",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Comment",
|
||||
"ResetAllTraffics": "Reset All Traffic",
|
||||
"SortedTrafficUsageReport": "Sorted Traffic Usage Report"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Operation successful!",
|
||||
"errorOperation": "❗ Error in operation.",
|
||||
"getInboundsFailed": "❌ Failed to get inbounds.",
|
||||
"getClientsFailed": "❌ Failed to get clients.",
|
||||
"canceled": "❌ {{ .Email }}: Operation canceled.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Client refreshed successfully.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IPs refreshed successfully.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Client's Telegram User refreshed successfully.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Traffic reset successfully.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Traffic limit saved successfully.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Expire days reset successfully.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: IP limit {{ .Count }} saved successfully.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IPs cleared successfully.",
|
||||
"getIpLog": "✅ {{ .Email }}: Get IP Log.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Get Telegram User Info.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Telegram User removed successfully.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Enabled successfully.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Disabled successfully.",
|
||||
"askToAddUserId": "Your configuration is not found!\r\nPlease ask your admin to use your Telegram ChatID in your configuration(s).\r\n\r\nYour ChatID: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Choose a Client for Inbound {{ .Inbound }}",
|
||||
"chooseInbound": "Choose an Inbound"
|
||||
}
|
||||
"noIpRecord": "❗ No IP Record!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Status",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelNode": "Node",
|
||||
"labelError": "Error",
|
||||
"labelDelay": "Delay",
|
||||
"labelUsername": "Username",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Reason",
|
||||
"labelSource": "Source",
|
||||
"statusCrashed": "CRASHED",
|
||||
"statusHigh": "HIGH",
|
||||
"statusSuccess": "SUCCESS",
|
||||
"statusFailed": "FAILED",
|
||||
"statusDown": "DOWN",
|
||||
"statusUp": "UP"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Éxito",
|
||||
"lastOnline": "Última conexión",
|
||||
"lastSubFetch": "Última descarga de suscripción",
|
||||
"getVersion": "Obtener versión",
|
||||
"install": "Instalar",
|
||||
"clients": "Clientes",
|
||||
"usage": "Uso",
|
||||
"twoFactorCode": "Código",
|
||||
"remained": "Restante",
|
||||
"security": "Seguridad",
|
||||
"emptyDnsDesc": "No hay servidores DNS añadidos.",
|
||||
"emptyFakeDnsDesc": "No hay servidores Fake DNS añadidos.",
|
||||
"emptyBalancersDesc": "No hay balanceadores añadidos.",
|
||||
"somethingWentWrong": "Algo salió mal",
|
||||
"subscription": {
|
||||
"title": "Información de suscripción",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Subido",
|
||||
"expiry": "Caducidad",
|
||||
"totalQuota": "Cuota total",
|
||||
"individualLinks": "Enlaces individuales",
|
||||
"active": "Activo",
|
||||
"inactive": "Inactivo",
|
||||
"unlimited": "Ilimitado",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Detener",
|
||||
"restartXray": "Reiniciar",
|
||||
"xraySwitch": "Versión",
|
||||
"xrayUpdates": "Actualizaciones de Xray",
|
||||
"xraySwitchClickDesk": "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales.",
|
||||
"updatePanel": "Actualizar panel",
|
||||
"panelUpdateDesc": "Esto actualizará 3X-UI a la última versión y reiniciará el servicio del panel.",
|
||||
"currentPanelVersion": "Versión actual del panel",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Descargar",
|
||||
"sent": "Enviado",
|
||||
"received": "Recibido",
|
||||
"xraySwitchVersionDialog": "¿Realmente deseas cambiar la versión de Xray?",
|
||||
"xraySwitchVersionDialogDesc": "Esto cambiará la versión de Xray a #version#.",
|
||||
"xraySwitchVersionPopover": "Xray se actualizó correctamente",
|
||||
"panelUpdateDialog": "¿Deseas actualizar el panel?",
|
||||
"panelUpdateDialogDesc": "Esto actualizará 3X-UI a la versión #version# y reiniciará el servicio del panel.",
|
||||
"panelUpdateStartedPopover": "Actualización del panel iniciada",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "La actualización no se completó correctamente. Revisa los registros del servidor o ejecuta 'x-ui update' desde la línea de comandos.",
|
||||
"panelUpdateUnknownTitle": "No se pudo confirmar si la actualización terminó",
|
||||
"panelUpdateUnknownDesc": "El panel no informó un resultado a tiempo. Recarga la página para comprobar la versión actual, o revisa los registros del servidor.",
|
||||
"geofileUpdateDialog": "¿Realmente deseas actualizar el geofichero?",
|
||||
"geofileUpdateDialogDesc": "Esto actualizará el archivo #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Esto actualizará todos los archivos.",
|
||||
"geofilesUpdateAll": "Actualizar todo",
|
||||
"geofileUpdatePopover": "Geofichero actualizado correctamente",
|
||||
"geodataTitle": "Actualización automática de Geodata",
|
||||
"geodataHint": "Xray descarga estos archivos según la programación y los recarga en caliente sin reiniciar. Las URL deben ser HTTPS. Cada archivo debe existir previamente en la carpeta bin para que Xray pueda actualizarlo.",
|
||||
"geodataCron": "Programación (cron)",
|
||||
"geodataOutbound": "Descargar a través de outbound (opcional)",
|
||||
"geodataFile": "Nombre de archivo",
|
||||
"geodataAddFile": "Añadir archivo",
|
||||
"geodataSaveRestart": "Guardar y reiniciar Xray",
|
||||
"geodataConfirmTitle": "¿Guardar la configuración de geodata?",
|
||||
"geodataConfirmContent": "Se actualizará la plantilla de configuración de Xray y se reiniciará Xray.",
|
||||
"geodataInvalidUrl": "Cada archivo necesita una URL HTTPS.",
|
||||
"geodataInvalidFile": "El nombre de archivo debe ser simple, p. ej. geosite_custom.dat (sin rutas).",
|
||||
"geodataInvalidCron": "Cron debe tener 5 campos, p. ej. 0 4 * * *",
|
||||
"geodataEmpty": "No hay archivos configurados. En las reglas de enrutamiento se referencian como ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "La instalación está en progreso, por favor no actualices esta página.",
|
||||
"logs": "Registros",
|
||||
"accessLogs": "Registros de acceso",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "La ruta base por defecto \"/\" es conocida — cámbiela a una ruta aleatoria.",
|
||||
"warnDefaultSubPath": "La ruta de suscripción por defecto \"/sub/\" es conocida — cámbiela.",
|
||||
"warnDefaultJsonPath": "La ruta de suscripción JSON por defecto \"/json/\" es conocida — cámbiela.",
|
||||
"TGBotSettings": "Bot de Telegram",
|
||||
"panelListeningIP": "IP de Escucha del Panel",
|
||||
"panelListeningIPDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
|
||||
"panelListeningDomain": "Dominio de Escucha del Panel",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Contraseña Actual",
|
||||
"newUsername": "Nuevo Nombre de Usuario",
|
||||
"newPassword": "Nueva Contraseña",
|
||||
"telegramBotEnable": "Habilitar bot de Telegram",
|
||||
"telegramBotEnableDesc": "Conéctese a las funciones de este panel a través del bot de Telegram.",
|
||||
"telegramToken": "Token de Telegram",
|
||||
"telegramTokenDesc": "Debe obtener el token del administrador de bots de Telegram {'@'}botfather.",
|
||||
"telegramProxy": "Proxy SOCKS",
|
||||
"telegramProxyDesc": "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía.",
|
||||
"telegramAPIServer": "Servidor API de Telegram",
|
||||
"telegramAPIServerDesc": "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado.",
|
||||
"telegramChatId": "IDs de Chat de Telegram para Administradores",
|
||||
"telegramChatIdDesc": "IDs de Chat múltiples separados por comas. Use {'@'}userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat.",
|
||||
"telegramNotifyTime": "Hora de Notificación del Bot de Telegram",
|
||||
"telegramNotifyTimeDesc": "Con qué frecuencia el bot de Telegram envía informes periódicos. Elige un intervalo predefinido o selecciona Personalizado para introducir una expresión crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — repetir en un intervalo",
|
||||
"hourly": "@hourly — cada hora",
|
||||
"daily": "@daily — cada día a las 00:00",
|
||||
"weekly": "@weekly — cada semana",
|
||||
"monthly": "@monthly — cada mes",
|
||||
"custom": "Personalizado (crontab)",
|
||||
"seconds": "Segundos",
|
||||
"minutes": "Minutos",
|
||||
"hours": "Horas",
|
||||
"interval": "Intervalo",
|
||||
"unit": "Unidad"
|
||||
},
|
||||
"tgNotifyBackup": "Respaldo de Base de Datos",
|
||||
"tgNotifyBackupDesc": "Incluir archivo de respaldo de base de datos con notificación de informe.",
|
||||
"tgNotifyLogin": "Notificación de Inicio de Sesión",
|
||||
"tgNotifyLoginDesc": "Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel.",
|
||||
"sessionMaxAge": "Edad Máxima de Sesión",
|
||||
"sessionMaxAgeDesc": "La duración de una sesión de inicio de sesión (unidad: minutos).",
|
||||
"expireTimeDiff": "Umbral de Expiración para Notificación",
|
||||
"expireTimeDiffDesc": "Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días).",
|
||||
"trafficDiff": "Umbral de Tráfico para Notificación",
|
||||
"trafficDiffDesc": "Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB).",
|
||||
"tgNotifyCpu": "Umbral de Alerta de Porcentaje de CPU",
|
||||
"tgNotifyCpuDesc": "Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %).",
|
||||
"timeZone": "Zona Horaria",
|
||||
"timeZoneDesc": "Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria.",
|
||||
"subSettings": "Suscripción",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Encriptar las configuraciones devueltas en la suscripción.",
|
||||
"subURI": "URI de proxy inverso",
|
||||
"subURIDesc": "La URL base completa (scheme://dominio[:puerto]/ruta/) para el enlace de suscripción y el código QR, usada en lugar de Dominio de Escucha/Puerto de Suscripción. Configúrela cuando la suscripción se acceda a través de un proxy inverso o un dominio/puerto distinto a los anteriores.",
|
||||
"externalTrafficInformEnable": "Informe de tráfico externo",
|
||||
"externalTrafficInformEnableDesc": "Informar a una API externa en cada actualización de tráfico.",
|
||||
"externalTrafficInformURI": "URI de información de tráfico externo",
|
||||
"externalTrafficInformURIDesc": "Las actualizaciones de tráfico se envían a este URI.",
|
||||
"restartXrayOnClientDisable": "Reiniciar Xray tras desactivación automática",
|
||||
"restartXrayOnClientDisableDesc": "Cuando un cliente se desactive automáticamente por vencimiento o límite de tráfico, reiniciar Xray.",
|
||||
"fragment": "Fragmentación",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Configuración de Sonidos",
|
||||
"trustedProxyCidrs": "CIDR de proxy de confianza",
|
||||
"trustedProxyCidrsDesc": "IP/CIDR separados por coma que pueden establecer las cabeceras de host, proto e IP del cliente reenviadas.",
|
||||
"ldap": {
|
||||
"enable": "Habilitar sincronización LDAP",
|
||||
"host": "Host LDAP",
|
||||
"port": "Puerto LDAP",
|
||||
"useTls": "Usar TLS (LDAPS)",
|
||||
"skipTlsVerify": "Omitir verificación de certificado TLS",
|
||||
"skipTlsVerifyDesc": "Inseguro — desactiva la validación del certificado del servidor. Usar solo con CA internos/no confiables.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Configurada; deja en blanco para mantener la contraseña actual.",
|
||||
"passwordUnconfigured": "No configurada.",
|
||||
"passwordPlaceholder": "Configurada — introduce un nuevo valor para reemplazar",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Filtro de usuario",
|
||||
"userAttr": "Atributo de usuario (username/email)",
|
||||
"vlessField": "Atributo flag VLESS",
|
||||
"flagField": "Atributo flag genérico (opcional)",
|
||||
"flagFieldDesc": "Si se establece, sobrescribe el flag VLESS — p. ej. shadowInactive.",
|
||||
"truthyValues": "Valores truthy",
|
||||
"truthyValuesDesc": "Separados por coma; por defecto: true,1,yes,on",
|
||||
"invertFlag": "Invertir flag",
|
||||
"invertFlagDesc": "Habilita cuando el atributo significa «deshabilitado» (p. ej. shadowInactive).",
|
||||
"syncSchedule": "Programación de sincronización",
|
||||
"syncScheduleDesc": "Cadena tipo cron, p. ej. @every 1m",
|
||||
"inboundTags": "Etiquetas de entradas",
|
||||
"inboundTagsDesc": "Entradas en las que la sincronización LDAP puede auto-crear o auto-eliminar clientes.",
|
||||
"noInbounds": "No se encontraron entradas. Crea una en Entradas primero.",
|
||||
"autoCreate": "Crear clientes automáticamente",
|
||||
"autoDelete": "Eliminar clientes automáticamente",
|
||||
"defaultTotalGb": "Total por defecto (GB)",
|
||||
"defaultExpiryDays": "Caducidad por defecto (días)",
|
||||
"defaultIpLimit": "Límite IP por defecto"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Inyecta máscaras TCP/UDP de finalmask de Xray y parámetros QUIC en cada perfil Xray JSON generado. Requiere una aplicación compatible con suscripciones Xray JSON y un núcleo Xray reciente.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Configuración Mux",
|
||||
"direct": "Conexión Directa",
|
||||
"directDesc": "Establece conexiones directas con dominios o rangos de IP de un país específico.",
|
||||
"notifications": "Notificaciones",
|
||||
"certs": "Certificados",
|
||||
"externalTraffic": "Tráfico Externo",
|
||||
"dateAndTime": "Fecha y Hora",
|
||||
"proxyAndServer": "Proxy y Servidor",
|
||||
"intervals": "Intervalos",
|
||||
"information": "Información",
|
||||
"profile": "Perfil",
|
||||
"language": "Idioma",
|
||||
"telegramBotLanguage": "Idioma del Bot de Telegram",
|
||||
"security": {
|
||||
"admin": "Credenciales de administrador",
|
||||
"twoFactor": "Autenticación de dos factores",
|
||||
"twoFactorEnable": "Habilitar 2FA",
|
||||
"twoFactorEnableDesc": "Añade una capa adicional de autenticación para mayor seguridad.",
|
||||
"twoFactorModalSetTitle": "Activar autenticación de dos factores",
|
||||
"twoFactorModalDeleteTitle": "Desactivar autenticación de dos factores",
|
||||
"twoFactorModalSteps": "Para configurar la autenticación de dos factores, sigue estos pasos:",
|
||||
"twoFactorModalFirstStep": "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación",
|
||||
"twoFactorModalSecondStep": "2. Ingresa el código de la aplicación",
|
||||
"twoFactorModalRemoveStep": "Ingresa el código de la aplicación para eliminar la autenticación de dos factores.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Cambiar credenciales",
|
||||
"twoFactorModalChangeCredentialsStep": "Ingrese el código de la aplicación para cambiar las credenciales del administrador.",
|
||||
"twoFactorModalSetSuccess": "La autenticación de dos factores se ha establecido con éxito",
|
||||
"twoFactorModalDeleteSuccess": "La autenticación de dos factores se ha eliminado con éxito",
|
||||
"twoFactorModalError": "Código incorrecto",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Error al obtener el tráfico saliente",
|
||||
"resetOutboundTrafficError": "Error al reiniciar el tráfico saliente"
|
||||
},
|
||||
"smtpSettings": "Configuración de SMTP",
|
||||
"smtpEnable": "Activar notificaciones por correo",
|
||||
"smtpEnableDesc": "Activar notificaciones por correo mediante SMTP",
|
||||
"smtpHost": "Servidor SMTP",
|
||||
"smtpHostDesc": "Nombre del servidor SMTP (p. ej. smtp.gmail.com)",
|
||||
"smtpPort": "Puerto SMTP",
|
||||
"smtpPortDesc": "Puerto del servidor SMTP (predeterminado: 587)",
|
||||
"smtpUsername": "Usuario SMTP",
|
||||
"smtpUsernameDesc": "Usuario de autenticación SMTP",
|
||||
"smtpFrom": "Dirección de remitente (From)",
|
||||
"smtpFromDesc": "Dirección usada en el encabezado From del correo. Déjelo vacío para usar el nombre de usuario.",
|
||||
"smtpFromName": "Nombre del remitente (From)",
|
||||
"smtpFromNameDesc": "Nombre para mostrar opcional antes de la dirección en el encabezado From.",
|
||||
"smtpPassword": "Contraseña SMTP",
|
||||
"smtpPasswordDesc": "Contraseña de autenticación SMTP",
|
||||
"smtpTo": "Destinatarios",
|
||||
"smtpToDesc": "Direcciones de correo de los destinatarios separadas por comas",
|
||||
"emailSettings": "Correo",
|
||||
"emailNotifications": "Notificaciones",
|
||||
"smtpEventBusNotify": "Notificaciones por correo de eventos",
|
||||
"smtpEventBusNotifyDesc": "Seleccione qué eventos generan notificaciones por correo",
|
||||
"tgEventBusNotify": "Notificaciones de Telegram de eventos",
|
||||
"tgEventBusNotifyDesc": "Seleccione qué eventos generan notificaciones de Telegram",
|
||||
"testSmtp": "Enviar correo de prueba",
|
||||
"testTgBot": "Enviar mensaje de prueba",
|
||||
"eventGroupOutbound": "Saliente",
|
||||
"eventGroupXray": "Núcleo de Xray",
|
||||
"eventGroupSystem": "Sistema",
|
||||
"eventGroupSecurity": "Seguridad",
|
||||
"eventGroupNode": "Nodos",
|
||||
"eventOutboundDown": "Caído",
|
||||
"eventOutboundUp": "Activo",
|
||||
"eventXrayCrash": "Caída",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Activo",
|
||||
"eventCPUHigh": "CPU alta (%)",
|
||||
"requestFailed": "La solicitud falló",
|
||||
"smtpEncryption": "Cifrado",
|
||||
"smtpEncryptionDesc": "Método de cifrado de la conexión SMTP",
|
||||
"smtpEncryptionNone": "Ninguno (texto sin cifrar)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (implícito)",
|
||||
"smtpStageConnect": "Conexión",
|
||||
"smtpStageAuth": "Autenticación",
|
||||
"smtpStageSend": "Envío",
|
||||
"smtpTestSuccess": "Correo de prueba enviado correctamente",
|
||||
"smtpHostNotConfigured": "Servidor SMTP no configurado",
|
||||
"smtpNoRecipients": "No hay destinatarios configurados",
|
||||
"smtpFromNotConfigured": "La dirección del remitente SMTP no está configurada",
|
||||
"eventLoginAttempt": "Intento de inicio de sesión",
|
||||
"telegramTokenConfigured": "Configurado; deje en blanco para mantener el token actual.",
|
||||
"telegramTokenPlaceholder": "Configurado: introduzca un nuevo token para reemplazarlo",
|
||||
"smtpPasswordConfigured": "Configurada; deje en blanco para mantener la contraseña actual.",
|
||||
"smtpPasswordPlaceholder": "Configurada: introduzca una nueva contraseña para reemplazarla",
|
||||
"smtpNotInitialized": "SMTP no inicializado",
|
||||
"tgBotNotEnabled": "El bot de Telegram no está activado",
|
||||
"tgTestFailed": "La prueba de Telegram falló",
|
||||
"tgTestSuccess": "Mensaje de prueba enviado a Telegram",
|
||||
"tgBotNotRunning": "El bot de Telegram no está en ejecución",
|
||||
"smtpErrorAuth": "Error de autenticación: compruebe el usuario y la contraseña",
|
||||
"smtpErrorStarttls": "El servidor requiere STARTTLS: cambie el tipo de cifrado",
|
||||
"smtpErrorTls": "El servidor requiere TLS: cambie el tipo de cifrado",
|
||||
"smtpErrorRefused": "Conexión rechazada: compruebe el servidor y el puerto",
|
||||
"smtpErrorTimeout": "Tiempo de conexión agotado: servidor inaccesible",
|
||||
"smtpErrorRelay": "El servidor rechaza el envío desde esta dirección",
|
||||
"smtpErrorEof": "Conexión cerrada por el servidor",
|
||||
"smtpErrorUnknown": "Error de SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Uso de memoria alto (%)",
|
||||
"remarkTemplate": "Plantilla de notas",
|
||||
"remarkTemplateDesc": "Cuando se define, esto reemplaza el modelo de notas para cada enlace de suscripción — escribe tu propio formato con los tokens de variable (usa el botón para insertarlos). Déjalo vacío para usar el modelo anterior.",
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "Dominios Directos",
|
||||
"ipv4Routing": "Enrutamiento IPv4",
|
||||
"ipv4RoutingDesc": "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4.",
|
||||
"Template": "Plantilla de Configuración de Xray",
|
||||
"TemplateDesc": "Genera el archivo de configuración final de Xray basado en esta plantilla.",
|
||||
"FreedomStrategy": "Configurar Estrategia para el Protocolo Freedom",
|
||||
"FreedomStrategyDesc": "Establece la estrategia de salida de la red en el Protocolo Freedom.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1492,11 +1318,6 @@
|
||||
"Balancers": "Equilibradores",
|
||||
"balancerTagRequired": "La etiqueta es obligatoria",
|
||||
"balancerSelectorRequired": "Elige al menos una salida",
|
||||
"balancerLive": "Destino actual",
|
||||
"balancerOverride": "Forzar destino",
|
||||
"balancerOverridePh": "Automático (estrategia)",
|
||||
"balancerLiveRefresh": "Actualizar estado del balanceador",
|
||||
"balancerNotRunning": "Este balanceador no está activo en el Xray en ejecución — guarda los cambios o inicia Xray primero",
|
||||
"routeTester": "Prueba de ruta",
|
||||
"routeTesterDesc": "Pregunta al Xray en ejecución qué salida gestionaría una conexión. No se envía tráfico real — la decisión viene directamente del motor de enrutamiento en vivo.",
|
||||
"routeTesterDest": "Dominio o IP",
|
||||
@@ -1508,7 +1329,6 @@
|
||||
"routeTesterViaBalancer": "vía balanceador",
|
||||
"routeTesterDefaultOutbound": "Ninguna regla de enrutamiento coincidió — el tráfico va a la salida predeterminada (primera).",
|
||||
"Routings": "Reglas de enrutamiento",
|
||||
"completeTemplate": "Todo",
|
||||
"logLevel": "Nivel de registro",
|
||||
"logLevelDesc": "El nivel de registro para registros de errores, que indica la información que debe registrarse.",
|
||||
"accessLog": "Registro de acceso",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Fuente",
|
||||
"dest": "Destino",
|
||||
"inbound": "Entrante",
|
||||
"balancer": "Equilibrador",
|
||||
"useComma": "Elementos separados por comas"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Este servidor ya está en la lista de salidas. Usa {reset} para renovar la clave.",
|
||||
"provisionFailed": "No se pudo crear la salida PIA. Inténtalo de nuevo."
|
||||
},
|
||||
"tabBalancerSettings": "Ajustes del balanceador",
|
||||
"tabObservatory": "Observatorio",
|
||||
"observatory": {
|
||||
"autoManaged": "Los observadores se gestionan automáticamente a partir de tus balanceadores. Ajusta abajo cómo sondean; las salidas vigiladas siguen los selectores del balanceador.",
|
||||
"emptyHint": "No hay ningún observador de conexión activo. Se añade uno automáticamente al crear un balanceador Least Ping o Least Load —o un balanceador Random / Round-robin con fallback— para que los balanceadores que usan observador puedan comprobar la salud de las salidas antes de elegir un destino.",
|
||||
"mixedLegacy": "Esta configuración contiene Observatory y Burst Observatory a la vez. Xray usa un único observador global, por lo que este estado mixto heredado no está soportado; al guardar balanceadores se normalizará a un solo observador.",
|
||||
"subjectSelector": "Salidas vigiladas",
|
||||
"subjectSelectorDesc": "Etiquetas de salida que sondea este observador. Se gestionan automáticamente a partir de tus balanceadores.",
|
||||
"probeURL": "URL de sondeo",
|
||||
"probeURLDesc": "URL solicitada para medir cada salida. Debe devolver HTTP 204.",
|
||||
"probeInterval": "Intervalo de sondeo",
|
||||
"probeIntervalDesc": "Con qué frecuencia se sondea cada salida, p. ej. 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Sondeo concurrente",
|
||||
"enableConcurrencyDesc": "Sondea todas las salidas vigiladas a la vez en lugar de una a una. Más rápido, pero más visible en la red.",
|
||||
"destination": "Destino de sondeo",
|
||||
"destinationDesc": "URL solicitada para medir cada salida. Debe devolver HTTP 204.",
|
||||
"connectivity": "Comprobación de conectividad",
|
||||
"connectivityDesc": "URL opcional de comprobación de red local, se prueba solo si falla el destino. Déjalo vacío para omitir.",
|
||||
"interval": "Intervalo de sondeo",
|
||||
"intervalDesc": "Tiempo medio entre sondeos por salida, p. ej. 1m. Mínimo 10s.",
|
||||
"timeout": "Tiempo de espera del sondeo",
|
||||
"timeoutDesc": "Cuánto esperar un sondeo antes de considerarlo fallido, p. ej. 5s.",
|
||||
"sampling": "Número de muestras",
|
||||
"samplingDesc": "Número de resultados de sondeo recientes que se conservan para puntuar cada salida.",
|
||||
"httpMethod": "Método HTTP",
|
||||
"httpMethodDesc": "Método HTTP usado para los sondeos.",
|
||||
"deleteAlsoObservatory": "Este es el último balanceador que usa el Observatorio, por lo que también se eliminará.",
|
||||
"deleteAlsoBurst": "Este es el último balanceador que usa el Observatorio Burst, por lo que también se eliminará."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Estrategia",
|
||||
"tag": "Etiqueta",
|
||||
"tagDuplicate": "Etiqueta ya usada por otro balanceador",
|
||||
"tagPlaceholder": "etiqueta única de balanceador",
|
||||
"selector": "Selector",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Ciclo: {path} → (volver a {start})",
|
||||
"expected": "Esperado",
|
||||
"expectedPlaceholder": "número óptimo de nodos",
|
||||
"maxRtt": "Máx. RTT",
|
||||
"tolerance": "Tolerancia",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Patrón de etiqueta",
|
||||
"costValue": "Peso",
|
||||
"costRegexp": "Coincidencia por expresión regular",
|
||||
"balancerDeleteInUse": "No se puede eliminar este balanceador — se usa como respaldo para: {names}",
|
||||
"balancerFallbackCycle": "No se puede establecer este balanceador como respaldo — crearía una dependencia circular.",
|
||||
"balancerFallbackInfo": "El tráfico se enrutará a través de: Balanceador → Loopback → Servidor → Balanceador destino → Conexión saliente. Esto agrega un salto adicional a través del servidor, lo que puede introducir ligeros retrasos.",
|
||||
"fallbackBalancerHint": "Seleccione otro balanceador como respaldo",
|
||||
"reservedPrefix": "El prefijo _bl_ está reservado para objetos loopback internos del balanceador"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Uso",
|
||||
"addOutbound": "Añadir salida"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Habilitar DNS",
|
||||
"enableDesc": "Habilitar servidor DNS incorporado",
|
||||
"tag": "Etiqueta de Entrada DNS",
|
||||
"tagDesc": "Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento.",
|
||||
"clientIp": "IP del cliente",
|
||||
"clientIpDesc": "Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS",
|
||||
"disableCache": "Desactivar caché",
|
||||
"disableCacheDesc": "Desactiva el almacenamiento en caché de DNS",
|
||||
"disableFallback": "Desactivar respaldo",
|
||||
"disableFallbackDesc": "Desactiva las consultas DNS de respaldo",
|
||||
"disableFallbackIfMatch": "Desactivar respaldo si coincide",
|
||||
"disableFallbackIfMatchDesc": "Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS",
|
||||
"enableParallelQuery": "Habilitar consulta paralela",
|
||||
"enableParallelQueryDesc": "Habilitar consultas DNS paralelas a múltiples servidores para una resolución más rápida",
|
||||
"strategy": "Estrategia de Consulta",
|
||||
"strategyDesc": "Estrategia general para resolver nombres de dominio",
|
||||
"add": "Agregar Servidor",
|
||||
"edit": "Editar Servidor",
|
||||
"domains": "Dominios",
|
||||
"expectIPs": "IPs esperadas",
|
||||
"unexpectIPs": "IPs inesperadas",
|
||||
"useSystemHosts": "Usar Hosts del sistema",
|
||||
"useSystemHostsDesc": "Usar el archivo hosts de un sistema instalado",
|
||||
"serveStale": "Servir caducados",
|
||||
"serveStaleDesc": "Devolver resultados caducados de la caché mientras se actualiza en segundo plano",
|
||||
"serveExpiredTTL": "TTL de caducados",
|
||||
"serveExpiredTTLDesc": "Validez (segundos) de las entradas caducadas en la caché; 0 = nunca caduca",
|
||||
"timeoutMs": "Tiempo de espera (ms)",
|
||||
"skipFallback": "Omitir respaldo",
|
||||
"finalQuery": "Consulta final",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Agregar Host",
|
||||
"hostsEmpty": "No hay Hosts definidos",
|
||||
"hostsDomain": "Dominio (ej. domain:example.com)",
|
||||
"hostsValues": "IP o dominio — escribe y presiona Enter",
|
||||
"usePreset": "Usar plantilla",
|
||||
"dnsPresetTitle": "Plantillas DNS",
|
||||
"dnsPresetFamily": "Familiar",
|
||||
"clearAll": "Eliminar todos",
|
||||
"clearAllTitle": "¿Eliminar todos los servidores DNS?",
|
||||
"clearAllConfirm": "Esto eliminará todos los servidores DNS de la lista. No se puede deshacer.",
|
||||
"dnsLeakWarning": "El DNS puede filtrarse por localhost, UDP/TCP sin cifrar, DoH/DoQ en modo local, consultas de respaldo o EDNS client IP. Usa DoH enrutado, fija los resolutores en hosts y desactiva el respaldo cuando la privacidad importe."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Agregar DNS Falso",
|
||||
"ipPool": "Subred del grupo de IP",
|
||||
"poolSize": "Tamaño del grupo"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Salida predeterminada",
|
||||
"defaultOutboundDesc": "El tráfico que no coincide con ninguna regla de enrutamiento usa esta salida (la primera de la lista)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Teclado cerrado!",
|
||||
"noResult": "❗ ¡Sin resultados!",
|
||||
"noQuery": "❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!",
|
||||
"wentWrong": "❌ ¡Algo salió mal!",
|
||||
"noIpRecord": "❗ ¡No hay registro de IP!",
|
||||
"noInbounds": "❗ ¡No se encontraron entradas!",
|
||||
"unlimited": "♾ Ilimitado (Restablecer)",
|
||||
"add": "Añadir",
|
||||
"month": "Mes",
|
||||
"months": "Meses",
|
||||
"days": "Días",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"unknown": "Desconocido",
|
||||
"inbounds": "Entradas",
|
||||
"clients": "Clientes",
|
||||
"offline": "🔴 Sin conexión",
|
||||
"online": "🟢 En línea",
|
||||
"commands": {
|
||||
"unknown": "❗ Comando desconocido",
|
||||
"pleaseChoose": "👇 Por favor elige:\r\n",
|
||||
"help": "🤖 ¡Bienvenido a este bot! Está diseñado para ofrecerte datos específicos del servidor y te permite hacer modificaciones según sea necesario.\r\n\r\n",
|
||||
"start": "👋 Hola <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Bienvenido al bot de gestión de <b>{{ .Hostname }}</b>.\r\n",
|
||||
"status": "✅ ¡El bot está bien!",
|
||||
"usage": "❗ ¡Por favor proporciona un texto para buscar!",
|
||||
"getID": "🆔 Tu ID: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Para reiniciar Xray Core:\r\n<code>/restart</code>\r\n\r\nPara buscar un correo electrónico de cliente:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Observación]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "Para buscar estadísticas, utiliza el siguiente comando:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ ¡Operación exitosa!",
|
||||
"restartFailed": "❗ Error en la operación.\r\n\r\n<code>Error: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core no está en ejecución.",
|
||||
"startDesc": "Mostrar el menú principal",
|
||||
"helpDesc": "Ayuda del bot",
|
||||
"statusDesc": "Comprobar el estado del bot",
|
||||
"idDesc": "Mostrar tu ID de Telegram",
|
||||
"usageDesc": "Ver el uso del cliente: /usage correo",
|
||||
"inboundDesc": "Buscar entradas: /inbound nombre (admin)",
|
||||
"restartDesc": "Reiniciar el núcleo de Xray (admin)",
|
||||
"clearallDesc": "Restablecer el tráfico de todos los clientes (admin)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ ¡Error al seleccionar usuario!",
|
||||
"userSaved": "✅ Usuario de Telegram guardado.",
|
||||
"loginSuccess": "✅ Has iniciado sesión en el panel con éxito.\r\n",
|
||||
"loginFailed": "❗️ Falló el inicio de sesión en el panel.\r\n",
|
||||
"report": "🕰 Informes programados: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Fecha y Hora: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Host: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 Versión de X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Versión de Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Estado: {{ .State }}\r\n",
|
||||
"username": "👤 Nombre de usuario: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Motivo: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Hora: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Entrada: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Puerto: {{ .Port }}\r\n",
|
||||
"expire": "📅 Fecha de Vencimiento: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Vence en: {{ .Time }}\r\n",
|
||||
"active": "💡 Activo: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Habilitado: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Estado de conexión: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Última conexión: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Subida: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Descarga: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Usuario de Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Cantidad de Agotados {{ .Type }}:\r\n",
|
||||
"onlinesCount": "🌐 Clientes en línea: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Desactivado: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Sí",
|
||||
"no": "❌ No",
|
||||
"received_email": "📧📥 Correo electrónico actualizado.",
|
||||
"received_comment": "💬📥 Comentario actualizado.",
|
||||
"email_prompt": "📧 Correo electrónico predeterminado: {{ .ClientEmail }}\n\nIntroduce tu correo electrónico.",
|
||||
"comment_prompt": "💬 Comentario predeterminado: {{ .ClientComment }}\n\nIntroduce tu comentario.",
|
||||
"cancel": "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄",
|
||||
"error_add_client": "⚠️ Error:\n\n {{ .error }}",
|
||||
"using_default_value": "Está bien, me quedaré con el valor predeterminado. 😊",
|
||||
"incorrect_input": "Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫",
|
||||
"AreYouSure": "¿Estás seguro? 🤔",
|
||||
"SuccessResetTraffic": "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito",
|
||||
"FailedResetTraffic": "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠️ Error: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes.",
|
||||
"eventOutboundDown": "El saliente {{ .Tag }} está CAÍDO",
|
||||
"eventOutboundUp": "El saliente {{ .Tag }} está ACTIVO",
|
||||
"eventErrorDetail": "Error: {{ .Error }}",
|
||||
"eventDelayDetail": "Retardo: {{ .Delay }} ms",
|
||||
"eventXrayCrash": "Xray se ha BLOQUEADO",
|
||||
"eventXrayCrashError": "Error: {{ .Error }}",
|
||||
"eventNodeDown": "El nodo {{ .Name }} está CAÍDO",
|
||||
"eventNodeUp": "El nodo {{ .Name }} está ACTIVO",
|
||||
"eventLoginFallback": "Inicio de sesión fallido desde {{ .Source }}",
|
||||
"memoryThreshold": "Uso de memoria {{ .Percent }}% supera el umbral de {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Cerrar Teclado",
|
||||
"cancel": "❌ Cancelar",
|
||||
"cancelReset": "❌ Cancelar Reinicio",
|
||||
"cancelIpLimit": "❌ Cancelar Límite de IP",
|
||||
"confirmResetTraffic": "✅ ¿Confirmar Reinicio de Tráfico?",
|
||||
"confirmClearIps": "✅ ¿Confirmar Limpiar IPs?",
|
||||
"confirmRemoveTGUser": "✅ ¿Confirmar Eliminar Usuario de Telegram?",
|
||||
"confirmToggle": "✅ ¿Confirmar habilitar/deshabilitar usuario?",
|
||||
"dbBackup": "Obtener Copia de Seguridad de BD",
|
||||
"serverUsage": "Uso del Servidor",
|
||||
"getInbounds": "Obtener Entradas",
|
||||
"depleteSoon": "Pronto se Agotará",
|
||||
"clientUsage": "Obtener Uso",
|
||||
"onlines": "Clientes en línea",
|
||||
"commands": "Comandos",
|
||||
"refresh": "🔄 Actualizar",
|
||||
"clearIPs": "❌ Limpiar IPs",
|
||||
"removeTGUser": "❌ Eliminar Usuario de Telegram",
|
||||
"selectTGUser": "👤 Seleccionar Usuario de Telegram",
|
||||
"selectOneTGUser": "👤 Selecciona un usuario de telegram:",
|
||||
"resetTraffic": "📈 Reiniciar Tráfico",
|
||||
"resetExpire": "📅 Cambiar fecha de Vencimiento",
|
||||
"ipLog": "🔢 Registro de IP",
|
||||
"ipLimit": "🔢 Límite de IP",
|
||||
"setTGUser": "👤 Establecer Usuario de Telegram",
|
||||
"toggle": "🔘 Habilitar / Deshabilitar",
|
||||
"custom": "🔢 Personalizado",
|
||||
"confirmNumber": "✅ Confirmar: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Confirmar agregando: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Límite de tráfico",
|
||||
"getBanLogs": "Registros de prohibición",
|
||||
"allClients": "Todos los Clientes",
|
||||
"addClient": "Añadir cliente",
|
||||
"submitDisable": "Enviar como deshabilitado ☑️",
|
||||
"submitEnable": "Enviar como habilitado ✅",
|
||||
"use_default": "🏷️ Usar por defecto",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Comentario",
|
||||
"ResetAllTraffics": "Reiniciar todo el tráfico",
|
||||
"SortedTrafficUsageReport": "Informe de uso de tráfico ordenado"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ ¡Exitosa!",
|
||||
"errorOperation": "❗ Error en la Operación.",
|
||||
"getInboundsFailed": "❌ Error al obtener las entradas",
|
||||
"getClientsFailed": "❌ No se pudo obtener los clientes.",
|
||||
"canceled": "❌ {{ .Email }} : Operación cancelada.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }} : Cliente actualizado exitosamente.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }} : IPs actualizadas exitosamente.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }} : Tráfico reiniciado exitosamente.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }} : Límite de Tráfico guardado exitosamente.",
|
||||
"expireResetSuccess": "✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente.",
|
||||
"resetIpSuccess": "✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente.",
|
||||
"clearIpSuccess": "✅ {{ .Email }} : IPs limpiadas exitosamente.",
|
||||
"getIpLog": "✅ {{ .Email }} : Obtener Registro de IP.",
|
||||
"getUserInfo": "✅ {{ .Email }} : Obtener Información de Usuario de Telegram.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente.",
|
||||
"enableSuccess": "✅ {{ .Email }} : Habilitado exitosamente.",
|
||||
"disableSuccess": "✅ {{ .Email }} : Deshabilitado exitosamente.",
|
||||
"askToAddUserId": "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ChatID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ChatID de usuario: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Elige un Cliente para Inbound {{ .Inbound }}",
|
||||
"chooseInbound": "Elige un Inbound"
|
||||
}
|
||||
"noIpRecord": "❗ ¡No hay registro de IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Estado",
|
||||
"labelOutbound": "Saliente",
|
||||
"labelNode": "Nodo",
|
||||
"labelError": "Error",
|
||||
"labelDelay": "Retardo",
|
||||
"labelUsername": "Usuario",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Motivo",
|
||||
"labelSource": "Origen",
|
||||
"statusCrashed": "BLOQUEADO",
|
||||
"statusHigh": "ALTA",
|
||||
"statusSuccess": "CORRECTO",
|
||||
"statusFailed": "FALLIDO",
|
||||
"statusDown": "CAÍDO",
|
||||
"statusUp": "ACTIVO"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "موفق",
|
||||
"lastOnline": "آخرین فعالیت",
|
||||
"lastSubFetch": "آخرین دریافت اشتراک",
|
||||
"getVersion": "دریافت نسخه",
|
||||
"install": "نصب",
|
||||
"clients": "کاربران",
|
||||
"usage": "استفاده",
|
||||
"twoFactorCode": "کد",
|
||||
"remained": "باقیمانده",
|
||||
"security": "امنیت",
|
||||
"emptyDnsDesc": "هیچ سرور DNS اضافه نشده است.",
|
||||
"emptyFakeDnsDesc": "هیچ سرور Fake DNS اضافه نشده است.",
|
||||
"emptyBalancersDesc": "هیچ بالانسر اضافه نشده است.",
|
||||
"somethingWentWrong": "مشکلی پیش آمد",
|
||||
"subscription": {
|
||||
"title": "اطلاعات سابسکریپشن",
|
||||
@@ -87,7 +83,6 @@
|
||||
"uploaded": "آپلود",
|
||||
"expiry": "تاریخ پایان",
|
||||
"totalQuota": "حجم کلی",
|
||||
"individualLinks": "لینکهای تکی",
|
||||
"active": "فعال",
|
||||
"inactive": "غیرفعال",
|
||||
"unlimited": "نامحدود",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "توقف",
|
||||
"restartXray": "راهاندازی مجدد",
|
||||
"xraySwitch": "نسخه",
|
||||
"xrayUpdates": "بهروزرسانیهای Xray",
|
||||
"xraySwitchClickDesk": "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمیتر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد",
|
||||
"updatePanel": "بهروزرسانی پنل",
|
||||
"panelUpdateDesc": "این عملیات 3X-UI را به آخرین نسخه بهروزرسانی میکند و سرویس پنل را مجدداً راهاندازی میکند.",
|
||||
"currentPanelVersion": "نسخه فعلی پنل",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "دانلود",
|
||||
"sent": "ارسال شده",
|
||||
"received": "دریافت شده",
|
||||
"xraySwitchVersionDialog": "آیا واقعاً میخواهید نسخه Xray را تغییر دهید؟",
|
||||
"xraySwitchVersionDialogDesc": "این کار نسخه Xray را به #version# تغییر میدهد.",
|
||||
"xraySwitchVersionPopover": "Xray با موفقیت بهروز شد",
|
||||
"panelUpdateDialog": "آیا مطمئن هستید که میخواهید پنل را بهروزرسانی کنید؟",
|
||||
"panelUpdateDialogDesc": "این 3X-UI را به نسخه #version# بهروزرسانی کرده و سرویس پنل را مجدداً راهاندازی میکند.",
|
||||
"panelUpdateStartedPopover": "بهروزرسانی پنل آغاز شد",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "بهروزرسانی با موفقیت به پایان نرسید. گزارشهای سرور را بررسی کنید یا دستور «x-ui update» را از خط فرمان اجرا کنید.",
|
||||
"panelUpdateUnknownTitle": "تأیید نشد که بهروزرسانی به پایان رسیده باشد",
|
||||
"panelUpdateUnknownDesc": "پنل بهموقع نتیجهای گزارش نکرد. صفحه را بارگذاری مجدد کنید تا نسخه فعلی را بررسی کنید یا گزارشهای سرور را بررسی کنید.",
|
||||
"geofileUpdateDialog": "آیا واقعاً میخواهید فایل جغرافیایی را بهروز کنید؟",
|
||||
"geofileUpdateDialogDesc": "این عمل فایل #filename# را بهروز میکند.",
|
||||
"geofilesUpdateDialogDesc": "با این کار همه فایلها بهروزرسانی میشوند.",
|
||||
"geofilesUpdateAll": "همه را بهروزرسانی کنید",
|
||||
"geofileUpdatePopover": "فایل جغرافیایی با موفقیت بهروز شد",
|
||||
"geodataTitle": "بهروزرسانی خودکار Geodata",
|
||||
"geodataHint": "Xray این فایلها را طبق زمانبندی دانلود کرده و بدون ریاستارت بارگذاری مجدد میکند. آدرسها باید HTTPS باشند. هر فایل باید از قبل در پوشه bin موجود باشد تا Xray بتواند آن را بهروزرسانی کند.",
|
||||
"geodataCron": "زمانبندی (cron)",
|
||||
"geodataOutbound": "دانلود از طریق خروجی (اختیاری)",
|
||||
"geodataFile": "نام فایل",
|
||||
"geodataAddFile": "افزودن فایل",
|
||||
"geodataSaveRestart": "ذخیره و ریاستارت Xray",
|
||||
"geodataConfirmTitle": "تنظیمات geodata ذخیره شود؟",
|
||||
"geodataConfirmContent": "قالب پیکربندی Xray بهروزرسانی و Xray ریاستارت میشود.",
|
||||
"geodataInvalidUrl": "هر فایل به یک آدرس HTTPS نیاز دارد.",
|
||||
"geodataInvalidFile": "نام فایل باید ساده باشد، مانند geosite_custom.dat (بدون مسیر).",
|
||||
"geodataInvalidCron": "Cron باید ۵ بخش داشته باشد، مثل 0 4 * * *",
|
||||
"geodataEmpty": "فایلی پیکربندی نشده است. در قوانین مسیریابی فایلها به صورت ext:geosite_custom.dat:category استفاده میشوند.",
|
||||
"dontRefresh": "در حال نصب، لطفا صفحه را رفرش نکنید",
|
||||
"logs": "لاگها",
|
||||
"accessLogs": "لاگهای دسترسی",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "مسیر پایه پیشفرض «/» شناختهشده است — آن را به یک مسیر تصادفی تغییر دهید.",
|
||||
"warnDefaultSubPath": "مسیر ساب پیشفرض «/sub/» شناختهشده است — تغییرش دهید.",
|
||||
"warnDefaultJsonPath": "مسیر JSON ساب پیشفرض «/json/» شناختهشده است — تغییرش دهید.",
|
||||
"TGBotSettings": "ربات تلگرام",
|
||||
"panelListeningIP": "آدرس آیپی",
|
||||
"panelListeningIPDesc": "آدرس آیپی برای وب پنل. برای گوشدادن بهتمام آیپیها خالیبگذارید",
|
||||
"panelListeningDomain": "نام دامنه",
|
||||
@@ -1111,43 +1081,12 @@
|
||||
"currentPassword": "رمزعبور فعلی",
|
||||
"newUsername": "نامکاربری جدید",
|
||||
"newPassword": "رمزعبور جدید",
|
||||
"telegramBotEnable": "فعالسازی ربات تلگرام",
|
||||
"telegramBotEnableDesc": "ربات تلگرام را فعال میکند",
|
||||
"telegramToken": "توکن تلگرام",
|
||||
"telegramTokenDesc": "دریافت کنید {'@'}botfather توکن را میتوانید از",
|
||||
"telegramProxy": "پراکسی SOCKS",
|
||||
"telegramProxyDesc": "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی",
|
||||
"telegramAPIServer": "سرور API تلگرام",
|
||||
"telegramAPIServerDesc": "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید",
|
||||
"telegramChatId": "آیدی چت مدیر",
|
||||
"telegramChatIdDesc": "دریافت کنید ('/id'یا (دستور ({'@'}userinfobot) آیدی(های) چت تلگرام مدیر، از",
|
||||
"telegramNotifyTime": "زمان نوتیفیکیشن",
|
||||
"telegramNotifyTimeDesc": "هر چند وقت یکبار ربات تلگرام گزارش دورهای بفرستد. یک بازهٔ آماده انتخاب کنید یا گزینهٔ سفارشی را بزنید تا عبارت crontab وارد کنید.",
|
||||
"notifyTime": {
|
||||
"every": "@every — تکرار در یک بازه",
|
||||
"hourly": "@hourly — هر ساعت",
|
||||
"daily": "@daily — هر روز ساعت ۰۰:۰۰",
|
||||
"weekly": "@weekly — هر هفته",
|
||||
"monthly": "@monthly — هر ماه",
|
||||
"custom": "سفارشی (crontab)",
|
||||
"seconds": "ثانیه",
|
||||
"minutes": "دقیقه",
|
||||
"hours": "ساعت",
|
||||
"interval": "بازه زمانی",
|
||||
"unit": "واحد"
|
||||
},
|
||||
"tgNotifyBackup": "پشتیبانگیری از دیتابیس",
|
||||
"tgNotifyBackupDesc": "فایل پشتیباندیتابیس را بههمراه گزارش ارسال میکند",
|
||||
"tgNotifyLogin": "اعلان ورود",
|
||||
"tgNotifyLoginDesc": "نامکاربری، آدرس آیپی، و زمان ورود، فردی که سعی میکند وارد پنل شود را نمایش میدهد",
|
||||
"sessionMaxAge": "بیشینه زمان جلسه وب",
|
||||
"sessionMaxAgeDesc": "(بیشینه زمانی که میتوانید لاگین بمانید. (واحد: دقیقه",
|
||||
"expireTimeDiff": "آستانه زمان باقی مانده",
|
||||
"expireTimeDiffDesc": "(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز",
|
||||
"trafficDiff": "آستانه ترافیک باقی مانده",
|
||||
"trafficDiffDesc": "(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت",
|
||||
"tgNotifyCpu": "آستانه هشدار بار پردازنده",
|
||||
"tgNotifyCpuDesc": "(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال میشود. (واحد: درصد",
|
||||
"timeZone": "منطقه زمانی",
|
||||
"timeZoneDesc": "وظایف برنامه ریزی شده بر اساس این منطقهزمانی اجرا میشود",
|
||||
"subSettings": "سابسکریپشن",
|
||||
@@ -1212,10 +1151,6 @@
|
||||
"subEncryptDesc": "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه",
|
||||
"subURI": "پروکسی معکوس URI مسیر",
|
||||
"subURIDesc": "آدرس پایه کامل (scheme://domain[:port]/path/) برای لینک سابسکریپشن و کد QR، به جای نام دامنه/پورت استفاده میشود. هر وقت سابسکریپشن از پشت یک پراکسی معکوس یا روی دامنه/پورت متفاوتی نسبت به موارد بالا در دسترس است، این را پر کنید.",
|
||||
"externalTrafficInformEnable": "اطلاع رسانی خارجی مصرف ترافیک",
|
||||
"externalTrafficInformEnableDesc": "به API خارجی در هر بهروزرسانی ترافیک اطلاع بده.",
|
||||
"externalTrafficInformURI": "لینک اطلاع رسانی خارجی مصرف ترافیک",
|
||||
"externalTrafficInformURIDesc": "ترافیک های مصرفی به این لینک هم ارسال می شود",
|
||||
"restartXrayOnClientDisable": "ریاستارت Xray بعد از غیرفعالسازی خودکار",
|
||||
"restartXrayOnClientDisableDesc": "وقتی کاربر بهصورت خودکار بهدلیل اتمام زمان یا ترافیک غیرفعال میشود، Xray ریاستارت شود.",
|
||||
"fragment": "فرگمنت",
|
||||
@@ -1225,38 +1160,6 @@
|
||||
"noisesSett": "تنظیمات Noises",
|
||||
"trustedProxyCidrs": "CIDRهای پراکسی مورد اعتماد",
|
||||
"trustedProxyCidrsDesc": "IPها/CIDRها (با کاما) که مجازند هدرهای host، proto و client IP فوروارد را تنظیم کنند.",
|
||||
"ldap": {
|
||||
"enable": "فعالسازی همگامسازی LDAP",
|
||||
"host": "میزبان LDAP",
|
||||
"port": "پورت LDAP",
|
||||
"useTls": "استفاده از TLS (LDAPS)",
|
||||
"skipTlsVerify": "رد کردن تأیید گواهی TLS",
|
||||
"skipTlsVerifyDesc": "ناامن — اعتبارسنجی گواهی سرور را غیرفعال میکند. فقط برای CAهای داخلی/غیرمعتبر استفاده کنید.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "تنظیمشده؛ برای حفظ رمز فعلی خالی بگذارید.",
|
||||
"passwordUnconfigured": "تنظیم نشده.",
|
||||
"passwordPlaceholder": "تنظیمشده – برای جایگزینی مقدار جدید وارد کنید",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "فیلتر کاربر",
|
||||
"userAttr": "صفت کاربر (username/email)",
|
||||
"vlessField": "صفت پرچم VLESS",
|
||||
"flagField": "صفت پرچم عمومی (اختیاری)",
|
||||
"flagFieldDesc": "اگر تعیین شود، پرچم VLESS را override میکند — مثل shadowInactive.",
|
||||
"truthyValues": "مقادیر صحیح",
|
||||
"truthyValuesDesc": "با کاما جدا شده؛ پیشفرض: true,1,yes,on",
|
||||
"invertFlag": "وارونگی پرچم",
|
||||
"invertFlagDesc": "وقتی صفت به معنی «غیرفعال» است فعال کنید (مثل shadowInactive).",
|
||||
"syncSchedule": "زمانبندی همگامسازی",
|
||||
"syncScheduleDesc": "رشته شبیه cron، مثل @every 1m",
|
||||
"inboundTags": "تگهای ورودی",
|
||||
"inboundTagsDesc": "ورودیهایی که همگامسازی LDAP اجازه دارد روی آنها کاربر بسازد یا حذف کند.",
|
||||
"noInbounds": "هیچ ورودی یافت نشد. ابتدا از بخش ورودیها یکی بسازید.",
|
||||
"autoCreate": "ساخت خودکار کاربران",
|
||||
"autoDelete": "حذف خودکار کاربران",
|
||||
"defaultTotalGb": "حجم پیشفرض (GB)",
|
||||
"defaultExpiryDays": "انقضای پیشفرض (روز)",
|
||||
"defaultIpLimit": "محدودیت IP پیشفرض"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "ماسکهای TCP/UDP مربوط به Xray finalmask و پارامترهای QUIC را به هر پروفایل Xray JSON تولیدشده اضافه میکند. به برنامهای با پشتیبانی از اشتراک Xray JSON و هسته جدید Xray نیاز دارد.",
|
||||
@@ -1280,31 +1183,15 @@
|
||||
"muxSett": "تنظیمات ماکس",
|
||||
"direct": "اتصال مستقیم",
|
||||
"directDesc": "به طور مستقیم با دامنه ها یا محدوده آیپی یک کشور خاص ارتباط برقرار می کند",
|
||||
"notifications": "اعلانها",
|
||||
"certs": "گواهیها",
|
||||
"externalTraffic": "ترافیک خارجی",
|
||||
"dateAndTime": "تاریخ و زمان",
|
||||
"proxyAndServer": "پراکسی و سرور",
|
||||
"intervals": "فواصل",
|
||||
"information": "اطلاعات",
|
||||
"profile": "پروفایل",
|
||||
"language": "زبان",
|
||||
"telegramBotLanguage": "زبان ربات تلگرام",
|
||||
"security": {
|
||||
"admin": "اعتبارنامههای ادمین",
|
||||
"twoFactor": "احراز هویت دو مرحلهای",
|
||||
"twoFactorEnable": "فعالسازی 2FA",
|
||||
"twoFactorEnableDesc": "یک لایه اضافی امنیتی برای احراز هویت فراهم میکند.",
|
||||
"twoFactorModalSetTitle": "فعالسازی احراز هویت دو مرحلهای",
|
||||
"twoFactorModalDeleteTitle": "غیرفعالسازی احراز هویت دو مرحلهای",
|
||||
"twoFactorModalSteps": "برای راهاندازی احراز هویت دو مرحلهای، مراحل زیر را انجام دهید:",
|
||||
"twoFactorModalFirstStep": "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید",
|
||||
"twoFactorModalSecondStep": "2. کد را از برنامه وارد کنید",
|
||||
"twoFactorModalRemoveStep": "برای حذف احراز هویت دو مرحلهای، کد را از برنامه وارد کنید.",
|
||||
"twoFactorModalChangeCredentialsTitle": "تغییر اعتبارنامهها",
|
||||
"twoFactorModalChangeCredentialsStep": "برای تغییر اعتبارنامههای مدیر، کد را از برنامه وارد کنید.",
|
||||
"twoFactorModalSetSuccess": "احراز هویت دو مرحلهای با موفقیت برقرار شد",
|
||||
"twoFactorModalDeleteSuccess": "احراز هویت دو مرحلهای با موفقیت حذف شد",
|
||||
"twoFactorModalError": "کد نادرست",
|
||||
"show": "نمایش",
|
||||
"hide": "پنهان",
|
||||
@@ -1327,36 +1214,6 @@
|
||||
"getOutboundTrafficError": "خطا در دریافت ترافیک خروجی",
|
||||
"resetOutboundTrafficError": "خطا در بازنشانی ترافیک خروجی"
|
||||
},
|
||||
"smtpSettings": "تنظیمات SMTP",
|
||||
"smtpEnable": "فعالسازی اعلانهای ایمیلی",
|
||||
"smtpEnableDesc": "فعالسازی اعلانهای ایمیلی از طریق SMTP",
|
||||
"smtpHost": "میزبان SMTP",
|
||||
"smtpHostDesc": "نام میزبان سرور SMTP (مثلاً smtp.gmail.com)",
|
||||
"smtpPort": "پورت SMTP",
|
||||
"smtpPortDesc": "پورت سرور SMTP (پیشفرض: ۵۸۷)",
|
||||
"smtpUsername": "نامکاربری SMTP",
|
||||
"smtpUsernameDesc": "نامکاربری احراز هویت SMTP",
|
||||
"smtpFrom": "آدرس فرستنده (From)",
|
||||
"smtpFromDesc": "آدرس استفادهشده در سرآیند From ایمیل. برای استفاده از نام کاربری، خالی بگذارید.",
|
||||
"smtpFromName": "نام فرستنده (From)",
|
||||
"smtpFromNameDesc": "نام نمایشی اختیاری پیش از آدرس در سرآیند From.",
|
||||
"smtpPassword": "رمز عبور SMTP",
|
||||
"smtpPasswordDesc": "رمز عبور احراز هویت SMTP",
|
||||
"smtpTo": "گیرندگان",
|
||||
"smtpToDesc": "آدرسهای ایمیل گیرندگان، جداشده با کاما",
|
||||
"emailSettings": "ایمیل",
|
||||
"emailNotifications": "اعلانها",
|
||||
"smtpEventBusNotify": "اعلانهای رویداد ایمیلی",
|
||||
"smtpEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان ایمیلی را فعال میکنند",
|
||||
"tgEventBusNotify": "اعلانهای رویداد تلگرام",
|
||||
"tgEventBusNotifyDesc": "انتخاب کنید کدام رویدادها اعلان تلگرام را فعال میکنند",
|
||||
"testSmtp": "ارسال ایمیل آزمایشی",
|
||||
"testTgBot": "ارسال پیام آزمایشی",
|
||||
"eventGroupOutbound": "خروجی",
|
||||
"eventGroupXray": "هسته Xray",
|
||||
"eventGroupSystem": "سیستم",
|
||||
"eventGroupSecurity": "امنیت",
|
||||
"eventGroupNode": "نودها",
|
||||
"eventOutboundDown": "قطع",
|
||||
"eventOutboundUp": "وصل",
|
||||
"eventXrayCrash": "کرش",
|
||||
@@ -1364,36 +1221,7 @@
|
||||
"eventNodeUp": "وصل",
|
||||
"eventCPUHigh": "بالا بودن CPU (٪)",
|
||||
"requestFailed": "درخواست ناموفق بود",
|
||||
"smtpEncryption": "رمزنگاری",
|
||||
"smtpEncryptionDesc": "روش رمزنگاری اتصال SMTP",
|
||||
"smtpEncryptionNone": "هیچکدام (متن ساده)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (ضمنی)",
|
||||
"smtpStageConnect": "اتصال",
|
||||
"smtpStageAuth": "احراز هویت",
|
||||
"smtpStageSend": "ارسال",
|
||||
"smtpTestSuccess": "ایمیل آزمایشی با موفقیت ارسال شد",
|
||||
"smtpHostNotConfigured": "میزبان SMTP پیکربندی نشده است",
|
||||
"smtpNoRecipients": "هیچ گیرندهای پیکربندی نشده است",
|
||||
"smtpFromNotConfigured": "آدرس فرستنده SMTP پیکربندی نشده است",
|
||||
"eventLoginAttempt": "تلاش برای ورود",
|
||||
"telegramTokenConfigured": "پیکربندی شده؛ برای حفظ توکن فعلی خالی بگذارید.",
|
||||
"telegramTokenPlaceholder": "پیکربندی شده - برای جایگزینی، توکن جدید وارد کنید",
|
||||
"smtpPasswordConfigured": "پیکربندی شده؛ برای حفظ رمز عبور فعلی خالی بگذارید.",
|
||||
"smtpPasswordPlaceholder": "پیکربندی شده - برای جایگزینی، رمز عبور جدید وارد کنید",
|
||||
"smtpNotInitialized": "SMTP مقداردهی اولیه نشده است",
|
||||
"tgBotNotEnabled": "ربات تلگرام فعال نیست",
|
||||
"tgTestFailed": "آزمایش تلگرام ناموفق بود",
|
||||
"tgTestSuccess": "پیام آزمایشی به تلگرام ارسال شد",
|
||||
"tgBotNotRunning": "ربات تلگرام در حال اجرا نیست",
|
||||
"smtpErrorAuth": "احراز هویت ناموفق بود — نامکاربری و رمز عبور را بررسی کنید",
|
||||
"smtpErrorStarttls": "سرور به STARTTLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
|
||||
"smtpErrorTls": "سرور به TLS نیاز دارد — نوع رمزنگاری را تغییر دهید",
|
||||
"smtpErrorRefused": "اتصال رد شد — میزبان و پورت را بررسی کنید",
|
||||
"smtpErrorTimeout": "مهلت اتصال به پایان رسید — میزبان در دسترس نیست",
|
||||
"smtpErrorRelay": "سرور ارسال از این آدرس را رد میکند",
|
||||
"smtpErrorEof": "اتصال توسط سرور بسته شد",
|
||||
"smtpErrorUnknown": "خطای SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "مصرف حافظه بالا (%)",
|
||||
"validation": {
|
||||
"pathLeadingSlash": "مسیر باید با / شروع شود"
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "دامنههای مستقیم",
|
||||
"ipv4Routing": "IPv4 مسیریابی",
|
||||
"ipv4RoutingDesc": "این گزینهها ترافیک را از طریق آیپی نسخه4 سرور، به مقصد هدایت میکند",
|
||||
"Template": "پیکربندی پیشرفته الگو ایکسری",
|
||||
"TemplateDesc": "فایل پیکربندی نهایی ایکسری بر اساس این الگو ایجاد میشود",
|
||||
"FreedomStrategy": "Freedom استراتژی پروتکل",
|
||||
"FreedomStrategyDesc": "تعیین میکند Freedom استراتژی خروجی شبکه را برای پروتکل",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1492,11 +1318,6 @@
|
||||
"Balancers": "بالانسرها",
|
||||
"balancerTagRequired": "تگ الزامی است",
|
||||
"balancerSelectorRequired": "حداقل یک خروجی انتخاب کنید",
|
||||
"balancerLive": "هدف زنده",
|
||||
"balancerOverride": "اجبار مسیر",
|
||||
"balancerOverridePh": "خودکار (استراتژی)",
|
||||
"balancerLiveRefresh": "بهروزرسانی وضعیت زنده بالانسر",
|
||||
"balancerNotRunning": "این بالانسر در Xray در حال اجرا فعال نیست — ابتدا تغییرات را ذخیره کنید یا Xray را روشن کنید",
|
||||
"routeTester": "آزمایش مسیر",
|
||||
"routeTesterDesc": "از Xray در حال اجرا بپرسید یک اتصال از کدام خروجی عبور میکند. هیچ ترافیکی ارسال نمیشود — پاسخ مستقیماً از موتور مسیریابی زنده میآید.",
|
||||
"routeTesterDest": "دامنه یا IP",
|
||||
@@ -1513,7 +1334,6 @@
|
||||
"importOutbounds": "ورود خروجیها",
|
||||
"exportOutbounds": "خروج خروجیها",
|
||||
"importInvalidJson": "JSON نامعتبر — یک آرایه یا یک شیء با کلید متناظر انتظار میرود.",
|
||||
"completeTemplate": "همه",
|
||||
"logLevel": "سطح گزارش",
|
||||
"logLevelDesc": "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند.",
|
||||
"accessLog": "مسیر گزارش",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "مبدا",
|
||||
"dest": "مقصد",
|
||||
"inbound": "ورودی",
|
||||
"balancer": "بالانسر",
|
||||
"useComma": "موارد جدا شده با کاما"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "این سرور از قبل در فهرست خروجی است. برای تمدید کلید {reset} را بزنید.",
|
||||
"provisionFailed": "ساخت خروجی PIA ممکن نشد. دوباره تلاش کنید."
|
||||
},
|
||||
"tabBalancerSettings": "تنظیمات بالانسر",
|
||||
"tabObservatory": "رصدخانه",
|
||||
"observatory": {
|
||||
"autoManaged": "رصدگرها بهصورت خودکار از روی بالانسرهای شما مدیریت میشوند. در ادامه میتوانید نحوهٔ پروبزدن را تنظیم کنید؛ خروجیهای تحت نظر از سلکتورهای بالانسر پیروی میکنند.",
|
||||
"emptyHint": "هیچ رصدگر اتصالی فعال نیست. وقتی یک بالانسر Least Ping یا Least Load بسازید — یا یک بالانسر Random / Round-robin همراه با fallback — بهصورت خودکار یکی اضافه میشود تا بالانسرهای متکی به رصدگر بتوانند پیش از انتخاب مقصد، سلامت خروجیها را بررسی کنند.",
|
||||
"mixedLegacy": "این پیکربندی هم Observatory و هم Burst Observatory دارد. Xray فقط از یک رصدگر سراسری استفاده میکند، بنابراین این حالت قدیمیِ ترکیبی پشتیبانی نمیشود؛ ذخیرهٔ بالانسرها آن را به یک رصدگر عادیسازی میکند.",
|
||||
"subjectSelector": "خروجیهای تحت نظر",
|
||||
"subjectSelectorDesc": "تگ خروجیهایی که این رصدگر پروب میکند. بهصورت خودکار از روی بالانسرهای شما مدیریت میشود.",
|
||||
"probeURL": "آدرس پروب (URL)",
|
||||
"probeURLDesc": "آدرسی که برای سنجش هر خروجی فراخوانی میشود. باید HTTP 204 برگرداند.",
|
||||
"probeInterval": "بازهٔ پروب",
|
||||
"probeIntervalDesc": "هر چند وقت یکبار هر خروجی پروب شود، مثلاً 30s یا 1m یا 2h45m.",
|
||||
"enableConcurrency": "پروب همزمان",
|
||||
"enableConcurrencyDesc": "همهٔ خروجیهای تحت نظر را بهجای یکییکی، همزمان پروب کن. سریعتر است اما در شبکه نمایانتر.",
|
||||
"destination": "مقصد پروب",
|
||||
"destinationDesc": "آدرسی که برای سنجش هر خروجی فراخوانی میشود. باید HTTP 204 برگرداند.",
|
||||
"connectivity": "بررسی اتصال",
|
||||
"connectivityDesc": "آدرس اختیاری برای بررسی شبکهٔ محلی که فقط پس از شکست مقصد امتحان میشود. برای نادیدهگرفتن خالی بگذارید.",
|
||||
"interval": "بازهٔ پروب",
|
||||
"intervalDesc": "میانگین فاصلهٔ زمانی بین پروبها برای هر خروجی، مثلاً 1m. کمینه 10s.",
|
||||
"timeout": "مهلت پروب",
|
||||
"timeoutDesc": "چه مدت برای پاسخ یک پروب صبر شود تا ناموفق بهحساب بیاید، مثلاً 5s.",
|
||||
"sampling": "تعداد نمونهگیری",
|
||||
"samplingDesc": "تعداد نتایج اخیر پروب که برای امتیازدهی به هر خروجی نگه داشته میشود.",
|
||||
"httpMethod": "متد HTTP",
|
||||
"httpMethodDesc": "متد HTTP که برای پروبها استفاده میشود.",
|
||||
"deleteAlsoObservatory": "این آخرین بالانسری است که از Observatory استفاده میکند، بنابراین آن هم حذف خواهد شد.",
|
||||
"deleteAlsoBurst": "این آخرین بالانسری است که از Burst Observatory استفاده میکند، بنابراین آن هم حذف خواهد شد."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "استراتژی",
|
||||
"tag": "تگ",
|
||||
"tagDuplicate": "این تگ توسط بالانسر دیگری استفاده شده است",
|
||||
"tagPlaceholder": "تگ منحصربهفرد بالانسر",
|
||||
"selector": "انتخابگر",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "حلقه: {path} → (بازگشت به {start})",
|
||||
"expected": "مورد انتظار",
|
||||
"expectedPlaceholder": "تعداد نود بهینه",
|
||||
"maxRtt": "حداکثر RTT",
|
||||
"tolerance": "تحمل",
|
||||
"baselines": "خطوط پایه",
|
||||
"costs": "هزینهها",
|
||||
"costMatch": "الگوی برچسب",
|
||||
"costValue": "وزن",
|
||||
"costRegexp": "تطبیق با عبارت باقاعده",
|
||||
"balancerDeleteInUse": "امکان حذف این بالانسر وجود ندارد — به عنوان پشتیبان برای موارد زیر استفاده میشود: {names}",
|
||||
"balancerFallbackCycle": "امکان تنظیم این بالانسر به عنوان پشتیبان وجود ندارد — وابستگی دایرهای ایجاد میکند.",
|
||||
"balancerFallbackInfo": "ترافیک از مسیر زیر مسیریابی میشود: بالانسر → Loopback → سرور → بالانسر مقصد → اتصال خروجی. این یک پرش اضافی از طریق سرور اضافه میکند که ممکن است تأخیرهای جزئی ایجاد کند.",
|
||||
"fallbackBalancerHint": "یک بالانسر دیگر به عنوان پشتیبان انتخاب کنید",
|
||||
"reservedPrefix": "پیشوند _bl_ برای اشیاء loopback داخلی بالانسر رزرو شده است"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "مصرف",
|
||||
"addOutbound": "افزودن خروجی"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "فعال کردن حل دامنه",
|
||||
"enableDesc": "سرور حل دامنه داخلی را فعال کنید",
|
||||
"tag": "برچسب",
|
||||
"tagDesc": "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود",
|
||||
"clientIp": "آیپی کلاینت",
|
||||
"clientIpDesc": "برای اطلاعرسانی به سرور درباره مکان IP مشخصشده در طول درخواستهای DNS استفاده میشود",
|
||||
"disableCache": "غیرفعالسازی کش",
|
||||
"disableCacheDesc": "کش DNS را غیرفعال میکند",
|
||||
"disableFallback": "غیرفعالسازی Fallback",
|
||||
"disableFallbackDesc": "درخواستهای DNS Fallback را غیرفعال میکند",
|
||||
"disableFallbackIfMatch": "غیرفعالسازی Fallback در صورت تطابق",
|
||||
"disableFallbackIfMatchDesc": "درخواستهای DNS Fallback را زمانی که لیست دامنههای مطابقتیافته سرور DNS فعال است، غیرفعال میکند",
|
||||
"enableParallelQuery": "فعالسازی پرسوجوی موازی",
|
||||
"enableParallelQueryDesc": "فعالسازی پرسوجوهای DNS موازی به چندین سرور برای وضوح سریعتر",
|
||||
"strategy": "استراتژی پرسوجو",
|
||||
"strategyDesc": "استراتژی کلی برای حل نام دامنه",
|
||||
"add": "افزودن سرور",
|
||||
"edit": "ویرایش سرور",
|
||||
"domains": "دامنهها",
|
||||
"expectIPs": "آیپیهای مورد انتظار",
|
||||
"unexpectIPs": "آیپیهای غیرمنتظره",
|
||||
"useSystemHosts": "استفاده از Hosts سیستم",
|
||||
"useSystemHostsDesc": "استفاده از فایل hosts یک سیستم نصبشده",
|
||||
"serveStale": "ارائه نتایج منقضی",
|
||||
"serveStaleDesc": "بازگرداندن نتایج منقضی کش هنگام بروزرسانی در پسزمینه",
|
||||
"serveExpiredTTL": "TTL نتایج منقضی",
|
||||
"serveExpiredTTLDesc": "مدت اعتبار نتایج منقضی به ثانیه؛ ۰ یعنی هرگز منقضی نمیشود",
|
||||
"timeoutMs": "زمان انتظار (میلیثانیه)",
|
||||
"skipFallback": "رد کردن Fallback",
|
||||
"finalQuery": "پرسوجوی نهایی",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "افزودن Host",
|
||||
"hostsEmpty": "هیچ Host تعریف نشده",
|
||||
"hostsDomain": "دامنه (مثلاً domain:example.com)",
|
||||
"hostsValues": "آیپی یا دامنه — تایپ کنید و Enter بزنید",
|
||||
"usePreset": "استفاده از پیشتنظیم",
|
||||
"dnsPresetTitle": "پیشتنظیمهای DNS",
|
||||
"dnsPresetFamily": "خانوادگی",
|
||||
"clearAll": "حذف همه",
|
||||
"clearAllTitle": "حذف همه سرورهای DNS؟",
|
||||
"clearAllConfirm": "این کار همه سرورهای DNS را از لیست حذف میکند و قابل بازگشت نیست.",
|
||||
"dnsLeakWarning": "DNS ممکن است از طریق localhost، UDP/TCP ساده، حالت محلی DoH/DoQ، درخواستهای fallback یا EDNS client IP نشت کند. برای حریم خصوصی از DoH مسیریابیشده، مقدارهای hosts ثابت و غیرفعالسازی fallback استفاده کنید."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "افزودن دیاناس جعلی",
|
||||
"ipPool": "زیرشبکه استخر آیپی",
|
||||
"poolSize": "اندازه استخر"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "خروجی پیشفرض",
|
||||
"defaultOutboundDesc": "ترافیکی که با هیچ قانون مسیریابی جور نشود از این خروجی استفاده میکند (اولین خروجی در فهرست)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ صفحه کلید بسته شد!",
|
||||
"noResult": "❗ نتیجه ای یافت نشد!",
|
||||
"noQuery": "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!",
|
||||
"wentWrong": "❌ مشکلی پیش آمد!",
|
||||
"noIpRecord": "❗ رکورد آی پی وجود ندارد!",
|
||||
"noInbounds": "❗ هیچ ورودی یافت نشد!",
|
||||
"unlimited": "♾ نامحدود(ریست)",
|
||||
"add": "افزودن",
|
||||
"month": "ماه",
|
||||
"months": "ماه",
|
||||
"days": "روز",
|
||||
"hours": "ساعت",
|
||||
"minutes": "دقیقه",
|
||||
"unknown": "نامشخص",
|
||||
"inbounds": "ورودیها",
|
||||
"clients": "کاربران",
|
||||
"offline": "🔴 آفلاین",
|
||||
"online": "🟢 آنلاین",
|
||||
"commands": {
|
||||
"unknown": "❗ دستور ناشناخته",
|
||||
"pleaseChoose": "👇 لطفاً انتخاب کنید:\r\n",
|
||||
"help": "🤖 به این ربات خوش آمدید! این ربات برای ارائه دادههای خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را میدهد.\r\n\r\n",
|
||||
"start": "👋 سلام <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n",
|
||||
"status": "✅ ربات در حالت عادی است!",
|
||||
"usage": "❗ لطفاً یک متن برای جستجو وارد کنید!",
|
||||
"getID": "🆔 شناسه شما: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "برای راهاندازی مجدد Xray Core:\r\n<code>/restart</code>\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودیها (با آمار مشتری):\r\n<code>/inbound [توضیحات]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "برای جستجوی آمار، از دستور زیر استفاده کنید:\r\n<code>/usage [ایمیل]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ عملیات با موفقیت انجام شد!",
|
||||
"restartFailed": "❗ خطا در عملیات.\r\n\r\n<code>خطا: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core در حال اجرا نیست.",
|
||||
"startDesc": "نمایش منوی اصلی",
|
||||
"helpDesc": "راهنمای ربات",
|
||||
"statusDesc": "بررسی وضعیت ربات",
|
||||
"idDesc": "نمایش شناسه تلگرام شما",
|
||||
"usageDesc": "مشاهده مصرف کاربر: /usage ایمیل",
|
||||
"inboundDesc": "جستجوی ورودیها: /inbound نام (مدیر)",
|
||||
"restartDesc": "راهاندازی مجدد هسته Xray (مدیر)",
|
||||
"clearallDesc": "صفر کردن ترافیک همه کاربران (مدیر)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "بار پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ خطا در انتخاب کاربر!",
|
||||
"userSaved": "✅ کاربر تلگرام ذخیره شد.",
|
||||
"loginSuccess": "✅ با موفقیت به پنل وارد شدید.\r\n",
|
||||
"loginFailed": "❗️ ورود به پنل ناموفقبود \r\n",
|
||||
"report": "🕰 گزارشاتزمانبندیشده: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ تاریخوزمان: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 میزبان: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 نسخهپنل: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 نسخههسته: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPها:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ مدتکارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ وضعیت: {{ .State }}\r\n",
|
||||
"username": "👤 نامکاربری: {{ .Username }}\r\n",
|
||||
"reason": "❗️ دلیل: {{ .Reason }}\r\n",
|
||||
"time": "⏰ زمان: {{ .Time }}\r\n",
|
||||
"inbound": "📍 ورودی: {{ .Remark }}\r\n",
|
||||
"port": "🔌 پورت: {{ .Port }}\r\n",
|
||||
"expire": "📅 تاریخانقضا: {{ .Time }}\r\n\r\n",
|
||||
"expireIn": "📅 باقی مانده تا انقضا: {{ .Time }}\r\n\r\n",
|
||||
"active": "💡 فعال: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 وضعیت: {{ .Enable }}\r\n",
|
||||
"online": "🌐 وضعیت اتصال: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 آخرین فعالیت: {{ .Time }}\r\n",
|
||||
"email": "📧 ایمیل: {{ .Email }}\r\n",
|
||||
"upload": "🔼 آپلود: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 دانلود: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 کل: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 کاربر تلگرام: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 تعداد {{ .Type }} بهاتمامرسیدهاست:\r\n",
|
||||
"onlinesCount": "🌐 کاربرانآنلاین: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 غیرفعال: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 بهزودیبهپایانخواهدرسید: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 زمانپشتیبانگیری: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 تازهسازی شده در: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ بله",
|
||||
"no": "❌ خیر",
|
||||
"received_email": "📧📥 ایمیل بهروزرسانی شد.",
|
||||
"received_comment": "💬📥 نظر بهروزرسانی شد.",
|
||||
"email_prompt": "📧 ایمیل پیشفرض: {{ .ClientEmail }}\n\nایمیل خود را وارد کنید.",
|
||||
"comment_prompt": "💬 نظر پیشفرض: {{ .ClientComment }}\n\nنظر خود را وارد کنید.",
|
||||
"cancel": "❌ فرآیند لغو شد! \n\nمیتوانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄",
|
||||
"error_add_client": "⚠️ خطا:\n\n {{ .error }}",
|
||||
"using_default_value": "باشه، از مقدار پیشفرض استفاده میکنم. 😊",
|
||||
"incorrect_input": "ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫",
|
||||
"AreYouSure": "مطمئنی؟ 🤔",
|
||||
"SuccessResetTraffic": "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیتآمیز",
|
||||
"FailedResetTraffic": "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠️ خطا: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید.",
|
||||
"eventOutboundDown": "خروجی {{ .Tag }} قطع است",
|
||||
"eventOutboundUp": "خروجی {{ .Tag }} وصل است",
|
||||
"eventErrorDetail": "خطا: {{ .Error }}",
|
||||
"eventDelayDetail": "تأخیر: {{ .Delay }} میلیثانیه",
|
||||
"eventXrayCrash": "Xray کرش کرد",
|
||||
"eventXrayCrashError": "خطا: {{ .Error }}",
|
||||
"eventNodeDown": "نود {{ .Name }} قطع است",
|
||||
"eventNodeUp": "نود {{ .Name }} وصل است",
|
||||
"eventLoginFallback": "ورود ناموفق از {{ .Source }}",
|
||||
"memoryThreshold": "مصرف حافظه {{ .Percent }}% از حد آستانه {{ .Threshold }}% فراتر رفته است"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ بستن کیبورد",
|
||||
"cancel": "❌ لغو",
|
||||
"cancelReset": "❌ لغو تنظیم مجدد",
|
||||
"cancelIpLimit": "❌ لغو محدودیت آیپی",
|
||||
"confirmResetTraffic": "✅ تأیید تنظیم مجدد ترافیک؟",
|
||||
"confirmClearIps": "✅ تأیید پاکسازی آدرسهای آیپی؟",
|
||||
"confirmRemoveTGUser": "✅ تأیید حذف کاربر تلگرام؟",
|
||||
"confirmToggle": "✅ تایید فعال/غیرفعال کردن کاربر؟",
|
||||
"dbBackup": "دریافت پشتیبان",
|
||||
"serverUsage": "استفاده از سیستم",
|
||||
"getInbounds": "دریافت ورودیها",
|
||||
"depleteSoon": "بهزودی به پایان خواهد رسید",
|
||||
"clientUsage": "دریافت آمار کاربر",
|
||||
"onlines": "کاربران آنلاین",
|
||||
"commands": "دستورات",
|
||||
"refresh": "🔄 تازهسازی",
|
||||
"clearIPs": "❌ پاکسازی آدرسها",
|
||||
"removeTGUser": "❌ حذف کاربر تلگرام",
|
||||
"selectTGUser": "👤 انتخاب کاربر تلگرام",
|
||||
"selectOneTGUser": "👤 یک کاربر تلگرام را انتخاب کنید:",
|
||||
"resetTraffic": "📈 تنظیم مجدد ترافیک",
|
||||
"resetExpire": "📅 تنظیم مجدد تاریخ انقضا",
|
||||
"ipLog": "🔢 لاگ آدرسهای IP",
|
||||
"ipLimit": "🔢 محدودیت IP",
|
||||
"setTGUser": "👤 تنظیم کاربر تلگرام",
|
||||
"toggle": "🔘 فعال / غیرفعال",
|
||||
"custom": "🔢 سفارشی",
|
||||
"confirmNumber": "✅ تایید: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ تایید اضافه کردن: {{ .Num }}",
|
||||
"limitTraffic": "🚧 محدودیت ترافیک",
|
||||
"getBanLogs": "گزارش های بلوک را دریافت کنید",
|
||||
"allClients": "همه مشتریان",
|
||||
"addClient": "افزودن مشتری",
|
||||
"submitDisable": "ارسال به عنوان غیرفعال ☑️",
|
||||
"submitEnable": "ارسال به عنوان فعال ✅",
|
||||
"use_default": "🏷️ استفاده از پیشفرض",
|
||||
"change_email": "⚙️📧 ایمیل",
|
||||
"change_comment": "⚙️💬 نظر",
|
||||
"ResetAllTraffics": "بازنشانی همه ترافیکها",
|
||||
"SortedTrafficUsageReport": "گزارش استفاده از ترافیک مرتبشده"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ انجام شد!",
|
||||
"errorOperation": "❗ خطا در عملیات.",
|
||||
"getInboundsFailed": "❌ دریافت ورودیها با خطا مواجه شد.",
|
||||
"getClientsFailed": "❌ دریافت مشتریان با شکست مواجه شد.",
|
||||
"canceled": "❌ {{ .Email }} : عملیات لغو شد.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }} : کلاینت با موفقیت تازهسازی شد.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }} : آدرسها با موفقیت تازهسازی شدند.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازهسازی شد.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }} : محدودیت ترافیک با موفقیت ذخیره شد.",
|
||||
"expireResetSuccess": "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد.",
|
||||
"resetIpSuccess": "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد.",
|
||||
"clearIpSuccess": "✅ {{ .Email }} : آدرسها با موفقیت پاکسازی شدند.",
|
||||
"getIpLog": "✅ {{ .Email }} : دریافت لاگ آدرسهای IP.",
|
||||
"getUserInfo": "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد.",
|
||||
"enableSuccess": "✅ {{ .Email }} : با موفقیت فعال شد.",
|
||||
"disableSuccess": "✅ {{ .Email }} : با موفقیت غیرفعال شد.",
|
||||
"askToAddUserId": "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "یک مشتری برای ورودی {{ .Inbound }} انتخاب کنید",
|
||||
"chooseInbound": "یک ورودی انتخاب کنید"
|
||||
}
|
||||
"noIpRecord": "❗ رکورد آی پی وجود ندارد!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "وضعیت",
|
||||
"labelOutbound": "خروجی",
|
||||
"labelNode": "نود",
|
||||
"labelError": "خطا",
|
||||
"labelDelay": "تأخیر",
|
||||
"labelUsername": "نامکاربری",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "دلیل",
|
||||
"labelSource": "مبدأ",
|
||||
"statusCrashed": "کرش کرد",
|
||||
"statusHigh": "بالا",
|
||||
"statusSuccess": "موفق",
|
||||
"statusFailed": "ناموفق",
|
||||
"statusDown": "قطع",
|
||||
"statusUp": "وصل"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Berhasil",
|
||||
"lastOnline": "Terakhir online",
|
||||
"lastSubFetch": "Pengambilan langganan terakhir",
|
||||
"getVersion": "Dapatkan Versi",
|
||||
"install": "Instal",
|
||||
"clients": "Klien",
|
||||
"usage": "Penggunaan",
|
||||
"twoFactorCode": "Kode",
|
||||
"remained": "Tersisa",
|
||||
"security": "Keamanan",
|
||||
"emptyDnsDesc": "Tidak ada server DNS yang ditambahkan.",
|
||||
"emptyFakeDnsDesc": "Tidak ada server Fake DNS yang ditambahkan.",
|
||||
"emptyBalancersDesc": "Tidak ada penyeimbang yang ditambahkan.",
|
||||
"somethingWentWrong": "Terjadi kesalahan",
|
||||
"subscription": {
|
||||
"title": "Info langganan",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Diunggah",
|
||||
"expiry": "Kedaluwarsa",
|
||||
"totalQuota": "Kuota total",
|
||||
"individualLinks": "Tautan individual",
|
||||
"active": "Aktif",
|
||||
"inactive": "Nonaktif",
|
||||
"unlimited": "Tanpa batas",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Hentikan",
|
||||
"restartXray": "Mulai ulang",
|
||||
"xraySwitch": "Versi",
|
||||
"xrayUpdates": "Pembaruan Xray",
|
||||
"xraySwitchClickDesk": "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini.",
|
||||
"updatePanel": "Perbarui Panel",
|
||||
"panelUpdateDesc": "Ini akan memperbarui 3X-UI ke rilis terbaru dan me-restart layanan panel.",
|
||||
"currentPanelVersion": "Versi panel saat ini",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Unduh",
|
||||
"sent": "Dikirim",
|
||||
"received": "Diterima",
|
||||
"xraySwitchVersionDialog": "Apakah Anda yakin ingin mengubah versi Xray?",
|
||||
"xraySwitchVersionDialogDesc": "Ini akan mengubah versi Xray ke #version#.",
|
||||
"xraySwitchVersionPopover": "Xray berhasil diperbarui",
|
||||
"panelUpdateDialog": "Apakah Anda benar-benar ingin memperbarui panel?",
|
||||
"panelUpdateDialogDesc": "Ini akan memperbarui 3X-UI ke #version# dan me-restart layanan panel.",
|
||||
"panelUpdateStartedPopover": "Pembaruan panel dimulai",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "Pembaruan tidak selesai dengan sukses. Periksa log server, atau jalankan 'x-ui update' dari baris perintah.",
|
||||
"panelUpdateUnknownTitle": "Tidak dapat memastikan pembaruan selesai",
|
||||
"panelUpdateUnknownDesc": "Panel tidak melaporkan hasil tepat waktu. Muat ulang untuk memeriksa versi saat ini, atau periksa log server.",
|
||||
"geofileUpdateDialog": "Apakah Anda yakin ingin memperbarui geofile?",
|
||||
"geofileUpdateDialogDesc": "Ini akan memperbarui file #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Ini akan memperbarui semua berkas.",
|
||||
"geofilesUpdateAll": "Perbarui semua",
|
||||
"geofileUpdatePopover": "Geofile berhasil diperbarui",
|
||||
"geodataTitle": "Pembaruan Otomatis Geodata",
|
||||
"geodataHint": "Xray mengunduh berkas ini sesuai jadwal dan memuat ulang tanpa restart. URL harus HTTPS. Setiap berkas harus sudah ada di folder bin agar Xray dapat memperbaruinya.",
|
||||
"geodataCron": "Jadwal (cron)",
|
||||
"geodataOutbound": "Unduh melalui outbound (opsional)",
|
||||
"geodataFile": "Nama berkas",
|
||||
"geodataAddFile": "Tambah berkas",
|
||||
"geodataSaveRestart": "Simpan & Mulai Ulang Xray",
|
||||
"geodataConfirmTitle": "Simpan pengaturan geodata?",
|
||||
"geodataConfirmContent": "Templat konfigurasi Xray akan diperbarui dan Xray akan dimulai ulang.",
|
||||
"geodataInvalidUrl": "Setiap berkas memerlukan URL HTTPS.",
|
||||
"geodataInvalidFile": "Nama berkas harus berupa nama sederhana, mis. geosite_custom.dat (tanpa path).",
|
||||
"geodataInvalidCron": "Cron harus terdiri dari 5 bagian, mis. 0 4 * * *",
|
||||
"geodataEmpty": "Belum ada berkas yang dikonfigurasi. Pada aturan routing, rujuk berkas sebagai ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini",
|
||||
"logs": "Log",
|
||||
"accessLogs": "Log Akses",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "Base path default \"/\" sudah umum diketahui — ubah ke path acak.",
|
||||
"warnDefaultSubPath": "Path langganan default \"/sub/\" sudah umum diketahui — ubahlah.",
|
||||
"warnDefaultJsonPath": "Path langganan JSON default \"/json/\" sudah umum diketahui — ubahlah.",
|
||||
"TGBotSettings": "Bot Telegram",
|
||||
"panelListeningIP": "IP Pendengar",
|
||||
"panelListeningIPDesc": "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)",
|
||||
"panelListeningDomain": "Domain Pendengar",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Kata Sandi Saat Ini",
|
||||
"newUsername": "Username Baru",
|
||||
"newPassword": "Kata Sandi Baru",
|
||||
"telegramBotEnable": "Aktifkan Bot Telegram",
|
||||
"telegramBotEnableDesc": "Mengaktifkan bot Telegram.",
|
||||
"telegramToken": "Token Telegram",
|
||||
"telegramTokenDesc": "Token bot Telegram yang diperoleh dari '{'@'}BotFather'.",
|
||||
"telegramProxy": "Proxy SOCKS",
|
||||
"telegramProxyDesc": "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)",
|
||||
"telegramAPIServer": "Server API Telegram",
|
||||
"telegramAPIServerDesc": "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default.",
|
||||
"telegramChatId": "ID Obrolan Admin",
|
||||
"telegramChatIdDesc": "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini {'@'}userinfobot) atau (gunakan perintah '/id' di bot)",
|
||||
"telegramNotifyTime": "Waktu Notifikasi",
|
||||
"telegramNotifyTimeDesc": "Seberapa sering bot Telegram mengirim laporan berkala. Pilih interval siap pakai, atau pilih Kustom untuk memasukkan ekspresi crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — ulangi dalam interval",
|
||||
"hourly": "@hourly — setiap jam",
|
||||
"daily": "@daily — setiap hari pukul 00:00",
|
||||
"weekly": "@weekly — setiap minggu",
|
||||
"monthly": "@monthly — setiap bulan",
|
||||
"custom": "Kustom (crontab)",
|
||||
"seconds": "Detik",
|
||||
"minutes": "Menit",
|
||||
"hours": "Jam",
|
||||
"interval": "Interval",
|
||||
"unit": "Satuan"
|
||||
},
|
||||
"tgNotifyBackup": "Cadangan Database",
|
||||
"tgNotifyBackupDesc": "Kirim berkas cadangan database dengan laporan.",
|
||||
"tgNotifyLogin": "Notifikasi Login",
|
||||
"tgNotifyLoginDesc": "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda.",
|
||||
"sessionMaxAge": "Durasi Sesi",
|
||||
"sessionMaxAgeDesc": "Durasi di mana Anda dapat tetap masuk. (unit: menit)",
|
||||
"expireTimeDiff": "Notifikasi Tanggal Kedaluwarsa",
|
||||
"expireTimeDiffDesc": "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)",
|
||||
"trafficDiff": "Notifikasi Batas Traffic",
|
||||
"trafficDiffDesc": "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)",
|
||||
"tgNotifyCpu": "Notifikasi Beban CPU",
|
||||
"tgNotifyCpuDesc": "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)",
|
||||
"timeZone": "Zone Waktu",
|
||||
"timeZoneDesc": "Tugas terjadwal akan berjalan berdasarkan zona waktu ini.",
|
||||
"subSettings": "Langganan",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64.",
|
||||
"subURI": "URI Proxy Terbalik",
|
||||
"subURIDesc": "URL dasar lengkap (scheme://domain[:port]/path/) untuk tautan langganan dan kode QR, digunakan sebagai pengganti Domain/Port Pendengar. Atur ini kapan pun langganan diakses melalui reverse proxy atau domain/port yang berbeda dari yang di atas.",
|
||||
"externalTrafficInformEnable": "Informasikan API eksternal pada setiap pembaruan lalu lintas.",
|
||||
"externalTrafficInformEnableDesc": "Beritahu API eksternal setiap kali ada pembaruan trafik.",
|
||||
"externalTrafficInformURI": "Lalu Lintas Eksternal Menginformasikan URI",
|
||||
"externalTrafficInformURIDesc": "Pembaruan lalu lintas dikirim ke URI ini.",
|
||||
"restartXrayOnClientDisable": "Nyalakan Ulang Xray Setelah Nonaktif Otomatis",
|
||||
"restartXrayOnClientDisableDesc": "Saat klien otomatis dinonaktifkan karena kedaluwarsa atau batas trafik, mulai ulang Xray.",
|
||||
"fragment": "Fragmentasi",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Pengaturan Noises",
|
||||
"trustedProxyCidrs": "CIDR proxy tepercaya",
|
||||
"trustedProxyCidrsDesc": "IP/CIDR (dipisahkan koma) yang diizinkan mengatur header forwarded host, proto, dan client IP.",
|
||||
"ldap": {
|
||||
"enable": "Aktifkan sinkronisasi LDAP",
|
||||
"host": "LDAP host",
|
||||
"port": "Port LDAP",
|
||||
"useTls": "Gunakan TLS (LDAPS)",
|
||||
"skipTlsVerify": "Lewati verifikasi sertifikat TLS",
|
||||
"skipTlsVerifyDesc": "Tidak aman — menonaktifkan validasi sertifikat server. Gunakan hanya dengan CA internal/tidak terpercaya.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Terkonfigurasi; biarkan kosong untuk mempertahankan kata sandi saat ini.",
|
||||
"passwordUnconfigured": "Tidak terkonfigurasi.",
|
||||
"passwordPlaceholder": "Terkonfigurasi — masukkan nilai baru untuk menggantikan",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Filter pengguna",
|
||||
"userAttr": "Atribut pengguna (username/email)",
|
||||
"vlessField": "Atribut flag VLESS",
|
||||
"flagField": "Atribut flag umum (opsional)",
|
||||
"flagFieldDesc": "Jika diatur, menimpa flag VLESS — mis. shadowInactive.",
|
||||
"truthyValues": "Nilai truthy",
|
||||
"truthyValuesDesc": "Dipisahkan koma; default: true,1,yes,on",
|
||||
"invertFlag": "Balik flag",
|
||||
"invertFlagDesc": "Aktifkan saat atribut berarti «dinonaktifkan» (mis. shadowInactive).",
|
||||
"syncSchedule": "Jadwal sinkronisasi",
|
||||
"syncScheduleDesc": "String mirip cron, mis. @every 1m",
|
||||
"inboundTags": "Tag inbound",
|
||||
"inboundTagsDesc": "Inbound di mana sinkronisasi LDAP dapat membuat/menghapus klien secara otomatis.",
|
||||
"noInbounds": "Tidak ada inbound. Buat dulu di Inbound.",
|
||||
"autoCreate": "Buat klien otomatis",
|
||||
"autoDelete": "Hapus klien otomatis",
|
||||
"defaultTotalGb": "Total default (GB)",
|
||||
"defaultExpiryDays": "Kedaluwarsa default (hari)",
|
||||
"defaultIpLimit": "Batas IP default"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Menyisipkan mask TCP/UDP Xray finalmask dan parameter QUIC ke setiap profil Xray JSON yang dibuat. Membutuhkan aplikasi klien yang mendukung langganan Xray JSON dan inti Xray terbaru.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Pengaturan Mux",
|
||||
"direct": "Koneksi langsung",
|
||||
"directDesc": "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu.",
|
||||
"notifications": "Notifikasi",
|
||||
"certs": "Sertifikat",
|
||||
"externalTraffic": "Lalu Lintas Eksternal",
|
||||
"dateAndTime": "Tanggal dan Waktu",
|
||||
"proxyAndServer": "Proxy dan Server",
|
||||
"intervals": "Interval",
|
||||
"information": "Informasi",
|
||||
"profile": "Profil",
|
||||
"language": "Bahasa",
|
||||
"telegramBotLanguage": "Bahasa Bot Telegram",
|
||||
"security": {
|
||||
"admin": "Kredensial admin",
|
||||
"twoFactor": "Autentikasi dua faktor",
|
||||
"twoFactorEnable": "Aktifkan 2FA",
|
||||
"twoFactorEnableDesc": "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih.",
|
||||
"twoFactorModalSetTitle": "Aktifkan autentikasi dua faktor",
|
||||
"twoFactorModalDeleteTitle": "Nonaktifkan autentikasi dua faktor",
|
||||
"twoFactorModalSteps": "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:",
|
||||
"twoFactorModalFirstStep": "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi",
|
||||
"twoFactorModalSecondStep": "2. Masukkan kode dari aplikasi",
|
||||
"twoFactorModalRemoveStep": "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Ubah kredensial",
|
||||
"twoFactorModalChangeCredentialsStep": "Masukkan kode dari aplikasi untuk mengubah kredensial administrator.",
|
||||
"twoFactorModalSetSuccess": "Autentikasi dua faktor telah berhasil dibuat",
|
||||
"twoFactorModalDeleteSuccess": "Autentikasi dua faktor telah berhasil dihapus",
|
||||
"twoFactorModalError": "Kode salah",
|
||||
"show": "Tampilkan",
|
||||
"hide": "Sembunyikan",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Gagal mendapatkan lalu lintas keluar",
|
||||
"resetOutboundTrafficError": "Gagal mereset lalu lintas keluar"
|
||||
},
|
||||
"smtpSettings": "Pengaturan SMTP",
|
||||
"smtpEnable": "Aktifkan Notifikasi Email",
|
||||
"smtpEnableDesc": "Aktifkan notifikasi email melalui SMTP",
|
||||
"smtpHost": "Host SMTP",
|
||||
"smtpHostDesc": "Nama host server SMTP (mis. smtp.gmail.com)",
|
||||
"smtpPort": "Port SMTP",
|
||||
"smtpPortDesc": "Port server SMTP (bawaan: 587)",
|
||||
"smtpUsername": "Nama Pengguna SMTP",
|
||||
"smtpUsernameDesc": "Nama pengguna autentikasi SMTP",
|
||||
"smtpFrom": "Alamat Pengirim (From)",
|
||||
"smtpFromDesc": "Alamat yang dipakai pada header From email. Kosongkan untuk memakai nama pengguna.",
|
||||
"smtpFromName": "Nama Pengirim (From)",
|
||||
"smtpFromNameDesc": "Nama tampilan opsional sebelum alamat pada header From.",
|
||||
"smtpPassword": "Kata Sandi SMTP",
|
||||
"smtpPasswordDesc": "Kata sandi autentikasi SMTP",
|
||||
"smtpTo": "Penerima",
|
||||
"smtpToDesc": "Alamat email penerima dipisahkan dengan koma",
|
||||
"emailSettings": "Email",
|
||||
"emailNotifications": "Notifikasi",
|
||||
"smtpEventBusNotify": "Notifikasi Peristiwa Email",
|
||||
"smtpEventBusNotifyDesc": "Pilih peristiwa yang memicu notifikasi email",
|
||||
"tgEventBusNotify": "Notifikasi Peristiwa Telegram",
|
||||
"tgEventBusNotifyDesc": "Pilih peristiwa yang memicu notifikasi Telegram",
|
||||
"testSmtp": "Kirim Email Uji",
|
||||
"testTgBot": "Kirim Pesan Uji",
|
||||
"eventGroupOutbound": "Outbound",
|
||||
"eventGroupXray": "Xray Core",
|
||||
"eventGroupSystem": "Sistem",
|
||||
"eventGroupSecurity": "Keamanan",
|
||||
"eventGroupNode": "Node",
|
||||
"eventOutboundDown": "Mati",
|
||||
"eventOutboundUp": "Aktif",
|
||||
"eventXrayCrash": "Crash",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Aktif",
|
||||
"eventCPUHigh": "CPU tinggi (%)",
|
||||
"requestFailed": "Permintaan gagal",
|
||||
"smtpEncryption": "Enkripsi",
|
||||
"smtpEncryptionDesc": "Metode enkripsi koneksi SMTP",
|
||||
"smtpEncryptionNone": "Tidak ada (teks biasa)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (implisit)",
|
||||
"smtpStageConnect": "Koneksi",
|
||||
"smtpStageAuth": "Autentikasi",
|
||||
"smtpStageSend": "Kirim",
|
||||
"smtpTestSuccess": "Email uji berhasil dikirim",
|
||||
"smtpHostNotConfigured": "Host SMTP belum dikonfigurasi",
|
||||
"smtpNoRecipients": "Tidak ada penerima yang dikonfigurasi",
|
||||
"smtpFromNotConfigured": "Alamat pengirim SMTP belum dikonfigurasi",
|
||||
"eventLoginAttempt": "Percobaan masuk",
|
||||
"telegramTokenConfigured": "Terkonfigurasi; kosongkan untuk mempertahankan token saat ini.",
|
||||
"telegramTokenPlaceholder": "Terkonfigurasi - masukkan token baru untuk mengganti",
|
||||
"smtpPasswordConfigured": "Terkonfigurasi; kosongkan untuk mempertahankan kata sandi saat ini.",
|
||||
"smtpPasswordPlaceholder": "Terkonfigurasi - masukkan kata sandi baru untuk mengganti",
|
||||
"smtpNotInitialized": "SMTP belum diinisialisasi",
|
||||
"tgBotNotEnabled": "Bot Telegram tidak aktif",
|
||||
"tgTestFailed": "Uji Telegram gagal",
|
||||
"tgTestSuccess": "Pesan uji terkirim ke Telegram",
|
||||
"tgBotNotRunning": "Bot Telegram tidak berjalan",
|
||||
"smtpErrorAuth": "Autentikasi gagal — periksa nama pengguna dan kata sandi",
|
||||
"smtpErrorStarttls": "Server memerlukan STARTTLS — ubah jenis enkripsi",
|
||||
"smtpErrorTls": "Server memerlukan TLS — ubah jenis enkripsi",
|
||||
"smtpErrorRefused": "Koneksi ditolak — periksa host dan port",
|
||||
"smtpErrorTimeout": "Koneksi waktu habis — host tidak dapat dijangkau",
|
||||
"smtpErrorRelay": "Server menolak pengiriman dari alamat ini",
|
||||
"smtpErrorEof": "Koneksi ditutup oleh server",
|
||||
"smtpErrorUnknown": "Kesalahan SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Penggunaan memori tinggi (%)",
|
||||
"remarkTemplate": "Templat Catatan",
|
||||
"remarkTemplateDesc": "Jika diatur, ini menggantikan model catatan untuk setiap tautan langganan — tulis format Anda sendiri dengan token variabel (gunakan tombol untuk menyisipkannya). Biarkan kosong untuk memakai model di atas.",
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "Domain Langsung",
|
||||
"ipv4Routing": "Perutean IPv4",
|
||||
"ipv4RoutingDesc": "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4.",
|
||||
"Template": "Template Konfigurasi Xray Lanjutan",
|
||||
"TemplateDesc": "File konfigurasi Xray akhir akan dibuat berdasarkan template ini.",
|
||||
"FreedomStrategy": "Strategi Protokol Freedom",
|
||||
"FreedomStrategyDesc": "Atur strategi output untuk jaringan dalam Protokol Freedom.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "Penyeimbang",
|
||||
"balancerTagRequired": "Tag wajib diisi",
|
||||
"balancerSelectorRequired": "Pilih setidaknya satu outbound",
|
||||
"balancerLive": "Target Saat Ini",
|
||||
"balancerOverride": "Paksa Tujuan",
|
||||
"balancerOverridePh": "Otomatis (strategi)",
|
||||
"balancerLiveRefresh": "Perbarui status penyeimbang beban",
|
||||
"balancerNotRunning": "Penyeimbang ini tidak aktif di Xray yang berjalan — simpan perubahan atau mulai Xray terlebih dahulu",
|
||||
"routeTester": "Uji Rute",
|
||||
"routeTesterDesc": "Tanyakan kepada Xray yang berjalan outbound mana yang akan menangani koneksi. Tidak ada lalu lintas yang dikirim — keputusan langsung datang dari mesin routing langsung.",
|
||||
"routeTesterDest": "Domain atau IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "melalui penyeimbang",
|
||||
"routeTesterDefaultOutbound": "Tidak ada aturan routing yang cocok — lalu lintas menuju outbound default (pertama).",
|
||||
"Routings": "Aturan Pengalihan",
|
||||
"completeTemplate": "Semua",
|
||||
"logLevel": "Tingkat Log",
|
||||
"logLevelDesc": "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat.",
|
||||
"accessLog": "Log Akses",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Sumber",
|
||||
"dest": "Tujuan",
|
||||
"inbound": "Masuk",
|
||||
"balancer": "Pengimbang",
|
||||
"useComma": "Item yang dipisahkan koma"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Server ini sudah ada di daftar outbound. Gunakan {reset} untuk memperbarui kuncinya.",
|
||||
"provisionFailed": "Gagal membuat outbound PIA. Coba lagi."
|
||||
},
|
||||
"tabBalancerSettings": "Pengaturan Balancer",
|
||||
"tabObservatory": "Observatory",
|
||||
"observatory": {
|
||||
"autoManaged": "Observer dikelola otomatis dari balancer Anda. Atur cara mereka melakukan probe di bawah; outbound yang dipantau mengikuti selector balancer.",
|
||||
"emptyHint": "Tidak ada observer koneksi yang aktif. Satu akan ditambahkan otomatis saat Anda membuat balancer Least Ping atau Least Load — atau balancer Random / Round-robin dengan fallback — sehingga balancer yang memakai observer dapat memeriksa kesehatan outbound sebelum memilih target.",
|
||||
"mixedLegacy": "Konfigurasi ini berisi Observatory dan Burst Observatory sekaligus. Xray memakai satu observer global, jadi status campuran lama ini tidak didukung; menyimpan balancer akan menormalkannya menjadi satu observer.",
|
||||
"subjectSelector": "Outbound yang Dipantau",
|
||||
"subjectSelectorDesc": "Tag outbound yang di-probe observer ini. Dikelola otomatis dari balancer Anda.",
|
||||
"probeURL": "URL Probe",
|
||||
"probeURLDesc": "URL yang diminta untuk mengukur setiap outbound. Harus mengembalikan HTTP 204.",
|
||||
"probeInterval": "Interval Probe",
|
||||
"probeIntervalDesc": "Seberapa sering memprobe tiap outbound, mis. 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Probe Bersamaan",
|
||||
"enableConcurrencyDesc": "Probe semua outbound yang dipantau sekaligus, bukan satu per satu. Lebih cepat, tetapi lebih terlihat di jaringan.",
|
||||
"destination": "Tujuan Probe",
|
||||
"destinationDesc": "URL yang diminta untuk mengukur setiap outbound. Harus mengembalikan HTTP 204.",
|
||||
"connectivity": "Pemeriksaan Konektivitas",
|
||||
"connectivityDesc": "URL pemeriksaan jaringan lokal opsional, dicoba hanya setelah tujuan gagal. Kosongkan untuk melewati.",
|
||||
"interval": "Interval Probe",
|
||||
"intervalDesc": "Rata-rata waktu antar probe per outbound, mis. 1m. Minimal 10s.",
|
||||
"timeout": "Batas Waktu Probe",
|
||||
"timeoutDesc": "Berapa lama menunggu probe sebelum dianggap gagal, mis. 5s.",
|
||||
"sampling": "Jumlah Sampling",
|
||||
"samplingDesc": "Jumlah hasil probe terbaru yang disimpan untuk menilai tiap outbound.",
|
||||
"httpMethod": "Metode HTTP",
|
||||
"httpMethodDesc": "Metode HTTP yang digunakan untuk probe.",
|
||||
"deleteAlsoObservatory": "Ini balancer terakhir yang memakai Observatory, jadi itu juga akan dihapus.",
|
||||
"deleteAlsoBurst": "Ini balancer terakhir yang memakai Burst Observatory, jadi itu juga akan dihapus."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Strategi",
|
||||
"tag": "Tag",
|
||||
"tagDuplicate": "Tag sudah digunakan oleh balancer lain",
|
||||
"tagPlaceholder": "tag balancer unik",
|
||||
"selector": "Selector",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Siklus: {path} → (kembali ke {start})",
|
||||
"expected": "Diharapkan",
|
||||
"expectedPlaceholder": "jumlah node optimal",
|
||||
"maxRtt": "Maks. RTT",
|
||||
"tolerance": "Toleransi",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Pola tag",
|
||||
"costValue": "Bobot",
|
||||
"costRegexp": "Pencocokan ekspresi reguler",
|
||||
"balancerDeleteInUse": "Tidak dapat menghapus load balancer ini — digunakan sebagai fallback untuk: {names}",
|
||||
"balancerFallbackCycle": "Tidak dapat mengatur load balancer ini sebagai fallback — akan menciptakan dependensi siklik.",
|
||||
"balancerFallbackInfo": "Lalu lintas akan di-routing melalui: Load Balancer → Loopback → Server → Load Balancer tujuan → Koneksi keluar. Ini menambahkan hop tambahan melalui server, yang dapat menyebabkan sedikit penundaan.",
|
||||
"fallbackBalancerHint": "Pilih load balancer lain sebagai fallback",
|
||||
"reservedPrefix": "Awalan _bl_ dicadangkan untuk objek loopback internal load balancer"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Penggunaan",
|
||||
"addOutbound": "Tambah outbound"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Aktifkan DNS",
|
||||
"enableDesc": "Aktifkan server DNS bawaan",
|
||||
"tag": "Tanda DNS Masuk",
|
||||
"tagDesc": "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan.",
|
||||
"clientIp": "IP Klien",
|
||||
"clientIpDesc": "Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS",
|
||||
"disableCache": "Nonaktifkan cache",
|
||||
"disableCacheDesc": "Menonaktifkan caching DNS",
|
||||
"disableFallback": "Nonaktifkan Fallback",
|
||||
"disableFallbackDesc": "Menonaktifkan kueri DNS fallback",
|
||||
"disableFallbackIfMatch": "Nonaktifkan Fallback Jika Cocok",
|
||||
"disableFallbackIfMatchDesc": "Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi",
|
||||
"enableParallelQuery": "Aktifkan Kueri Paralel",
|
||||
"enableParallelQueryDesc": "Aktifkan kueri DNS paralel ke beberapa server untuk resolusi yang lebih cepat",
|
||||
"strategy": "Strategi Kueri",
|
||||
"strategyDesc": "Strategi keseluruhan untuk menyelesaikan nama domain",
|
||||
"add": "Tambahkan Server",
|
||||
"edit": "Sunting Server",
|
||||
"domains": "Domain",
|
||||
"expectIPs": "IP yang Diharapkan",
|
||||
"unexpectIPs": "IP tak terduga",
|
||||
"useSystemHosts": "Gunakan Hosts Sistem",
|
||||
"useSystemHostsDesc": "Gunakan file hosts dari sistem yang terinstal",
|
||||
"serveStale": "Sajikan Kedaluwarsa",
|
||||
"serveStaleDesc": "Mengembalikan hasil cache yang kedaluwarsa saat memperbarui di latar belakang",
|
||||
"serveExpiredTTL": "TTL Kedaluwarsa",
|
||||
"serveExpiredTTLDesc": "Masa berlaku (detik) entri cache kedaluwarsa; 0 = tidak pernah kedaluwarsa",
|
||||
"timeoutMs": "Batas waktu (ms)",
|
||||
"skipFallback": "Lewati Fallback",
|
||||
"finalQuery": "Kueri Akhir",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Tambah Host",
|
||||
"hostsEmpty": "Tidak ada Host yang ditentukan",
|
||||
"hostsDomain": "Domain (mis. domain:example.com)",
|
||||
"hostsValues": "IP atau domain — ketik dan tekan Enter",
|
||||
"usePreset": "Gunakan templat",
|
||||
"dnsPresetTitle": "Templat DNS",
|
||||
"dnsPresetFamily": "Keluarga",
|
||||
"clearAll": "Hapus Semua",
|
||||
"clearAllTitle": "Hapus semua server DNS?",
|
||||
"clearAllConfirm": "Ini akan menghapus semua server DNS dari daftar. Tidak dapat dibatalkan.",
|
||||
"dnsLeakWarning": "DNS dapat bocor lewat localhost, UDP/TCP polos, DoH/DoQ mode lokal, kueri fallback, atau EDNS client IP. Gunakan DoH yang dirutekan, pin resolver di hosts, dan nonaktifkan fallback saat privasi penting."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Tambahkan DNS Palsu",
|
||||
"ipPool": "Subnet Kumpulan IP",
|
||||
"poolSize": "Ukuran Kolam"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Outbound default",
|
||||
"defaultOutboundDesc": "Lalu lintas tanpa aturan routing memakai outbound ini (yang pertama dalam daftar)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Keyboard ditutup!",
|
||||
"noResult": "❗ Tidak ada hasil!",
|
||||
"noQuery": "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!",
|
||||
"wentWrong": "❌ Terjadi kesalahan!",
|
||||
"noIpRecord": "❗ Tidak ada Catatan IP!",
|
||||
"noInbounds": "❗ Tidak ada inbound yang ditemukan!",
|
||||
"unlimited": "♾ Tidak terbatas (Reset)",
|
||||
"add": "Tambah",
|
||||
"month": "Bulan",
|
||||
"months": "Bulan",
|
||||
"days": "Hari",
|
||||
"hours": "Jam",
|
||||
"minutes": "Menit",
|
||||
"unknown": "Tidak diketahui",
|
||||
"inbounds": "Inbound",
|
||||
"clients": "Klien",
|
||||
"offline": "🔴 Offline",
|
||||
"online": "🟢 Online",
|
||||
"commands": {
|
||||
"unknown": "❗ Perintah tidak dikenal.",
|
||||
"pleaseChoose": "👇 Harap pilih:\r\n",
|
||||
"help": "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n",
|
||||
"start": "👋 Halo <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Selamat datang di <b>{{.Hostname }}</b> bot managemen.\r\n",
|
||||
"status": "✅ Bot dalam keadaan baik!",
|
||||
"usage": "❗ Harap berikan teks untuk mencari!",
|
||||
"getID": "🆔 ID Anda: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Untuk memulai ulang Xray Core:\r\n<code>/restart</code>\r\n\r\nUntuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n<code>/inbound [Catatan]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "Untuk mencari statistik, gunakan perintah berikut:\r\n<code>/usage [Email]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Operasi berhasil!",
|
||||
"restartFailed": "❗ Kesalahan dalam operasi.\r\n\r\n<code>Error: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core tidak berjalan.",
|
||||
"startDesc": "Tampilkan menu utama",
|
||||
"helpDesc": "Bantuan bot",
|
||||
"statusDesc": "Periksa status bot",
|
||||
"idDesc": "Tampilkan ID Telegram Anda",
|
||||
"usageDesc": "Lihat pemakaian klien: /usage email",
|
||||
"inboundDesc": "Cari inbound: /inbound nama (admin)",
|
||||
"restartDesc": "Mulai ulang inti Xray (admin)",
|
||||
"clearallDesc": "Reset trafik semua klien (admin)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Kesalahan dalam pemilihan pengguna!",
|
||||
"userSaved": "✅ Pengguna Telegram tersimpan.",
|
||||
"loginSuccess": "✅ Berhasil masuk ke panel.\r\n",
|
||||
"loginFailed": "❗️ Gagal masuk ke panel.\r\n",
|
||||
"report": "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Host: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 Versi 3X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Versi Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Status: {{ .State }}\r\n",
|
||||
"username": "👤 Nama Pengguna: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Alasan: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Waktu: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Inbound: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Port: {{ .Port }}\r\n",
|
||||
"expire": "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Kadaluarsa Dalam: {{ .Time }}\r\n",
|
||||
"active": "💡 Aktif: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Diaktifkan: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Status Koneksi: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Terakhir online: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Unggah: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Unduh: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Pengguna Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Jumlah Habis {{ .Type }}:\r\n",
|
||||
"onlinesCount": "🌐 Klien Online: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Dinonaktifkan: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Waktu Backup: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Ya",
|
||||
"no": "❌ Tidak",
|
||||
"received_email": "📧📥 Email diperbarui.",
|
||||
"received_comment": "💬📥 Komentar diperbarui.",
|
||||
"email_prompt": "📧 Email Default: {{ .ClientEmail }}\n\nMasukkan email Anda.",
|
||||
"comment_prompt": "💬 Komentar Default: {{ .ClientComment }}\n\nMasukkan komentar Anda.",
|
||||
"cancel": "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄",
|
||||
"error_add_client": "⚠️ Error:\n\n {{ .error }}",
|
||||
"using_default_value": "Oke, saya akan tetap menggunakan nilai default. 😊",
|
||||
"incorrect_input": "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫",
|
||||
"AreYouSure": "Apakah kamu yakin? 🤔",
|
||||
"SuccessResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil",
|
||||
"FailedResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠️ Kesalahan: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Proses reset traffic selesai untuk semua klien.",
|
||||
"eventOutboundDown": "Outbound {{ .Tag }} MATI",
|
||||
"eventOutboundUp": "Outbound {{ .Tag }} AKTIF",
|
||||
"eventErrorDetail": "Kesalahan: {{ .Error }}",
|
||||
"eventDelayDetail": "Penundaan: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xray CRASH",
|
||||
"eventXrayCrashError": "Kesalahan: {{ .Error }}",
|
||||
"eventNodeDown": "Node {{ .Name }} MATI",
|
||||
"eventNodeUp": "Node {{ .Name }} AKTIF",
|
||||
"eventLoginFallback": "Gagal masuk dari {{ .Source }}",
|
||||
"memoryThreshold": "Penggunaan memori {{ .Percent }}% melebihi ambang batas {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Tutup Papan Ketik",
|
||||
"cancel": "❌ Batal",
|
||||
"cancelReset": "❌ Batal Reset",
|
||||
"cancelIpLimit": "❌ Batal Batas IP",
|
||||
"confirmResetTraffic": "✅ Konfirmasi Reset Lalu Lintas?",
|
||||
"confirmClearIps": "✅ Konfirmasi Hapus IPs?",
|
||||
"confirmRemoveTGUser": "✅ Konfirmasi Hapus Pengguna Telegram?",
|
||||
"confirmToggle": "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?",
|
||||
"dbBackup": "Dapatkan Cadangan DB",
|
||||
"serverUsage": "Penggunaan Server",
|
||||
"getInbounds": "Dapatkan Inbounds",
|
||||
"depleteSoon": "Habis Sebentar",
|
||||
"clientUsage": "Dapatkan Penggunaan",
|
||||
"onlines": "Klien Online",
|
||||
"commands": "Perintah",
|
||||
"refresh": "🔄 Perbarui",
|
||||
"clearIPs": "❌ Hapus IPs",
|
||||
"removeTGUser": "❌ Hapus Pengguna Telegram",
|
||||
"selectTGUser": "👤 Pilih Pengguna Telegram",
|
||||
"selectOneTGUser": "👤 Pilih Pengguna Telegram:",
|
||||
"resetTraffic": "📈 Reset Lalu Lintas",
|
||||
"resetExpire": "📅 Ubah Tanggal Kadaluarsa",
|
||||
"ipLog": "🔢 Log IP",
|
||||
"ipLimit": "🔢 Batas IP",
|
||||
"setTGUser": "👤 Set Pengguna Telegram",
|
||||
"toggle": "🔘 Aktifkan / Nonaktifkan",
|
||||
"custom": "🔢 Kustom",
|
||||
"confirmNumber": "✅ Konfirmasi: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Konfirmasi menambahkan: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Batas Lalu Lintas",
|
||||
"getBanLogs": "Dapatkan Log Pemblokiran",
|
||||
"allClients": "Semua Klien",
|
||||
"addClient": "Tambah Klien",
|
||||
"submitDisable": "Kirim Sebagai Nonaktif ☑️",
|
||||
"submitEnable": "Kirim Sebagai Aktif ✅",
|
||||
"use_default": "🏷️ Gunakan Default",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Komentar",
|
||||
"ResetAllTraffics": "Reset Semua Lalu Lintas",
|
||||
"SortedTrafficUsageReport": "Laporan Penggunaan Lalu Lintas yang Terurut"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Operasi berhasil!",
|
||||
"errorOperation": "❗ Kesalahan dalam operasi.",
|
||||
"getInboundsFailed": "❌ Gagal mendapatkan inbounds.",
|
||||
"getClientsFailed": "❌ Gagal mendapatkan klien.",
|
||||
"canceled": "❌ {{ .Email }}: Operasi dibatalkan.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Klien diperbarui dengan berhasil.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IP diperbarui dengan berhasil.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Lalu lintas direset dengan berhasil.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IP dihapus dengan berhasil.",
|
||||
"getIpLog": "✅ {{ .Email }}: Dapatkan Log IP.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Diaktifkan dengan berhasil.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Dinonaktifkan dengan berhasil.",
|
||||
"askToAddUserId": "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ChatID Telegram Anda dalam konfigurasi Anda.\r\n\r\nChatID Pengguna Anda: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Pilih Klien untuk Inbound {{ .Inbound }}",
|
||||
"chooseInbound": "Pilih Inbound"
|
||||
}
|
||||
"noIpRecord": "❗ Tidak ada Catatan IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Status",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelNode": "Node",
|
||||
"labelError": "Kesalahan",
|
||||
"labelDelay": "Penundaan",
|
||||
"labelUsername": "Nama Pengguna",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Alasan",
|
||||
"labelSource": "Sumber",
|
||||
"statusCrashed": "CRASH",
|
||||
"statusHigh": "TINGGI",
|
||||
"statusSuccess": "BERHASIL",
|
||||
"statusFailed": "GAGAL",
|
||||
"statusDown": "MATI",
|
||||
"statusUp": "AKTIF"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "成功",
|
||||
"lastOnline": "最終オンライン",
|
||||
"lastSubFetch": "最終サブスクリプション取得",
|
||||
"getVersion": "バージョン取得",
|
||||
"install": "インストール",
|
||||
"clients": "クライアント",
|
||||
"usage": "利用状況",
|
||||
"twoFactorCode": "コード",
|
||||
"remained": "残り",
|
||||
"security": "セキュリティ",
|
||||
"emptyDnsDesc": "追加されたDNSサーバーはありません。",
|
||||
"emptyFakeDnsDesc": "追加されたFake DNSサーバーはありません。",
|
||||
"emptyBalancersDesc": "追加されたバランサーはありません。",
|
||||
"somethingWentWrong": "エラーが発生しました",
|
||||
"subscription": {
|
||||
"title": "サブスクリプション情報",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "アップロード",
|
||||
"expiry": "有効期限",
|
||||
"totalQuota": "合計クォータ",
|
||||
"individualLinks": "個別リンク",
|
||||
"active": "有効",
|
||||
"inactive": "無効",
|
||||
"unlimited": "無制限",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "再起動",
|
||||
"xraySwitch": "バージョン",
|
||||
"xrayUpdates": "Xrayの更新",
|
||||
"xraySwitchClickDesk": "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。",
|
||||
"updatePanel": "パネルを更新",
|
||||
"panelUpdateDesc": "これにより3X-UIが最新リリースに更新され、パネルサービスが再起動されます。",
|
||||
"currentPanelVersion": "現在のパネルバージョン",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "ダウンロード",
|
||||
"sent": "送信",
|
||||
"received": "受信",
|
||||
"xraySwitchVersionDialog": "Xrayのバージョンを本当に変更しますか?",
|
||||
"xraySwitchVersionDialogDesc": "Xrayのバージョンが#version#に変更されます。",
|
||||
"xraySwitchVersionPopover": "Xrayの更新が成功しました",
|
||||
"panelUpdateDialog": "本当にパネルを更新しますか?",
|
||||
"panelUpdateDialogDesc": "これにより3X-UIが#version#に更新され、パネルサービスが再起動されます。",
|
||||
"panelUpdateStartedPopover": "パネルの更新を開始しました",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "更新が正常に完了しませんでした。サーバーのログを確認するか、コマンドラインで「x-ui update」を実行してください。",
|
||||
"panelUpdateUnknownTitle": "更新が完了したか確認できませんでした",
|
||||
"panelUpdateUnknownDesc": "パネルから時間内に結果が報告されませんでした。再読み込みして現在のバージョンを確認するか、サーバーのログを確認してください。",
|
||||
"geofileUpdateDialog": "ジオファイルを本当に更新しますか?",
|
||||
"geofileUpdateDialogDesc": "これにより#filename#ファイルが更新されます。",
|
||||
"geofilesUpdateDialogDesc": "これにより、すべてのファイルが更新されます。",
|
||||
"geofilesUpdateAll": "すべて更新",
|
||||
"geofileUpdatePopover": "ジオファイルの更新が成功しました",
|
||||
"geodataTitle": "Geodata 自動更新",
|
||||
"geodataHint": "Xray はスケジュールに従ってこれらのファイルをダウンロードし、再起動なしでホットリロードします。URL は HTTPS が必須です。各ファイルは事前に bin フォルダーに存在している必要があります。",
|
||||
"geodataCron": "スケジュール (cron)",
|
||||
"geodataOutbound": "アウトバウンド経由でダウンロード(任意)",
|
||||
"geodataFile": "ファイル名",
|
||||
"geodataAddFile": "ファイルを追加",
|
||||
"geodataSaveRestart": "保存して Xray を再起動",
|
||||
"geodataConfirmTitle": "geodata 設定を保存しますか?",
|
||||
"geodataConfirmContent": "Xray 設定テンプレートを更新し、Xray を再起動します。",
|
||||
"geodataInvalidUrl": "各ファイルには HTTPS URL が必要です。",
|
||||
"geodataInvalidFile": "ファイル名はパスを含まない単純な名前にしてください(例: geosite_custom.dat)。",
|
||||
"geodataInvalidCron": "Cron は 5 フィールド必要です(例: 0 4 * * *)",
|
||||
"geodataEmpty": "ファイルが設定されていません。ルーティングルールでは ext:geosite_custom.dat:category の形式で参照します。",
|
||||
"dontRefresh": "インストール中、このページをリロードしないでください",
|
||||
"logs": "ログ",
|
||||
"accessLogs": "アクセスログ",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "デフォルトのベースパス \"/\" はよく知られています — ランダムなパスに変更してください。",
|
||||
"warnDefaultSubPath": "デフォルトのサブスクリプションパス \"/sub/\" はよく知られています — 変更してください。",
|
||||
"warnDefaultJsonPath": "デフォルトの JSON サブスクリプションパス \"/json/\" はよく知られています — 変更してください。",
|
||||
"TGBotSettings": "Telegram Bot",
|
||||
"panelListeningIP": "パネル監視IP",
|
||||
"panelListeningIPDesc": "デフォルトではすべてのIPを監視する",
|
||||
"panelListeningDomain": "パネル監視ドメイン",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "旧パスワード",
|
||||
"newUsername": "新しいユーザー名",
|
||||
"newPassword": "新しいパスワード",
|
||||
"telegramBotEnable": "Telegramボットを有効にする",
|
||||
"telegramBotEnableDesc": "Telegramボット機能を有効にする",
|
||||
"telegramToken": "Telegram トークン",
|
||||
"telegramTokenDesc": "'{'@'}BotFather'から取得したTelegramボットトークン",
|
||||
"telegramProxy": "SOCKS プロキシ",
|
||||
"telegramProxyDesc": "SOCKS5プロキシを有効にしてTelegramに接続する(ガイドに従って設定を調整)",
|
||||
"telegramAPIServer": "Telegram API サーバー",
|
||||
"telegramAPIServerDesc": "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する",
|
||||
"telegramChatId": "管理者チャットID",
|
||||
"telegramChatIdDesc": "Telegram管理者チャットID(複数の場合はカンマで区切る){'@'}userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する",
|
||||
"telegramNotifyTime": "通知時間",
|
||||
"telegramNotifyTimeDesc": "Telegram ボットが定期レポートを送信する頻度です。プリセットの間隔を選ぶか、「カスタム」を選んで crontab 式を入力します。",
|
||||
"notifyTime": {
|
||||
"every": "@every — 一定間隔で繰り返す",
|
||||
"hourly": "@hourly — 1時間ごと",
|
||||
"daily": "@daily — 毎日 00:00",
|
||||
"weekly": "@weekly — 毎週",
|
||||
"monthly": "@monthly — 毎月",
|
||||
"custom": "カスタム (crontab)",
|
||||
"seconds": "秒",
|
||||
"minutes": "分",
|
||||
"hours": "時間",
|
||||
"interval": "間隔",
|
||||
"unit": "単位"
|
||||
},
|
||||
"tgNotifyBackup": "データベースバックアップ",
|
||||
"tgNotifyBackupDesc": "レポート付きのデータベースバックアップファイルを送信",
|
||||
"tgNotifyLogin": "ログイン通知",
|
||||
"tgNotifyLoginDesc": "誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する",
|
||||
"sessionMaxAge": "セッション期間",
|
||||
"sessionMaxAgeDesc": "ログイン状態を保持する期間(単位:分)",
|
||||
"expireTimeDiff": "有効期限通知のしきい値",
|
||||
"expireTimeDiffDesc": "このしきい値に達した場合、有効期限に関する通知を受け取る(単位:日)",
|
||||
"trafficDiff": "トラフィック消耗しきい値",
|
||||
"trafficDiffDesc": "このしきい値に達した場合、トラフィック消耗に関する通知を受け取る(単位:GB)",
|
||||
"tgNotifyCpu": "CPU負荷通知しきい値",
|
||||
"tgNotifyCpuDesc": "CPU負荷がこのしきい値を超えた場合、通知を受け取る(単位:%)",
|
||||
"timeZone": "タイムゾーン",
|
||||
"timeZoneDesc": "定時タスクはこのタイムゾーンの時間に従って実行される",
|
||||
"subSettings": "サブスクリプション設定",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "サブスクリプションサービスが返す内容をBase64エンコードする",
|
||||
"subURI": "リバースプロキシURI",
|
||||
"subURIDesc": "サブスクリプションリンクとQRコードに使われる完全なベースURL(scheme://domain[:port]/path/)で、監視ドメイン/監視ポートの代わりに使用されます。サブスクリプションがリバースプロキシ経由、または上記と異なるドメイン/ポートでアクセスされる場合に設定してください。",
|
||||
"externalTrafficInformEnable": "外部トラフィック情報",
|
||||
"externalTrafficInformEnableDesc": "トラフィック更新ごとに外部 API に通知。",
|
||||
"externalTrafficInformURI": "外部トラフィック通知 URI",
|
||||
"externalTrafficInformURIDesc": "トラフィックの更新ごとに外部 API に通知します。",
|
||||
"restartXrayOnClientDisable": "自動無効化後に Xray を再起動",
|
||||
"restartXrayOnClientDisableDesc": "有効期限切れまたはトラフィック上限でクライアントが自動的に無効化されたとき、Xray を再起動します。",
|
||||
"fragment": "フラグメント",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Noises設定",
|
||||
"trustedProxyCidrs": "信頼できるプロキシ CIDR",
|
||||
"trustedProxyCidrsDesc": "転送される host、proto、クライアント IP ヘッダーを設定可能な IP/CIDR (カンマ区切り)。",
|
||||
"ldap": {
|
||||
"enable": "LDAP 同期を有効化",
|
||||
"host": "LDAP host",
|
||||
"port": "LDAP ポート",
|
||||
"useTls": "TLS (LDAPS) を使用",
|
||||
"skipTlsVerify": "TLS 証明書の検証をスキップ",
|
||||
"skipTlsVerifyDesc": "安全ではありません — サーバー証明書の検証を無効化します。内部/信頼できない CA でのみ使用してください。",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "設定済み;現在のパスワードを保持するには空のままにします。",
|
||||
"passwordUnconfigured": "未設定。",
|
||||
"passwordPlaceholder": "設定済み — 置き換えるには新しい値を入力",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "ユーザーフィルター",
|
||||
"userAttr": "ユーザー属性 (username/email)",
|
||||
"vlessField": "VLESS flag 属性",
|
||||
"flagField": "汎用 flag 属性 (任意)",
|
||||
"flagFieldDesc": "設定すると VLESS flag を上書きします — 例: shadowInactive。",
|
||||
"truthyValues": "Truthy 値",
|
||||
"truthyValuesDesc": "カンマ区切り;デフォルト: true,1,yes,on",
|
||||
"invertFlag": "flag を反転",
|
||||
"invertFlagDesc": "属性が「無効」を意味する場合に有効化 (例: shadowInactive)。",
|
||||
"syncSchedule": "同期スケジュール",
|
||||
"syncScheduleDesc": "cron 風の文字列、例 @every 1m",
|
||||
"inboundTags": "インバウンドタグ",
|
||||
"inboundTagsDesc": "LDAP 同期がクライアントを自動作成/削除できるインバウンド。",
|
||||
"noInbounds": "インバウンドが見つかりません。先にインバウンドで作成してください。",
|
||||
"autoCreate": "クライアントを自動作成",
|
||||
"autoDelete": "クライアントを自動削除",
|
||||
"defaultTotalGb": "デフォルト合計 (GB)",
|
||||
"defaultExpiryDays": "デフォルト有効期限 (日)",
|
||||
"defaultIpLimit": "デフォルト IP 制限"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "生成される各 Xray JSON プロファイルへ Xray finalmask の TCP/UDP マスクと QUIC パラメーターを追加します。Xray JSON サブスクリプション対応アプリと新しい Xray コアが必要です。",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "マルチプレクサ設定",
|
||||
"direct": "直接接続",
|
||||
"directDesc": "特定の国のドメインまたはIP範囲に直接接続する",
|
||||
"notifications": "通知",
|
||||
"certs": "証明書",
|
||||
"externalTraffic": "外部トラフィック",
|
||||
"dateAndTime": "日付と時刻",
|
||||
"proxyAndServer": "プロキシとサーバー",
|
||||
"intervals": "間隔",
|
||||
"information": "情報",
|
||||
"profile": "プロフィール",
|
||||
"language": "言語",
|
||||
"telegramBotLanguage": "Telegram Botの言語",
|
||||
"security": {
|
||||
"admin": "管理者の資格情報",
|
||||
"twoFactor": "二段階認証",
|
||||
"twoFactorEnable": "2FAを有効化",
|
||||
"twoFactorEnableDesc": "セキュリティを強化するために追加の認証層を追加します。",
|
||||
"twoFactorModalSetTitle": "二段階認証を有効にする",
|
||||
"twoFactorModalDeleteTitle": "二段階認証を無効にする",
|
||||
"twoFactorModalSteps": "二段階認証を設定するには、次の手順を実行してください:",
|
||||
"twoFactorModalFirstStep": "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます",
|
||||
"twoFactorModalSecondStep": "2. アプリからコードを入力してください",
|
||||
"twoFactorModalRemoveStep": "二段階認証を削除するには、アプリからコードを入力してください。",
|
||||
"twoFactorModalChangeCredentialsTitle": "認証情報の変更",
|
||||
"twoFactorModalChangeCredentialsStep": "管理者の認証情報を変更するには、アプリケーションからコードを入力してください。",
|
||||
"twoFactorModalSetSuccess": "二要素認証が正常に設定されました",
|
||||
"twoFactorModalDeleteSuccess": "二要素認証が正常に削除されました",
|
||||
"twoFactorModalError": "コードが間違っています",
|
||||
"show": "表示",
|
||||
"hide": "非表示",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "送信トラフィックの取得エラー",
|
||||
"resetOutboundTrafficError": "送信トラフィックのリセットエラー"
|
||||
},
|
||||
"smtpSettings": "SMTP設定",
|
||||
"smtpEnable": "メール通知を有効化",
|
||||
"smtpEnableDesc": "SMTP経由のメール通知を有効にします",
|
||||
"smtpHost": "SMTPホスト",
|
||||
"smtpHostDesc": "SMTPサーバーのホスト名(例: smtp.gmail.com)",
|
||||
"smtpPort": "SMTPポート",
|
||||
"smtpPortDesc": "SMTPサーバーのポート(既定値: 587)",
|
||||
"smtpUsername": "SMTPユーザー名",
|
||||
"smtpUsernameDesc": "SMTP認証用のユーザー名",
|
||||
"smtpFrom": "送信元アドレス (From)",
|
||||
"smtpFromDesc": "メールの From ヘッダーに使用するアドレス。空欄の場合はユーザー名を使用します。",
|
||||
"smtpFromName": "送信者名 (From)",
|
||||
"smtpFromNameDesc": "From ヘッダーでアドレスの前に表示される任意の表示名。",
|
||||
"smtpPassword": "SMTPパスワード",
|
||||
"smtpPasswordDesc": "SMTP認証用のパスワード",
|
||||
"smtpTo": "受信者",
|
||||
"smtpToDesc": "受信者のメールアドレス(カンマ区切り)",
|
||||
"emailSettings": "メール",
|
||||
"emailNotifications": "通知",
|
||||
"smtpEventBusNotify": "メールイベント通知",
|
||||
"smtpEventBusNotifyDesc": "メール通知をトリガーするイベントを選択してください",
|
||||
"tgEventBusNotify": "Telegramイベント通知",
|
||||
"tgEventBusNotifyDesc": "Telegram通知をトリガーするイベントを選択してください",
|
||||
"testSmtp": "テストメールを送信",
|
||||
"testTgBot": "テストメッセージを送信",
|
||||
"eventGroupOutbound": "アウトバウンド",
|
||||
"eventGroupXray": "Xrayコア",
|
||||
"eventGroupSystem": "システム",
|
||||
"eventGroupSecurity": "セキュリティ",
|
||||
"eventGroupNode": "ノード",
|
||||
"eventOutboundDown": "ダウン",
|
||||
"eventOutboundUp": "アップ",
|
||||
"eventXrayCrash": "クラッシュ",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "アップ",
|
||||
"eventCPUHigh": "CPU高負荷(%)",
|
||||
"requestFailed": "リクエストに失敗しました",
|
||||
"smtpEncryption": "暗号化",
|
||||
"smtpEncryptionDesc": "SMTP接続の暗号化方式",
|
||||
"smtpEncryptionNone": "なし(平文)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS(暗黙的)",
|
||||
"smtpStageConnect": "接続",
|
||||
"smtpStageAuth": "認証",
|
||||
"smtpStageSend": "送信",
|
||||
"smtpTestSuccess": "テストメールを正常に送信しました",
|
||||
"smtpHostNotConfigured": "SMTPホストが設定されていません",
|
||||
"smtpNoRecipients": "受信者が設定されていません",
|
||||
"smtpFromNotConfigured": "SMTP送信者アドレスが設定されていません",
|
||||
"eventLoginAttempt": "ログイン試行",
|
||||
"telegramTokenConfigured": "設定済み。現在のトークンを維持する場合は空欄のままにしてください。",
|
||||
"telegramTokenPlaceholder": "設定済み - 置き換えるには新しいトークンを入力してください",
|
||||
"smtpPasswordConfigured": "設定済み。現在のパスワードを維持する場合は空欄のままにしてください。",
|
||||
"smtpPasswordPlaceholder": "設定済み - 置き換えるには新しいパスワードを入力してください",
|
||||
"smtpNotInitialized": "SMTPが初期化されていません",
|
||||
"tgBotNotEnabled": "Telegramボットが有効になっていません",
|
||||
"tgTestFailed": "Telegramのテストに失敗しました",
|
||||
"tgTestSuccess": "Telegramにテストメッセージを送信しました",
|
||||
"tgBotNotRunning": "Telegramボットが実行されていません",
|
||||
"smtpErrorAuth": "認証に失敗しました — ユーザー名とパスワードを確認してください",
|
||||
"smtpErrorStarttls": "サーバーはSTARTTLSを要求しています — 暗号化方式を変更してください",
|
||||
"smtpErrorTls": "サーバーはTLSを要求しています — 暗号化方式を変更してください",
|
||||
"smtpErrorRefused": "接続が拒否されました — ホストとポートを確認してください",
|
||||
"smtpErrorTimeout": "接続がタイムアウトしました — ホストに到達できません",
|
||||
"smtpErrorRelay": "サーバーはこのアドレスからの送信を拒否しています",
|
||||
"smtpErrorEof": "サーバーによって接続が閉じられました",
|
||||
"smtpErrorUnknown": "SMTPエラー: {{ .Error }}",
|
||||
"eventMemoryHigh": "メモリ使用率が高い (%)",
|
||||
"remarkTemplate": "備考テンプレート",
|
||||
"remarkTemplateDesc": "設定すると、すべてのサブスクリプションリンクの備考モデルを置き換えます — 変数トークンを使って独自の形式を記述してください(ボタンで挿入できます)。空欄にすると上記のモデルが使用されます。",
|
||||
@@ -1483,8 +1311,6 @@
|
||||
"directdomains": "直接ドメイン",
|
||||
"ipv4Routing": "IPv4 ルーティング",
|
||||
"ipv4RoutingDesc": "このオプションはIPv4のみを介してターゲットドメインへルーティングします",
|
||||
"Template": "高度なXray設定テンプレート",
|
||||
"TemplateDesc": "最終的なXray設定ファイルはこのテンプレートに基づいて生成されます",
|
||||
"FreedomStrategy": "Freedom プロトコル戦略",
|
||||
"FreedomStrategyDesc": "Freedomプロトコル内のネットワークの出力戦略を設定する",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "負荷分散",
|
||||
"balancerTagRequired": "タグは必須です",
|
||||
"balancerSelectorRequired": "アウトバウンドを少なくとも1つ選んでください",
|
||||
"balancerLive": "現在のターゲット",
|
||||
"balancerOverride": "ターゲット強制",
|
||||
"balancerOverridePh": "自動(ストラテジー)",
|
||||
"balancerLiveRefresh": "ロードバランサーのライブ状態を更新",
|
||||
"balancerNotRunning": "このバランサーは実行中の Xray でアクティブではありません — 変更を保存するか、先に Xray を起動してください",
|
||||
"routeTester": "ルートテスト",
|
||||
"routeTesterDesc": "実行中の Xray にどのアウトバウンドが接続を処理するか問い合わせます。実際のトラフィックは送信されません — 判断はライブルーティングエンジンから直接取得されます。",
|
||||
"routeTesterDest": "ドメインまたは IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "バランサー経由",
|
||||
"routeTesterDefaultOutbound": "ルーティングルールに一致しませんでした — トラフィックはデフォルト(最初の)アウトバウンドに送られます。",
|
||||
"Routings": "ルーティングルール",
|
||||
"completeTemplate": "すべて",
|
||||
"logLevel": "ログレベル",
|
||||
"logLevelDesc": "エラーログのレベルを指定し、記録する情報を示します",
|
||||
"accessLog": "アクセスログ",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "ソース",
|
||||
"dest": "宛先アドレス",
|
||||
"inbound": "インバウンド",
|
||||
"balancer": "負荷分散",
|
||||
"useComma": "カンマ区切りの項目"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "このサーバーは既にアウトバウンド一覧にあります。鍵を更新するには {reset} を使ってください。",
|
||||
"provisionFailed": "PIA アウトバウンドを作成できませんでした。もう一度お試しください。"
|
||||
},
|
||||
"tabBalancerSettings": "バランサー設定",
|
||||
"tabObservatory": "オブザーバトリ",
|
||||
"observatory": {
|
||||
"autoManaged": "オブザーバはバランサーから自動的に管理されます。プローブの方法は下で調整できます。監視対象のアウトバウンドはバランサーのセレクターに従います。",
|
||||
"emptyHint": "有効な接続オブザーバはありません。Least Ping または Least Load のバランサー、あるいは fallback 付きの Random / Round-robin バランサーを作成すると自動的に追加され、オブザーバを使うバランサーがターゲットを選ぶ前にアウトバウンドの健全性を確認できるようになります。",
|
||||
"mixedLegacy": "この設定には Observatory と Burst Observatory の両方が含まれています。Xray は単一のグローバルオブザーバを使用するため、この古い混在状態はサポートされません。バランサーを保存すると 1 つのオブザーバに正規化されます。",
|
||||
"subjectSelector": "監視対象のアウトバウンド",
|
||||
"subjectSelectorDesc": "このオブザーバがプローブするアウトバウンドのタグ。バランサーから自動的に管理されます。",
|
||||
"probeURL": "プローブ URL",
|
||||
"probeURLDesc": "各アウトバウンドを測定するために取得する URL。HTTP 204 を返す必要があります。",
|
||||
"probeInterval": "プローブ間隔",
|
||||
"probeIntervalDesc": "各アウトバウンドをプローブする頻度。例: 30s、1m、2h45m。",
|
||||
"enableConcurrency": "並行プローブ",
|
||||
"enableConcurrencyDesc": "監視対象のアウトバウンドを1つずつではなく一度にプローブします。高速ですが、ネットワーク上で目立ちます。",
|
||||
"destination": "プローブ先",
|
||||
"destinationDesc": "各アウトバウンドを測定するために取得する URL。HTTP 204 を返す必要があります。",
|
||||
"connectivity": "接続チェック",
|
||||
"connectivityDesc": "任意のローカルネットワーク確認 URL。プローブ先が失敗した場合にのみ試行されます。空欄でスキップ。",
|
||||
"interval": "プローブ間隔",
|
||||
"intervalDesc": "アウトバウンドごとのプローブ間の平均時間。例: 1m。最小 10s。",
|
||||
"timeout": "プローブ タイムアウト",
|
||||
"timeoutDesc": "プローブを失敗とみなすまでの待機時間。例: 5s。",
|
||||
"sampling": "サンプリング数",
|
||||
"samplingDesc": "各アウトバウンドを評価するために保持する直近のプローブ結果の数。",
|
||||
"httpMethod": "HTTP メソッド",
|
||||
"httpMethodDesc": "プローブに使用する HTTP メソッド。",
|
||||
"deleteAlsoObservatory": "これは Observatory を使用する最後のバランサーのため、こちらも削除されます。",
|
||||
"deleteAlsoBurst": "これは Burst Observatory を使用する最後のバランサーのため、こちらも削除されます。"
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "戦略",
|
||||
"tag": "タグ",
|
||||
"tagDuplicate": "このタグは他のバランサーで使用されています",
|
||||
"tagPlaceholder": "一意のバランサータグ",
|
||||
"selector": "セレクター",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "循環: {path} → ({start} に戻る)",
|
||||
"expected": "期待値",
|
||||
"expectedPlaceholder": "最適ノード数",
|
||||
"maxRtt": "最大 RTT",
|
||||
"tolerance": "許容範囲",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "タグパターン",
|
||||
"costValue": "重み",
|
||||
"costRegexp": "正規表現で一致",
|
||||
"balancerDeleteInUse": "このバランサーを削除できません — 以下のバランサーのフォールバックとして使用されています:{names}",
|
||||
"balancerFallbackCycle": "このバランサーをフォールバックに設定できません — 循環依存が発生します。",
|
||||
"balancerFallbackInfo": "トラフィックは以下のルートで転送されます:バランサー → Loopback → サーバー → ターゲットバランサー → アウトバウンド。これによりサーバーを経由する追加ホップが発生し、多少の遅延が生じる場合があります。",
|
||||
"fallbackBalancerHint": "フォールバックとして別のバランサーを選択してください",
|
||||
"reservedPrefix": "プレフィックス _bl_ はバランサーの内部ループバックオブジェクト用に予約されています"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "使用量",
|
||||
"addOutbound": "アウトバウンドを追加"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "DNSを有効にする",
|
||||
"enableDesc": "組み込みDNSサーバーを有効にする",
|
||||
"tag": "DNSインバウンドタグ",
|
||||
"tagDesc": "このタグはルーティングルールでインバウンドタグとして使用できます",
|
||||
"clientIp": "クライアントIP",
|
||||
"clientIpDesc": "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます",
|
||||
"disableCache": "キャッシュを無効にする",
|
||||
"disableCacheDesc": "DNSキャッシュを無効にします",
|
||||
"disableFallback": "フォールバックを無効にする",
|
||||
"disableFallbackDesc": "フォールバックDNSクエリを無効にします",
|
||||
"disableFallbackIfMatch": "一致した場合にフォールバックを無効にする",
|
||||
"disableFallbackIfMatchDesc": "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします",
|
||||
"enableParallelQuery": "並列クエリを有効にする",
|
||||
"enableParallelQueryDesc": "複数のサーバーへの並列DNSクエリを有効にして、より高速な解決を実現",
|
||||
"strategy": "クエリ戦略",
|
||||
"strategyDesc": "ドメイン名解決の全体的な戦略",
|
||||
"add": "サーバー追加",
|
||||
"edit": "サーバー編集",
|
||||
"domains": "ドメイン",
|
||||
"expectIPs": "期待されるIP",
|
||||
"unexpectIPs": "予期しないIP",
|
||||
"useSystemHosts": "システムのHostsを使用",
|
||||
"useSystemHostsDesc": "インストール済みシステムのhostsファイルを使用する",
|
||||
"serveStale": "期限切れキャッシュを使用",
|
||||
"serveStaleDesc": "バックグラウンドで更新中に期限切れキャッシュ結果を返す",
|
||||
"serveExpiredTTL": "期限切れTTL",
|
||||
"serveExpiredTTLDesc": "期限切れキャッシュエントリの有効期間(秒)。0 = 無期限",
|
||||
"timeoutMs": "タイムアウト (ms)",
|
||||
"skipFallback": "フォールバックをスキップ",
|
||||
"finalQuery": "最終クエリ",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Host を追加",
|
||||
"hostsEmpty": "Host が定義されていません",
|
||||
"hostsDomain": "ドメイン (例: domain:example.com)",
|
||||
"hostsValues": "IP またはドメイン — 入力して Enter",
|
||||
"usePreset": "テンプレートを使用",
|
||||
"dnsPresetTitle": "DNSテンプレート",
|
||||
"dnsPresetFamily": "ファミリー",
|
||||
"clearAll": "すべて削除",
|
||||
"clearAllTitle": "すべての DNS サーバを削除しますか?",
|
||||
"clearAllConfirm": "リストからすべての DNS サーバが削除されます。この操作は元に戻せません。",
|
||||
"dnsLeakWarning": "DNS は localhost、暗号化なしの UDP/TCP、ローカルモードの DoH/DoQ、フォールバック問い合わせ、EDNS client IP から漏れる可能性があります。プライバシー重視ではルーティングされた DoH、hosts 固定、フォールバック無効化を使ってください。"
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "フェイクDNS追加",
|
||||
"ipPool": "IPプールサブネット",
|
||||
"poolSize": "プールサイズ"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "デフォルトアウトバウンド",
|
||||
"defaultOutboundDesc": "ルーティング規則に一致しないトラフィックはこのアウトバウンドを使います(一覧の先頭)。"
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ キーボードを閉じました!",
|
||||
"noResult": "❗ 結果がありません!",
|
||||
"noQuery": "❌ クエリが見つかりません!コマンドを再利用してください!",
|
||||
"wentWrong": "❌ 何かがうまくいかなかった!",
|
||||
"noIpRecord": "❗ IPレコードがありません!",
|
||||
"noInbounds": "❗ インバウンドが見つかりません!",
|
||||
"unlimited": "♾ 無制限(リセット)",
|
||||
"add": "追加",
|
||||
"month": "月",
|
||||
"months": "ヶ月",
|
||||
"days": "日間",
|
||||
"hours": "時間",
|
||||
"minutes": "分",
|
||||
"unknown": "不明",
|
||||
"inbounds": "インバウンド",
|
||||
"clients": "クライアント",
|
||||
"offline": "🔴 オフライン",
|
||||
"online": "🟢 オンライン",
|
||||
"commands": {
|
||||
"unknown": "❗ 不明なコマンド",
|
||||
"pleaseChoose": "👇 選択してください:\r\n",
|
||||
"help": "🤖 このボットをご利用いただきありがとうございます!サーバーから特定のデータを提供し、必要な変更を行うことができます。\r\n\r\n",
|
||||
"start": "👋 こんにちは、<i>{{ .Firstname }}</i>。\r\n",
|
||||
"welcome": "🤖 <b>{{ .Hostname }}</b> 管理ボットへようこそ。\r\n",
|
||||
"status": "✅ ボットは正常に動作しています!",
|
||||
"usage": "❗ 検索するテキストを入力してください!",
|
||||
"getID": "🆔 あなたのIDは:<code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Xray Coreを再起動するには:\r\n<code>/restart</code>\r\n\r\nクライアントの電子メールを検索するには:\r\n<code>/usage [電子メール]</code>\r\n\r\nインバウンド(クライアントの統計情報を含む)を検索するには:\r\n<code>/inbound [備考]</code>\r\n\r\nTelegramチャットID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "統計情報を検索するには、次のコマンドを使用してください:\r\n<code>/usage [電子メール]</code>\r\n\r\nTelegramチャットID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ 操作成功!",
|
||||
"restartFailed": "❗ 操作エラー。\r\n\r\n<code>エラー: {{ .Error }}</code>",
|
||||
"xrayNotRunning": "❗ Xray Core は動作していません。",
|
||||
"startDesc": "メインメニューを表示",
|
||||
"helpDesc": "ボットのヘルプ",
|
||||
"statusDesc": "ボットの状態を確認",
|
||||
"idDesc": "Telegram IDを表示",
|
||||
"usageDesc": "クライアント使用量を表示: /usage メール",
|
||||
"inboundDesc": "インバウンド検索: /inbound 備考(管理者)",
|
||||
"restartDesc": "Xray コアを再起動(管理者)",
|
||||
"clearallDesc": "全クライアントのトラフィックをリセット(管理者)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました",
|
||||
"selectUserFailed": "❌ ユーザーの選択に失敗しました!",
|
||||
"userSaved": "✅ Telegramユーザーが保存されました。",
|
||||
"loginSuccess": "✅ パネルに正常にログインしました。\r\n",
|
||||
"loginFailed": "❗️ パネルのログインに失敗しました。\r\n",
|
||||
"report": "🕰 定期報告:{{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ 日時:{{ .DateTime }}\r\n",
|
||||
"hostname": "💻 ホスト: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 X-UI バージョン:{{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray バージョン: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ ステータス: {{ .State }}\r\n",
|
||||
"username": "👤 ユーザー名:{{ .Username }}\r\n",
|
||||
"reason": "❗️ 理由:{{ .Reason }}\r\n",
|
||||
"time": "⏰ 時間:{{ .Time }}\r\n",
|
||||
"inbound": "📍 インバウンド: {{ .Remark }}\r\n",
|
||||
"port": "🔌 ポート: {{ .Port }}\r\n",
|
||||
"expire": "📅 有効期限:{{ .Time }}\r\n",
|
||||
"expireIn": "📅 残り時間:{{ .Time }}\r\n",
|
||||
"active": "💡 有効:{{ .Enable }}\r\n",
|
||||
"enabled": "🚨 有効化済み:{{ .Enable }}\r\n",
|
||||
"online": "🌐 接続ステータス:{{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 最終オンライン: {{ .Time }}\r\n",
|
||||
"email": "📧 メール: {{ .Email }}\r\n",
|
||||
"upload": "🔼 アップロード: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 ダウンロード: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 合計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Telegramユーザー:{{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 消耗済みの {{ .Type }} 数量:\r\n",
|
||||
"onlinesCount": "🌐 オンラインクライアント:{{ .Count }}\r\n",
|
||||
"disabled": "🛑 無効化:{{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 間もなく消耗:{{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 バックアップ時間:{{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ はい",
|
||||
"no": "❌ いいえ",
|
||||
"received_email": "📧📥 メールが更新されました。",
|
||||
"received_comment": "💬📥 コメントが更新されました。",
|
||||
"email_prompt": "📧 デフォルトメール: {{ .ClientEmail }}\n\nメールを入力してください。",
|
||||
"comment_prompt": "💬 デフォルトコメント: {{ .ClientComment }}\n\nコメントを入力してください。",
|
||||
"cancel": "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄",
|
||||
"error_add_client": "⚠️ エラー:\n\n {{ .error }}",
|
||||
"using_default_value": "わかりました、デフォルト値を使用します。 😊",
|
||||
"incorrect_input": "入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫",
|
||||
"AreYouSure": "本当にいいですか?🤔",
|
||||
"SuccessResetTraffic": "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功",
|
||||
"FailedResetTraffic": "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ エラー: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 すべてのクライアントのトラフィックリセットが完了しました。",
|
||||
"eventOutboundDown": "アウトバウンド {{ .Tag }} がダウンしています",
|
||||
"eventOutboundUp": "アウトバウンド {{ .Tag }} が復旧しました",
|
||||
"eventErrorDetail": "エラー: {{ .Error }}",
|
||||
"eventDelayDetail": "遅延: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xrayがクラッシュしました",
|
||||
"eventXrayCrashError": "エラー: {{ .Error }}",
|
||||
"eventNodeDown": "ノード {{ .Name }} がダウンしています",
|
||||
"eventNodeUp": "ノード {{ .Name }} が復旧しました",
|
||||
"eventLoginFallback": "{{ .Source }} からのログインに失敗しました",
|
||||
"memoryThreshold": "メモリ使用率 {{ .Percent }}% がしきい値 {{ .Threshold }}% を超えました"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ キーボードを閉じる",
|
||||
"cancel": "❌ キャンセル",
|
||||
"cancelReset": "❌ リセットをキャンセル",
|
||||
"cancelIpLimit": "❌ IP制限をキャンセル",
|
||||
"confirmResetTraffic": "✅ トラフィックをリセットしますか?",
|
||||
"confirmClearIps": "✅ IPをクリアしますか?",
|
||||
"confirmRemoveTGUser": "✅ Telegramユーザーを削除しますか?",
|
||||
"confirmToggle": "✅ ユーザーを有効/無効にしますか?",
|
||||
"dbBackup": "データベースバックアップを取得",
|
||||
"serverUsage": "サーバーの使用状況",
|
||||
"getInbounds": "インバウンド情報を取得",
|
||||
"depleteSoon": "間もなく消耗",
|
||||
"clientUsage": "使用状況を取得",
|
||||
"onlines": "オンラインクライアント",
|
||||
"commands": "コマンド",
|
||||
"refresh": "🔄 更新",
|
||||
"clearIPs": "❌ IPをクリア",
|
||||
"removeTGUser": "❌ Telegramユーザーを削除",
|
||||
"selectTGUser": "👤 Telegramユーザーを選択",
|
||||
"selectOneTGUser": "👤 1人のTelegramユーザーを選択:",
|
||||
"resetTraffic": "📈 トラフィックをリセット",
|
||||
"resetExpire": "📅 有効期限を変更",
|
||||
"ipLog": "🔢 IPログ",
|
||||
"ipLimit": "🔢 IP制限",
|
||||
"setTGUser": "👤 Telegramユーザーを設定",
|
||||
"toggle": "🔘 有効/無効",
|
||||
"custom": "🔢 カスタム",
|
||||
"confirmNumber": "✅ 確認: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ 追加を確認:{{ .Num }}",
|
||||
"limitTraffic": "🚧 トラフィック制限",
|
||||
"getBanLogs": "禁止ログ",
|
||||
"allClients": "すべてのクライアント",
|
||||
"addClient": "クライアントを追加",
|
||||
"submitDisable": "無効として送信 ☑️",
|
||||
"submitEnable": "有効として送信 ✅",
|
||||
"use_default": "🏷️ デフォルトを使用",
|
||||
"change_email": "⚙️📧 メール",
|
||||
"change_comment": "⚙️💬 コメント",
|
||||
"ResetAllTraffics": "すべてのトラフィックをリセット",
|
||||
"SortedTrafficUsageReport": "ソートされたトラフィック使用レポート"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ 成功!",
|
||||
"errorOperation": "❗ 操作エラー。",
|
||||
"getInboundsFailed": "❌ インバウンド情報の取得に失敗しました。",
|
||||
"getClientsFailed": "❌ クライアントの取得に失敗しました。",
|
||||
"canceled": "❌ {{ .Email }}:操作がキャンセルされました。",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}:クライアントが正常に更新されました。",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}:IPが正常に更新されました。",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}:クライアントのTelegramユーザーが正常に更新されました。",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}:トラフィックが正常にリセットされました。",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}:トラフィック制限が正常に保存されました。",
|
||||
"expireResetSuccess": "✅ {{ .Email }}:有効期限の日数が正常にリセットされました。",
|
||||
"resetIpSuccess": "✅ {{ .Email }}:IP制限数が正常に保存されました:{{ .Count }}。",
|
||||
"clearIpSuccess": "✅ {{ .Email }}:IPが正常にクリアされました。",
|
||||
"getIpLog": "✅ {{ .Email }}:IPログの取得。",
|
||||
"getUserInfo": "✅ {{ .Email }}:Telegramユーザー情報の取得。",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}:Telegramユーザーが正常に削除されました。",
|
||||
"enableSuccess": "✅ {{ .Email }}:正常に有効化されました。",
|
||||
"disableSuccess": "✅ {{ .Email }}:正常に無効化されました。",
|
||||
"askToAddUserId": "設定が見つかりませんでした!\r\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\r\n\r\nあなたのユーザーChatID:<code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "インバウンド {{ .Inbound }} のクライアントを選択",
|
||||
"chooseInbound": "インバウンドを選択"
|
||||
}
|
||||
"noIpRecord": "❗ IPレコードがありません!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "ステータス",
|
||||
"labelOutbound": "アウトバウンド",
|
||||
"labelNode": "ノード",
|
||||
"labelError": "エラー",
|
||||
"labelDelay": "遅延",
|
||||
"labelUsername": "ユーザー名",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "理由",
|
||||
"labelSource": "送信元",
|
||||
"statusCrashed": "クラッシュ",
|
||||
"statusHigh": "高負荷",
|
||||
"statusSuccess": "成功",
|
||||
"statusFailed": "失敗",
|
||||
"statusDown": "ダウン",
|
||||
"statusUp": "アップ"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Com Sucesso",
|
||||
"lastOnline": "Última vez online",
|
||||
"lastSubFetch": "Última busca da assinatura",
|
||||
"getVersion": "Obter Versão",
|
||||
"install": "Instalar",
|
||||
"clients": "Clientes",
|
||||
"usage": "Uso",
|
||||
"twoFactorCode": "Código",
|
||||
"remained": "Restante",
|
||||
"security": "Segurança",
|
||||
"emptyDnsDesc": "Nenhum servidor DNS adicionado.",
|
||||
"emptyFakeDnsDesc": "Nenhum servidor Fake DNS adicionado.",
|
||||
"emptyBalancersDesc": "Nenhum balanceador adicionado.",
|
||||
"somethingWentWrong": "Algo deu errado",
|
||||
"subscription": {
|
||||
"title": "Informações da assinatura",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Enviado",
|
||||
"expiry": "Validade",
|
||||
"totalQuota": "Cota total",
|
||||
"individualLinks": "Links individuais",
|
||||
"active": "Ativo",
|
||||
"inactive": "Inativo",
|
||||
"unlimited": "Ilimitado",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Parar",
|
||||
"restartXray": "Reiniciar",
|
||||
"xraySwitch": "Versão",
|
||||
"xrayUpdates": "Atualizações do Xray",
|
||||
"xraySwitchClickDesk": "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais.",
|
||||
"updatePanel": "Atualizar painel",
|
||||
"panelUpdateDesc": "Isso atualizará o 3X-UI para a versão mais recente e reiniciará o serviço do painel.",
|
||||
"currentPanelVersion": "Versão atual do painel",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Download",
|
||||
"sent": "Enviado",
|
||||
"received": "Recebido",
|
||||
"xraySwitchVersionDialog": "Você realmente deseja alterar a versão do Xray?",
|
||||
"xraySwitchVersionDialogDesc": "Isso mudará a versão do Xray para #version#.",
|
||||
"xraySwitchVersionPopover": "Xray atualizado com sucesso",
|
||||
"panelUpdateDialog": "Deseja realmente atualizar o painel?",
|
||||
"panelUpdateDialogDesc": "Isso atualizará o 3X-UI para #version# e reiniciará o serviço do painel.",
|
||||
"panelUpdateStartedPopover": "Atualização do painel iniciada",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "A atualização não foi concluída com sucesso. Verifique os logs do servidor ou execute 'x-ui update' na linha de comando.",
|
||||
"panelUpdateUnknownTitle": "Não foi possível confirmar se a atualização terminou",
|
||||
"panelUpdateUnknownDesc": "O painel não retornou um resultado a tempo. Recarregue a página para verificar a versão atual, ou verifique os logs do servidor.",
|
||||
"geofileUpdateDialog": "Você realmente deseja atualizar o geofile?",
|
||||
"geofileUpdateDialogDesc": "Isso atualizará o arquivo #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Isso atualizará todos os arquivos.",
|
||||
"geofilesUpdateAll": "Atualizar tudo",
|
||||
"geofileUpdatePopover": "Geofile atualizado com sucesso",
|
||||
"geodataTitle": "Atualização automática de Geodata",
|
||||
"geodataHint": "O Xray baixa esses arquivos conforme o agendamento e os recarrega sem reiniciar. As URLs devem ser HTTPS. Cada arquivo já deve existir na pasta bin para que o Xray possa atualizá-lo.",
|
||||
"geodataCron": "Agendamento (cron)",
|
||||
"geodataOutbound": "Baixar através de outbound (opcional)",
|
||||
"geodataFile": "Nome do arquivo",
|
||||
"geodataAddFile": "Adicionar arquivo",
|
||||
"geodataSaveRestart": "Salvar e reiniciar o Xray",
|
||||
"geodataConfirmTitle": "Salvar configurações de geodata?",
|
||||
"geodataConfirmContent": "O modelo de configuração do Xray será atualizado e o Xray será reiniciado.",
|
||||
"geodataInvalidUrl": "Cada arquivo precisa de uma URL HTTPS.",
|
||||
"geodataInvalidFile": "O nome do arquivo deve ser simples, ex.: geosite_custom.dat (sem caminhos).",
|
||||
"geodataInvalidCron": "O cron deve ter 5 campos, ex.: 0 4 * * *",
|
||||
"geodataEmpty": "Nenhum arquivo configurado. Nas regras de roteamento, referencie como ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Instalação em andamento, por favor não atualize a página",
|
||||
"logs": "Logs",
|
||||
"accessLogs": "Logs de acesso",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "O caminho base padrão \"/\" é bem conhecido — altere para um caminho aleatório.",
|
||||
"warnDefaultSubPath": "O caminho de assinatura padrão \"/sub/\" é bem conhecido — altere-o.",
|
||||
"warnDefaultJsonPath": "O caminho de assinatura JSON padrão \"/json/\" é bem conhecido — altere-o.",
|
||||
"TGBotSettings": "Bot do Telegram",
|
||||
"panelListeningIP": "IP de Escuta",
|
||||
"panelListeningIPDesc": "O endereço IP para o painel web. (deixe em branco para escutar em todos os IPs)",
|
||||
"panelListeningDomain": "Domínio de Escuta",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Senha Atual",
|
||||
"newUsername": "Novo Nome de Usuário",
|
||||
"newPassword": "Nova Senha",
|
||||
"telegramBotEnable": "Ativar Bot do Telegram",
|
||||
"telegramBotEnableDesc": "Ativa o bot do Telegram.",
|
||||
"telegramToken": "Token do Telegram",
|
||||
"telegramTokenDesc": "O token do bot do Telegram obtido de '{'@'}BotFather'.",
|
||||
"telegramProxy": "Proxy SOCKS",
|
||||
"telegramProxyDesc": "Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)",
|
||||
"telegramAPIServer": "Servidor API do Telegram",
|
||||
"telegramAPIServerDesc": "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão.",
|
||||
"telegramChatId": "ID de Chat do Administrador",
|
||||
"telegramChatIdDesc": "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui {'@'}userinfobot) ou (use o comando '/id' no bot)",
|
||||
"telegramNotifyTime": "Hora da Notificação",
|
||||
"telegramNotifyTimeDesc": "Com que frequência o bot do Telegram envia relatórios periódicos. Escolha um intervalo predefinido ou selecione Personalizado para inserir uma expressão crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — repetir em um intervalo",
|
||||
"hourly": "@hourly — a cada hora",
|
||||
"daily": "@daily — todos os dias às 00:00",
|
||||
"weekly": "@weekly — toda semana",
|
||||
"monthly": "@monthly — todo mês",
|
||||
"custom": "Personalizado (crontab)",
|
||||
"seconds": "Segundos",
|
||||
"minutes": "Minutos",
|
||||
"hours": "Horas",
|
||||
"interval": "Intervalo",
|
||||
"unit": "Unidade"
|
||||
},
|
||||
"tgNotifyBackup": "Backup do Banco de Dados",
|
||||
"tgNotifyBackupDesc": "Enviar arquivo de backup do banco de dados junto com o relatório.",
|
||||
"tgNotifyLogin": "Notificação de Login",
|
||||
"tgNotifyLoginDesc": "Receba notificações sobre o nome de usuário, endereço IP e horário sempre que alguém tentar fazer login no seu painel web.",
|
||||
"sessionMaxAge": "Duração da Sessão",
|
||||
"sessionMaxAgeDesc": "A duração pela qual você pode permanecer logado. (unidade: minuto)",
|
||||
"expireTimeDiff": "Notificação de Expiração",
|
||||
"expireTimeDiffDesc": "Receba notificações sobre a data de expiração ao atingir esse limite. (unidade: dia)",
|
||||
"trafficDiff": "Notificação de Limite de Tráfego",
|
||||
"trafficDiffDesc": "Receba notificações sobre o limite de tráfego ao atingir esse limite. (unidade: GB)",
|
||||
"tgNotifyCpu": "Notificação de Carga da CPU",
|
||||
"tgNotifyCpuDesc": "Receba notificações se a carga da CPU ultrapassar esse limite. (unidade: %)",
|
||||
"timeZone": "Fuso Horário",
|
||||
"timeZoneDesc": "As tarefas agendadas serão executadas com base nesse fuso horário.",
|
||||
"subSettings": "Assinatura",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "O conteúdo retornado pelo serviço de assinatura será codificado em Base64.",
|
||||
"subURI": "URI de Proxy Reverso",
|
||||
"subURIDesc": "A URL base completa (scheme://dominio[:porta]/caminho/) para o link de assinatura e o código QR, usada em vez de Domínio/Porta de Escuta. Configure isso sempre que a assinatura for acessada por um proxy reverso ou um domínio/porta diferente dos acima.",
|
||||
"externalTrafficInformEnable": "Informações de tráfego externo",
|
||||
"externalTrafficInformEnableDesc": "Informar API externa a cada atualização de tráfego.",
|
||||
"externalTrafficInformURI": "URI de informação de tráfego externo",
|
||||
"externalTrafficInformURIDesc": "As atualizações de tráfego são enviadas para este URI.",
|
||||
"restartXrayOnClientDisable": "Reiniciar Xray Após Desativação Automática",
|
||||
"restartXrayOnClientDisableDesc": "Quando um cliente for desativado automaticamente por expiração ou limite de tráfego, reinicie o Xray.",
|
||||
"fragment": "Fragmentação",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Configurações de Noises",
|
||||
"trustedProxyCidrs": "CIDRs de proxy confiável",
|
||||
"trustedProxyCidrsDesc": "IPs/CIDRs separados por vírgula que podem definir os cabeçalhos host, proto e IP do cliente encaminhados.",
|
||||
"ldap": {
|
||||
"enable": "Habilitar sincronização LDAP",
|
||||
"host": "Host LDAP",
|
||||
"port": "Porta LDAP",
|
||||
"useTls": "Usar TLS (LDAPS)",
|
||||
"skipTlsVerify": "Pular verificação de certificado TLS",
|
||||
"skipTlsVerifyDesc": "Inseguro — desativa a validação do certificado do servidor. Use apenas com CAs internos/não confiáveis.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Configurada; deixe em branco para manter a senha atual.",
|
||||
"passwordUnconfigured": "Não configurada.",
|
||||
"passwordPlaceholder": "Configurada — digite um novo valor para substituir",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Filtro de usuário",
|
||||
"userAttr": "Atributo de usuário (username/email)",
|
||||
"vlessField": "Atributo flag VLESS",
|
||||
"flagField": "Atributo flag genérico (opcional)",
|
||||
"flagFieldDesc": "Se definido, sobrescreve o flag VLESS — ex. shadowInactive.",
|
||||
"truthyValues": "Valores truthy",
|
||||
"truthyValuesDesc": "Separados por vírgula; padrão: true,1,yes,on",
|
||||
"invertFlag": "Inverter flag",
|
||||
"invertFlagDesc": "Habilite quando o atributo significar «desabilitado» (ex. shadowInactive).",
|
||||
"syncSchedule": "Agendamento da sincronização",
|
||||
"syncScheduleDesc": "String tipo cron, ex. @every 1m",
|
||||
"inboundTags": "Tags de entradas",
|
||||
"inboundTagsDesc": "Entradas nas quais a sincronização LDAP pode auto-criar ou auto-excluir clientes.",
|
||||
"noInbounds": "Nenhuma entrada encontrada. Crie uma em Entradas primeiro.",
|
||||
"autoCreate": "Criar clientes automaticamente",
|
||||
"autoDelete": "Excluir clientes automaticamente",
|
||||
"defaultTotalGb": "Total padrão (GB)",
|
||||
"defaultExpiryDays": "Expiração padrão (dias)",
|
||||
"defaultIpLimit": "Limite de IP padrão"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Injeta máscaras TCP/UDP do finalmask do Xray e parâmetros QUIC em cada perfil Xray JSON gerado. Requer um aplicativo compatível com assinaturas Xray JSON e um núcleo Xray recente.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Configurações de Mux",
|
||||
"direct": "Conexão Direta",
|
||||
"directDesc": "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico.",
|
||||
"notifications": "Notificações",
|
||||
"certs": "Certificados",
|
||||
"externalTraffic": "Tráfego Externo",
|
||||
"dateAndTime": "Data e Hora",
|
||||
"proxyAndServer": "Proxy e Servidor",
|
||||
"intervals": "Intervalos",
|
||||
"information": "Informação",
|
||||
"profile": "Perfil",
|
||||
"language": "Idioma",
|
||||
"telegramBotLanguage": "Idioma do Bot do Telegram",
|
||||
"security": {
|
||||
"admin": "Credenciais de administrador",
|
||||
"twoFactor": "Autenticação de dois fatores",
|
||||
"twoFactorEnable": "Ativar 2FA",
|
||||
"twoFactorEnableDesc": "Adiciona uma camada extra de autenticação para mais segurança.",
|
||||
"twoFactorModalSetTitle": "Ativar autenticação de dois fatores",
|
||||
"twoFactorModalDeleteTitle": "Desativar autenticação de dois fatores",
|
||||
"twoFactorModalSteps": "Para configurar a autenticação de dois fatores, siga alguns passos:",
|
||||
"twoFactorModalFirstStep": "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo",
|
||||
"twoFactorModalSecondStep": "2. Digite o código do aplicativo",
|
||||
"twoFactorModalRemoveStep": "Digite o código do aplicativo para remover a autenticação de dois fatores.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Alterar credenciais",
|
||||
"twoFactorModalChangeCredentialsStep": "Insira o código do aplicativo para alterar as credenciais do administrador.",
|
||||
"twoFactorModalSetSuccess": "A autenticação de dois fatores foi estabelecida com sucesso",
|
||||
"twoFactorModalDeleteSuccess": "A autenticação de dois fatores foi excluída com sucesso",
|
||||
"twoFactorModalError": "Código incorreto",
|
||||
"show": "Mostrar",
|
||||
"hide": "Ocultar",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Erro ao obter tráfego de saída",
|
||||
"resetOutboundTrafficError": "Erro ao redefinir tráfego de saída"
|
||||
},
|
||||
"smtpSettings": "Configurações SMTP",
|
||||
"smtpEnable": "Ativar notificações por e-mail",
|
||||
"smtpEnableDesc": "Ativar notificações por e-mail via SMTP",
|
||||
"smtpHost": "Servidor SMTP",
|
||||
"smtpHostDesc": "Nome do host do servidor SMTP (ex.: smtp.gmail.com)",
|
||||
"smtpPort": "Porta SMTP",
|
||||
"smtpPortDesc": "Porta do servidor SMTP (padrão: 587)",
|
||||
"smtpUsername": "Usuário SMTP",
|
||||
"smtpUsernameDesc": "Nome de usuário para autenticação SMTP",
|
||||
"smtpFrom": "Endereço do remetente (From)",
|
||||
"smtpFromDesc": "Endereço usado no cabeçalho From do e-mail. Deixe vazio para usar o nome de usuário.",
|
||||
"smtpFromName": "Nome do remetente (From)",
|
||||
"smtpFromNameDesc": "Nome de exibição opcional antes do endereço no cabeçalho From.",
|
||||
"smtpPassword": "Senha SMTP",
|
||||
"smtpPasswordDesc": "Senha para autenticação SMTP",
|
||||
"smtpTo": "Destinatários",
|
||||
"smtpToDesc": "Endereços de e-mail dos destinatários separados por vírgula",
|
||||
"emailSettings": "E-mail",
|
||||
"emailNotifications": "Notificações",
|
||||
"smtpEventBusNotify": "Notificações de eventos por e-mail",
|
||||
"smtpEventBusNotifyDesc": "Selecione quais eventos disparam notificações por e-mail",
|
||||
"tgEventBusNotify": "Notificações de eventos no Telegram",
|
||||
"tgEventBusNotifyDesc": "Selecione quais eventos disparam notificações no Telegram",
|
||||
"testSmtp": "Enviar e-mail de teste",
|
||||
"testTgBot": "Enviar mensagem de teste",
|
||||
"eventGroupOutbound": "Outbound",
|
||||
"eventGroupXray": "Núcleo Xray",
|
||||
"eventGroupSystem": "Sistema",
|
||||
"eventGroupSecurity": "Segurança",
|
||||
"eventGroupNode": "Nós",
|
||||
"eventOutboundDown": "Inativo",
|
||||
"eventOutboundUp": "Ativo",
|
||||
"eventXrayCrash": "Falha",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Ativo",
|
||||
"eventCPUHigh": "CPU alta (%)",
|
||||
"requestFailed": "Falha na requisição",
|
||||
"smtpEncryption": "Criptografia",
|
||||
"smtpEncryptionDesc": "Método de criptografia da conexão SMTP",
|
||||
"smtpEncryptionNone": "Nenhuma (texto puro)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (implícito)",
|
||||
"smtpStageConnect": "Conexão",
|
||||
"smtpStageAuth": "Autenticação",
|
||||
"smtpStageSend": "Envio",
|
||||
"smtpTestSuccess": "E-mail de teste enviado com sucesso",
|
||||
"smtpHostNotConfigured": "Servidor SMTP não configurado",
|
||||
"smtpNoRecipients": "Nenhum destinatário configurado",
|
||||
"smtpFromNotConfigured": "Endereço do remetente SMTP não configurado",
|
||||
"eventLoginAttempt": "Tentativa de login",
|
||||
"telegramTokenConfigured": "Configurado; deixe em branco para manter o token atual.",
|
||||
"telegramTokenPlaceholder": "Configurado - insira um novo token para substituir",
|
||||
"smtpPasswordConfigured": "Configurada; deixe em branco para manter a senha atual.",
|
||||
"smtpPasswordPlaceholder": "Configurada - insira uma nova senha para substituir",
|
||||
"smtpNotInitialized": "SMTP não inicializado",
|
||||
"tgBotNotEnabled": "O bot do Telegram não está ativado",
|
||||
"tgTestFailed": "Falha no teste do Telegram",
|
||||
"tgTestSuccess": "Mensagem de teste enviada ao Telegram",
|
||||
"tgBotNotRunning": "O bot do Telegram não está em execução",
|
||||
"smtpErrorAuth": "Falha na autenticação — verifique o nome de usuário e a senha",
|
||||
"smtpErrorStarttls": "O servidor requer STARTTLS — altere o tipo de criptografia",
|
||||
"smtpErrorTls": "O servidor requer TLS — altere o tipo de criptografia",
|
||||
"smtpErrorRefused": "Conexão recusada — verifique o host e a porta",
|
||||
"smtpErrorTimeout": "Tempo de conexão esgotado — host inacessível",
|
||||
"smtpErrorRelay": "O servidor rejeita o envio a partir deste endereço",
|
||||
"smtpErrorEof": "Conexão encerrada pelo servidor",
|
||||
"smtpErrorUnknown": "Erro de SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Uso de memória alto (%)",
|
||||
"remarkTemplate": "Modelo de Observação",
|
||||
"remarkTemplateDesc": "Quando definido, isto substitui o modelo de observação de cada link de assinatura — escreva seu próprio formato com os tokens de variáveis (use o botão para inseri-los). Deixe vazio para usar o modelo acima.",
|
||||
@@ -1483,8 +1311,6 @@
|
||||
"directdomains": "Domínios Diretos",
|
||||
"ipv4Routing": "Roteamento IPv4",
|
||||
"ipv4RoutingDesc": "Essas opções roteam o tráfego para um destino específico via IPv4.",
|
||||
"Template": "Modelo de Configuração Avançada do Xray",
|
||||
"TemplateDesc": "O arquivo final de configuração do Xray será gerado com base neste modelo.",
|
||||
"FreedomStrategy": "Estratégia do Protocolo Freedom",
|
||||
"FreedomStrategyDesc": "Definir a estratégia de saída para a rede no Protocolo Freedom.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "Balanceadores",
|
||||
"balancerTagRequired": "A tag é obrigatória",
|
||||
"balancerSelectorRequired": "Selecione pelo menos uma saída",
|
||||
"balancerLive": "Destino atual",
|
||||
"balancerOverride": "Forçar destino",
|
||||
"balancerOverridePh": "Automático (estratégia)",
|
||||
"balancerLiveRefresh": "Atualizar estado do balanceador",
|
||||
"balancerNotRunning": "Este balanceador não está ativo no Xray em execução — salve as alterações ou inicie o Xray primeiro",
|
||||
"routeTester": "Teste de rota",
|
||||
"routeTesterDesc": "Pergunte ao Xray em execução qual saída trataria uma conexão. Nenhum tráfego é enviado — a decisão vem diretamente do motor de roteamento ao vivo.",
|
||||
"routeTesterDest": "Domínio ou IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "via balanceador",
|
||||
"routeTesterDefaultOutbound": "Nenhuma regra de roteamento correspondeu — o tráfego vai para a saída padrão (primeira).",
|
||||
"Routings": "Regras de Roteamento",
|
||||
"completeTemplate": "Tudo",
|
||||
"logLevel": "Nível de Log",
|
||||
"logLevelDesc": "O nível de log para erros, indicando a informação que precisa ser registrada.",
|
||||
"accessLog": "Log de Acesso",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Fonte",
|
||||
"dest": "Destino",
|
||||
"inbound": "Entrada",
|
||||
"balancer": "Balanceador",
|
||||
"useComma": "Itens separados por vírgula"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Este servidor já está na lista de saídas. Use {reset} para renovar a chave.",
|
||||
"provisionFailed": "Não foi possível criar a saída PIA. Tente novamente."
|
||||
},
|
||||
"tabBalancerSettings": "Configurações do balanceador",
|
||||
"tabObservatory": "Observatório",
|
||||
"observatory": {
|
||||
"autoManaged": "Os observadores são gerenciados automaticamente a partir dos seus balanceadores. Ajuste abaixo como eles sondam; as saídas monitoradas seguem os seletores do balanceador.",
|
||||
"emptyHint": "Nenhum observador de conexão ativo. Um é adicionado automaticamente ao criar um balanceador Least Ping ou Least Load — ou um balanceador Random / Round-robin com fallback — para que balanceadores com observador possam verificar a saúde das saídas antes de escolher um destino.",
|
||||
"mixedLegacy": "Esta configuração contém Observatory e Burst Observatory ao mesmo tempo. O Xray usa um único observador global, então esse estado misto legado não é suportado; ao salvar os balanceadores ele será normalizado para um único observador.",
|
||||
"subjectSelector": "Saídas monitoradas",
|
||||
"subjectSelectorDesc": "Tags de saída que este observador sonda. Gerenciadas automaticamente a partir dos seus balanceadores.",
|
||||
"probeURL": "URL de sondagem",
|
||||
"probeURLDesc": "URL requisitada para medir cada saída. Deve retornar HTTP 204.",
|
||||
"probeInterval": "Intervalo de sondagem",
|
||||
"probeIntervalDesc": "Com que frequência sondar cada saída, ex.: 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Sondagem concorrente",
|
||||
"enableConcurrencyDesc": "Sonda todas as saídas monitoradas de uma vez, em vez de uma a uma. Mais rápido, mas mais visível na rede.",
|
||||
"destination": "Destino da sondagem",
|
||||
"destinationDesc": "URL requisitada para medir cada saída. Deve retornar HTTP 204.",
|
||||
"connectivity": "Verificação de conectividade",
|
||||
"connectivityDesc": "URL opcional de verificação da rede local, testada apenas após o destino falhar. Deixe vazio para ignorar.",
|
||||
"interval": "Intervalo de sondagem",
|
||||
"intervalDesc": "Tempo médio entre sondagens por saída, ex.: 1m. Mínimo 10s.",
|
||||
"timeout": "Tempo limite da sondagem",
|
||||
"timeoutDesc": "Quanto esperar por uma sondagem antes de considerá-la falha, ex.: 5s.",
|
||||
"sampling": "Número de amostras",
|
||||
"samplingDesc": "Número de resultados de sondagem recentes mantidos para pontuar cada saída.",
|
||||
"httpMethod": "Método HTTP",
|
||||
"httpMethodDesc": "Método HTTP usado nas sondagens.",
|
||||
"deleteAlsoObservatory": "Este é o último balanceador que usa o Observatório, então ele também será removido.",
|
||||
"deleteAlsoBurst": "Este é o último balanceador que usa o Observatório Burst, então ele também será removido."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Estratégia",
|
||||
"tag": "Tag",
|
||||
"tagDuplicate": "Tag já usada por outro balanceador",
|
||||
"tagPlaceholder": "tag única do balanceador",
|
||||
"selector": "Seletor",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Ciclo: {path} → (voltar para {start})",
|
||||
"expected": "Esperado",
|
||||
"expectedPlaceholder": "número ótimo de nós",
|
||||
"maxRtt": "Máx. RTT",
|
||||
"tolerance": "Tolerância",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Padrão de tag",
|
||||
"costValue": "Peso",
|
||||
"costRegexp": "Correspondência por expressão regular",
|
||||
"balancerDeleteInUse": "Não é possível excluir este balanceador — ele é usado como fallback para: {names}",
|
||||
"balancerFallbackCycle": "Não é possível definir este balanceador como fallback — isso criaria uma dependência circular.",
|
||||
"balancerFallbackInfo": "O tráfego será roteado através de: Balanceador → Loopback → Servidor → Balanceador de destino → Conexão de saída. Isso adiciona um salto extra pelo servidor, o que pode introduzir pequenos atrasos.",
|
||||
"fallbackBalancerHint": "Selecione outro balanceador como fallback",
|
||||
"reservedPrefix": "O prefixo _bl_ é reservado para objetos loopback internos do balanceador"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Uso",
|
||||
"addOutbound": "Adicionar saída"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Ativar DNS",
|
||||
"enableDesc": "Ativar o servidor DNS integrado",
|
||||
"tag": "Tag de Entrada DNS",
|
||||
"tagDesc": "Esta tag estará disponível como uma tag de Entrada nas regras de roteamento.",
|
||||
"clientIp": "IP do Cliente",
|
||||
"clientIpDesc": "Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS",
|
||||
"disableCache": "Desativar cache",
|
||||
"disableCacheDesc": "Desativa o cache de DNS",
|
||||
"disableFallback": "Desativar Fallback",
|
||||
"disableFallbackDesc": "Desativa consultas DNS de fallback",
|
||||
"disableFallbackIfMatch": "Desativar Fallback Se Corresponder",
|
||||
"disableFallbackIfMatchDesc": "Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida",
|
||||
"enableParallelQuery": "Habilitar Consulta Paralela",
|
||||
"enableParallelQueryDesc": "Habilitar consultas DNS paralelas para múltiplos servidores para resolução mais rápida",
|
||||
"strategy": "Estratégia de Consulta",
|
||||
"strategyDesc": "Estratégia geral para resolver nomes de domínio",
|
||||
"add": "Adicionar Servidor",
|
||||
"edit": "Editar Servidor",
|
||||
"domains": "Domínios",
|
||||
"expectIPs": "IPs Esperadas",
|
||||
"unexpectIPs": "IPs inesperados",
|
||||
"useSystemHosts": "Usar Hosts do sistema",
|
||||
"useSystemHostsDesc": "Usar o arquivo hosts de um sistema instalado",
|
||||
"serveStale": "Servir Expirados",
|
||||
"serveStaleDesc": "Retornar resultados expirados do cache enquanto atualiza em segundo plano",
|
||||
"serveExpiredTTL": "TTL de Expirados",
|
||||
"serveExpiredTTLDesc": "Validade (segundos) das entradas expiradas no cache; 0 = nunca expira",
|
||||
"timeoutMs": "Tempo limite (ms)",
|
||||
"skipFallback": "Ignorar Fallback",
|
||||
"finalQuery": "Consulta Final",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Adicionar Host",
|
||||
"hostsEmpty": "Nenhum Host definido",
|
||||
"hostsDomain": "Domínio (ex. domain:example.com)",
|
||||
"hostsValues": "IP ou domínio — digite e pressione Enter",
|
||||
"usePreset": "Usar modelo",
|
||||
"dnsPresetTitle": "Modelos DNS",
|
||||
"dnsPresetFamily": "Familiar",
|
||||
"clearAll": "Remover Todos",
|
||||
"clearAllTitle": "Remover todos os servidores DNS?",
|
||||
"clearAllConfirm": "Isso remove todos os servidores DNS da lista. Não pode ser desfeito.",
|
||||
"dnsLeakWarning": "DNS pode vazar por localhost, UDP/TCP sem criptografia, DoH/DoQ em modo local, consultas de fallback ou EDNS client IP. Use DoH roteado, fixe resolvedores em hosts e desative fallback quando privacidade importar."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Adicionar Fake DNS",
|
||||
"ipPool": "Sub-rede do Pool de IP",
|
||||
"poolSize": "Tamanho do Pool"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Saída padrão",
|
||||
"defaultOutboundDesc": "Tráfego sem regra de roteamento usa esta saída (a primeira da lista)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Teclado fechado!",
|
||||
"noResult": "❗ Nenhum resultado!",
|
||||
"noQuery": "❌ Consulta não encontrada! Por favor, use o comando novamente!",
|
||||
"wentWrong": "❌ Algo deu errado!",
|
||||
"noIpRecord": "❗ Nenhum registro de IP!",
|
||||
"noInbounds": "❗ Nenhum inbound encontrado!",
|
||||
"unlimited": "♾ Ilimitado (Reset)",
|
||||
"add": "Adicionar",
|
||||
"month": "Mês",
|
||||
"months": "Meses",
|
||||
"days": "Dias",
|
||||
"hours": "Horas",
|
||||
"minutes": "Minutos",
|
||||
"unknown": "Desconhecido",
|
||||
"inbounds": "Entradas",
|
||||
"clients": "Clientes",
|
||||
"offline": "🔴 Offline",
|
||||
"online": "🟢 Online",
|
||||
"commands": {
|
||||
"unknown": "❗ Comando desconhecido.",
|
||||
"pleaseChoose": "👇 Escolha:\r\n",
|
||||
"help": "🤖 Bem-vindo a este bot! Ele foi projetado para oferecer dados específicos do painel da web e permite que você faça as modificações necessárias.\r\n\r\n",
|
||||
"start": "👋 Olá <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Bem-vindo ao bot de gerenciamento do <b>{{ .Hostname }}</b>.\r\n",
|
||||
"status": "✅ Bot está OK!",
|
||||
"usage": "❗ Por favor, forneça um texto para pesquisar!",
|
||||
"getID": "🆔 Seu ID: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Para reiniciar o Xray Core:\r\n<code>/restart</code>\r\n\r\nPara pesquisar por um email de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "Para pesquisar por estatísticas, use o seguinte comando:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Operação bem-sucedida!",
|
||||
"restartFailed": "❗ Erro na operação.\r\n\r\n<code>Erro: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core não está em execução.",
|
||||
"startDesc": "Mostrar menu principal",
|
||||
"helpDesc": "Ajuda do bot",
|
||||
"statusDesc": "Verificar status do bot",
|
||||
"idDesc": "Mostrar seu ID do Telegram",
|
||||
"usageDesc": "Ver o uso do cliente: /usage email",
|
||||
"inboundDesc": "Buscar entradas: /inbound nome (admin)",
|
||||
"restartDesc": "Reiniciar o núcleo Xray (admin)",
|
||||
"clearallDesc": "Zerar o tráfego de todos os clientes (admin)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Erro na seleção do usuário!",
|
||||
"userSaved": "✅ Usuário do Telegram salvo.",
|
||||
"loginSuccess": "✅ Conectado ao painel com sucesso.\r\n",
|
||||
"loginFailed": "❗️Tentativa de login no painel falhou.\r\n",
|
||||
"report": "🕰 Relatórios agendados: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Data&Hora: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Host: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 Versão 3X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Versão Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Tempo de atividade: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Carga do sistema: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Tráfego: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Status: {{ .State }}\r\n",
|
||||
"username": "👤 Nome de usuário: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Motivo: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Hora: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Entrada: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Porta: {{ .Port }}\r\n",
|
||||
"expire": "📅 Data de expiração: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Expira em: {{ .Time }}\r\n",
|
||||
"active": "💡 Ativo: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Ativado: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Status da conexão: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Última vez online: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Upload: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Download: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Usuário do Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Contagem de {{ .Type }} esgotado:\r\n",
|
||||
"onlinesCount": "🌐 Clientes online: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Desativado: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Esgotar em breve: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Hora do backup: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Sim",
|
||||
"no": "❌ Não",
|
||||
"received_email": "📧📥 E-mail atualizado.",
|
||||
"received_comment": "💬📥 Comentário atualizado.",
|
||||
"email_prompt": "📧 E-mail Padrão: {{ .ClientEmail }}\n\nDigite seu e-mail.",
|
||||
"comment_prompt": "💬 Comentário Padrão: {{ .ClientComment }}\n\nDigite seu comentário.",
|
||||
"cancel": "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄",
|
||||
"error_add_client": "⚠️ Erro:\n\n {{ .error }}",
|
||||
"using_default_value": "Tudo bem, vou manter o valor padrão. 😊",
|
||||
"incorrect_input": "Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫",
|
||||
"AreYouSure": "Você tem certeza? 🤔",
|
||||
"SuccessResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso",
|
||||
"FailedResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠️ Erro: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Processo de redefinição de tráfego concluído para todos os clientes.",
|
||||
"eventOutboundDown": "O outbound {{ .Tag }} está INATIVO",
|
||||
"eventOutboundUp": "O outbound {{ .Tag }} está ATIVO",
|
||||
"eventErrorDetail": "Erro: {{ .Error }}",
|
||||
"eventDelayDetail": "Latência: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "O Xray FALHOU",
|
||||
"eventXrayCrashError": "Erro: {{ .Error }}",
|
||||
"eventNodeDown": "O nó {{ .Name }} está INATIVO",
|
||||
"eventNodeUp": "O nó {{ .Name }} está ATIVO",
|
||||
"eventLoginFallback": "Falha de login a partir de {{ .Source }}",
|
||||
"memoryThreshold": "Uso de memória {{ .Percent }}% excede o limite de {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Fechar teclado",
|
||||
"cancel": "❌ Cancelar",
|
||||
"cancelReset": "❌ Cancelar redefinição",
|
||||
"cancelIpLimit": "❌ Cancelar limite de IP",
|
||||
"confirmResetTraffic": "✅ Confirmar redefinição de tráfego?",
|
||||
"confirmClearIps": "✅ Confirmar limpar IPs?",
|
||||
"confirmRemoveTGUser": "✅ Confirmar remover usuário do Telegram?",
|
||||
"confirmToggle": "✅ Confirmar ativar/desativar usuário?",
|
||||
"dbBackup": "Obter backup do DB",
|
||||
"serverUsage": "Uso do servidor",
|
||||
"getInbounds": "Obter Inbounds",
|
||||
"depleteSoon": "Esgotar em breve",
|
||||
"clientUsage": "Obter uso",
|
||||
"onlines": "Clientes online",
|
||||
"commands": "Comandos",
|
||||
"refresh": "🔄 Atualizar",
|
||||
"clearIPs": "❌ Limpar IPs",
|
||||
"removeTGUser": "❌ Remover usuário do Telegram",
|
||||
"selectTGUser": "👤 Selecionar usuário do Telegram",
|
||||
"selectOneTGUser": "👤 Selecione um usuário do Telegram:",
|
||||
"resetTraffic": "📈 Redefinir tráfego",
|
||||
"resetExpire": "📅 Alterar data de expiração",
|
||||
"ipLog": "🔢 Log de IP",
|
||||
"ipLimit": "🔢 Limite de IP",
|
||||
"setTGUser": "👤 Definir usuário do Telegram",
|
||||
"toggle": "🔘 Ativar / Desativar",
|
||||
"custom": "🔢 Personalizado",
|
||||
"confirmNumber": "✅ Confirmar: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Confirmar adicionar: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Limite de tráfego",
|
||||
"getBanLogs": "Obter logs de banimento",
|
||||
"allClients": "Todos os clientes",
|
||||
"addClient": "Adicionar Cliente",
|
||||
"submitDisable": "Enviar como Desativado ☑️",
|
||||
"submitEnable": "Enviar como Ativado ✅",
|
||||
"use_default": "🏷️ Usar padrão",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Comentário",
|
||||
"ResetAllTraffics": "Redefinir Todo o Tráfego",
|
||||
"SortedTrafficUsageReport": "Relatório de Uso de Tráfego Ordenado"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Operação bem-sucedida!",
|
||||
"errorOperation": "❗ Erro na operação.",
|
||||
"getInboundsFailed": "❌ Falha ao obter inbounds.",
|
||||
"getClientsFailed": "❌ Falha ao obter clientes.",
|
||||
"canceled": "❌ {{ .Email }}: Operação cancelada.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Cliente atualizado com sucesso.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IPs atualizados com sucesso.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Usuário do Telegram do cliente atualizado com sucesso.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Tráfego redefinido com sucesso.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Limite de tráfego salvo com sucesso.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Dias de expiração redefinidos com sucesso.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: Limite de IP {{ .Count }} salvo com sucesso.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IPs limpos com sucesso.",
|
||||
"getIpLog": "✅ {{ .Email }}: Obter log de IP.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Obter informações do usuário do Telegram.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Usuário do Telegram removido com sucesso.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Ativado com sucesso.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Desativado com sucesso.",
|
||||
"askToAddUserId": "Sua configuração não foi encontrada!\r\nPeça ao seu administrador para usar seu Telegram ChatID em suas configurações.\r\n\r\nSeu ChatID: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Escolha um cliente para Inbound {{ .Inbound }}",
|
||||
"chooseInbound": "Escolha um Inbound"
|
||||
}
|
||||
"noIpRecord": "❗ Nenhum registro de IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Status",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelNode": "Nó",
|
||||
"labelError": "Erro",
|
||||
"labelDelay": "Latência",
|
||||
"labelUsername": "Nome de usuário",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Motivo",
|
||||
"labelSource": "Origem",
|
||||
"statusCrashed": "FALHOU",
|
||||
"statusHigh": "ALTA",
|
||||
"statusSuccess": "SUCESSO",
|
||||
"statusFailed": "FALHOU",
|
||||
"statusDown": "INATIVO",
|
||||
"statusUp": "ATIVO"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Успешно",
|
||||
"lastOnline": "Был(а) в сети",
|
||||
"lastSubFetch": "Последнее получение подписки",
|
||||
"getVersion": "Узнать версию",
|
||||
"install": "Установка",
|
||||
"clients": "Клиенты",
|
||||
"usage": "Использование",
|
||||
"twoFactorCode": "Код 2FA",
|
||||
"remained": "Остаток",
|
||||
"security": "Безопасность",
|
||||
"emptyDnsDesc": "Нет добавленных DNS-серверов.",
|
||||
"emptyFakeDnsDesc": "Нет добавленных Fake DNS-серверов.",
|
||||
"emptyBalancersDesc": "Нет добавленных балансировщиков.",
|
||||
"somethingWentWrong": "Что-то пошло не так",
|
||||
"subscription": {
|
||||
"title": "Информация о подписке",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Отправлено",
|
||||
"expiry": "Срок действия",
|
||||
"totalQuota": "Общий лимит",
|
||||
"individualLinks": "Индивидуальные ссылки",
|
||||
"active": "Активна",
|
||||
"inactive": "Неактивна",
|
||||
"unlimited": "Неограниченно",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Стоп",
|
||||
"restartXray": "Перезапуск",
|
||||
"xraySwitch": "Выбор версии",
|
||||
"xrayUpdates": "Обновления Xray",
|
||||
"xraySwitchClickDesk": "Важно: старые версии могут не поддерживать текущие настройки",
|
||||
"updatePanel": "Обновить панель",
|
||||
"panelUpdateDesc": "Это обновит 3X-UI до последнего релиза и перезапустит сервис панели.",
|
||||
"currentPanelVersion": "Текущая версия панели",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Скачать",
|
||||
"sent": "Отправлено",
|
||||
"received": "Получено",
|
||||
"xraySwitchVersionDialog": "Переключить версию Xray",
|
||||
"xraySwitchVersionDialogDesc": "Вы точно хотите сменить версию Xray?",
|
||||
"xraySwitchVersionPopover": "Xray успешно обновлён",
|
||||
"panelUpdateDialog": "Вы действительно хотите обновить панель?",
|
||||
"panelUpdateDialogDesc": "Это обновит 3X-UI до версии #version# и перезапустит сервис панели.",
|
||||
"panelUpdateStartedPopover": "Обновление панели началось",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "Обновление не завершилось успешно. Проверьте журналы сервера или выполните 'x-ui update' в командной строке.",
|
||||
"panelUpdateUnknownTitle": "Не удалось подтвердить завершение обновления",
|
||||
"panelUpdateUnknownDesc": "Панель не сообщила результат вовремя. Перезагрузите страницу, чтобы проверить текущую версию, или проверьте журналы сервера.",
|
||||
"geofileUpdateDialog": "Вы действительно хотите обновить геофайл?",
|
||||
"geofileUpdateDialogDesc": "Это обновит файл #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Это обновит все геофайлы.",
|
||||
"geofilesUpdateAll": "Обновить все",
|
||||
"geofileUpdatePopover": "Геофайлы успешно обновлены",
|
||||
"geodataTitle": "Автообновление Geodata",
|
||||
"geodataHint": "Xray скачивает эти файлы по расписанию и перезагружает их без перезапуска. URL должны быть HTTPS. Файл должен уже существовать в папке bin, прежде чем Xray сможет его обновлять.",
|
||||
"geodataCron": "Расписание (cron)",
|
||||
"geodataOutbound": "Скачивать через outbound (необязательно)",
|
||||
"geodataFile": "Имя файла",
|
||||
"geodataAddFile": "Добавить файл",
|
||||
"geodataSaveRestart": "Сохранить и перезапустить Xray",
|
||||
"geodataConfirmTitle": "Сохранить настройки geodata?",
|
||||
"geodataConfirmContent": "Шаблон конфигурации Xray будет обновлён, а Xray перезапущен.",
|
||||
"geodataInvalidUrl": "Для каждого файла нужен HTTPS URL.",
|
||||
"geodataInvalidFile": "Имя файла должно быть простым, например geosite_custom.dat (без путей).",
|
||||
"geodataInvalidCron": "Cron должен содержать 5 полей, напр. 0 4 * * *",
|
||||
"geodataEmpty": "Файлы не настроены. В правилах маршрутизации файлы указываются как ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Установка в процессе. Не обновляйте страницу",
|
||||
"logs": "Логи",
|
||||
"accessLogs": "Логи доступа",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "Базовый путь по умолчанию \"/\" широко известен — измените его на случайный.",
|
||||
"warnDefaultSubPath": "Путь подписки по умолчанию \"/sub/\" широко известен — измените его.",
|
||||
"warnDefaultJsonPath": "JSON-путь подписки по умолчанию \"/json/\" широко известен — измените его.",
|
||||
"TGBotSettings": "Telegram-бот",
|
||||
"panelListeningIP": "IP-адрес для управления панелью",
|
||||
"panelListeningIPDesc": "Оставьте пустым для подключения с любого IP",
|
||||
"panelListeningDomain": "Домен панели",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Текущий пароль",
|
||||
"newUsername": "Новый логин",
|
||||
"newPassword": "Новый пароль",
|
||||
"telegramBotEnable": "Включить Telegram бота",
|
||||
"telegramBotEnableDesc": "Доступ к функциям панели через Telegram-бота",
|
||||
"telegramToken": "Telegram-токен",
|
||||
"telegramTokenDesc": "Необходимо получить токен у менеджера ботов Telegram {'@'}botfather",
|
||||
"telegramProxy": "SOCKS-прокси",
|
||||
"telegramProxyDesc": "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству.",
|
||||
"telegramAPIServer": "Telegram API Server",
|
||||
"telegramAPIServerDesc": "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию.",
|
||||
"telegramChatId": "User ID администратора бота",
|
||||
"telegramChatIdDesc": "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте {'@'}userinfobot или команду '/id' в боте.",
|
||||
"telegramNotifyTime": "Частота уведомлений для администраторов от бота",
|
||||
"telegramNotifyTimeDesc": "Как часто бот Telegram отправляет периодические отчёты. Выберите готовый интервал или «Произвольный», чтобы ввести выражение crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — повторять с интервалом",
|
||||
"hourly": "@hourly — каждый час",
|
||||
"daily": "@daily — каждый день в 00:00",
|
||||
"weekly": "@weekly — каждую неделю",
|
||||
"monthly": "@monthly — каждый месяц",
|
||||
"custom": "Произвольный (crontab)",
|
||||
"seconds": "Секунды",
|
||||
"minutes": "Минуты",
|
||||
"hours": "Часы",
|
||||
"interval": "Интервал",
|
||||
"unit": "Единица"
|
||||
},
|
||||
"tgNotifyBackup": "Резервное копирование базы данных",
|
||||
"tgNotifyBackupDesc": "Отправлять уведомление с файлом резервной копии базы данных",
|
||||
"tgNotifyLogin": "Уведомление о входе",
|
||||
"tgNotifyLoginDesc": "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель.",
|
||||
"sessionMaxAge": "Продолжительность сессии",
|
||||
"sessionMaxAgeDesc": "Продолжительность сессии в системе (значение: минута)",
|
||||
"expireTimeDiff": "Задержка уведомления об истечении сессии",
|
||||
"expireTimeDiffDesc": "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)",
|
||||
"trafficDiff": "Порог трафика для уведомления",
|
||||
"trafficDiffDesc": "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)",
|
||||
"tgNotifyCpu": "Порог нагрузки на ЦП для уведомления",
|
||||
"tgNotifyCpuDesc": "Уведомление администраторов в Telegram, если нагрузка на ЦП превышает этот порог (значение: %)",
|
||||
"timeZone": "Часовой пояс",
|
||||
"timeZoneDesc": "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе",
|
||||
"subSettings": "Подписка",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Шифровать возвращенные конфиги в подписке",
|
||||
"subURI": "URI обратного прокси",
|
||||
"subURIDesc": "Полный базовый URL (schema://домен[:порт]/путь/) для ссылки подписки и QR-кода вместо «Домена прослушивания»/«Порта подписки». Заполните, если подписка доступна через reverse-proxy или на другом домене/порту.",
|
||||
"externalTrafficInformEnable": "Информация о внешнем трафике",
|
||||
"externalTrafficInformEnableDesc": "Уведомлять внешний API при каждом обновлении трафика.",
|
||||
"externalTrafficInformURI": "URI информации о внешнем трафике",
|
||||
"externalTrafficInformURIDesc": "Обновления трафика отправляются на этот URI",
|
||||
"restartXrayOnClientDisable": "Перезапускать Xray после автоотключения",
|
||||
"restartXrayOnClientDisableDesc": "Когда клиент автоматически отключается из-за окончания срока действия или лимита трафика, перезапускать Xray.",
|
||||
"fragment": "Фрагментация",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Настройки Noises",
|
||||
"trustedProxyCidrs": "Доверенные CIDR прокси",
|
||||
"trustedProxyCidrsDesc": "IP/CIDR через запятую, которым разрешено устанавливать заголовки forwarded host, proto и client IP.",
|
||||
"ldap": {
|
||||
"enable": "Включить LDAP-синхронизацию",
|
||||
"host": "LDAP-хост",
|
||||
"port": "Порт LDAP",
|
||||
"useTls": "Использовать TLS (LDAPS)",
|
||||
"skipTlsVerify": "Пропустить проверку сертификата TLS",
|
||||
"skipTlsVerifyDesc": "Небезопасно — отключает проверку сертификата сервера. Используйте только с внутренними/недоверенными CA.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Настроено; оставьте пустым, чтобы сохранить текущий пароль.",
|
||||
"passwordUnconfigured": "Не настроено.",
|
||||
"passwordPlaceholder": "Настроено — введите новое значение для замены",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Фильтр пользователя",
|
||||
"userAttr": "Атрибут пользователя (username/email)",
|
||||
"vlessField": "Атрибут VLESS-флага",
|
||||
"flagField": "Общий атрибут флага (опц.)",
|
||||
"flagFieldDesc": "Если задано, переопределяет флаг VLESS — напр. shadowInactive.",
|
||||
"truthyValues": "Truthy-значения",
|
||||
"truthyValuesDesc": "Через запятую; по умолчанию: true,1,yes,on",
|
||||
"invertFlag": "Инвертировать флаг",
|
||||
"invertFlagDesc": "Включите, когда атрибут означает «отключено» (напр. shadowInactive).",
|
||||
"syncSchedule": "Расписание синхронизации",
|
||||
"syncScheduleDesc": "Строка типа cron, напр. @every 1m",
|
||||
"inboundTags": "Теги входящих",
|
||||
"inboundTagsDesc": "Входящие, на которых LDAP-синхронизация может авто-создавать или авто-удалять клиентов.",
|
||||
"noInbounds": "Входящие не найдены. Сначала создайте входящий.",
|
||||
"autoCreate": "Авто-создание клиентов",
|
||||
"autoDelete": "Авто-удаление клиентов",
|
||||
"defaultTotalGb": "Объём по умолчанию (ГБ)",
|
||||
"defaultExpiryDays": "Срок по умолчанию (дни)",
|
||||
"defaultIpLimit": "Лимит IP по умолчанию"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Добавляет TCP/UDP-маски finalmask Xray и параметры QUIC в каждый созданный профиль Xray JSON. Требуется приложение с поддержкой подписок Xray JSON и свежая версия ядра Xray.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Настройки Mux",
|
||||
"direct": "Прямое подключение",
|
||||
"directDesc": "Устанавливает прямые соединения с доменами или IP-адресами определённой страны.",
|
||||
"notifications": "Уведомления",
|
||||
"certs": "Сертификаты",
|
||||
"externalTraffic": "Внешний трафик",
|
||||
"dateAndTime": "Дата и время",
|
||||
"proxyAndServer": "Прокси и сервер",
|
||||
"intervals": "Интервалы",
|
||||
"information": "Информация",
|
||||
"profile": "Профиль",
|
||||
"language": "Язык интерфейса",
|
||||
"telegramBotLanguage": "Язык Telegram-бота",
|
||||
"security": {
|
||||
"admin": "Учетные данные администратора",
|
||||
"twoFactor": "Двухфакторная аутентификация",
|
||||
"twoFactorEnable": "Включить 2FA",
|
||||
"twoFactorEnableDesc": "Добавляет дополнительный уровень аутентификации для повышения безопасности.",
|
||||
"twoFactorModalSetTitle": "Включить двухфакторную аутентификацию",
|
||||
"twoFactorModalDeleteTitle": "Отключить двухфакторную аутентификацию",
|
||||
"twoFactorModalSteps": "Для настройки двухфакторной аутентификации выполните несколько шагов:",
|
||||
"twoFactorModalFirstStep": "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение",
|
||||
"twoFactorModalSecondStep": "2. Введите код из приложения",
|
||||
"twoFactorModalRemoveStep": "Введите код из приложения, чтобы отключить двухфакторную аутентификацию.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Изменить учетные данные",
|
||||
"twoFactorModalChangeCredentialsStep": "Введите код из приложения, чтобы изменить учетные данные администратора.",
|
||||
"twoFactorModalSetSuccess": "Двухфакторная аутентификация была успешно установлена",
|
||||
"twoFactorModalDeleteSuccess": "Двухфакторная аутентификация была успешно удалена",
|
||||
"twoFactorModalError": "Неверный код",
|
||||
"show": "Показать",
|
||||
"hide": "Скрыть",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Ошибка получения трафика исходящего подключения",
|
||||
"resetOutboundTrafficError": "Ошибка сброса трафика исходящего подключения"
|
||||
},
|
||||
"smtpSettings": "Настройки SMTP",
|
||||
"smtpEnable": "Включить уведомления по Email",
|
||||
"smtpEnableDesc": "Включить уведомления по email через SMTP",
|
||||
"smtpHost": "SMTP хост",
|
||||
"smtpHostDesc": "Имя хоста SMTP сервера (например smtp.gmail.com)",
|
||||
"smtpPort": "SMTP порт",
|
||||
"smtpPortDesc": "Порт SMTP сервера (по умолчанию: 587)",
|
||||
"smtpUsername": "SMTP логин",
|
||||
"smtpUsernameDesc": "Логин для аутентификации SMTP",
|
||||
"smtpFrom": "Адрес отправителя (From)",
|
||||
"smtpFromDesc": "Адрес в заголовке From письма. Оставьте пустым, чтобы использовать имя пользователя.",
|
||||
"smtpFromName": "Имя отправителя (From)",
|
||||
"smtpFromNameDesc": "Необязательное отображаемое имя перед адресом в заголовке From.",
|
||||
"smtpPassword": "SMTP пароль",
|
||||
"smtpPasswordDesc": "Пароль для аутентификации SMTP",
|
||||
"smtpTo": "Получатели",
|
||||
"smtpToDesc": "Адреса получателей через запятую",
|
||||
"emailSettings": "Email",
|
||||
"emailNotifications": "Уведомления",
|
||||
"smtpEventBusNotify": "Email уведомления о событиях",
|
||||
"smtpEventBusNotifyDesc": "Выберите события для email уведомлений",
|
||||
"tgEventBusNotify": "Telegram уведомления о событиях",
|
||||
"tgEventBusNotifyDesc": "Выберите события для Telegram уведомлений",
|
||||
"testSmtp": "Отправить тестовое письмо",
|
||||
"testTgBot": "Отправить тестовое сообщение",
|
||||
"eventGroupOutbound": "Исходящие",
|
||||
"eventGroupXray": "Ядро Xray",
|
||||
"eventGroupSystem": "Система",
|
||||
"eventGroupSecurity": "Безопасность",
|
||||
"eventGroupNode": "Узлы",
|
||||
"eventOutboundDown": "Недоступен",
|
||||
"eventOutboundUp": "Работает",
|
||||
"eventXrayCrash": "Сбой",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "В сети",
|
||||
"eventCPUHigh": "Превышение порога CPU (%)",
|
||||
"requestFailed": "Запрос не удался",
|
||||
"smtpEncryption": "Шифрование",
|
||||
"smtpEncryptionDesc": "Метод шифрования SMTP соединения",
|
||||
"smtpEncryptionNone": "Нет (открытый текст)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (неявное)",
|
||||
"smtpStageConnect": "Подключение",
|
||||
"smtpStageAuth": "Аутентификация",
|
||||
"smtpStageSend": "Отправка",
|
||||
"smtpTestSuccess": "Тестовое письмо отправлено успешно",
|
||||
"smtpHostNotConfigured": "SMTP хост не настроен",
|
||||
"smtpNoRecipients": "Получатели не настроены",
|
||||
"smtpFromNotConfigured": "Адрес отправителя SMTP не настроен",
|
||||
"eventLoginAttempt": "Попытка входа",
|
||||
"telegramTokenConfigured": "Настроен; оставьте пустым для сохранения текущего токена.",
|
||||
"telegramTokenPlaceholder": "Настроен - введите новый токен для замены",
|
||||
"smtpPasswordConfigured": "Настроен; оставьте пустым для сохранения текущего пароля.",
|
||||
"smtpPasswordPlaceholder": "Настроен - введите новый пароль для замены",
|
||||
"smtpNotInitialized": "SMTP не инициализирован",
|
||||
"tgBotNotEnabled": "Telegram бот не включен",
|
||||
"tgTestFailed": "Тест Telegram не удался",
|
||||
"tgTestSuccess": "Тестовое сообщение отправлено в Telegram",
|
||||
"tgBotNotRunning": "Telegram бот не запущен",
|
||||
"smtpErrorAuth": "Ошибка аутентификации — проверьте логин и пароль",
|
||||
"smtpErrorStarttls": "Сервер требует STARTTLS — измените тип шифрования",
|
||||
"smtpErrorTls": "Сервер требует TLS — измените тип шифрования",
|
||||
"smtpErrorRefused": "Соединение отклонено — проверьте хост и порт",
|
||||
"smtpErrorTimeout": "Таймаут соединения — хост недоступен",
|
||||
"smtpErrorRelay": "Сервер отклоняет отправку с этого адреса",
|
||||
"smtpErrorEof": "Соединение закрыто сервером",
|
||||
"smtpErrorUnknown": "Ошибка SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Превышение порога памяти (%)",
|
||||
"remarkTemplate": "Шаблон примечания",
|
||||
"remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше.",
|
||||
@@ -1483,8 +1311,6 @@
|
||||
"directdomains": "Прямые домены",
|
||||
"ipv4Routing": "Правила IPv4",
|
||||
"ipv4RoutingDesc": "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4",
|
||||
"Template": "Шаблон конфигурации Xray",
|
||||
"TemplateDesc": "На основе шаблона создаётся конфигурационный файл Xray.",
|
||||
"FreedomStrategy": "Настройка стратегии протокола Freedom",
|
||||
"FreedomStrategyDesc": "Установка стратегии вывода сети в протоколе Freedom",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "Балансировщик",
|
||||
"balancerTagRequired": "Тег обязателен",
|
||||
"balancerSelectorRequired": "Выберите хотя бы одно исходящее",
|
||||
"balancerLive": "Текущая цель",
|
||||
"balancerOverride": "Переопределить",
|
||||
"balancerOverridePh": "Авто (стратегия)",
|
||||
"balancerLiveRefresh": "Обновить состояние балансировщика",
|
||||
"balancerNotRunning": "Этот балансировщик неактивен в запущенном Xray — сохраните изменения или запустите Xray",
|
||||
"routeTester": "Тест маршрута",
|
||||
"routeTesterDesc": "Спросите запущенный Xray, через какой исходящий будет обработано соединение. Трафик не отправляется — решение поступает напрямую от живого движка маршрутизации.",
|
||||
"routeTesterDest": "Домен или IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "через балансировщик",
|
||||
"routeTesterDefaultOutbound": "Ни одно правило маршрутизации не совпало — трафик направляется в исходящий по умолчанию (первый).",
|
||||
"Routings": "Маршрутизация",
|
||||
"completeTemplate": "Все",
|
||||
"logLevel": "Уровень логов",
|
||||
"logLevelDesc": "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать.",
|
||||
"accessLog": "Логи доступа",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Источник",
|
||||
"dest": "Пункт назначения",
|
||||
"inbound": "Входящее подключение",
|
||||
"balancer": "Балансировщик",
|
||||
"useComma": "Элементы, разделённые запятыми"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Этот сервер уже в списке исходящих. Чтобы обновить ключ, нажмите {reset}.",
|
||||
"provisionFailed": "Не удалось создать исходящее PIA. Попробуйте ещё раз."
|
||||
},
|
||||
"tabBalancerSettings": "Настройки балансировщика",
|
||||
"tabObservatory": "Обсерватория",
|
||||
"observatory": {
|
||||
"autoManaged": "Наблюдатели управляются автоматически на основе ваших балансировщиков. Ниже можно настроить, как они опрашивают; отслеживаемые исходящие следуют за селекторами балансировщика.",
|
||||
"emptyHint": "Нет активного наблюдателя соединений. Он добавляется автоматически при создании балансировщика Least Ping или Least Load — либо Random / Round-robin с fallback — чтобы балансировщики с наблюдателем могли проверять состояние исходящих перед выбором цели.",
|
||||
"mixedLegacy": "В этой конфигурации одновременно есть Observatory и Burst Observatory. Xray использует один глобальный наблюдатель, поэтому такое устаревшее смешанное состояние не поддерживается; сохранение балансировщиков нормализует его до одного наблюдателя.",
|
||||
"subjectSelector": "Отслеживаемые исходящие",
|
||||
"subjectSelectorDesc": "Теги исходящих, которые опрашивает этот наблюдатель. Управляется автоматически на основе ваших балансировщиков.",
|
||||
"probeURL": "URL пробы",
|
||||
"probeURLDesc": "URL, запрашиваемый для измерения каждого исходящего. Должен возвращать HTTP 204.",
|
||||
"probeInterval": "Интервал пробы",
|
||||
"probeIntervalDesc": "Как часто опрашивать каждый исходящий, например 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Параллельные пробы",
|
||||
"enableConcurrencyDesc": "Опрашивать все отслеживаемые исходящие сразу, а не по одному. Быстрее, но заметнее в сети.",
|
||||
"destination": "Назначение пробы",
|
||||
"destinationDesc": "URL, запрашиваемый для измерения каждого исходящего. Должен возвращать HTTP 204.",
|
||||
"connectivity": "Проверка связи",
|
||||
"connectivityDesc": "Необязательный URL проверки локальной сети, используется только после сбоя назначения. Оставьте пустым, чтобы пропустить.",
|
||||
"interval": "Интервал пробы",
|
||||
"intervalDesc": "Среднее время между пробами для каждого исходящего, например 1m. Минимум 10s.",
|
||||
"timeout": "Тайм-аут пробы",
|
||||
"timeoutDesc": "Сколько ждать ответа на пробу, прежде чем считать её неуспешной, например 5s.",
|
||||
"sampling": "Размер выборки",
|
||||
"samplingDesc": "Сколько последних результатов проб хранится для оценки каждого исходящего.",
|
||||
"httpMethod": "Метод HTTP",
|
||||
"httpMethodDesc": "Метод HTTP, используемый для проб.",
|
||||
"deleteAlsoObservatory": "Это последний балансировщик, использующий Observatory, поэтому он тоже будет удалён.",
|
||||
"deleteAlsoBurst": "Это последний балансировщик, использующий Burst Observatory, поэтому он тоже будет удалён."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Стратегия",
|
||||
"tag": "Тег",
|
||||
"tagDuplicate": "Тег уже используется другим балансировщиком",
|
||||
"tagPlaceholder": "уникальный тег балансировщика",
|
||||
"selector": "Селектор",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Цикл: {path} → (обратно к {start})",
|
||||
"expected": "Ожидаемое",
|
||||
"expectedPlaceholder": "оптимальное число узлов",
|
||||
"maxRtt": "Макс. RTT",
|
||||
"tolerance": "Допуск",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Шаблон тега",
|
||||
"costValue": "Вес",
|
||||
"costRegexp": "Совпадение по регулярному выражению",
|
||||
"balancerDeleteInUse": "Невозможно удалить этот балансировщик — он используется как запасной для: {names}",
|
||||
"balancerFallbackCycle": "Невозможно назначить этот балансировщик запасным — это создаст циклическую зависимость.",
|
||||
"balancerFallbackInfo": "Трафик будет маршрутизирован через: Балансировщик → Loopback → Сервер → Целевой балансировщик → Исходящее соединение. Это добавляет дополнительный хоп через сервер, что может вызвать небольшие задержки.",
|
||||
"fallbackBalancerHint": "Выберите другой балансировщик в качестве запасного",
|
||||
"reservedPrefix": "Префикс _bl_ зарезервирован для внутренних loopback-объектов балансеров"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Использование",
|
||||
"addOutbound": "Добавить исходящий"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Включить DNS",
|
||||
"enableDesc": "Включить встроенный DNS-сервер",
|
||||
"tag": "Название тега DNS",
|
||||
"tagDesc": "Этот тег будет доступен как входящий тег в правилах маршрутизации.",
|
||||
"clientIp": "IP клиента",
|
||||
"clientIpDesc": "Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов",
|
||||
"disableCache": "Отключить кэш",
|
||||
"disableCacheDesc": "Отключает кэширование DNS",
|
||||
"disableFallback": "Отключить резервный DNS",
|
||||
"disableFallbackDesc": "Отключает резервные DNS-запросы",
|
||||
"disableFallbackIfMatch": "Отключить резервный DNS при совпадении",
|
||||
"disableFallbackIfMatchDesc": "Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера",
|
||||
"enableParallelQuery": "Включить параллельные запросы",
|
||||
"enableParallelQueryDesc": "Включить параллельные DNS-запросы к нескольким серверам для более быстрого разрешения",
|
||||
"strategy": "Стратегия запроса",
|
||||
"strategyDesc": "Общая стратегия разрешения доменных имен",
|
||||
"add": "Создать DNS",
|
||||
"edit": "Редактировать DNS",
|
||||
"domains": "Домены",
|
||||
"expectIPs": "Ожидаемые IP",
|
||||
"unexpectIPs": "Неожидаемые IP",
|
||||
"useSystemHosts": "Использовать системные Hosts",
|
||||
"useSystemHostsDesc": "Использовать файл hosts из установленной системы",
|
||||
"serveStale": "Использовать устаревшие",
|
||||
"serveStaleDesc": "Возвращать устаревшие результаты из кэша во время обновления в фоне",
|
||||
"serveExpiredTTL": "TTL устаревших",
|
||||
"serveExpiredTTLDesc": "Срок действия (секунды) устаревших записей кэша; 0 = бессрочно",
|
||||
"timeoutMs": "Тайм-аут (мс)",
|
||||
"skipFallback": "Пропустить Fallback",
|
||||
"finalQuery": "Финальный запрос",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Добавить Host",
|
||||
"hostsEmpty": "Host не определены",
|
||||
"hostsDomain": "Домен (напр. domain:example.com)",
|
||||
"hostsValues": "IP или домен — введите и нажмите Enter",
|
||||
"usePreset": "Использовать шаблон",
|
||||
"dnsPresetTitle": "Шаблоны DNS",
|
||||
"dnsPresetFamily": "Семейный",
|
||||
"clearAll": "Удалить все",
|
||||
"clearAllTitle": "Удалить все DNS-серверы?",
|
||||
"clearAllConfirm": "Все DNS-серверы будут удалены из списка. Это действие нельзя отменить.",
|
||||
"dnsLeakWarning": "DNS может утекать через localhost, обычный UDP/TCP, локальный режим DoH/DoQ, fallback-запросы или EDNS client IP. Для приватности используйте маршрутизируемый DoH, закрепляйте резолверы в hosts и отключайте fallback."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Создать Fake DNS",
|
||||
"ipPool": "Подсеть пула IP",
|
||||
"poolSize": "Размер пула"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Исходящий по умолчанию",
|
||||
"defaultOutboundDesc": "Трафик без совпадения с правилами маршрутизации идёт через этот исходящий (первый в списке)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Клавиатура закрыта.",
|
||||
"noResult": "❗ Нет результатов.",
|
||||
"noQuery": "❌ Запрос не найден. Пожалуйста, повторите команду.",
|
||||
"wentWrong": "❌ Что-то пошло не так...",
|
||||
"noIpRecord": "❗ Нет записей об IP-адресе.",
|
||||
"noInbounds": "❗ У вас не настроено ни одного входящего подключения.",
|
||||
"unlimited": "♾ Безлимит",
|
||||
"add": "Добавить",
|
||||
"month": "Месяц",
|
||||
"months": "Месяцев",
|
||||
"days": "Дней",
|
||||
"hours": "Часов",
|
||||
"minutes": "Минуты",
|
||||
"unknown": "Неизвестно",
|
||||
"inbounds": "Входящие",
|
||||
"clients": "Клиенты",
|
||||
"offline": "🔴 Не в сети",
|
||||
"online": "🟢 В сети",
|
||||
"commands": {
|
||||
"unknown": "❗ Неизвестная команда",
|
||||
"pleaseChoose": "👇 Пожалуйста, выберите:\r\n",
|
||||
"help": "🤖 Добро пожаловать! Этот бот предназначен для предоставления вам данных с сервера и позволяет вносить изменения на него.\r\n\r\n",
|
||||
"start": "👋 Привет, <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>!\r\n",
|
||||
"status": "✅ Бот функционирует нормально.",
|
||||
"usage": "❗ Пожалуйста, укажите email для поиска.",
|
||||
"getID": "🆔 Ваш User ID: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Ядро Xray успешно перезапущено.",
|
||||
"restartFailed": "❗ Ошибка при перезапуске Xray-core.\r\n\r\n<code>Ошибка: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core не запущен.",
|
||||
"startDesc": "Показать главное меню",
|
||||
"helpDesc": "Справка по боту",
|
||||
"statusDesc": "Проверить статус бота",
|
||||
"idDesc": "Показать ваш Telegram ID",
|
||||
"usageDesc": "Показать трафик клиента: /usage email",
|
||||
"inboundDesc": "Поиск входящих: /inbound имя (админ)",
|
||||
"restartDesc": "Перезапустить ядро Xray (админ)",
|
||||
"clearallDesc": "Сбросить трафик всех клиентов (админ)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Ошибка при выборе пользователя.",
|
||||
"userSaved": "✅ Пользователь Telegram сохранен.",
|
||||
"loginSuccess": "✅ Успешный вход в панель.\r\n",
|
||||
"loginFailed": "❗️ Ошибка входа в панель.\r\n",
|
||||
"report": "🕰 Запланированные отчеты: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Дата и время: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Хост: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 Версия X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Версия Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Статус: {{ .State }}\r\n",
|
||||
"username": "👤 Имя пользователя: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Причина: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Время: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Входящий: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Порт: {{ .Port }}\r\n",
|
||||
"expire": "📅 Дата окончания: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Окончание через: {{ .Time }}\r\n",
|
||||
"active": "💡 Активен: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Активен: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Статус соединения: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Был(а) в сети: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Загрузка: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Загрузка: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Всего: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Telegram User ID: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Количество исчерпанных {{ .Type }}:\r\n",
|
||||
"onlinesCount": "🌐 Клиентов онлайн: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Отключено: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Время резервного копирования: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Да",
|
||||
"no": "❌ Нет",
|
||||
"received_email": "📧📥 Email обновлен.",
|
||||
"received_comment": "💬📥 Комментарий обновлён.",
|
||||
"email_prompt": "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email.",
|
||||
"comment_prompt": "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий.",
|
||||
"cancel": "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄",
|
||||
"error_add_client": "⚠️ Ошибка:\n\n {{ .error }}",
|
||||
"using_default_value": "Используется значение по умолчанию👌",
|
||||
"incorrect_input": "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫",
|
||||
"AreYouSure": "Вы уверены? 🤔",
|
||||
"SuccessResetTraffic": "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно",
|
||||
"FailedResetTraffic": "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Сброс трафика завершён для всех клиентов.",
|
||||
"eventOutboundDown": "Исходящее подключение {{ .Tag }} НЕДОСТУПНО",
|
||||
"eventOutboundUp": "Исходящее подключение {{ .Tag }} РАБОТАЕТ",
|
||||
"eventErrorDetail": "Ошибка: {{ .Error }}",
|
||||
"eventDelayDetail": "Задержка: {{ .Delay }} мс",
|
||||
"eventXrayCrash": "Сбой Xray",
|
||||
"eventXrayCrashError": "Ошибка: {{ .Error }}",
|
||||
"eventNodeDown": "Узел {{ .Name }} НЕДОСТУПЕН",
|
||||
"eventNodeUp": "Узел {{ .Name }} В СЕТИ",
|
||||
"eventLoginFallback": "Неудачный вход с {{ .Source }}",
|
||||
"memoryThreshold": "🔴 Использование памяти {{ .Percent }}% превышает пороговое значение {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Закрыть клавиатуру",
|
||||
"cancel": "❌ Отмена",
|
||||
"cancelReset": "❌ Отменить сброс",
|
||||
"cancelIpLimit": "❌ Отменить лимит IP",
|
||||
"confirmResetTraffic": "✅ Подтвердить сброс трафика?",
|
||||
"confirmClearIps": "✅ Подтвердить очистку IP?",
|
||||
"confirmRemoveTGUser": "✅ Подтвердить удаление пользователя Telegram?",
|
||||
"confirmToggle": "✅ Подтвердить вкл/выкл пользователя?",
|
||||
"dbBackup": "📂 Бэкап БД",
|
||||
"serverUsage": "💻 Состояние сервера",
|
||||
"getInbounds": "🔌 Входящие подключения",
|
||||
"depleteSoon": "⚠️ Скоро конец",
|
||||
"clientUsage": "Статистика клиента",
|
||||
"onlines": "🟢 Онлайн",
|
||||
"commands": "🖱️ Команды",
|
||||
"refresh": "🔄 Обновить",
|
||||
"clearIPs": "❌ Очистить IP",
|
||||
"removeTGUser": "❌ Удалить пользователя Telegram",
|
||||
"selectTGUser": "👤 Выбрать пользователя Telegram",
|
||||
"selectOneTGUser": "👤 Выберите пользователя Telegram:",
|
||||
"resetTraffic": "📈 Сбросить трафик",
|
||||
"resetExpire": "📅 Изменить дату окончания",
|
||||
"ipLog": "🔢 Лог IP",
|
||||
"ipLimit": "🔢 Лимит IP",
|
||||
"setTGUser": "👤 Установить пользователя Telegram",
|
||||
"toggle": "🔘 Вкл./Выкл.",
|
||||
"custom": "🔢 Своё",
|
||||
"confirmNumber": "✅ Подтвердить: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Подтвердить добавление: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Лимит трафика",
|
||||
"getBanLogs": "📄 Лог банов",
|
||||
"allClients": "👥 Все клиенты",
|
||||
"addClient": "➕ Новый клиент",
|
||||
"submitDisable": "Добавить отключенным ☑️",
|
||||
"submitEnable": "Добавить включенным ✅",
|
||||
"use_default": "🏷️ Использовать по умолчанию",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Комментарий",
|
||||
"ResetAllTraffics": "Сбросить весь трафик",
|
||||
"SortedTrafficUsageReport": "Отсортированный отчет об использовании трафика"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Успешно!",
|
||||
"errorOperation": "❗ Ошибка в операции.",
|
||||
"getInboundsFailed": "❌ Не удалось получить входящие подключения.",
|
||||
"getClientsFailed": "❌ Не удалось получить клиентов.",
|
||||
"canceled": "❌ {{ .Email }}: Операция отменена.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Клиент успешно обновлен.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IP-адреса успешно обновлены.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Трафик успешно сброшен.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Лимит трафика успешно установлен.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Срок действия успешно сброшен.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IP-адреса успешно очищены.",
|
||||
"getIpLog": "✅ {{ .Email }}: Получен лог IP.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Получена информация о пользователе Telegram.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Пользователь Telegram успешно удален.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Включено успешно.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Отключено успешно.",
|
||||
"askToAddUserId": "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Выберите клиента для входящего подключения {{ .Inbound }}",
|
||||
"chooseInbound": "Выберите входящее подключение"
|
||||
}
|
||||
"noIpRecord": "❗ Нет записей об IP-адресе."
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Статус",
|
||||
"labelOutbound": "Исходящее подключение",
|
||||
"labelNode": "Узел",
|
||||
"labelError": "Ошибка",
|
||||
"labelDelay": "Задержка",
|
||||
"labelUsername": "Имя пользователя",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Причина",
|
||||
"labelSource": "Источник",
|
||||
"statusCrashed": "СБОЙ",
|
||||
"statusHigh": "ВЫСОКАЯ",
|
||||
"statusSuccess": "УСПЕШНО",
|
||||
"statusFailed": "НЕУДАЧНО",
|
||||
"statusDown": "НЕДОСТУПЕН",
|
||||
"statusUp": "РАБОТАЕТ"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Başarılı",
|
||||
"lastOnline": "Son Çevrimiçi",
|
||||
"lastSubFetch": "Son abonelik çekme",
|
||||
"getVersion": "Sürümü Al",
|
||||
"install": "Yükle",
|
||||
"clients": "Kullanıcılar",
|
||||
"usage": "Kullanım",
|
||||
"twoFactorCode": "Kod",
|
||||
"remained": "Kalan",
|
||||
"security": "Güvenlik",
|
||||
"emptyDnsDesc": "Eklenmiş DNS sunucusu yok.",
|
||||
"emptyFakeDnsDesc": "Eklenmiş Fake DNS sunucusu yok.",
|
||||
"emptyBalancersDesc": "Eklenmiş dengeleyici yok.",
|
||||
"somethingWentWrong": "Bir hata oluştu",
|
||||
"subscription": {
|
||||
"title": "Abonelik Bilgisi",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Yüklenen",
|
||||
"expiry": "Son Kullanma",
|
||||
"totalQuota": "Toplam Kota",
|
||||
"individualLinks": "Bireysel Bağlantılar",
|
||||
"active": "Aktif",
|
||||
"inactive": "Pasif",
|
||||
"unlimited": "Sınırsız",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Durdur",
|
||||
"restartXray": "Yeniden Başlat",
|
||||
"xraySwitch": "Sürüm",
|
||||
"xrayUpdates": "Xray Güncellemeleri",
|
||||
"xraySwitchClickDesk": "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir.",
|
||||
"updatePanel": "Paneli Güncelle",
|
||||
"panelUpdateDesc": "Bu işlem 3X-UI'yi en son sürüme güncelleyecek ve panel servisini yeniden başlatacaktır.",
|
||||
"currentPanelVersion": "Mevcut panel sürümü",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "İndirme",
|
||||
"sent": "Gönderilen",
|
||||
"received": "Alınan",
|
||||
"xraySwitchVersionDialog": "Xray sürümünü gerçekten değiştirmek istiyor musunuz?",
|
||||
"xraySwitchVersionDialogDesc": "Bu işlem Xray sürümünü #version# olarak değiştirecektir.",
|
||||
"xraySwitchVersionPopover": "Xray başarıyla güncellendi",
|
||||
"panelUpdateDialog": "Gerçekten paneli güncellemek istiyor musunuz?",
|
||||
"panelUpdateDialogDesc": "Bu işlem 3X-UI'yi #version# sürümüne güncelleyecek ve panel servisini yeniden başlatacaktır.",
|
||||
"panelUpdateStartedPopover": "Panel güncellemesi başlatıldı",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "Güncelleme başarıyla tamamlanamadı. Sunucu günlüklerini kontrol edin veya komut satırından 'x-ui update' çalıştırın.",
|
||||
"panelUpdateUnknownTitle": "Güncellemenin tamamlandığı doğrulanamadı",
|
||||
"panelUpdateUnknownDesc": "Panel zamanında bir sonuç bildirmedi. Geçerli sürümü kontrol etmek için sayfayı yenileyin veya sunucu günlüklerini kontrol edin.",
|
||||
"geofileUpdateDialog": "Geofile'ı gerçekten güncellemek istiyor musunuz?",
|
||||
"geofileUpdateDialogDesc": "Bu işlem #filename# dosyasını güncelleyecektir.",
|
||||
"geofilesUpdateDialogDesc": "Bu, tüm dosyaları güncelleyecektir.",
|
||||
"geofilesUpdateAll": "Tümünü Güncelle",
|
||||
"geofileUpdatePopover": "Geofile başarıyla güncellendi",
|
||||
"geodataTitle": "Geodata Otomatik Güncelleme",
|
||||
"geodataHint": "Xray bu dosyaları zamanlamaya göre indirir ve yeniden başlatmadan canlı yükler. URL'ler HTTPS olmalıdır. Xray'in güncelleyebilmesi için her dosyanın bin klasöründe önceden mevcut olması gerekir.",
|
||||
"geodataCron": "Zamanlama (cron)",
|
||||
"geodataOutbound": "Outbound üzerinden indir (isteğe bağlı)",
|
||||
"geodataFile": "Dosya adı",
|
||||
"geodataAddFile": "Dosya ekle",
|
||||
"geodataSaveRestart": "Kaydet ve Xray'i Yeniden Başlat",
|
||||
"geodataConfirmTitle": "Geodata ayarları kaydedilsin mi?",
|
||||
"geodataConfirmContent": "Xray yapılandırma şablonu güncellenecek ve Xray yeniden başlatılacak.",
|
||||
"geodataInvalidUrl": "Her dosya için bir HTTPS URL gerekir.",
|
||||
"geodataInvalidFile": "Dosya adı yalnızca dosya adı olmalıdır, örn. geosite_custom.dat (yol içermemeli).",
|
||||
"geodataInvalidCron": "Cron 5 alandan oluşmalıdır, örn. 0 4 * * *",
|
||||
"geodataEmpty": "Yapılandırılmış dosya yok. Yönlendirme kurallarında dosyalar ext:geosite_custom.dat:category olarak kullanılır.",
|
||||
"dontRefresh": "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin",
|
||||
"logs": "Günlükler",
|
||||
"accessLogs": "Erişim Günlükleri",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "Varsayılan temel yol \"/\" yaygın olarak bilinmektedir — rastgele bir yol ile değiştirin.",
|
||||
"warnDefaultSubPath": "Varsayılan abonelik yolu \"/sub/\" yaygın olarak bilinmektedir — değiştirin.",
|
||||
"warnDefaultJsonPath": "Varsayılan JSON abonelik yolu \"/json/\" yaygın olarak bilinmektedir — değiştirin.",
|
||||
"TGBotSettings": "Telegram Botu",
|
||||
"panelListeningIP": "Dinleme IP",
|
||||
"panelListeningIPDesc": "Web paneli için IP adresi. (tüm IP'leri dinlemek için boş bırakın)",
|
||||
"panelListeningDomain": "Dinleme Alan Adı",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Mevcut Şifre",
|
||||
"newUsername": "Yeni Kullanıcı Adı",
|
||||
"newPassword": "Yeni Şifre",
|
||||
"telegramBotEnable": "Telegram Botunu Etkinleştir",
|
||||
"telegramBotEnableDesc": "Telegram botunu etkinleştirir.",
|
||||
"telegramToken": "Telegram Token",
|
||||
"telegramTokenDesc": "'{'@'}BotFather'dan alınan Telegram bot token.",
|
||||
"telegramProxy": "SOCKS Proxy",
|
||||
"telegramProxyDesc": "Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)",
|
||||
"telegramAPIServer": "Telegram API Sunucusu",
|
||||
"telegramAPIServerDesc": "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın.",
|
||||
"telegramChatId": "Yönetici Sohbet Kimliği",
|
||||
"telegramChatIdDesc": "Telegram Yönetici Sohbet Kimliği (Chat ID). Birden fazla ise virgülle ayırın. ({'@'}userinfobot'tan alabilirsiniz veya botta '/id' komutunu kullanabilirsiniz.)",
|
||||
"telegramNotifyTime": "Bildirim Zamanı",
|
||||
"telegramNotifyTimeDesc": "Telegram botunun periyodik raporları gönderme sıklığı. Hazır bir aralık seçin veya bir crontab ifadesi girmek için Özel'i seçin.",
|
||||
"notifyTime": {
|
||||
"every": "@every — bir aralıkla tekrarla",
|
||||
"hourly": "@hourly — her saat",
|
||||
"daily": "@daily — her gün 00:00'da",
|
||||
"weekly": "@weekly — her hafta",
|
||||
"monthly": "@monthly — her ay",
|
||||
"custom": "Özel (crontab)",
|
||||
"seconds": "Saniye",
|
||||
"minutes": "Dakika",
|
||||
"hours": "Saat",
|
||||
"interval": "Aralık",
|
||||
"unit": "Birim"
|
||||
},
|
||||
"tgNotifyBackup": "Veritabanı Yedeği",
|
||||
"tgNotifyBackupDesc": "Bir rapor ile birlikte veritabanı yedek dosyasını gönderir.",
|
||||
"tgNotifyLogin": "Giriş Bildirimi",
|
||||
"tgNotifyLoginDesc": "Birisi web panelinize giriş yapmaya çalıştığında kullanıcı adı, IP adresi ve zaman hakkında bildirim almanızı sağlar.",
|
||||
"sessionMaxAge": "Oturum Süresi",
|
||||
"sessionMaxAgeDesc": "Giriş yaptıktan sonra oturum süresi. (birim: dakika)",
|
||||
"expireTimeDiff": "Son Kullanma Tarihi Bildirimi",
|
||||
"expireTimeDiffDesc": "Bu eşik seviyesine ulaşıldığında son kullanma tarihi hakkında bildirim almanızı sağlar. (birim: gün)",
|
||||
"trafficDiff": "Trafik Sınırı Bildirimi",
|
||||
"trafficDiffDesc": "Bu eşik seviyesine ulaşıldığında trafik sınırı hakkında bildirim almanızı sağlar. (birim: GB)",
|
||||
"tgNotifyCpu": "CPU Yükü Bildirimi",
|
||||
"tgNotifyCpuDesc": "CPU yükü bu eşik seviyesini aşarsa bildirim almanızı sağlar. (birim: %)",
|
||||
"timeZone": "Saat Dilimi",
|
||||
"timeZoneDesc": "Planlanmış görevler bu saat dilimine göre çalışacaktır.",
|
||||
"subSettings": "Abonelik",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Abonelik hizmetinin döndürülen içeriğini Base64 ile şifreler.",
|
||||
"subURI": "Ters Proxy URI",
|
||||
"subURIDesc": "Abonelik bağlantısı ve QR kodu için Dinleme Alan Adı/Dinleme Portu yerine kullanılan tam temel URL (scheme://alanadi[:port]/yol/). Abonelik bir ters proxy üzerinden veya yukarıdakilerden farklı bir alan adı/port üzerinden erişildiğinde bunu ayarlayın.",
|
||||
"externalTrafficInformEnable": "Harici Trafik Bilgisi",
|
||||
"externalTrafficInformEnableDesc": "Her trafik güncellemesinde harici API'yi bilgilendirir.",
|
||||
"externalTrafficInformURI": "Harici Trafik Bilgisi URI'si",
|
||||
"externalTrafficInformURIDesc": "Trafik güncellemeleri bu URI'ye gönderilir.",
|
||||
"restartXrayOnClientDisable": "Otomatik Devre Dışı Sonrası Xray'i Yeniden Başlat",
|
||||
"restartXrayOnClientDisableDesc": "Bir kullanıcı süre dolumu veya trafik limiti nedeniyle otomatik devre dışı bırakıldığında Xray'i yeniden başlatır.",
|
||||
"fragment": "Parçalama",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Noises Ayarları",
|
||||
"trustedProxyCidrs": "Güvenilir Proxy CIDR'leri",
|
||||
"trustedProxyCidrsDesc": "İletilen host, proto ve istemci IP başlıklarını ayarlamasına izin verilen IP'ler/CIDR'ler (virgülle ayrılmış).",
|
||||
"ldap": {
|
||||
"enable": "LDAP senkronizasyonunu etkinleştir",
|
||||
"host": "LDAP sunucusu",
|
||||
"port": "LDAP bağlantı noktası",
|
||||
"useTls": "TLS kullan (LDAPS)",
|
||||
"skipTlsVerify": "TLS sertifika doğrulamasını atla",
|
||||
"skipTlsVerifyDesc": "Güvenli değil — sunucu sertifika doğrulamasını devre dışı bırakır. Yalnızca dahili/güvenilmeyen CA'larla kullanın.",
|
||||
"bindDn": "DN'yi yazın",
|
||||
"passwordConfigured": "Yapılandırıldı; mevcut parolayı korumak için boş bırakın.",
|
||||
"passwordUnconfigured": "Yapılandırılmadı.",
|
||||
"passwordPlaceholder": "Yapılandırıldı — değiştirmek için yeni değer girin",
|
||||
"baseDn": "Temel DN",
|
||||
"userFilter": "Kullanıcı filtresi",
|
||||
"userAttr": "Kullanıcı özniteliği (username/email)",
|
||||
"vlessField": "VLESS flag özniteliği",
|
||||
"flagField": "Genel flag özniteliği (opsiyonel)",
|
||||
"flagFieldDesc": "Ayarlanırsa VLESS flag'ini geçersiz kılar — örn. shadowInactive.",
|
||||
"truthyValues": "Truthy değerler",
|
||||
"truthyValuesDesc": "Virgülle ayrılmış; varsayılan: true,1,yes,on",
|
||||
"invertFlag": "Flag'i tersine çevir",
|
||||
"invertFlagDesc": "Öznitelik «devre dışı» anlamına geldiğinde etkinleştirin (örn. shadowInactive).",
|
||||
"syncSchedule": "Senkronizasyon programı",
|
||||
"syncScheduleDesc": "cron benzeri dize, örn. @every 1m",
|
||||
"inboundTags": "Gelen bağlantı etiketleri",
|
||||
"inboundTagsDesc": "LDAP senkronizasyonunun istemci otomatik oluşturup/silebileceği gelen bağlantılar.",
|
||||
"noInbounds": "Gelen bağlantı bulunamadı. Önce Gelen Bağlantılar'nda bir tane oluşturun.",
|
||||
"autoCreate": "Kullanıcıları otomatik oluştur",
|
||||
"autoDelete": "Kullanıcıları otomatik sil",
|
||||
"defaultTotalGb": "Varsayılan toplam (GB)",
|
||||
"defaultExpiryDays": "Varsayılan son kullanma (gün)",
|
||||
"defaultIpLimit": "Varsayılan IP limiti"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Son Maske",
|
||||
"finalMaskDesc": "Xray finalmask TCP/UDP maskelerini ve QUIC parametrelerini oluşturulan her Xray JSON profiline ekler. Xray JSON aboneliklerini destekleyen bir istemci uygulaması ve güncel bir Xray çekirdeği gerektirir.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Mux Ayarları",
|
||||
"direct": "Doğrudan Bağlantı",
|
||||
"directDesc": "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar.",
|
||||
"notifications": "Bildirimler",
|
||||
"certs": "Sertifikalar",
|
||||
"externalTraffic": "Harici Trafik",
|
||||
"dateAndTime": "Tarih ve Saat",
|
||||
"proxyAndServer": "Proxy ve Sunucu",
|
||||
"intervals": "Aralıklar",
|
||||
"information": "Bilgi",
|
||||
"profile": "Profil",
|
||||
"language": "Dil",
|
||||
"telegramBotLanguage": "Telegram Bot Dili",
|
||||
"security": {
|
||||
"admin": "Yönetici Kimlik Bilgileri",
|
||||
"twoFactor": "İki Adımlı Doğrulama",
|
||||
"twoFactorEnable": "2FA'yı Etkinleştir",
|
||||
"twoFactorEnableDesc": "Daha fazla güvenlik için ek bir doğrulama katmanı ekler.",
|
||||
"twoFactorModalSetTitle": "İki Adımlı Doğrulamayı Etkinleştir",
|
||||
"twoFactorModalDeleteTitle": "İki Adımlı Doğrulamayı Devre Dışı Bırak",
|
||||
"twoFactorModalSteps": "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:",
|
||||
"twoFactorModalFirstStep": "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki metni kopyalayıp uygulamaya yapıştırın.",
|
||||
"twoFactorModalSecondStep": "2. Uygulamadaki kodu girin",
|
||||
"twoFactorModalRemoveStep": "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Kimlik Bilgilerini Değiştir",
|
||||
"twoFactorModalChangeCredentialsStep": "Yönetici kimlik bilgilerini değiştirmek için uygulamadaki kodu girin.",
|
||||
"twoFactorModalSetSuccess": "İki faktörlü kimlik doğrulama başarıyla kuruldu.",
|
||||
"twoFactorModalDeleteSuccess": "İki faktörlü kimlik doğrulama başarıyla silindi.",
|
||||
"twoFactorModalError": "Yanlış kod",
|
||||
"show": "Göster",
|
||||
"hide": "Gizle",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Giden trafik alınırken hata oluştu.",
|
||||
"resetOutboundTrafficError": "Giden trafik sıfırlanırken hata oluştu."
|
||||
},
|
||||
"smtpSettings": "SMTP Ayarları",
|
||||
"smtpEnable": "E-posta Bildirimlerini Etkinleştir",
|
||||
"smtpEnableDesc": "SMTP üzerinden e-posta bildirimlerini etkinleştirin",
|
||||
"smtpHost": "SMTP Sunucusu",
|
||||
"smtpHostDesc": "SMTP sunucu ana bilgisayar adı (örn. smtp.gmail.com)",
|
||||
"smtpPort": "SMTP Bağlantı Noktası",
|
||||
"smtpPortDesc": "SMTP sunucu bağlantı noktası (varsayılan: 587)",
|
||||
"smtpUsername": "SMTP Kullanıcı Adı",
|
||||
"smtpUsernameDesc": "SMTP kimlik doğrulama kullanıcı adı",
|
||||
"smtpFrom": "Gönderen Adresi (From)",
|
||||
"smtpFromDesc": "E-postanın From başlığında kullanılan adres. Boş bırakılırsa kullanıcı adı kullanılır.",
|
||||
"smtpFromName": "Gönderen Adı (From)",
|
||||
"smtpFromNameDesc": "From başlığında adresten önce görünen isteğe bağlı ad.",
|
||||
"smtpPassword": "SMTP Parolası",
|
||||
"smtpPasswordDesc": "SMTP kimlik doğrulama parolası",
|
||||
"smtpTo": "Alıcılar",
|
||||
"smtpToDesc": "Virgülle ayrılmış alıcı e-posta adresleri",
|
||||
"emailSettings": "E-posta",
|
||||
"emailNotifications": "Bildirimler",
|
||||
"smtpEventBusNotify": "E-posta Olay Bildirimleri",
|
||||
"smtpEventBusNotifyDesc": "Hangi olayların e-posta bildirimi tetikleyeceğini seçin",
|
||||
"tgEventBusNotify": "Telegram Olay Bildirimleri",
|
||||
"tgEventBusNotifyDesc": "Hangi olayların Telegram bildirimi tetikleyeceğini seçin",
|
||||
"testSmtp": "Test E-postası Gönder",
|
||||
"testTgBot": "Test Mesajı Gönder",
|
||||
"eventGroupOutbound": "Giden Bağlantı",
|
||||
"eventGroupXray": "Xray Çekirdeği",
|
||||
"eventGroupSystem": "Sistem",
|
||||
"eventGroupSecurity": "Güvenlik",
|
||||
"eventGroupNode": "Düğümler",
|
||||
"eventOutboundDown": "Çevrimdışı",
|
||||
"eventOutboundUp": "Çevrimiçi",
|
||||
"eventXrayCrash": "Çökme",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Çevrimiçi",
|
||||
"eventCPUHigh": "Yüksek CPU (%)",
|
||||
"requestFailed": "İstek başarısız oldu",
|
||||
"smtpEncryption": "Şifreleme",
|
||||
"smtpEncryptionDesc": "SMTP bağlantı şifreleme yöntemi",
|
||||
"smtpEncryptionNone": "Yok (düz metin)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (örtük)",
|
||||
"smtpStageConnect": "Bağlantı",
|
||||
"smtpStageAuth": "Kimlik Doğrulama",
|
||||
"smtpStageSend": "Gönderim",
|
||||
"smtpTestSuccess": "Test e-postası başarıyla gönderildi",
|
||||
"smtpHostNotConfigured": "SMTP sunucusu yapılandırılmamış",
|
||||
"smtpNoRecipients": "Yapılandırılmış alıcı yok",
|
||||
"smtpFromNotConfigured": "SMTP gönderen adresi yapılandırılmamış",
|
||||
"eventLoginAttempt": "Oturum açma denemesi",
|
||||
"telegramTokenConfigured": "Yapılandırıldı; mevcut belirteci korumak için boş bırakın.",
|
||||
"telegramTokenPlaceholder": "Yapılandırıldı - değiştirmek için yeni bir belirteç girin",
|
||||
"smtpPasswordConfigured": "Yapılandırıldı; mevcut parolayı korumak için boş bırakın.",
|
||||
"smtpPasswordPlaceholder": "Yapılandırıldı - değiştirmek için yeni bir parola girin",
|
||||
"smtpNotInitialized": "SMTP başlatılmadı",
|
||||
"tgBotNotEnabled": "Telegram botu etkin değil",
|
||||
"tgTestFailed": "Telegram testi başarısız oldu",
|
||||
"tgTestSuccess": "Test mesajı Telegram'a gönderildi",
|
||||
"tgBotNotRunning": "Telegram botu çalışmıyor",
|
||||
"smtpErrorAuth": "Kimlik doğrulama başarısız — kullanıcı adını ve parolayı kontrol edin",
|
||||
"smtpErrorStarttls": "Sunucu STARTTLS gerektiriyor — şifreleme türünü değiştirin",
|
||||
"smtpErrorTls": "Sunucu TLS gerektiriyor — şifreleme türünü değiştirin",
|
||||
"smtpErrorRefused": "Bağlantı reddedildi — sunucuyu ve bağlantı noktasını kontrol edin",
|
||||
"smtpErrorTimeout": "Bağlantı zaman aşımına uğradı — sunucuya ulaşılamıyor",
|
||||
"smtpErrorRelay": "Sunucu bu adresten gönderimi reddediyor",
|
||||
"smtpErrorEof": "Bağlantı sunucu tarafından kapatıldı",
|
||||
"smtpErrorUnknown": "SMTP hatası: {{ .Error }}",
|
||||
"eventMemoryHigh": "Bellek kullanımı yüksek (%)",
|
||||
"remarkTemplate": "Açıklama Şablonu",
|
||||
"remarkTemplateDesc": "Ayarlandığında, her abonelik bağlantısının açıklama modelinin yerini alır — değişken belirteçleriyle kendi formatınızı yazın (eklemek için düğmeyi kullanın). Yukarıdaki modeli kullanmak için boş bırakın.",
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "Doğrudan Alan Adları",
|
||||
"ipv4Routing": "IPv4 Yönlendirme",
|
||||
"ipv4RoutingDesc": "Bu seçenekler belirli bir varış yerine IPv4 üzerinden trafiği yönlendirir.",
|
||||
"Template": "Gelişmiş Xray Yapılandırma Şablonu",
|
||||
"TemplateDesc": "Nihai Xray yapılandırma dosyası bu şablona göre oluşturulacaktır.",
|
||||
"FreedomStrategy": "Freedom Protokol Stratejisi",
|
||||
"FreedomStrategyDesc": "Freedom Protokolünde ağın çıkış stratejisini ayarlayın.",
|
||||
"FreedomHappyEyeballs": "Özgürlük Dolu Gözler (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "Dengeleyiciler",
|
||||
"balancerTagRequired": "Etiket zorunludur",
|
||||
"balancerSelectorRequired": "En az bir giden bağlantı seçin",
|
||||
"balancerLive": "Anlık Hedef",
|
||||
"balancerOverride": "Hedef Zorla",
|
||||
"balancerOverridePh": "Otomatik (strateji)",
|
||||
"balancerLiveRefresh": "Dengeleyici durumunu yenile",
|
||||
"balancerNotRunning": "Bu dengeleyici çalışan Xray'de etkin değil — değişikliklerinizi kaydedin veya önce Xray'i başlatın",
|
||||
"routeTester": "Rota Testi",
|
||||
"routeTesterDesc": "Çalışan Xray'e hangi giden bağlantının bir isteği işleyeceğini sorun. Gerçek trafik gönderilmez — karar doğrudan canlı yönlendirme motorundan gelir.",
|
||||
"routeTesterDest": "Alan adı veya IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "dengeleyici aracılığıyla",
|
||||
"routeTesterDefaultOutbound": "Hiçbir yönlendirme kuralı eşleşmedi — trafik varsayılan (ilk) giden bağlantıya yönlendirilir.",
|
||||
"Routings": "Yönlendirme Kuralları",
|
||||
"completeTemplate": "Tümü",
|
||||
"logLevel": "Günlük Seviyesi",
|
||||
"logLevelDesc": "Hata günlükleri için kayıt seviyesi; hangi detayda bilginin kaydedileceğini belirler.",
|
||||
"accessLog": "Erişim Günlüğü",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Kaynak",
|
||||
"dest": "Hedef",
|
||||
"inbound": "Gelen Bağlantı",
|
||||
"balancer": "Dengeleyici",
|
||||
"useComma": "Virgülle ayrılmış öğeler"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Bu sunucu zaten çıkış listesinde. Anahtarı yenilemek için {reset} kullanın.",
|
||||
"provisionFailed": "PIA çıkışı oluşturulamadı. Yeniden deneyin."
|
||||
},
|
||||
"tabBalancerSettings": "Dengeleyici Ayarları",
|
||||
"tabObservatory": "Gözlemci",
|
||||
"observatory": {
|
||||
"autoManaged": "Gözlemciler dengeleyicilerinize göre otomatik yönetilir. Nasıl sınama yapacaklarını aşağıdan ayarlayın; izlenen çıkışlar dengeleyici seçicilerini izler.",
|
||||
"emptyHint": "Etkin bir bağlantı gözlemcisi yok. Least Ping veya Least Load dengeleyici — ya da fallback içeren Random / Round-robin dengeleyici — oluşturduğunuzda otomatik olarak bir tane eklenir; böylece gözlemci kullanan dengeleyiciler hedef seçmeden önce çıkış sağlığını kontrol edebilir.",
|
||||
"mixedLegacy": "Bu yapılandırmada hem Observatory hem de Burst Observatory var. Xray tek bir global gözlemci kullanır, bu nedenle bu eski karma durum desteklenmez; dengeleyiciler kaydedildiğinde tek bir gözlemciye normalleştirilir.",
|
||||
"subjectSelector": "İzlenen Çıkışlar",
|
||||
"subjectSelectorDesc": "Bu gözlemcinin sınadığı çıkış etiketleri. Dengeleyicilerinize göre otomatik yönetilir.",
|
||||
"probeURL": "Sınama URL'si",
|
||||
"probeURLDesc": "Her çıkışı ölçmek için istenen URL. HTTP 204 döndürmelidir.",
|
||||
"probeInterval": "Sınama Aralığı",
|
||||
"probeIntervalDesc": "Her çıkışın ne sıklıkta sınanacağı, örn. 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Eşzamanlı Sınama",
|
||||
"enableConcurrencyDesc": "İzlenen tüm çıkışları tek tek yerine aynı anda sına. Daha hızlı ama ağda daha görünür.",
|
||||
"destination": "Sınama Hedefi",
|
||||
"destinationDesc": "Her çıkışı ölçmek için istenen URL. HTTP 204 döndürmelidir.",
|
||||
"connectivity": "Bağlantı Denetimi",
|
||||
"connectivityDesc": "İsteğe bağlı yerel ağ denetim URL'si; yalnızca hedef başarısız olduktan sonra denenir. Atlamak için boş bırakın.",
|
||||
"interval": "Sınama Aralığı",
|
||||
"intervalDesc": "Çıkış başına sınamalar arasındaki ortalama süre, örn. 1m. En az 10s.",
|
||||
"timeout": "Sınama Zaman Aşımı",
|
||||
"timeoutDesc": "Bir sınamanın başarısız sayılmadan önce ne kadar bekleneceği, örn. 5s.",
|
||||
"sampling": "Örnekleme Sayısı",
|
||||
"samplingDesc": "Her çıkışı puanlamak için tutulan son sınama sonucu sayısı.",
|
||||
"httpMethod": "HTTP Yöntemi",
|
||||
"httpMethodDesc": "Sınamalar için kullanılan HTTP yöntemi.",
|
||||
"deleteAlsoObservatory": "Bu, Observatory kullanan son dengeleyici, bu yüzden o da kaldırılacak.",
|
||||
"deleteAlsoBurst": "Bu, Burst Observatory kullanan son dengeleyici, bu yüzden o da kaldırılacak."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Strateji",
|
||||
"tag": "Etiket",
|
||||
"tagDuplicate": "Etiket başka bir dengeleyici tarafından kullanılıyor",
|
||||
"tagPlaceholder": "benzersiz dengeleyici etiketi",
|
||||
"selector": "Seçici",
|
||||
"fallback": "Yedek Çözüm",
|
||||
"cycleTooltip": "Döngü: {path} → ({start} adresine geri dön)",
|
||||
"expected": "Beklenen",
|
||||
"expectedPlaceholder": "optimal düğüm sayısı",
|
||||
"maxRtt": "Maks. RTT",
|
||||
"tolerance": "Tolerans",
|
||||
"baselines": "Temel Değerler",
|
||||
"costs": "Maliyetler",
|
||||
"costMatch": "Etiket deseni",
|
||||
"costValue": "Ağırlık",
|
||||
"costRegexp": "Düzenli ifade eşleşmesi",
|
||||
"balancerDeleteInUse": "Bu dengeleyici silinemez — şu dengeleyicilerin yedeği olarak kullanılmaktadır: {names}",
|
||||
"balancerFallbackCycle": "Bu dengeleyiciyi yedek olarak ayarlayamazsınız — döngüsel bağımlılık oluşturur.",
|
||||
"balancerFallbackInfo": "Trafik şu yoldan yönlendirilecektir: Dengeleyici → Loopback → Sunucu → Hedef dengeleyici → Bağlantı çıkışı. Bu, sunucu üzerinden ek bir atlama ekler ve hafif gecikmelere neden olabilir.",
|
||||
"fallbackBalancerHint": "Yedek olarak başka bir dengeleyici seçin",
|
||||
"reservedPrefix": " _bl_ ön eki dahili dengeleyici loopback nesneleri için ayrılmıştır"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Kullanım",
|
||||
"addOutbound": "Giden Bağlantı Ekle"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "DNS'yi Etkinleştir",
|
||||
"enableDesc": "Dahili DNS sunucusunu etkinleştirir.",
|
||||
"tag": "DNS Gelen Bağlantı Etiketi",
|
||||
"tagDesc": "Bu etiket, yönlendirme kurallarında gelen bağlantı etiketi olarak kullanılabilir.",
|
||||
"clientIp": "İstemci IP",
|
||||
"clientIpDesc": "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır.",
|
||||
"disableCache": "Önbelleği Devre Dışı Bırak",
|
||||
"disableCacheDesc": "DNS önbelleğini devre dışı bırakır.",
|
||||
"disableFallback": "Yedeklemeyi Devre Dışı Bırak",
|
||||
"disableFallbackDesc": "Yedek DNS sorgularını devre dışı bırakır.",
|
||||
"disableFallbackIfMatch": "Eşleşirse Yedeklemeyi Devre Dışı Bırak",
|
||||
"disableFallbackIfMatchDesc": "Alan adı listesi eşleştiğinde yedek DNS sorgularını devre dışı bırakır.",
|
||||
"enableParallelQuery": "Paralel Sorguyu Etkinleştir",
|
||||
"enableParallelQueryDesc": "Daha hızlı çözümleme için birden fazla sunucuya paralel DNS sorgularını etkinleştirir.",
|
||||
"strategy": "Sorgu Stratejisi",
|
||||
"strategyDesc": "Alan adlarını çözmek için genel strateji.",
|
||||
"add": "Sunucu Ekle",
|
||||
"edit": "Sunucuyu Düzenle",
|
||||
"domains": "Alan Adları",
|
||||
"expectIPs": "Beklenen IP'ler",
|
||||
"unexpectIPs": "Beklenmeyen IP'ler",
|
||||
"useSystemHosts": "Sistem Hosts'larını Kullan",
|
||||
"useSystemHostsDesc": "İşletim sisteminin hosts dosyasını kullanır.",
|
||||
"serveStale": "Süresi Dolmuş Sonuçları Sun",
|
||||
"serveStaleDesc": "Arka planda yenilenirken süresi dolmuş önbellek sonuçlarını döndürür.",
|
||||
"serveExpiredTTL": "Süresi Dolmuş TTL",
|
||||
"serveExpiredTTLDesc": "Süresi dolmuş önbellek girdilerinin geçerlilik süresi (saniye); 0 = asla",
|
||||
"timeoutMs": "Zaman Aşımı (ms)",
|
||||
"skipFallback": "Yedekleri Atla",
|
||||
"finalQuery": "Son Sorgu",
|
||||
"hosts": "Sunucular",
|
||||
"hostsAdd": "Host Ekle",
|
||||
"hostsEmpty": "Tanımlı Host yok.",
|
||||
"hostsDomain": "Alan adı (ör. domain:example.com)",
|
||||
"hostsValues": "IP veya alan adı — yazıp Enter'a basın",
|
||||
"usePreset": "Şablon Kullan",
|
||||
"dnsPresetTitle": "DNS Şablonları",
|
||||
"dnsPresetFamily": "Aile",
|
||||
"clearAll": "Tümünü Sil",
|
||||
"clearAllTitle": "Tüm DNS sunucularını sil?",
|
||||
"clearAllConfirm": "Bu işlem tüm DNS sunucularını listeden kaldırır. Geri alınamaz.",
|
||||
"dnsLeakWarning": "DNS; localhost, düz UDP/TCP, yerel mod DoH/DoQ, fallback sorguları veya EDNS client IP üzerinden sızabilir. Gizlilik önemliyse yönlendirilmiş DoH, hosts sabitlemeleri ve kapalı fallback kullanın."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Sahte DNS Ekle",
|
||||
"ipPool": "IP Havuzu Alt Ağı",
|
||||
"poolSize": "Havuz Boyutu"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Varsayılan giden",
|
||||
"defaultOutboundDesc": "Yönlendirme kuralıyla eşleşmeyen trafik bu gideni kullanır (listedeki ilk giden)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Klavye kapatıldı!",
|
||||
"noResult": "❗ Sonuç yok!",
|
||||
"noQuery": "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!",
|
||||
"wentWrong": "❌ Bir şeyler yanlış gitti!",
|
||||
"noIpRecord": "❗ IP Kaydı Yok!",
|
||||
"noInbounds": "❗ Gelen bağlantı bulunamadı!",
|
||||
"unlimited": "♾ Sınırsız (Sıfırla)",
|
||||
"add": "Ekle",
|
||||
"month": "Ay",
|
||||
"months": "Aylar",
|
||||
"days": "Günler",
|
||||
"hours": "Saatler",
|
||||
"minutes": "Dakikalar",
|
||||
"unknown": "Bilinmeyen",
|
||||
"inbounds": "Gelen Bağlantılar",
|
||||
"clients": "Kullanıcılar",
|
||||
"offline": "🔴 Çevrimdışı",
|
||||
"online": "🟢 Çevrimiçi",
|
||||
"commands": {
|
||||
"unknown": "❗ Bilinmeyen komut.",
|
||||
"pleaseChoose": "👇 Lütfen seçin:\r\n",
|
||||
"help": "🤖 Bu bota hoş geldiniz! Web panelinden belirli verileri sunmak ve gerektiğinde değişiklik yapmanıza olanak tanımak için tasarlanmıştır.\r\n\r\n",
|
||||
"start": "👋 Merhaba <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 <b>{{ .Hostname }}</b> yönetim botuna hoş geldiniz.\r\n",
|
||||
"status": "✅ Bot çalışıyor!",
|
||||
"usage": "❗ Lütfen aramak için bir metin sağlayın!",
|
||||
"getID": "🆔 Kimliğiniz: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Xray Core'u yeniden başlatmak için:\r\n<code>/restart</code>\r\n\r\nBir kullanıcının istatistiklerini aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nGelen bağlantılarnı aramak için (kullanıcı istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği (Chat ID):\r\n<code>/id</code>",
|
||||
"helpClientCommands": "İstatistiklerinizi görmek için şu komutu kullanın:\r\n\r\n<code>/usage [E-posta]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>",
|
||||
"restartUsage": "<code>/restart</code>",
|
||||
"restartSuccess": "✅ İşlem başarılı!",
|
||||
"restartFailed": "❗ İşlem hatası.\r\n\r\n<code>Hata: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core çalışmıyor.",
|
||||
"startDesc": "Ana menüyü göster",
|
||||
"helpDesc": "Bot yardımı",
|
||||
"statusDesc": "Bot durumunu kontrol et",
|
||||
"idDesc": "Telegram Kimliğinizi gösterir",
|
||||
"usageDesc": "İstemci kullanımını göster: /usage e-posta",
|
||||
"inboundDesc": "Gelenleri ara: /inbound ad (yönetici)",
|
||||
"restartDesc": "Xray çekirdeğini yeniden başlat (yönetici)",
|
||||
"clearallDesc": "Tüm istemcilerin trafiğini sıfırla (yönetici)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "CPU Yükü ({{ .Percent }}%), {{ .Threshold }}% eşiğini aşıyor",
|
||||
"selectUserFailed": "❌ Kullanıcı seçiminde hata!",
|
||||
"userSaved": "✅ Telegram Kullanıcısı kaydedildi.",
|
||||
"loginSuccess": "✅ Panele başarıyla giriş yapıldı.\r\n",
|
||||
"loginFailed": "❗️Panele giriş denemesi başarısız oldu.\r\n",
|
||||
"report": "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Tarih ve Saat: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Sunucu: {{ .Hostname }}",
|
||||
"version": "🚀 3X-UI Sürümü: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray Sürümü: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}",
|
||||
"ip": "🌐 IP: {{ .IP }}",
|
||||
"ips": "🔢 IP'ler:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Çalışma Süresi: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Sistem Yükü: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}",
|
||||
"traffic": "🚦 Trafik: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Durum: {{ .State }}\r\n",
|
||||
"username": "👤 Kullanıcı Adı: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Sebep: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Zaman: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Gelen Bağlantı: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Bağlantı Noktası: {{ .Port }}",
|
||||
"expire": "📅 Son Kullanma Tarihi: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Sona Erecek: {{ .Time }}\r\n",
|
||||
"active": "💡 Aktif: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Etkin: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Bağlantı durumu: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Son Çevrimiçi: {{ .Time }}\r\n",
|
||||
"email": "📧 E-posta: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Yükleme: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 İndirme: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Telegram Kullanıcısı: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Limiti Dolan {{ .Type }} sayısı:\r\n",
|
||||
"onlinesCount": "🌐 Çevrimiçi Kullanıcılar: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Devre Dışı: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Kotası Dolmak Üzere: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Yedekleme Zamanı: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Evet",
|
||||
"no": "❌ Hayır",
|
||||
"received_email": "📧📥 E-posta güncellendi.",
|
||||
"received_comment": "💬📥 Yorum güncellendi.",
|
||||
"email_prompt": "📧 Varsayılan E-posta: {{ .ClientEmail }}\n\nE-postanızı girin.",
|
||||
"comment_prompt": "💬 Varsayılan Yorum: {{ .ClientComment }}\n\nYorumunuzu girin.",
|
||||
"cancel": "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄",
|
||||
"error_add_client": "⚠️ Hata:\n\n {{ .error }}",
|
||||
"using_default_value": "Tamam, varsayılan değeri kullanacağım. 😊",
|
||||
"incorrect_input": "Geçersiz değer girdiniz.\nİfadeler arasında boşluk olmamalıdır.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫",
|
||||
"AreYouSure": "Emin misiniz? 🤔",
|
||||
"SuccessResetTraffic": "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı",
|
||||
"FailedResetTraffic": "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠️ Hata: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Tüm kullanıcılar için trafik sıfırlama işlemi tamamlandı.",
|
||||
"eventOutboundDown": "{{ .Tag }} giden bağlantısı ÇEVRİMDIŞI",
|
||||
"eventOutboundUp": "{{ .Tag }} giden bağlantısı ÇEVRİMİÇİ",
|
||||
"eventErrorDetail": "Hata: {{ .Error }}",
|
||||
"eventDelayDetail": "Gecikme: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xray ÇÖKTÜ",
|
||||
"eventXrayCrashError": "Hata: {{ .Error }}",
|
||||
"eventNodeDown": "{{ .Name }} düğümü ÇEVRİMDIŞI",
|
||||
"eventNodeUp": "{{ .Name }} düğümü ÇEVRİMİÇİ",
|
||||
"eventLoginFallback": "{{ .Source }} adresinden oturum açma başarısız",
|
||||
"memoryThreshold": "Bellek kullanımı {{ .Percent }}% eşiği {{ .Threshold }}% aşıyor"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Klavyeyi Kapat",
|
||||
"cancel": "❌ İptal",
|
||||
"cancelReset": "❌ Sıfırlamayı İptal Et",
|
||||
"cancelIpLimit": "❌ IP Limitini İptal Et",
|
||||
"confirmResetTraffic": "✅ Trafiği Sıfırlamayı Onayla?",
|
||||
"confirmClearIps": "✅ IP'leri Temizlemeyi Onayla?",
|
||||
"confirmRemoveTGUser": "✅ Telegram Kullanıcısını Kaldırmayı Onayla?",
|
||||
"confirmToggle": "✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?",
|
||||
"dbBackup": "Veritabanı Yedeği Al",
|
||||
"serverUsage": "Sunucu Kullanımı",
|
||||
"getInbounds": "Gelen Bağlantılarnı Al",
|
||||
"depleteSoon": "Kotası Dolmak Üzere",
|
||||
"clientUsage": "Kullanıcı İstatistikleri",
|
||||
"onlines": "Çevrimiçi Kullanıcılar",
|
||||
"commands": "Komutlar",
|
||||
"refresh": "🔄 Yenile",
|
||||
"clearIPs": "❌ IP'leri Temizle",
|
||||
"removeTGUser": "❌ Telegram Kullanıcısını Kaldır",
|
||||
"selectTGUser": "👤 Telegram Kullanıcısını Seç",
|
||||
"selectOneTGUser": "👤 Bir Telegram Kullanıcısını Seçin:",
|
||||
"resetTraffic": "📈 Trafiği Sıfırla",
|
||||
"resetExpire": "📅 Son Kullanma Tarihini Değiştir",
|
||||
"ipLog": "🔢 IP Günlüğü",
|
||||
"ipLimit": "🔢 IP Limiti",
|
||||
"setTGUser": "👤 Telegram Kullanıcısını Ayarla",
|
||||
"toggle": "🔘 Etkinleştir / Devre Dışı Bırak",
|
||||
"custom": "🔢 Özel",
|
||||
"confirmNumber": "✅ Onayla: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Ekleme onayı: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Trafik Sınırı",
|
||||
"getBanLogs": "Yasak Günlüklerini Al",
|
||||
"allClients": "Tüm Kullanıcılar",
|
||||
"addClient": "Kullanıcı Ekle",
|
||||
"submitDisable": "Devre Dışı Olarak Gönder ☑️",
|
||||
"submitEnable": "Etkin Olarak Gönder ✅",
|
||||
"use_default": "🏷️ Varsayılanı Kullan",
|
||||
"change_email": "⚙️📧 E-posta",
|
||||
"change_comment": "⚙️💬 Yorum",
|
||||
"ResetAllTraffics": "Tüm Trafikleri Sıfırla",
|
||||
"SortedTrafficUsageReport": "Sıralı Trafik Kullanım Raporu"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ İşlem başarılı!",
|
||||
"errorOperation": "❗ İşlemde hata.",
|
||||
"getInboundsFailed": "❌ Gelen Bağlantılar alınamadı.",
|
||||
"getClientsFailed": "❌ Kullanıcılar alınamadı.",
|
||||
"canceled": "❌ {{ .Email }}: İşlem iptal edildi.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Kullanıcı başarıyla yenilendi.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IP'ler başarıyla yenilendi.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Kullanıcının Telegram Kimliği başarıyla yenilendi.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Trafik başarıyla sıfırlandı.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: IP limiti {{ .Count }} başarıyla kaydedildi.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IP'ler başarıyla temizlendi.",
|
||||
"getIpLog": "✅ {{ .Email }}: IP Günlüğü alındı.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Telegram Kullanıcı Bilgisi alındı.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Telegram Kullanıcısı başarıyla kaldırıldı.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Başarıyla etkinleştirildi.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Başarıyla devre dışı bırakıldı.",
|
||||
"askToAddUserId": "Yapılandırmanız bulunamadı!\r\nLütfen yöneticinizden Telegram Chat ID'nizi yapılandırmanıza eklemesini isteyin.\r\n\r\nSizin Chat ID'niz: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Gelen Bağlantı {{ .Inbound }} için bir Kullanıcı Seçin",
|
||||
"chooseInbound": "Bir Gelen Bağlantı Seçin"
|
||||
}
|
||||
"noIpRecord": "❗ IP Kaydı Yok!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Durum",
|
||||
"labelOutbound": "Giden Bağlantı",
|
||||
"labelNode": "Düğüm",
|
||||
"labelError": "Hata",
|
||||
"labelDelay": "Gecikme",
|
||||
"labelUsername": "Kullanıcı Adı",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Neden",
|
||||
"labelSource": "Kaynak",
|
||||
"statusCrashed": "ÇÖKTÜ",
|
||||
"statusHigh": "YÜKSEK",
|
||||
"statusSuccess": "BAŞARILI",
|
||||
"statusFailed": "BAŞARISIZ",
|
||||
"statusDown": "ÇEVRİMDIŞI",
|
||||
"statusUp": "ÇEVRİMİÇİ"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Успішно",
|
||||
"lastOnline": "Був(ла) онлайн",
|
||||
"lastSubFetch": "Останнє отримання підписки",
|
||||
"getVersion": "Отримати версію",
|
||||
"install": "Встановити",
|
||||
"clients": "Клієнти",
|
||||
"usage": "Використання",
|
||||
"twoFactorCode": "Код",
|
||||
"remained": "Залишилося",
|
||||
"security": "Беспека",
|
||||
"emptyDnsDesc": "Немає доданих DNS-серверів.",
|
||||
"emptyFakeDnsDesc": "Немає доданих Fake DNS-серверів.",
|
||||
"emptyBalancersDesc": "Немає доданих балансувальників.",
|
||||
"somethingWentWrong": "Щось пішло не так",
|
||||
"subscription": {
|
||||
"title": "Інформація про підписку",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Відвантажено",
|
||||
"expiry": "Термін дії",
|
||||
"totalQuota": "Загальна квота",
|
||||
"individualLinks": "Окремі посилання",
|
||||
"active": "Активна",
|
||||
"inactive": "Неактивна",
|
||||
"unlimited": "Безліміт",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Стоп",
|
||||
"restartXray": "Перезапуск",
|
||||
"xraySwitch": "Версія",
|
||||
"xrayUpdates": "Оновлення Xray",
|
||||
"xraySwitchClickDesk": "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями.",
|
||||
"updatePanel": "Оновити панель",
|
||||
"panelUpdateDesc": "Це оновить 3X-UI до останнього релізу та перезапустить сервіс панелі.",
|
||||
"currentPanelVersion": "Поточна версія панелі",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Завантажити",
|
||||
"sent": "Відправлено",
|
||||
"received": "Отримано",
|
||||
"xraySwitchVersionDialog": "Ви дійсно хочете змінити версію Xray?",
|
||||
"xraySwitchVersionDialogDesc": "Це змінить версію Xray на #version#.",
|
||||
"xraySwitchVersionPopover": "Xray успішно оновлено",
|
||||
"panelUpdateDialog": "Ви дійсно хочете оновити панель?",
|
||||
"panelUpdateDialogDesc": "Це оновить 3X-UI до #version# та перезапустить сервіс панелі.",
|
||||
"panelUpdateStartedPopover": "Розпочато оновлення панелі",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "Оновлення не завершилося успішно. Перевірте журнали сервера або виконайте 'x-ui update' у командному рядку.",
|
||||
"panelUpdateUnknownTitle": "Не вдалося підтвердити завершення оновлення",
|
||||
"panelUpdateUnknownDesc": "Панель не повідомила результат вчасно. Перезавантажте сторінку, щоб перевірити поточну версію, або перевірте журнали сервера.",
|
||||
"geofileUpdateDialog": "Ви дійсно хочете оновити геофайл?",
|
||||
"geofileUpdateDialogDesc": "Це оновить файл #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Це оновить усі геофайли.",
|
||||
"geofilesUpdateAll": "Оновити все",
|
||||
"geofileUpdatePopover": "Геофайл успішно оновлено",
|
||||
"geodataTitle": "Автооновлення Geodata",
|
||||
"geodataHint": "Xray завантажує ці файли за розкладом і перезавантажує їх без перезапуску. URL мають бути HTTPS. Файл має вже існувати в теці bin, щоб Xray міг його оновлювати.",
|
||||
"geodataCron": "Розклад (cron)",
|
||||
"geodataOutbound": "Завантажувати через outbound (необов’язково)",
|
||||
"geodataFile": "Ім’я файлу",
|
||||
"geodataAddFile": "Додати файл",
|
||||
"geodataSaveRestart": "Зберегти та перезапустити Xray",
|
||||
"geodataConfirmTitle": "Зберегти налаштування geodata?",
|
||||
"geodataConfirmContent": "Шаблон конфігурації Xray буде оновлено, а Xray перезапущено.",
|
||||
"geodataInvalidUrl": "Для кожного файлу потрібен HTTPS URL.",
|
||||
"geodataInvalidFile": "Ім’я файлу має бути простим, напр. geosite_custom.dat (без шляхів).",
|
||||
"geodataInvalidCron": "Cron має містити 5 полів, напр. 0 4 * * *",
|
||||
"geodataEmpty": "Файли не налаштовано. У правилах маршрутизації файли вказуються як ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Інсталяція триває, будь ласка, не оновлюйте цю сторінку",
|
||||
"logs": "Логи",
|
||||
"accessLogs": "Логи доступу",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "Базовий шлях за замовчуванням \"/\" широко відомий — змініть його на випадковий.",
|
||||
"warnDefaultSubPath": "Шлях підписки за замовчуванням \"/sub/\" широко відомий — змініть його.",
|
||||
"warnDefaultJsonPath": "JSON-шлях підписки за замовчуванням \"/json/\" широко відомий — змініть його.",
|
||||
"TGBotSettings": "Telegram-бот",
|
||||
"panelListeningIP": "Слухати IP",
|
||||
"panelListeningIPDesc": "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)",
|
||||
"panelListeningDomain": "Домен прослуховування",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Поточний пароль",
|
||||
"newUsername": "Нове ім'я користувача",
|
||||
"newPassword": "Новий пароль",
|
||||
"telegramBotEnable": "Увімкнути Telegram Bot",
|
||||
"telegramBotEnableDesc": "Вмикає бота Telegram.",
|
||||
"telegramToken": "Telegram-токен",
|
||||
"telegramTokenDesc": "Токен бота Telegram, отриманий від '{'@'}BotFather'.",
|
||||
"telegramProxy": "SOCKS-проксі",
|
||||
"telegramProxyDesc": "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)",
|
||||
"telegramAPIServer": "Telegram API сервер",
|
||||
"telegramAPIServerDesc": "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням.",
|
||||
"telegramChatId": "Ідентифікатор чату адміністратора",
|
||||
"telegramChatIdDesc": "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут {'@'}userinfobot) або (використовуйте команду '/id' у боті)",
|
||||
"telegramNotifyTime": "Час сповіщення",
|
||||
"telegramNotifyTimeDesc": "Як часто бот Telegram надсилає періодичні звіти. Виберіть готовий інтервал або «Власний», щоб ввести вираз crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — повторювати з інтервалом",
|
||||
"hourly": "@hourly — щогодини",
|
||||
"daily": "@daily — щодня о 00:00",
|
||||
"weekly": "@weekly — щотижня",
|
||||
"monthly": "@monthly — щомісяця",
|
||||
"custom": "Власний (crontab)",
|
||||
"seconds": "Секунди",
|
||||
"minutes": "Хвилини",
|
||||
"hours": "Години",
|
||||
"interval": "Інтервал",
|
||||
"unit": "Одиниця"
|
||||
},
|
||||
"tgNotifyBackup": "Резервне копіювання бази даних",
|
||||
"tgNotifyBackupDesc": "Надіслати файл резервної копії бази даних зі звітом.",
|
||||
"tgNotifyLogin": "Сповіщення про вхід",
|
||||
"tgNotifyLoginDesc": "Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель.",
|
||||
"sessionMaxAge": "Тривалість сеансу",
|
||||
"sessionMaxAgeDesc": "Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)",
|
||||
"expireTimeDiff": "Повідомлення про дату закінчення",
|
||||
"expireTimeDiffDesc": "Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)",
|
||||
"trafficDiff": "Повідомлення про обмеження трафіку",
|
||||
"trafficDiffDesc": "Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)",
|
||||
"tgNotifyCpu": "Сповіщення про завантаження ЦП",
|
||||
"tgNotifyCpuDesc": "Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)",
|
||||
"timeZone": "Часовий пояс",
|
||||
"timeZoneDesc": "Заплановані завдання виконуватимуться на основі цього часового поясу.",
|
||||
"subSettings": "Підписка",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Повернений вміст послуги підписки матиме кодування Base64.",
|
||||
"subURI": "URI зворотного проксі",
|
||||
"subURIDesc": "Повна базова URL-адреса (scheme://домен[:порт]/шлях/) для посилання підписки та QR-коду, використовується замість Домену/Порту прослуховування. Вкажіть це, якщо підписка доступна через зворотний проксі або на іншому домені/порту, ніж вказано вище.",
|
||||
"externalTrafficInformEnable": "Інформація про зовнішній трафік",
|
||||
"externalTrafficInformEnableDesc": "Повідомляти зовнішній API про кожне оновлення трафіку.",
|
||||
"externalTrafficInformURI": "Інформаційний URI зовнішнього трафіку",
|
||||
"externalTrafficInformURIDesc": "Оновлення трафіку надсилаються на цей URI.",
|
||||
"restartXrayOnClientDisable": "Перезапускати Xray після авто-вимкнення",
|
||||
"restartXrayOnClientDisableDesc": "Коли клієнт автоматично вимикається через закінчення терміну дії або ліміт трафіку, перезапускати Xray.",
|
||||
"fragment": "Фрагментація",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Налаштування Noises",
|
||||
"trustedProxyCidrs": "Довірені CIDR проксі",
|
||||
"trustedProxyCidrsDesc": "IP/CIDR через кому, яким дозволено встановлювати заголовки forwarded host, proto та client IP.",
|
||||
"ldap": {
|
||||
"enable": "Увімкнути LDAP-синхронізацію",
|
||||
"host": "LDAP-хост",
|
||||
"port": "Порт LDAP",
|
||||
"useTls": "Використовувати TLS (LDAPS)",
|
||||
"skipTlsVerify": "Пропустити перевірку сертифіката TLS",
|
||||
"skipTlsVerifyDesc": "Небезпечно — вимикає перевірку сертифіката сервера. Використовуйте лише з внутрішніми/ненадійними CA.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Налаштовано; залиште порожнім для збереження поточного паролю.",
|
||||
"passwordUnconfigured": "Не налаштовано.",
|
||||
"passwordPlaceholder": "Налаштовано — введіть нове значення для заміни",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Фільтр користувача",
|
||||
"userAttr": "Атрибут користувача (username/email)",
|
||||
"vlessField": "Атрибут VLESS-flag",
|
||||
"flagField": "Загальний атрибут flag (опц.)",
|
||||
"flagFieldDesc": "Якщо задано, перевизначає VLESS flag — напр. shadowInactive.",
|
||||
"truthyValues": "Truthy-значення",
|
||||
"truthyValuesDesc": "Через кому; за замовч.: true,1,yes,on",
|
||||
"invertFlag": "Інвертувати flag",
|
||||
"invertFlagDesc": "Увімкніть, коли атрибут означає «вимкнено» (напр. shadowInactive).",
|
||||
"syncSchedule": "Розклад синхронізації",
|
||||
"syncScheduleDesc": "Рядок типу cron, напр. @every 1m",
|
||||
"inboundTags": "Теги вхідних",
|
||||
"inboundTagsDesc": "Вхідні, на яких LDAP-синхронізація може авто-створювати або авто-видаляти клієнтів.",
|
||||
"noInbounds": "Вхідних не знайдено. Спочатку створіть один у Вхідних.",
|
||||
"autoCreate": "Авто-створення клієнтів",
|
||||
"autoDelete": "Авто-видалення клієнтів",
|
||||
"defaultTotalGb": "Обсяг за замовч. (ГБ)",
|
||||
"defaultExpiryDays": "Термін за замовч. (дні)",
|
||||
"defaultIpLimit": "Ліміт IP за замовч."
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Додає TCP/UDP-маски finalmask Xray і параметри QUIC до кожного створеного профілю Xray JSON. Потрібен застосунок із підтримкою підписок Xray JSON і нова версія ядра Xray.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Налаштування Mux",
|
||||
"direct": "Пряме підключення",
|
||||
"directDesc": "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни.",
|
||||
"notifications": "Сповіщення",
|
||||
"certs": "Сертифікати",
|
||||
"externalTraffic": "Зовнішній трафік",
|
||||
"dateAndTime": "Дата та час",
|
||||
"proxyAndServer": "Проксі та сервер",
|
||||
"intervals": "Інтервали",
|
||||
"information": "Інформація",
|
||||
"profile": "Профіль",
|
||||
"language": "Мова",
|
||||
"telegramBotLanguage": "Мова Telegram-бота",
|
||||
"security": {
|
||||
"admin": "Облікові дані адміністратора",
|
||||
"twoFactor": "Двофакторна аутентифікація",
|
||||
"twoFactorEnable": "Увімкнути 2FA",
|
||||
"twoFactorEnableDesc": "Додає додатковий рівень аутентифікації для підвищення безпеки.",
|
||||
"twoFactorModalSetTitle": "Увімкнути двофакторну аутентифікацію",
|
||||
"twoFactorModalDeleteTitle": "Вимкнути двофакторну аутентифікацію",
|
||||
"twoFactorModalSteps": "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:",
|
||||
"twoFactorModalFirstStep": "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму",
|
||||
"twoFactorModalSecondStep": "2. Введіть код з програми",
|
||||
"twoFactorModalRemoveStep": "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Змінити облікові дані",
|
||||
"twoFactorModalChangeCredentialsStep": "Введіть код з додатку, щоб змінити облікові дані адміністратора.",
|
||||
"twoFactorModalSetSuccess": "Двофакторна аутентифікація була успішно встановлена",
|
||||
"twoFactorModalDeleteSuccess": "Двофакторна аутентифікація була успішно видалена",
|
||||
"twoFactorModalError": "Невірний код",
|
||||
"show": "Показати",
|
||||
"hide": "Сховати",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Помилка отримання вихідного трафіку",
|
||||
"resetOutboundTrafficError": "Помилка скидання вихідного трафіку"
|
||||
},
|
||||
"smtpSettings": "Налаштування SMTP",
|
||||
"smtpEnable": "Увімкнути сповіщення електронною поштою",
|
||||
"smtpEnableDesc": "Увімкнути сповіщення електронною поштою через SMTP",
|
||||
"smtpHost": "Хост SMTP",
|
||||
"smtpHostDesc": "Ім'я хоста сервера SMTP (наприклад, smtp.gmail.com)",
|
||||
"smtpPort": "Порт SMTP",
|
||||
"smtpPortDesc": "Порт сервера SMTP (типово: 587)",
|
||||
"smtpUsername": "Ім'я користувача SMTP",
|
||||
"smtpUsernameDesc": "Ім'я користувача для автентифікації SMTP",
|
||||
"smtpFrom": "Адреса відправника (From)",
|
||||
"smtpFromDesc": "Адреса в заголовку From листа. Залиште порожнім, щоб використати ім'я користувача.",
|
||||
"smtpFromName": "Ім'я відправника (From)",
|
||||
"smtpFromNameDesc": "Необов'язкове відображуване ім'я перед адресою в заголовку From.",
|
||||
"smtpPassword": "Пароль SMTP",
|
||||
"smtpPasswordDesc": "Пароль для автентифікації SMTP",
|
||||
"smtpTo": "Отримувачі",
|
||||
"smtpToDesc": "Адреси електронної пошти отримувачів, розділені комами",
|
||||
"emailSettings": "Електронна пошта",
|
||||
"emailNotifications": "Сповіщення",
|
||||
"smtpEventBusNotify": "Сповіщення про події електронною поштою",
|
||||
"smtpEventBusNotifyDesc": "Виберіть, які події спричиняють сповіщення електронною поштою",
|
||||
"tgEventBusNotify": "Сповіщення про події в Telegram",
|
||||
"tgEventBusNotifyDesc": "Виберіть, які події спричиняють сповіщення в Telegram",
|
||||
"testSmtp": "Надіслати тестовий лист",
|
||||
"testTgBot": "Надіслати тестове повідомлення",
|
||||
"eventGroupOutbound": "Вихідні з'єднання",
|
||||
"eventGroupXray": "Ядро Xray",
|
||||
"eventGroupSystem": "Система",
|
||||
"eventGroupSecurity": "Безпека",
|
||||
"eventGroupNode": "Вузли",
|
||||
"eventOutboundDown": "Недоступне",
|
||||
"eventOutboundUp": "Доступне",
|
||||
"eventXrayCrash": "Збій",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Доступний",
|
||||
"eventCPUHigh": "Високе навантаження на CPU (%)",
|
||||
"requestFailed": "Запит не вдалося виконати",
|
||||
"smtpEncryption": "Шифрування",
|
||||
"smtpEncryptionDesc": "Метод шифрування з'єднання SMTP",
|
||||
"smtpEncryptionNone": "Немає (відкритий текст)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (неявне)",
|
||||
"smtpStageConnect": "З'єднання",
|
||||
"smtpStageAuth": "Автентифікація",
|
||||
"smtpStageSend": "Надсилання",
|
||||
"smtpTestSuccess": "Тестовий лист успішно надіслано",
|
||||
"smtpHostNotConfigured": "Хост SMTP не налаштовано",
|
||||
"smtpNoRecipients": "Отримувачів не налаштовано",
|
||||
"smtpFromNotConfigured": "Адресу відправника SMTP не налаштовано",
|
||||
"eventLoginAttempt": "Спроба входу",
|
||||
"telegramTokenConfigured": "Налаштовано; залиште порожнім, щоб зберегти поточний токен.",
|
||||
"telegramTokenPlaceholder": "Налаштовано — введіть новий токен для заміни",
|
||||
"smtpPasswordConfigured": "Налаштовано; залиште порожнім, щоб зберегти поточний пароль.",
|
||||
"smtpPasswordPlaceholder": "Налаштовано — введіть новий пароль для заміни",
|
||||
"smtpNotInitialized": "SMTP не ініціалізовано",
|
||||
"tgBotNotEnabled": "Бот Telegram не увімкнено",
|
||||
"tgTestFailed": "Тест Telegram не вдався",
|
||||
"tgTestSuccess": "Тестове повідомлення надіслано в Telegram",
|
||||
"tgBotNotRunning": "Бот Telegram не запущено",
|
||||
"smtpErrorAuth": "Помилка автентифікації — перевірте ім'я користувача та пароль",
|
||||
"smtpErrorStarttls": "Сервер вимагає STARTTLS — змініть тип шифрування",
|
||||
"smtpErrorTls": "Сервер вимагає TLS — змініть тип шифрування",
|
||||
"smtpErrorRefused": "У з'єднанні відмовлено — перевірте хост і порт",
|
||||
"smtpErrorTimeout": "Час очікування з'єднання вичерпано — хост недоступний",
|
||||
"smtpErrorRelay": "Сервер відхиляє надсилання з цієї адреси",
|
||||
"smtpErrorEof": "З'єднання закрито сервером",
|
||||
"smtpErrorUnknown": "Помилка SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Високе використання пам'яті (%)",
|
||||
"remarkTemplate": "Шаблон примітки",
|
||||
"remarkTemplateDesc": "Якщо задано, це замінює модель примітки для кожного посилання підписки — напишіть власний формат із токенами змінних (використовуйте кнопку для їх вставлення). Залиште порожнім, щоб використовувати модель вище.",
|
||||
@@ -1475,8 +1303,6 @@
|
||||
"directdomains": "Прямі домени",
|
||||
"ipv4Routing": "Маршрутизація IPv4",
|
||||
"ipv4RoutingDesc": "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4.",
|
||||
"Template": "Шаблон розширеної конфігурації Xray",
|
||||
"TemplateDesc": "Остаточний конфігураційний файл Xray буде створено на основі цього шаблону.",
|
||||
"FreedomStrategy": "Стратегія протоколу свободи",
|
||||
"FreedomStrategyDesc": "Установити стратегію виведення для мережі в протоколі свободи.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1492,11 +1318,6 @@
|
||||
"Balancers": "Балансери",
|
||||
"balancerTagRequired": "Тег обов'язковий",
|
||||
"balancerSelectorRequired": "Виберіть принаймні один вихідний",
|
||||
"balancerLive": "Поточна ціль",
|
||||
"balancerOverride": "Примусова ціль",
|
||||
"balancerOverridePh": "Авто (стратегія)",
|
||||
"balancerLiveRefresh": "Оновити стан балансувальника",
|
||||
"balancerNotRunning": "Цей балансувальник неактивний у запущеному Xray — збережіть зміни або спочатку запустіть Xray",
|
||||
"routeTester": "Тест маршруту",
|
||||
"routeTesterDesc": "Запитайте запущений Xray, через який вихідний буде оброблено з'єднання. Реальний трафік не надсилається — рішення надходить безпосередньо від живого рушія маршрутизації.",
|
||||
"routeTesterDest": "Домен або IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"metricsListen": "Точка доступу метрик",
|
||||
"metricsListenDesc": "Надає метрики Xray у стилі Prometheus за цією адресою:порт (наприклад, 127.0.0.1:11111). Залиште порожнім, щоб вимкнути. Прив'яжіть до localhost і використовуйте зворотний проксі — доступ без автентифікації.",
|
||||
"metricsTag": "Тег метрик",
|
||||
"completeTemplate": "Усі",
|
||||
"logLevel": "Рівень журналу",
|
||||
"logLevelDesc": "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати.",
|
||||
"accessLog": "Журнал доступу",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Джерело",
|
||||
"dest": "Пункт призначення",
|
||||
"inbound": "Вхідний",
|
||||
"balancer": "Балансувальник",
|
||||
"useComma": "Елементи, розділені комами"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Цей сервер уже в списку вихідних. Щоб оновити ключ, натисніть {reset}.",
|
||||
"provisionFailed": "Не вдалося створити вихідний PIA. Спробуйте ще раз."
|
||||
},
|
||||
"tabBalancerSettings": "Налаштування балансувальника",
|
||||
"tabObservatory": "Обсерваторія",
|
||||
"observatory": {
|
||||
"autoManaged": "Спостерігачі керуються автоматично на основі ваших балансувальників. Нижче можна налаштувати, як вони опитують; відстежувані вихідні слідують за селекторами балансувальника.",
|
||||
"emptyHint": "Немає активного спостерігача з’єднань. Його буде додано автоматично під час створення балансувальника Least Ping або Least Load — чи Random / Round-robin із fallback — щоб балансувальники зі спостерігачем могли перевіряти стан вихідних перед вибором цілі.",
|
||||
"mixedLegacy": "Ця конфігурація містить і Observatory, і Burst Observatory. Xray використовує одного глобального спостерігача, тому такий застарілий змішаний стан не підтримується; збереження балансувальників нормалізує його до одного спостерігача.",
|
||||
"subjectSelector": "Відстежувані вихідні",
|
||||
"subjectSelectorDesc": "Теги вихідних, які опитує цей спостерігач. Керується автоматично на основі ваших балансувальників.",
|
||||
"probeURL": "URL проби",
|
||||
"probeURLDesc": "URL, що запитується для вимірювання кожного вихідного. Має повертати HTTP 204.",
|
||||
"probeInterval": "Інтервал проби",
|
||||
"probeIntervalDesc": "Як часто опитувати кожен вихідний, напр. 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Паралельні проби",
|
||||
"enableConcurrencyDesc": "Опитувати всі відстежувані вихідні одночасно, а не по одному. Швидше, але помітніше в мережі.",
|
||||
"destination": "Призначення проби",
|
||||
"destinationDesc": "URL, що запитується для вимірювання кожного вихідного. Має повертати HTTP 204.",
|
||||
"connectivity": "Перевірка з’єднання",
|
||||
"connectivityDesc": "Необов’язковий URL перевірки локальної мережі, використовується лише після збою призначення. Залиште порожнім, щоб пропустити.",
|
||||
"interval": "Інтервал проби",
|
||||
"intervalDesc": "Середній час між пробами для кожного вихідного, напр. 1m. Мінімум 10s.",
|
||||
"timeout": "Тайм-аут проби",
|
||||
"timeoutDesc": "Скільки чекати на пробу, перш ніж вважати її невдалою, напр. 5s.",
|
||||
"sampling": "Розмір вибірки",
|
||||
"samplingDesc": "Скільки останніх результатів проб зберігається для оцінювання кожного вихідного.",
|
||||
"httpMethod": "Метод HTTP",
|
||||
"httpMethodDesc": "Метод HTTP, що використовується для проб.",
|
||||
"deleteAlsoObservatory": "Це останній балансувальник, що використовує Observatory, тож його теж буде видалено.",
|
||||
"deleteAlsoBurst": "Це останній балансувальник, що використовує Burst Observatory, тож його теж буде видалено."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Стратегія",
|
||||
"tag": "Тег",
|
||||
"tagDuplicate": "Тег уже використовується іншим балансувальником",
|
||||
"tagPlaceholder": "унікальний тег балансувальника",
|
||||
"selector": "Селектор",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Цикл: {path} → (назад до {start})",
|
||||
"expected": "Очікуване",
|
||||
"expectedPlaceholder": "оптимальна кількість вузлів",
|
||||
"maxRtt": "Макс. RTT",
|
||||
"tolerance": "Допуск",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Шаблон тегу",
|
||||
"costValue": "Вага",
|
||||
"costRegexp": "Збіг за регулярним виразом",
|
||||
"balancerDeleteInUse": "Неможливо видалити цей балансувач — він використовується як резервний для: {names}",
|
||||
"balancerFallbackCycle": "Неможливо призначити цей балансувач резервним — це створить циклічну залежність.",
|
||||
"balancerFallbackInfo": "Трафік буде маршрутизовано через: Балансувач → Loopback → Сервер → Цільовий балансувач → Вихідне зʼєднання. Це додає додатковий хоп через сервер, що може спричинити невеликі затримки.",
|
||||
"fallbackBalancerHint": "Оберіть інший балансувач як резервний",
|
||||
"reservedPrefix": "Префікс _bl_ зарезервовано для внутрішніх loopback-об'єктів балансувальника"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Використання",
|
||||
"addOutbound": "Додати вихідний"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Увімкнути DNS",
|
||||
"enableDesc": "Увімкнути вбудований DNS-сервер",
|
||||
"tag": "Мітка вхідного DNS",
|
||||
"tagDesc": "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації.",
|
||||
"clientIp": "IP клієнта",
|
||||
"clientIpDesc": "Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів",
|
||||
"disableCache": "Вимкнути кеш",
|
||||
"disableCacheDesc": "Вимкнути кешування DNS",
|
||||
"disableFallback": "Вимкнути резервний DNS",
|
||||
"disableFallbackDesc": "Вимкнути резервні DNS-запити",
|
||||
"disableFallbackIfMatch": "Вимкнути резервний DNS при збігу",
|
||||
"disableFallbackIfMatchDesc": "Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера",
|
||||
"enableParallelQuery": "Увімкнути паралельні запити",
|
||||
"enableParallelQueryDesc": "Увімкнути паралельні DNS-запити до кількох серверів для швидшого вирішення",
|
||||
"strategy": "Стратегія запиту",
|
||||
"strategyDesc": "Загальна стратегія вирішення доменних імен",
|
||||
"add": "Додати сервер",
|
||||
"edit": "Редагувати сервер",
|
||||
"domains": "Домени",
|
||||
"expectIPs": "Очікувані IP",
|
||||
"unexpectIPs": "Неочікувані IP",
|
||||
"useSystemHosts": "Використовувати системні Hosts",
|
||||
"useSystemHostsDesc": "Використовувати файл hosts з встановленої системи",
|
||||
"serveStale": "Видавати застарілі",
|
||||
"serveStaleDesc": "Повертати застарілі результати з кешу під час фонового оновлення",
|
||||
"serveExpiredTTL": "TTL застарілих",
|
||||
"serveExpiredTTLDesc": "Термін дії (секунди) застарілих записів кешу; 0 = ніколи",
|
||||
"timeoutMs": "Тайм-аут (мс)",
|
||||
"skipFallback": "Пропустити Fallback",
|
||||
"finalQuery": "Фінальний запит",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Додати Host",
|
||||
"hostsEmpty": "Host не визначено",
|
||||
"hostsDomain": "Домен (напр. domain:example.com)",
|
||||
"hostsValues": "IP або домен — введіть і натисніть Enter",
|
||||
"usePreset": "Використати шаблон",
|
||||
"dnsPresetTitle": "Шаблони DNS",
|
||||
"dnsPresetFamily": "Сімейний",
|
||||
"clearAll": "Видалити всі",
|
||||
"clearAllTitle": "Видалити всі DNS-сервери?",
|
||||
"clearAllConfirm": "Усі DNS-сервери буде видалено зі списку. Дію не можна скасувати.",
|
||||
"dnsLeakWarning": "DNS може витікати через localhost, звичайний UDP/TCP, локальний режим DoH/DoQ, fallback-запити або EDNS client IP. Для приватності використовуйте маршрутизований DoH, фіксацію hosts і вимикайте fallback."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Додати підроблений DNS",
|
||||
"ipPool": "Підмережа IP-пулу",
|
||||
"poolSize": "Розмір пулу"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Вихідний за замовчуванням",
|
||||
"defaultOutboundDesc": "Трафік без збігу з правилами маршрутизації йде через цей вихідний (перший у списку)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Клавіатуру закрито!",
|
||||
"noResult": "❗ Немає результату!",
|
||||
"noQuery": "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!",
|
||||
"wentWrong": "❌ Щось пішло не так!",
|
||||
"noIpRecord": "❗ Немає запису IP!",
|
||||
"noInbounds": "❗ Вхідні не знайдені!",
|
||||
"unlimited": "♾ Необмежено (Скинути)",
|
||||
"add": "Додати",
|
||||
"month": "Місяць",
|
||||
"months": "Місяці",
|
||||
"days": "Дні",
|
||||
"hours": "Години",
|
||||
"minutes": "Хвилини",
|
||||
"unknown": "Невідомо",
|
||||
"inbounds": "Вхідні",
|
||||
"clients": "Клієнти",
|
||||
"offline": "🔴 Не в мережі",
|
||||
"online": "🟢 У мережі",
|
||||
"commands": {
|
||||
"unknown": "❗ Невідома команда.",
|
||||
"pleaseChoose": "👇 Будь ласка, виберіть:\r\n",
|
||||
"help": "🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\r\n\r\n",
|
||||
"start": "👋 Привіт <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Ласкаво просимо до <b>{{ .Hostname }}</b> бота керування.\r\n",
|
||||
"status": "✅ Бот в порядку!",
|
||||
"usage": "❗ Введіть текст для пошуку!",
|
||||
"getID": "🆔 Ваш ідентифікатор: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Для перезапуску Xray Core:\r\n<code>/restart</code>\r\n\r\nДля пошуку електронної пошти клієнта:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n<code>/inbound [Примітка]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "Для пошуку статистики використовуйте наступну команду:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Операція успішна!",
|
||||
"restartFailed": "❗ Помилка в операції.\r\n\r\n<code>Помилка: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core не запущений.",
|
||||
"startDesc": "Показати головне меню",
|
||||
"helpDesc": "Довідка по боту",
|
||||
"statusDesc": "Перевірити статус бота",
|
||||
"idDesc": "Показати ваш Telegram ID",
|
||||
"usageDesc": "Показати трафік клієнта: /usage email",
|
||||
"inboundDesc": "Пошук вхідних: /inbound назва (адмін)",
|
||||
"restartDesc": "Перезапустити ядро Xray (адмін)",
|
||||
"clearallDesc": "Скинути трафік усіх клієнтів (адмін)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "Навантаження ЦП {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Помилка під час вибору користувача!",
|
||||
"userSaved": "✅ Користувача Telegram збережено.",
|
||||
"loginSuccess": "✅ Успішно ввійшли в панель\r\n",
|
||||
"loginFailed": "❗️ Помилка входу в панель.\r\n",
|
||||
"report": "🕰 Заплановані звіти: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Дата й час: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Хост: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 3X-UI Версія: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray Версія: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IP:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Статус: {{ .State }}\r\n",
|
||||
"username": "👤 Ім'я користувача: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Причина: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Час: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Вхідний: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Порт: {{ .Port }}\r\n",
|
||||
"expire": "📅 Дата закінчення: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Термін дії: {{ .Time }}\r\n",
|
||||
"active": "💡 Активний: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Увімкнено: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Стан підключення: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Був(ла) онлайн: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Завантаження: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Завантаження: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Усього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Користувач Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Вичерпано кількість {{ .Type }} count:\r\n",
|
||||
"onlinesCount": "🌐 Онлайн-клієнти: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Вимкнено: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Скоро вичерпається: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Час резервного копіювання: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Так",
|
||||
"no": "❌ Ні",
|
||||
"received_email": "📧📥 Електронна пошта оновлена.",
|
||||
"received_comment": "💬📥 Коментар оновлено.",
|
||||
"email_prompt": "📧 Стандартний email: {{ .ClientEmail }}\n\nВведіть ваш email.",
|
||||
"comment_prompt": "💬 Стандартний коментар: {{ .ClientComment }}\n\nВведіть ваш коментар.",
|
||||
"cancel": "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄",
|
||||
"error_add_client": "⚠️ Помилка:\n\n {{ .error }}",
|
||||
"using_default_value": "Гаразд, залишу значення за замовчуванням. 😊",
|
||||
"incorrect_input": "Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫",
|
||||
"AreYouSure": "Ви впевнені? 🤔",
|
||||
"SuccessResetTraffic": "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно",
|
||||
"FailedResetTraffic": "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠️ Помилка: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Процес скидання трафіку завершено для всіх клієнтів.",
|
||||
"eventOutboundDown": "Вихідне з'єднання {{ .Tag }} НЕДОСТУПНЕ",
|
||||
"eventOutboundUp": "Вихідне з'єднання {{ .Tag }} ДОСТУПНЕ",
|
||||
"eventErrorDetail": "Помилка: {{ .Error }}",
|
||||
"eventDelayDetail": "Затримка: {{ .Delay }} мс",
|
||||
"eventXrayCrash": "Стався збій Xray",
|
||||
"eventXrayCrashError": "Помилка: {{ .Error }}",
|
||||
"eventNodeDown": "Вузол {{ .Name }} НЕДОСТУПНИЙ",
|
||||
"eventNodeUp": "Вузол {{ .Name }} ДОСТУПНИЙ",
|
||||
"eventLoginFallback": "Невдала спроба входу з {{ .Source }}",
|
||||
"memoryThreshold": "Використання пам'яті {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Закрити клавіатуру",
|
||||
"cancel": "❌ Скасувати",
|
||||
"cancelReset": "❌ Скасувати скидання",
|
||||
"cancelIpLimit": "❌ Скасувати обмеження IP",
|
||||
"confirmResetTraffic": "✅ Підтвердити скидання трафіку?",
|
||||
"confirmClearIps": "✅ Підтвердити очищення IP-адрес?",
|
||||
"confirmRemoveTGUser": "✅ Підтвердити видалення користувача Telegram?",
|
||||
"confirmToggle": "✅ Підтвердити ввімкнути/вимкнути користувача?",
|
||||
"dbBackup": "Отримати резервну копію БД",
|
||||
"serverUsage": "Використання сервера",
|
||||
"getInbounds": "Отримати вхідні",
|
||||
"depleteSoon": "Скоро вичерпати",
|
||||
"clientUsage": "Отримати використання",
|
||||
"onlines": "Онлайн-клієнти",
|
||||
"commands": "Команди",
|
||||
"refresh": "🔄 Оновити",
|
||||
"clearIPs": "❌ Очистити IP-адреси",
|
||||
"removeTGUser": "❌ Видалити користувача Telegram",
|
||||
"selectTGUser": "👤 Виберіть користувача Telegram",
|
||||
"selectOneTGUser": "👤 Виберіть користувача Telegram:",
|
||||
"resetTraffic": "📈 Скинути трафік",
|
||||
"resetExpire": "📅 Змінити термін дії",
|
||||
"ipLog": "🔢 IP журнал",
|
||||
"ipLimit": "🔢 IP Ліміт",
|
||||
"setTGUser": "👤 Встановити користувача Telegram",
|
||||
"toggle": "🔘 Увімкнути / Вимкнути",
|
||||
"custom": "🔢 Своє",
|
||||
"confirmNumber": "✅ Підтвердити: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Підтвердити додавання: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Ліміт трафіку",
|
||||
"getBanLogs": "Отримати журнали заборон",
|
||||
"allClients": "Всі Клієнти",
|
||||
"addClient": "Додати клієнта",
|
||||
"submitDisable": "Надіслати як вимкнено ☑️",
|
||||
"submitEnable": "Надіслати як увімкнено ✅",
|
||||
"use_default": "🏷️ Використати типове",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Коментар",
|
||||
"ResetAllTraffics": "Скинути весь трафік",
|
||||
"SortedTrafficUsageReport": "Відсортований звіт про використання трафіку"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Операція успішна!",
|
||||
"errorOperation": "❗ Помилка в роботі.",
|
||||
"getInboundsFailed": "❌ Не вдалося отримати вхідні повідомлення.",
|
||||
"getClientsFailed": "❌ Не вдалося отримати клієнтів.",
|
||||
"canceled": "❌ {{ .Email }}: Операцію скасовано.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}: Клієнт успішно оновлено.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}: IP-адреси успішно оновлено.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}: Трафік скинуто успішно.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: Ліміт трафіку успішно збережено.",
|
||||
"expireResetSuccess": "✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії.",
|
||||
"resetIpSuccess": "✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено.",
|
||||
"clearIpSuccess": "✅ {{ .Email }}: IP успішно очищено.",
|
||||
"getIpLog": "✅ {{ .Email }}: Отримати IP-журнал.",
|
||||
"getUserInfo": "✅ {{ .Email }}: Отримати інформацію про користувача Telegram.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}: Користувача Telegram видалено успішно.",
|
||||
"enableSuccess": "✅ {{ .Email }}: Увімкнути успішно.",
|
||||
"disableSuccess": "✅ {{ .Email }}: Успішно вимкнено.",
|
||||
"askToAddUserId": "Вашу конфігурацію не знайдено!\r\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\r\n\r\nВаш ідентифікатор користувача: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Виберіть клієнта для Вхідного {{ .Inbound }}",
|
||||
"chooseInbound": "Виберіть Вхідний"
|
||||
}
|
||||
"noIpRecord": "❗ Немає запису IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Статус",
|
||||
"labelOutbound": "Вихідне з'єднання",
|
||||
"labelNode": "Вузол",
|
||||
"labelError": "Помилка",
|
||||
"labelDelay": "Затримка",
|
||||
"labelUsername": "Ім'я користувача",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Причина",
|
||||
"labelSource": "Джерело",
|
||||
"statusCrashed": "ЗБІЙ",
|
||||
"statusHigh": "ВИСОКЕ",
|
||||
"statusSuccess": "УСПІШНО",
|
||||
"statusFailed": "НЕВДАЛО",
|
||||
"statusDown": "НЕДОСТУПНО",
|
||||
"statusUp": "ДОСТУПНО"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "Thành công",
|
||||
"lastOnline": "Lần online gần nhất",
|
||||
"lastSubFetch": "Lần tải gói đăng ký gần nhất",
|
||||
"getVersion": "Lấy phiên bản",
|
||||
"install": "Cài đặt",
|
||||
"clients": "Các khách hàng",
|
||||
"usage": "Sử dụng",
|
||||
"twoFactorCode": "Mã",
|
||||
"remained": "Còn lại",
|
||||
"security": "Bảo vệ",
|
||||
"emptyDnsDesc": "Không có máy chủ DNS nào được thêm.",
|
||||
"emptyFakeDnsDesc": "Không có máy chủ Fake DNS nào được thêm.",
|
||||
"emptyBalancersDesc": "Không có bộ cân bằng tải nào được thêm.",
|
||||
"somethingWentWrong": "Đã xảy ra lỗi",
|
||||
"subscription": {
|
||||
"title": "Thông tin đăng ký",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "Đã tải lên",
|
||||
"expiry": "Hết hạn",
|
||||
"totalQuota": "Tổng hạn mức",
|
||||
"individualLinks": "Liên kết riêng lẻ",
|
||||
"active": "Hoạt động",
|
||||
"inactive": "Không hoạt động",
|
||||
"unlimited": "Không giới hạn",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "Dừng",
|
||||
"restartXray": "Khởi động lại",
|
||||
"xraySwitch": "Phiên bản",
|
||||
"xrayUpdates": "Cập nhật Xray",
|
||||
"xraySwitchClickDesk": "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại.",
|
||||
"updatePanel": "Cập nhật Panel",
|
||||
"panelUpdateDesc": "Điều này sẽ cập nhật 3X-UI lên bản phát hành mới nhất và khởi động lại dịch vụ panel.",
|
||||
"currentPanelVersion": "Phiên bản panel hiện tại",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "Tải xuống",
|
||||
"sent": "Đã gửi",
|
||||
"received": "Đã nhận",
|
||||
"xraySwitchVersionDialog": "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?",
|
||||
"xraySwitchVersionDialogDesc": "Hành động này sẽ thay đổi phiên bản Xray thành #version#.",
|
||||
"xraySwitchVersionPopover": "Xray đã được cập nhật thành công",
|
||||
"panelUpdateDialog": "Bạn có chắc muốn cập nhật panel không?",
|
||||
"panelUpdateDialogDesc": "Điều này sẽ cập nhật 3X-UI lên #version# và khởi động lại dịch vụ panel.",
|
||||
"panelUpdateStartedPopover": "Bắt đầu cập nhật panel",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "Bản cập nhật không hoàn tất thành công. Hãy kiểm tra nhật ký máy chủ, hoặc chạy 'x-ui update' từ dòng lệnh.",
|
||||
"panelUpdateUnknownTitle": "Không thể xác nhận việc cập nhật đã hoàn tất",
|
||||
"panelUpdateUnknownDesc": "Panel không báo cáo kết quả kịp thời. Hãy tải lại trang để kiểm tra phiên bản hiện tại, hoặc kiểm tra nhật ký máy chủ.",
|
||||
"geofileUpdateDialog": "Bạn có chắc chắn muốn cập nhật geofile không?",
|
||||
"geofileUpdateDialogDesc": "Hành động này sẽ cập nhật tệp #filename#.",
|
||||
"geofilesUpdateDialogDesc": "Thao tác này sẽ cập nhật tất cả các tập tin.",
|
||||
"geofilesUpdateAll": "Cập nhật tất cả",
|
||||
"geofileUpdatePopover": "Geofile đã được cập nhật thành công",
|
||||
"geodataTitle": "Tự động cập nhật Geodata",
|
||||
"geodataHint": "Xray tải các tệp này theo lịch và nạp lại nóng mà không cần khởi động lại. URL phải là HTTPS. Mỗi tệp phải tồn tại sẵn trong thư mục bin thì Xray mới có thể cập nhật.",
|
||||
"geodataCron": "Lịch cập nhật (cron)",
|
||||
"geodataOutbound": "Tải qua outbound (tùy chọn)",
|
||||
"geodataFile": "Tên tệp",
|
||||
"geodataAddFile": "Thêm tệp",
|
||||
"geodataSaveRestart": "Lưu và khởi động lại Xray",
|
||||
"geodataConfirmTitle": "Lưu cài đặt geodata?",
|
||||
"geodataConfirmContent": "Mẫu cấu hình Xray sẽ được cập nhật và Xray sẽ khởi động lại.",
|
||||
"geodataInvalidUrl": "Mỗi tệp cần một URL HTTPS.",
|
||||
"geodataInvalidFile": "Tên tệp phải là tên đơn giản, ví dụ geosite_custom.dat (không chứa đường dẫn).",
|
||||
"geodataInvalidCron": "Cron phải có 5 trường, ví dụ 0 4 * * *",
|
||||
"geodataEmpty": "Chưa cấu hình tệp nào. Trong quy tắc định tuyến, tham chiếu tệp dạng ext:geosite_custom.dat:category.",
|
||||
"dontRefresh": "Đang tiến hành cài đặt, vui lòng không làm mới trang này.",
|
||||
"logs": "Nhật ký",
|
||||
"accessLogs": "Nhật ký truy cập",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "Đường dẫn cơ sở mặc định \"/\" đã quá phổ biến — đổi sang đường dẫn ngẫu nhiên.",
|
||||
"warnDefaultSubPath": "Đường dẫn đăng ký mặc định \"/sub/\" đã quá phổ biến — đổi nó.",
|
||||
"warnDefaultJsonPath": "Đường dẫn đăng ký JSON mặc định \"/json/\" đã quá phổ biến — đổi nó.",
|
||||
"TGBotSettings": "Bot Telegram",
|
||||
"panelListeningIP": "IP Nghe của bảng điều khiển",
|
||||
"panelListeningIPDesc": "Mặc định để trống để nghe tất cả các IP.",
|
||||
"panelListeningDomain": "Tên miền của nghe bảng điều khiển",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "Mật khẩu hiện tại",
|
||||
"newUsername": "Tên người dùng mới",
|
||||
"newPassword": "Mật khẩu mới",
|
||||
"telegramBotEnable": "Bật Bot Telegram",
|
||||
"telegramBotEnableDesc": "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram",
|
||||
"telegramToken": "Token Telegram",
|
||||
"telegramTokenDesc": "Bạn phải nhận token từ quản lý bot Telegram {'@'}botfather",
|
||||
"telegramProxy": "SOCKS Proxy",
|
||||
"telegramProxyDesc": "Nếu bạn cần socks5 proxy để kết nối với Telegram. Điều chỉnh cài đặt của nó theo hướng dẫn.",
|
||||
"telegramAPIServer": "Máy chủ API Telegram",
|
||||
"telegramAPIServerDesc": "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định.",
|
||||
"telegramChatId": "Chat ID Telegram của quản trị viên",
|
||||
"telegramChatIdDesc": "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng {'@'}userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn.",
|
||||
"telegramNotifyTime": "Thời gian thông báo của bot Telegram",
|
||||
"telegramNotifyTimeDesc": "Tần suất bot Telegram gửi báo cáo định kỳ. Chọn một khoảng thời gian có sẵn, hoặc chọn Tùy chỉnh để nhập biểu thức crontab.",
|
||||
"notifyTime": {
|
||||
"every": "@every — lặp lại theo khoảng thời gian",
|
||||
"hourly": "@hourly — mỗi giờ",
|
||||
"daily": "@daily — mỗi ngày lúc 00:00",
|
||||
"weekly": "@weekly — mỗi tuần",
|
||||
"monthly": "@monthly — mỗi tháng",
|
||||
"custom": "Tùy chỉnh (crontab)",
|
||||
"seconds": "Giây",
|
||||
"minutes": "Phút",
|
||||
"hours": "Giờ",
|
||||
"interval": "Khoảng thời gian",
|
||||
"unit": "Đơn vị"
|
||||
},
|
||||
"tgNotifyBackup": "Sao lưu Cơ sở dữ liệu",
|
||||
"tgNotifyBackupDesc": "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo.",
|
||||
"tgNotifyLogin": "Thông báo Đăng nhập",
|
||||
"tgNotifyLoginDesc": "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn.",
|
||||
"sessionMaxAge": "Thời gian tối đa của phiên",
|
||||
"sessionMaxAgeDesc": "Thời gian của phiên đăng nhập (đơn vị: phút)",
|
||||
"expireTimeDiff": "Ngưỡng hết hạn cho thông báo",
|
||||
"expireTimeDiffDesc": "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)",
|
||||
"trafficDiff": "Ngưỡng lưu lượng cho thông báo",
|
||||
"trafficDiffDesc": "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)",
|
||||
"tgNotifyCpu": "Ngưỡng cảnh báo tỷ lệ CPU",
|
||||
"tgNotifyCpuDesc": "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)",
|
||||
"timeZone": "Múi giờ",
|
||||
"timeZoneDesc": "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này.",
|
||||
"subSettings": "Gói đăng ký",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "Mã hóa các cấu hình được trả về trong gói đăng ký",
|
||||
"subURI": "URI proxy trung gian",
|
||||
"subURIDesc": "URL cơ sở đầy đủ (scheme://tênmiền[:cổng]/đường-dẫn/) cho liên kết đăng ký và mã QR, dùng thay cho Tên miền/Cổng gói đăng ký. Hãy đặt giá trị này khi đăng ký được truy cập qua proxy trung gian hoặc một tên miền/cổng khác với các mục trên.",
|
||||
"externalTrafficInformEnable": "Thông báo giao thông bên ngoài",
|
||||
"externalTrafficInformEnableDesc": "Thông báo API ngoài mỗi khi cập nhật lưu lượng.",
|
||||
"externalTrafficInformURI": "URI thông báo lưu lượng truy cập bên ngoài",
|
||||
"externalTrafficInformURIDesc": "Cập nhật lưu lượng truy cập được gửi tới URI này.",
|
||||
"restartXrayOnClientDisable": "Khởi Động Lại Xray Sau Khi Tự Động Vô Hiệu Hóa",
|
||||
"restartXrayOnClientDisableDesc": "Khi người dùng bị vô hiệu hóa tự động do hết hạn hoặc chạm giới hạn lưu lượng, hãy khởi động lại Xray.",
|
||||
"fragment": "Sự phân mảnh",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Cài đặt Noises",
|
||||
"trustedProxyCidrs": "CIDR proxy tin cậy",
|
||||
"trustedProxyCidrsDesc": "IPs/CIDRs cách nhau bằng dấu phẩy được phép đặt header host, proto và IP client chuyển tiếp.",
|
||||
"ldap": {
|
||||
"enable": "Bật đồng bộ LDAP",
|
||||
"host": "LDAP host",
|
||||
"port": "Cổng LDAP",
|
||||
"useTls": "Dùng TLS (LDAPS)",
|
||||
"skipTlsVerify": "Bỏ qua xác minh chứng chỉ TLS",
|
||||
"skipTlsVerifyDesc": "Không an toàn — tắt xác thực chứng chỉ máy chủ. Chỉ dùng với CA nội bộ/không đáng tin.",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "Đã cấu hình; để trống để giữ mật khẩu hiện tại.",
|
||||
"passwordUnconfigured": "Chưa cấu hình.",
|
||||
"passwordPlaceholder": "Đã cấu hình — nhập giá trị mới để thay thế",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "Bộ lọc user",
|
||||
"userAttr": "Thuộc tính user (username/email)",
|
||||
"vlessField": "Thuộc tính flag VLESS",
|
||||
"flagField": "Thuộc tính flag chung (tùy chọn)",
|
||||
"flagFieldDesc": "Nếu đặt, sẽ ghi đè VLESS flag — ví dụ shadowInactive.",
|
||||
"truthyValues": "Giá trị truthy",
|
||||
"truthyValuesDesc": "Cách nhau bằng dấu phẩy; mặc định: true,1,yes,on",
|
||||
"invertFlag": "Đảo flag",
|
||||
"invertFlagDesc": "Bật khi thuộc tính có nghĩa «đã tắt» (ví dụ shadowInactive).",
|
||||
"syncSchedule": "Lịch đồng bộ",
|
||||
"syncScheduleDesc": "Chuỗi kiểu cron, ví dụ @every 1m",
|
||||
"inboundTags": "Tag inbound",
|
||||
"inboundTagsDesc": "Các inbound mà đồng bộ LDAP có thể tự tạo hoặc tự xóa client.",
|
||||
"noInbounds": "Không tìm thấy inbound. Hãy tạo một inbound trong mục Inbound trước.",
|
||||
"autoCreate": "Tự động tạo client",
|
||||
"autoDelete": "Tự động xóa client",
|
||||
"defaultTotalGb": "Tổng mặc định (GB)",
|
||||
"defaultExpiryDays": "Hết hạn mặc định (ngày)",
|
||||
"defaultIpLimit": "Giới hạn IP mặc định"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "Thêm mask TCP/UDP của Xray finalmask và tham số QUIC vào mỗi hồ sơ Xray JSON được tạo. Yêu cầu ứng dụng hỗ trợ đăng ký Xray JSON và lõi Xray mới.",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "Mux Cài đặt",
|
||||
"direct": "Kết nối trực tiếp",
|
||||
"directDesc": "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể.",
|
||||
"notifications": "Thông báo",
|
||||
"certs": "Chứng chỉ",
|
||||
"externalTraffic": "Lưu lượng bên ngoài",
|
||||
"dateAndTime": "Ngày và giờ",
|
||||
"proxyAndServer": "Proxy và máy chủ",
|
||||
"intervals": "Khoảng thời gian",
|
||||
"information": "Thông tin",
|
||||
"profile": "Hồ sơ",
|
||||
"language": "Ngôn ngữ",
|
||||
"telegramBotLanguage": "Ngôn ngữ của Bot Telegram",
|
||||
"security": {
|
||||
"admin": "Thông tin đăng nhập quản trị viên",
|
||||
"twoFactor": "Xác thực hai yếu tố",
|
||||
"twoFactorEnable": "Bật 2FA",
|
||||
"twoFactorEnableDesc": "Thêm một lớp bảo mật bổ sung để tăng cường an toàn.",
|
||||
"twoFactorModalSetTitle": "Bật xác thực hai yếu tố",
|
||||
"twoFactorModalDeleteTitle": "Tắt xác thực hai yếu tố",
|
||||
"twoFactorModalSteps": "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:",
|
||||
"twoFactorModalFirstStep": "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng",
|
||||
"twoFactorModalSecondStep": "2. Nhập mã từ ứng dụng",
|
||||
"twoFactorModalRemoveStep": "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố.",
|
||||
"twoFactorModalChangeCredentialsTitle": "Thay đổi thông tin xác thực",
|
||||
"twoFactorModalChangeCredentialsStep": "Nhập mã từ ứng dụng để thay đổi thông tin xác thực quản trị viên.",
|
||||
"twoFactorModalSetSuccess": "Xác thực hai yếu tố đã được thiết lập thành công",
|
||||
"twoFactorModalDeleteSuccess": "Xác thực hai yếu tố đã được xóa thành công",
|
||||
"twoFactorModalError": "Mã sai",
|
||||
"show": "Hiển thị",
|
||||
"hide": "Ẩn",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "Lỗi khi lấy lưu lượng truy cập đi",
|
||||
"resetOutboundTrafficError": "Lỗi khi đặt lại lưu lượng truy cập đi"
|
||||
},
|
||||
"smtpSettings": "Cài đặt SMTP",
|
||||
"smtpEnable": "Bật thông báo qua email",
|
||||
"smtpEnableDesc": "Bật thông báo qua email bằng SMTP",
|
||||
"smtpHost": "Máy chủ SMTP",
|
||||
"smtpHostDesc": "Tên máy chủ SMTP (ví dụ: smtp.gmail.com)",
|
||||
"smtpPort": "Cổng SMTP",
|
||||
"smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)",
|
||||
"smtpUsername": "Tên đăng nhập SMTP",
|
||||
"smtpUsernameDesc": "Tên đăng nhập xác thực SMTP",
|
||||
"smtpFrom": "Địa chỉ người gửi (From)",
|
||||
"smtpFromDesc": "Địa chỉ dùng trong tiêu đề From của email. Để trống để dùng tên người dùng.",
|
||||
"smtpFromName": "Tên người gửi (From)",
|
||||
"smtpFromNameDesc": "Tên hiển thị tùy chọn trước địa chỉ trong tiêu đề From.",
|
||||
"smtpPassword": "Mật khẩu SMTP",
|
||||
"smtpPasswordDesc": "Mật khẩu xác thực SMTP",
|
||||
"smtpTo": "Người nhận",
|
||||
"smtpToDesc": "Các địa chỉ email người nhận, phân cách bằng dấu phẩy",
|
||||
"emailSettings": "Email",
|
||||
"emailNotifications": "Thông báo",
|
||||
"smtpEventBusNotify": "Thông báo sự kiện qua email",
|
||||
"smtpEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua email",
|
||||
"tgEventBusNotify": "Thông báo sự kiện qua Telegram",
|
||||
"tgEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua Telegram",
|
||||
"testSmtp": "Gửi email thử nghiệm",
|
||||
"testTgBot": "Gửi tin nhắn thử nghiệm",
|
||||
"eventGroupOutbound": "Outbound",
|
||||
"eventGroupXray": "Xray Core",
|
||||
"eventGroupSystem": "Hệ thống",
|
||||
"eventGroupSecurity": "Bảo mật",
|
||||
"eventGroupNode": "Node",
|
||||
"eventOutboundDown": "Ngừng hoạt động",
|
||||
"eventOutboundUp": "Hoạt động",
|
||||
"eventXrayCrash": "Sự cố",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "Hoạt động",
|
||||
"eventCPUHigh": "CPU cao (%)",
|
||||
"requestFailed": "Yêu cầu thất bại",
|
||||
"smtpEncryption": "Mã hóa",
|
||||
"smtpEncryptionDesc": "Phương thức mã hóa kết nối SMTP",
|
||||
"smtpEncryptionNone": "Không (văn bản thuần)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS (ngầm định)",
|
||||
"smtpStageConnect": "Kết nối",
|
||||
"smtpStageAuth": "Xác thực",
|
||||
"smtpStageSend": "Gửi",
|
||||
"smtpTestSuccess": "Đã gửi email thử nghiệm thành công",
|
||||
"smtpHostNotConfigured": "Chưa cấu hình máy chủ SMTP",
|
||||
"smtpNoRecipients": "Chưa cấu hình người nhận",
|
||||
"smtpFromNotConfigured": "Chưa cấu hình địa chỉ người gửi SMTP",
|
||||
"eventLoginAttempt": "Lần thử đăng nhập",
|
||||
"telegramTokenConfigured": "Đã cấu hình; để trống để giữ token hiện tại.",
|
||||
"telegramTokenPlaceholder": "Đã cấu hình - nhập token mới để thay thế",
|
||||
"smtpPasswordConfigured": "Đã cấu hình; để trống để giữ mật khẩu hiện tại.",
|
||||
"smtpPasswordPlaceholder": "Đã cấu hình - nhập mật khẩu mới để thay thế",
|
||||
"smtpNotInitialized": "SMTP chưa được khởi tạo",
|
||||
"tgBotNotEnabled": "Bot Telegram chưa được bật",
|
||||
"tgTestFailed": "Thử nghiệm Telegram thất bại",
|
||||
"tgTestSuccess": "Đã gửi tin nhắn thử nghiệm tới Telegram",
|
||||
"tgBotNotRunning": "Bot Telegram không hoạt động",
|
||||
"smtpErrorAuth": "Xác thực thất bại — kiểm tra tên đăng nhập và mật khẩu",
|
||||
"smtpErrorStarttls": "Máy chủ yêu cầu STARTTLS — thay đổi kiểu mã hóa",
|
||||
"smtpErrorTls": "Máy chủ yêu cầu TLS — thay đổi kiểu mã hóa",
|
||||
"smtpErrorRefused": "Kết nối bị từ chối — kiểm tra máy chủ và cổng",
|
||||
"smtpErrorTimeout": "Hết thời gian kết nối — không thể truy cập máy chủ",
|
||||
"smtpErrorRelay": "Máy chủ từ chối gửi từ địa chỉ này",
|
||||
"smtpErrorEof": "Kết nối đã bị máy chủ đóng",
|
||||
"smtpErrorUnknown": "Lỗi SMTP: {{ .Error }}",
|
||||
"eventMemoryHigh": "Sử dụng bộ nhớ cao (%)",
|
||||
"remarkTemplate": "Mẫu ghi chú",
|
||||
"remarkTemplateDesc": "Khi được đặt, mục này thay thế mô hình ghi chú cho mọi liên kết đăng ký — hãy viết định dạng riêng của bạn bằng các token biến (dùng nút để chèn chúng). Để trống để dùng mô hình ở trên.",
|
||||
@@ -1483,8 +1311,6 @@
|
||||
"directdomains": "Tên Miền Trực Tiếp",
|
||||
"ipv4Routing": "Định tuyến IPv4",
|
||||
"ipv4RoutingDesc": "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4.",
|
||||
"Template": "Mẫu Cấu hình Xray",
|
||||
"TemplateDesc": "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này.",
|
||||
"FreedomStrategy": "Cấu hình Chiến lược cho Giao thức Freedom",
|
||||
"FreedomStrategyDesc": "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom.",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "Cân bằng",
|
||||
"balancerTagRequired": "Tag là bắt buộc",
|
||||
"balancerSelectorRequired": "Chọn ít nhất một outbound",
|
||||
"balancerLive": "Mục tiêu hiện tại",
|
||||
"balancerOverride": "Ghi đè đích",
|
||||
"balancerOverridePh": "Tự động (chiến lược)",
|
||||
"balancerLiveRefresh": "Làm mới trạng thái bộ cân bằng tải",
|
||||
"balancerNotRunning": "Bộ cân bằng này không hoạt động trong Xray đang chạy — hãy lưu thay đổi hoặc khởi động Xray trước",
|
||||
"routeTester": "Kiểm tra tuyến đường",
|
||||
"routeTesterDesc": "Hỏi Xray đang chạy outbound nào sẽ xử lý kết nối. Không có lưu lượng nào được gửi — quyết định đến thẳng từ công cụ định tuyến trực tiếp.",
|
||||
"routeTesterDest": "Tên miền hoặc IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "qua bộ cân bằng tải",
|
||||
"routeTesterDefaultOutbound": "Không có quy tắc định tuyến nào khớp — lưu lượng đến outbound mặc định (đầu tiên).",
|
||||
"Routings": "Quy tắc định tuyến",
|
||||
"completeTemplate": "Tất cả",
|
||||
"logLevel": "Mức đăng nhập",
|
||||
"logLevelDesc": "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại.",
|
||||
"accessLog": "Nhật ký truy cập",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "Nguồn",
|
||||
"dest": "Đích",
|
||||
"inbound": "Vào",
|
||||
"balancer": "Cân bằng",
|
||||
"useComma": "Các mục được phân tách bằng dấu phẩy"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "Máy chủ này đã có trong danh sách outbound. Dùng {reset} để gia hạn khóa.",
|
||||
"provisionFailed": "Không tạo được outbound PIA. Hãy thử lại."
|
||||
},
|
||||
"tabBalancerSettings": "Cài đặt Balancer",
|
||||
"tabObservatory": "Observatory",
|
||||
"observatory": {
|
||||
"autoManaged": "Observer được quản lý tự động từ các balancer của bạn. Điều chỉnh cách chúng dò ở bên dưới; các outbound được theo dõi sẽ tuân theo selector của balancer.",
|
||||
"emptyHint": "Không có observer kết nối nào đang hoạt động. Một observer sẽ được thêm tự động khi bạn tạo balancer Least Ping hoặc Least Load — hoặc balancer Random / Round-robin có fallback — để các balancer dùng observer có thể kiểm tra sức khỏe outbound trước khi chọn mục tiêu.",
|
||||
"mixedLegacy": "Cấu hình này có cả Observatory và Burst Observatory. Xray chỉ dùng một observer toàn cục, nên trạng thái hỗn hợp cũ này không được hỗ trợ; khi lưu balancer, nó sẽ được chuẩn hóa về một observer.",
|
||||
"subjectSelector": "Outbound được theo dõi",
|
||||
"subjectSelectorDesc": "Các thẻ outbound mà observer này dò. Được quản lý tự động từ các balancer của bạn.",
|
||||
"probeURL": "URL dò",
|
||||
"probeURLDesc": "URL được yêu cầu để đo mỗi outbound. Phải trả về HTTP 204.",
|
||||
"probeInterval": "Khoảng thời gian dò",
|
||||
"probeIntervalDesc": "Tần suất dò mỗi outbound, ví dụ 30s, 1m, 2h45m.",
|
||||
"enableConcurrency": "Dò đồng thời",
|
||||
"enableConcurrencyDesc": "Dò tất cả outbound được theo dõi cùng lúc thay vì lần lượt. Nhanh hơn nhưng dễ bị phát hiện trên mạng hơn.",
|
||||
"destination": "Đích dò",
|
||||
"destinationDesc": "URL được yêu cầu để đo mỗi outbound. Phải trả về HTTP 204.",
|
||||
"connectivity": "Kiểm tra kết nối",
|
||||
"connectivityDesc": "URL kiểm tra mạng cục bộ tùy chọn, chỉ thử sau khi đích thất bại. Để trống để bỏ qua.",
|
||||
"interval": "Khoảng thời gian dò",
|
||||
"intervalDesc": "Thời gian trung bình giữa các lần dò cho mỗi outbound, ví dụ 1m. Tối thiểu 10s.",
|
||||
"timeout": "Thời gian chờ dò",
|
||||
"timeoutDesc": "Thời gian chờ một lần dò trước khi coi là thất bại, ví dụ 5s.",
|
||||
"sampling": "Số mẫu",
|
||||
"samplingDesc": "Số kết quả dò gần đây được giữ để chấm điểm mỗi outbound.",
|
||||
"httpMethod": "Phương thức HTTP",
|
||||
"httpMethodDesc": "Phương thức HTTP dùng cho việc dò.",
|
||||
"deleteAlsoObservatory": "Đây là balancer cuối cùng dùng Observatory, nên nó cũng sẽ bị xóa.",
|
||||
"deleteAlsoBurst": "Đây là balancer cuối cùng dùng Burst Observatory, nên nó cũng sẽ bị xóa."
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "Chiến lược",
|
||||
"tag": "Tag",
|
||||
"tagDuplicate": "Tag đã được dùng bởi balancer khác",
|
||||
"tagPlaceholder": "tag balancer duy nhất",
|
||||
"selector": "Selector",
|
||||
"fallback": "Fallback",
|
||||
"cycleTooltip": "Vòng lặp: {path} → (quay lại {start})",
|
||||
"expected": "Kỳ vọng",
|
||||
"expectedPlaceholder": "số node tối ưu",
|
||||
"maxRtt": "RTT tối đa",
|
||||
"tolerance": "Dung sai",
|
||||
"baselines": "Baselines",
|
||||
"costs": "Costs",
|
||||
"costMatch": "Mẫu thẻ",
|
||||
"costValue": "Trọng số",
|
||||
"costRegexp": "Khớp biểu thức chính quy",
|
||||
"balancerDeleteInUse": "Không thể xóa load balancer này — nó được sử dụng làm dự phòng cho: {names}",
|
||||
"balancerFallbackCycle": "Không thể đặt load balancer này làm dự phòng — sẽ tạo ra phụ thuộc vòng.",
|
||||
"balancerFallbackInfo": "Lưu lượng sẽ được định tuyến qua: Load Balancer → Loopback → Máy chủ → Load Balancer mục tiêu → kết nối ra ngoài. Điều này thêm một hop bổ sung qua máy chủ, có thể gây ra độ trễ nhẹ.",
|
||||
"fallbackBalancerHint": "Chọn một load balancer khác làm dự phòng",
|
||||
"reservedPrefix": "Tiền tố _bl_ được dành riêng cho các đối tượng loopback nội bộ của bộ cân bằng"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "Sử dụng",
|
||||
"addOutbound": "Thêm outbound"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "Kích hoạt DNS",
|
||||
"enableDesc": "Kích hoạt máy chủ DNS tích hợp",
|
||||
"tag": "Thẻ gửi đến DNS",
|
||||
"tagDesc": "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến.",
|
||||
"clientIp": "IP khách hàng",
|
||||
"clientIpDesc": "Được sử dụng để thông báo cho máy chủ về vị trí IP được chỉ định trong các truy vấn DNS",
|
||||
"disableCache": "Tắt bộ nhớ đệm",
|
||||
"disableCacheDesc": "Tắt bộ nhớ đệm DNS",
|
||||
"disableFallback": "Tắt Fallback",
|
||||
"disableFallbackDesc": "Tắt các truy vấn DNS Fallback",
|
||||
"disableFallbackIfMatch": "Tắt Fallback Nếu Khớp",
|
||||
"disableFallbackIfMatchDesc": "Tắt các truy vấn DNS Fallback khi danh sách tên miền khớp của máy chủ DNS được kích hoạt",
|
||||
"enableParallelQuery": "Bật Truy vấn Song song",
|
||||
"enableParallelQueryDesc": "Bật truy vấn DNS song song đến nhiều máy chủ để phân giải nhanh hơn",
|
||||
"strategy": "Chiến lược truy vấn",
|
||||
"strategyDesc": "Chiến lược tổng thể để phân giải tên miền",
|
||||
"add": "Thêm máy chủ",
|
||||
"edit": "Chỉnh sửa máy chủ",
|
||||
"domains": "Tên miền",
|
||||
"expectIPs": "Các IP Dự Kiến",
|
||||
"unexpectIPs": "IP không mong muốn",
|
||||
"useSystemHosts": "Sử dụng Hosts hệ thống",
|
||||
"useSystemHostsDesc": "Sử dụng file hosts từ hệ thống đã cài đặt",
|
||||
"serveStale": "Phục vụ kết quả hết hạn",
|
||||
"serveStaleDesc": "Trả về kết quả cache đã hết hạn trong khi làm mới ở chế độ nền",
|
||||
"serveExpiredTTL": "TTL hết hạn",
|
||||
"serveExpiredTTLDesc": "Thời gian hiệu lực (giây) của các mục cache hết hạn; 0 = không bao giờ hết hạn",
|
||||
"timeoutMs": "Thời gian chờ (ms)",
|
||||
"skipFallback": "Bỏ qua Fallback",
|
||||
"finalQuery": "Truy vấn cuối",
|
||||
"hosts": "Hosts",
|
||||
"hostsAdd": "Thêm Host",
|
||||
"hostsEmpty": "Chưa có Host nào",
|
||||
"hostsDomain": "Tên miền (vd. domain:example.com)",
|
||||
"hostsValues": "IP hoặc tên miền — nhập và nhấn Enter",
|
||||
"usePreset": "Dùng mẫu",
|
||||
"dnsPresetTitle": "Mẫu DNS",
|
||||
"dnsPresetFamily": "Gia đình",
|
||||
"clearAll": "Xóa tất cả",
|
||||
"clearAllTitle": "Xóa tất cả máy chủ DNS?",
|
||||
"clearAllConfirm": "Thao tác này sẽ xóa toàn bộ máy chủ DNS khỏi danh sách. Không thể hoàn tác.",
|
||||
"dnsLeakWarning": "DNS có thể rò rỉ qua localhost, UDP/TCP không mã hóa, DoH/DoQ chế độ cục bộ, truy vấn fallback hoặc EDNS client IP. Hãy dùng DoH được định tuyến, ghim resolver trong hosts và tắt fallback khi cần riêng tư."
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "Thêm DNS giả",
|
||||
"ipPool": "Mạng con nhóm IP",
|
||||
"poolSize": "Kích thước bể bơi"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "Outbound mặc định",
|
||||
"defaultOutboundDesc": "Lưu lượng không khớp quy tắc định tuyến dùng outbound này (mục đầu danh sách)."
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ Bàn phím đã đóng!",
|
||||
"noResult": "❗ Không có kết quả!",
|
||||
"noQuery": "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!",
|
||||
"wentWrong": "❌ Đã xảy ra lỗi!",
|
||||
"noIpRecord": "❗ Không có bản ghi IP!",
|
||||
"noInbounds": "❗ Không tìm thấy inbound!",
|
||||
"unlimited": "♾ Không giới hạn (Đặt lại)",
|
||||
"add": "Thêm",
|
||||
"month": "Tháng",
|
||||
"months": "Tháng",
|
||||
"days": "Ngày",
|
||||
"hours": "Giờ",
|
||||
"minutes": "Phút",
|
||||
"unknown": "Không xác định",
|
||||
"inbounds": "Inbound",
|
||||
"clients": "Client",
|
||||
"offline": "🔴 Ngoại tuyến",
|
||||
"online": "🟢 Trực tuyến",
|
||||
"commands": {
|
||||
"unknown": "❗ Lệnh không rõ",
|
||||
"pleaseChoose": "👇 Vui lòng chọn:\r\n",
|
||||
"help": "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n",
|
||||
"start": "👋 Xin chào <i>{{ .Firstname }}</i>.\r\n",
|
||||
"welcome": "🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\r\n",
|
||||
"status": "✅ Bot hoạt động bình thường!",
|
||||
"usage": "❗ Vui lòng cung cấp văn bản để tìm kiếm!",
|
||||
"getID": "🆔 ID của bạn: <code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "Để khởi động lại Xray Core:\r\n<code>/restart</code>\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "Để tìm kiếm thống kê, sử dụng lệnh sau:\r\n<code>/usage [Email]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ Hoạt động thành công!",
|
||||
"restartFailed": "❗ Lỗi trong quá trình hoạt động.\r\n\r\n<code>Lỗi: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core không chạy.",
|
||||
"startDesc": "Hiển thị menu chính",
|
||||
"helpDesc": "Trợ giúp bot",
|
||||
"statusDesc": "Kiểm tra trạng thái bot",
|
||||
"idDesc": "Hiển thị ID Telegram của bạn",
|
||||
"usageDesc": "Xem mức dùng của khách: /usage email",
|
||||
"inboundDesc": "Tìm inbound: /inbound tên (quản trị)",
|
||||
"restartDesc": "Khởi động lại lõi Xray (quản trị)",
|
||||
"clearallDesc": "Đặt lại lưu lượng mọi khách hàng (quản trị)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ Lỗi khi chọn người dùng!",
|
||||
"userSaved": "✅ Người dùng Telegram đã được lưu.",
|
||||
"loginSuccess": "✅ Đăng nhập thành công vào bảng điều khiển.\r\n",
|
||||
"loginFailed": "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n",
|
||||
"report": "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ Ngày-Giờ: {{ .DateTime }}\r\n",
|
||||
"hostname": "💻 Host: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 Phiên bản X-UI: {{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Phiên bản Xray: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ Trạng thái: {{ .State }}\r\n",
|
||||
"username": "👤 Tên người dùng: {{ .Username }}\r\n",
|
||||
"reason": "❗️ Lý do: {{ .Reason }}\r\n",
|
||||
"time": "⏰ Thời gian: {{ .Time }}\r\n",
|
||||
"inbound": "📍 Inbound: {{ .Remark }}\r\n",
|
||||
"port": "🔌 Cổng: {{ .Port }}\r\n",
|
||||
"expire": "📅 Ngày hết hạn: {{ .Time }}\r\n",
|
||||
"expireIn": "📅 Hết hạn sau: {{ .Time }}\r\n",
|
||||
"active": "💡 Đang hoạt động: {{ .Enable }}\r\n",
|
||||
"enabled": "🚨 Đã bật: {{ .Enable }}\r\n",
|
||||
"online": "🌐 Trạng thái kết nối: {{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 Lần online gần nhất: {{ .Time }}\r\n",
|
||||
"email": "📧 Email: {{ .Email }}\r\n",
|
||||
"upload": "🔼 Tải lên: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 Tải xuống: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 Tổng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 Người dùng Telegram: {{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 Số lần cạn kiệt {{ .Type }}:\r\n",
|
||||
"onlinesCount": "🌐 Khách hàng trực tuyến: {{ .Count }}\r\n",
|
||||
"disabled": "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 Thời gian sao lưu: {{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ Có",
|
||||
"no": "❌ Không",
|
||||
"received_email": "📧📥 Email đã được cập nhật.",
|
||||
"received_comment": "💬📥 Bình luận đã được cập nhật.",
|
||||
"email_prompt": "📧 Email mặc định: {{ .ClientEmail }}\n\nVui lòng nhập email của bạn.",
|
||||
"comment_prompt": "💬 Bình luận mặc định: {{ .ClientComment }}\n\nVui lòng nhập bình luận của bạn.",
|
||||
"cancel": "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄",
|
||||
"error_add_client": "⚠️ Lỗi:\n\n {{ .error }}",
|
||||
"using_default_value": "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊",
|
||||
"incorrect_input": "Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫",
|
||||
"AreYouSure": "Bạn có chắc không? 🤔",
|
||||
"SuccessResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công",
|
||||
"FailedResetTraffic": "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng.",
|
||||
"eventOutboundDown": "Outbound {{ .Tag }} đã NGỪNG HOẠT ĐỘNG",
|
||||
"eventOutboundUp": "Outbound {{ .Tag }} đã HOẠT ĐỘNG",
|
||||
"eventErrorDetail": "Lỗi: {{ .Error }}",
|
||||
"eventDelayDetail": "Độ trễ: {{ .Delay }}ms",
|
||||
"eventXrayCrash": "Xray GẶP SỰ CỐ",
|
||||
"eventXrayCrashError": "Lỗi: {{ .Error }}",
|
||||
"eventNodeDown": "Node {{ .Name }} đã NGỪNG HOẠT ĐỘNG",
|
||||
"eventNodeUp": "Node {{ .Name }} đã HOẠT ĐỘNG",
|
||||
"eventLoginFallback": "Đăng nhập thất bại từ {{ .Source }}",
|
||||
"memoryThreshold": "Sử dụng bộ nhớ {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ Đóng Bàn Phím",
|
||||
"cancel": "❌ Hủy",
|
||||
"cancelReset": "❌ Hủy Đặt Lại",
|
||||
"cancelIpLimit": "❌ Hủy Giới Hạn IP",
|
||||
"confirmResetTraffic": "✅ Xác Nhận Đặt Lại Lưu Lượng?",
|
||||
"confirmClearIps": "✅ Xác Nhận Xóa Các IP?",
|
||||
"confirmRemoveTGUser": "✅ Xác Nhận Xóa Người Dùng Telegram?",
|
||||
"confirmToggle": "✅ Xác nhận Bật/Tắt người dùng?",
|
||||
"dbBackup": "Tải bản sao lưu cơ sở dữ liệu",
|
||||
"serverUsage": "Sử Dụng Máy Chủ",
|
||||
"getInbounds": "Lấy cổng vào",
|
||||
"depleteSoon": "Depleted Soon",
|
||||
"clientUsage": "Lấy Sử Dụng",
|
||||
"onlines": "Khách hàng trực tuyến",
|
||||
"commands": "Lệnh",
|
||||
"refresh": "🔄 Cập Nhật",
|
||||
"clearIPs": "❌ Xóa IP",
|
||||
"removeTGUser": "❌ Xóa Người Dùng Telegram",
|
||||
"selectTGUser": "👤 Chọn Người Dùng Telegram",
|
||||
"selectOneTGUser": "👤 Chọn một người dùng telegram:",
|
||||
"resetTraffic": "📈 Đặt Lại Lưu Lượng",
|
||||
"resetExpire": "📅 Thay đổi ngày hết hạn",
|
||||
"ipLog": "🔢 Nhật ký địa chỉ IP",
|
||||
"ipLimit": "🔢 Giới Hạn địa chỉ IP",
|
||||
"setTGUser": "👤 Đặt Người Dùng Telegram",
|
||||
"toggle": "🔘 Bật / Tắt",
|
||||
"custom": "🔢 Tùy chỉnh",
|
||||
"confirmNumber": "✅ Xác nhận: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ Xác nhận thêm: {{ .Num }}",
|
||||
"limitTraffic": "🚧 Giới hạn lưu lượng",
|
||||
"getBanLogs": "Cấm nhật ký",
|
||||
"allClients": "Tất cả Khách hàng",
|
||||
"addClient": "Thêm Khách Hàng",
|
||||
"submitDisable": "Gửi Dưới Dạng Vô Hiệu ☑️",
|
||||
"submitEnable": "Gửi Dưới Dạng Kích Hoạt ✅",
|
||||
"use_default": "🏷️ Sử Dụng Mặc Định",
|
||||
"change_email": "⚙️📧 Email",
|
||||
"change_comment": "⚙️💬 Bình Luận",
|
||||
"ResetAllTraffics": "Đặt lại tất cả lưu lượng",
|
||||
"SortedTrafficUsageReport": "Báo cáo sử dụng lưu lượng đã sắp xếp"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ Thành công!",
|
||||
"errorOperation": "❗ Lỗi Trong Quá Trình Thực Hiện.",
|
||||
"getInboundsFailed": "❌ Không Thể Lấy Được Inbounds",
|
||||
"getClientsFailed": "❌ Không thể lấy khách hàng.",
|
||||
"canceled": "❌ {{ .Email }} : Thao Tác Đã Bị Hủy.",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng.",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs.",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram.",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công.",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }} : Đã lưu thành công giới hạn lưu lượng.",
|
||||
"expireResetSuccess": "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công.",
|
||||
"resetIpSuccess": "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công.",
|
||||
"clearIpSuccess": "✅ {{ .Email }} : IP Đã Được Xóa Thành Công.",
|
||||
"getIpLog": "✅ {{ .Email }} : Lấy nhật ký IP Thành Công.",
|
||||
"getUserInfo": "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công.",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công.",
|
||||
"enableSuccess": "✅ {{ .Email }} : Đã Bật Thành Công.",
|
||||
"disableSuccess": "✅ {{ .Email }} : Đã Tắt Thành Công.",
|
||||
"askToAddUserId": "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "Chọn một Khách hàng cho Inbound {{ .Inbound }}",
|
||||
"chooseInbound": "Chọn một Inbound"
|
||||
}
|
||||
"noIpRecord": "❗ Không có bản ghi IP!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Trạng thái",
|
||||
"labelOutbound": "Outbound",
|
||||
"labelNode": "Node",
|
||||
"labelError": "Lỗi",
|
||||
"labelDelay": "Độ trễ",
|
||||
"labelUsername": "Tên đăng nhập",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "Lý do",
|
||||
"labelSource": "Nguồn",
|
||||
"statusCrashed": "GẶP SỰ CỐ",
|
||||
"statusHigh": "CAO",
|
||||
"statusSuccess": "THÀNH CÔNG",
|
||||
"statusFailed": "THẤT BẠI",
|
||||
"statusDown": "NGỪNG HOẠT ĐỘNG",
|
||||
"statusUp": "HOẠT ĐỘNG"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "成功",
|
||||
"lastOnline": "上次在线",
|
||||
"lastSubFetch": "上次获取订阅",
|
||||
"getVersion": "获取版本",
|
||||
"install": "安装",
|
||||
"clients": "客户端",
|
||||
"usage": "使用情况",
|
||||
"twoFactorCode": "代码",
|
||||
"remained": "剩余",
|
||||
"security": "安全",
|
||||
"emptyDnsDesc": "未添加 DNS 服务器。",
|
||||
"emptyFakeDnsDesc": "未添加 Fake DNS 服务器。",
|
||||
"emptyBalancersDesc": "未添加负载均衡器。",
|
||||
"somethingWentWrong": "出了点问题",
|
||||
"subscription": {
|
||||
"title": "订阅信息",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "已上传",
|
||||
"expiry": "到期",
|
||||
"totalQuota": "总配额",
|
||||
"individualLinks": "单独链接",
|
||||
"active": "启用",
|
||||
"inactive": "停用",
|
||||
"unlimited": "无限制",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "重启",
|
||||
"xraySwitch": "版本",
|
||||
"xrayUpdates": "Xray 更新",
|
||||
"xraySwitchClickDesk": "请谨慎选择,因为较旧版本可能与当前配置不兼容",
|
||||
"updatePanel": "更新面板",
|
||||
"panelUpdateDesc": "这将把 3X-UI 更新到最新版本并重启面板服务。",
|
||||
"currentPanelVersion": "当前面板版本",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "下载",
|
||||
"sent": "已发送",
|
||||
"received": "已接收",
|
||||
"xraySwitchVersionDialog": "您确定要更改 Xray 版本吗?",
|
||||
"xraySwitchVersionDialogDesc": "这将把 Xray 版本更改为 #version#。",
|
||||
"xraySwitchVersionPopover": "Xray 更新成功",
|
||||
"panelUpdateDialog": "您确定要更新面板吗?",
|
||||
"panelUpdateDialogDesc": "这将把 3X-UI 更新到 #version# 并重启面板服务。",
|
||||
"panelUpdateStartedPopover": "已开始更新面板",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "更新未成功完成。请检查服务器日志,或在命令行运行「x-ui update」。",
|
||||
"panelUpdateUnknownTitle": "无法确认更新是否已完成",
|
||||
"panelUpdateUnknownDesc": "面板未能及时返回结果。请刷新页面查看当前版本,或检查服务器日志。",
|
||||
"geofileUpdateDialog": "您确定要更新地理文件吗?",
|
||||
"geofileUpdateDialogDesc": "这将更新 #filename# 文件。",
|
||||
"geofilesUpdateDialogDesc": "这将更新所有文件。",
|
||||
"geofilesUpdateAll": "全部更新",
|
||||
"geofileUpdatePopover": "地理文件更新成功",
|
||||
"geodataTitle": "Geodata 自动更新",
|
||||
"geodataHint": "Xray 会按计划下载这些文件并热重载,无需重启。URL 必须为 HTTPS。文件必须已存在于 bin 目录中,Xray 才能对其更新。",
|
||||
"geodataCron": "更新计划 (cron)",
|
||||
"geodataOutbound": "通过出站下载(可选)",
|
||||
"geodataFile": "文件名",
|
||||
"geodataAddFile": "添加文件",
|
||||
"geodataSaveRestart": "保存并重启 Xray",
|
||||
"geodataConfirmTitle": "保存 geodata 设置?",
|
||||
"geodataConfirmContent": "将更新 Xray 配置模板并重启 Xray。",
|
||||
"geodataInvalidUrl": "每个文件都需要 HTTPS 地址。",
|
||||
"geodataInvalidFile": "文件名必须是纯文件名,例如 geosite_custom.dat(不能包含路径)。",
|
||||
"geodataInvalidCron": "Cron 必须为 5 段,例如 0 4 * * *",
|
||||
"geodataEmpty": "尚未配置文件。路由规则中可通过 ext:geosite_custom.dat:category 引用文件。",
|
||||
"dontRefresh": "安装中,请勿刷新此页面",
|
||||
"logs": "日志",
|
||||
"accessLogs": "访问日志",
|
||||
@@ -1061,19 +1032,7 @@
|
||||
"pinnedCertPlaceholder": "base64 或 hex 的 SHA-256",
|
||||
"fetchPin": "获取",
|
||||
"pinFetched": "已获取节点当前证书",
|
||||
"pinFetchFailed": "无法获取证书",
|
||||
"nodeType": "节点类型",
|
||||
"nodeTypeTraditional": "传统面板节点",
|
||||
"nodeTypeAgent": "Agent 轻量节点",
|
||||
"agentHint": "Agent 节点由节点机主动连接主控,无需填写地址、端口和 API 令牌。创建后会生成安装命令。",
|
||||
"agentTokenTitle": "Agent 节点安装",
|
||||
"agentTokenCreated": "节点 {{name}} 创建成功",
|
||||
"agentTokenHint": "复制下面的命令,在节点机上运行即可完成安装。此 token 只显示一次!",
|
||||
"agentTokenInstructions": "命令会自动下载安装脚本、配置主控地址和 token,约 30 秒后节点上线。",
|
||||
"agentTokenCopied": "安装命令已复制",
|
||||
"agentTokenCopyFailed": "复制失败,请手动复制",
|
||||
"copyToken": "复制安装命令",
|
||||
"agentMasterUrlHint": "主控地址(agent 用来连接主控,如有域名或反代请改成对外地址):"
|
||||
"pinFetchFailed": "无法获取证书"
|
||||
},
|
||||
"settings": {
|
||||
"defaultTag": "默认",
|
||||
@@ -1094,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "默认根路径 \"/\" 众所周知 — 请更改为随机路径。",
|
||||
"warnDefaultSubPath": "默认订阅路径 \"/sub/\" 众所周知 — 请更改。",
|
||||
"warnDefaultJsonPath": "默认 JSON 订阅路径 \"/json/\" 众所周知 — 请更改。",
|
||||
"TGBotSettings": "Telegram 机器人",
|
||||
"panelListeningIP": "面板监听 IP",
|
||||
"panelListeningIPDesc": "默认留空监听所有 IP",
|
||||
"panelListeningDomain": "面板监听域名",
|
||||
@@ -1119,43 +1077,12 @@
|
||||
"currentPassword": "原密码",
|
||||
"newUsername": "新用户名",
|
||||
"newPassword": "新密码",
|
||||
"telegramBotEnable": "启用 Telegram 机器人",
|
||||
"telegramBotEnableDesc": "启用 Telegram 机器人功能",
|
||||
"telegramToken": "Telegram Token",
|
||||
"telegramTokenDesc": "从 '{'@'}BotFather' 获取的 Telegram 机器人令牌",
|
||||
"telegramProxy": "SOCKS 代理",
|
||||
"telegramProxyDesc": "启用 SOCKS5 代理连接到 Telegram(根据指南调整设置)",
|
||||
"telegramAPIServer": "Telegram API 服务器",
|
||||
"telegramAPIServerDesc": "要使用的 Telegram API 服务器。留空以使用默认服务器。",
|
||||
"telegramChatId": "管理员聊天 ID",
|
||||
"telegramChatIdDesc": "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 {'@'}userinfobot 获取,或在机器人中使用 '/id' 命令获取)",
|
||||
"telegramNotifyTime": "通知时间",
|
||||
"telegramNotifyTimeDesc": "Telegram 机器人发送周期性报告的频率。选择预设间隔,或选择“自定义”以输入 crontab 表达式。",
|
||||
"notifyTime": {
|
||||
"every": "@every — 按间隔重复",
|
||||
"hourly": "@hourly — 每小时",
|
||||
"daily": "@daily — 每天 00:00",
|
||||
"weekly": "@weekly — 每周",
|
||||
"monthly": "@monthly — 每月",
|
||||
"custom": "自定义 (crontab)",
|
||||
"seconds": "秒",
|
||||
"minutes": "分钟",
|
||||
"hours": "小时",
|
||||
"interval": "间隔",
|
||||
"unit": "单位"
|
||||
},
|
||||
"tgNotifyBackup": "数据库备份",
|
||||
"tgNotifyBackupDesc": "发送带有报告的数据库备份文件",
|
||||
"tgNotifyLogin": "登录通知",
|
||||
"tgNotifyLoginDesc": "当有人试图登录你的面板时显示用户名、IP 地址和时间",
|
||||
"sessionMaxAge": "会话时长",
|
||||
"sessionMaxAgeDesc": "保持登录状态的时长(单位:分钟)",
|
||||
"expireTimeDiff": "到期通知阈值",
|
||||
"expireTimeDiffDesc": "达到此阈值时,将收到有关到期时间的通知(单位:天)",
|
||||
"trafficDiff": "流量耗尽阈值",
|
||||
"trafficDiffDesc": "达到此阈值时,将收到有关流量耗尽的通知(单位:GB)",
|
||||
"tgNotifyCpu": "CPU 负载通知阈值",
|
||||
"tgNotifyCpuDesc": "CPU 负载超过此阈值时,将收到通知(单位:%)",
|
||||
"timeZone": "时区",
|
||||
"timeZoneDesc": "定时任务将按照该时区的时间运行",
|
||||
"subSettings": "订阅设置",
|
||||
@@ -1220,10 +1147,6 @@
|
||||
"subEncryptDesc": "订阅服务返回的内容将采用 Base64 编码",
|
||||
"subURI": "反向代理 URI",
|
||||
"subURIDesc": "用于订阅链接和二维码的完整基础 URL(scheme://域名[:端口]/路径/),会替代监听域名/监听端口使用。当订阅通过反向代理访问,或使用与上面不同的域名/端口访问时,请设置此项。",
|
||||
"externalTrafficInformEnable": "外部交通通知",
|
||||
"externalTrafficInformEnableDesc": "每次流量更新时通知外部 API。",
|
||||
"externalTrafficInformURI": "外部流量通知 URI",
|
||||
"externalTrafficInformURIDesc": "流量更新将发送到此 URI",
|
||||
"restartXrayOnClientDisable": "客户端自动禁用后重启 Xray",
|
||||
"restartXrayOnClientDisableDesc": "当客户端因到期或流量超限被自动禁用时,重启 Xray。",
|
||||
"fragment": "分片",
|
||||
@@ -1233,38 +1156,6 @@
|
||||
"noisesSett": "Noises 设置",
|
||||
"trustedProxyCidrs": "可信代理 CIDR",
|
||||
"trustedProxyCidrsDesc": "允许设置转发 host、proto 和客户端 IP 标头的 IP/CIDR(逗号分隔)。",
|
||||
"ldap": {
|
||||
"enable": "启用 LDAP 同步",
|
||||
"host": "LDAP host",
|
||||
"port": "LDAP 端口",
|
||||
"useTls": "使用 TLS (LDAPS)",
|
||||
"skipTlsVerify": "跳过 TLS 证书验证",
|
||||
"skipTlsVerifyDesc": "不安全 — 禁用服务器证书验证。仅用于内部/不受信任的 CA。",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "已配置;留空以保留当前密码。",
|
||||
"passwordUnconfigured": "未配置。",
|
||||
"passwordPlaceholder": "已配置 - 输入新值以替换",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "用户筛选",
|
||||
"userAttr": "用户属性 (username/email)",
|
||||
"vlessField": "VLESS flag 属性",
|
||||
"flagField": "通用 flag 属性 (可选)",
|
||||
"flagFieldDesc": "如设置,将覆盖 VLESS flag — 如 shadowInactive。",
|
||||
"truthyValues": "Truthy 值",
|
||||
"truthyValuesDesc": "逗号分隔;默认: true,1,yes,on",
|
||||
"invertFlag": "反转 flag",
|
||||
"invertFlagDesc": "当属性表示已禁用时启用 (如 shadowInactive)。",
|
||||
"syncSchedule": "同步计划",
|
||||
"syncScheduleDesc": "类 cron 字符串,如 @every 1m",
|
||||
"inboundTags": "入站标签",
|
||||
"inboundTagsDesc": "允许 LDAP 同步自动创建或删除客户端的入站。",
|
||||
"noInbounds": "未找到入站。请先在“入站”中创建。",
|
||||
"autoCreate": "自动创建客户端",
|
||||
"autoDelete": "自动删除客户端",
|
||||
"defaultTotalGb": "默认总流量 (GB)",
|
||||
"defaultExpiryDays": "默认到期 (天)",
|
||||
"defaultIpLimit": "默认 IP 限制"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "将 Xray finalmask TCP/UDP 掩码和 QUIC 参数注入每个生成的 Xray JSON 配置。需要支持 Xray JSON 订阅的客户端应用和较新的 Xray 核心。",
|
||||
@@ -1288,31 +1179,15 @@
|
||||
"muxSett": "复用器设置",
|
||||
"direct": "直接连接",
|
||||
"directDesc": "直接与特定国家的域或 IP 范围建立连接",
|
||||
"notifications": "通知",
|
||||
"certs": "证书",
|
||||
"externalTraffic": "外部流量",
|
||||
"dateAndTime": "日期和时间",
|
||||
"proxyAndServer": "代理和服务器",
|
||||
"intervals": "间隔",
|
||||
"information": "信息",
|
||||
"profile": "资料",
|
||||
"language": "语言",
|
||||
"telegramBotLanguage": "Telegram 机器人语言",
|
||||
"security": {
|
||||
"admin": "管理员凭据",
|
||||
"twoFactor": "双重验证",
|
||||
"twoFactorEnable": "启用 2FA",
|
||||
"twoFactorEnableDesc": "增加额外的验证层以提高安全性。",
|
||||
"twoFactorModalSetTitle": "启用双重认证",
|
||||
"twoFactorModalDeleteTitle": "停用双重认证",
|
||||
"twoFactorModalSteps": "要设定双重认证,请执行以下步骤:",
|
||||
"twoFactorModalFirstStep": "1. 在认证应用程序中扫描此 QR 码,或复制 QR 码附近的令牌并粘贴到应用程序中",
|
||||
"twoFactorModalSecondStep": "2. 输入应用程序中的验证码",
|
||||
"twoFactorModalRemoveStep": "输入应用程序中的验证码以移除双重认证。",
|
||||
"twoFactorModalChangeCredentialsTitle": "更改凭据",
|
||||
"twoFactorModalChangeCredentialsStep": "输入应用程序中的代码以更改管理员凭据。",
|
||||
"twoFactorModalSetSuccess": "双因素认证已成功建立",
|
||||
"twoFactorModalDeleteSuccess": "双因素认证已成功删除",
|
||||
"twoFactorModalError": "验证码错误",
|
||||
"show": "显示",
|
||||
"hide": "隐藏",
|
||||
@@ -1335,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "获取出站流量错误",
|
||||
"resetOutboundTrafficError": "重置出站流量错误"
|
||||
},
|
||||
"smtpSettings": "SMTP 设置",
|
||||
"smtpEnable": "启用邮件通知",
|
||||
"smtpEnableDesc": "通过 SMTP 启用邮件通知",
|
||||
"smtpHost": "SMTP 主机",
|
||||
"smtpHostDesc": "SMTP 服务器主机名(例如 smtp.gmail.com)",
|
||||
"smtpPort": "SMTP 端口",
|
||||
"smtpPortDesc": "SMTP 服务器端口(默认:587)",
|
||||
"smtpUsername": "SMTP 用户名",
|
||||
"smtpUsernameDesc": "SMTP 认证用户名",
|
||||
"smtpFrom": "发件人地址 (From)",
|
||||
"smtpFromDesc": "邮件 From 头使用的地址。留空则使用用户名。",
|
||||
"smtpFromName": "发件人名称 (From)",
|
||||
"smtpFromNameDesc": "From 头中显示在地址前的可选显示名称。",
|
||||
"smtpPassword": "SMTP 密码",
|
||||
"smtpPasswordDesc": "SMTP 认证密码",
|
||||
"smtpTo": "收件人",
|
||||
"smtpToDesc": "以逗号分隔的收件人邮箱地址",
|
||||
"emailSettings": "邮件",
|
||||
"emailNotifications": "通知",
|
||||
"smtpEventBusNotify": "邮件事件通知",
|
||||
"smtpEventBusNotifyDesc": "选择触发邮件通知的事件",
|
||||
"tgEventBusNotify": "Telegram 事件通知",
|
||||
"tgEventBusNotifyDesc": "选择触发 Telegram 通知的事件",
|
||||
"testSmtp": "发送测试邮件",
|
||||
"testTgBot": "发送测试消息",
|
||||
"eventGroupOutbound": "出站",
|
||||
"eventGroupXray": "Xray 核心",
|
||||
"eventGroupSystem": "系统",
|
||||
"eventGroupSecurity": "安全",
|
||||
"eventGroupNode": "节点",
|
||||
"eventOutboundDown": "断开",
|
||||
"eventOutboundUp": "恢复",
|
||||
"eventXrayCrash": "崩溃",
|
||||
@@ -1372,36 +1217,7 @@
|
||||
"eventNodeUp": "上线",
|
||||
"eventCPUHigh": "CPU 占用过高(%)",
|
||||
"requestFailed": "请求失败",
|
||||
"smtpEncryption": "加密",
|
||||
"smtpEncryptionDesc": "SMTP 连接加密方式",
|
||||
"smtpEncryptionNone": "无(明文)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS(隐式)",
|
||||
"smtpStageConnect": "连接",
|
||||
"smtpStageAuth": "认证",
|
||||
"smtpStageSend": "发送",
|
||||
"smtpTestSuccess": "测试邮件发送成功",
|
||||
"smtpHostNotConfigured": "尚未配置 SMTP 主机",
|
||||
"smtpNoRecipients": "尚未配置收件人",
|
||||
"smtpFromNotConfigured": "未配置 SMTP 发件人地址",
|
||||
"eventLoginAttempt": "登录尝试",
|
||||
"telegramTokenConfigured": "已配置;留空则保留当前令牌。",
|
||||
"telegramTokenPlaceholder": "已配置——输入新令牌以替换",
|
||||
"smtpPasswordConfigured": "已配置;留空则保留当前密码。",
|
||||
"smtpPasswordPlaceholder": "已配置——输入新密码以替换",
|
||||
"smtpNotInitialized": "SMTP 尚未初始化",
|
||||
"tgBotNotEnabled": "Telegram 机器人未启用",
|
||||
"tgTestFailed": "Telegram 测试失败",
|
||||
"tgTestSuccess": "测试消息已发送至 Telegram",
|
||||
"tgBotNotRunning": "Telegram 机器人未运行",
|
||||
"smtpErrorAuth": "认证失败——请检查用户名和密码",
|
||||
"smtpErrorStarttls": "服务器要求 STARTTLS——请更改加密类型",
|
||||
"smtpErrorTls": "服务器要求 TLS——请更改加密类型",
|
||||
"smtpErrorRefused": "连接被拒绝——请检查主机和端口",
|
||||
"smtpErrorTimeout": "连接超时——主机无法访问",
|
||||
"smtpErrorRelay": "服务器拒绝从此地址发送",
|
||||
"smtpErrorEof": "连接被服务器关闭",
|
||||
"smtpErrorUnknown": "SMTP 错误:{{ .Error }}",
|
||||
"eventMemoryHigh": "内存使用率高 (%)",
|
||||
"remarkTemplate": "备注模板",
|
||||
"remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。",
|
||||
@@ -1495,8 +1311,6 @@
|
||||
"directdomains": "直接域名",
|
||||
"ipv4Routing": "IPv4 路由",
|
||||
"ipv4RoutingDesc": "此选项将仅通过 IPv4 路由到目标域",
|
||||
"Template": "高级 Xray 配置模板",
|
||||
"TemplateDesc": "最终的 Xray 配置文件将基于此模板生成",
|
||||
"FreedomStrategy": "Freedom 协议策略",
|
||||
"FreedomStrategyDesc": "设置 Freedom 协议中网络的输出策略",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1512,11 +1326,6 @@
|
||||
"Balancers": "负载均衡",
|
||||
"balancerTagRequired": "标签为必填项",
|
||||
"balancerSelectorRequired": "至少选择一个出站",
|
||||
"balancerLive": "当前目标",
|
||||
"balancerOverride": "强制指定",
|
||||
"balancerOverridePh": "自动(策略)",
|
||||
"balancerLiveRefresh": "刷新负载均衡器实时状态",
|
||||
"balancerNotRunning": "此负载均衡器在运行中的 Xray 未激活 — 请先保存更改或启动 Xray",
|
||||
"routeTester": "路由测试",
|
||||
"routeTesterDesc": "向运行中的 Xray 查询某个连接将使用哪个出站。不会发送真实流量 — 结果直接来自实时路由引擎。",
|
||||
"routeTesterDest": "域名或 IP",
|
||||
@@ -1528,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "经由负载均衡器",
|
||||
"routeTesterDefaultOutbound": "无路由规则匹配 — 流量将发往默认(第一个)出站。",
|
||||
"Routings": "路由规则",
|
||||
"completeTemplate": "全部",
|
||||
"logLevel": "日志级别",
|
||||
"logLevelDesc": "错误日志的日志级别,用于指示需要记录的信息",
|
||||
"accessLog": "访问日志",
|
||||
@@ -1556,7 +1364,6 @@
|
||||
"source": "来源",
|
||||
"dest": "目的地址",
|
||||
"inbound": "入站",
|
||||
"balancer": "负载均衡",
|
||||
"useComma": "逗号分隔的项目"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1737,32 +1544,7 @@
|
||||
"alreadyAdded": "该服务器已在出站列表中。要用新密钥请点 {reset}。",
|
||||
"provisionFailed": "无法生成 PIA 出站,请重试。"
|
||||
},
|
||||
"tabBalancerSettings": "负载均衡设置",
|
||||
"tabObservatory": "观测器",
|
||||
"observatory": {
|
||||
"autoManaged": "观测器会根据你的负载均衡器自动管理。可在下方调整探测方式;被观测的出站会跟随负载均衡器的选择器。",
|
||||
"emptyHint": "当前没有活动的连接观测器。当你创建 Least Ping 或 Least Load 负载均衡器,或带有 fallback 的 Random / Round-robin 负载均衡器时,会自动添加一个,以便依赖观测器的负载均衡器在选择目标前检查出站健康状态。",
|
||||
"mixedLegacy": "此配置同时包含 Observatory 和 Burst Observatory。Xray 只使用一个全局观测器,因此不支持这种旧式混合状态;保存负载均衡器时会将其规范化为单个观测器。",
|
||||
"subjectSelector": "被观测的出站",
|
||||
"subjectSelectorDesc": "该观测器探测的出站标签。根据你的负载均衡器自动管理。",
|
||||
"probeURL": "探测 URL",
|
||||
"probeURLDesc": "用于测量每个出站而请求的 URL,应返回 HTTP 204。",
|
||||
"probeInterval": "探测间隔",
|
||||
"probeIntervalDesc": "每个出站的探测频率,例如 30s、1m、2h45m。",
|
||||
"enableConcurrency": "并发探测",
|
||||
"enableConcurrencyDesc": "一次性探测所有被观测的出站,而不是逐个探测。更快,但在网络上更明显。",
|
||||
"destination": "探测目标",
|
||||
"destinationDesc": "用于测量每个出站而请求的 URL,应返回 HTTP 204。",
|
||||
"connectivity": "连通性检查",
|
||||
"connectivityDesc": "可选的本地网络检查 URL,仅在目标失败后才尝试。留空则跳过。",
|
||||
"interval": "探测间隔",
|
||||
"intervalDesc": "每个出站两次探测之间的平均时间,例如 1m。最小 10s。",
|
||||
"timeout": "探测超时",
|
||||
"timeoutDesc": "判定探测失败前的等待时长,例如 5s。",
|
||||
"sampling": "采样数量",
|
||||
"samplingDesc": "为每个出站评分而保留的最近探测结果数量。",
|
||||
"httpMethod": "HTTP 方法",
|
||||
"httpMethodDesc": "探测所用的 HTTP 方法。",
|
||||
"deleteAlsoObservatory": "这是最后一个使用 Observatory 的负载均衡器,因此它也会被一并移除。",
|
||||
"deleteAlsoBurst": "这是最后一个使用 Burst Observatory 的负载均衡器,因此它也会被一并移除。"
|
||||
},
|
||||
@@ -1774,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "策略",
|
||||
"tag": "标签",
|
||||
"tagDuplicate": "该标签已被其他均衡器使用",
|
||||
"tagPlaceholder": "唯一均衡器标签",
|
||||
"selector": "选择器",
|
||||
"fallback": "备用负载均衡器",
|
||||
"cycleTooltip": "循环: {path} → (回到 {start})",
|
||||
"expected": "期望",
|
||||
"expectedPlaceholder": "最佳节点数",
|
||||
"maxRtt": "最大 RTT",
|
||||
"tolerance": "容差",
|
||||
"baselines": "基准值",
|
||||
"costs": "成本设置",
|
||||
"costMatch": "标签匹配模式",
|
||||
"costValue": "权重",
|
||||
"costRegexp": "正则表达式匹配",
|
||||
"balancerDeleteInUse": "无法删除此负载均衡器 — 它被用作以下负载均衡器的备用:{names}",
|
||||
"balancerFallbackCycle": "无法将此负载均衡器设置为备用 — 这会创建循环依赖。",
|
||||
"balancerFallbackInfo": "流量将通过以下路径路由:负载均衡器 → Loopback → 服务器 → 目标负载均衡器 → 出站连接。这会增加一个经过服务器的额外跳转,可能会引入轻微延迟。",
|
||||
"fallbackBalancerHint": "选择另一个负载均衡器作为备用",
|
||||
"reservedPrefix": "_bl_ 前缀保留给内部负载均衡器回环对象"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1895,55 +1659,8 @@
|
||||
"usage": "使用",
|
||||
"addOutbound": "添加出站"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "启用 DNS",
|
||||
"enableDesc": "启用内置 DNS 服务器",
|
||||
"tag": "DNS 入站标签",
|
||||
"tagDesc": "此标签将在路由规则中可用作入站标签",
|
||||
"clientIp": "客户端 IP",
|
||||
"clientIpDesc": "用于在 DNS 查询期间通知服务器指定的 IP 位置",
|
||||
"disableCache": "禁用缓存",
|
||||
"disableCacheDesc": "禁用 DNS 缓存",
|
||||
"disableFallback": "禁用回退",
|
||||
"disableFallbackDesc": "禁用回退 DNS 查询",
|
||||
"disableFallbackIfMatch": "匹配时禁用回退",
|
||||
"disableFallbackIfMatchDesc": "当 DNS 服务器的匹配域名列表命中时,禁用回退 DNS 查询",
|
||||
"enableParallelQuery": "启用并行查询",
|
||||
"enableParallelQueryDesc": "启用并行 DNS 查询到多个服务器以实现更快的解析",
|
||||
"strategy": "查询策略",
|
||||
"strategyDesc": "解析域名的总体策略",
|
||||
"add": "添加服务器",
|
||||
"edit": "编辑服务器",
|
||||
"domains": "域名",
|
||||
"expectIPs": "预期 IP",
|
||||
"unexpectIPs": "意外 IP",
|
||||
"useSystemHosts": "使用系统 Hosts",
|
||||
"useSystemHostsDesc": "使用已安装系统的 hosts 文件",
|
||||
"serveStale": "提供过期结果",
|
||||
"serveStaleDesc": "在后台刷新时返回过期的缓存结果",
|
||||
"serveExpiredTTL": "过期 TTL",
|
||||
"serveExpiredTTLDesc": "过期缓存条目的有效期(秒);0 = 永不过期",
|
||||
"timeoutMs": "超时 (毫秒)",
|
||||
"skipFallback": "跳过回退",
|
||||
"finalQuery": "最终查询",
|
||||
"hosts": "主机映射",
|
||||
"hostsAdd": "添加 Host",
|
||||
"hostsEmpty": "未定义任何 Host",
|
||||
"hostsDomain": "域名 (例如 domain:example.com)",
|
||||
"hostsValues": "IP 或域名 — 输入后按 Enter",
|
||||
"usePreset": "使用模板",
|
||||
"dnsPresetTitle": "DNS 模板",
|
||||
"dnsPresetFamily": "家庭",
|
||||
"clearAll": "删除全部",
|
||||
"clearAllTitle": "删除所有 DNS 服务器?",
|
||||
"clearAllConfirm": "此操作将从列表中删除所有 DNS 服务器,且无法撤销。",
|
||||
"dnsLeakWarning": "DNS 可能通过 localhost、明文 UDP/TCP、本地模式 DoH/DoQ、回退查询或 EDNS client IP 泄漏。重视隐私时请使用经路由的 DoH、在 hosts 中固定解析器,并禁用回退。"
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "添加假 DNS",
|
||||
"ipPool": "IP 池子网",
|
||||
"poolSize": "池大小"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "默认出站",
|
||||
"defaultOutboundDesc": "未匹配任何路由规则的流量走此出站(列表中的第一个出站)。"
|
||||
},
|
||||
@@ -2067,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ 自定义键盘已关闭!",
|
||||
"noResult": "❗ 没有结果!",
|
||||
"noQuery": "❌ 未找到查询!请再次使用该命令!",
|
||||
"wentWrong": "❌ 出了点问题!",
|
||||
"noIpRecord": "❗ 没有 IP 记录!",
|
||||
"noInbounds": "❗ 未找到入站!",
|
||||
"unlimited": "♾ 无限(重置)",
|
||||
"add": "添加",
|
||||
"month": "月",
|
||||
"months": "月",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"unknown": "未知",
|
||||
"inbounds": "入站",
|
||||
"clients": "客户端",
|
||||
"offline": "🔴 离线",
|
||||
"online": "🟢 在线",
|
||||
"commands": {
|
||||
"unknown": "❗ 未知命令",
|
||||
"pleaseChoose": "👇 请选择:\r\n",
|
||||
"help": "🤖 欢迎使用本机器人!它旨在为您提供来自服务器的特定数据,并允许您进行必要的修改。\r\n\r\n",
|
||||
"start": "👋 你好,<i>{{ .Firstname }}</i>。\r\n",
|
||||
"welcome": "🤖 欢迎来到 <b>{{ .Hostname }}</b> 管理机器人。\r\n",
|
||||
"status": "✅ 机器人正常运行!",
|
||||
"usage": "❗ 请输入要搜索的文本!",
|
||||
"getID": "🆔 您的 ID 为:<code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "要重新启动 Xray Core:\r\n<code>/restart</code>\r\n\r\n要搜索客户电子邮件:\r\n<code>/usage [电子邮件]</code>\r\n\r\n要搜索入站(带有客户统计数据):\r\n<code>/inbound [备注]</code>\r\n\r\nTelegram 聊天 ID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "要搜索统计数据,请使用以下命令:\r\n<code>/usage [电子邮件]</code>\r\n\r\nTelegram 聊天 ID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ 操作成功!",
|
||||
"restartFailed": "❗ 操作错误。\r\n\r\n<code>错误: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core 未运行。",
|
||||
"startDesc": "显示主菜单",
|
||||
"helpDesc": "机器人帮助",
|
||||
"statusDesc": "检查机器人状态",
|
||||
"idDesc": "显示您的 Telegram ID",
|
||||
"usageDesc": "查看客户端用量:/usage 邮箱",
|
||||
"inboundDesc": "搜索入站:/inbound 备注(管理员)",
|
||||
"restartDesc": "重启 Xray 内核(管理员)",
|
||||
"clearallDesc": "重置所有客户端流量(管理员)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "CPU 使用率为 {{ .Percent }}%,超过阈值 {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ 用户选择错误!",
|
||||
"userSaved": "✅ 电报用户已保存。",
|
||||
"loginSuccess": "✅ 成功登录到面板。\r\n",
|
||||
"loginFailed": "❗️ 面板登录失败。\r\n",
|
||||
"report": "🕰 定时报告:{{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ 日期时间:{{ .DateTime }}\r\n",
|
||||
"hostname": "💻 主机: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 X-UI 版本:{{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ 服务器运行时间:{{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 服务器负载:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ 状态: {{ .State }}\r\n",
|
||||
"username": "👤 用户名:{{ .Username }}\r\n",
|
||||
"reason": "❗️ 原因:{{ .Reason }}\r\n",
|
||||
"time": "⏰ 时间:{{ .Time }}\r\n",
|
||||
"inbound": "📍 入站: {{ .Remark }}\r\n",
|
||||
"port": "🔌 端口: {{ .Port }}\r\n",
|
||||
"expire": "📅 过期日期:{{ .Time }}\r\n",
|
||||
"expireIn": "📅 剩余时间:{{ .Time }}\r\n",
|
||||
"active": "💡 激活:{{ .Enable }}\r\n",
|
||||
"enabled": "🚨 已启用:{{ .Enable }}\r\n",
|
||||
"online": "🌐 连接状态:{{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 上次在线: {{ .Time }}\r\n",
|
||||
"email": "📧 邮箱: {{ .Email }}\r\n",
|
||||
"upload": "🔼 上传: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 下载: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 总计: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 电报用户:{{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 耗尽的 {{ .Type }} 数量:\r\n",
|
||||
"onlinesCount": "🌐 在线客户:{{ .Count }}\r\n",
|
||||
"disabled": "🛑 禁用:{{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 即将耗尽:{{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 备份时间:{{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 刷新时间:{{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ 是的",
|
||||
"no": "❌ 否",
|
||||
"received_email": "📧📥 邮箱已更新。",
|
||||
"received_comment": "💬📥 评论已更新。",
|
||||
"email_prompt": "📧 默认邮箱: {{ .ClientEmail }}\n\n请输入您的邮箱。",
|
||||
"comment_prompt": "💬 默认评论: {{ .ClientComment }}\n\n请输入您的评论。",
|
||||
"cancel": "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄",
|
||||
"error_add_client": "⚠️ 错误:\n\n {{ .error }}",
|
||||
"using_default_value": "好的,我会使用默认值。 😊",
|
||||
"incorrect_input": "您的输入无效。\n短语应连续输入,不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫",
|
||||
"AreYouSure": "你确定吗?🤔",
|
||||
"SuccessResetTraffic": "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功",
|
||||
"FailedResetTraffic": "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠️ 错误: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 所有客户的流量重置已完成。",
|
||||
"eventOutboundDown": "出站 {{ .Tag }} 已断开",
|
||||
"eventOutboundUp": "出站 {{ .Tag }} 已恢复",
|
||||
"eventErrorDetail": "错误:{{ .Error }}",
|
||||
"eventDelayDetail": "延迟:{{ .Delay }} 毫秒",
|
||||
"eventXrayCrash": "Xray 已崩溃",
|
||||
"eventXrayCrashError": "错误:{{ .Error }}",
|
||||
"eventNodeDown": "节点 {{ .Name }} 已离线",
|
||||
"eventNodeUp": "节点 {{ .Name }} 已上线",
|
||||
"eventLoginFallback": "来自 {{ .Source }} 的登录失败",
|
||||
"memoryThreshold": "内存使用率 {{ .Percent }}% 超过阈值 {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ 关闭键盘",
|
||||
"cancel": "❌ 取消",
|
||||
"cancelReset": "❌ 取消重置",
|
||||
"cancelIpLimit": "❌ 取消 IP 限制",
|
||||
"confirmResetTraffic": "✅ 确认重置流量?",
|
||||
"confirmClearIps": "✅ 确认清除 IP?",
|
||||
"confirmRemoveTGUser": "✅ 确认移除 Telegram 用户?",
|
||||
"confirmToggle": "✅ 确认启用/禁用用户?",
|
||||
"dbBackup": "获取数据库备份",
|
||||
"serverUsage": "服务器使用情况",
|
||||
"getInbounds": "获取入站信息",
|
||||
"depleteSoon": "即将耗尽",
|
||||
"clientUsage": "获取使用情况",
|
||||
"onlines": "在线客户端",
|
||||
"commands": "命令",
|
||||
"refresh": "🔄 刷新",
|
||||
"clearIPs": "❌ 清除 IP",
|
||||
"removeTGUser": "❌ 移除 Telegram 用户",
|
||||
"selectTGUser": "👤 选择 Telegram 用户",
|
||||
"selectOneTGUser": "👤 选择一个 Telegram 用户:",
|
||||
"resetTraffic": "📈 重置流量",
|
||||
"resetExpire": "📅 更改到期日期",
|
||||
"ipLog": "🔢 IP 日志",
|
||||
"ipLimit": "🔢 IP 限制",
|
||||
"setTGUser": "👤 设置 Telegram 用户",
|
||||
"toggle": "🔘 启用/禁用",
|
||||
"custom": "🔢 自定义",
|
||||
"confirmNumber": "✅ 确认: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ 确认添加:{{ .Num }}",
|
||||
"limitTraffic": "🚧 流量限制",
|
||||
"getBanLogs": "禁止日志",
|
||||
"allClients": "所有客户",
|
||||
"addClient": "添加客户",
|
||||
"submitDisable": "提交为禁用 ☑️",
|
||||
"submitEnable": "提交为启用 ✅",
|
||||
"use_default": "🏷️ 使用默认",
|
||||
"change_email": "⚙️📧 邮箱",
|
||||
"change_comment": "⚙️💬 评论",
|
||||
"ResetAllTraffics": "重置所有流量",
|
||||
"SortedTrafficUsageReport": "排序的流量使用报告"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ 成功!",
|
||||
"errorOperation": "❗ 操作错误。",
|
||||
"getInboundsFailed": "❌ 获取入站信息失败。",
|
||||
"getClientsFailed": "❌ 获取客户失败。",
|
||||
"canceled": "❌ {{ .Email }}:操作已取消。",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}:客户端刷新成功。",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}:IP 刷新成功。",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}:客户端的 Telegram 用户刷新成功。",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}:流量已重置成功。",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: 流量限制保存成功。",
|
||||
"expireResetSuccess": "✅ {{ .Email }}:过期天数已重置成功。",
|
||||
"resetIpSuccess": "✅ {{ .Email }}:成功保存 IP 限制数量为 {{ .Count }}。",
|
||||
"clearIpSuccess": "✅ {{ .Email }}:IP 已成功清除。",
|
||||
"getIpLog": "✅ {{ .Email }}:获取 IP 日志。",
|
||||
"getUserInfo": "✅ {{ .Email }}:获取 Telegram 用户信息。",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}:Telegram 用户已成功移除。",
|
||||
"enableSuccess": "✅ {{ .Email }}:已成功启用。",
|
||||
"disableSuccess": "✅ {{ .Email }}:已成功禁用。",
|
||||
"askToAddUserId": "未找到您的配置!\r\n请向管理员询问,在您的配置中使用您的 Telegram 用户 ChatID。\r\n\r\n您的用户 ChatID:<code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "为入站 {{ .Inbound }} 选择一个客户",
|
||||
"chooseInbound": "选择一个入站"
|
||||
}
|
||||
"noIpRecord": "❗ 没有 IP 记录!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "状态",
|
||||
"labelOutbound": "出站",
|
||||
"labelNode": "节点",
|
||||
"labelError": "错误",
|
||||
"labelDelay": "延迟",
|
||||
"labelUsername": "用户名",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "原因",
|
||||
"labelSource": "来源",
|
||||
"statusCrashed": "已崩溃",
|
||||
"statusHigh": "过高",
|
||||
"statusSuccess": "成功",
|
||||
"statusFailed": "失败",
|
||||
"statusDown": "断开",
|
||||
"statusUp": "恢复"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
@@ -67,16 +67,12 @@
|
||||
"success": "成功",
|
||||
"lastOnline": "上次上線",
|
||||
"lastSubFetch": "上次取得訂閱",
|
||||
"getVersion": "獲取版本",
|
||||
"install": "安裝",
|
||||
"clients": "客戶端",
|
||||
"usage": "使用情況",
|
||||
"twoFactorCode": "代碼",
|
||||
"remained": "剩餘",
|
||||
"security": "安全",
|
||||
"emptyDnsDesc": "未添加 DNS 伺服器。",
|
||||
"emptyFakeDnsDesc": "未添加 Fake DNS 伺服器。",
|
||||
"emptyBalancersDesc": "未添加負載平衡器。",
|
||||
"somethingWentWrong": "發生錯誤",
|
||||
"subscription": {
|
||||
"title": "訂閱資訊",
|
||||
@@ -86,7 +82,6 @@
|
||||
"uploaded": "已上傳",
|
||||
"expiry": "到期",
|
||||
"totalQuota": "總配額",
|
||||
"individualLinks": "個別連結",
|
||||
"active": "啟用",
|
||||
"inactive": "停用",
|
||||
"unlimited": "無限制",
|
||||
@@ -136,9 +131,6 @@
|
||||
"xrayStatus": "Xray",
|
||||
"stopXray": "停止",
|
||||
"restartXray": "重新啟動",
|
||||
"xraySwitch": "版本",
|
||||
"xrayUpdates": "Xray 更新",
|
||||
"xraySwitchClickDesk": "請謹慎選擇,因為較舊版本可能與當前配置不相容",
|
||||
"updatePanel": "更新面板",
|
||||
"panelUpdateDesc": "這將把 3X-UI 更新到最新版本並重新啟動面板服務。",
|
||||
"currentPanelVersion": "目前面板版本",
|
||||
@@ -200,9 +192,6 @@
|
||||
"download": "下載",
|
||||
"sent": "已發送",
|
||||
"received": "已接收",
|
||||
"xraySwitchVersionDialog": "您確定要變更 Xray 版本嗎?",
|
||||
"xraySwitchVersionDialogDesc": "這將會把 Xray 版本變更為 #version#。",
|
||||
"xraySwitchVersionPopover": "Xray 更新成功",
|
||||
"panelUpdateDialog": "您確定要更新面板嗎?",
|
||||
"panelUpdateDialogDesc": "這將把 3X-UI 更新到 #version# 並重新啟動面板服務。",
|
||||
"panelUpdateStartedPopover": "面板更新已開始",
|
||||
@@ -210,24 +199,6 @@
|
||||
"panelUpdateFailedDesc": "更新未成功完成。請檢查伺服器日誌,或在命令列執行「x-ui update」。",
|
||||
"panelUpdateUnknownTitle": "無法確認更新是否已完成",
|
||||
"panelUpdateUnknownDesc": "面板未能及時回報結果。請重新整理以檢查目前版本,或檢查伺服器日誌。",
|
||||
"geofileUpdateDialog": "您確定要更新地理檔案嗎?",
|
||||
"geofileUpdateDialogDesc": "這將更新 #filename# 檔案。",
|
||||
"geofilesUpdateDialogDesc": "這將更新所有文件。",
|
||||
"geofilesUpdateAll": "全部更新",
|
||||
"geofileUpdatePopover": "地理檔案更新成功",
|
||||
"geodataTitle": "Geodata 自動更新",
|
||||
"geodataHint": "Xray 會按排程下載這些檔案並熱重載,無需重啟。URL 必須為 HTTPS。檔案必須已存在於 bin 目錄中,Xray 才能對其更新。",
|
||||
"geodataCron": "更新排程 (cron)",
|
||||
"geodataOutbound": "透過出站下載(可選)",
|
||||
"geodataFile": "檔案名稱",
|
||||
"geodataAddFile": "新增檔案",
|
||||
"geodataSaveRestart": "儲存並重啟 Xray",
|
||||
"geodataConfirmTitle": "儲存 geodata 設定?",
|
||||
"geodataConfirmContent": "將更新 Xray 設定範本並重啟 Xray。",
|
||||
"geodataInvalidUrl": "每個檔案都需要 HTTPS 位址。",
|
||||
"geodataInvalidFile": "檔案名稱必須是純檔名,例如 geosite_custom.dat(不能包含路徑)。",
|
||||
"geodataInvalidCron": "Cron 必須為 5 段,例如 0 4 * * *",
|
||||
"geodataEmpty": "尚未設定檔案。路由規則中可透過 ext:geosite_custom.dat:category 引用檔案。",
|
||||
"dontRefresh": "安裝中,請勿重新整理此頁面",
|
||||
"logs": "記錄",
|
||||
"accessLogs": "存取記錄",
|
||||
@@ -1082,7 +1053,6 @@
|
||||
"warnDefaultBasePath": "預設根路徑 \"/\" 廣為人知 — 請更改為隨機路徑。",
|
||||
"warnDefaultSubPath": "預設訂閱路徑 \"/sub/\" 廣為人知 — 請更改。",
|
||||
"warnDefaultJsonPath": "預設 JSON 訂閱路徑 \"/json/\" 廣為人知 — 請更改。",
|
||||
"TGBotSettings": "Telegram 機器人",
|
||||
"panelListeningIP": "面板監聽 IP",
|
||||
"panelListeningIPDesc": "預設留空監聽所有 IP",
|
||||
"panelListeningDomain": "面板監聽域名",
|
||||
@@ -1107,43 +1077,12 @@
|
||||
"currentPassword": "原密碼",
|
||||
"newUsername": "新使用者名稱",
|
||||
"newPassword": "新密碼",
|
||||
"telegramBotEnable": "啟用 Telegram 機器人",
|
||||
"telegramBotEnableDesc": "啟用 Telegram 機器人功能",
|
||||
"telegramToken": "Telegram Token",
|
||||
"telegramTokenDesc": "從 '{'@'}BotFather' 獲取的 Telegram 機器人令牌",
|
||||
"telegramProxy": "SOCKS 代理",
|
||||
"telegramProxyDesc": "啟用 SOCKS5 代理連線到 Telegram(根據指南調整設定)",
|
||||
"telegramAPIServer": "Telegram API 伺服器",
|
||||
"telegramAPIServerDesc": "要使用的 Telegram API 伺服器。留空以使用預設伺服器。",
|
||||
"telegramChatId": "管理員聊天 ID",
|
||||
"telegramChatIdDesc": "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 {'@'}userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)",
|
||||
"telegramNotifyTime": "通知時間",
|
||||
"telegramNotifyTimeDesc": "Telegram 機器人傳送週期性報告的頻率。選擇預設間隔,或選擇「自訂」以輸入 crontab 運算式。",
|
||||
"notifyTime": {
|
||||
"every": "@every — 依間隔重複",
|
||||
"hourly": "@hourly — 每小時",
|
||||
"daily": "@daily — 每天 00:00",
|
||||
"weekly": "@weekly — 每週",
|
||||
"monthly": "@monthly — 每月",
|
||||
"custom": "自訂 (crontab)",
|
||||
"seconds": "秒",
|
||||
"minutes": "分鐘",
|
||||
"hours": "小時",
|
||||
"interval": "間隔",
|
||||
"unit": "單位"
|
||||
},
|
||||
"tgNotifyBackup": "資料庫備份",
|
||||
"tgNotifyBackupDesc": "傳送帶有報告的資料庫備份檔案",
|
||||
"tgNotifyLogin": "登入通知",
|
||||
"tgNotifyLoginDesc": "當有人試圖登入你的面板時顯示使用者名稱、IP 地址和時間",
|
||||
"sessionMaxAge": "會話時長",
|
||||
"sessionMaxAgeDesc": "保持登入狀態的時長(單位:分鐘)",
|
||||
"expireTimeDiff": "到期通知閾值",
|
||||
"expireTimeDiffDesc": "達到此閾值時,將收到有關到期時間的通知(單位:天)",
|
||||
"trafficDiff": "流量耗盡閾值",
|
||||
"trafficDiffDesc": "達到此閾值時,將收到有關流量耗盡的通知(單位:GB)",
|
||||
"tgNotifyCpu": "CPU 負載通知閾值",
|
||||
"tgNotifyCpuDesc": "CPU 負載超過此閾值時,將收到通知(單位:%)",
|
||||
"timeZone": "時區",
|
||||
"timeZoneDesc": "定時任務將按照該時區的時間執行",
|
||||
"subSettings": "訂閱設定",
|
||||
@@ -1208,10 +1147,6 @@
|
||||
"subEncryptDesc": "訂閱服務返回的內容將採用 Base64 編碼",
|
||||
"subURI": "反向代理 URI",
|
||||
"subURIDesc": "用於訂閱連結和 QR 碼的完整基礎 URL(scheme://域名[:埠]/路徑/),會取代監聽域名/監聽埠使用。當訂閱透過反向代理存取,或使用與上面不同的域名/埠存取時,請設定此項。",
|
||||
"externalTrafficInformEnable": "外部交通通知",
|
||||
"externalTrafficInformEnableDesc": "每次流量更新時通知外部 API。",
|
||||
"externalTrafficInformURI": "外部流量通知 URI",
|
||||
"externalTrafficInformURIDesc": "流量更新將會傳送到此 URI",
|
||||
"restartXrayOnClientDisable": "用戶自動停用後重新啟動 Xray",
|
||||
"restartXrayOnClientDisableDesc": "當用戶因到期或流量上限而被自動停用時,重新啟動 Xray。",
|
||||
"fragment": "分片",
|
||||
@@ -1221,38 +1156,6 @@
|
||||
"noisesSett": "Noises 設定",
|
||||
"trustedProxyCidrs": "信任代理 CIDR",
|
||||
"trustedProxyCidrsDesc": "允許設定轉發 host、proto 與客戶端 IP 標頭的 IP/CIDR(逗號分隔)。",
|
||||
"ldap": {
|
||||
"enable": "啟用 LDAP 同步",
|
||||
"host": "LDAP host",
|
||||
"port": "LDAP 連接埠",
|
||||
"useTls": "使用 TLS (LDAPS)",
|
||||
"skipTlsVerify": "略過 TLS 憑證驗證",
|
||||
"skipTlsVerifyDesc": "不安全 — 停用伺服器憑證驗證。僅用於內部/不受信任的 CA。",
|
||||
"bindDn": "Bind DN",
|
||||
"passwordConfigured": "已設定;留空以保留目前密碼。",
|
||||
"passwordUnconfigured": "未設定。",
|
||||
"passwordPlaceholder": "已設定 - 輸入新值以取代",
|
||||
"baseDn": "Base DN",
|
||||
"userFilter": "使用者篩選",
|
||||
"userAttr": "使用者屬性 (username/email)",
|
||||
"vlessField": "VLESS flag 屬性",
|
||||
"flagField": "通用 flag 屬性 (選用)",
|
||||
"flagFieldDesc": "若設定,將覆寫 VLESS flag — 如 shadowInactive。",
|
||||
"truthyValues": "Truthy 值",
|
||||
"truthyValuesDesc": "以逗號分隔;預設: true,1,yes,on",
|
||||
"invertFlag": "反轉 flag",
|
||||
"invertFlagDesc": "當屬性表示已停用時啟用 (如 shadowInactive)。",
|
||||
"syncSchedule": "同步排程",
|
||||
"syncScheduleDesc": "類 cron 字串,如 @every 1m",
|
||||
"inboundTags": "入站標籤",
|
||||
"inboundTagsDesc": "允許 LDAP 同步自動建立或刪除客戶端的入站。",
|
||||
"noInbounds": "未找到入站。請先在「入站」中建立。",
|
||||
"autoCreate": "自動建立客戶端",
|
||||
"autoDelete": "自動刪除客戶端",
|
||||
"defaultTotalGb": "預設總流量 (GB)",
|
||||
"defaultExpiryDays": "預設到期 (天)",
|
||||
"defaultIpLimit": "預設 IP 限制"
|
||||
},
|
||||
"subFormats": {
|
||||
"finalMask": "Final Mask",
|
||||
"finalMaskDesc": "將 Xray finalmask TCP/UDP 遮罩和 QUIC 參數注入每個產生的 Xray JSON 設定。需要支援 Xray JSON 訂閱的用戶端應用程式和較新的 Xray 核心。",
|
||||
@@ -1276,31 +1179,15 @@
|
||||
"muxSett": "複用器設定",
|
||||
"direct": "直接連線",
|
||||
"directDesc": "直接與特定國家的域或 IP 範圍建立連線",
|
||||
"notifications": "通知",
|
||||
"certs": "證書",
|
||||
"externalTraffic": "外部流量",
|
||||
"dateAndTime": "日期和時間",
|
||||
"proxyAndServer": "代理和伺服器",
|
||||
"intervals": "間隔",
|
||||
"information": "資訊",
|
||||
"profile": "資料",
|
||||
"language": "語言",
|
||||
"telegramBotLanguage": "Telegram 機器人語言",
|
||||
"security": {
|
||||
"admin": "管理員憑證",
|
||||
"twoFactor": "雙重驗證",
|
||||
"twoFactorEnable": "啟用 2FA",
|
||||
"twoFactorEnableDesc": "增加額外的驗證層以提高安全性。",
|
||||
"twoFactorModalSetTitle": "啟用雙重認證",
|
||||
"twoFactorModalDeleteTitle": "停用雙重認證",
|
||||
"twoFactorModalSteps": "要設定雙重認證,請執行以下步驟:",
|
||||
"twoFactorModalFirstStep": "1. 在認證應用程式中掃描此 QR 碼,或複製 QR 碼附近的令牌並貼到應用程式中",
|
||||
"twoFactorModalSecondStep": "2. 輸入應用程式中的驗證碼",
|
||||
"twoFactorModalRemoveStep": "輸入應用程式中的驗證碼以移除雙重認證。",
|
||||
"twoFactorModalChangeCredentialsTitle": "更改憑證",
|
||||
"twoFactorModalChangeCredentialsStep": "輸入應用程式中的代碼以更改管理員憑證。",
|
||||
"twoFactorModalSetSuccess": "雙重身份驗證已成功建立",
|
||||
"twoFactorModalDeleteSuccess": "雙重身份驗證已成功刪除",
|
||||
"twoFactorModalError": "驗證碼錯誤",
|
||||
"show": "顯示",
|
||||
"hide": "隱藏",
|
||||
@@ -1323,36 +1210,6 @@
|
||||
"getOutboundTrafficError": "取得出站流量錯誤",
|
||||
"resetOutboundTrafficError": "重設出站流量錯誤"
|
||||
},
|
||||
"smtpSettings": "SMTP 設定",
|
||||
"smtpEnable": "啟用電子郵件通知",
|
||||
"smtpEnableDesc": "透過 SMTP 啟用電子郵件通知",
|
||||
"smtpHost": "SMTP 主機",
|
||||
"smtpHostDesc": "SMTP 伺服器主機名稱(例如 smtp.gmail.com)",
|
||||
"smtpPort": "SMTP 連接埠",
|
||||
"smtpPortDesc": "SMTP 伺服器連接埠(預設:587)",
|
||||
"smtpUsername": "SMTP 使用者名稱",
|
||||
"smtpUsernameDesc": "SMTP 驗證使用者名稱",
|
||||
"smtpFrom": "寄件者位址 (From)",
|
||||
"smtpFromDesc": "郵件 From 標頭使用的位址。留空則使用使用者名稱。",
|
||||
"smtpFromName": "寄件者名稱 (From)",
|
||||
"smtpFromNameDesc": "From 標頭中顯示在位址前的可選顯示名稱。",
|
||||
"smtpPassword": "SMTP 密碼",
|
||||
"smtpPasswordDesc": "SMTP 驗證密碼",
|
||||
"smtpTo": "收件人",
|
||||
"smtpToDesc": "以逗號分隔的收件人電子郵件地址",
|
||||
"emailSettings": "電子郵件",
|
||||
"emailNotifications": "通知",
|
||||
"smtpEventBusNotify": "電子郵件事件通知",
|
||||
"smtpEventBusNotifyDesc": "選擇觸發電子郵件通知的事件",
|
||||
"tgEventBusNotify": "Telegram 事件通知",
|
||||
"tgEventBusNotifyDesc": "選擇觸發 Telegram 通知的事件",
|
||||
"testSmtp": "傳送測試郵件",
|
||||
"testTgBot": "傳送測試訊息",
|
||||
"eventGroupOutbound": "出站",
|
||||
"eventGroupXray": "Xray 核心",
|
||||
"eventGroupSystem": "系統",
|
||||
"eventGroupSecurity": "安全性",
|
||||
"eventGroupNode": "節點",
|
||||
"eventOutboundDown": "中斷",
|
||||
"eventOutboundUp": "恢復",
|
||||
"eventXrayCrash": "當機",
|
||||
@@ -1360,36 +1217,7 @@
|
||||
"eventNodeUp": "上線",
|
||||
"eventCPUHigh": "CPU 偏高(%)",
|
||||
"requestFailed": "請求失敗",
|
||||
"smtpEncryption": "加密",
|
||||
"smtpEncryptionDesc": "SMTP 連線加密方式",
|
||||
"smtpEncryptionNone": "無(純文字)",
|
||||
"smtpEncryptionStartTLS": "STARTTLS",
|
||||
"smtpEncryptionTLS": "TLS(隱含)",
|
||||
"smtpStageConnect": "連線",
|
||||
"smtpStageAuth": "驗證",
|
||||
"smtpStageSend": "傳送",
|
||||
"smtpTestSuccess": "測試郵件已成功傳送",
|
||||
"smtpHostNotConfigured": "尚未設定 SMTP 主機",
|
||||
"smtpNoRecipients": "尚未設定收件人",
|
||||
"smtpFromNotConfigured": "未設定 SMTP 寄件人地址",
|
||||
"eventLoginAttempt": "登入嘗試",
|
||||
"telegramTokenConfigured": "已設定;留空以保留目前的權杖。",
|
||||
"telegramTokenPlaceholder": "已設定 - 輸入新權杖以取代",
|
||||
"smtpPasswordConfigured": "已設定;留空以保留目前的密碼。",
|
||||
"smtpPasswordPlaceholder": "已設定 - 輸入新密碼以取代",
|
||||
"smtpNotInitialized": "SMTP 尚未初始化",
|
||||
"tgBotNotEnabled": "Telegram 機器人未啟用",
|
||||
"tgTestFailed": "Telegram 測試失敗",
|
||||
"tgTestSuccess": "測試訊息已傳送至 Telegram",
|
||||
"tgBotNotRunning": "Telegram 機器人未執行",
|
||||
"smtpErrorAuth": "驗證失敗 — 請檢查使用者名稱和密碼",
|
||||
"smtpErrorStarttls": "伺服器需要 STARTTLS — 請變更加密類型",
|
||||
"smtpErrorTls": "伺服器需要 TLS — 請變更加密類型",
|
||||
"smtpErrorRefused": "連線遭拒 — 請檢查主機和連接埠",
|
||||
"smtpErrorTimeout": "連線逾時 — 無法連線至主機",
|
||||
"smtpErrorRelay": "伺服器拒絕從此地址傳送",
|
||||
"smtpErrorEof": "連線已被伺服器關閉",
|
||||
"smtpErrorUnknown": "SMTP 錯誤:{{ .Error }}",
|
||||
"eventMemoryHigh": "記憶體使用率高 (%)",
|
||||
"remarkTemplate": "備註範本",
|
||||
"remarkTemplateDesc": "設定後,這將取代每個訂閱連結的備註模型——使用變數標記撰寫您自己的格式(使用按鈕來插入)。留空則使用上方的模型。",
|
||||
@@ -1483,8 +1311,6 @@
|
||||
"directdomains": "直接域名",
|
||||
"ipv4Routing": "IPv4 路由",
|
||||
"ipv4RoutingDesc": "此選項將僅通過 IPv4 路由到目標域",
|
||||
"Template": "高階 Xray 配置模板",
|
||||
"TemplateDesc": "最終的 Xray 配置檔案將基於此模板生成",
|
||||
"FreedomStrategy": "Freedom 協議策略",
|
||||
"FreedomStrategyDesc": "設定 Freedom 協議中網路的輸出策略",
|
||||
"FreedomHappyEyeballs": "Freedom Happy Eyeballs (IPv4/IPv6)",
|
||||
@@ -1500,11 +1326,6 @@
|
||||
"Balancers": "負載均衡",
|
||||
"balancerTagRequired": "標籤為必填",
|
||||
"balancerSelectorRequired": "至少選擇一個出站",
|
||||
"balancerLive": "目前目標",
|
||||
"balancerOverride": "強制指定",
|
||||
"balancerOverridePh": "自動(策略)",
|
||||
"balancerLiveRefresh": "重新整理負載均衡器即時狀態",
|
||||
"balancerNotRunning": "此負載均衡器在執行中的 Xray 未啟用 — 請先儲存變更或啟動 Xray",
|
||||
"routeTester": "路由測試",
|
||||
"routeTesterDesc": "向執行中的 Xray 查詢某個連線將使用哪個出站。不會傳送真實流量 — 結果直接來自即時路由引擎。",
|
||||
"routeTesterDest": "網域或 IP",
|
||||
@@ -1516,7 +1337,6 @@
|
||||
"routeTesterViaBalancer": "經由負載均衡器",
|
||||
"routeTesterDefaultOutbound": "無路由規則匹配 — 流量將導向預設(第一個)出站。",
|
||||
"Routings": "路由規則",
|
||||
"completeTemplate": "全部",
|
||||
"logLevel": "日誌級別",
|
||||
"logLevelDesc": "錯誤日誌的日誌級別,用於指示需要記錄的資訊",
|
||||
"accessLog": "訪問日誌",
|
||||
@@ -1544,7 +1364,6 @@
|
||||
"source": "來源",
|
||||
"dest": "目的地址",
|
||||
"inbound": "入站",
|
||||
"balancer": "負載均衡",
|
||||
"useComma": "逗號分隔的項目"
|
||||
},
|
||||
"routing": {
|
||||
@@ -1725,32 +1544,7 @@
|
||||
"alreadyAdded": "此伺服器已在出站清單中。若要換新金鑰請按 {reset}。",
|
||||
"provisionFailed": "無法產生 PIA 出站,請重試。"
|
||||
},
|
||||
"tabBalancerSettings": "負載平衡設定",
|
||||
"tabObservatory": "觀測器",
|
||||
"observatory": {
|
||||
"autoManaged": "觀測器會根據你的負載平衡器自動管理。可在下方調整探測方式;被觀測的出站會跟隨負載平衡器的選擇器。",
|
||||
"emptyHint": "目前沒有作用中的連線觀測器。當你建立 Least Ping 或 Least Load 負載平衡器,或帶有 fallback 的 Random / Round-robin 負載平衡器時,會自動新增一個,讓依賴觀測器的負載平衡器能在選擇目標前檢查出站健康狀態。",
|
||||
"mixedLegacy": "此設定同時包含 Observatory 與 Burst Observatory。Xray 只使用一個全域觀測器,因此不支援這種舊式混合狀態;儲存負載平衡器時會將其正規化為單一觀測器。",
|
||||
"subjectSelector": "被觀測的出站",
|
||||
"subjectSelectorDesc": "此觀測器探測的出站標籤。會根據你的負載平衡器自動管理。",
|
||||
"probeURL": "探測 URL",
|
||||
"probeURLDesc": "用於量測每個出站而請求的 URL,應回傳 HTTP 204。",
|
||||
"probeInterval": "探測間隔",
|
||||
"probeIntervalDesc": "每個出站的探測頻率,例如 30s、1m、2h45m。",
|
||||
"enableConcurrency": "並行探測",
|
||||
"enableConcurrencyDesc": "一次探測所有被觀測的出站,而非逐一探測。較快,但在網路上更明顯。",
|
||||
"destination": "探測目標",
|
||||
"destinationDesc": "用於量測每個出站而請求的 URL,應回傳 HTTP 204。",
|
||||
"connectivity": "連線檢查",
|
||||
"connectivityDesc": "選用的本機網路檢查 URL,僅在目標失敗後才嘗試。留空則略過。",
|
||||
"interval": "探測間隔",
|
||||
"intervalDesc": "每個出站兩次探測之間的平均時間,例如 1m。最小 10s。",
|
||||
"timeout": "探測逾時",
|
||||
"timeoutDesc": "判定探測失敗前的等待時間,例如 5s。",
|
||||
"sampling": "取樣數量",
|
||||
"samplingDesc": "為每個出站評分而保留的最近探測結果數量。",
|
||||
"httpMethod": "HTTP 方法",
|
||||
"httpMethodDesc": "探測所用的 HTTP 方法。",
|
||||
"deleteAlsoObservatory": "這是最後一個使用 Observatory 的負載平衡器,因此它也會一併被移除。",
|
||||
"deleteAlsoBurst": "這是最後一個使用 Burst Observatory 的負載平衡器,因此它也會一併被移除。"
|
||||
},
|
||||
@@ -1762,25 +1556,7 @@
|
||||
},
|
||||
"balancer": {
|
||||
"balancerStrategy": "策略",
|
||||
"tag": "標籤",
|
||||
"tagDuplicate": "該標籤已被其他均衡器使用",
|
||||
"tagPlaceholder": "唯一均衡器標籤",
|
||||
"selector": "選擇器",
|
||||
"fallback": "備用負載平衡器",
|
||||
"cycleTooltip": "循環: {path} → (回到 {start})",
|
||||
"expected": "期望",
|
||||
"expectedPlaceholder": "最佳節點數",
|
||||
"maxRtt": "最大 RTT",
|
||||
"tolerance": "容差",
|
||||
"baselines": "基準值",
|
||||
"costs": "成本設定",
|
||||
"costMatch": "標籤比對模式",
|
||||
"costValue": "權重",
|
||||
"costRegexp": "正規表示式比對",
|
||||
"balancerDeleteInUse": "無法刪除此負載平衡器 — 它被用作以下負載平衡器的備用:{names}",
|
||||
"balancerFallbackCycle": "無法將此負載平衡器設定為備用 — 這會建立循環依賴。",
|
||||
"balancerFallbackInfo": "流量將透過以下路徑路由:負載平衡器 → Loopback → 伺服器 → 目標負載平衡器 → 出站連線。這會增加一個經過伺服器的額外跳轉,可能會引入輕微延遲。",
|
||||
"fallbackBalancerHint": "選擇另一個負載平衡器作為備用",
|
||||
"reservedPrefix": "_bl_ 前綴保留給內部負載均衡器迴圈物件"
|
||||
},
|
||||
"wireguard": {
|
||||
@@ -1883,55 +1659,8 @@
|
||||
"usage": "使用",
|
||||
"addOutbound": "新增出站"
|
||||
},
|
||||
"dns": {
|
||||
"enable": "啟用 DNS",
|
||||
"enableDesc": "啟用內建 DNS 伺服器",
|
||||
"tag": "DNS 入站標籤",
|
||||
"tagDesc": "此標籤將在路由規則中可用作入站標籤",
|
||||
"clientIp": "客戶端 IP",
|
||||
"clientIpDesc": "用於在 DNS 查詢期間通知伺服器指定的 IP 位置",
|
||||
"disableCache": "禁用快取",
|
||||
"disableCacheDesc": "禁用 DNS 快取",
|
||||
"disableFallback": "禁用回退",
|
||||
"disableFallbackDesc": "禁用回退 DNS 查詢",
|
||||
"disableFallbackIfMatch": "匹配時禁用回退",
|
||||
"disableFallbackIfMatchDesc": "當 DNS 伺服器的匹配域名列表命中時,禁用回退 DNS 查詢",
|
||||
"enableParallelQuery": "啟用並行查詢",
|
||||
"enableParallelQueryDesc": "啟用並行 DNS 查詢到多個伺服器以實現更快的解析",
|
||||
"strategy": "查詢策略",
|
||||
"strategyDesc": "解析域名的總體策略",
|
||||
"add": "新增伺服器",
|
||||
"edit": "編輯伺服器",
|
||||
"domains": "網域",
|
||||
"expectIPs": "預期 IP",
|
||||
"unexpectIPs": "意外 IP",
|
||||
"useSystemHosts": "使用系統 Hosts",
|
||||
"useSystemHostsDesc": "使用已安裝系統的 hosts 檔案",
|
||||
"serveStale": "提供過期結果",
|
||||
"serveStaleDesc": "在背景重新整理時傳回過期的快取結果",
|
||||
"serveExpiredTTL": "過期 TTL",
|
||||
"serveExpiredTTLDesc": "過期快取項目的有效期(秒);0 = 永不過期",
|
||||
"timeoutMs": "逾時 (毫秒)",
|
||||
"skipFallback": "跳過回退",
|
||||
"finalQuery": "最終查詢",
|
||||
"hosts": "主機對應",
|
||||
"hostsAdd": "新增 Host",
|
||||
"hostsEmpty": "未定義任何 Host",
|
||||
"hostsDomain": "網域 (例如 domain:example.com)",
|
||||
"hostsValues": "IP 或網域 — 輸入後按 Enter",
|
||||
"usePreset": "使用範本",
|
||||
"dnsPresetTitle": "DNS 範本",
|
||||
"dnsPresetFamily": "家庭",
|
||||
"clearAll": "全部刪除",
|
||||
"clearAllTitle": "刪除所有 DNS 伺服器?",
|
||||
"clearAllConfirm": "此操作將從清單中刪除所有 DNS 伺服器,無法復原。",
|
||||
"dnsLeakWarning": "DNS 可能透過 localhost、明文 UDP/TCP、本機模式 DoH/DoQ、回退查詢或 EDNS client IP 洩漏。重視隱私時請使用經路由的 DoH、在 hosts 固定解析器,並停用回退。"
|
||||
},
|
||||
"fakedns": {
|
||||
"add": "新增假 DNS",
|
||||
"ipPool": "IP 池子網",
|
||||
"poolSize": "池大小"
|
||||
},
|
||||
"dns": {},
|
||||
"fakedns": {},
|
||||
"defaultOutbound": "預設出站",
|
||||
"defaultOutboundDesc": "未符合任何路由規則的流量走此出站(清單中的第一個出站)。"
|
||||
},
|
||||
@@ -2055,198 +1784,7 @@
|
||||
}
|
||||
},
|
||||
"tgbot": {
|
||||
"keyboardClosed": "❌ 自定義鍵盤已關閉!",
|
||||
"noResult": "❗ 沒有結果!",
|
||||
"noQuery": "❌ 未找到查詢!請再次使用該命令!",
|
||||
"wentWrong": "❌ 出了點問題!",
|
||||
"noIpRecord": "❗ 沒有 IP 記錄!",
|
||||
"noInbounds": "❗ 未找到入站!",
|
||||
"unlimited": "♾ 無限(重置)",
|
||||
"add": "添加",
|
||||
"month": "月",
|
||||
"months": "月",
|
||||
"days": "天",
|
||||
"hours": "小時",
|
||||
"minutes": "分鐘",
|
||||
"unknown": "未知",
|
||||
"inbounds": "入站",
|
||||
"clients": "客戶端",
|
||||
"offline": "🔴 離線",
|
||||
"online": "🟢 上線",
|
||||
"commands": {
|
||||
"unknown": "❗ 未知命令",
|
||||
"pleaseChoose": "👇 請選擇:\r\n",
|
||||
"help": "🤖 歡迎使用本機器人!它旨在為您提供來自伺服器的特定資料,並允許您進行必要的修改。\r\n\r\n",
|
||||
"start": "👋 你好,<i>{{ .Firstname }}</i>。\r\n",
|
||||
"welcome": "🤖 歡迎來到 <b>{{ .Hostname }}</b> 管理機器人。\r\n",
|
||||
"status": "✅ 機器人正常執行!",
|
||||
"usage": "❗ 請輸入要搜尋的文字!",
|
||||
"getID": "🆔 您的 ID 為:<code>{{ .ID }}</code>",
|
||||
"helpAdminCommands": "要重新啟動 Xray Core:\r\n<code>/restart</code>\r\n\r\n要搜尋客戶電子郵件:\r\n<code>/usage [電子郵件]</code>\r\n\r\n要搜尋入站(帶有客戶統計資料):\r\n<code>/inbound [備註]</code>\r\n\r\nTelegram 聊天 ID:\r\n<code>/id</code>",
|
||||
"helpClientCommands": "要搜尋統計資料,請使用以下命令:\r\n<code>/usage [電子郵件]</code>\r\n\r\nTelegram 聊天 ID:\r\n<code>/id</code>",
|
||||
"restartUsage": "\r\n\r\n<code>/restart</code>",
|
||||
"restartSuccess": "✅ 操作成功!",
|
||||
"restartFailed": "❗ 操作錯誤。\r\n\r\n<code>錯誤: {{ .Error }}</code>.",
|
||||
"xrayNotRunning": "❗ Xray Core 未運行。",
|
||||
"startDesc": "顯示主選單",
|
||||
"helpDesc": "機器人幫助",
|
||||
"statusDesc": "檢查機器人狀態",
|
||||
"idDesc": "顯示您的 Telegram ID",
|
||||
"usageDesc": "查看客戶端用量:/usage 郵箱",
|
||||
"inboundDesc": "搜尋入站:/inbound 備註(管理員)",
|
||||
"restartDesc": "重啟 Xray 核心(管理員)",
|
||||
"clearallDesc": "重置所有客戶端流量(管理員)"
|
||||
},
|
||||
"messages": {
|
||||
"cpuThreshold": "CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%",
|
||||
"selectUserFailed": "❌ 使用者選擇錯誤!",
|
||||
"userSaved": "✅ 電報使用者已儲存。",
|
||||
"loginSuccess": "✅ 成功登入到面板。\r\n",
|
||||
"loginFailed": "❗️ 面板登入失敗。\r\n",
|
||||
"report": "🕰 定時報告:{{ .RunTime }}\r\n",
|
||||
"datetime": "⏰ 日期時間:{{ .DateTime }}\r\n",
|
||||
"hostname": "💻 主機: {{ .Hostname }}\r\n",
|
||||
"version": "🚀 X-UI 版本:{{ .Version }}\r\n",
|
||||
"xrayVersion": "📡 Xray 版本: {{ .XrayVersion }}\r\n",
|
||||
"ipv6": "🌐 IPv6: {{ .IPv6 }}\r\n",
|
||||
"ipv4": "🌐 IPv4: {{ .IPv4 }}\r\n",
|
||||
"ip": "🌐 IP: {{ .IP }}\r\n",
|
||||
"ips": "🔢 IPs:\r\n{{ .IPs }}\r\n",
|
||||
"serverUpTime": "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n",
|
||||
"serverLoad": "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n",
|
||||
"serverMemory": "📋 RAM: {{ .Current }}/{{ .Total }}\r\n",
|
||||
"tcpCount": "🔹 TCP: {{ .Count }}\r\n",
|
||||
"udpCount": "🔸 UDP: {{ .Count }}\r\n",
|
||||
"traffic": "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n",
|
||||
"xrayStatus": "ℹ️ 狀態: {{ .State }}\r\n",
|
||||
"username": "👤 使用者名稱:{{ .Username }}\r\n",
|
||||
"reason": "❗️ 原因:{{ .Reason }}\r\n",
|
||||
"time": "⏰ 時間:{{ .Time }}\r\n",
|
||||
"inbound": "📍 入站: {{ .Remark }}\r\n",
|
||||
"port": "🔌 連接埠: {{ .Port }}\r\n",
|
||||
"expire": "📅 過期日期:{{ .Time }}\r\n",
|
||||
"expireIn": "📅 剩餘時間:{{ .Time }}\r\n",
|
||||
"active": "💡 啟用:{{ .Enable }}\r\n",
|
||||
"enabled": "🚨 已啟用:{{ .Enable }}\r\n",
|
||||
"online": "🌐 連線狀態:{{ .Status }}\r\n",
|
||||
"lastOnline": "🔙 上次上線: {{ .Time }}\r\n",
|
||||
"email": "📧 電子郵件: {{ .Email }}\r\n",
|
||||
"upload": "🔼 上傳: ↑{{ .Upload }}\r\n",
|
||||
"download": "🔽 下載: ↓{{ .Download }}\r\n",
|
||||
"total": "📊 總計: ↑↓{{ .UpDown }} / {{ .Total }}\r\n",
|
||||
"TGUser": "👤 電報使用者:{{ .TelegramID }}\r\n",
|
||||
"exhaustedCount": "🚨 耗盡的 {{ .Type }} 數量:\r\n",
|
||||
"onlinesCount": "🌐 線上客戶:{{ .Count }}\r\n",
|
||||
"disabled": "🛑 禁用:{{ .Disabled }}\r\n",
|
||||
"depleteSoon": "🔜 即將耗盡:{{ .Deplete }}\r\n\r\n",
|
||||
"backupTime": "🗄 備份時間:{{ .Time }}\r\n",
|
||||
"refreshedOn": "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n",
|
||||
"yes": "✅ 是的",
|
||||
"no": "❌ 否",
|
||||
"received_email": "📧📥 電子郵件已更新。",
|
||||
"received_comment": "💬📥 評論已更新。",
|
||||
"email_prompt": "📧 預設電子郵件: {{ .ClientEmail }}\n\n請輸入您的電子郵件。",
|
||||
"comment_prompt": "💬 預設評論: {{ .ClientComment }}\n\n請輸入您的評論。",
|
||||
"cancel": "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄",
|
||||
"error_add_client": "⚠️ 錯誤:\n\n {{ .error }}",
|
||||
"using_default_value": "好的,我會使用預設值。 😊",
|
||||
"incorrect_input": "您的輸入無效。\n短語應連續輸入,不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫",
|
||||
"AreYouSure": "你確定嗎?🤔",
|
||||
"SuccessResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功",
|
||||
"FailedResetTraffic": "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]",
|
||||
"FinishProcess": "🔚 所有客戶的流量重置已完成。",
|
||||
"eventOutboundDown": "出站 {{ .Tag }} 已中斷",
|
||||
"eventOutboundUp": "出站 {{ .Tag }} 已恢復",
|
||||
"eventErrorDetail": "錯誤:{{ .Error }}",
|
||||
"eventDelayDetail": "延遲:{{ .Delay }} 毫秒",
|
||||
"eventXrayCrash": "Xray 已當機",
|
||||
"eventXrayCrashError": "錯誤:{{ .Error }}",
|
||||
"eventNodeDown": "節點 {{ .Name }} 已離線",
|
||||
"eventNodeUp": "節點 {{ .Name }} 已上線",
|
||||
"eventLoginFallback": "來自 {{ .Source }} 的登入失敗",
|
||||
"memoryThreshold": "記憶體使用率 {{ .Percent }}% 超過閾值 {{ .Threshold }}%"
|
||||
},
|
||||
"buttons": {
|
||||
"closeKeyboard": "❌ 關閉鍵盤",
|
||||
"cancel": "❌ 取消",
|
||||
"cancelReset": "❌ 取消重置",
|
||||
"cancelIpLimit": "❌ 取消 IP 限制",
|
||||
"confirmResetTraffic": "✅ 確認重置流量?",
|
||||
"confirmClearIps": "✅ 確認清除 IP?",
|
||||
"confirmRemoveTGUser": "✅ 確認移除 Telegram 使用者?",
|
||||
"confirmToggle": "✅ 確認啟用/禁用使用者?",
|
||||
"dbBackup": "獲取資料庫備份",
|
||||
"serverUsage": "伺服器使用情況",
|
||||
"getInbounds": "獲取入站資訊",
|
||||
"depleteSoon": "即將耗盡",
|
||||
"clientUsage": "獲取使用情況",
|
||||
"onlines": "線上客戶端",
|
||||
"commands": "命令",
|
||||
"refresh": "🔄 重新整理",
|
||||
"clearIPs": "❌ 清除 IP",
|
||||
"removeTGUser": "❌ 移除 Telegram 使用者",
|
||||
"selectTGUser": "👤 選擇 Telegram 使用者",
|
||||
"selectOneTGUser": "👤 選擇一個 Telegram 使用者:",
|
||||
"resetTraffic": "📈 重置流量",
|
||||
"resetExpire": "📅 更改到期日期",
|
||||
"ipLog": "🔢 IP 日誌",
|
||||
"ipLimit": "🔢 IP 限制",
|
||||
"setTGUser": "👤 設定 Telegram 使用者",
|
||||
"toggle": "🔘 啟用/禁用",
|
||||
"custom": "🔢 自訂",
|
||||
"confirmNumber": "✅ 確認: {{ .Num }}",
|
||||
"confirmNumberAdd": "✅ 確認新增:{{ .Num }}",
|
||||
"limitTraffic": "🚧 流量限制",
|
||||
"getBanLogs": "禁止日誌",
|
||||
"allClients": "所有客戶",
|
||||
"addClient": "新增客戶",
|
||||
"submitDisable": "以停用方式送出 ☑️",
|
||||
"submitEnable": "以啟用方式送出 ✅",
|
||||
"use_default": "🏷️ 使用預設值",
|
||||
"change_email": "⚙️📧 電子郵件",
|
||||
"change_comment": "⚙️💬 評論",
|
||||
"ResetAllTraffics": "重設所有流量",
|
||||
"SortedTrafficUsageReport": "排序過的流量使用報告"
|
||||
},
|
||||
"answers": {
|
||||
"successfulOperation": "✅ 成功!",
|
||||
"errorOperation": "❗ 操作錯誤。",
|
||||
"getInboundsFailed": "❌ 獲取入站資訊失敗。",
|
||||
"getClientsFailed": "❌ 獲取客戶失敗。",
|
||||
"canceled": "❌ {{ .Email }}:操作已取消。",
|
||||
"clientRefreshSuccess": "✅ {{ .Email }}:客戶端重新整理成功。",
|
||||
"IpRefreshSuccess": "✅ {{ .Email }}:IP 重新整理成功。",
|
||||
"TGIdRefreshSuccess": "✅ {{ .Email }}:客戶端的 Telegram 使用者重新整理成功。",
|
||||
"resetTrafficSuccess": "✅ {{ .Email }}:流量已重置成功。",
|
||||
"setTrafficLimitSuccess": "✅ {{ .Email }}: 流量限制儲存成功。",
|
||||
"expireResetSuccess": "✅ {{ .Email }}:過期天數已重置成功。",
|
||||
"resetIpSuccess": "✅ {{ .Email }}:成功儲存 IP 限制數量為 {{ .Count }}。",
|
||||
"clearIpSuccess": "✅ {{ .Email }}:IP 已成功清除。",
|
||||
"getIpLog": "✅ {{ .Email }}:獲取 IP 日誌。",
|
||||
"getUserInfo": "✅ {{ .Email }}:獲取 Telegram 使用者資訊。",
|
||||
"removedTGUserSuccess": "✅ {{ .Email }}:Telegram 使用者已成功移除。",
|
||||
"enableSuccess": "✅ {{ .Email }}:已成功啟用。",
|
||||
"disableSuccess": "✅ {{ .Email }}:已成功禁用。",
|
||||
"askToAddUserId": "未找到您的配置!\r\n請向管理員詢問,在您的配置中使用您的 Telegram 使用者 ChatID。\r\n\r\n您的使用者 ChatID:<code>{{ .TgUserID }}</code>",
|
||||
"chooseClient": "為入站 {{ .Inbound }} 選擇一個客戶",
|
||||
"chooseInbound": "選擇一個入站"
|
||||
}
|
||||
"noIpRecord": "❗ 沒有 IP 記錄!"
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "狀態",
|
||||
"labelOutbound": "出站",
|
||||
"labelNode": "節點",
|
||||
"labelError": "錯誤",
|
||||
"labelDelay": "延遲",
|
||||
"labelUsername": "使用者名稱",
|
||||
"labelIP": "IP",
|
||||
"labelReason": "原因",
|
||||
"labelSource": "來源",
|
||||
"statusCrashed": "已當機",
|
||||
"statusHigh": "偏高",
|
||||
"statusSuccess": "成功",
|
||||
"statusFailed": "失敗",
|
||||
"statusDown": "中斷",
|
||||
"statusUp": "恢復"
|
||||
}
|
||||
"email": {}
|
||||
}
|
||||
|
||||
+11
-21
@@ -305,24 +305,14 @@ const (
|
||||
// startTask schedules background jobs (Xray checks, traffic jobs, cron
|
||||
// jobs) which the panel relies on for periodic maintenance and monitoring.
|
||||
func (s *Server) startTask(restartXray bool, loc *time.Location) {
|
||||
if restartXray {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Warning("start xray failed:", err)
|
||||
}
|
||||
}
|
||||
// Check whether xray is running every second
|
||||
_, _ = s.cron.AddJob(cadenceXrayRunning, job.NewCheckXrayRunningJob())
|
||||
|
||||
// Check if xray needs to be restarted every 30 seconds
|
||||
_, _ = s.cron.AddFunc(cadenceXrayRestart, func() {
|
||||
s.xrayService.ApplyPendingRestart()
|
||||
})
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
_, _ = s.cron.AddJob(cadenceXrayTraffic, job.NewXrayTrafficJob())
|
||||
}()
|
||||
// Lite control-plane build: the local xray lifecycle is retired. The
|
||||
// panel never starts a local core, so the crash watchdog
|
||||
// (CheckXrayRunningJob), the pending-restart ticker
|
||||
// (ApplyPendingRestart) and the local-core traffic collector
|
||||
// (XrayTrafficJob) are not scheduled anymore; node traffic keeps flowing
|
||||
// through NodeTrafficSyncJob from agent reports. The restartXray flag is
|
||||
// retained for signature compatibility and intentionally unused.
|
||||
_ = restartXray
|
||||
|
||||
// Reconcile mtproto (mtg) sidecars and scrape their traffic
|
||||
mtJob := job.NewMtprotoJob()
|
||||
@@ -334,8 +324,9 @@ func (s *Server) startTask(restartXray bool, loc *time.Location) {
|
||||
_, _ = s.cron.AddJob(cadenceAmneziaWG, awgJob)
|
||||
go awgJob.Run()
|
||||
|
||||
// check client ips from log file every 10 sec
|
||||
_, _ = s.cron.AddJob(cadenceClientIPScan, job.NewCheckClientIpJob())
|
||||
// The client-IP limit job only reads the local xray core's online
|
||||
// API, which never exists in this build, so it is not scheduled
|
||||
// (it would idle every 10s against a dead API).
|
||||
|
||||
_, _ = s.cron.AddJob(cadenceNodeHeartbeat, job.NewNodeHeartbeatJob())
|
||||
|
||||
@@ -346,7 +337,6 @@ func (s *Server) startTask(restartXray bool, loc *time.Location) {
|
||||
|
||||
_, _ = s.cron.AddJob(cadenceReapOrphans, job.NewReapSyncOrphansJob())
|
||||
|
||||
|
||||
// check client ips from log file every day
|
||||
_, _ = s.cron.AddJob("@daily", job.NewClearLogsJob())
|
||||
_, _ = s.cron.AddJob(cadenceXrayLogPrune, job.NewPruneXrayLogsJob())
|
||||
|
||||
@@ -61,6 +61,19 @@ type XrayAPI struct {
|
||||
grpcClient *grpc.ClientConn
|
||||
isConnected bool
|
||||
StatsLastValues map[string]int64
|
||||
|
||||
// committedStats is the agent reporting path's last ACKNOWLEDGED
|
||||
// baseline: the raw core counters as of the most recent snapshot the
|
||||
// master confirmed receiving (HTTP 200). It is deliberately separate
|
||||
// from StatsLastValues — GetTraffic (the panel's local accounting)
|
||||
// advances its baseline at read time, while the agent path may only
|
||||
// advance on CommitTrafficSnapshot, so a failed upload never loses
|
||||
// the deltas it carried. committedReady is false until a baseline has
|
||||
// been established (first snapshot committed, or one restored from
|
||||
// disk); while false, snapshots report no deltas, mirroring
|
||||
// GetTraffic's first-poll rule.
|
||||
committedStats map[string]int64
|
||||
committedReady bool
|
||||
}
|
||||
|
||||
// Init connects to the Xray API server.
|
||||
@@ -650,6 +663,114 @@ func (x *XrayAPI) GetTraffic() ([]*Traffic, []*ClientTraffic, error) {
|
||||
return mapToSlice(tagTrafficMap), mapToSlice(emailTrafficMap), nil
|
||||
}
|
||||
|
||||
// TrafficSnapshot is one read of the core counters taken for the agent
|
||||
// reporting path: the deltas accrued since the last COMMITTED baseline,
|
||||
// plus Values, the raw cumulative counters the deltas were computed from.
|
||||
//
|
||||
// Unlike GetTraffic — which advances its baseline as it reads, the right
|
||||
// contract for the panel's local accounting job — a snapshot changes no
|
||||
// state. The caller reports the deltas and only then calls
|
||||
// CommitTrafficSnapshot; if the report fails, the next snapshot recomputes
|
||||
// against the same committed baseline and the unreported traffic is
|
||||
// included again, because the core counters are cumulative and are never
|
||||
// reset by polling.
|
||||
type TrafficSnapshot struct {
|
||||
Traffics []*Traffic
|
||||
ClientTraffics []*ClientTraffic
|
||||
// Values holds every counter returned by the core at snapshot time,
|
||||
// keyed by stat name. CommitTrafficSnapshot installs exactly this map
|
||||
// as the new committed baseline.
|
||||
Values map[string]int64
|
||||
}
|
||||
|
||||
// BaselineValues returns a copy of the baseline CommitTrafficSnapshot
|
||||
// would install for this snapshot, so callers can persist it (the agent
|
||||
// writes it to disk before/while committing).
|
||||
func (s *TrafficSnapshot) BaselineValues() map[string]int64 {
|
||||
out := make(map[string]int64, len(s.Values))
|
||||
for k, v := range s.Values {
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetTrafficSnapshot reads the core counters and computes deltas against
|
||||
// the committed baseline WITHOUT advancing it. Per-stat rules match
|
||||
// GetTraffic exactly: while no baseline has been established the snapshot
|
||||
// reports no deltas (it only proposes one), a counter the baseline has
|
||||
// never seen counts from zero, and a counter below its baseline (the core
|
||||
// restarted) counts from zero as well.
|
||||
func (x *XrayAPI) GetTrafficSnapshot() (*TrafficSnapshot, error) {
|
||||
if x.grpcClient == nil {
|
||||
return nil, common.NewError("xray api is not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
stats, err := x.queryStats(ctx)
|
||||
if err != nil {
|
||||
logger.Debug("Failed to query Xray stats:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snap := &TrafficSnapshot{Values: make(map[string]int64, len(stats))}
|
||||
tagTrafficMap := make(map[string]*Traffic)
|
||||
emailTrafficMap := make(map[string]*ClientTraffic)
|
||||
|
||||
for _, stat := range stats {
|
||||
snap.Values[stat.Name] = stat.Value
|
||||
if !x.committedReady {
|
||||
continue
|
||||
}
|
||||
lastValue, ok := x.committedStats[stat.Name]
|
||||
if !ok || stat.Value < lastValue {
|
||||
lastValue = 0
|
||||
}
|
||||
value := stat.Value - lastValue
|
||||
if matches := trafficRegex.FindStringSubmatch(stat.Name); len(matches) == 4 {
|
||||
processTraffic(matches, value, tagTrafficMap)
|
||||
} else if matches := clientTrafficRegex.FindStringSubmatch(stat.Name); len(matches) == 3 {
|
||||
processClientTraffic(matches, value, emailTrafficMap)
|
||||
}
|
||||
}
|
||||
|
||||
snap.Traffics = mapToSlice(tagTrafficMap)
|
||||
snap.ClientTraffics = mapToSlice(emailTrafficMap)
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
// CommitTrafficSnapshot marks a snapshot as acknowledged by the master:
|
||||
// its Values become the committed baseline that later snapshots diff
|
||||
// against. Installing only the counters the core currently reports also
|
||||
// retires baselines for deleted inbounds/clients, the same end state
|
||||
// GetTraffic's pruning converges to.
|
||||
func (x *XrayAPI) CommitTrafficSnapshot(s *TrafficSnapshot) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
x.committedStats = s.BaselineValues()
|
||||
x.committedReady = true
|
||||
}
|
||||
|
||||
// RestoreTrafficBaseline installs a committed baseline recovered from
|
||||
// persistent storage (the agent saves one after every acknowledged
|
||||
// report), so a restarted agent keeps diffing against the last values the
|
||||
// master confirmed instead of swallowing the unreported tail into a
|
||||
// fresh baseline. An empty map leaves the baseline unestablished: the
|
||||
// next snapshot is the baseline-only first pass, exactly like a first
|
||||
// run with no state file.
|
||||
func (x *XrayAPI) RestoreTrafficBaseline(values map[string]int64) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
x.committedStats = make(map[string]int64, len(values))
|
||||
for k, v := range values {
|
||||
x.committedStats[k] = v
|
||||
}
|
||||
x.committedReady = true
|
||||
}
|
||||
|
||||
// OnlineIP is one source address of a live connection, with the unix time (seconds)
|
||||
// the core last dispatched a link from it.
|
||||
type OnlineIP struct {
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package xray
|
||||
|
||||
import "testing"
|
||||
|
||||
// The snapshot/commit contract is what keeps agent traffic reporting from
|
||||
// losing deltas when the master is unreachable: a snapshot never advances
|
||||
// the baseline, only CommitTrafficSnapshot does. These tests pin that
|
||||
// contract against the scripted stats server, plus the recovery paths
|
||||
// (baseline restore after an agent restart, counter reset after a core
|
||||
// restart) and the isolation from GetTraffic's read-advances semantics.
|
||||
|
||||
func clientUp(t *testing.T, traffics []*ClientTraffic, email string) (int64, bool) {
|
||||
t.Helper()
|
||||
for _, ct := range traffics {
|
||||
if ct.Email == email {
|
||||
return ct.Up, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// TestTrafficSnapshotFirstPassIsBaselineOnly mirrors the GetTraffic rule:
|
||||
// counters may already hold unattributable traffic when reporting starts,
|
||||
// so the first snapshot proposes a baseline and reports nothing — and
|
||||
// committing it is what arms delta reporting.
|
||||
func TestTrafficSnapshotFirstPassIsBaselineOnly(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 5000)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 5300)},
|
||||
})
|
||||
|
||||
snap, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot: %v", err)
|
||||
}
|
||||
if len(snap.ClientTraffics) != 0 {
|
||||
t.Fatalf("first snapshot reported %+v, want baseline only", snap.ClientTraffics[0])
|
||||
}
|
||||
if got := snap.Values["user>>>alice>>>traffic>>>uplink"]; got != 5000 {
|
||||
t.Fatalf("snapshot values = %d, want the raw counter 5000", got)
|
||||
}
|
||||
|
||||
api.CommitTrafficSnapshot(snap)
|
||||
|
||||
snap, err = api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot after commit: %v", err)
|
||||
}
|
||||
up, ok := clientUp(t, snap.ClientTraffics, "alice")
|
||||
if !ok || up != 300 {
|
||||
t.Fatalf("after baseline commit, delta = %d (found %v), want 300", up, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrafficSnapshotWithoutCommitReReports is the core of the outage fix:
|
||||
// while reports fail, the baseline stays put and every later snapshot
|
||||
// re-includes the unreported traffic, so a recovery report carries the
|
||||
// whole accumulated amount — exactly once.
|
||||
func TestTrafficSnapshotWithoutCommitReReports(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 100)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 260)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 700)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 750)},
|
||||
})
|
||||
|
||||
base, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (baseline): %v", err)
|
||||
}
|
||||
api.CommitTrafficSnapshot(base)
|
||||
|
||||
// Two polls whose uploads "fail": no commit happens between them.
|
||||
snap1, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (failed round 1): %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, snap1.ClientTraffics, "alice"); up != 160 {
|
||||
t.Fatalf("first unreported delta = %d, want 160", up)
|
||||
}
|
||||
snap2, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (failed round 2): %v", err)
|
||||
}
|
||||
// Not 440+160 and not just 440: the delta is recomputed from the
|
||||
// committed baseline, covering both failed rounds in one amount.
|
||||
if up, _ := clientUp(t, snap2.ClientTraffics, "alice"); up != 600 {
|
||||
t.Fatalf("accumulated delta after two failed rounds = %d, want 600", up)
|
||||
}
|
||||
|
||||
// The master recovers and acknowledges this report.
|
||||
api.CommitTrafficSnapshot(snap2)
|
||||
|
||||
snap3, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (after recovery): %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, snap3.ClientTraffics, "alice"); up != 50 {
|
||||
t.Fatalf("delta after recovery commit = %d, want only the new 50", up)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrafficSnapshotRestoreBaselineSkipsBaselinePass covers an agent
|
||||
// restart with a persisted baseline: a fresh XrayAPI with the restored
|
||||
// values must diff against them immediately — the traffic that accrued
|
||||
// while the agent was down is reported, not folded into a new baseline.
|
||||
func TestTrafficSnapshotRestoreBaselineSkipsBaselinePass(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 1250)},
|
||||
})
|
||||
api.RestoreTrafficBaseline(map[string]int64{
|
||||
"user>>>alice>>>traffic>>>uplink": 1000,
|
||||
})
|
||||
|
||||
snap, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot: %v", err)
|
||||
}
|
||||
up, ok := clientUp(t, snap.ClientTraffics, "alice")
|
||||
if !ok || up != 250 {
|
||||
t.Fatalf("restored-baseline delta = %d (found %v), want 250 with no baseline-only pass", up, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrafficSnapshotCountsAfterCounterReset: the core restarts and its
|
||||
// counters start over below the committed baseline. The whole new counter
|
||||
// is new traffic — never a negative delta — and after committing, normal
|
||||
// diffing resumes in the new epoch.
|
||||
func TestTrafficSnapshotCountsAfterCounterReset(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 250)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 300)},
|
||||
})
|
||||
api.RestoreTrafficBaseline(map[string]int64{
|
||||
"user>>>alice>>>traffic>>>uplink": 900,
|
||||
})
|
||||
|
||||
snap, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (after reset): %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, snap.ClientTraffics, "alice"); up != 250 {
|
||||
t.Fatalf("delta after counter reset = %d, want 250 counted from zero", up)
|
||||
}
|
||||
api.CommitTrafficSnapshot(snap)
|
||||
|
||||
snap, err = api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot (new epoch delta): %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, snap.ClientTraffics, "alice"); up != 50 {
|
||||
t.Fatalf("delta in new epoch = %d, want 50", up)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrafficSnapshotNewStatCountsFromZero: a counter that appears after
|
||||
// the baseline was committed (client's first use) is entirely new traffic.
|
||||
func TestTrafficSnapshotNewStatCountsFromZero(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{
|
||||
stat("user>>>alice>>>traffic>>>uplink", 180),
|
||||
stat("user>>>bob>>>traffic>>>uplink", 4096),
|
||||
stat("inbound>>>in-443>>>traffic>>>downlink", 7000),
|
||||
},
|
||||
})
|
||||
api.RestoreTrafficBaseline(map[string]int64{
|
||||
"user>>>alice>>>traffic>>>uplink": 100,
|
||||
})
|
||||
|
||||
snap, err := api.GetTrafficSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot: %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, snap.ClientTraffics, "alice"); up != 80 {
|
||||
t.Fatalf("alice delta = %d, want 80", up)
|
||||
}
|
||||
if up, _ := clientUp(t, snap.ClientTraffics, "bob"); up != 4096 {
|
||||
t.Fatalf("bob delta = %d, want his full counter 4096", up)
|
||||
}
|
||||
if len(snap.Traffics) != 1 || snap.Traffics[0].Tag != "in-443" || snap.Traffics[0].Down != 7000 {
|
||||
t.Fatalf("inbound deltas = %+v, want in-443 down 7000 from zero", snap.Traffics)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSnapshotDoesNotDisturbGetTraffic pins the range constraint: the
|
||||
// panel path must keep its exact read-advances-baseline semantics even
|
||||
// when snapshot calls interleave on the same client — the two baselines
|
||||
// are fully independent.
|
||||
func TestSnapshotDoesNotDisturbGetTraffic(t *testing.T) {
|
||||
api := startFakeStats(t, [][]statEntry{
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 100)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 160)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 400)},
|
||||
{stat("user>>>alice>>>traffic>>>uplink", 460)},
|
||||
})
|
||||
|
||||
// Panel poll 1: baseline only.
|
||||
if _, _, err := api.GetTraffic(); err != nil {
|
||||
t.Fatalf("GetTraffic (baseline): %v", err)
|
||||
}
|
||||
// Agent snapshot in between: must not move the panel's baseline.
|
||||
if _, err := api.GetTrafficSnapshot(); err != nil {
|
||||
t.Fatalf("GetTrafficSnapshot: %v", err)
|
||||
}
|
||||
// Panel poll 2: sees 400-100=300, including what accrued while the
|
||||
// snapshot was taken — proof the snapshot consumed nothing.
|
||||
_, clients, err := api.GetTraffic()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTraffic: %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, clients, "alice"); up != 300 {
|
||||
t.Fatalf("panel delta after interleaved snapshot = %d, want 300", up)
|
||||
}
|
||||
if got := api.StatsLastValues["user>>>alice>>>traffic>>>uplink"]; got != 400 {
|
||||
t.Fatalf("panel baseline = %d, want 400 (advanced at read, as before)", got)
|
||||
}
|
||||
// Panel poll 3 advances per poll, exactly as it always has.
|
||||
_, clients, err = api.GetTraffic()
|
||||
if err != nil {
|
||||
t.Fatalf("GetTraffic: %v", err)
|
||||
}
|
||||
if up, _ := clientUp(t, clients, "alice"); up != 60 {
|
||||
t.Fatalf("panel delta = %d, want 60", up)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
_ "unsafe"
|
||||
|
||||
@@ -84,6 +85,12 @@ func runAgent(args []string) {
|
||||
log.Fatalf("Failed to load agent config from %s: %v", configPath, err)
|
||||
}
|
||||
|
||||
// The acknowledged traffic baseline persists next to the agent config
|
||||
// (its state home, e.g. /etc/x-ui), unless the config names a path.
|
||||
if cfg.BaselinePath == "" {
|
||||
cfg.BaselinePath = filepath.Join(filepath.Dir(configPath), "agent-traffic-baseline.json")
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.InitLogger(logging.INFO)
|
||||
log.Printf("Starting 3x-ui agent (node: %s)", cfg.NodeName)
|
||||
|
||||
Reference in New Issue
Block a user