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:
MHSanaei
2026-05-28 18:03:07 +02:00
parent 0829f1ecd4
commit 72b97efa8a
34 changed files with 6391 additions and 1125 deletions
+24 -24
View File
@@ -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>
+1 -1
View File
@@ -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')}>