diff --git a/build.mjs b/build.mjs index 06611a4..ea4645c 100644 --- a/build.mjs +++ b/build.mjs @@ -38,7 +38,10 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) { '@primer/octicons-react', 'react-bootstrap-icons', 'countries-list', + 'i18next', + 'react-i18next', './src/utils', + './src/_locales/i18n', ], }, output: { diff --git a/package-lock.json b/package-lock.json index 36f72c3..77bcd0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "file-saver": "^2.0.5", "github-markdown-css": "^5.2.0", "gpt-3-encoder": "^1.1.4", + "i18next": "^22.4.13", "katex": "^0.16.4", "lodash-es": "^4.17.21", "parse5": "^6.0.1", @@ -24,6 +25,7 @@ "react-bootstrap-icons": "^1.10.2", "react-dom": "npm:@preact/compat@^17.1.2", "react-draggable": "^4.4.5", + "react-i18next": "^12.2.0", "react-markdown": "^8.0.5", "react-tabs": "^4.2.1", "rehype-highlight": "^6.0.0", @@ -1666,7 +1668,6 @@ "version": "7.21.0", "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.21.0.tgz", "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -4766,6 +4767,14 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-void-elements": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", @@ -4798,6 +4807,14 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "22.4.13", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-22.4.13.tgz", + "integrity": "sha512-GX7flMHRRqQA0I1yGLmaZ4Hwt1JfLqagk8QPDPZsqekbKtXsuIngSVWM/s3SLgNkrEXjA+0sMGNuOEkkmyqmWg==", + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -7576,6 +7593,27 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "12.2.0", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-12.2.0.tgz", + "integrity": "sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", @@ -7701,8 +7739,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -8900,6 +8937,14 @@ "unist-util-stringify-position": "^3.0.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 41d8248..1c7b54c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "file-saver": "^2.0.5", "github-markdown-css": "^5.2.0", "gpt-3-encoder": "^1.1.4", + "i18next": "^22.4.13", "katex": "^0.16.4", "lodash-es": "^4.17.21", "parse5": "^6.0.1", @@ -37,6 +38,7 @@ "react-bootstrap-icons": "^1.10.2", "react-dom": "npm:@preact/compat@^17.1.2", "react-draggable": "^4.4.5", + "react-i18next": "^12.2.0", "react-markdown": "^8.0.5", "react-tabs": "^4.2.1", "rehype-highlight": "^6.0.0", diff --git a/src/_locales/en/main.json b/src/_locales/en/main.json new file mode 100644 index 0000000..529241e --- /dev/null +++ b/src/_locales/en/main.json @@ -0,0 +1,87 @@ +{ + "General": "General", + "Selection Tools": "Selection Tools", + "Sites": "Sites", + "Advanced": "Advanced", + "Donate": "Donate", + "Triggers": "Triggers", + "Theme": "Theme", + "API Mode": "API Mode", + "Get": "Get", + "Balance": "Balance", + "Preferred Language": "Preferred Language", + "Insert ChatGPT at the top of search results": "Insert ChatGPT at the top of search results", + "Lock scrollbar while answering": "Lock scrollbar while answering", + "Current Version": "Current Version", + "Latest": "Latest", + "Help | Changelog ": "Help | Changelog ", + "Custom ChatGPT Web API Url": "Custom ChatGPT Web API Url", + "Custom ChatGPT Web API Path": "Custom ChatGPT Web API Path", + "Custom OpenAI API Url": "Custom OpenAI API Url", + "Custom Site Regex:": "Custom Site Regex:", + "Exclusively use Custom Site Regex for website matching,": "Exclusively use Custom Site Regex for website matching,", + "ignoring built-in rules": "ignoring built-in rules", + "Input Query:": "Input Query:", + "Append Query:": "Append Query:", + "Prepend Query:": "Prepend Query:", + "Wechat Pay": "Wechat Pay", + "Type your question here\nEnter to send, shift + enter to break line": "Type your question here\nEnter to send, shift + enter to break line", + "Wait for the answer to finish and then continue here": "Wait for the answer to finish and then continue here", + "Ask ChatGPT": "Ask ChatGPT", + "No Input Found": "No Input Found", + "You:": "You:", + "Collapse": "Collapse", + "Expand": "Expand", + "Stop": "Stop", + "Continue on official website": "Continue on official website", + "Error:": "Error:", + "Copy": "Copy", + "Question:": "Question:", + "Answer:": "Answer:", + "Waiting for response...": "Waiting for response...", + "Close the Window": "Close the Window", + "Pin the Window": "Pin the Window", + "Float the Window": "Float the Window", + "Save Conversation": "Save Conversation", + "UNAUTHORIZED": "UNAUTHORIZED", + "Please login at https://chat.openai.com first": "Please login at https://chat.openai.com first", + "Then open https://chat.openai.com/api/auth/session": "Then open https://chat.openai.com/api/auth/session", + "And refresh this page or type you question again": "And refresh this page or type you question again", + "Consider creating an api key at https://platform.openai.com/account/api-keys": "Consider creating an api key at https://platform.openai.com/account/api-keys", + "OpenAI Security Check Required": "OpenAI Security Check Required", + "Please open https://chat.openai.com/api/auth/session": "Please open https://chat.openai.com/api/auth/session", + "Please open https://chat.openai.com": "Please open https://chat.openai.com", + "New Chat": "New Chat", + "Summarize Page": "Summarize Page", + "Translate": "Translate", + "Translate (Bidirectional)": "Translate (Bidirectional)", + "Summary": "Summary", + "Polish": "Polish", + "Sentiment Analysis": "Sentiment Analysis", + "Divide Paragraphs": "Divide Paragraphs", + "Code Explain": "Code Explain", + "Ask": "Ask", + "Always": "Always", + "Manually": "Manually", + "When query ends with question mark (?)": "When query ends with question mark (?)", + "Light": "Light", + "Dark": "Dark", + "Auto": "Auto", + "ChatGPT (Web)": "ChatGPT (Web)", + "ChatGPT (Web, GPT-4)": "ChatGPT (Web, GPT-4)", + "Bing (Web, GPT-4)": "Bing (Web, GPT-4)", + "ChatGPT (GPT-3.5-turbo)": "ChatGPT (GPT-3.5-turbo)", + "ChatGPT (GPT-4-8k)": "ChatGPT (GPT-4-8k)", + "ChatGPT (GPT-4-32k)": "ChatGPT (GPT-4-32k)", + "GPT-3.5": "GPT-3.5", + "Custom Model": "Custom Model", + "Balanced": "Balanced", + "Creative": "Creative", + "Precise": "Precise", + "Fast": "Fast", + "API Key": "API Key", + "Model Name": "Model Name", + "Custom Model API Url": "Custom Model API Url", + "Loading...": "Loading...", + "Feedback": "Feedback" +} diff --git a/src/_locales/i18n.mjs b/src/_locales/i18n.mjs new file mode 100644 index 0000000..871f918 --- /dev/null +++ b/src/_locales/i18n.mjs @@ -0,0 +1,11 @@ +import i18n from 'i18next' +import { initReactI18next } from 'react-i18next' +import { resources } from './resources' + +i18n.use(initReactI18next).init({ + resources, + fallbackLng: 'en', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, +}) diff --git a/src/_locales/resources.mjs b/src/_locales/resources.mjs new file mode 100644 index 0000000..52dc3eb --- /dev/null +++ b/src/_locales/resources.mjs @@ -0,0 +1,15 @@ +import en from './en/main.json' +import zhHans from './zh-hans/main.json' +import zhHant from './zh-hant/main.json' + +export const resources = { + en: { + translation: en, + }, + zh: { + translation: zhHans, + }, + zhHant: { + translation: zhHant, + }, +} diff --git a/src/_locales/zh-hans/main.json b/src/_locales/zh-hans/main.json new file mode 100644 index 0000000..d28cc7b --- /dev/null +++ b/src/_locales/zh-hans/main.json @@ -0,0 +1,87 @@ +{ + "General": "常规", + "Selection Tools": "选择浮动工具", + "Sites": "站点适配", + "Advanced": "高级", + "Donate": "打赏", + "Triggers": "触发方式", + "Theme": "主题", + "API Mode": "API模式", + "Get": "获取", + "Balance": "余额", + "Preferred Language": "语言偏好", + "Insert ChatGPT at the top of search results": "将对话卡片插入到搜索结果顶部", + "Lock scrollbar while answering": "回答时锁定滚动条", + "Current Version": "当前版本", + "Latest": "最新", + "Help | Changelog ": "帮助 | 更新日志 ", + "Custom ChatGPT Web API Url": "自定义的ChatGPT网页API地址", + "Custom ChatGPT Web API Path": "自定义的ChatGPT网页API路径", + "Custom OpenAI API Url": "自定义的OpenAI API地址", + "Custom Site Regex:": "自定义站点正则匹配:", + "Exclusively use Custom Site Regex for website matching,": "只使用自定义站点正则匹配,", + "ignoring built-in rules": "忽略内置站点规则", + "Input Query:": "输入的查询选择器:", + "Append Query:": "挂载到末尾的查询选择器:", + "Prepend Query:": "插入到开头的查询选择器:", + "Wechat Pay": "微信打赏", + "Type your question here\nEnter to send, shift + enter to break line": "在此输入你的问题\n回车 发送, shift+回车 换行", + "Wait for the answer to finish and then continue here": "等待回答完成, 然后在此继续", + "Ask ChatGPT": "询问ChatGPT", + "No Input Found": "无输入", + "You:": "你:", + "Collapse": "折叠", + "Expand": "展开", + "Stop": "停止", + "Continue on official website": "在官网继续", + "Error:": "错误:", + "Copy": "复制", + "Question:": "问题:", + "Answer:": "回答:", + "Waiting for response...": "等待响应...", + "Close the Window": "关闭窗口", + "Pin the Window": "固定窗口", + "Float the Window": "浮出/分裂窗口", + "Save Conversation": "保存对话", + "UNAUTHORIZED": "未授权", + "Please login at https://chat.openai.com first": "请先登录 https://chat.openai.com", + "Then open https://chat.openai.com/api/auth/session": "然后打开 https://chat.openai.com/api/auth/session", + "And refresh this page or type you question again": "之后刷新页面或重新输入你的问题", + "Consider creating an api key at https://platform.openai.com/account/api-keys": "考虑在 https://platform.openai.com/account/api-keys 创建一个API Key", + "OpenAI Security Check Required": "需要通过OpenAI的安全检查", + "Please open https://chat.openai.com/api/auth/session": "请打开 https://chat.openai.com/api/auth/session", + "Please open https://chat.openai.com": "请打开 https://chat.openai.com", + "New Chat": "新建聊天", + "Summarize Page": "总结本页", + "Translate": "翻译", + "Translate (Bidirectional)": "双向翻译", + "Summary": "总结", + "Polish": "润色", + "Sentiment Analysis": "情感分析", + "Divide Paragraphs": "段落划分", + "Code Explain": "代码解释", + "Ask": "询问", + "Always": "自动触发", + "Manually": "手动触发", + "When query ends with question mark (?)": "问题以问号结尾时触发", + "Light": "浅色", + "Dark": "深色", + "Auto": "自动", + "ChatGPT (Web)": "ChatGPT (网页版)", + "ChatGPT (Web, GPT-4)": "ChatGPT (网页版, GPT-4)", + "Bing (Web, GPT-4)": "Bing (网页版, GPT-4)", + "ChatGPT (GPT-3.5-turbo)": "ChatGPT (GPT-3.5-turbo)", + "ChatGPT (GPT-4-8k)": "ChatGPT (GPT-4-8k)", + "ChatGPT (GPT-4-32k)": "ChatGPT (GPT-4-32k)", + "GPT-3.5": "GPT-3.5", + "Custom Model": "自定义模型", + "Balanced": "平衡", + "Creative": "有创造力", + "Precise": "精确", + "Fast": "快速", + "API Key": "API Key", + "Model Name": "模型名", + "Custom Model API Url": "自定义模型的API地址", + "Loading...": "正在读取...", + "Feedback": "反馈" +} diff --git a/src/_locales/zh-hant/main.json b/src/_locales/zh-hant/main.json new file mode 100644 index 0000000..658d997 --- /dev/null +++ b/src/_locales/zh-hant/main.json @@ -0,0 +1,87 @@ +{ + "General": "常規", + "Selection Tools": "選擇浮動工具", + "Sites": "站點適配", + "Advanced": "高級", + "Donate": "打賞", + "Triggers": "觸發方式", + "Theme": "主題", + "API Mode": "API模式", + "Get": "獲取", + "Balance": "余額", + "Preferred Language": "語言偏好", + "Insert ChatGPT at the top of search results": "將對話卡片插入到搜索結果頂部", + "Lock scrollbar while answering": "回答時鎖定滾動條", + "Current Version": "當前版本", + "Latest": "最新", + "Help | Changelog ": "幫助 | 更新日誌 ", + "Custom ChatGPT Web API Url": "自定義的ChatGPT網頁API地址", + "Custom ChatGPT Web API Path": "自定義的ChatGPT網頁API路徑", + "Custom OpenAI API Url": "自定義的OpenAI API地址", + "Custom Site Regex:": "自定義站點正則匹配:", + "Exclusively use Custom Site Regex for website matching,": "只使用自定義站點正則匹配,", + "ignoring built-in rules": "忽略內置站點規則", + "Input Query:": "輸入的查詢選擇器:", + "Append Query:": "掛載到末尾的查詢選擇器:", + "Prepend Query:": "插入到開頭的查詢選擇器:", + "Wechat Pay": "微信打賞", + "Type your question here\nEnter to send, shift + enter to break line": "在此輸入你的問題\n回車 發送, shift+回車 換行", + "Wait for the answer to finish and then continue here": "等待回答完成, 然後在此繼續", + "Ask ChatGPT": "詢問ChatGPT", + "No Input Found": "無輸入", + "You:": "你:", + "Collapse": "折疊", + "Expand": "展開", + "Stop": "停止", + "Continue on official website": "在官網繼續", + "Error:": "錯誤:", + "Copy": "復製", + "Question:": "問題:", + "Answer:": "回答:", + "Waiting for response...": "等待響應...", + "Close the Window": "關閉窗口", + "Pin the Window": "固定窗口", + "Float the Window": "浮出/分裂窗口", + "Save Conversation": "保存對話", + "UNAUTHORIZED": "未授權", + "Please login at https://chat.openai.com first": "請先登錄 https://chat.openai.com", + "Then open https://chat.openai.com/api/auth/session": "然後打開 https://chat.openai.com/api/auth/session", + "And refresh this page or type you question again": "之後刷新頁面或重新輸入你的問題", + "Consider creating an api key at https://platform.openai.com/account/api-keys": "考慮在 https://platform.openai.com/account/api-keys 創建一個API Key", + "OpenAI Security Check Required": "需要通過OpenAI的安全檢查", + "Please open https://chat.openai.com/api/auth/session": "請打開 https://chat.openai.com/api/auth/session", + "Please open https://chat.openai.com": "請打開 https://chat.openai.com", + "New Chat": "新建聊天", + "Summarize Page": "總結本頁", + "Translate": "翻譯", + "Translate (Bidirectional)": "雙向翻譯", + "Summary": "總結", + "Polish": "潤色", + "Sentiment Analysis": "情感分析", + "Divide Paragraphs": "段落劃分", + "Code Explain": "代碼解釋", + "Ask": "詢問", + "Always": "自動觸發", + "Manually": "手動觸發", + "When query ends with question mark (?)": "問題以問號結尾時觸發", + "Light": "淺色", + "Dark": "深色", + "Auto": "自動", + "ChatGPT (Web)": "ChatGPT (網頁版)", + "ChatGPT (Web, GPT-4)": "ChatGPT (網頁版, GPT-4)", + "Bing (Web, GPT-4)": "Bing (網頁版, GPT-4)", + "ChatGPT (GPT-3.5-turbo)": "ChatGPT (GPT-3.5-turbo)", + "ChatGPT (GPT-4-8k)": "ChatGPT (GPT-4-8k)", + "ChatGPT (GPT-4-32k)": "ChatGPT (GPT-4-32k)", + "GPT-3.5": "GPT-3.5", + "Custom Model": "自定義模型", + "Balanced": "平衡", + "Creative": "有創造力", + "Precise": "精確", + "Fast": "快速", + "API Key": "API Key", + "Model Name": "模型名", + "Custom Model API Url": "自定義模型的API地址", + "Loading...": "正在讀取...", + "Feedback": "反饋" +} diff --git a/src/background/index.mjs b/src/background/index.mjs index 758ee23..8a9e119 100644 --- a/src/background/index.mjs +++ b/src/background/index.mjs @@ -13,6 +13,7 @@ import { chatgptWebModelKeys, customApiModelKeys, defaultConfig, + getPreferredLanguageKey, getUserConfig, gptApiModelKeys, isUsingCustomModel, @@ -20,6 +21,8 @@ import { } from '../config/index.mjs' import { isSafari } from '../utils/is-safari' import { config as menuConfig } from '../content-script/menu-tools' +import { t, changeLanguage } from 'i18next' +import '../_locales/i18n' const KEY_ACCESS_TOKEN = 'accessToken' const cache = new ExpiryMap(10 * 1000) @@ -140,7 +143,10 @@ Browser.commands.onCommand.addListener(async (command) => { }) function refreshMenu() { - Browser.contextMenus.removeAll().then(() => { + Browser.contextMenus.removeAll().then(async () => { + await getPreferredLanguageKey().then((lang) => { + changeLanguage(lang) + }) const menuId = 'ChatGPTBox-Menu' Browser.contextMenus.create({ id: menuId, @@ -152,7 +158,7 @@ function refreshMenu() { Browser.contextMenus.create({ id: menuId + k, parentId: menuId, - title: v.label, + title: t(v.label), contexts: ['all'], }) } @@ -168,7 +174,7 @@ function refreshMenu() { Browser.contextMenus.create({ id: menuId + key, parentId: menuId, - title: desc, + title: t(desc), contexts: ['selection'], }) } diff --git a/src/components/ConversationCard/index.jsx b/src/components/ConversationCard/index.jsx index fd4b5a9..31ce6a2 100644 --- a/src/components/ConversationCard/index.jsx +++ b/src/components/ConversationCard/index.jsx @@ -11,6 +11,7 @@ import { render } from 'preact' import FloatingToolbar from '../FloatingToolbar' import { useClampWindowSize } from '../../hooks/use-clamp-window-size' import { defaultConfig, getUserConfig } from '../../config/index.mjs' +import { useTranslation } from 'react-i18next' const logo = Browser.runtime.getURL('logo.png') @@ -29,6 +30,7 @@ class ConversationItemData extends Object { } function ConversationCard(props) { + const { t } = useTranslation() const [isReady, setIsReady] = useState(!props.question) const [port, setPort] = useState(() => Browser.runtime.connect()) const [session, setSession] = useState(props.session) @@ -44,7 +46,7 @@ function ConversationCard(props) { return [ new ConversationItemData( 'answer', - '
Waiting for response...
', + `${t(`Waiting for response...`)}
`, ), ] else return [] @@ -131,20 +133,26 @@ function ConversationCard(props) { switch (msg.error) { case 'UNAUTHORIZED': UpdateAnswer( - `UNAUTHORIZEDWaiting for response...
', + `${t('Waiting for response...')}
`, ) setConversationItemData([...conversationItemData, newQuestion, newAnswer]) setIsReady(false) diff --git a/src/components/ConversationItem/index.jsx b/src/components/ConversationItem/index.jsx index 7bfd5bd..384f237 100644 --- a/src/components/ConversationItem/index.jsx +++ b/src/components/ConversationItem/index.jsx @@ -4,8 +4,10 @@ import { ChevronDownIcon, LinkExternalIcon, XCircleIcon } from '@primer/octicons import CopyButton from '../CopyButton' import PropTypes from 'prop-types' import MarkdownRender from '../MarkdownRender/markdown.jsx' +import { useTranslation } from 'react-i18next' export function ConversationItem({ type, content, session, done, port }) { + const { t } = useTranslation() const [collapsed, setCollapsed] = useState(false) switch (type) { @@ -13,15 +15,23 @@ export function ConversationItem({ type, content, session, done, port }) { return (You:
+{t('You:')}
- {session && session.aiName ? `${session.aiName}:` : 'Loading...'} + {session && session.aiName ? `${t(session.aiName)}:` : t('Loading...')}
Error:
+{t('Error:')}
-