diff options
| author | Haishan <[email protected]> | 2020-12-06 14:57:59 +0800 |
|---|---|---|
| committer | Haishan <[email protected]> | 2020-12-06 20:19:51 +0800 |
| commit | 8a50ef4ef2f6f6044d36ea2f4fe06e663083972e (patch) | |
| tree | da098c1434b5f745f391330dde37b6468deec45b /src | |
| parent | a8c6cd23ce2b585362f515080b2167990c554fed (diff) | |
feat: initial Chinese UI language support
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.tsx | 1 | ||||
| -rw-r--r-- | src/components/Config.module.css | 6 | ||||
| -rw-r--r-- | src/components/Config.tsx | 51 | ||||
| -rw-r--r-- | src/components/Connections.tsx | 10 | ||||
| -rw-r--r-- | src/components/Home.tsx | 4 | ||||
| -rw-r--r-- | src/components/Logs.tsx | 8 | ||||
| -rw-r--r-- | src/components/Rules.tsx | 5 | ||||
| -rw-r--r-- | src/components/SideBar.module.css | 1 | ||||
| -rw-r--r-- | src/components/SideBar.tsx | 8 | ||||
| -rw-r--r-- | src/components/TrafficChart.tsx | 12 | ||||
| -rw-r--r-- | src/components/TrafficNow.tsx | 14 | ||||
| -rw-r--r-- | src/components/proxies/Proxies.tsx | 9 | ||||
| -rw-r--r-- | src/components/proxies/Settings.tsx | 24 | ||||
| -rw-r--r-- | src/components/shared/Select.module.css | 1 | ||||
| -rw-r--r-- | src/custom.d.ts | 4 | ||||
| -rw-r--r-- | src/i18n/en.ts | 34 | ||||
| -rw-r--r-- | src/i18n/zh.ts | 34 | ||||
| -rw-r--r-- | src/misc/i18n.ts | 61 |
18 files changed, 238 insertions, 49 deletions
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 <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, -}; 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 ( <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} /> 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 ( <div> - <ContentHeader title="Overview" /> + <ContentHeader title={t('Overview')} /> <div className={s0.root}> <div> <TrafficNow /> 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 ( <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}> 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 ( <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 */} 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)} /> ))} </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> 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 ( <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> 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 ( <> <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 diff --git a/src/components/proxies/Settings.tsx b/src/components/proxies/Settings.tsx index bb859ac..703fb0d 100644 --- a/src/components/proxies/Settings.tsx +++ b/src/components/proxies/Settings.tsx @@ -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" diff --git a/src/components/shared/Select.module.css b/src/components/shared/Select.module.css index cbd1ffe..3ea430e 100644 --- a/src/components/shared/Select.module.css +++ b/src/components/shared/Select.module.css @@ -1,5 +1,6 @@ .select { height: 30px; + line-height: 1.5; width: 100%; padding-left: 8px; appearance: none; diff --git a/src/custom.d.ts b/src/custom.d.ts index db7360a..3a9b7ac 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -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; diff --git a/src/i18n/en.ts b/src/i18n/en.ts new file mode 100644 index 0000000..be720c3 --- /dev/null +++ b/src/i18n/en.ts @@ -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', +}; diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts new file mode 100644 index 0000000..0c28260 --- /dev/null +++ b/src/i18n/zh.ts @@ -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: '语言', +}; diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts new file mode 100644 index 0000000..023c5d7 --- /dev/null +++ b/src/misc/i18n.ts @@ -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; |
