diff --git a/package.json b/package.json
index 8f4dbe1..ee412b8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app.tsx b/src/app.tsx
index b4cc818..5d2f226 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,4 +1,5 @@
import 'modern-normalize/modern-normalize.css';
+import './misc/i18n';
import React from 'react';
import ReactDOM from 'react-dom';
diff --git a/src/components/Config.module.css b/src/components/Config.module.css
index d7fa40a..1f71765 100644
--- a/src/components/Config.module.css
+++ b/src/components/Config.module.css
@@ -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;
+}
diff --git a/src/components/Config.tsx b/src/components/Config.tsx
index 981eae1..4659e16 100644
--- a/src/components/Config.tsx
+++ b/src/components/Config.tsx
@@ -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 ;
}
+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 (
-
+
{portFields.map((f) =>
configState[f.key] !== undefined ? (
@@ -242,7 +260,7 @@ function ConfigImpl({
-
Chart Style
+
{t('chart_style')}
-
-
Latency Test URL
+
+
{t('latency_test_url')}
Action
+
+
{t('lang')}
+
+ i18n.changeLanguage(e.target.value)}
+ />
+
+
);
}
-
-// @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,
-};
diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx
index 078d32e..63b0010 100644
--- a/src/components/Connections.tsx
+++ b/src/components/Connections.tsx
@@ -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 (
-
+
- Active
+ {t('Active')}
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
- Closed
+ {t('Closed')}
{/* @ts-expect-error ts-migrate(2786) FIXME: 'ConnQty' cannot be used as a JSX component. */}
diff --git a/src/components/Home.tsx b/src/components/Home.tsx
index 532379b..a6df373 100644
--- a/src/components/Home.tsx
+++ b/src/components/Home.tsx
@@ -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 (
-
+
diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx
index 5bc1f5d..3a4dabd 100644
--- a/src/components/Logs.tsx
+++ b/src/components/Logs.tsx
@@ -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 (
-
+
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject
' is not assig... Remove this comment to see the full error message */}
@@ -89,7 +91,7 @@ function Logs({ dispatch, logLevel, apiConfig, logs }) {
-
No logs yet, hang tight...
+
{t('no_logs')}
) : (
diff --git a/src/components/Rules.tsx b/src/components/Rules.tsx
index dab479c..008ce3c 100644
--- a/src/components/Rules.tsx
+++ b/src/components/Rules.tsx
@@ -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 (
-
+
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'number | MutableRefObject
' is not assig... Remove this comment to see the full error message */}
diff --git a/src/components/SideBar.module.css b/src/components/SideBar.module.css
index 7ecb3c3..744d29d 100644
--- a/src/components/SideBar.module.css
+++ b/src/components/SideBar.module.css
@@ -1,5 +1,6 @@
.root {
background: var(--color-bg-sidebar);
+ min-width: 150px;
position: relative;
}
diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx
index 6cfd829..973f003 100644
--- a/src/components/SideBar.tsx
+++ b/src/components/SideBar.tsx
@@ -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)}
/>
))}
:
}
-
+
diff --git a/src/components/TrafficChart.tsx b/src/components/TrafficChart.tsx
index 5fcdf7d..056cac6 100644
--- a/src/components/TrafficChart.tsx
+++ b/src/components/TrafficChart.tsx
@@ -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);
diff --git a/src/components/TrafficNow.tsx b/src/components/TrafficNow.tsx
index cfab65b..fbcc4e9 100644
--- a/src/components/TrafficNow.tsx
+++ b/src/components/TrafficNow.tsx
@@ -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 (
-
Upload
+
{t('Upload')}
{upStr}
-
Download
+
{t('Download')}
{downStr}
-
Upload Total
+
{t('Upload Total')}
{upTotal}
-
Download Total
+
{t('Download Total')}
{dlTotal}
-
Active Connections
+
{t('Active Connections')}
{connNumber}
diff --git a/src/components/proxies/Proxies.tsx b/src/components/proxies/Proxies.tsx
index 7fbe99c..6c3db7d 100644
--- a/src/components/proxies/Proxies.tsx
+++ b/src/components/proxies/Proxies.tsx
@@ -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 (
<>
-
+
-
+
setIsSettingsModalOpen(true)}>
@@ -120,7 +123,7 @@ function Proxies({
: }
onClick={requestDelayAllFn}
- text="Test Latency"
+ text={t('Test Latency')}
position={fabPosition}
/>
-
Sorting in group
+
{t('sort_in_grp')}
{
+ return [o[0], t(o[1])];
+ })}
selected={appConfig.proxySortBy}
onChange={handleProxySortByOnChange}
/>
@@ -52,7 +56,7 @@ function Settings({ appConfig }) {
-
Hide unavailable proxies
+
{t('hide_unavail_proxies')}
-
Automatically close old connections
+
{t('auto_close_conns')}
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;
diff --git a/tsconfig.json b/tsconfig.json
index b7f0ee5..caaea79 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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
}
}
diff --git a/webpack.config.js b/webpack.config.js
index 9aaa9f4..067318d 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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?$/,
diff --git a/yarn.lock b/yarn.lock
index 258d196..ef5283b 100644
--- a/yarn.lock
+++ b/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"