From f97b60f1cbb5d7f2174707289fcfcedfcb5c2da1 Mon Sep 17 00:00:00 2001 From: Dreamacro <305009791@qq.com> Date: Fri, 5 Jul 2019 16:55:56 +0800 Subject: [PATCH] Optimization: remove i18n to reduce bundle size --- package-lock.json | 58 +++++-------- package.json | 7 +- src/components/Tags/index.tsx | 9 +- src/containers/App.tsx | 4 - .../ExternalControllerDrawer/index.tsx | 4 +- src/containers/Logs/index.tsx | 5 +- src/containers/Proxies/index.tsx | 4 +- src/containers/Rules/index.tsx | 4 +- src/containers/Settings/index.tsx | 19 ++--- src/containers/Sidebar/index.tsx | 5 +- src/i18n/index.ts | 85 ++++++++++++++----- src/models/I18n.ts | 10 --- src/models/index.ts | 1 - src/render.tsx | 6 +- src/stores/HookStore.ts | 4 +- 15 files changed, 118 insertions(+), 107 deletions(-) delete mode 100644 src/models/I18n.ts diff --git a/package-lock.json b/package-lock.json index d40edaf..efd4f76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -952,6 +952,21 @@ "integrity": "sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q==", "dev": true }, + "@types/lodash": { + "version": "4.14.135", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.135.tgz", + "integrity": "sha512-Ed+tSZ9qM1oYpi5kzdsBuOzcAIn1wDW+e8TFJ50IMJMlSopGdJgKAbhHzN6h1E1OfjlGOr2JepzEWtg9NIfoNg==", + "dev": true + }, + "@types/lodash-es": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.3.tgz", + "integrity": "sha512-iHI0i7ZAL1qepz1Y7f3EKg/zUMDwDfTzitx+AlHhJJvXwenP682ZyGbgPSc5Ej3eEAKVbNWKFuwOadCj5vBbYQ==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -5925,14 +5940,6 @@ } } }, - "html-parse-stringify2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", - "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", - "requires": { - "void-elements": "^2.0.1" - } - }, "html-tags": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.0.0.tgz", @@ -6095,19 +6102,6 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, - "i18next": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.6.tgz", - "integrity": "sha512-bdNhzhcM6RG5m82RypVguCrAQNie/ycxW0Q5C6K9UDWD5hqApZfdJFbj4Ikz9jxIR+Ja1eg0yCQLhlCT+opwIg==", - "requires": { - "@babel/runtime": "^7.3.1" - } - }, - "i18next-browser-languagedetector": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.1.tgz", - "integrity": "sha512-WFjPLNPWl62uu07AHY2g+KsC9qz0tyMq+OZEB/H7N58YKL/JLiCz9U709gaR20Mule/Ppn+uyfVx5REJJjn1HA==" - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7209,6 +7203,11 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -9256,15 +9255,6 @@ } } }, - "react-i18next": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-10.11.3.tgz", - "integrity": "sha512-+kR0SQrTSws+NQfqK6Xe2FR6tMyfIPB6voxUqnLQ35Eh7T0vfe+v7eC4fF3pZlGGyn0qPKr294ACGbIz74u0sQ==", - "requires": { - "@babel/runtime": "^7.3.1", - "html-parse-stringify2": "2.0.1" - } - }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -11799,7 +11789,8 @@ "typescript": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", - "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==" + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "dev": true }, "uglify-js": { "version": "3.4.10", @@ -12260,11 +12251,6 @@ "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", "dev": true }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" - }, "watchpack": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", diff --git a/package.json b/package.json index 0d15bf0..6c1814f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@babel/preset-react": "^7.0.0", "@hot-loader/react-dom": "^16.8.6", "@types/classnames": "^2.2.8", + "@types/lodash-es": "^4.17.3", "@types/node": "^12.0.10", "@types/react": "^16.8.22", "@types/react-dom": "^16.8.4", @@ -64,6 +65,7 @@ "tslint": "^5.18.0", "tslint-config-standard": "^8.0.1", "tslint-loader": "^3.6.0", + "typescript": "^3.5.2", "webpack": "^4.35.2", "webpack-cli": "^3.3.5", "webpack-dev-middleware": "^3.7.0", @@ -76,16 +78,13 @@ "classnames": "^2.2.6", "dayjs": "^1.8.14", "eventemitter3": "^4.0.0", - "i18next": "^17.0.6", - "i18next-browser-languagedetector": "^3.0.1", "immer": "^3.1.3", + "lodash-es": "^4.17.11", "react": "^16.8.6", "react-dom": "^16.8.6", - "react-i18next": "^10.11.2", "react-router-dom": "^5.0.1", "react-virtualized-auto-sizer": "^1.0.2", "react-window": "^1.8.4", - "typescript": "^3.5.2", "unstated-next": "^1.1.0", "use-immer": "^0.3.2", "yaml": "^1.6.0" diff --git a/src/components/Tags/index.tsx b/src/components/Tags/index.tsx index 22f029b..700ae86 100644 --- a/src/components/Tags/index.tsx +++ b/src/components/Tags/index.tsx @@ -1,11 +1,11 @@ import React, { useState, useRef, useLayoutEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { BaseComponentProps, I18nProps } from '@models' +import { containers } from '@stores' +import { BaseComponentProps } from '@models' import { noop } from '@lib/helper' import classnames from 'classnames' import './style.scss' -interface TagsProps extends BaseComponentProps, I18nProps { +interface TagsProps extends BaseComponentProps { data: string[] onClick: (name: string) => void select: string @@ -16,7 +16,8 @@ interface TagsProps extends BaseComponentProps, I18nProps { export function Tags (props: TagsProps) { const { className, data, onClick, select, canClick, rowHeight: rawHeight } = props - const { t } = useTranslation(['Proxies']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('Proxies') const [expand, setExpand] = useState(false) const [showExtend, setShowExtend] = useState(false) diff --git a/src/containers/App.tsx b/src/containers/App.tsx index ae21200..f950d3e 100644 --- a/src/containers/App.tsx +++ b/src/containers/App.tsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react' import { Route, Redirect } from 'react-router-dom' import { hot } from 'react-hot-loader/root' import classnames from 'classnames' -import { I18nProps } from '@models' import { isClashX } from '@lib/jsBridge' import './App.scss' @@ -15,9 +14,6 @@ import SlideBar from '@containers/Sidebar' import ExternalControllerModal from '@containers/ExternalControllerDrawer' import { getLogsStreamReader } from '@lib/request' -export interface AppProps extends I18nProps { -} - function App () { useEffect(() => { getLogsStreamReader() diff --git a/src/containers/ExternalControllerDrawer/index.tsx b/src/containers/ExternalControllerDrawer/index.tsx index 975e12e..e148d27 100644 --- a/src/containers/ExternalControllerDrawer/index.tsx +++ b/src/containers/ExternalControllerDrawer/index.tsx @@ -1,12 +1,12 @@ import React, { useEffect } from 'react' -import { useTranslation } from 'react-i18next' import { useObject } from '@lib/hook' import { Modal, Input, Row, Col, Alert } from '@components' import { containers } from '@stores' import './style.scss' export default function ExternalController () { - const { t } = useTranslation(['Settings']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('Settings') const { data: info, update, fetch } = containers.useAPIInfo() const { unauthorized: { hidden, visible } } = containers.useData() const { value, set, change } = useObject({ diff --git a/src/containers/Logs/index.tsx b/src/containers/Logs/index.tsx index 23325b1..551f50e 100644 --- a/src/containers/Logs/index.tsx +++ b/src/containers/Logs/index.tsx @@ -1,6 +1,6 @@ import React, { useLayoutEffect, useEffect, useRef, useState } from 'react' import dayjs from 'dayjs' -import { useTranslation } from 'react-i18next' +import { containers } from '@stores' import { Card, Header } from '@components' import { getLogsStreamReader } from '@lib/request' import { StreamReader } from '@lib/streamer' @@ -11,7 +11,8 @@ export default function Logs () { const listRef = useRef() const logsRef = useRef([]) const [logs, setLogs] = useState([]) - const { t } = useTranslation(['Logs']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('Logs') useLayoutEffect(() => { const ul = listRef.current diff --git a/src/containers/Proxies/index.tsx b/src/containers/Proxies/index.tsx index 5031a01..c25d16a 100644 --- a/src/containers/Proxies/index.tsx +++ b/src/containers/Proxies/index.tsx @@ -1,5 +1,4 @@ import React, { useLayoutEffect } from 'react' -import { useTranslation } from 'react-i18next' import EE from '@lib/event' import { Card, Header, Icon } from '@components' import { containers } from '@stores' @@ -9,7 +8,8 @@ import './style.scss' export default function Proxies () { const { data, fetch } = containers.useData() - const { t } = useTranslation(['Proxies']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('Proxies') useLayoutEffect(() => { fetch() diff --git a/src/containers/Rules/index.tsx b/src/containers/Rules/index.tsx index d999a03..c620f50 100644 --- a/src/containers/Rules/index.tsx +++ b/src/containers/Rules/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect } from 'react' -import { useTranslation } from 'react-i18next' import { Header, Card, Row, Col } from '@components' import { containers } from '@stores' import { FixedSizeList as List } from 'react-window' @@ -8,7 +7,8 @@ import './style.scss' export default function Rules () { const { data, fetch } = containers.useData() - const { t } = useTranslation(['Rules']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('Rules') const { rules } = data useEffect(() => { diff --git a/src/containers/Settings/index.tsx b/src/containers/Settings/index.tsx index 55b32c4..737476b 100644 --- a/src/containers/Settings/index.tsx +++ b/src/containers/Settings/index.tsx @@ -1,6 +1,4 @@ import React, { useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import i18next from 'i18next' import { Header, Card, Row, Col, Switch, ButtonSelect, ButtonSelectOptions, Input, Icon } from '@components' import { containers } from '@stores' import { updateConfig } from '@lib/request' @@ -9,11 +7,7 @@ import { to } from '@lib/helper' import { isClashX, jsBridge } from '@lib/jsBridge' import './style.scss' -const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh' }, { label: 'English', value: 'en' }] - -function changeLanguage (language: string) { - i18next.changeLanguage(language) -} +const languageOptions: ButtonSelectOptions[] = [{ label: '中文', value: 'zh_CN' }, { label: 'English', value: 'en_US' }] async function handleStartAtLoginChange (state: boolean) { await jsBridge.setStartAtLogin(state) @@ -27,7 +21,8 @@ export default function Settings () { const { data: clashXData, fetch: fetchClashXData } = containers.useClashXData() const { data, fetch, unauthorized: { show } } = containers.useData() const { data: apiInfo } = containers.useAPIInfo() - const { t, i18n } = useTranslation(['Settings']) + const { useTranslation, setLang, lang } = containers.useI18n() + const { t } = useTranslation('Settings') const { value: info, change } = useObject({ socks5ProxyPort: 7891, httpProxyPort: 7890, @@ -53,6 +48,10 @@ export default function Settings () { } } + function changeLanguage (language: string) { + setLang(language) + } + async function handleHttpPortSave () { const [, err] = await to(updateConfig({ 'port': info.httpProxyPort })) if (!err) { @@ -111,7 +110,7 @@ export default function Settings () { {t('labels.language')} - + @@ -199,7 +198,7 @@ export default function Settings () { -

