From 5579df6d5487bb2a45265788cb524e2136ab3fbd Mon Sep 17 00:00:00 2001 From: sweetcola <838689894@qq.com> Date: Mon, 29 Nov 2021 16:04:51 +0800 Subject: [PATCH] add: custom translate source Users can add custom sources to translate their selected text now. --- public/_locales/en/messages.json | 15 ++ public/_locales/ja/messages.json | 15 ++ public/_locales/zh_CN/messages.json | 15 ++ public/_locales/zh_TW/messages.json | 15 ++ .../MtAddSource/SourceSelector/index.tsx | 3 +- src/components/MtResult/index.tsx | 4 +- src/components/MtResult/style.css | 10 ++ src/components/SourceFavicon/index.tsx | 33 ++-- src/components/SourceFavicon/style.css | 9 + .../TsHistory/HistoryResultPanel/index.tsx | 6 +- src/components/TsVia/index.tsx | 3 +- src/constants/defaultOptions.ts | 3 +- src/constants/translateSource.ts | 1 + .../content/SingleTranslateResult/index.tsx | 4 +- .../CustomTranslateSourceDisplay/index.tsx | 160 ++++++++++++++++++ .../CustomTranslateSourceDisplay/style.css | 7 + .../options/MultipleSourcesDisplay/index.tsx | 27 +-- src/entry/options/Options/index.tsx | 4 +- .../sections/DefaultTranslateOptions.tsx | 47 ++++- src/entry/options/RegExpList/index.tsx | 35 ++-- src/entry/options/index.tsx | 16 +- .../popup/SingleTranslateResult/index.tsx | 4 +- src/public/request.ts | 8 +- src/public/switch-translate-source.ts | 5 +- src/public/translate/custom/check-result.ts | 61 +++++++ src/public/translate/custom/index.ts | 4 + src/public/translate/custom/translate.ts | 110 ++++++++++++ src/types/index.ts | 7 + 28 files changed, 553 insertions(+), 78 deletions(-) create mode 100644 src/entry/options/CustomTranslateSourceDisplay/index.tsx create mode 100644 src/entry/options/CustomTranslateSourceDisplay/style.css create mode 100644 src/public/translate/custom/check-result.ts create mode 100644 src/public/translate/custom/index.ts create mode 100644 src/public/translate/custom/translate.ts diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 0857e8d3..f8697385 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -368,6 +368,18 @@ "optionsMultipleTranslateSourceListDescription": { "message": "The source you check will be displayed in panel by default. The order of the sources is related to the order in which you checked." }, + "optionsCustomTranslateSource": { + "message": "Custom translate source" + }, + "optionsCustomTranslateSourceDescription": { + "message": "You can add custom translate sources which built by yourself or others." + }, + "optionsCustomTranslateSourceLearn": { + "message": "Learn more about custom translate source." + }, + "optionsURLCanNotBeEmpty": { + "message": "URL can not be empty." + }, "themePreset": { "message": "Preset" }, @@ -452,6 +464,9 @@ "wordRelated": { "message": "Related" }, + "wordName": { + "message": "Name" + }, "sentenceAddTranslateSource": { "message": "Please add a translate source." }, diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 60b48095..86567500 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -368,6 +368,18 @@ "optionsMultipleTranslateSourceListDescription": { "message": "チェックしたソースは、デフォルトでパネルに表示されます。ソースの順序は、チェックした順序に関連しています。" }, + "optionsCustomTranslateSource": { + "message": "カスタム翻訳ソース" + }, + "optionsCustomTranslateSourceDescription": { + "message": "自分や他の人が作成したカスタム翻訳ソースを追加できます。" + }, + "optionsCustomTranslateSourceLearn": { + "message": "カスタム翻訳ソースをもっと知る。" + }, + "optionsURLCanNotBeEmpty": { + "message": "URLを空にすることはできません。" + }, "themePreset": { "message": "プリセット" }, @@ -452,6 +464,9 @@ "wordRelated": { "message": "関連" }, + "wordName": { + "message": "名称" + }, "sentenceAddTranslateSource": { "message": "翻訳ソースを追加してください。" }, diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index f8170c40..b9de497b 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -368,6 +368,18 @@ "optionsMultipleTranslateSourceListDescription": { "message": "你勾选的源将默认被展示在面板中。源的顺序与你勾选的顺序相关。" }, + "optionsCustomTranslateSource": { + "message": "自定义翻译源" + }, + "optionsCustomTranslateSourceDescription": { + "message": "你可以添加自己或是其他人构造的自定义翻译源。" + }, + "optionsCustomTranslateSourceLearn": { + "message": "深入了解自定义翻译源。" + }, + "optionsURLCanNotBeEmpty": { + "message": "URL 不能为空。" + }, "themePreset": { "message": "预设" }, @@ -452,6 +464,9 @@ "wordRelated": { "message": "相关" }, + "wordName": { + "message": "名称" + }, "sentenceAddTranslateSource": { "message": "请添加翻译源。" }, diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 8e84dac2..00aeac01 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -368,6 +368,18 @@ "optionsMultipleTranslateSourceListDescription": { "message": "你勾選的源將默認被展示在面板中。源的順序與你勾選的順序相關。" }, + "optionsCustomTranslateSource": { + "message": "自定義翻譯源" + }, + "optionsCustomTranslateSourceDescription": { + "message": "你可以添加自己或是其他人構造的自定義翻譯源。" + }, + "optionsCustomTranslateSourceLearn": { + "message": "深入瞭解自定義翻譯源。" + }, + "optionsURLCanNotBeEmpty": { + "message": "URL 不能為空。" + }, "themePreset": { "message": "預設" }, @@ -452,6 +464,9 @@ "wordRelated": { "message": "相關" }, + "wordName": { + "message": "名稱" + }, "sentenceAddTranslateSource": { "message": "請添加翻譯源。" }, diff --git a/src/components/MtAddSource/SourceSelector/index.tsx b/src/components/MtAddSource/SourceSelector/index.tsx index 5e2f989b..b638abbe 100644 --- a/src/components/MtAddSource/SourceSelector/index.tsx +++ b/src/components/MtAddSource/SourceSelector/index.tsx @@ -4,6 +4,7 @@ import { TranslateSource, translateSource } from '../../../constants/translateSo import SourceFavicon from '../../SourceFavicon'; import './style.css'; import { Translation } from '../../../redux/slice/multipleTranslateSlice'; +import { getOptions } from '../../../public/options'; type SourceSelectorProps = { show: boolean; @@ -26,7 +27,7 @@ const SourceSelector: React.FC = ({ show, hideCallback, tra }, [addSource, hideCallback]); useEffect(() => { - setSourceList(translateSource.filter(v => translations.findIndex(v1 => v1.source === v.source) < 0)); + setSourceList(translateSource.concat(getOptions().customTranslateSourceList).filter(v => translations.findIndex(v1 => v1.source === v.source) < 0)); }, [translations]); return ( diff --git a/src/components/MtResult/index.tsx b/src/components/MtResult/index.tsx index 43bd3aae..76a07e4c 100644 --- a/src/components/MtResult/index.tsx +++ b/src/components/MtResult/index.tsx @@ -24,8 +24,8 @@ const MtResult: React.FC = ({ source, translateRequest, remove, r className='mt-result__head button flex-justify-content-space-between' onClick={() => setFold(!fold)} > - - + + {translateRequest.status === 'loading' && } {translateRequest.status === 'finished' && <> , 'className'>; -const SourceFavicon: React.FC = ({ source }) => { +const SourceFavicon: React.FC = ({ source, className }) => { return ( - <> - favicon + + {getFavicon(source)} {getName(source)} - + ); }; const getFavicon = (source: string) => { switch (source) { - case GOOGLE_COM: return google; - case BING_COM: return bing; - case MOJIDICT_COM: return mojidict; - case BAIDU_COM: return baidu; - case MICROSOFT_COM: return microsoft; - default: return; + case GOOGLE_COM: return FaviconImg(google); + case BING_COM: return FaviconImg(bing); + case MOJIDICT_COM: return FaviconImg(mojidict); + case BAIDU_COM: return FaviconImg(baidu); + case MICROSOFT_COM: return FaviconImg(microsoft); + default: return (
{(getOptions().customTranslateSourceList.find(v => v.source === source)?.name ?? source)[0]}
); } }; +const FaviconImg = (src: string) => (favicon); + const getName = (source: string) => { switch (source) { case GOOGLE_COM: return 'Google'; @@ -42,8 +41,8 @@ const getName = (source: string) => { case MOJIDICT_COM: return 'Mojidict'; case BAIDU_COM: return 'Baidu'; case MICROSOFT_COM: return 'Microsoft'; - default: return; + default: return getOptions().customTranslateSourceList.find(v => v.source === source)?.name ?? source; } -} +}; export default SourceFavicon; \ No newline at end of file diff --git a/src/components/SourceFavicon/style.css b/src/components/SourceFavicon/style.css index 26a1ff4e..50754174 100644 --- a/src/components/SourceFavicon/style.css +++ b/src/components/SourceFavicon/style.css @@ -6,4 +6,13 @@ padding: 0; width: 20px; height: 20px; +} +.favicon--mock { + justify-content: center; + text-align: center; + font-size: 14px; + border-radius: 5px; + color: #fff; + background: #999; + font-weight: bold; } \ No newline at end of file diff --git a/src/components/TsHistory/HistoryResultPanel/index.tsx b/src/components/TsHistory/HistoryResultPanel/index.tsx index 4ff15450..b36093ad 100644 --- a/src/components/TsHistory/HistoryResultPanel/index.tsx +++ b/src/components/TsHistory/HistoryResultPanel/index.tsx @@ -34,9 +34,9 @@ const HistoryResultPanel: React.FC = ({ translations, t ref={panelEle} > {translations.map(({ source, translateRequest }) => (
-
- - +
+ + {translateRequest.status === 'finished' && <> void; @@ -17,7 +18,7 @@ const TsVia: React.FC = ({ sourceChange, source, disableSourceChange via = ({ showRtAnd onChange={handleSelectionChange} from={from} to={to} - languageCodes={langCode[source]} + languageCodes={langCode[source] ?? googleLangCode} />
diff --git a/src/entry/options/CustomTranslateSourceDisplay/index.tsx b/src/entry/options/CustomTranslateSourceDisplay/index.tsx new file mode 100644 index 00000000..523c8a34 --- /dev/null +++ b/src/entry/options/CustomTranslateSourceDisplay/index.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import IconFont from '../../../components/IconFont'; +import defaultOptions from '../../../constants/defaultOptions'; +import { getMessage } from '../../../public/i18n'; +import { getOptions, initOptions } from '../../../public/options'; +import { checkResultFromCustomSource } from '../../../public/translate/custom/check-result'; +import { CustomTranslateSource } from '../../../types'; +import './style.css'; + +type CustomTranslateSourceDisplayProps = { + customTranslateSources: CustomTranslateSource[]; + onChange: (customTranslateSources: CustomTranslateSource[]) => void; +}; + +const CustomTranslateSourceDisplay: React.FC = ({ customTranslateSources, onChange }) => { + const [modifying, setModifying] = useState(false); + const [updated, setUpdated] = useState(false); + const [customSources, setCustomSources] = useState([]); + const [message, setMessage] = useState(''); + + const urlInputRef = useRef(null); + const nameInputRef = useRef(null); + + const testDataRef = useRef({ id: 0, url: '' }); + + useEffect(() => { + setCustomSources([...customTranslateSources]); + }, [customTranslateSources]); + + const onAddBtnClick = useCallback(() => { + if (!urlInputRef.current || !nameInputRef.current) { return; } + const url = urlInputRef.current.value ?? ''; + const name = nameInputRef.current.value.substring(0, 20) || 'Custom source'; + + if (testDataRef.current.url === url) { return; } + + try { + new URL(url); + + ++testDataRef.current.id; + + urlInputRef.current.disabled = true; + nameInputRef.current.disabled = true; + + setMessage(`URL: ${url} ${getMessage('wordRequesting')}`); + + const id = testDataRef.current.id; + + testCustomSource(url).then(() => { + if (!urlInputRef.current || !nameInputRef.current || testDataRef.current.id !== id) { return; } + + setCustomSources(customSources.concat({ + url, + name, + source: window.btoa(Number(new Date()).toString() + Math.floor(Math.random() * 10000).toString()) + })); + + setUpdated(true); + setMessage(''); + + urlInputRef.current.value = ''; + nameInputRef.current.value = ''; + }).catch((err) => { + if (testDataRef.current.id !== id) { return; } + + setMessage((err as Error).message); + }).finally(() => { + if (!urlInputRef.current || !nameInputRef.current || testDataRef.current.id !== id) { return; } + + urlInputRef.current.disabled = false; + nameInputRef.current.disabled = false; + + testDataRef.current.url = ''; + }); + } + catch (err) { + setMessage(`Error: ${(err as Error).message}`); + } + }, [customSources]); + + const onSaveBtnClick = useCallback(() => { + onChange(customSources); + initOptions({ ...defaultOptions, customTranslateSourceList: customSources }); + + setModifying(false); + setUpdated(false); + setMessage(''); + testDataRef.current = { id: testDataRef.current.id + 1, url: '' }; + }, [onChange, customSources]); + + const onCancelBtnClick = useCallback(() => { + setUpdated(false); + setModifying(false); + setMessage(''); + setCustomSources(customTranslateSources); + testDataRef.current = { id: testDataRef.current.id + 1, url: '' }; + }, [customTranslateSources]); + + return ( +
+
+
+
URL
+
{getMessage('wordName')}
+ +
+ {customSources.length > 0 ? customSources.map(({ url, name, source }, i) => (
+ + + {modifying &&
+ { + setCustomSources(customSources.filter((value, j) => (i !== j))); + setUpdated(true); + }} + /> +
} +
)) :
{getMessage('contentNoRecord')}
} + {modifying &&
+ + + +
} + {message &&
{message}
} + {modifying &&
+ + +
} +
+
+ ); +}; + +const testCustomSource = async (url: string) => { + const fetchJSON = { + text: 'test', + from: 'auto', + to: 'en', + userLang: navigator.language, + preferred: [getOptions().preferredLanguage, getOptions().secondPreferredLanguage] + }; + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(fetchJSON) + }).catch(() => { throw new Error('Error: Connection timed out.'); }); + + if (!res.ok) { throw new Error(`Error: Bad request(http code: ${res.status}).`); } + + const data = await res.json(); + + checkResultFromCustomSource(data); +}; + +export default CustomTranslateSourceDisplay; \ No newline at end of file diff --git a/src/entry/options/CustomTranslateSourceDisplay/style.css b/src/entry/options/CustomTranslateSourceDisplay/style.css new file mode 100644 index 00000000..db0f5b64 --- /dev/null +++ b/src/entry/options/CustomTranslateSourceDisplay/style.css @@ -0,0 +1,7 @@ +.custom-translate-source__item { + display: grid; + grid-template-columns: 45% 35% auto; + grid-column-gap: 1%; + align-items: center; + padding: 3px; +} \ No newline at end of file diff --git a/src/entry/options/MultipleSourcesDisplay/index.tsx b/src/entry/options/MultipleSourcesDisplay/index.tsx index 30882368..b4b96e48 100644 --- a/src/entry/options/MultipleSourcesDisplay/index.tsx +++ b/src/entry/options/MultipleSourcesDisplay/index.tsx @@ -1,34 +1,35 @@ import React, { useCallback, useMemo } from 'react'; import IconFont from '../../../components/IconFont'; import SourceFavicon from '../../../components/SourceFavicon'; -import { translateSource } from '../../../constants/translateSource'; +import { TranslateSource } from '../../../constants/translateSource'; import { getMessage } from '../../../public/i18n'; import './style.css'; type MultipleSourcesDisplayProps = { - sources: string[]; + enabledSources: string[]; + sources: TranslateSource[]; onChange: (sources: string[]) => void; }; -const MultipleSourcesDisplay: React.FC = ({ sources, onChange }) => { - const sourcesEnabled = useMemo(() => { - return sources.reduce((total, current) => ({ ...total, [current]: true }), {}); - }, [sources]); +const MultipleSourcesDisplay: React.FC = ({ enabledSources, sources, onChange }) => { + const enabledSourcesMap = useMemo(() => { + return enabledSources.reduce((total, current) => ({ ...total, [current]: true }), {}); + }, [enabledSources]); const onSourceItemClick = useCallback((source) => { - if (sources.includes(source)) { - onChange(sources.filter(value => value !== source)); + if (enabledSources.includes(source)) { + onChange(enabledSources.filter(value => value !== source)); } else { - onChange(sources.concat(source)); + onChange(enabledSources.concat(source)); } - }, [sources, onChange]); + }, [enabledSources, onChange]); return (
- {translateSource.map(({ source }) => (
- onSourceItemClick(source)} /> + {sources.map(({ source }) => (
+ onSourceItemClick(source)} /> @@ -37,7 +38,7 @@ const MultipleSourcesDisplay: React.FC = ({ sources
{getMessage('optionsPreview')}
- {sources.length > 0 ? sources.map((source, index) => (
+ {enabledSources.length > 0 ? enabledSources.map((source, index) => (
{index !== 0 && }
)) :
--
} diff --git a/src/entry/options/Options/index.tsx b/src/entry/options/Options/index.tsx index 3ae4b695..66249ca8 100644 --- a/src/entry/options/Options/index.tsx +++ b/src/entry/options/Options/index.tsx @@ -81,7 +81,8 @@ const Options: React.FC = () => { webPageTranslateDirectly, noControlBarWhileFirstActivating, afterSelectingTextRegExpList, - translateButtonsTL + translateButtonsTL, + customTranslateSourceList } = useOptions(useOptionsDependency); const updateStorage = useCallback((key, value) => (setLocalStorage({[key]: value})), []); @@ -155,6 +156,7 @@ const Options: React.FC = () => { defaultTranslateFrom={defaultTranslateFrom} defaultTranslateTo={defaultTranslateTo} useDotCn={useDotCn} + customTranslateSourceList={customTranslateSourceList} />
{getMessage('optionsTextPreprocessing')}
>; const DefaultTranslateOptions: React.FC = ({ @@ -37,7 +40,8 @@ const DefaultTranslateOptions: React.FC = ({ defaultTranslateSource, defaultTranslateFrom, defaultTranslateTo, - useDotCn + useDotCn, + customTranslateSourceList }) => { return (
@@ -80,6 +84,32 @@ const DefaultTranslateOptions: React.FC = ({ />
{getMessage('optionsPreferredLanguageDescription')}
+
+ {getMessage('optionsCustomTranslateSource')} +
+ {getMessage('optionsCustomTranslateSourceDescription')} + + {getMessage('optionsCustomTranslateSourceLearn')} + +
+
+ { + // If user delete the using custom sources, remove them from options(multipleTranslateSourceList/defaultTranslateSource). + const availableSources = translateSource.concat(value).map(v => v.source); + updateStorage('multipleTranslateSourceList', multipleTranslateSourceList.filter(v => availableSources.includes(v))); + updateStorage('defaultTranslateSource', availableSources.includes(defaultTranslateSource) ? defaultTranslateSource : GOOGLE_COM); + + updateStorage('customTranslateSourceList', value) + }} + /> +
+
= ({
{getMessage('optionsMultipleTranslateSourceListDescription')}
updateStorage('multipleTranslateSourceList', value)} />
@@ -124,7 +155,7 @@ const DefaultTranslateOptions: React.FC = ({ {getMessage('optionsSource')} { const { source, from, to } = switchTranslateSource(value, { @@ -143,7 +174,7 @@ const DefaultTranslateOptions: React.FC = ({ message='optionsFrom' value={defaultTranslateFrom} onChange={value => updateStorage('defaultTranslateFrom', value)} - options={langCode[defaultTranslateSource][userLanguage]} + options={(langCode[defaultTranslateSource] ?? googleLangCode)[userLanguage]} optionValue='code' optionLabel='name' /> @@ -153,7 +184,7 @@ const DefaultTranslateOptions: React.FC = ({ message='optionsTo' value={defaultTranslateTo} onChange={value => updateStorage('defaultTranslateTo', value)} - options={langCode[defaultTranslateSource][userLanguage]} + options={(langCode[defaultTranslateSource] ?? googleLangCode)[userLanguage]} optionValue='code' optionLabel='name' /> diff --git a/src/entry/options/RegExpList/index.tsx b/src/entry/options/RegExpList/index.tsx index c10126ec..230ee8ae 100644 --- a/src/entry/options/RegExpList/index.tsx +++ b/src/entry/options/RegExpList/index.tsx @@ -38,9 +38,10 @@ const RegExpList: React.FC = ({ textPreprocessingRegExpList, on {getMessage('optionsPattern')} {getMessage('optionsFlags')} {getMessage('optionsReplacement')} +
- {regExpList.map((v, i) => (
+ {regExpList.length > 0 ? regExpList.map((v, i) => (
@@ -55,7 +56,7 @@ const RegExpList: React.FC = ({ textPreprocessingRegExpList, on /> } -
))} +
)) :
{getMessage('contentNoRecord')}
}
{modifyMode &&
@@ -100,22 +101,20 @@ const RegExpList: React.FC = ({ textPreprocessingRegExpList, on />
} {errorMessage &&
{errorMessage}
} -
- {modifyMode ? <> - - - : } -
+ {modifyMode &&
+ + +
}
); }; diff --git a/src/entry/options/index.tsx b/src/entry/options/index.tsx index e8ddde79..e4310263 100644 --- a/src/entry/options/index.tsx +++ b/src/entry/options/index.tsx @@ -5,13 +5,21 @@ import './style.css'; import '../../styles/global.css'; import { getI18nMessage } from '../../public/chrome-call'; import { appendColorVarsStyle } from '../../public/inject-style'; +import { getLocalStorageAsync } from '../../public/utils'; +import { DefaultOptions } from '../../types'; +import defaultOptions from '../../constants/defaultOptions'; +import { initOptions } from '../../public/options'; appendColorVarsStyle(document.head); document.documentElement.id = 'sc-translator-root'; document.title = `${getI18nMessage('optionsTitle')} - ${getI18nMessage('extName')}`; -ReactDOM.render( - , - document.getElementById('root') -); \ No newline at end of file +getLocalStorageAsync(Object.keys(defaultOptions) as (keyof DefaultOptions)[]).then((options) => { + initOptions(options); + + ReactDOM.render( + , + document.getElementById('root') + ); +}) \ No newline at end of file diff --git a/src/entry/popup/SingleTranslateResult/index.tsx b/src/entry/popup/SingleTranslateResult/index.tsx index 66ec8bc7..a9e54ef3 100644 --- a/src/entry/popup/SingleTranslateResult/index.tsx +++ b/src/entry/popup/SingleTranslateResult/index.tsx @@ -3,7 +3,7 @@ import LanguageSelection from '../../../components/LanguageSelection'; import { sendTranslate } from '../../../public/send'; import RawText from '../../../components/RawText'; import TsResult from '../../../components/TsResult'; -import { langCode } from '../../../constants/langCode'; +import { googleLangCode, langCode } from '../../../constants/langCode'; import TsVia from '../../../components/TsVia'; import { switchTranslateSource } from '../../../public/switch-translate-source'; import { getOptions } from '../../../public/options'; @@ -88,7 +88,7 @@ const SingleTranslateResult: React.FC = ({ autoTrans onChange={handleSelectionChange} from={from} to={to} - languageCodes={langCode[source]} + languageCodes={langCode[source] ?? googleLangCode} />
diff --git a/src/public/request.ts b/src/public/request.ts index 644510c6..0db386a3 100644 --- a/src/public/request.ts +++ b/src/public/request.ts @@ -8,10 +8,9 @@ import google from '../public/translate/google'; import bing from '../public/translate/bing'; import mojidict from '../public/translate/mojidict'; import baidu from '../public/translate/baidu'; +import custom from '../public/translate/custom'; import { bingSwitchLangCode, baiduSwitchLangCode } from '../public/switch-lang-code'; -import { SOURCE_ERROR } from '../constants/errorCodes'; import { AudioCallback, DetectCallback, TranslateCallback } from './send'; -import { getError } from './translate/utils'; type TranslateRequestObject = { source: string; @@ -50,8 +49,9 @@ export const translate = ({ source, translateId, requestObj }: TranslateRequestO requestObj.secondPreferredLanguage = baiduSwitchLangCode(requestObj.secondPreferredLanguage); break; default: - const err = getError(SOURCE_ERROR); - cb?.({ suc: false, data: err, translateId }); + custom.translate(requestObj, source) + .then(result => cb({ suc: true, data: result, translateId })) + .catch(err => cb({ suc: false, data: err, translateId })); return; } diff --git a/src/public/switch-translate-source.ts b/src/public/switch-translate-source.ts index 1efeb792..c9668af7 100644 --- a/src/public/switch-translate-source.ts +++ b/src/public/switch-translate-source.ts @@ -22,6 +22,9 @@ export const switchTranslateSource = (targetSource: string, { source, from, to } from = baiduSwitchLangCode(from) in baiduLangCode ? from : ''; to = baiduSwitchLangCode(to) in baiduLangCode ? to : ''; return { source: targetSource, from, to }; - default: return { source, from, to }; + default: + from = from in googleLangCode ? from : ''; + to = to in googleLangCode ? to : ''; + return { source: targetSource, from, to }; } }; \ No newline at end of file diff --git a/src/public/translate/custom/check-result.ts b/src/public/translate/custom/check-result.ts new file mode 100644 index 00000000..fc753072 --- /dev/null +++ b/src/public/translate/custom/check-result.ts @@ -0,0 +1,61 @@ +export const checkResultFromCustomSource = (result: any) => { + // required key "result", "from", "to" + if (!('from' in result) || !('to' in result) || !('result' in result)) { + const errorMessage = `Error: ` + + `${!('result' in result) ? '"result"' : ''} ` + + `${!('from' in result) ? '"from"' : ''} ` + + `${!('to' in result) ? '"to"' : ''} is/are required in response data.`; + + throw new Error(errorMessage); + } + + // check "result" + if (!Array.isArray(result.result)) { + throw new Error('Error: "result" is not array.'); + } + else if (!isAllStringInArray(result.result)) { + throw new Error('Error: "result" must be an array of strings.'); + } + + // check "from" + if (typeof result.from !== 'string') { + throw new Error('Error: "from" is not string.'); + } + + // check "to" + if (typeof result.to !== 'string') { + throw new Error('Error: "to" is not string.'); + } + + // check "dict" + if ('dict' in result) { + if (!Array.isArray(result.dict)) { + throw new Error('Error: "dict" is not array.'); + } + else if (!isAllStringInArray(result.dict)) { + throw new Error('Error: "dict" must be an array of strings.'); + } + } + + // check "related" + if ('related' in result) { + if (!Array.isArray(result.related)) { + throw new Error('Error: "related" is not an array.'); + } + else if (!isAllStringInArray(result.related)) { + throw new Error('Error: "related" must be an array of strings.'); + } + } + + // check "phonetic" + if ('phonetic' in result && typeof result.phonetic !== 'string') { + throw new Error('Error: "phonetic" is not string.'); + } +}; + +const isAllStringInArray = (array: any[]) => { + for (let i = 0; i < array.length; i++) { + if (typeof array[i] !== 'string') { return false; } + } + return true; +}; \ No newline at end of file diff --git a/src/public/translate/custom/index.ts b/src/public/translate/custom/index.ts new file mode 100644 index 00000000..c2d30201 --- /dev/null +++ b/src/public/translate/custom/index.ts @@ -0,0 +1,4 @@ +import { translate } from './translate'; + +const custom = { translate }; +export default custom; \ No newline at end of file diff --git a/src/public/translate/custom/translate.ts b/src/public/translate/custom/translate.ts new file mode 100644 index 00000000..b57f3e10 --- /dev/null +++ b/src/public/translate/custom/translate.ts @@ -0,0 +1,110 @@ +import { SOURCE_ERROR } from '../../../constants/errorCodes'; +import { DefaultOptions, TranslateResult } from '../../../types'; +import { getLocalStorageAsync } from '../../utils'; +import { TranslateParams } from '../translate-types'; +import { fetchData, getError } from '../utils'; +import { langCode } from '../google/lang-code'; +import { LANGUAGE_NOT_SOPPORTED, RESULT_ERROR } from '../error-codes'; +import { checkResultFromCustomSource } from './check-result'; + +type PickedOptions = Pick; +const keys: (keyof PickedOptions)[] = ['customTranslateSourceList']; + +type FetchCustomSourceJSON = { + text: string; + from: string; + to: string; + userLang: string; + preferred: [string, string]; +}; + +// type DataFromCustomSource = { +// result: string[]; +// from: string; +// to: string; +// dict?: string[]; +// phonetic?: string; +// related?: string[]; +// } | { +// code: string; +// }; + +export const translate = async ({ text, from = '', to = '', preferredLanguage = '', secondPreferredLanguage = '' }: TranslateParams, source: string) => { + const { customTranslateSourceList } = await getLocalStorageAsync(keys); + const customTranslateSource = customTranslateSourceList.find(value => value.source === source); + + if (!customTranslateSource) { throw getError(SOURCE_ERROR); } + + preferredLanguage = preferredLanguage || 'en'; + secondPreferredLanguage = secondPreferredLanguage || 'en'; + + const originTo = to; + const originFrom = from; + + from = from || 'auto'; + to = to || (from === preferredLanguage ? secondPreferredLanguage : preferredLanguage); + + if (!(from in langCode) || !(to in langCode)) { throw getError(LANGUAGE_NOT_SOPPORTED); } + + let fetchJSON: FetchCustomSourceJSON = { + text, + from, + to, + userLang: navigator.language, + preferred: [preferredLanguage, secondPreferredLanguage] + }; + + const res = await fetchData(customTranslateSource.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(fetchJSON) + }); + + try { + let data = await res.json(); + + if (data.code) { throw getError(data.code); } + + checkResultFromCustomSource(data); + + if (!originFrom && !originTo && data.from === to && data.to === to && preferredLanguage !== secondPreferredLanguage) { + to = secondPreferredLanguage; + + const newRes = await fetchData(customTranslateSource.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ ...fetchJSON, from: data.from, to }) + }); + + data = await newRes.json(); + + if (data.code) { throw getError(data.code); } + + checkResultFromCustomSource(data); + } + + const result: TranslateResult = { + text, + from: data.from, + to, + result: data.result, + dict: data.dict, + phonetic: data.phonetic, + related: data.related + }; + + return result; + } + catch (err) { + if ((err as ReturnType).code) { + throw err; + } + else { + throw getError(RESULT_ERROR); + } + } +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 5a46b465..2687a56a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -55,6 +55,12 @@ export type TranslateButtonsTL = { third: string; }; +export type CustomTranslateSource = { + name: string; + url: string; + source: string; +}; + export type DefaultOptions = { userLanguage: string; defaultTranslateSource: string; @@ -115,6 +121,7 @@ export type DefaultOptions = { afterSelectingTextRegExpList: TextPreprocessingRegExp[]; translateButtonsTL: TranslateButtonsTL; sourceParamsCache: SourceParams; + customTranslateSourceList: CustomTranslateSource[]; }; // Only work in "src/entry/background/install.ts".