feat: initial Chinese UI language support

This commit is contained in:
Haishan 2020-12-06 14:57:59 +08:00
parent a8c6cd23ce
commit 8a50ef4ef2
22 changed files with 301 additions and 54 deletions

View file

@ -33,6 +33,9 @@
"fontsource-roboto-mono": "^3.0.3", "fontsource-roboto-mono": "^3.0.3",
"framer-motion": "^2.9.5", "framer-motion": "^2.9.5",
"history": "^5.0.0", "history": "^5.0.0",
"i18next": "^19.8.4",
"i18next-browser-languagedetector": "^6.0.1",
"i18next-http-backend": "^1.0.21",
"immer": "^8.0.0", "immer": "^8.0.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",
@ -43,6 +46,7 @@
"react-dom": "0.0.0-experimental-94c0244ba", "react-dom": "0.0.0-experimental-94c0244ba",
"react-feather": "^2.0.9", "react-feather": "^2.0.9",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-i18next": "^11.7.4",
"react-icons": "^3.10.0", "react-icons": "^3.10.0",
"react-modal": "^3.12.1", "react-modal": "^3.12.1",
"react-query": "^2.26.3", "react-query": "^2.26.3",
@ -80,6 +84,7 @@
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "^6.1.0",
"@types/react-modal": "^3.10.6", "@types/react-modal": "^3.10.6",
"@types/react-table": "^7.0.25",
"@types/react-tabs": "^2.3.2", "@types/react-tabs": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^4.9.0", "@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0", "@typescript-eslint/parser": "^4.9.0",

View file

@ -1,4 +1,5 @@
import 'modern-normalize/modern-normalize.css'; import 'modern-normalize/modern-normalize.css';
import './misc/i18n';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';

View file

@ -11,7 +11,7 @@
.section { .section {
padding: 6px 15px 15px; padding: 6px 15px 15px;
@media (--breakpoint-not-small) { @media (--breakpoint-not-small) {
padding: 10px 40px 40px; padding: 0 40px 40px;
} }
} }
@ -28,3 +28,7 @@
.label { .label {
padding: 16px 0; padding: 16px 0;
} }
.narrow {
width: 360px;
}

View file

