diff --git a/docs/package-lock.json b/docs/package-lock.json
index 9c3b0b3..448e181 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -4266,69 +4266,6 @@
"node": ">=4"
}
},
- "node_modules/css-select": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
- "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "boolbase": "^1.0.0",
- "css-what": "^6.1.0",
- "domhandler": "^5.0.2",
- "domutils": "^3.0.1",
- "nth-check": "^2.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/fb55"
- }
- },
- "node_modules/css-select/node_modules/dom-serializer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
- "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.2",
- "entities": "^4.2.0"
- },
- "funding": {
- "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domhandler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
- "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "domelementtype": "^2.3.0"
- },
- "engines": {
- "node": ">= 4"
- },
- "funding": {
- "url": "https://github.com/fb55/domhandler?sponsor=1"
- }
- },
- "node_modules/css-select/node_modules/domutils": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
- "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "dom-serializer": "^2.0.0",
- "domelementtype": "^2.3.0",
- "domhandler": "^5.0.3"
- },
- "funding": {
- "url": "https://github.com/fb55/domutils?sponsor=1"
- }
- },
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
@@ -4339,20 +4276,6 @@
"postcss-value-parser": "^4.0.2"
}
},
- "node_modules/css-tree": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
- "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.30",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
- }
- },
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
@@ -4364,42 +4287,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
- "node_modules/csso": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
- "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "css-tree": "~2.2.0"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/css-tree": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
- "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "mdn-data": "2.0.28",
- "source-map-js": "^1.0.1"
- },
- "engines": {
- "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
- "npm": ">=7.0.0"
- }
- },
- "node_modules/csso/node_modules/mdn-data": {
- "version": "2.0.28",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
- "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
- "optional": true,
- "peer": true
- },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -7078,13 +6965,6 @@
"url": "https://opencollective.com/unified"
}
},
- "node_modules/mdn-data": {
- "version": "2.0.30",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
- "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
- "optional": true,
- "peer": true
- },
"node_modules/mermaid": {
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.6.1.tgz",
@@ -11728,42 +11608,6 @@
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="
},
- "node_modules/svgo": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz",
- "integrity": "sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@trysound/sax": "0.2.0",
- "commander": "^7.2.0",
- "css-select": "^5.1.0",
- "css-tree": "^2.3.1",
- "css-what": "^6.1.0",
- "csso": "^5.0.5",
- "picocolors": "^1.0.0"
- },
- "bin": {
- "svgo": "bin/svgo"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/svgo"
- }
- },
- "node_modules/svgo/node_modules/commander": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
- "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
@@ -11868,19 +11712,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/typescript": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
- "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
- "peer": true,
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
"node_modules/unified": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz",
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 98aedce..c39f8bf 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -10,10 +10,12 @@
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
+ "i18next": "^23.11.5",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
+ "react-i18next": "^14.1.2",
"react-markdown": "^8.0.7",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",
@@ -354,11 +356,12 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.20.13",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz",
- "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==",
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz",
+ "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==",
+ "license": "MIT",
"dependencies": {
- "regenerator-runtime": "^0.13.11"
+ "regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1485,7 +1488,7 @@
"version": "18.0.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz",
"integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@types/react": "*"
}
@@ -4134,6 +4137,15 @@
"react-is": "^16.7.0"
}
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/human-signals": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz",
@@ -4158,6 +4170,29 @@
"url": "https://github.com/sponsors/typicode"
}
},
+ "node_modules/i18next": {
+ "version": "23.11.5",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz",
+ "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
@@ -6678,6 +6713,28 @@
"react": ">= 16.8 || 18.0.0"
}
},
+ "node_modules/react-i18next": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz",
+ "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.9",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 23.2.3",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -6875,9 +6932,10 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.13.11",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
- "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ "version": "0.14.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
@@ -7923,6 +7981,15 @@
"vite": "^2.6.0 || 3 || 4 || 5"
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 5c6ef82..ebcbb3f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -21,10 +21,12 @@
"dependencies": {
"@reduxjs/toolkit": "^1.9.2",
"@vercel/analytics": "^0.1.10",
+ "i18next": "^23.11.5",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
+ "react-i18next": "^14.1.2",
"react-markdown": "^8.0.7",
"react-redux": "^8.0.5",
"react-router-dom": "^6.8.1",
@@ -58,5 +60,4 @@
"vite": "^5.0.13",
"vite-plugin-svgr": "^4.2.0"
}
-
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4a8ef6c..3083f1f 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -7,6 +7,7 @@ import { inject } from '@vercel/analytics';
import { useMediaQuery } from './hooks';
import { useState } from 'react';
import Setting from './settings';
+import './locale/i18n';
inject();
diff --git a/frontend/src/Hero.tsx b/frontend/src/Hero.tsx
index cd6d439..69bf23a 100644
--- a/frontend/src/Hero.tsx
+++ b/frontend/src/Hero.tsx
@@ -1,28 +1,16 @@
+import { Fragment } from 'react';
import DocsGPT3 from './assets/cute_docsgpt3.svg';
-const demos: { header: string; query: string }[] = [
- {
- header: 'Learn about DocsGPT',
- query: 'What is DocsGPT?',
- },
- {
- header: 'Summarise documentation',
- query: 'Summarise current context',
- },
- {
- header: 'Write Code',
- query: 'Write code for api request to /api/answer',
- },
- {
- header: 'Learning Assistance',
- query: 'Write potential questions for context',
- },
-];
-
+import { useTranslation } from 'react-i18next';
export default function Hero({
handleQuestion,
}: {
handleQuestion: (question: string) => void;
}) {
+ const { t } = useTranslation();
+ const demos = t('demo', { returnObjects: true }) as Array<{
+ header: string;
+ query: string;
+ }>;
return (
- {demos.map((demo) => (
- <>
-
- >
- ))}
+ {demos?.map(
+ (demo: { header: string; query: string }, key: number) =>
+ demo.header &&
+ demo.query && (
+
+
+
+ ),
+ )}
);
diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx
index 9c5df17..76e0960 100644
--- a/frontend/src/Navigation.tsx
+++ b/frontend/src/Navigation.tsx
@@ -39,7 +39,7 @@ import SelectDocsModal from './preferences/SelectDocsModal';
import ConversationTile from './conversation/ConversationTile';
import { useDarkTheme } from './hooks';
import SourceDropdown from './components/SourceDropdown';
-
+import { useTranslation } from 'react-i18next';
interface NavigationProps {
navOpen: boolean;
setNavOpen: React.Dispatch>;
@@ -70,6 +70,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
const { isMobile } = useMediaQuery();
const [isDarkTheme] = useDarkTheme();
const [isDocsListOpen, setIsDocsListOpen] = useState(false);
+ const { t } = useTranslation();
const isApiKeySet = useSelector(selectApiKeyStatus);
const [apiKeyModalState, setApiKeyModalState] =
@@ -265,14 +266,14 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
className="opacity-80 group-hover:opacity-100"
/>
- New Chat
+ {t('newChat')}
{conversations && conversations.length > 0 ? (
{conversations?.map((conversation) => (
@@ -310,7 +311,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
onClick={() => setUploadModalState('ACTIVE')}
>
-
Source Docs
+
{t('sourceDocs')}
- Settings
+ {t('settings.label')}
@@ -345,7 +346,7 @@ export default function Navigation({ navOpen, setNavOpen }: NavigationProps) {
alt="icon"
className="ml-2 w-5 filter dark:invert"
/>
-
About
+
{t('about')}
diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx
index 05c8638..cf4949e 100644
--- a/frontend/src/conversation/Conversation.tsx
+++ b/frontend/src/conversation/Conversation.tsx
@@ -14,9 +14,10 @@ import {
import Send from './../assets/send.svg';
import SendDark from './../assets/send_dark.svg';
import Spinner from './../assets/spinner.svg';
-import SpinnerDark from './../assets/spinner-dark.svg'
+import SpinnerDark from './../assets/spinner-dark.svg';
import { FEEDBACK, Query } from './conversationModels';
import { sendFeedback } from './conversationApi';
+import { useTranslation } from 'react-i18next';
import ArrowDown from './../assets/arrow-down.svg';
export default function Conversation() {
const queries = useSelector(selectQueries);
@@ -28,6 +29,7 @@ export default function Conversation() {
const [hasScrolledToLast, setHasScrolledToLast] = useState(true);
const fetchStream = useRef(null);
const [eventInterrupt, setEventInterrupt] = useState(false);
+ const { t } = useTranslation();
const handleUserInterruption = () => {
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
@@ -177,7 +179,7 @@ export default function Conversation() {
id="inputbox"
ref={inputRef}
tabIndex={1}
- placeholder="Type your message here..."
+ placeholder={t('inputPlaceholder')}
contentEditable
onPaste={handlePaste}
className={`max-h-24 min-h-[3.8rem] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-full bg-white py-2 pl-4 pr-9 text-base leading-10 opacity-100 focus:outline-none dark:bg-raisin-black dark:text-bright-gray`}
@@ -212,7 +214,7 @@ export default function Conversation() {
)}
- DocsGPT uses GenAI, please review critial information using sources.
+ {t('tagline')}
diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json
new file mode 100644
index 0000000..414e14b
--- /dev/null
+++ b/frontend/src/locale/en.json
@@ -0,0 +1,100 @@
+{
+ "language": "English",
+ "chat": "Chat",
+ "newChat": "New Chat",
+ "myPlan": "My Plan",
+ "about": "About",
+ "inputPlaceholder": "Type your message here...",
+ "tagline": "DocsGPT uses GenAI, please review critial information using sources.",
+ "sourceDocs": "Source Docs",
+ "none": "None",
+ "cancel":"Cancel",
+ "demo": [
+ {
+ "header": "Learn about DocsGPT",
+ "query": "What is DocsGPT?"
+ },
+ {
+ "header": "Summarise documentation",
+ "query": "Summarise current context"
+ },
+ {
+ "header": "Write Code",
+ "query": "Write code for api request to /api/answer"
+ },
+ {
+ "header": "Learning Assistance",
+ "query": "Write potential questions for context"
+ }
+ ],
+ "settings": {
+ "label": "Settings",
+ "general": {
+ "label": "General",
+ "selectTheme": "Select Theme",
+ "light": "Light",
+ "dark": "Dark",
+ "selectLanguage": "Select Language",
+ "chunks": "Chunks processed per query",
+ "prompt": "Active Prompt",
+ "deleteAllLabel": "Delete all Conversation",
+ "deleteAllBtn": "Delete all",
+ "addNew": "Add New"
+ },
+ "documents": {
+ "label": "Documents",
+ "name": "Document Name",
+ "date": "Vector Date",
+ "type": "Type",
+ "tokenUsage": "Token Usage"
+ },
+ "apiKeys": {
+ "label": "API Keys",
+ "name": "Name",
+ "key": "API Key",
+ "sourceDoc": "Source Document",
+ "createNew": "Create New"
+ }
+ },
+ "modals": {
+ "uploadDoc": {
+ "label": "Upload New Documentation",
+ "file": "From File",
+ "remote": "Remote",
+ "name": "Name",
+ "choose": "Choose Files",
+ "info": "Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb",
+ "uploadedFiles": "Uploaded Files",
+ "cancel": "Cancel",
+ "train": "Train",
+ "link": "Link",
+ "urlLink": "URL Link",
+ "reddit": {
+ "id": "Client ID",
+ "secret": "Client Secret",
+ "agent": "User agent",
+ "searchQueries": "Search queries",
+ "numberOfPosts": "Number of posts"
+ }
+ },
+ "createAPIKey": {
+ "label": "Create New API Key",
+ "apiKeyName": "API Key Name",
+ "chunks": "Chunks processed per query",
+ "prompt": "Select active prompt",
+ "sourceDoc": "Source document",
+ "create": "Create"
+ },
+ "saveKey": {
+ "note": "Please save your Key",
+ "disclaimer": "This is the only time your key will be shown.",
+ "copy": "Copy",
+ "copied": "Copied",
+ "confirm": "I saved the Key"
+ },
+ "deleteConv": {
+ "confirm": "Are you sure you want to delete all the conversations?",
+ "delete": "Delete"
+ }
+ }
+}
diff --git a/frontend/src/locale/es.json b/frontend/src/locale/es.json
new file mode 100644
index 0000000..5caa315
--- /dev/null
+++ b/frontend/src/locale/es.json
@@ -0,0 +1,100 @@
+{
+ "language": "Spanish",
+ "chat": "Chat",
+ "newChat": "Nuevo Chat",
+ "myPlan": "Mi Plan",
+ "about": "Acerca de",
+ "inputPlaceholder": "Escribe tu mensaje aquí...",
+ "tagline": "DocsGPT utiliza GenAI, por favor revisa información crítica utilizando fuentes.",
+ "sourceDocs": "Documentos Fuente",
+ "none": "Nada",
+ "cancel": "Cancelar",
+ "demo": [
+ {
+ "header": "Aprende sobre DocsGPT",
+ "query": "¿Qué es DocsGPT?"
+ },
+ {
+ "header": "Resumir documentación",
+ "query": "Resumir contexto actual"
+ },
+ {
+ "header": "Escribir Código",
+ "query": "Escribir código para solicitud de API a /api/answer"
+ },
+ {
+ "header": "Asistencia de Aprendizaje",
+ "query": "Escribe posibles preguntas para el contexto"
+ }
+ ],
+ "settings": {
+ "label": "Configuración",
+ "general": {
+ "label": "General",
+ "selectTheme": "Seleccionar Tema",
+ "light": "de luz",
+ "dark": "oscura",
+ "selectLanguage": "Seleccionar Idioma",
+ "chunks": "Trozos procesados por consulta",
+ "prompt": "Prompt Activo",
+ "deleteAllLabel": "Eliminar toda la Conversación",
+ "deleteAllBtn": "Eliminar todo",
+ "addNew": "Agregar Nuevo"
+ },
+ "documents": {
+ "label": "Documentos",
+ "name": "Nombre del Documento",
+ "date": "Fecha Vector",
+ "type": "Tipo",
+ "tokenUsage": "Uso de Tokens"
+ },
+ "apiKeys": {
+ "label": "Claves API",
+ "name": "Nombre",
+ "key": "Clave de API",
+ "sourceDoc": "Documento Fuente",
+ "createNew": "Crear Nuevo"
+ }
+ },
+ "modals": {
+ "uploadDoc": {
+ "label": "Subir Nueva Documentación",
+ "file": "Desde Archivo",
+ "remote": "Remota",
+ "name": "Nombre",
+ "choose": "Seleccionar Archivos",
+ "info": "Por favor, suba archivos .pdf, .txt, .rst, .docx, .md, .zip limitados a 25 MB",
+ "uploadedFiles": "Archivos Subidos",
+ "cancel": "Cancelar",
+ "train": "Entrenar",
+ "link": "Enlace",
+ "urlLink": "Enlace URL",
+ "reddit": {
+ "id": "ID de Cliente",
+ "secret": "Secreto de Cliente",
+ "agent": "Agente de Usuario",
+ "searchQueries": "Consultas de Búsqueda",
+ "numberOfPosts": "Número de publicaciones"
+ }
+ },
+ "createAPIKey": {
+ "label": "Crear Nueva Clave de API",
+ "apiKeyName": "Nombre de la Clave de API",
+ "chunks": "Fragmentos procesados por consulta",
+ "prompt": "Seleccione el prompt activo",
+ "sourceDoc": "Documento Fuente",
+ "create": "Crear"
+ },
+ "saveKey": {
+ "note": "Por favor, guarde su Clave",
+ "disclaimer": "Esta es la única vez que se mostrará su clave.",
+ "copy": "Copiar",
+ "copied": "Copiado",
+ "confirm": "He guardado la Clave"
+ },
+ "deleteConv": {
+ "confirm": "¿Está seguro de que desea eliminar todas las conversaciones?",
+ "delete": "Eliminar"
+ }
+ }
+}
diff --git a/frontend/src/locale/i18n.ts b/frontend/src/locale/i18n.ts
new file mode 100644
index 0000000..6c170f8
--- /dev/null
+++ b/frontend/src/locale/i18n.ts
@@ -0,0 +1,21 @@
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+
+import en from './en.json'; //English
+import es from './es.json'; //Spanish
+
+i18n.use(initReactI18next).init({
+ resources: {
+ en: {
+ translation: en,
+ },
+ es: {
+ translation: es,
+ },
+ },
+});
+
+const locale = localStorage.getItem('docsgpt-locale') ?? 'en';
+i18n.changeLanguage(locale);
+
+export default i18n;
diff --git a/frontend/src/modals/ConfirmationModal.tsx b/frontend/src/modals/ConfirmationModal.tsx
index c5b4cf4..0b39440 100644
--- a/frontend/src/modals/ConfirmationModal.tsx
+++ b/frontend/src/modals/ConfirmationModal.tsx
@@ -1,6 +1,6 @@
import Exit from '../assets/exit.svg';
import { ActiveState } from '../models/misc';
-
+import { useTranslation } from 'react-i18next';
function ConfirmationModal({
message,
modalState,
@@ -18,6 +18,7 @@ function ConfirmationModal({
cancelLabel?: string;
handleCancel?: () => void;
}) {
+ const { t } = useTranslation();
return (
- {cancelLabel ? cancelLabel : 'Cancel'}
+ {cancelLabel ? cancelLabel : t('cancel')}
diff --git a/frontend/src/modals/DeleteConvModal.tsx b/frontend/src/modals/DeleteConvModal.tsx
index 4371fc1..c2fc5e3 100644
--- a/frontend/src/modals/DeleteConvModal.tsx
+++ b/frontend/src/modals/DeleteConvModal.tsx
@@ -3,7 +3,7 @@ import { useDispatch } from 'react-redux';
import { ActiveState } from '../models/misc';
import { useMediaQuery, useOutsideAlerter } from '../hooks';
import ConfirmationModal from './ConfirmationModal';
-
+import { useTranslation } from 'react-i18next';
import { Action } from '@reduxjs/toolkit';
export default function DeleteConvModal({
@@ -18,7 +18,7 @@ export default function DeleteConvModal({
const modalRef = React.useRef(null);
const dispatch = useDispatch();
const { isMobile } = useMediaQuery();
-
+ const { t } = useTranslation();
useOutsideAlerter(
modalRef,
() => {
@@ -40,10 +40,10 @@ export default function DeleteConvModal({
return (
diff --git a/frontend/src/modals/index.tsx b/frontend/src/modals/index.tsx
index a13d703..1dcffd6 100644
--- a/frontend/src/modals/index.tsx
+++ b/frontend/src/modals/index.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-
+import { useTranslation } from 'react-i18next';
interface ModalProps {
handleSubmit: () => void;
isCancellable: boolean;
@@ -12,6 +12,7 @@ interface ModalProps {
}
const Modal = (props: ModalProps) => {
+ const { t } = useTranslation();
return (
{
} absolute z-30 h-screen w-screen bg-gray-alpha`}
>
{props.render()}
-
+
@@ -32,7 +33,7 @@ const Modal = (props: ModalProps) => {
onClick={() => props.handleCancel && props.handleCancel()}
className="cursor-pointer rounded-3xl px-5 py-2 text-sm font-medium hover:bg-gray-100 dark:bg-transparent dark:text-light-gray dark:hover:bg-[#767183]/50"
>
- Cancel
+ {t('cancel')}
)}
diff --git a/frontend/src/settings/APIKeys.tsx b/frontend/src/settings/APIKeys.tsx
index b7a98c8..8264af0 100644
--- a/frontend/src/settings/APIKeys.tsx
+++ b/frontend/src/settings/APIKeys.tsx
@@ -9,13 +9,14 @@ import {
import { selectSourceDocs } from '../preferences/preferenceSlice';
import Exit from '../assets/exit.svg';
import Trash from '../assets/trash.svg';
-
+import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const embeddingsName =
import.meta.env.VITE_EMBEDDINGS_NAME ||
'huggingface_sentence-transformers/all-mpnet-base-v2';
const APIKeys: React.FC = () => {
+ const { t } = useTranslation();
const [isCreateModalOpen, setCreateModal] = React.useState(false);
const [isSaveKeyModalOpen, setSaveKeyModal] = React.useState(false);
const [newKey, setNewKey] = React.useState('');
@@ -97,7 +98,7 @@ const APIKeys: React.FC = () => {
onClick={() => setCreateModal(true)}
className="rounded-full bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
>
- Create new
+ {t('settings.apiKeys.createNew')}
{isCreateModalOpen && (
@@ -117,11 +118,15 @@ const APIKeys: React.FC = () => {
- Name |
+
+ {t('settings.apiKeys.name')}
+ |
- Source document
+ {t('settings.apiKeys.sourceDoc')}
+ |
+
+ {t('settings.apiKeys.key')}
|
- API Key |
|
@@ -216,7 +221,7 @@ const CreateAPIKeyModal: React.FC = ({
};
})
: [];
-
+ const { t } = useTranslation();
return (
@@ -225,12 +230,12 @@ const CreateAPIKeyModal: React.FC
= ({
- Create New API Key
+ {t('modals.createAPIKey.label')}
- API Key Name
+ {t('modals.createAPIKey.apiKeyName')}
= ({
setSourcePath(selection)
@@ -255,7 +260,7 @@ const CreateAPIKeyModal: React.FC = ({
setPrompt(value)
}
@@ -264,7 +269,7 @@ const CreateAPIKeyModal: React.FC = ({
- Chunks processed per query
+ {t('modals.createAPIKey.chunks')}
= ({
}
className="float-right mt-4 rounded-full bg-purple-30 px-5 py-2 text-sm text-white hover:bg-[#6F3FD1] disabled:opacity-50"
>
- Create
+ {t('modals.createAPIKey.create')}
@@ -296,6 +301,7 @@ const CreateAPIKeyModal: React.FC
= ({
const SaveAPIKeyModal: React.FC = ({ apiKey, close }) => {
const [isCopied, setIsCopied] = React.useState(false);
+ const { t } = useTranslation();
const handleCopyKey = () => {
navigator.clipboard.writeText(apiKey);
setIsCopied(true);
@@ -306,9 +312,12 @@ const SaveAPIKeyModal: React.FC = ({ apiKey, close }) => {
- Please save your Key
+
+ {' '}
+ {t('modals.saveKey.note')}
+
- This is the only time your key will be shown.
+ {t('modals.saveKey.disclaimer')}
@@ -319,14 +328,14 @@ const SaveAPIKeyModal: React.FC = ({ apiKey, close }) => {
className="my-1 h-10 w-20 rounded-full border border-solid border-purple-30 p-2 text-sm text-purple-30 hover:bg-purple-30 hover:text-white"
onClick={handleCopyKey}
>
- {isCopied ? 'Copied' : 'Copy'}
+ {isCopied ? t('modals.saveKey.copied') : t('modals.saveKey.copy')}
diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx
index c7e7e83..e73ce73 100644
--- a/frontend/src/settings/Documents.tsx
+++ b/frontend/src/settings/Documents.tsx
@@ -1,10 +1,12 @@
import { DocumentsProps } from '../models/misc';
import Trash from '../assets/trash.svg';
import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
const Documents: React.FC = ({
documents,
handleDeleteDocument,
}) => {
+ const { t } = useTranslation();
return (
@@ -12,10 +14,18 @@ const Documents: React.FC
= ({
- Document Name |
- Vector Date |
- Token usage |
- Type |
+
+ {t('settings.documents.name')}
+ |
+
+ {t('settings.documents.date')}
+ |
+
+ {t('settings.documents.tokenUsage')}
+ |
+
+ {t('settings.documents.type')}
+ |
|
diff --git a/frontend/src/settings/General.tsx b/frontend/src/settings/General.tsx
index c098af1..a7c5d01 100644
--- a/frontend/src/settings/General.tsx
+++ b/frontend/src/settings/General.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Prompts from './Prompts';
import { useDarkTheme } from '../hooks';
+import { useTranslation } from 'react-i18next';
import Dropdown from '../components/Dropdown';
import {
selectPrompt,
@@ -16,8 +17,22 @@ import {
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const General: React.FC = () => {
+ const {
+ t,
+ i18n: { changeLanguage, language },
+ } = useTranslation();
const themes = ['Light', 'Dark'];
- const languages = ['English'];
+
+ const languageOptions = [
+ {
+ label: 'English',
+ value: 'en',
+ },
+ {
+ label: 'Spanish',
+ value: 'es',
+ },
+ ];
const chunks = ['0', '2', '4', '6', '8', '10'];
const token_limits = new Map([
[0, 'None'],
@@ -37,7 +52,12 @@ const General: React.FC = () => {
isDarkTheme ? 'Dark' : 'Light',
);
const dispatch = useDispatch();
- const [selectedLanguage, setSelectedLanguage] = React.useState(languages[0]);
+ const locale = localStorage.getItem('docsgpt-locale');
+ const [selectedLanguage, setSelectedLanguage] = React.useState(
+ locale
+ ? languageOptions.find((option) => option.value === locale)
+ : languageOptions[0],
+ );
const selectedPrompt = useSelector(selectPrompt);
React.useEffect(() => {
@@ -59,7 +79,9 @@ const General: React.FC = () => {
return (
-
Select Theme
+
+ {t('settings.general.selectTheme')}
+
{
/>
-
- Select Language
+
+ {t('settings.general.selectLanguage')}
{
+ setSelectedLanguage(selectedOption);
+ changeLanguage(selectedOption.value);
+ localStorage.setItem('docsgpt-locale', selectedOption.value);
+ }}
size="w-56"
rounded="3xl"
border="border"
@@ -87,7 +113,7 @@ const General: React.FC = () => {
- Chunks processed per query
+ {t('settings.general.chunks')}
{
- Delete all conversations
+ {t('settings.general.deleteAllLabel')}
diff --git a/frontend/src/settings/Prompts.tsx b/frontend/src/settings/Prompts.tsx
index e5af189..2bae07e 100644
--- a/frontend/src/settings/Prompts.tsx
+++ b/frontend/src/settings/Prompts.tsx
@@ -2,9 +2,8 @@ import React from 'react';
import { PromptProps, ActiveState } from '../models/misc';
import Dropdown from '../components/Dropdown';
import PromptsModal from '../preferences/PromptsModal';
-
+import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
-
const Prompts: React.FC = ({
prompts,
selectedPrompt,
@@ -34,7 +33,10 @@ const Prompts: React.FC = ({
});
const [modalType, setModalType] = React.useState<'ADD' | 'EDIT'>('ADD');
const [modalState, setModalState] = React.useState('INACTIVE');
-
+ const {
+ t,
+ i18n: { changeLanguage, language },
+ } = useTranslation();
const handleAddPrompt = async () => {
try {
const response = await fetch(`${apiHost}/api/create_prompt`, {
@@ -158,7 +160,9 @@ const Prompts: React.FC = ({
-
Active Prompt
+
+ {t('settings.general.prompt')}
+
= ({
setModalState('ACTIVE');
}}
>
- Add new
+ {t('settings.general.addNew')}
diff --git a/frontend/src/settings/index.tsx b/frontend/src/settings/index.tsx
index 1c0fb82..9c0714c 100644
--- a/frontend/src/settings/index.tsx
+++ b/frontend/src/settings/index.tsx
@@ -11,12 +11,18 @@ import {
import { Doc } from '../preferences/preferenceApi';
import ArrowLeft from '../assets/arrow-left.svg';
import ArrowRight from '../assets/arrow-right.svg';
+import { useTranslation } from 'react-i18next';
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
const Settings: React.FC = () => {
const dispatch = useDispatch();
- const tabs = ['General', 'Documents', 'API Keys'];
+ const { t } = useTranslation();
+ const tabs = [
+ t('settings.general.label'),
+ t('settings.documents.label'),
+ t('settings.apiKeys.label'),
+ ];
const [activeTab, setActiveTab] = React.useState('General');
const [widgetScreenshot, setWidgetScreenshot] = React.useState
(
null,
@@ -45,7 +51,7 @@ const Settings: React.FC = () => {
return (
- Settings
+ {t('settings.label')}
@@ -100,9 +106,9 @@ const Settings: React.FC = () => {
function renderActiveTab() {
switch (activeTab) {
- case 'General':
+ case t('settings.general.label'):
return
;
- case 'Documents':
+ case t('settings.documents.label'):
return (
{
onWidgetScreenshotChange={updateWidgetScreenshot} // Add this line
/>
);
- case 'API Keys':
+ case t('settings.apiKeys.label'):
return ;
default:
return null;
diff --git a/frontend/src/upload/Upload.tsx b/frontend/src/upload/Upload.tsx
index 605b4fa..6437acc 100644
--- a/frontend/src/upload/Upload.tsx
+++ b/frontend/src/upload/Upload.tsx
@@ -6,7 +6,7 @@ import { ActiveState } from '../models/misc';
import { getDocs } from '../preferences/preferenceApi';
import { setSourceDocs } from '../preferences/preferenceSlice';
import Dropdown from '../components/Dropdown';
-
+import { useTranslation } from 'react-i18next';
export default function Upload({
modalState,
setModalState,
@@ -24,6 +24,7 @@ export default function Upload({
search_queries: [''],
number_posts: 10,
});
+ const { t } = useTranslation();
const urlOptions: { label: string; value: string }[] = [
{ label: 'Crawler', value: 'crawler' },
// { label: 'Sitemap', value: 'sitemap' },
@@ -238,7 +239,7 @@ export default function Upload({
view = (
<>
- Upload New Documentation
+ {t('modals.uploadDoc.label')}
{activeTab === 'file' && (
@@ -272,21 +273,21 @@ export default function Upload({
>
- Name
+ {t('modals.uploadDoc.name')}
- Choose Files
+ {t('modals.uploadDoc.choose')}
- Please upload .pdf, .txt, .rst, .docx, .md, .zip limited to 25mb
+ {t('modals.uploadDoc.info')}
- Uploaded Files
+ {t('modals.uploadDoc.uploadedFiles')}
{files.map((file) => (
@@ -294,7 +295,9 @@ export default function Upload({
))}
{files.length === 0 && (
-
None
+
+ {t('none')}
+
)}
>
@@ -313,7 +316,7 @@ export default function Upload({
{urlType.label !== 'Reddit' ? (
<>
- Name
+ {t('modals.uploadDoc.name')}
- Link
+ {t('modals.uploadDoc.link')}
>
@@ -349,7 +352,7 @@ export default function Upload({
>
- Client ID
+ {t('modals.uploadDoc.reddit.id')}
- Client secret
+ {t('modals.uploadDoc.reddit.secret')}
- User agent
+ {t('modals.uploadDoc.reddit.agent')}
- Search queries
+ {t('modals.uploadDoc.reddit.searchQueries')}
- Number of posts
+ {t('modals.uploadDoc.reddit.numberOfPosts')}
>
@@ -422,14 +425,14 @@ export default function Upload({
activeTab === 'file'
}
>
- Train
+ {t('modals.uploadDoc.train')}
) : (
)}
>