{t('versionString', { version: 'unknown' })}

+

{t('versionString')}

{t('checkUpdate')} diff --git a/src/containers/Sidebar/index.tsx b/src/containers/Sidebar/index.tsx index 30085d7..20ed561 100644 --- a/src/containers/Sidebar/index.tsx +++ b/src/containers/Sidebar/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { NavLink } from 'react-router-dom' -import { useTranslation } from 'react-i18next' import classnames from 'classnames' +import { containers } from '@stores' import './style.scss' const logo = require('@assets/logo.png') @@ -17,7 +17,8 @@ interface SidebarProps { export default function Sidebar (props: SidebarProps) { const { routes } = props - const { t } = useTranslation(['SideBar']) + const { useTranslation } = containers.useI18n() + const { t } = useTranslation('SideBar') const navlinks = routes.map( ({ path, name, exact, noMobile }) => ( diff --git a/src/i18n/index.ts b/src/i18n/index.ts index d9647f2..03fcc6c 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,29 +1,70 @@ -import i18n from 'i18next' -import LanguageDetector from 'i18next-browser-languagedetector' +import { useState, useCallback } from 'react' +import get from 'lodash/get' +import { getLocalStorageItem, setLocalStorageItem } from '@lib/helper' -// locales import en_US from './en_US' import zh_CN from './zh_CN' -const options = { - fallbackLng: 'en_US', - - ns: [ - 'SideBar', - 'Settings', - 'Logs' - ], - - resources: { - en: en_US, - zh: zh_CN - }, - - react: { - wait: true - } +const Language = { + en_US, + zh_CN } -i18n.use(LanguageDetector).init(options) +const languageKey = 'language' -export default i18n +const locales = Object.keys(Language) + +function getNavigatorLanguage (): string[] { + const found: string[] = [] + + if (window.navigator) { + if (window.navigator.languages) { + for (const lan of window.navigator.languages) { + found.push(lan) + } + } else if (window.navigator.language) { + found.push(navigator.language) + } + } + + return found +} + +function getLanguage () { + const localLanguage = getLocalStorageItem(languageKey) + if (localLanguage && locales.includes(localLanguage)) { + return localLanguage + } + + const navigatorLanguage = getNavigatorLanguage() + for (const language of navigatorLanguage) { + if (language.includes('zh')) { + return 'zh_CN' + } else if (language.includes('us')) { + return 'en_US' + } + } + + return 'en_US' +} + +export function useI18n () { + const [lang, set] = useState(getLanguage()) + + function setLang (lang: string) { + set(lang) + setLocalStorageItem(languageKey, lang) + } + + const useTranslation = useCallback( + function (namespace: string) { + function t (path: string) { + return get(Language[lang][namespace], path) as string + } + return { t } + }, + [lang] + ) + + return { lang, locales, setLang, useTranslation } +} diff --git a/src/models/I18n.ts b/src/models/I18n.ts deleted file mode 100644 index 4b00658..0000000 --- a/src/models/I18n.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface I18nProps { - t? ( - key: string, - variables?: { - [key: string]: any - } - ): string - - lng?: string -} diff --git a/src/models/index.ts b/src/models/index.ts index 4b515ef..9c60e9c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -2,5 +2,4 @@ export * from './BaseProps' export * from './Config' export * from './Proxy' export * from './Rule' -export * from './I18n' export * from './Cipher' diff --git a/src/render.tsx b/src/render.tsx index 543ce22..9731e6e 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -1,19 +1,15 @@ import * as React from 'react' import { render } from 'react-dom' import { HashRouter } from 'react-router-dom' -import { I18nextProvider } from 'react-i18next' import { Provider as Global } from '@stores' import App from '@containers/App' -import i18n from '@i18n' export default function renderApp () { const rootEl = document.getElementById('root') const AppInstance = ( - - - + ) diff --git a/src/stores/HookStore.ts b/src/stores/HookStore.ts index 3f8dfac..37898fb 100644 --- a/src/stores/HookStore.ts +++ b/src/stores/HookStore.ts @@ -4,6 +4,7 @@ import * as API from '@lib/request' import { useObject, composeContainer } from '@lib/hook' import { jsBridge } from '@lib/jsBridge' import { setLocalStorageItem, partition, to } from '@lib/helper' +import { useI18n } from '@i18n' function useData () { const { value: data, change } = useObject({ @@ -98,7 +99,8 @@ function useClashXData () { const { Provider, containers } = composeContainer({ useData, useAPIInfo, - useClashXData + useClashXData, + useI18n }) export { Provider, containers }