@ -1,5 +1,8 @@
import PropTypes from 'prop-types'; import * as React from 'react';
import 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 { import {
getClashAPIConfig, getClashAPIConfig,
@ -67,12 +70,17 @@ const portFields = [
{ key: 'redir-port', label: 'Redir Port' }, { key: 'redir-port', label: 'Redir Port' },
]; ];
const mapState = (s) => ({ const langOptions = [
['zh', '中文'],
['en', 'English'],
];
const mapState = (s: State) => ({
configs: getConfigs(s), configs: getConfigs(s),
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),
}); });
const mapState2 = (s) => ({ const mapState2 = (s: State) => ({
selectedChartStyleIndex: getSelectedChartStyleIndex(s), selectedChartStyleIndex: getSelectedChartStyleIndex(s),
latencyTestUrl: getLatencyTestUrl(s), latencyTestUrl: getLatencyTestUrl(s),
apiConfig: getClashAPIConfig(s), apiConfig: getClashAPIConfig(s),
@ -88,13 +96,21 @@ function ConfigContainer({ dispatch, configs, apiConfig }) {
return <Config configs={configs} />; return <Config configs={configs} />;
} }
type ConfigImplProps = {
dispatch: DispatchFn;
configs: ClashGeneralConfig;
selectedChartStyleIndex: number;
latencyTestUrl: string;
apiConfig: ClashAPIConfig;
};
function ConfigImpl({ function ConfigImpl({
dispatch, dispatch,
configs, configs,
selectedChartStyleIndex, selectedChartStyleIndex,
latencyTestUrl, latencyTestUrl,
apiConfig, apiConfig,
}) { }: ConfigImplProps) {
const [configState, setConfigStateInternal] = useState(configs); const [configState, setConfigStateInternal] = useState(configs);
const refConfigs = useRef(configs); const refConfigs = useRef(configs);
useEffect(() => { useEffect(() => {
@ -188,9 +204,11 @@ function ConfigImpl({
return typeof m === 'string' && m[0].toUpperCase() + m.slice(1); return typeof m === 'string' && m[0].toUpperCase() + m.slice(1);
}, [configState.mode]); }, [configState.mode]);
const { t, i18n } = useTranslation();
return ( return (
<div> <div>
<ContentHeader title="Config" /> <ContentHeader title={t('Config')} />
<div className={s0.root}> <div className={s0.root}>
{portFields.map((f) => {portFields.map((f) =>
configState[f.key] !== undefined ? ( configState[f.key] !== undefined ? (
@ -242,7 +260,7 @@ function ConfigImpl({
<div className={s0.section}> <div className={s0.section}>
<div> <div>
<div className={s0.label}>Chart Style</div> <div className={s0.label}>{t('chart_style')}</div>
<Selection2 <Selection2
OptionComponent={TrafficChartSample} OptionComponent={TrafficChartSample}
optionPropsList={propsList} optionPropsList={propsList}
@ -250,8 +268,8 @@ function ConfigImpl({
onChange={selectChartStyleIndex} onChange={selectChartStyleIndex}
/> />
</div> </div>
<div style={{ maxWidth: 360 }}> <div className={s0.narrow}>
<div className={s0.label}>Latency Test URL</div> <div className={s0.label}>{t('latency_test_url')}</div>
<SelfControlledInput <SelfControlledInput
name="latencyTestUrl" name="latencyTestUrl"
type="text" type="text"
@ -263,12 +281,17 @@ function ConfigImpl({
<div className={s0.label}>Action</div> <div className={s0.label}>Action</div>
<Button label="Switch backend" onClick={openAPIConfigModal} /> <Button label="Switch backend" onClick={openAPIConfigModal} />
</div> </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>
</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,
};

View file

@ -2,6 +2,7 @@ import './Connections.css';
import React from 'react'; import React from 'react';
import { Pause, Play, X as IconClose } from 'react-feather'; import { Pause, Play, X as IconClose } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'; import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { ConnectionItem } from 'src/api/connections'; import { ConnectionItem } from 'src/api/connections';
import { State } from 'src/store/types'; import { State } from 'src/store/types';
@ -176,9 +177,12 @@ function Conn({ apiConfig }) {
useEffect(() => { useEffect(() => {
return connAPI.fetchData(apiConfig, read); return connAPI.fetchData(apiConfig, read);
}, [apiConfig, read]); }, [apiConfig, read]);
const { t } = useTranslation();
return ( return (
<div> <div>
<ContentHeader title="Connections" /> <ContentHeader title={t('Connections')} />
<Tabs> <Tabs>
<div <div
style={{ style={{
@ -189,14 +193,14 @@ function Conn({ apiConfig }) {
> >
<TabList> <TabList>
<Tab> <Tab>
<span>Active</span> <span>{t('Active')}</span>
<span className={s.connQty}> <span className={s.connQty}>
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */} {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
<ConnQty qty={filteredConns.length} /> <ConnQty qty={filteredConns.length} />
</span> </span>
</Tab> </Tab>
<Tab> <Tab>
<span>Closed</span> <span>{t('Closed')}</span>
<span className={s.connQty}> <span className={s.connQty}>
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */} {/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
<ConnQty qty={filteredClosedConns.length} /> <ConnQty qty={filteredClosedConns.length} />

View file

@ -1,4 +1,5 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import ContentHeader from './ContentHeader'; import ContentHeader from './ContentHeader';
import s0 from './Home.module.css'; import s0 from './Home.module.css';
@ -7,9 +8,10 @@ import TrafficChart from './TrafficChart';
import TrafficNow from './TrafficNow'; import TrafficNow from './TrafficNow';
export default function Home() { export default function Home() {
const { t } = useTranslation();
return ( return (
<div> <div>
<ContentHeader title="Overview" /> <ContentHeader title={t('Overview')} />
<div className={s0.root}> <div className={s0.root}>
<div> <div>
<TrafficNow /> <TrafficNow />

View file

@ -1,5 +1,6 @@
import cx from 'clsx'; 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 { areEqual, FixedSizeList as List } from 'react-window';
import { fetchLogs } from '../api/logs'; import { fetchLogs } from '../api/logs';
@ -73,10 +74,11 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
fetchLogs({ ...apiConfig, logLevel }, appendLogInternal); fetchLogs({ ...apiConfig, logLevel }, appendLogInternal);
}, [apiConfig, logLevel, appendLogInternal]); }, [apiConfig, logLevel, appendLogInternal]);
const [refLogsContainer, containerHeight] = useRemainingViewPortHeight(); const [refLogsContainer, containerHeight] = useRemainingViewPortHeight();
const { t } = useTranslation();
return ( return (
<div> <div>
<ContentHeader title="Logs" /> <ContentHeader title={t('Logs')} />
<LogSearch /> <LogSearch />
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */} {/* @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 }}> <div ref={refLogsContainer} style={{ paddingBottom }}>
@ -89,7 +91,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
<div className={s0.logPlaceholderIcon}> <div className={s0.logPlaceholderIcon}>
<SvgYacd width={200} height={200} /> <SvgYacd width={200} height={200} />
</div> </div>
<div>No logs yet, hang tight...</div> <div>{t('no_logs')}</div>
</div> </div>
) : ( ) : (
<div className={s0.logsWrapper}> <div className={s0.logsWrapper}>

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { RotateCw } from 'react-feather'; import { RotateCw } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { queryCache, useQuery } from 'react-query'; import { queryCache, useQuery } from 'react-query';
import { areEqual, VariableSizeList } from 'react-window'; import { areEqual, VariableSizeList } from 'react-window';
import { useRecoilState } from 'recoil'; 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 // @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 getItemSize = getItemSizeFactory({ rules, provider });
const { t } = useTranslation();
return ( return (
<div> <div>
<div className={s.header}> <div className={s.header}>
<ContentHeader title="Rules" /> <ContentHeader title={t('Rules')} />
<TextFilter /> <TextFilter />
</div> </div>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */} {/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject<any>' is not assig... Remove this comment to see the full error message */}

View file

@ -1,5 +1,6 @@
.root { .root {
background: var(--color-bg-sidebar); background: var(--color-bg-sidebar);
min-width: 150px;
position: relative; position: relative;
} }

View file

@ -2,6 +2,7 @@ import Tooltip from '@reach/tooltip';
import cx from 'clsx'; import cx from 'clsx';
import * as React from 'react'; import * as React from 'react';
import { Info } from 'react-feather'; import { Info } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { import {
FcAreaChart, FcAreaChart,
FcDocument, FcDocument,
@ -85,6 +86,7 @@ const pages = [
]; ];
function SideBar({ dispatch, theme }) { function SideBar({ dispatch, theme }) {
const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const switchThemeHooked = useCallback(() => { const switchThemeHooked = useCallback(() => {
dispatch(switchTheme()); dispatch(switchTheme());
@ -99,13 +101,13 @@ function SideBar({ dispatch, theme }) {
to={to} to={to}
isActive={location.pathname === to} isActive={location.pathname === to}
iconId={iconId} iconId={iconId}
labelText={labelText} labelText={t(labelText)}
/> />
))} ))}
</div> </div>
<div className={s.footer}> <div className={s.footer}>
<Tooltip <Tooltip
label="theme" label={t('theme')}
aria-label={ aria-label={
'switch to ' + (theme === 'light' ? 'dark' : 'light') + ' theme' 'switch to ' + (theme === 'light' ? 'dark' : 'light') + ' theme'
} }
@ -117,7 +119,7 @@ function SideBar({ dispatch, theme }) {
{theme === 'light' ? <MoonA /> : <Sun />} {theme === 'light' ? <MoonA /> : <Sun />}
</button> </button>
</Tooltip> </Tooltip>
<Tooltip label="about"> <Tooltip label={t('about')}>
<Link to="/about" className={s.iconWrapper}> <Link to="/about" className={s.iconWrapper}>
<Info size={20} /> <Info size={20} />
</Link> </Link>

View file

@ -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 { fetchData } from '../api/traffic';
import useLineChart from '../hooks/useLineChart'; import useLineChart from '../hooks/useLineChart';
@ -10,6 +11,8 @@ import {
import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app'; import { getClashAPIConfig, getSelectedChartStyleIndex } from '../store/app';
import { connect } from './StateProvider'; import { connect } from './StateProvider';
const { useMemo } = React;
const chartWrapperStyle = { const chartWrapperStyle = {
// make chartjs chart responsive // make chartjs chart responsive
position: 'relative', position: 'relative',
@ -26,6 +29,7 @@ export default connect(mapState)(TrafficChart);
function TrafficChart({ apiConfig, selectedChartStyleIndex }) { function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
const Chart = chartJSResource.read(); const Chart = chartJSResource.read();
const traffic = fetchData(apiConfig); const traffic = fetchData(apiConfig);
const { t } = useTranslation();
const data = useMemo( const data = useMemo(
() => ({ () => ({
labels: traffic.labels, labels: traffic.labels,
@ -33,18 +37,18 @@ function TrafficChart({ apiConfig, selectedChartStyleIndex }) {
{ {
...commonDataSetProps, ...commonDataSetProps,
...chartStyles[selectedChartStyleIndex].up, ...chartStyles[selectedChartStyleIndex].up,
label: 'Up', label: t('Up'),
data: traffic.up, data: traffic.up,
}, },
{ {
...commonDataSetProps, ...commonDataSetProps,
...chartStyles[selectedChartStyleIndex].down, ...chartStyles[selectedChartStyleIndex].down,
label: 'Down', label: t('Down'),
data: traffic.down, data: traffic.down,
}, },
], ],
}), }),
[traffic, selectedChartStyleIndex] [traffic, selectedChartStyleIndex, t]
); );
useLineChart(Chart, 'trafficChart', data, traffic); useLineChart(Chart, 'trafficChart', data, traffic);

View file

@ -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 * as connAPI from '../api/connections';
import { fetchData } from '../api/traffic'; import { fetchData } from '../api/traffic';
@ -15,28 +16,29 @@ const mapState = (s) => ({
export default connect(mapState)(TrafficNow); export default connect(mapState)(TrafficNow);
function TrafficNow({ apiConfig }) { function TrafficNow({ apiConfig }) {
const { t } = useTranslation();
const { upStr, downStr } = useSpeed(apiConfig); const { upStr, downStr } = useSpeed(apiConfig);
const { upTotal, dlTotal, connNumber } = useConnection(apiConfig); const { upTotal, dlTotal, connNumber } = useConnection(apiConfig);
return ( return (
<div className={s0.TrafficNow}> <div className={s0.TrafficNow}>
<div className="sec"> <div className="sec">
<div>Upload</div> <div>{t('Upload')}</div>
<div>{upStr}</div> <div>{upStr}</div>
</div> </div>
<div className="sec"> <div className="sec">
<div>Download</div> <div>{t('Download')}</div>
<div>{downStr}</div> <div>{downStr}</div>
</div> </div>
<div className="sec"> <div className="sec">
<div>Upload Total</div> <div>{t('Upload Total')}</div>
<div>{upTotal}</div> <div>{upTotal}</div>
</div> </div>
<div className="sec"> <div className="sec">
<div>Download Total</div> <div>{t('Download Total')}</div>
<div>{dlTotal}</div> <div>{dlTotal}</div>
</div> </div>
<div className="sec"> <div className="sec">
<div>Active Connections</div> <div>{t('Active Connections')}</div>
<div>{connNumber}</div> <div>{connNumber}</div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,7 @@
import Tooltip from '@reach/tooltip'; import Tooltip from '@reach/tooltip';
import * as React from 'react'; import * as React from 'react';
import { Zap } from 'react-feather'; import { Zap } from 'react-feather';
import { useTranslation } from 'react-i18next';
import { getClashAPIConfig } from '../../store/app'; import { getClashAPIConfig } from '../../store/app';
import { import {
@ -80,6 +81,8 @@ function Proxies({
proxies: { closeModalClosePrevConns, closePrevConnsAndTheModal }, proxies: { closeModalClosePrevConns, closePrevConnsAndTheModal },
} = useStoreActions(); } = useStoreActions();
const { t } = useTranslation();
return ( return (
<> <>
<BaseModal <BaseModal
@ -89,12 +92,12 @@ function Proxies({
<Settings /> <Settings />
</BaseModal> </BaseModal>
<div className={s0.topBar}> <div className={s0.topBar}>
<ContentHeader title="Proxies" /> <ContentHeader title={t('Proxies')} />
<div className={s0.topBarRight}> <div className={s0.topBarRight}>
<div className={s0.textFilterContainer}> <div className={s0.textFilterContainer}>
<TextFilter /> <TextFilter />
</div> </div>
<Tooltip label="settings"> <Tooltip label={t('settings')}>
<Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}> <Button kind="minimal" onClick={() => setIsSettingsModalOpen(true)}>
<Equalizer size={16} /> <Equalizer size={16} />
</Button> </Button>
@ -120,7 +123,7 @@ function Proxies({
<Fab <Fab
icon={isTestingLatency ? <ColorZap /> : <Zap width={16} height={16} />} icon={isTestingLatency ? <ColorZap /> : <Zap width={16} height={16} />}
onClick={requestDelayAllFn} onClick={requestDelayAllFn}
text="Test Latency" text={t('Test Latency')}
position={fabPosition} position={fabPosition}
/> />
<BaseModal <BaseModal

View file

@ -1,21 +1,22 @@
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next';
import Select from 'src/components/shared/Select';
import { import {
getAutoCloseOldConns, getAutoCloseOldConns,
getHideUnavailableProxies, getHideUnavailableProxies,
getProxySortBy, getProxySortBy,
} from '../../store/app'; } from '../../store/app';
import Select from '../shared/Select';
import { connect, useStoreActions } from '../StateProvider'; import { connect, useStoreActions } from '../StateProvider';
import Switch from '../SwitchThemed'; import Switch from '../SwitchThemed';
import s from './Settings.module.css'; import s from './Settings.module.css';
const options = [ const options = [
['Natural', 'Original order in config file'], ['Natural', 'order_natural'],
['LatencyAsc', 'By latency from small to big'], ['LatencyAsc', 'order_latency_asc'],
['LatencyDesc', 'By latency from big to small'], ['LatencyDesc', 'order_latency_desc'],
['NameAsc', 'By name alphabetically (A-Z)'], ['NameAsc', 'order_name_asc'],
['NameDesc', 'By name alphabetically (Z-A)'], ['NameDesc', 'order_name_desc'],
]; ];
const { useCallback } = React; const { useCallback } = React;
@ -38,13 +39,16 @@ function Settings({ appConfig }) {
}, },
[updateAppConfig] [updateAppConfig]
); );
const { t } = useTranslation();
return ( return (
<> <>
<div className={s.labeledInput}> <div className={s.labeledInput}>
<span>Sorting in group</span> <span>{t('sort_in_grp')}</span>
<div> <div>
<Select <Select
options={options} options={options.map((o) => {
return [o[0], t(o[1])];
})}
selected={appConfig.proxySortBy} selected={appConfig.proxySortBy}
onChange={handleProxySortByOnChange} onChange={handleProxySortByOnChange}
/> />
@ -52,7 +56,7 @@ function Settings({ appConfig }) {
</div> </div>
<hr /> <hr />
<div className={s.labeledInput}> <div className={s.labeledInput}>
<span>Hide unavailable proxies</span> <span>{t('hide_unavail_proxies')}</span>
<div> <div>
<Switch <Switch
name="hideUnavailableProxies" name="hideUnavailableProxies"
@ -62,7 +66,7 @@ function Settings({ appConfig }) {
</div> </div>
</div> </div>
<div className={s.labeledInput}> <div className={s.labeledInput}>
<span>Automatically close old connections</span> <span>{t('auto_close_conns')}</span>
<div> <div>
<Switch <Switch
name="autoCloseOldConns" name="autoCloseOldConns"

View file

@ -1,5 +1,6 @@
.select { .select {
height: 30px; height: 30px;
line-height: 1.5;
width: 100%; width: 100%;
padding-left: 8px; padding-left: 8px;
appearance: none; appearance: none;

4
src/custom.d.ts vendored
View file

@ -7,6 +7,10 @@ declare module '*.module.css' {
export default classes; export default classes;
} }
interface Window {
i18n: any;
}
// webpack definePlugin replacing variables // webpack definePlugin replacing variables
declare const __VERSION__: string; declare const __VERSION__: string;
declare const __DEV__: string; declare const __DEV__: string;

34
src/i18n/en.ts Normal file
View 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
View 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
View 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;

View file

@ -1,4 +1,5 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@ -23,6 +24,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": false,
"suppressImplicitAnyIndexErrors": true, "suppressImplicitAnyIndexErrors": true,
"types": ["jest"] "types": ["jest"],
"resolveJsonModule": true
} }
} }

View file

@ -132,9 +132,7 @@ module.exports = {
// https://github.com/webpack/webpack/issues/11467 // https://github.com/webpack/webpack/issues/11467
{ {
test: /\.m?js/, test: /\.m?js/,
resolve: { resolve: { fullySpecified: false },
fullySpecified: false,
},
}, },
{ {
test: /\.[tj]sx?$/, test: /\.[tj]sx?$/,

View file

@ -1156,7 +1156,7 @@
dependencies: dependencies:
regenerator-runtime "^0.13.4" 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" version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
@ -1710,6 +1710,13 @@
dependencies: dependencies:
"@types/react" "*" "@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": "@types/react-tabs@^2.3.2":
version "2.3.2" version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/react-tabs/-/react-tabs-2.3.2.tgz#99fb6866bbc6912d44f7bbc99eca03fbbd217960" 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" relateurl "^0.2.7"
terser "^4.6.3" 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: html-webpack-plugin@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz#625097650886b97ea5dae331c320e3238f6c121c" 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" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== 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: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 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" lower-case "^2.0.1"
tslib "^1.10.0" 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": "node-libs-browser@^1.0.0 || ^2.0.0":
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" 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-fast-compare "^3.1.1"
react-side-effect "^2.1.0" 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: react-icons@^3.10.0:
version "3.11.0" version "3.11.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.11.0.tgz#2ca2903dfab8268ca18ebd8cc2e879921ec3b254" 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" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== 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: warning@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"