mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 04:19:34 +00:00
i18n(panel): migrate hardcoded panel strings to en-US and translate all locales
Surface ~400 hardcoded English labels, tooltips, placeholders, dt/divider text, modal okText/cancelText, and Spin loading from the panel pages (clients/groups/inbounds/nodes/settings/xray/sub/index) into web/translation/en-US.json under existing pages.<page>.* namespaces, with JSX swapped to t(...). Brand and protocol identifiers (TLS, MTU, SNI, NordVPN, Cloudflare WARP, etc.) stay literal. Sync all 12 non-English locales (ar-EG, es-ES, fa-IR, id-ID, ja-JP, pt-BR, ru-RU, tr-TR, uk-UA, vi-VN, zh-CN, zh-TW) to match en-US's structure and translate the 521 new key paths per locale. Every locale file now has 1539 lines, mirroring en-US ordering. Also remove a dead duplicate "info": "Info" key under pages.inbounds that collided with the new pages.inbounds.info.* object. Backend: bulk attach/detach errors in web/service/client.go now route through logger.Warningf (so they appear under /panel/api/server/logs/) instead of only living on the response payload.
This commit is contained in:
@@ -50,7 +50,6 @@ export default function BulkAttachInboundsModal({
|
||||
const skipped = result.skipped?.length ?? 0;
|
||||
const errors = result.errors?.length ?? 0;
|
||||
if (errors > 0) {
|
||||
console.error('[BulkAttach] failures:', result.errors);
|
||||
messageApi.warning(
|
||||
t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }),
|
||||
);
|
||||
|
||||
@@ -50,7 +50,6 @@ export default function BulkDetachInboundsModal({
|
||||
const skipped = result.skipped?.length ?? 0;
|
||||
const errors = result.errors?.length ?? 0;
|
||||
if (errors > 0) {
|
||||
console.error('[BulkDetach] failures:', result.errors);
|
||||
messageApi.warning(
|
||||
t('pages.clients.detachFromInboundsResultMixed', { detached, skipped, errors }),
|
||||
);
|
||||
|
||||
@@ -411,7 +411,7 @@ export default function GroupsPage() {
|
||||
<AppSidebar />
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={!fetched} delay={200} description="Loading…" size="large">
|
||||
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : (
|
||||
|
||||
@@ -128,7 +128,6 @@ export default function AttachClientsModal({
|
||||
const skipped = result.skipped?.length ?? 0;
|
||||
const errors = result.errors?.length ?? 0;
|
||||
if (errors > 0) {
|
||||
console.error('[AttachClients] failures:', result.errors);
|
||||
messageApi.warning(t('pages.inbounds.attachClientsResultMixed', { attached, skipped, errors }));
|
||||
} else {
|
||||
messageApi.success(t('pages.inbounds.attachClientsResult', { attached, skipped }));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -506,7 +506,7 @@ export default function InboundInfoModal({
|
||||
)}
|
||||
{inbound.isVlessTlsFlow && (
|
||||
<tr>
|
||||
<td>Flow</td>
|
||||
<td>{t('pages.clients.flow')}</td>
|
||||
<td>
|
||||
{clientSettings?.flow ? <Tag>{clientSettings.flow}</Tag> : <Tag color="orange">{t('none')}</Tag>}
|
||||
</td>
|
||||
@@ -729,18 +729,18 @@ export default function InboundInfoModal({
|
||||
)}
|
||||
{inbound.isXHTTP && (
|
||||
<div className="info-row">
|
||||
<dt>Mode</dt>
|
||||
<dt>{t('pages.inbounds.info.mode')}</dt>
|
||||
<dd><Tag>{inbound.stream?.xhttp?.mode}</Tag></dd>
|
||||
</div>
|
||||
)}
|
||||
{inbound.isGrpc && (
|
||||
<>
|
||||
<div className="info-row">
|
||||
<dt>grpc serviceName</dt>
|
||||
<dt>{t('pages.inbounds.info.grpcServiceName')}</dt>
|
||||
<dd><Tag className="value-tag">{inbound.serviceName}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>grpc multiMode</dt>
|
||||
<dt>{t('pages.inbounds.info.grpcMultiMode')}</dt>
|
||||
<dd><Tag>{String(inbound.stream?.grpc?.multiMode)}</Tag></dd>
|
||||
</div>
|
||||
</>
|
||||
@@ -805,16 +805,16 @@ export default function InboundInfoModal({
|
||||
{inbound.protocol === Protocols.TUN && inbound.settings && (
|
||||
<dl className="info-list info-list-block">
|
||||
<div className="info-row">
|
||||
<dt>Interface name</dt>
|
||||
<dt>{t('pages.inbounds.info.interfaceName')}</dt>
|
||||
<dd><Tag color="green" className="value-tag">{inbound.settings.name as string}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>MTU</dt>
|
||||
<dt>{t('pages.inbounds.info.mtu')}</dt>
|
||||
<dd><Tag color="green">{inbound.settings.mtu as number}</Tag></dd>
|
||||
</div>
|
||||
{Array.isArray(inbound.settings.gateway) && (inbound.settings.gateway as string[]).length > 0 && (
|
||||
<div className="info-row">
|
||||
<dt>Gateway</dt>
|
||||
<dt>{t('pages.inbounds.info.gateway')}</dt>
|
||||
<dd>
|
||||
{(inbound.settings.gateway as string[]).map((ip, j) => (
|
||||
<Tag key={`tun-gw-${j}`} color="green" className="value-tag">{ip}</Tag>
|
||||
@@ -824,7 +824,7 @@ export default function InboundInfoModal({
|
||||
)}
|
||||
{Array.isArray(inbound.settings.dns) && (inbound.settings.dns as string[]).length > 0 && (
|
||||
<div className="info-row">
|
||||
<dt>DNS</dt>
|
||||
<dt>{t('pages.inbounds.info.dns')}</dt>
|
||||
<dd>
|
||||
{(inbound.settings.dns as string[]).map((ip, j) => (
|
||||
<Tag key={`tun-dns-${j}`} color="green">{ip}</Tag>
|
||||
@@ -833,12 +833,12 @@ export default function InboundInfoModal({
|
||||
</div>
|
||||
)}
|
||||
<div className="info-row">
|
||||
<dt>Outbounds interface</dt>
|
||||
<dt>{t('pages.inbounds.info.outboundsInterface')}</dt>
|
||||
<dd><Tag color="green">{(inbound.settings.autoOutboundsInterface as string) || 'auto'}</Tag></dd>
|
||||
</div>
|
||||
{Array.isArray(inbound.settings.autoSystemRoutingTable) && (inbound.settings.autoSystemRoutingTable as string[]).length > 0 && (
|
||||
<div className="info-row">
|
||||
<dt>Auto system routes</dt>
|
||||
<dt>{t('pages.inbounds.info.autoSystemRoutes')}</dt>
|
||||
<dd>
|
||||
{(inbound.settings.autoSystemRoutingTable as string[]).map((cidr, j) => (
|
||||
<Tag key={`tun-rt-${j}`} color="green">{cidr}</Tag>
|
||||
@@ -864,7 +864,7 @@ export default function InboundInfoModal({
|
||||
<dd><Tag color="green">{inbound.settings.allowedNetwork as string}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>FollowRedirect</dt>
|
||||
<dt>{t('pages.inbounds.info.followRedirect')}</dt>
|
||||
<dd>
|
||||
<Tag color={inbound.settings.followRedirect ? 'green' : 'red'}>
|
||||
{inbound.settings.followRedirect ? t('enabled') : t('disabled')}
|
||||
@@ -877,7 +877,7 @@ export default function InboundInfoModal({
|
||||
{dbInbound.isMixed && inbound.settings && (
|
||||
<dl className="info-list info-list-block">
|
||||
<div className="info-row">
|
||||
<dt>Auth</dt>
|
||||
<dt>{t('pages.inbounds.info.auth')}</dt>
|
||||
<dd>
|
||||
<Tag color={inbound.settings.auth === 'password' ? 'green' : 'orange'}>
|
||||
{inbound.settings.auth as string}
|
||||
@@ -969,19 +969,19 @@ export default function InboundInfoModal({
|
||||
<>
|
||||
<dl className="info-list info-list-block">
|
||||
<div className="info-row">
|
||||
<dt>Secret key</dt>
|
||||
<dt>{t('pages.xray.wireguard.secretKey')}</dt>
|
||||
<dd><Tag className="value-tag">{inbound.settings.secretKey as string}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>Public key</dt>
|
||||
<dt>{t('pages.xray.wireguard.publicKey')}</dt>
|
||||
<dd><Tag className="value-tag">{inbound.settings.pubKey as string}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>MTU</dt>
|
||||
<dt>{t('pages.inbounds.info.mtu')}</dt>
|
||||
<dd><Tag>{inbound.settings.mtu as number}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>No-kernel TUN</dt>
|
||||
<dt>{t('pages.inbounds.info.noKernelTun')}</dt>
|
||||
<dd>
|
||||
<Tag color={inbound.settings.noKernelTun ? 'green' : 'default'}>
|
||||
{String(inbound.settings.noKernelTun)}
|
||||
@@ -991,14 +991,14 @@ export default function InboundInfoModal({
|
||||
</dl>
|
||||
{Array.isArray(inbound.settings.peers) && (inbound.settings.peers as { privateKey: string; publicKey: string; psk: string; allowedIPs?: string[]; keepAlive?: number }[]).map((peer, idx) => (
|
||||
<Fragment key={idx}>
|
||||
<Divider>Peer {idx + 1}</Divider>
|
||||
<Divider>{t('pages.inbounds.info.peerNumber', { n: idx + 1 })}</Divider>
|
||||
<dl className="info-list info-list-block">
|
||||
<div className="info-row">
|
||||
<dt>Secret key</dt>
|
||||
<dt>{t('pages.xray.wireguard.secretKey')}</dt>
|
||||
<dd><Tag className="value-tag">{peer.privateKey}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>Public key</dt>
|
||||
<dt>{t('pages.xray.wireguard.publicKey')}</dt>
|
||||
<dd><Tag className="value-tag">{peer.publicKey}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
@@ -1006,7 +1006,7 @@ export default function InboundInfoModal({
|
||||
<dd><Tag className="value-tag">{peer.psk}</Tag></dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>Allowed IPs</dt>
|
||||
<dt>{t('pages.xray.wireguard.allowedIPs')}</dt>
|
||||
<dd>
|
||||
{(peer.allowedIPs || []).map((ip, j) => (
|
||||
<Tag key={`wg-ip-${idx}-${j}`} className="value-tag">{ip}</Tag>
|
||||
@@ -1014,14 +1014,14 @@ export default function InboundInfoModal({
|
||||
</dd>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<dt>Keep alive</dt>
|
||||
<dt>{t('pages.inbounds.info.keepAlive')}</dt>
|
||||
<dd><Tag>{peer.keepAlive}</Tag></dd>
|
||||
</div>
|
||||
</dl>
|
||||
{wireguardConfigs[idx] && (
|
||||
<div className="link-panel">
|
||||
<div className="link-panel-header">
|
||||
<Tag color="green">Peer {idx + 1} config</Tag>
|
||||
<Tag color="green">{t('pages.inbounds.info.peerNumberConfig', { n: idx + 1 })}</Tag>
|
||||
<Tooltip title={t('copy')}>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => copyText(wireguardConfigs[idx], t)} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -171,7 +171,7 @@ export default function InboundsPage() {
|
||||
confirm: (value: string) => Promise<boolean | void> | boolean | void;
|
||||
}) => {
|
||||
setPromptTitle(opts.title);
|
||||
setPromptOkText(opts.okText || 'OK');
|
||||
setPromptOkText(opts.okText || t('confirm'));
|
||||
setPromptType(opts.type || 'textarea');
|
||||
setPromptInitial(opts.value || '');
|
||||
setPromptHandler(() => opts.confirm);
|
||||
@@ -316,8 +316,8 @@ export default function InboundsPage() {
|
||||
|
||||
const importInbound = useCallback(() => {
|
||||
openPrompt({
|
||||
title: 'Import inbound',
|
||||
okText: 'Import',
|
||||
title: t('pages.inbounds.importInbound'),
|
||||
okText: t('pages.inbounds.import'),
|
||||
type: 'textarea',
|
||||
value: '',
|
||||
confirm: async (value) => {
|
||||
@@ -434,9 +434,9 @@ export default function InboundsPage() {
|
||||
case 'subs': exportAllSubs(); break;
|
||||
case 'resetInbounds':
|
||||
modal.confirm({
|
||||
title: 'Reset all inbound traffic?',
|
||||
okText: 'Reset',
|
||||
cancelText: 'Cancel',
|
||||
title: t('pages.inbounds.resetAllTrafficTitle'),
|
||||
okText: t('reset'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: async () => {
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/resetAllTraffics');
|
||||
if (msg?.success) await refresh();
|
||||
@@ -518,7 +518,7 @@ export default function InboundsPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={!fetched} delay={200} description="Loading…" size="large">
|
||||
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : (
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function NodesPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={!fetched} delay={200} description="Loading…" size="large">
|
||||
<Spin spinning={!fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : (
|
||||
|
||||
@@ -160,8 +160,8 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
|
||||
<SettingListItem
|
||||
paddings="small"
|
||||
title="Trusted proxy CIDRs"
|
||||
description="Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers."
|
||||
title={t('pages.settings.trustedProxyCidrs')}
|
||||
description={t('pages.settings.trustedProxyCidrsDesc')}
|
||||
>
|
||||
<Input
|
||||
value={allSetting.trustedProxyCIDRs}
|
||||
@@ -271,58 +271,58 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
label: 'LDAP',
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title="Enable LDAP sync">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
|
||||
<Switch checked={allSetting.ldapEnable} onChange={(v) => updateSetting({ ldapEnable: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="LDAP host">
|
||||
<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="LDAP port">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.port')}>
|
||||
<InputNumber value={allSetting.ldapPort} min={1} max={65535} style={{ width: '100%' }}
|
||||
onChange={(v) => updateSetting({ ldapPort: Number(v) || 0 })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Use TLS (LDAPS)">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.useTls')}>
|
||||
<Switch checked={allSetting.ldapUseTLS} onChange={(v) => updateSetting({ ldapUseTLS: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Bind DN">
|
||||
<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 ? 'Configured; leave blank to keep current password.' : 'Not configured.'}
|
||||
description={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordConfigured') : t('pages.settings.ldap.passwordUnconfigured')}
|
||||
>
|
||||
<Input.Password
|
||||
value={allSetting.ldapPassword}
|
||||
placeholder={allSetting.hasLdapPassword ? 'Configured - enter a new value to replace' : ''}
|
||||
placeholder={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordPlaceholder') : ''}
|
||||
onChange={(e) => updateSetting({ ldapPassword: e.target.value })}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Base DN">
|
||||
<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="User filter">
|
||||
<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="User attribute (username/email)">
|
||||
<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="VLESS flag attribute">
|
||||
<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="Generic flag attribute (optional)" description="If set, overrides VLESS flag — e.g. shadowInactive.">
|
||||
<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="Truthy values" description="Comma-separated; default: true,1,yes,on">
|
||||
<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="Invert flag" description="Enable when the attribute means disabled (e.g. shadowInactive).">
|
||||
<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="Sync schedule" description="Cron-like string, e.g. @every 1m">
|
||||
<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="Inbound tags" description="Inbounds that LDAP sync may auto-create or auto-delete clients on.">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.inboundTags')} description={t('pages.settings.ldap.inboundTagsDesc')}>
|
||||
<>
|
||||
<Select
|
||||
mode="multiple"
|
||||
@@ -332,25 +332,25 @@ export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProp
|
||||
options={inboundOptions}
|
||||
/>
|
||||
{inboundOptions.length === 0 && (
|
||||
<div className="ldap-no-inbounds">No inbounds found. Create one in Inbounds first.</div>
|
||||
<div className="ldap-no-inbounds">{t('pages.settings.ldap.noInbounds')}</div>
|
||||
)}
|
||||
</>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Auto create clients">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoCreate')}>
|
||||
<Switch checked={allSetting.ldapAutoCreate} onChange={(v) => updateSetting({ ldapAutoCreate: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Auto delete clients">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoDelete')}>
|
||||
<Switch checked={allSetting.ldapAutoDelete} onChange={(v) => updateSetting({ ldapAutoDelete: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Default total (GB)">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultTotalGb')}>
|
||||
<InputNumber value={allSetting.ldapDefaultTotalGB} min={0} style={{ width: '100%' }}
|
||||
onChange={(v) => updateSetting({ ldapDefaultTotalGB: Number(v) || 0 })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Default expiry (days)">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultExpiryDays')}>
|
||||
<InputNumber value={allSetting.ldapDefaultExpiryDays} min={0} style={{ width: '100%' }}
|
||||
onChange={(v) => updateSetting({ ldapDefaultExpiryDays: Number(v) || 0 })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Default IP limit">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultIpLimit')}>
|
||||
<InputNumber value={allSetting.ldapDefaultLimitIP} min={0} style={{ width: '100%' }}
|
||||
onChange={(v) => updateSetting({ ldapDefaultLimitIP: Number(v) || 0 })} />
|
||||
</SettingListItem>
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function SettingsPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
|
||||
<Spin spinning={spinning || !fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : (
|
||||
|
||||
@@ -264,19 +264,19 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
label: t('pages.settings.fragmentSett'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title="Packets">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packets')}>
|
||||
<Input value={fragmentObj.packets} placeholder="1-1 | 1-3 | tlshello | …"
|
||||
onChange={(e) => setFragmentField('packets', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Length">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.length')}>
|
||||
<Input value={fragmentObj.length} placeholder="100-200"
|
||||
onChange={(e) => setFragmentField('length', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Interval">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.interval')}>
|
||||
<Input value={fragmentObj.interval} placeholder="10-20"
|
||||
onChange={(e) => setFragmentField('interval', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Max split">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.maxSplit')}>
|
||||
<Input value={fragmentObj.maxSplit} placeholder="300-400"
|
||||
onChange={(e) => setFragmentField('maxSplit', e.target.value)} />
|
||||
</SettingListItem>
|
||||
@@ -291,20 +291,20 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Noises',
|
||||
label: t('pages.settings.subFormats.noises'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title="Noises" description={t('pages.settings.noisesDesc')}>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.noises')} description={t('pages.settings.noisesDesc')}>
|
||||
<Switch checked={noisesEnabled} onChange={setNoisesEnabled} />
|
||||
</SettingListItem>
|
||||
{noisesEnabled && (
|
||||
<div className="nested-block">
|
||||
<Collapse items={noisesArray.map((noise, index) => ({
|
||||
key: String(index),
|
||||
label: `Noise №${index + 1}`,
|
||||
label: t('pages.settings.subFormats.noiseItem', { n: index + 1 }),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title="Type">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.type')}>
|
||||
<Select
|
||||
value={noise.type}
|
||||
style={{ width: '100%' }}
|
||||
@@ -312,15 +312,15 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
options={['rand', 'base64', 'str', 'hex'].map((p) => ({ value: p, label: p }))}
|
||||
/>
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Packet">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.packet')}>
|
||||
<Input value={noise.packet} placeholder="5-10"
|
||||
onChange={(e) => updateNoiseField(index, 'packet', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Delay (ms)">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.delayMs')}>
|
||||
<Input value={noise.delay} placeholder="10-20"
|
||||
onChange={(e) => updateNoiseField(index, 'delay', e.target.value)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Apply to">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.applyTo')}>
|
||||
<Select
|
||||
value={noise.applyTo}
|
||||
style={{ width: '100%' }}
|
||||
@@ -338,7 +338,7 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
</>
|
||||
),
|
||||
}))} />
|
||||
<Button type="primary" style={{ marginTop: 10 }} onClick={addNoise}>+ Noise</Button>
|
||||
<Button type="primary" style={{ marginTop: 10 }} onClick={addNoise}>{t('pages.settings.subFormats.addNoise')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -360,15 +360,15 @@ export default function SubscriptionFormatsTab({ allSetting, updateSetting }: Su
|
||||
label: t('pages.settings.muxSett'),
|
||||
children: (
|
||||
<>
|
||||
<SettingListItem paddings="small" title="Concurrency">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.concurrency')}>
|
||||
<InputNumber value={muxObj.concurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('concurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="xudp concurrency">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpConcurrency')}>
|
||||
<InputNumber value={muxObj.xudpConcurrency} min={-1} max={1024} style={{ width: '100%' }}
|
||||
onChange={(v) => setMuxField('xudpConcurrency', Number(v) || 0)} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="xudp UDP 443">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subFormats.xudpUdp443')}>
|
||||
<Select
|
||||
value={muxObj.xudpProxyUDP443}
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -33,10 +33,10 @@ export default function SubscriptionGeneralTab({ allSetting, updateSetting }: Su
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subEnable')} description={t('pages.settings.subEnableDesc')}>
|
||||
<Switch checked={allSetting.subEnable} onChange={(v) => updateSetting({ subEnable: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="JSON subscription" description={t('pages.settings.subJsonEnable')}>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subJsonEnableTitle')} description={t('pages.settings.subJsonEnable')}>
|
||||
<Switch checked={allSetting.subJsonEnable} onChange={(v) => updateSetting({ subJsonEnable: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title="Clash / Mihomo subscription">
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subClashEnableTitle')}>
|
||||
<Switch checked={allSetting.subClashEnable} onChange={(v) => updateSetting({ subClashEnable: v })} />
|
||||
</SettingListItem>
|
||||
<SettingListItem paddings="small" title={t('pages.settings.subListen')} description={t('pages.settings.subListenDesc')}>
|
||||
|
||||
@@ -135,19 +135,19 @@ export default function BalancerFormModal({
|
||||
>
|
||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
|
||||
<Form.Item
|
||||
label="Tag"
|
||||
label={t('pages.xray.balancer.tag')}
|
||||
required
|
||||
validateStatus={issues.tag ? 'error' : duplicateTag ? 'warning' : ''}
|
||||
help={issues.tag || (duplicateTag ? 'Tag already used by another balancer' : '')}
|
||||
help={issues.tag || (duplicateTag ? t('pages.xray.balancer.tagDuplicate') : '')}
|
||||
hasFeedback
|
||||
>
|
||||
<Input
|
||||
value={state.tag}
|
||||
onChange={(e) => update('tag', e.target.value)}
|
||||
placeholder="unique balancer tag"
|
||||
placeholder={t('pages.xray.balancer.tagPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Strategy">
|
||||
<Form.Item label={t('pages.xray.balancer.balancerStrategy')}>
|
||||
<Select
|
||||
value={state.strategy}
|
||||
onChange={(v) => update('strategy', v)}
|
||||
@@ -155,7 +155,7 @@ export default function BalancerFormModal({
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Selector"
|
||||
label={t('pages.xray.balancer.selector')}
|
||||
required
|
||||
validateStatus={issues.selector ? 'error' : ''}
|
||||
help={issues.selector || ''}
|
||||
@@ -169,7 +169,7 @@ export default function BalancerFormModal({
|
||||
options={outboundTags.map((tg) => ({ value: tg, label: tg }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Fallback">
|
||||
<Form.Item label={t('pages.xray.balancer.fallback')}>
|
||||
<Select
|
||||
value={state.fallbackTag}
|
||||
onChange={(v) => update('fallbackTag', v ?? '')}
|
||||
@@ -180,23 +180,23 @@ export default function BalancerFormModal({
|
||||
|
||||
{state.strategy === 'leastLoad' && (
|
||||
<>
|
||||
<Form.Item label="Expected">
|
||||
<Form.Item label={t('pages.xray.balancer.expected')}>
|
||||
<InputNumber
|
||||
value={settings?.expected}
|
||||
onChange={(v) => updateSetting('expected', typeof v === 'number' ? v : undefined)}
|
||||
min={0}
|
||||
placeholder="optimal node count"
|
||||
placeholder={t('pages.xray.balancer.expectedPlaceholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Max RTT">
|
||||
<Form.Item label={t('pages.xray.balancer.maxRtt')}>
|
||||
<Input
|
||||
value={settings?.maxRTT ?? ''}
|
||||
onChange={(e) => updateSetting('maxRTT', e.target.value || undefined)}
|
||||
placeholder="e.g. 1s"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Tolerance">
|
||||
<Form.Item label={t('pages.xray.balancer.tolerance')}>
|
||||
<InputNumber
|
||||
value={settings?.tolerance}
|
||||
onChange={(v) => updateSetting('tolerance', typeof v === 'number' ? v : undefined)}
|
||||
@@ -207,7 +207,7 @@ export default function BalancerFormModal({
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Baselines">
|
||||
<Form.Item label={t('pages.xray.balancer.baselines')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -227,7 +227,7 @@ export default function BalancerFormModal({
|
||||
</Space.Compact>
|
||||
))}
|
||||
</Form.Item>
|
||||
<Form.Item label="Costs">
|
||||
<Form.Item label={t('pages.xray.balancer.costs')}>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Form, Input, message, Modal, Select, Tabs, Tag } from 'antd';
|
||||
import { LoginOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
|
||||
@@ -58,6 +59,7 @@ export default function NordModal({
|
||||
onRemoveOutbound,
|
||||
onRemoveRoutingRules,
|
||||
}: NordModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [nordData, setNordData] = useState<NordData | null>(null);
|
||||
@@ -185,7 +187,7 @@ export default function NordModal({
|
||||
})
|
||||
.sort((a: NordServer, b: NordServer) => a.load - b.load);
|
||||
setServers(next);
|
||||
if (next.length === 0) messageApi.warning('No servers found for the selected country');
|
||||
if (next.length === 0) messageApi.warning(t('pages.xray.nord.noServers'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -197,7 +199,7 @@ export default function NordModal({
|
||||
const tech = server.technologies?.find((tt) => tt.id === 35);
|
||||
const publicKey = tech?.metadata?.find((m) => m.name === 'public_key')?.value;
|
||||
if (!publicKey) {
|
||||
messageApi.error('Selected server does not advertise a NordLynx public key.');
|
||||
messageApi.error(t('pages.xray.nord.noPublicKey'));
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -216,7 +218,7 @@ export default function NordModal({
|
||||
const ob = buildNordOutbound();
|
||||
if (!ob) return;
|
||||
onAddOutbound(ob);
|
||||
messageApi.success('NordVPN outbound added');
|
||||
messageApi.success(t('pages.xray.nord.outboundAdded'));
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -231,7 +233,7 @@ export default function NordModal({
|
||||
oldTag,
|
||||
newTag: ob.tag as string,
|
||||
});
|
||||
messageApi.success('NordVPN outbound updated');
|
||||
messageApi.success(t('pages.xray.nord.outboundUpdated'));
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -245,7 +247,7 @@ export default function NordModal({
|
||||
items={[
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Access token',
|
||||
label: t('pages.xray.nord.accessToken'),
|
||||
children: (
|
||||
<Form
|
||||
colon={false}
|
||||
@@ -253,14 +255,14 @@ export default function NordModal({
|
||||
wrapperCol={{ md: { span: 18 } }}
|
||||
className="mt-20"
|
||||
>
|
||||
<Form.Item label="Access token">
|
||||
<Form.Item label={t('pages.xray.nord.accessToken')}>
|
||||
<Input
|
||||
value={token}
|
||||
placeholder="Access token"
|
||||
placeholder={t('pages.xray.nord.accessToken')}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
<Button type="primary" className="mt-10" loading={loading} icon={<LoginOutlined />} onClick={login}>
|
||||
Login
|
||||
{t('login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -268,7 +270,7 @@ export default function NordModal({
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
label: 'Private key',
|
||||
label: t('pages.xray.nord.privateKey'),
|
||||
children: (
|
||||
<Form
|
||||
colon={false}
|
||||
@@ -276,14 +278,14 @@ export default function NordModal({
|
||||
wrapperCol={{ md: { span: 18 } }}
|
||||
className="mt-20"
|
||||
>
|
||||
<Form.Item label="Private key">
|
||||
<Form.Item label={t('pages.xray.nord.privateKey')}>
|
||||
<Input
|
||||
value={manualKey}
|
||||
placeholder="Private key"
|
||||
placeholder={t('pages.xray.nord.privateKey')}
|
||||
onChange={(e) => setManualKey(e.target.value)}
|
||||
/>
|
||||
<Button type="primary" className="mt-10" loading={loading} icon={<SaveOutlined />} onClick={saveKey}>
|
||||
Save
|
||||
{t('save')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -297,25 +299,25 @@ export default function NordModal({
|
||||
<tbody>
|
||||
{nordData.token && (
|
||||
<tr className="row-odd">
|
||||
<td>Access token</td>
|
||||
<td>{t('pages.xray.nord.accessToken')}</td>
|
||||
<td>{nordData.token}</td>
|
||||
</tr>
|
||||
)}
|
||||
<tr>
|
||||
<td>Private key</td>
|
||||
<td>{t('pages.xray.nord.privateKey')}</td>
|
||||
<td>{nordData.private_key}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button loading={loading} type="primary" danger className="mt-8" onClick={logout}>
|
||||
Logout
|
||||
{t('logout')}
|
||||
</Button>
|
||||
|
||||
<Divider className="zero-margin">Settings</Divider>
|
||||
<Divider className="zero-margin">{t('pages.xray.warp.settings')}</Divider>
|
||||
|
||||
<Form colon={false} labelCol={{ md: { span: 6 } }} wrapperCol={{ md: { span: 18 } }} className="mt-10">
|
||||
<Form.Item label="Country">
|
||||
<Form.Item label={t('pages.xray.outbound.country')}>
|
||||
<Select
|
||||
value={countryId ?? undefined}
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
@@ -328,18 +330,18 @@ export default function NordModal({
|
||||
</Form.Item>
|
||||
|
||||
{cities.length > 0 && (
|
||||
<Form.Item label="City">
|
||||
<Form.Item label={t('pages.xray.outbound.city')}>
|
||||
<Select
|
||||
value={cityId}
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
onChange={setCityId}
|
||||
options={[{ value: null, label: 'All cities' }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
|
||||
options={[{ value: null, label: t('pages.xray.outbound.allCities') }, ...cities.map((c) => ({ value: c.id, label: c.name }))]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{filteredServers.length > 0 && (
|
||||
<Form.Item label="Server">
|
||||
<Form.Item label={t('pages.xray.outbound.server')}>
|
||||
<Select
|
||||
value={serverId}
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
@@ -363,17 +365,17 @@ export default function NordModal({
|
||||
)}
|
||||
</Form>
|
||||
|
||||
<Divider className="my-10">Outbound status</Divider>
|
||||
<Divider className="my-10">{t('pages.xray.outbound.outboundStatus')}</Divider>
|
||||
{nordOutboundIndex >= 0 ? (
|
||||
<>
|
||||
<Tag color="green">Enabled</Tag>
|
||||
<Tag color="green">{t('enabled')}</Tag>
|
||||
<Button type="primary" danger loading={loading} className="ml-8" onClick={resetOutbound}>
|
||||
Reset
|
||||
{t('reset')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tag color="orange">Disabled</Tag>
|
||||
<Tag color="orange">{t('disabled')}</Tag>
|
||||
<Button
|
||||
type="primary"
|
||||
className="ml-8"
|
||||
@@ -381,7 +383,7 @@ export default function NordModal({
|
||||
loading={loading}
|
||||
onClick={addOutbound}
|
||||
>
|
||||
Add outbound
|
||||
{t('pages.xray.warp.addOutbound')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -258,7 +258,7 @@ export default function OutboundsTab({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Tag',
|
||||
title: t('pages.xray.outbound.tag'),
|
||||
key: 'identity',
|
||||
align: 'left',
|
||||
render: (_v, record) => (
|
||||
@@ -316,7 +316,7 @@ export default function OutboundsTab({
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Latency',
|
||||
title: t('pages.nodes.latency'),
|
||||
key: 'testResult',
|
||||
align: 'left',
|
||||
width: 140,
|
||||
@@ -398,14 +398,14 @@ export default function OutboundsTab({
|
||||
</Col>
|
||||
<Col xs={24} sm={12} className="toolbar-right">
|
||||
<Space size="small" wrap>
|
||||
<Tooltip title="TCP: fast dial-only probe. HTTP: full request through xray.">
|
||||
<Tooltip title={t('pages.xray.outbound.testModeTooltip')}>
|
||||
<Radio.Group value={testMode} onChange={(e) => setTestMode(e.target.value)} buttonStyle="solid" size="small">
|
||||
<Radio.Button value="tcp">TCP</Radio.Button>
|
||||
<Radio.Button value="http">HTTP</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Tooltip>
|
||||
<Button type="primary" loading={testingAll} icon={<PlayCircleOutlined />} onClick={() => onTestAll(testMode)}>
|
||||
{!isMobile && 'Test all'}
|
||||
{!isMobile && t('pages.xray.outbound.testAll')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
placement="topRight"
|
||||
|
||||
@@ -293,7 +293,7 @@ export default function RoutingTab({
|
||||
<div className="action-cell">
|
||||
<HolderOutlined
|
||||
className="drag-handle"
|
||||
title="Drag to reorder"
|
||||
title={t('pages.xray.routing.dragToReorder')}
|
||||
onPointerDown={(ev: React.PointerEvent) => onHandlePointerDown(index, ev)}
|
||||
/>
|
||||
<span className="row-index">{index + 1}</span>
|
||||
@@ -326,7 +326,7 @@ export default function RoutingTab({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Source',
|
||||
title: t('pages.xray.rules.source'),
|
||||
align: 'left',
|
||||
width: 180,
|
||||
key: 'source',
|
||||
@@ -354,7 +354,7 @@ export default function RoutingTab({
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Destination',
|
||||
title: t('pages.xray.rules.dest'),
|
||||
align: 'left',
|
||||
key: 'destination',
|
||||
render: (_v, record) => (
|
||||
|
||||
@@ -148,8 +148,8 @@ export default function RuleFormModal({
|
||||
<Form colon={false} labelCol={{ md: { span: 8 } }} wrapperCol={{ md: { span: 14 } }}>
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
Source IPs <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('pages.xray.ruleForm.sourceIps')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -158,8 +158,8 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
Source port <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('pages.xray.ruleForm.sourcePort')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -168,15 +168,15 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
VLESS route <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('pages.xray.ruleForm.vlessRoute')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input value={form.vlessRoute} onChange={(e) => update('vlessRoute', e.target.value)} placeholder="53,443,1000-2000" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Network">
|
||||
<Form.Item label={t('pages.inbounds.network')}>
|
||||
<Select
|
||||
value={form.network}
|
||||
onChange={(v) => update('network', v)}
|
||||
@@ -184,7 +184,7 @@ export default function RuleFormModal({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Protocol">
|
||||
<Form.Item label={t('pages.inbounds.protocol')}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={form.protocol}
|
||||
@@ -193,7 +193,7 @@ export default function RuleFormModal({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Attributes">
|
||||
<Form.Item label={t('pages.xray.ruleForm.attributes')}>
|
||||
<Button size="small" icon={<PlusOutlined />} onClick={() => update('attrs', [...form.attrs, ['', '']])} />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ span: 24 }}>
|
||||
@@ -202,7 +202,7 @@ export default function RuleFormModal({
|
||||
<InputAddon>{`${idx + 1}`}</InputAddon>
|
||||
<Input
|
||||
value={attr[0]}
|
||||
placeholder="Name"
|
||||
placeholder={t('pages.nodes.name')}
|
||||
onChange={(e) => {
|
||||
const next = form.attrs.map((a, i) => (i === idx ? ([e.target.value, a[1]] as [string, string]) : a));
|
||||
update('attrs', next);
|
||||
@@ -210,7 +210,7 @@ export default function RuleFormModal({
|
||||
/>
|
||||
<Input
|
||||
value={attr[1]}
|
||||
placeholder="Value"
|
||||
placeholder={t('pages.xray.ruleForm.value')}
|
||||
onChange={(e) => {
|
||||
const next = form.attrs.map((a, i) => (i === idx ? ([a[0], e.target.value] as [string, string]) : a));
|
||||
update('attrs', next);
|
||||
@@ -226,7 +226,7 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
IP <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -236,8 +236,8 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
Domain <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('domainName')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -246,8 +246,8 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
User <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('pages.xray.ruleForm.user')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -256,15 +256,15 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Comma-separated list">
|
||||
Port <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.rules.useComma')}>
|
||||
{t('pages.inbounds.port')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Input value={form.port} onChange={(e) => update('port', e.target.value)} placeholder="53,443,1000-2000" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Inbound tags">
|
||||
<Form.Item label={t('pages.xray.ruleForm.inboundTags')}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={form.inboundTag}
|
||||
@@ -273,7 +273,7 @@ export default function RuleFormModal({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Outbound tag">
|
||||
<Form.Item label={t('pages.xray.ruleForm.outboundTag')}>
|
||||
<Select
|
||||
value={form.outboundTag}
|
||||
onChange={(v) => update('outboundTag', v)}
|
||||
@@ -283,8 +283,8 @@ export default function RuleFormModal({
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Tooltip title="Routes traffic through one of the configured load balancers">
|
||||
Balancer tag <QuestionCircleOutlined />
|
||||
<Tooltip title={t('pages.xray.ruleForm.balancerTagTooltip')}>
|
||||
{t('pages.xray.ruleForm.balancerTag')} <QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -72,6 +73,7 @@ export default function WarpModal({
|
||||
onResetOutbound,
|
||||
onRemoveOutbound,
|
||||
}: WarpModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [warpData, setWarpData] = useState<WarpData | null>(null);
|
||||
@@ -167,7 +169,7 @@ export default function WarpModal({
|
||||
setWarpConfig(null);
|
||||
setWarpPlus('');
|
||||
} else {
|
||||
setLicenseError(msg?.msg || 'Failed to set WARP license.');
|
||||
setLicenseError(msg?.msg || t('pages.xray.warp.licenseError'));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -192,7 +194,7 @@ export default function WarpModal({
|
||||
|
||||
function addOutbound() {
|
||||
if (!stagedOutbound) {
|
||||
messageApi.warning('Fetch the WARP config first.');
|
||||
messageApi.warning(t('pages.xray.warp.fetchFirst'));
|
||||
return;
|
||||
}
|
||||
onAddOutbound(stagedOutbound);
|
||||
@@ -213,49 +215,49 @@ export default function WarpModal({
|
||||
<Modal open={open} title="Cloudflare WARP" footer={null} onCancel={onClose}>
|
||||
{!hasWarp ? (
|
||||
<Button type="primary" loading={loading} icon={<ApiOutlined />} onClick={register}>
|
||||
Create WARP account
|
||||
{t('pages.xray.warp.createAccount')}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<table className="warp-data-table">
|
||||
<tbody>
|
||||
<tr className="row-odd">
|
||||
<td>Access token</td>
|
||||
<td>{t('pages.xray.warp.accessToken')}</td>
|
||||
<td>{warpData?.access_token}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device ID</td>
|
||||
<td>{t('pages.xray.warp.deviceId')}</td>
|
||||
<td>{warpData?.device_id}</td>
|
||||
</tr>
|
||||
<tr className="row-odd">
|
||||
<td>License key</td>
|
||||
<td>{t('pages.xray.warp.licenseKey')}</td>
|
||||
<td>{warpData?.license_key}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Private key</td>
|
||||
<td>{t('pages.xray.warp.privateKey')}</td>
|
||||
<td>{warpData?.private_key}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button loading={loading} type="primary" danger className="mt-8" icon={<DeleteOutlined />} onClick={delConfig}>
|
||||
Delete account
|
||||
{t('pages.xray.warp.deleteAccount')}
|
||||
</Button>
|
||||
|
||||
<Divider className="zero-margin">Settings</Divider>
|
||||
<Divider className="zero-margin">{t('pages.xray.warp.settings')}</Divider>
|
||||
|
||||
<Collapse
|
||||
className="my-10"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'WARP / WARP+ license key',
|
||||
label: t('pages.xray.warp.licenseKeyLabel'),
|
||||
children: (
|
||||
<Form colon={false} labelCol={{ md: { span: 6 } }} wrapperCol={{ md: { span: 14 } }}>
|
||||
<Form.Item label="Key">
|
||||
<Form.Item label={t('pages.xray.warp.key')}>
|
||||
<Input
|
||||
value={warpPlus}
|
||||
placeholder="26-char WARP+ key"
|
||||
placeholder={t('pages.xray.warp.keyPlaceholder')}
|
||||
onChange={(e) => {
|
||||
setWarpPlus(e.target.value);
|
||||
setLicenseError('');
|
||||
@@ -268,7 +270,7 @@ export default function WarpModal({
|
||||
loading={loading}
|
||||
onClick={updateLicense}
|
||||
>
|
||||
Update
|
||||
{t('update')}
|
||||
</Button>
|
||||
{licenseError && (
|
||||
<Alert title={licenseError} type="error" showIcon className="license-error" />
|
||||
@@ -281,9 +283,9 @@ export default function WarpModal({
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider className="zero-margin">Account info</Divider>
|
||||
<Divider className="zero-margin">{t('pages.xray.warp.accountInfo')}</Divider>
|
||||
<Button className="my-8" loading={loading} type="primary" icon={<SyncOutlined />} onClick={getConfig}>
|
||||
Refresh
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
|
||||
{hasConfig && (
|
||||
@@ -291,38 +293,38 @@ export default function WarpModal({
|
||||
<table className="warp-data-table">
|
||||
<tbody>
|
||||
<tr className="row-odd">
|
||||
<td>Device name</td>
|
||||
<td>{t('pages.xray.warp.deviceName')}</td>
|
||||
<td>{warpConfig?.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device model</td>
|
||||
<td>{t('pages.xray.warp.deviceModel')}</td>
|
||||
<td>{warpConfig?.model}</td>
|
||||
</tr>
|
||||
<tr className="row-odd">
|
||||
<td>Device enabled</td>
|
||||
<td>{t('pages.xray.warp.deviceEnabled')}</td>
|
||||
<td>{String(warpConfig?.enabled)}</td>
|
||||
</tr>
|
||||
{warpConfig?.account && (
|
||||
<>
|
||||
<tr>
|
||||
<td>Account type</td>
|
||||
<td>{t('pages.xray.warp.accountType')}</td>
|
||||
<td>{warpConfig.account.account_type}</td>
|
||||
</tr>
|
||||
<tr className="row-odd">
|
||||
<td>Role</td>
|
||||
<td>{t('pages.xray.warp.role')}</td>
|
||||
<td>{warpConfig.account.role}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WARP+ data</td>
|
||||
<td>{t('pages.xray.warp.warpPlusData')}</td>
|
||||
<td>{SizeFormatter.sizeFormat(warpConfig.account.premium_data)}</td>
|
||||
</tr>
|
||||
<tr className="row-odd">
|
||||
<td>Quota</td>
|
||||
<td>{t('pages.xray.warp.quota')}</td>
|
||||
<td>{SizeFormatter.sizeFormat(warpConfig.account.quota)}</td>
|
||||
</tr>
|
||||
{warpConfig.account.usage != null && (
|
||||
<tr>
|
||||
<td>Usage</td>
|
||||
<td>{t('pages.xray.warp.usage')}</td>
|
||||
<td>{SizeFormatter.sizeFormat(warpConfig.account.usage)}</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -331,19 +333,19 @@ export default function WarpModal({
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Divider className="my-10">Outbound status</Divider>
|
||||
<Divider className="my-10">{t('pages.xray.outbound.outboundStatus')}</Divider>
|
||||
{warpOutboundIndex >= 0 ? (
|
||||
<>
|
||||
<Tag color="green">Enabled</Tag>
|
||||
<Tag color="green">{t('enabled')}</Tag>
|
||||
<Button type="primary" danger loading={loading} className="ml-8" onClick={resetOutbound}>
|
||||
Reset
|
||||
{t('reset')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tag color="orange">Disabled</Tag>
|
||||
<Tag color="orange">{t('disabled')}</Tag>
|
||||
<Button type="primary" loading={loading} className="ml-8" icon={<PlusOutlined />} onClick={addOutbound}>
|
||||
Add outbound
|
||||
{t('pages.xray.warp.addOutbound')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -223,10 +223,10 @@ export default function XrayPage() {
|
||||
|
||||
function confirmRestart() {
|
||||
modal.confirm({
|
||||
title: 'Restart xray?',
|
||||
content: 'Reloads the xray service with the saved configuration.',
|
||||
okText: 'Restart',
|
||||
cancelText: 'Cancel',
|
||||
title: t('pages.xray.restartConfirmTitle'),
|
||||
content: t('pages.xray.restartConfirmContent'),
|
||||
okText: t('pages.xray.restart'),
|
||||
cancelText: t('cancel'),
|
||||
onOk: () => restartXray(),
|
||||
});
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export default function XrayPage() {
|
||||
|
||||
<Layout className="content-shell">
|
||||
<Layout.Content id="content-layout" className="content-area">
|
||||
<Spin spinning={spinning || !fetched} delay={200} description="Loading…" size="large">
|
||||
<Spin spinning={spinning || !fetched} delay={200} description={t('loading')} size="large">
|
||||
{!fetched ? (
|
||||
<div className="loading-spacer" />
|
||||
) : fetchError ? (
|
||||
@@ -281,7 +281,7 @@ export default function XrayPage() {
|
||||
{restartResult && (
|
||||
<Popover
|
||||
placement="rightTop"
|
||||
title="Xray restart output"
|
||||
title={t('pages.xray.restartOutputTitle')}
|
||||
content={<pre className="restart-result">{restartResult}</pre>}
|
||||
>
|
||||
<QuestionCircleOutlined className="restart-icon" />
|
||||
|
||||
Reference in New Issue
Block a user