feat: Internationalization User Interface Support. PR Welcome! (#89, #39)

This commit is contained in:
josc146
2023-03-29 14:17:16 +08:00
parent 3ce221ab21
commit 25ed2cc7c7
22 changed files with 541 additions and 78 deletions
+3
View File
@@ -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: {
+48 -3
View File
@@ -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",
+2
View File
@@ -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",
+87
View File
@@ -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"
}
+11
View File
@@ -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
},
})
+15
View File
@@ -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,
},
}
+87
View File
@@ -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": "反馈"
}
+87
View File
@@ -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": "反饋"
}
+9 -3
View File
@@ -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'],
})
}
+22 -14
View File
@@ -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',
'<p class="gpt-loading">Waiting for response...</p>',
`<p class="gpt-loading">${t(`Waiting for response...`)}</p>`,
),
]
else return []
@@ -131,20 +133,26 @@ function ConversationCard(props) {
switch (msg.error) {
case 'UNAUTHORIZED':
UpdateAnswer(
`UNAUTHORIZED<br>Please login at https://chat.openai.com first${
isSafari() ? '<br>Then open https://chat.openai.com/api/auth/session' : ''
}<br>And refresh this page or type you question again` +
`<br><br>Consider creating an api key at https://platform.openai.com/account/api-keys<hr>`,
`${t('UNAUTHORIZED')}<br>${t('Please login at https://chat.openai.com first')}${
isSafari() ? `<br>${t('Then open https://chat.openai.com/api/auth/session')}` : ''
}<br>${t('And refresh this page or type you question again')}` +
`<br><br>${t(
'Consider creating an api key at https://platform.openai.com/account/api-keys',
)}<hr>`,
false,
'error',
)
break
case 'CLOUDFLARE':
UpdateAnswer(
`OpenAI Security Check Required<br>Please open ${
isSafari() ? 'https://chat.openai.com/api/auth/session' : 'https://chat.openai.com'
}<br>And refresh this page or type you question again` +
`<br><br>Consider creating an api key at https://platform.openai.com/account/api-keys<hr>`,
`${t('OpenAI Security Check Required')}<br>${
isSafari()
? t('Please open https://chat.openai.com/api/auth/session')
: t('Please open https://chat.openai.com')
}<br>${t('And refresh this page or type you question again')}` +
`<br><br>${t(
'Consider creating an api key at https://platform.openai.com/account/api-keys',
)}<hr>`,
false,
'error',
)
@@ -172,7 +180,7 @@ function ConversationCard(props) {
<XLg
className="gpt-util-icon"
style="margin:5px 15px 0px;"
title="Close the Window"
title={t('Close the Window')}
size={16}
onClick={() => {
if (props.onClose) props.onClose()
@@ -182,7 +190,7 @@ function ConversationCard(props) {
<Pin
className="gpt-util-icon"
style="margin:5px 15px 0px;"
title="Pin the Window"
title={t('Pin the Window')}
size={16}
onClick={() => {
if (props.onDock) props.onDock()
@@ -196,7 +204,7 @@ function ConversationCard(props) {
) : (
<WindowDesktop
className="gpt-util-icon"
title="Float the Window"
title={t('Float the Window')}
size={16}
onClick={() => {
const position = { x: window.innerWidth / 2 - 300, y: window.innerHeight / 2 - 200 }
@@ -216,7 +224,7 @@ function ConversationCard(props) {
/>
)}
<span
title="Save Conversation"
title={t('Save Conversation')}
className="gpt-util-icon"
style="margin:15px;"
onClick={() => {
@@ -254,7 +262,7 @@ function ConversationCard(props) {
const newQuestion = new ConversationItemData('question', question + '\n<hr/>')
const newAnswer = new ConversationItemData(
'answer',
'<p class="gpt-loading">Waiting for response...</p>',
`<p class="gpt-loading">${t('Waiting for response...')}</p>`,
)
setConversationItemData([...conversationItemData, newQuestion, newAnswer])
setIsReady(false)
+37 -11
View File
@@ -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 (
<div className={type} dir="auto">
<div className="gpt-header">
<p>You:</p>
<p>{t('You:')}</p>
<div style="display: flex; gap: 15px;">
<CopyButton contentFn={() => content} size={14} />
{!collapsed ? (
<span title="Collapse" className="gpt-util-icon" onClick={() => setCollapsed(true)}>
<span
title={t('Collapse')}
className="gpt-util-icon"
onClick={() => setCollapsed(true)}
>
<XCircleIcon size={14} />
</span>
) : (
<span title="Expand" className="gpt-util-icon" onClick={() => setCollapsed(false)}>
<span
title={t('Expand')}
className="gpt-util-icon"
onClick={() => setCollapsed(false)}
>
<ChevronDownIcon size={14} />
</span>
)}
@@ -35,7 +45,7 @@ export function ConversationItem({ type, content, session, done, port }) {
<div className={type} dir="auto">
<div className="gpt-header">
<p style="white-space: nowrap;">
{session && session.aiName ? `${session.aiName}:` : 'Loading...'}
{session && session.aiName ? `${t(session.aiName)}:` : t('Loading...')}
</p>
<div style="display: flex; gap: 15px; align-items: center; white-space: nowrap;">
{!done && (
@@ -46,7 +56,7 @@ export function ConversationItem({ type, content, session, done, port }) {
port.postMessage({ stop: true })
}}
>
Stop
{t('Stop')}
</button>
)}
{done && session && session.conversationId && (
@@ -57,7 +67,7 @@ export function ConversationItem({ type, content, session, done, port }) {
)}
{session && session.conversationId && (
<a
title="Continue on official website"
title={t('Continue on official website')}
href={'https://chat.openai.com/chat/' + session.conversationId}
target="_blank"
rel="nofollow noopener noreferrer"
@@ -69,11 +79,19 @@ export function ConversationItem({ type, content, session, done, port }) {
)}
{session && <CopyButton contentFn={() => content} size={14} />}
{!collapsed ? (
<span title="Collapse" className="gpt-util-icon" onClick={() => setCollapsed(true)}>
<span
title={t('Collapse')}
className="gpt-util-icon"
onClick={() => setCollapsed(true)}
>
<XCircleIcon size={14} />
</span>
) : (
<span title="Expand" className="gpt-util-icon" onClick={() => setCollapsed(false)}>
<span
title={t('Expand')}
className="gpt-util-icon"
onClick={() => setCollapsed(false)}
>
<ChevronDownIcon size={14} />
</span>
)}
@@ -86,15 +104,23 @@ export function ConversationItem({ type, content, session, done, port }) {
return (
<div className={type} dir="auto">
<div className="gpt-header">
<p>Error:</p>
<p>{t('Error:')}</p>
<div style="display: flex; gap: 15px;">
<CopyButton contentFn={() => content} size={14} />
{!collapsed ? (
<span title="Collapse" className="gpt-util-icon" onClick={() => setCollapsed(true)}>
<span
title={t('Collapse')}
className="gpt-util-icon"
onClick={() => setCollapsed(true)}
>
<XCircleIcon size={14} />
</span>
) : (
<span title="Expand" className="gpt-util-icon" onClick={() => setCollapsed(false)}>
<span
title={t('Expand')}
className="gpt-util-icon"
onClick={() => setCollapsed(false)}
>
<ChevronDownIcon size={14} />
</span>
)}
+7 -1
View File
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { CheckIcon, CopyIcon } from '@primer/octicons-react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
CopyButton.propTypes = {
contentFn: PropTypes.func.isRequired,
@@ -9,6 +10,7 @@ CopyButton.propTypes = {
}
function CopyButton({ className, contentFn, size }) {
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const onClick = () => {
@@ -23,7 +25,11 @@ function CopyButton({ className, contentFn, size }) {
}
return (
<span title="Copy" className={`gpt-util-icon ${className ? className : ''}`} onClick={onClick}>
<span
title={t('Copy')}
className={`gpt-util-icon ${className ? className : ''}`}
onClick={onClick}
>
{copied ? <CheckIcon size={size} /> : <CopyIcon size={size} />}
</span>
)
+5 -3
View File
@@ -5,8 +5,10 @@ import ConversationCard from '../ConversationCard'
import { defaultConfig, getUserConfig } from '../../config/index.mjs'
import Browser from 'webextension-polyfill'
import { getPossibleElementByQuerySelector, endsWithQuestionMark } from '../../utils'
import { useTranslation } from 'react-i18next'
function DecisionCard(props) {
const { t } = useTranslation()
const [triggered, setTriggered] = useState(false)
const [config, setConfig] = useState(defaultConfig)
const [render, setRender] = useState(false)
@@ -103,7 +105,7 @@ function DecisionCard(props) {
className="gpt-inner manual-btn icon-and-text"
onClick={() => setTriggered(true)}
>
<SearchIcon size="small" /> Ask ChatGPT
<SearchIcon size="small" /> {t('Ask ChatGPT')}
</p>
)
case 'questionMark':
@@ -118,14 +120,14 @@ function DecisionCard(props) {
className="gpt-inner manual-btn icon-and-text"
onClick={() => setTriggered(true)}
>
<SearchIcon size="small" /> Ask ChatGPT
<SearchIcon size="small" /> {t('Ask ChatGPT')}
</p>
)
}
else
return (
<p className="gpt-inner icon-and-text">
<LightBulbIcon size="small" /> No Input Found
<LightBulbIcon size="small" /> {t('No Input Found')}
</p>
)
})()}
@@ -2,8 +2,10 @@ import PropTypes from 'prop-types'
import { memo, useCallback, useState } from 'react'
import { ThumbsupIcon, ThumbsdownIcon } from '@primer/octicons-react'
import Browser from 'webextension-polyfill'
import { useTranslation } from 'react-i18next'
const FeedbackForChatGPTWeb = (props) => {
const { t } = useTranslation()
const [action, setAction] = useState(null)
const clickThumbsUp = useCallback(async () => {
@@ -39,7 +41,7 @@ const FeedbackForChatGPTWeb = (props) => {
}, [props, action])
return (
<div title="Feedback" className="gpt-feedback">
<div title={t('Feedback')} className="gpt-feedback">
<span
onClick={clickThumbsUp}
className={action === 'thumbsUp' ? 'gpt-feedback-selected gpt-util-icon' : 'gpt-util-icon'}
+3 -1
View File
@@ -7,10 +7,12 @@ import { config as toolsConfig } from '../../content-script/selection-tools'
import { getClientPosition, isMobile, setElementPositionInViewport } from '../../utils'
import Draggable from 'react-draggable'
import { useClampWindowSize } from '../../hooks/use-clamp-window-size'
import { useTranslation } from 'react-i18next'
const logo = Browser.runtime.getURL('logo.png')
function FloatingToolbar(props) {
const { t } = useTranslation()
const [selection, setSelection] = useState(props.selection)
const [prompt, setPrompt] = useState(props.prompt)
const [triggered, setTriggered] = useState(props.triggered)
@@ -127,7 +129,7 @@ function FloatingToolbar(props) {
cloneElement(toolConfig.icon, {
size: 20,
className: 'chatgptbox-selection-toolbar-button',
title: toolConfig.label,
title: t(toolConfig.label),
onClick: async () => {
const p = getClientPosition(props.container)
props.container.style.position = 'fixed'
+4 -2
View File
@@ -1,8 +1,10 @@
import { useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { updateRefHeight } from '../../utils'
import { useTranslation } from 'react-i18next'
export function InputBox({ onSubmit, enabled }) {
const { t } = useTranslation()
const [value, setValue] = useState('')
const inputRef = useRef(null)
@@ -28,8 +30,8 @@ export function InputBox({ onSubmit, enabled }) {
className="interact-input"
placeholder={
enabled
? 'Type your question here\nEnter to send, shift + enter to break line'
: 'Wait for the answer to finish and then continue here'
? t('Type your question here\nEnter to send, shift + enter to break line')
: t('Wait for the answer to finish and then continue here')
}
value={value}
onChange={(e) => setValue(e.target.value)}
+14 -2
View File
@@ -63,7 +63,7 @@ export const defaultConfig = {
apiKey: '',
/** @type {keyof ModelMode}*/
modelMode: 'balanced',
preferredLanguage: navigator.language.substring(0, 2),
preferredLanguage: getNavigatorLanguage(),
insertAtTop: isMobile(),
lockWhenAnswer: false,
customModelApiUrl: 'http://localhost:8000/chat/completions',
@@ -98,7 +98,7 @@ export const defaultConfig = {
// unchangeable
userLanguage: navigator.language.substring(0, 2),
userLanguage: getNavigatorLanguage(),
selectionTools: [
'translate',
'translateBidi',
@@ -132,6 +132,12 @@ export const defaultConfig = {
],
}
export function getNavigatorLanguage() {
const l = navigator.language.toLowerCase()
if (['zh-hk', 'zh-mo', 'zh-tw', 'zh-cht', 'zh-hant'].includes(l)) return 'zhHant'
return navigator.language.substring(0, 2)
}
export function isUsingApiKey(config) {
return (
gptApiModelKeys.includes(config.modelName) || chatgptApiModelKeys.includes(config.modelName)
@@ -146,6 +152,12 @@ export function isUsingCustomModel(config) {
return customApiModelKeys.includes(config.modelName)
}
export async function getPreferredLanguageKey() {
const config = await getUserConfig()
if (config.preferredLanguage === 'auto') return config.userLanguage
return config.preferredLanguage
}
/**
* get user config from local storage
* @returns {Promise<UserConfig>}
+5
View File
@@ -2,6 +2,11 @@ import { languages } from 'countries-list'
import { defaultConfig, getUserConfig } from './index.mjs'
export const languageList = { auto: { name: 'Auto', native: 'Auto' }, ...languages }
languageList.zh.name = 'Chinese (Simplified)'
languageList.zh.native = '简体中文'
languageList.zhHant = { ...languageList.zh }
languageList.zhHant.name = 'Chinese (Traditional)'
languageList.zhHant.native = '正體中文'
export async function getUserLanguage() {
return languageList[defaultConfig.userLanguage].name
+19 -1
View File
@@ -5,7 +5,12 @@ import DecisionCard from '../components/DecisionCard'
import { config as siteConfig } from './site-adapters'
import { config as toolsConfig } from './selection-tools'
import { config as menuConfig } from './menu-tools'
import { clearOldAccessToken, getUserConfig, setAccessToken } from '../config/index.mjs'
import {
clearOldAccessToken,
getPreferredLanguageKey,
getUserConfig,
setAccessToken,
} from '../config/index.mjs'
import {
createElementAtPosition,
cropText,
@@ -17,6 +22,8 @@ import {
import FloatingToolbar from '../components/FloatingToolbar'
import Browser from 'webextension-polyfill'
import { getPreferredLanguage } from '../config/language.mjs'
import '../_locales/i18n'
import { changeLanguage } from 'i18next'
/**
* @param {SiteConfig} siteConfig
@@ -287,6 +294,17 @@ let userConfig
async function run() {
userConfig = await getUserConfig()
await getPreferredLanguageKey().then((lang) => {
changeLanguage(lang)
})
Browser.runtime.onMessage.addListener(async (message) => {
console.log(message)
if (message.type === 'CHANGE_LANG') {
const data = message.data
changeLanguage(data.lang)
}
})
if (isSafari()) await prepareForSafari()
prepareForSelectionTools()
prepareForSelectionToolsTouch()
+71 -35
View File
@@ -2,6 +2,7 @@ import '@picocss/pico'
import { useEffect, useState } from 'react'
import {
defaultConfig,
getPreferredLanguageKey,
getUserConfig,
isUsingApiKey,
isUsingCustomModel,
@@ -24,8 +25,10 @@ import bugmeacoffee from './donation/bugmeacoffee.png'
import { useWindowTheme } from '../hooks/use-window-theme.mjs'
import { languageList } from '../config/language.mjs'
import { isFirefox, isSafari } from '../utils/index.mjs'
import { useTranslation } from 'react-i18next'
function GeneralPart({ config, updateConfig }) {
const { t, i18n } = useTranslation()
const [balance, setBalance] = useState(null)
const getBalance = async () => {
@@ -41,7 +44,7 @@ function GeneralPart({ config, updateConfig }) {
return (
<>
<label>
<legend>Triggers</legend>
<legend>{t('Triggers')}</legend>
<select
required
onChange={(e) => {
@@ -52,14 +55,14 @@ function GeneralPart({ config, updateConfig }) {
{Object.entries(TriggerMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.triggerMode}>
{desc}
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>Theme</legend>
<legend>{t('Theme')}</legend>
<select
required
onChange={(e) => {
@@ -70,14 +73,14 @@ function GeneralPart({ config, updateConfig }) {
{Object.entries(ThemeMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.themeMode}>
{desc}
{t(desc)}
</option>
)
})}
</select>
</label>
<label>
<legend>API Mode</legend>
<legend>{t('API Mode')}</legend>
<span style="display: flex; gap: 15px;">
<select
style={
@@ -94,7 +97,7 @@ function GeneralPart({ config, updateConfig }) {
{Object.entries(Models).map(([key, model]) => {
return (
<option value={key} key={key} selected={key === config.modelName}>
{model.desc}
{t(model.desc)}
</option>
)
})}
@@ -111,7 +114,7 @@ function GeneralPart({ config, updateConfig }) {
{Object.entries(ModelMode).map(([key, desc]) => {
return (
<option value={key} key={key} selected={key === config.modelMode}>
{desc}
{t(desc)}
</option>
)
})}
@@ -123,7 +126,7 @@ function GeneralPart({ config, updateConfig }) {
<input
type="password"
value={config.apiKey}
placeholder="API Key"
placeholder={t('API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ apiKey: apiKey })
@@ -135,7 +138,9 @@ function GeneralPart({ config, updateConfig }) {
target="_blank"
rel="nofollow noopener noreferrer"
>
<button type="button">Get</button>
<button style="white-space: nowrap;" type="button">
{t('Get')}
</button>
</a>
) : balance ? (
<button type="button" onClick={getBalance}>
@@ -143,7 +148,7 @@ function GeneralPart({ config, updateConfig }) {
</button>
) : (
<button type="button" onClick={getBalance}>
Balance
{t('Balance')}
</button>
)}
</span>
@@ -153,7 +158,7 @@ function GeneralPart({ config, updateConfig }) {
<input
type="text"
value={config.customModelName}
placeholder="Model Name"
placeholder={t('Model Name')}
onChange={(e) => {
const customModelName = e.target.value
updateConfig({ customModelName: customModelName })
@@ -166,7 +171,7 @@ function GeneralPart({ config, updateConfig }) {
<input
type="text"
value={config.customModelApiUrl}
placeholder="Custom Model API Url"
placeholder={t('Custom Model API Url')}
onChange={(e) => {
const value = e.target.value
updateConfig({ customModelApiUrl: value })
@@ -175,13 +180,31 @@ function GeneralPart({ config, updateConfig }) {
)}
</label>
<label>
<legend>Preferred Language</legend>
<legend>{t('Preferred Language')}</legend>
<span style="display: flex; gap: 15px;">
<select
required
onChange={(e) => {
const preferredLanguageKey = e.target.value
updateConfig({ preferredLanguage: preferredLanguageKey })
let lang
if (preferredLanguageKey === 'auto') lang = config.userLanguage
else lang = preferredLanguageKey
i18n.changeLanguage(lang)
Browser.tabs.query({}).then((tabs) => {
tabs.forEach((tab) => {
Browser.tabs
.sendMessage(tab.id, {
type: 'CHANGE_LANG',
data: {
lang,
},
})
.catch(() => ({}))
})
})
}}
>
{Object.entries(languageList).map(([k, v]) => {
@@ -203,7 +226,7 @@ function GeneralPart({ config, updateConfig }) {
updateConfig({ insertAtTop: checked })
}}
/>
Insert ChatGPT at the top of search results
{t('Insert ChatGPT at the top of search results')}
</label>
<label>
<input
@@ -214,7 +237,7 @@ function GeneralPart({ config, updateConfig }) {
updateConfig({ lockWhenAnswer: checked })
}}
/>
Lock scrollbar while answering
{t('Lock scrollbar while answering')}
</label>
<br />
</>
@@ -227,10 +250,12 @@ GeneralPart.propTypes = {
}
function AdvancedPart({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
<label>
Custom ChatGPT Web API Url
{t('Custom ChatGPT Web API Url')}
<input
type="text"
value={config.customChatGptWebApiUrl}
@@ -241,7 +266,7 @@ function AdvancedPart({ config, updateConfig }) {
/>
</label>
<label>
Custom ChatGPT Web API Path
{t('Custom ChatGPT Web API Path')}
<input
type="text"
value={config.customChatGptWebApiPath}
@@ -252,7 +277,7 @@ function AdvancedPart({ config, updateConfig }) {
/>
</label>
<label>
Custom OpenAI API Url
{t('Custom OpenAI API Url')}
<input
type="text"
value={config.customOpenAiApiUrl}
@@ -263,7 +288,7 @@ function AdvancedPart({ config, updateConfig }) {
/>
</label>
<label>
Custom Site Regex:
{t('Custom Site Regex:')}
<input
type="text"
value={config.siteRegex}
@@ -282,12 +307,13 @@ function AdvancedPart({ config, updateConfig }) {
updateConfig({ userSiteRegexOnly: checked })
}}
/>
Exclusively use Custom Site Regex for website matching,{isFirefox() ? <br /> : ' '}ignoring
built-in rules
{`${t('Exclusively use Custom Site Regex for website matching,')}${
isFirefox() ? <br /> : ' '
}${t('ignoring built-in rules')}`}
</label>
<br />
<label>
Input Query:
{t('Input Query:')}
<input
type="text"
value={config.inputQuery}
@@ -298,7 +324,7 @@ function AdvancedPart({ config, updateConfig }) {
/>
</label>
<label>
Append Query:
{t('Append Query:')}
<input
type="text"
value={config.appendQuery}
@@ -309,7 +335,7 @@ function AdvancedPart({ config, updateConfig }) {
/>
</label>
<label>
Prepend Query:
{t('Prepend Query:')}
<input
type="text"
value={config.prependQuery}
@@ -329,6 +355,8 @@ AdvancedPart.propTypes = {
}
function SelectionTools({ config, updateConfig }) {
const { t } = useTranslation()
return (
<>
{config.selectionTools.map((key) => (
@@ -343,7 +371,7 @@ function SelectionTools({ config, updateConfig }) {
updateConfig({ activeSelectionTools })
}}
/>
{toolsConfig[key].label}
{t(toolsConfig[key].label)}
</label>
))}
</>
@@ -383,6 +411,8 @@ SiteAdapters.propTypes = {
}
function Donation() {
const { t } = useTranslation()
return (
<div style="display:flex;flex-direction:column;align-items:center;">
<a
@@ -394,7 +424,7 @@ function Donation() {
</a>
<br />
<>
Wechat Pay
{t('Wechat Pay')}
<img alt="wechatpay" src={wechatpay} />
</>
</div>
@@ -403,15 +433,17 @@ function Donation() {
// eslint-disable-next-line react/prop-types
function Footer({ currentVersion, latestVersion }) {
const { t } = useTranslation()
return (
<div className="footer">
<div>
Current Version: {currentVersion}{' '}
{`${t('Current Version')}: ${currentVersion} `}
{currentVersion === latestVersion ? (
'(Latest)'
`(${t('Latest')})`
) : (
<>
(Latest:{' '}
({`${t('Latest')}: `}
<a
href={'https://github.com/josStorer/chatGPTBox/releases/tag/v' + latestVersion}
target="_blank"
@@ -429,7 +461,7 @@ function Footer({ currentVersion, latestVersion }) {
target="_blank"
rel="nofollow noopener noreferrer"
>
<span>Help | Changelog </span>
<span>{t('Help | Changelog ')}</span>
<MarkGithubIcon />
</a>
</div>
@@ -438,6 +470,7 @@ function Footer({ currentVersion, latestVersion }) {
}
function Popup() {
const { t, i18n } = useTranslation()
const [config, setConfig] = useState(defaultConfig)
const [currentVersion, setCurrentVersion] = useState('')
const [latestVersion, setLatestVersion] = useState('')
@@ -449,6 +482,9 @@ function Popup() {
}
useEffect(() => {
getPreferredLanguageKey().then((lang) => {
i18n.changeLanguage(lang)
})
getUserConfig().then((config) => {
setConfig(config)
setCurrentVersion(Browser.runtime.getManifest().version.replace('v', ''))
@@ -469,11 +505,11 @@ function Popup() {
<form style="width:100%;">
<Tabs selectedTabClassName="popup-tab--selected">
<TabList>
<Tab className="popup-tab">General</Tab>
<Tab className="popup-tab">Selection Tools</Tab>
<Tab className="popup-tab">Sites</Tab>
<Tab className="popup-tab">Advanced</Tab>
{isSafari() ? null : <Tab className="popup-tab">Donate</Tab>}
<Tab className="popup-tab">{t('General')}</Tab>
<Tab className="popup-tab">{t('Selection Tools')}</Tab>
<Tab className="popup-tab">{t('Sites')}</Tab>
<Tab className="popup-tab">{t('Advanced')}</Tab>
{isSafari() ? null : <Tab className="popup-tab">{t('Donate')}</Tab>}
</TabList>
<TabPanel>
+1
View File
@@ -1,4 +1,5 @@
import { render } from 'preact'
import Popup from './Popup'
import '../_locales/i18n'
render(<Popup />, document.getElementById('app'))
+1 -1
View File
@@ -1,3 +1,3 @@
export function isFirefox() {
return navigator.userAgent.includes('Firefox')
return navigator.userAgent.toLowerCase().includes('firefox')
}