feat: initial Chinese UI language support
This commit is contained in:
parent
a8c6cd23ce
commit
8a50ef4ef2
22 changed files with 301 additions and 54 deletions
|
@ -33,6 +33,9 @@
|
|||
"fontsource-roboto-mono": "^3.0.3",
|
||||
"framer-motion": "^2.9.5",
|
||||
"history": "^5.0.0",
|
||||
"i18next": "^19.8.4",
|
||||
"i18next-browser-languagedetector": "^6.0.1",
|
||||
"i18next-http-backend": "^1.0.21",
|
||||
"immer": "^8.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
"lodash-es": "^4.17.14",
|
||||
|
@ -43,6 +46,7 @@
|
|||
"react-dom": "0.0.0-experimental-94c0244ba",
|
||||
"react-feather": "^2.0.9",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^11.7.4",
|
||||
"react-icons": "^3.10.0",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-query": "^2.26.3",
|
||||
|
@ -80,6 +84,7 @@
|
|||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-helmet": "^6.1.0",
|
||||
"@types/react-modal": "^3.10.6",
|
||||
"@types/react-table": "^7.0.25",
|
||||
"@types/react-tabs": "^2.3.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.9.0",
|
||||
"@typescript-eslint/parser": "^4.9.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'modern-normalize/modern-normalize.css';
|
||||
import './misc/i18n';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
.section {
|
||||
padding: 6px 15px 15px;
|
||||
@media (--breakpoint-not-small) {
|
||||
padding: 10px 40px 40px;
|
||||
padding: 0 40px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,3 +28,7 @@
|
|||
.label {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
width: 360px;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from 'src/components/shared/Select';
|
||||
import { ClashGeneralConfig, DispatchFn, State } from 'src/store/types';
|
||||
import { ClashAPIConfig } from 'src/types';
|
||||
|
||||
import {
|
||||
getClashAPIConfig,
|
||||
|
@ -67,12 +70,17 @@ const portFields = [
|
|||
{ key: 'redir-port', label: 'Redir Port' },
|
||||
];
|
||||
|
||||
const mapState = (s) => ({
|
||||
const langOptions = [
|
||||
['zh', '中文'],
|
||||
['en', 'English'],
|
||||
];
|
||||
|
||||
const mapState = (s: State) => ({
|
||||
configs: getConfigs(s),
|
||||
apiConfig: getClashAPIConfig(s),
|
||||
});
|
||||
|
||||
const mapState2 = (s) => ({
|
||||
const mapState2 = (s: State) => ({
|
||||
selectedChartStyleIndex: getSelectedChartStyleIndex(s),
|
||||
latencyTestUrl: getLatencyTestUrl(s),
|
||||
apiConfig: getClashAPIConfig(s),
|
||||
|
@ -88,13 +96,21 @@ function ConfigContainer({ dispatch, configs, apiConfig }) {
|
|||
return <Config configs={configs} />;
|
||||
}
|
||||
|
||||
type ConfigImplProps = {
|
||||
dispatch: DispatchFn;
|
||||
configs: ClashGeneralConfig;
|
||||
selectedChartStyleIndex: number;
|
||||
latencyTestUrl: string;
|
||||
apiConfig: ClashAPIConfig;
|
||||
};
|
||||
|
||||
function ConfigImpl({
|
||||
dispatch,
|
||||
configs,
|
||||
selectedChartStyleIndex,
|
||||
latencyTestUrl,
|
||||
apiConfig,
|
||||
}) {
|
||||
}: ConfigImplProps) {
|
||||
const [configState, setConfigStateInternal] = useState(configs);
|
||||
const refConfigs = useRef(configs);
|
||||
useEffect(() => {
|
||||
|
@ -188,9 +204,11 @@ function ConfigImpl({
|
|||
return typeof m === 'string' && m[0].toUpperCase() + m.slice(1);
|
||||
}, [configState.mode]);
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Config" />
|
||||
<ContentHeader title={t('Config')} />
|
||||
<div className={s0.root}>
|
||||
{portFields.map((f) =>
|
||||
configState[f.key] !== undefined ? (
|
||||
|
@ -242,7 +260,7 @@ function ConfigImpl({
|
|||
|
||||
<div className={s0.section}>
|
||||
<div>
|
||||
<div className={s0.label}>Chart Style</div>
|
||||
<div className={s0.label}>{t('chart_style')}</div>
|
||||
<Selection2
|
||||
OptionComponent={TrafficChartSample}
|
||||
optionPropsList={propsList}
|
||||
|
@ -250,8 +268,8 @@ function ConfigImpl({
|
|||
onChange={selectChartStyleIndex}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ maxWidth: 360 }}>
|
||||
<div className={s0.label}>Latency Test URL</div>
|
||||
<div className={s0.narrow}>
|
||||
<div className={s0.label}>{t('latency_test_url')}</div>
|
||||
<SelfControlledInput
|
||||
name="latencyTestUrl"
|
||||
type="text"
|
||||
|
@ -263,12 +281,17 @@ function ConfigImpl({
|
|||
<div className={s0.label}>Action</div>
|
||||
<Button label="Switch backend" onClick={openAPIConfigModal} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={s0.label}>{t('lang')}</div>
|
||||
<div className={s0.narrow}>
|
||||
<Select
|
||||
options={langOptions}
|
||||
selected={i18n.language}
|
||||
onChange={(e) => i18n.changeLanguage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'propTypes' does not exist on type '(prop... Remove this comment to see the full error message
|
||||
Config.propTypes = {
|
||||
configs: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import './Connections.css';
|
|||
|
||||
import React from 'react';
|
||||
import { Pause, Play, X as IconClose } from 'react-feather';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { ConnectionItem } from 'src/api/connections';
|
||||
import { State } from 'src/store/types';
|
||||
|
@ -176,9 +177,12 @@ function Conn({ apiConfig }) {
|
|||
useEffect(() => {
|
||||
return connAPI.fetchData(apiConfig, read);
|
||||
}, [apiConfig, read]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Connections" />
|
||||
<ContentHeader title={t('Connections')} />
|
||||
<Tabs>
|
||||
<div
|
||||
style={{
|
||||
|
@ -189,14 +193,14 @@ function Conn({ apiConfig }) {
|
|||
>
|
||||
<TabList>
|
||||
<Tab>
|
||||
<span>Active</span>
|
||||
<span>{t('Active')}</span>
|
||||
<span className={s.connQty}>
|
||||
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
|
||||
<ConnQty qty={filteredConns.length} />
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab>
|
||||
<span>Closed</span>
|
||||
<span>{t('Closed')}</span>
|
||||
<span className={s.connQty}>
|
||||
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
|
||||
<ConnQty qty={filteredClosedConns.length} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { Suspense } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ContentHeader from './ContentHeader';
|
||||
import s0 from './Home.module.css';
|
||||
|
@ -7,9 +8,10 @@ import TrafficChart from './TrafficChart';
|
|||
import TrafficNow from './TrafficNow';
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Overview" />
|
||||
<ContentHeader title={t('Overview')} />
|
||||
<div className={s0.root}>
|
||||
<div>
|
||||
<TrafficNow />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import cx from 'clsx';
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { areEqual, FixedSizeList as List } from 'react-window';
|
||||
|
||||
import { fetchLogs } from '../api/logs';
|
||||
|
@ -73,10 +74,11 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
|
|||
fetchLogs({ ...apiConfig, logLevel }, appendLogInternal);
|
||||
}, [apiConfig, logLevel, appendLogInternal]);
|
||||
const [refLogsContainer, containerHeight] = useRemainingViewPortHeight();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ContentHeader title="Logs" />
|
||||
<ContentHeader title={t('Logs')} />
|
||||
<LogSearch />
|
||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */}
|
||||
<div ref={refLogsContainer} style={{ paddingBottom }}>
|
||||
|
@ -89,7 +91,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
|
|||
<div className={s0.logPlaceholderIcon}>
|
||||
<SvgYacd width={200} height={200} />
|
||||
</div>
|
||||
<div>No logs yet, hang tight...</div>
|
||||
<div>{t('no_logs')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={s0.logsWrapper}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { RotateCw } from 'react-feather';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { queryCache, useQuery } from 'react-query';
|
||||
import { areEqual, VariableSizeList } from 'react-window';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
@ -114,10 +115,12 @@ function Rules({ apiConfig }) {
|
|||
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ rules: RuleItem[]; provider: {... Remove this comment to see the full error message
|
||||
const getItemSize = getItemSizeFactory({ rules, provider });
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={s.header}>
|
||||
<ContentHeader title="Rules" />
|
||||
<ContentHeader title={t('Rules')} />
|
||||
<TextFilter />
|
||||
</div>
|
||||
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.root {
|
||||
background: var(--color-bg-sidebar);
|
||||
min-width: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import Tooltip from '@reach/tooltip';
|
|||
import cx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { Info } from 'react-feather';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FcAreaChart,
|
||||
FcDocument,
|
||||
|
@ -85,6 +86,7 @@ const pages = [
|
|||
];
|
||||
|
||||
function SideBar({ dispatch, theme }) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const switchThemeHooked = useCallback(() => {
|
||||
dispatch(switchTheme());
|
||||
|
@ -99,13 +101,13 @@ function SideBar({ dispatch, theme }) {
|
|||
to={to}
|
||||
isActive={location.pathname === to}
|
||||
iconId={iconId}
|
||||
labelText={labelText}
|
||||
labelText={t(labelText)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={s.footer}>
|
||||
<Tooltip
|
||||
label="theme"
|
||||
label={t('theme')}
|
||||
aria-label={
|
||||
'switch to ' + (theme === 'light' ? 'dark' : 'light') + ' theme'
|
||||
}
|
||||
|
@ -117,7 +119,7 @@ function SideBar({ dispatch, theme }) {
|
|||
{theme === 'light' ? <MoonA /> : <Sun />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="about">
|
||||
<Tooltip label={t('about')}>
|
||||
<Link to="/about" className={s.iconWrapper}>
|
||||
<Info size={20} />
|
||||
</Link>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { fetchData } from '../api/traffic';
|
||||
import useLineChart from '../hooks/useLineChart';
|
||||
|
@ -10,6 +11,8 @@ import {
|
|||
import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app';
|
||||
import { connect } from './StateProvider';
|
||||
|
||||
const { useMemo } = React;
|
||||
|
||||
const chartWrapperStyle = {
|
||||
// make chartjs chart responsive
|
||||
position: 'relative',
|
||||
|
@ -26,6 +29,7 @@ export default connect(mapState)(TrafficChart);
|
|||
function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
|
||||
const Chart = chartJSResource.read();
|
||||
const traffic = fetchData(apiConfig);
|
||||
const { t } = useTranslation();
|
||||
const data = useMemo(
|
||||
() => ({
|
||||
labels: traffic.labels,
|
||||
|
@ -33,18 +37,18 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
|
|||
{
|
||||
...commonDataSetProps,
|
||||
...chartStyles[selectedChartStyleIndex].up,
|
||||
label: 'Up',
|
||||
label: t('Up'),
|
||||
data: traffic.up,
|
||||
},
|
||||
{
|
||||
...commonDataSetProps,
|
||||
...chartStyles[selectedChartStyleIndex].down,
|
||||
label: 'Down',
|
||||
label: t('Down'),
|
||||
data: traffic.down,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[traffic, selectedChartStyleIndex]
|
||||
[traffic, selectedChartStyleIndex, t]
|
||||
);
|
||||
|
||||
useLineChart(Chart, 'trafficChart', data, traffic);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as connAPI from '../api/connections';
|
||||
import { fetchData } from '../api/traffic';
|
||||
|
@ -15,28 +16,29 @@ const mapState = (s) => ({
|
|||
export default connect(mapState)(TrafficNow);
|
||||
|
||||
function TrafficNow({ apiConfig }) {
|
||||
const { t } = useTranslation();
|
||||
const { upStr, downStr } = useSpeed(apiConfig);
|
||||
const { upTotal, dlTotal, connNumber } = useConnection(apiConfig);
|
||||
return (
|
||||
<div className={s0.TrafficNow}>
|
||||
<div className="sec">
|
||||
<div>Upload</div>
|
||||
<div>{t('Upload')}</div>
|
||||
<div>{upStr}</div>
|
||||
</div>
|
||||
<div className="sec">
|
||||
<div>Download</div>
|
||||
<div>{t('Download')}</div>
|
||||
<div>{downStr}</div>
|
||||
</div>
|
||||
<div className="sec">
|
||||
<div>Upload Total</div>
|
||||
<div>{t('Upload Total')}</div>
|
||||
<div>{upTotal}</div>
|
||||
</div>
|
||||
<div className="sec">
|
||||
<div>Download Total</div>
|
||||
<div>{t('Download Total')}</div>
|
||||
<div>{dlTotal}</div>
|
||||
</div>
|
||||
<div className="sec">
|
||||
<div>Active Connections</div>
|
||||
<div>{t('Active Connections')}</div>
|
||||
<div>{connNumber}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Tooltip from '@reach/tooltip';
|
||||
import * as React from 'react';
|
||||
import { Zap } from 'react-feather';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getClashAPIConfig } from '../../store/app';
|
||||
import {
|
||||
|
@ -80,6 +81,8 @@ function Proxies({
|
|||
proxies: { closeModalClosePrevConns, closePrevConnsAndTheModal },
|
||||
} = useStoreActions();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseModal
|
||||
|
@ -89,12 +92,12 @@ function Proxies({
|
|||
<Settings />
|
||||
</BaseModal>
|
||||
<div className={s0.topBar}>
|
||||
<ContentHeader title="Proxies" />
|
||||
<ContentHeader title={t('Proxies')} />
|
||||
<div className={s0.topBarRight}>
|
||||
<div className={s0.textFilterContainer}>
|
||||
<TextFilter />
|
||||
</div>
|
||||
<Tooltip label="settings">
|
||||
<Tooltip label={t('settings')}>
|
||||
<Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}>
|
||||
<Equalizer size={16} />
|
||||
</Button>
|
||||
|
@ -120,7 +123,7 @@ function Proxies({
|
|||
<Fab
|
||||
icon={isTestingLatency ? <ColorZap /> : <Zap width={16} height={16} />}
|
||||
onClick={requestDelayAllFn}
|
||||
text="Test Latency"
|
||||
text={t('Test Latency')}
|
||||
position={fabPosition}
|
||||
/>
|
||||
<BaseModal
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from 'src/components/shared/Select';
|
||||
|
||||
import {
|
||||
getAutoCloseOldConns,
|
||||
getHideUnavailableProxies,
|
||||
getProxySortBy,
|
||||
} from '../../store/app';
|
||||
import Select from '../shared/Select';
|
||||
import { connect, useStoreActions } from '../StateProvider';
|
||||
import Switch from '../SwitchThemed';
|
||||
import s from './Settings.module.css';
|
||||
|
||||
const options = [
|
||||
['Natural', 'Original order in config file'],
|
||||
['LatencyAsc', 'By latency from small to big'],
|
||||
['LatencyDesc', 'By latency from big to small'],
|
||||
['NameAsc', 'By name alphabetically (A-Z)'],
|
||||
['NameDesc', 'By name alphabetically (Z-A)'],
|
||||
['Natural', 'order_natural'],
|
||||
['LatencyAsc', 'order_latency_asc'],
|
||||
['LatencyDesc', 'order_latency_desc'],
|
||||
['NameAsc', 'order_name_asc'],
|
||||
['NameDesc', 'order_name_desc'],
|
||||
];
|
||||
|
||||
const { useCallback } = React;
|
||||
|
@ -38,13 +39,16 @@ function Settings({ appConfig }) {
|
|||
},
|
||||
[updateAppConfig]
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div className={s.labeledInput}>
|
||||
<span>Sorting in group</span>
|
||||
<span>{t('sort_in_grp')}</span>
|
||||
<div>
|
||||
<Select
|
||||
options={options}
|
||||
options={options.map((o) => {
|
||||
return [o[0], t(o[1])];
|
||||
})}
|
||||
selected={appConfig.proxySortBy}
|
||||
onChange={handleProxySortByOnChange}
|
||||
/>
|
||||
|
@ -52,7 +56,7 @@ function Settings({ appConfig }) {
|
|||
</div>
|
||||
<hr />
|
||||
<div className={s.labeledInput}>
|
||||
<span>Hide unavailable proxies</span>
|
||||
<span>{t('hide_unavail_proxies')}</span>
|
||||
<div>
|
||||
<Switch
|
||||
name="hideUnavailableProxies"
|
||||
|
@ -62,7 +66,7 @@ function Settings({ appConfig }) {
|
|||
</div>
|
||||
</div>
|
||||
<div className={s.labeledInput}>
|
||||
<span>Automatically close old connections</span>
|
||||
<span>{t('auto_close_conns')}</span>
|
||||
<div>
|
||||
<Switch
|
||||
name="autoCloseOldConns"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.select {
|
||||
height: 30px;
|
||||
line-height: 1.5;
|
||||
width: 100%;
|
||||
padding-left: 8px;
|
||||
appearance: none;
|
||||
|
|
4
src/custom.d.ts
vendored
4
src/custom.d.ts
vendored
|
@ -7,6 +7,10 @@ declare module '*.module.css' {
|
|||
export default classes;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
i18n: any;
|
||||
}
|
||||
|
||||
// webpack definePlugin replacing variables
|
||||
declare const __VERSION__: string;
|
||||
declare const __DEV__: string;
|
||||
|
|
34
src/i18n/en.ts
Normal file
34
src/i18n/en.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
export const data = {
|
||||
Overview: 'Overview',
|
||||
Proxies: 'Proxies',
|
||||
Rules: 'Rules',
|
||||
Conns: 'Conns',
|
||||
Config: 'Config',
|
||||
Logs: 'Logs',
|
||||
Upload: 'Upload',
|
||||
Download: 'Download',
|
||||
'Upload Total': 'Upload Total',
|
||||
'Download Total': 'Download Total',
|
||||
'Active Connections': 'Active Connections',
|
||||
Up: 'Up',
|
||||
Down: 'Down',
|
||||
'Test Latency': 'Test Latency',
|
||||
settings: 'settings',
|
||||
sort_in_grp: 'Sorting in group',
|
||||
hide_unavail_proxies: 'Hide unavailable proxies',
|
||||
auto_close_conns: 'Automatically close old connections',
|
||||
order_natural: 'Original order in config file',
|
||||
order_latency_asc: 'By latency from small to big',
|
||||
order_latency_desc: 'By latency from big to small',
|
||||
order_name_asc: 'By name alphabetically (A-Z)',
|
||||
order_name_desc: 'By name alphabetically (Z-A)',
|
||||
Connections: 'Connections',
|
||||
Active: 'Active',
|
||||
Closed: 'Closed',
|
||||
theme: 'theme',
|
||||
about: 'about',
|
||||
no_logs: 'No logs yet, hang tight...',
|
||||
chart_style: 'Chart Style',
|
||||
latency_test_url: 'Latency Test URL',
|
||||
lang: 'Language',
|
||||
};
|
34
src/i18n/zh.ts
Normal file
34
src/i18n/zh.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
export const data = {
|
||||
Overview: '概述',
|
||||
Proxies: '代理',
|
||||
Rules: '规则',
|
||||
Conns: '连接',
|
||||
Config: '配置',
|
||||
Logs: '日志',
|
||||
Upload: '上传',
|
||||
Download: '下载',
|
||||
'Upload Total': '上传总量',
|
||||
'Download Total': '下载总量',
|
||||
'Active Connections': '活动连接',
|
||||
Up: '上传',
|
||||
Down: '下载',
|
||||
'Test Latency': '延迟测速',
|
||||
settings: '设置',
|
||||
sort_in_grp: '代理组条目排序',
|
||||
hide_unavail_proxies: '隐藏不可用代理',
|
||||
auto_close_conns: '切换代理时自动断开旧连接',
|
||||
order_natural: '原 config 文件中的排序',
|
||||
order_latency_asc: '按延迟从小到大',
|
||||
order_latency_desc: '按延迟从大到小',
|
||||
order_name_asc: '按名称字母排序 (A-Z)',
|
||||
order_name_desc: '按名称字母排序 (Z-A)',
|
||||
Connections: '连接',
|
||||
Active: '活动',
|
||||
Closed: '已断开',
|
||||
theme: '主题',
|
||||
about: '关于',
|
||||
no_logs: '暂无日志...',
|
||||
chart_style: '流量图样式',
|
||||
latency_test_url: '延迟测速 URL',
|
||||
lang: '语言',
|
||||
};
|
61
src/misc/i18n.ts
Normal file
61
src/misc/i18n.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import i18next from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import HttpBackend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
const allLocales = {
|
||||
zh: import('src/i18n/zh'),
|
||||
en: import('src/i18n/en'),
|
||||
};
|
||||
|
||||
type BackendRequestCallback = (
|
||||
err: null,
|
||||
result: { status: number; data: any }
|
||||
) => void;
|
||||
|
||||
i18next
|
||||
.use(HttpBackend)
|
||||
.use(initReactI18next)
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
// resources,
|
||||
backend: {
|
||||
loadPath: '/__{{lng}}/{{ns}}.json',
|
||||
request: function (
|
||||
_options: any,
|
||||
url: string,
|
||||
_payload: any,
|
||||
callback: BackendRequestCallback
|
||||
) {
|
||||
let p: PromiseLike<{ data: any }>;
|
||||
|
||||
switch (url) {
|
||||
case '/__zh/translation.json':
|
||||
p = allLocales.zh;
|
||||
break;
|
||||
case '/__en/translation.json':
|
||||
default:
|
||||
p = allLocales.en;
|
||||
break;
|
||||
}
|
||||
|
||||
if (p) {
|
||||
p.then((mod) => {
|
||||
callback(null, { status: 200, data: mod.data });
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
supportedLngs: ['en', 'zh'],
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.i18n = i18next;
|
||||
}
|
||||
|
||||
export default i18next;
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
@ -23,6 +24,7 @@
|
|||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"suppressImplicitAnyIndexErrors": true,
|
||||
"types": ["jest"]
|
||||
"types": ["jest"],
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,9 +132,7 @@ module.exports = {
|
|||
// https://github.com/webpack/webpack/issues/11467
|
||||
{
|
||||
test: /\.m?js/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
resolve: { fullySpecified: false },
|
||||
},
|
||||
{
|
||||
test: /\.[tj]sx?$/,
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -1156,7 +1156,7 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.12.5":
|
||||
"@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1":
|
||||
version "7.12.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
||||
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
|
||||
|
@ -1710,6 +1710,13 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-table@^7.0.25":
|
||||
version "7.0.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-table/-/react-table-7.0.25.tgz#79efba1c58149d75b3c030634ed36215b7ba8390"
|
||||
integrity sha512-MLWxIiFKIW2CjcB8yQ5LfLNyVfwXfIYm2yrQfTkroK5tJ3Ai+Xzq73EQcdKWQvi/nLk431v2WV0cf30VQV+5Ow==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-tabs@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-tabs/-/react-tabs-2.3.2.tgz#99fb6866bbc6912d44f7bbc99eca03fbbd217960"
|
||||
|
@ -4427,6 +4434,13 @@ html-minifier-terser@^5.0.1:
|
|||
relateurl "^0.2.7"
|
||||
terser "^4.6.3"
|
||||
|
||||
html-parse-stringify2@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a"
|
||||
integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=
|
||||
dependencies:
|
||||
void-elements "^2.0.1"
|
||||
|
||||
html-webpack-plugin@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c"
|
||||
|
@ -4486,6 +4500,27 @@ human-signals@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||
|
||||
i18next-browser-languagedetector@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.0.1.tgz#83654bc87302be2a6a5a75146ffea97b4ca268cf"
|
||||
integrity sha512-3H+OsNQn3FciomUU0d4zPFHsvJv4X66lBelXk9hnIDYDsveIgT7dWZ3/VvcSlpKk9lvCK770blRZ/CwHMXZqWw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
|
||||
i18next-http-backend@^1.0.21:
|
||||
version "1.0.21"
|
||||
resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-1.0.21.tgz#cee901b3527dad5165fa91de973b6aa6404c1c37"
|
||||
integrity sha512-UDeHoV2B+31Gr++0KFAVjM5l+SEwePpF6sfDyaDq5ennM9QNJ78PBEMPStwkreEm4h5C8sT7M1JdNQrLcU1Wdg==
|
||||
dependencies:
|
||||
node-fetch "2.6.1"
|
||||
|
||||
i18next@^19.8.4:
|
||||
version "19.8.4"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.8.4.tgz#447718f2a26319b8debdbcc6fbc1a9761be7316b"
|
||||
integrity sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.0"
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
@ -5357,6 +5392,11 @@ no-case@^3.0.3:
|
|||
lower-case "^2.0.1"
|
||||
tslib "^1.10.0"
|
||||
|
||||
node-fetch@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
"node-libs-browser@^1.0.0 || ^2.0.0":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
|
||||
|
@ -6422,6 +6462,14 @@ react-helmet@^6.1.0:
|
|||
react-fast-compare "^3.1.1"
|
||||
react-side-effect "^2.1.0"
|
||||
|
||||
react-i18next@^11.7.4:
|
||||
version "11.7.4"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.4.tgz#6c0142e15652d8dd80cd7d857e36efe2e9d4d09a"
|
||||
integrity sha512-Aq0+QVW7NMYuAtk0Stcwp4jWeNTd1p5XefAfBPcjs/4c/2duG3v3G3zdtn8fC8L4EyA/coKLwdULHI+lYTbF8w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
html-parse-stringify2 "2.0.1"
|
||||
|
||||
react-icons@^3.10.0:
|
||||
version "3.11.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.11.0.tgz#2ca2903dfab8268ca18ebd8cc2e879921ec3b254"
|
||||
|
@ -7682,6 +7730,11 @@ vm-browserify@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
|
||||
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
|
||||
|
||||
void-elements@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
|
||||
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
|
||||
|
||||
warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
|
|
Loading…
Reference in a new issue