diff --git a/package-lock.json b/package-lock.json index 5bb9411c..726e1da0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "twinny", - "version": "3.19.2", + "version": "3.19.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twinny", - "version": "3.19.2", + "version": "3.19.3", "cpu": [ "x64", "arm64" @@ -36,12 +36,15 @@ "handlebars-loader": "^1.7.3", "hypercore-crypto": "^3.4.2", "hyperswarm": "^4.7.15", + "i18next": "^23.16.5", + "i18next-http-backend": "^2.6.2", "ignore": "^6.0.2", "js-yaml": "^4.1.0", "node-polyfill-webpack-plugin": "^3.0.0", "onnxruntime-web": "^1.18.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^15.1.1", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "rehype-raw": "^7.0.0", @@ -6029,6 +6032,14 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8523,6 +8534,14 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "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==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", @@ -8683,6 +8702,36 @@ "resolved": "https://registry.npmjs.org/hypertrace/-/hypertrace-1.4.2.tgz", "integrity": "sha512-sa6iq1FaJ03Db3eUl5ZodyOL3fheyrum9xzeHasXOQ/AprTT6vS1WjpbXfYkHhmzVmyn0jBW/VsCb1QaBkGyow==" }, + "node_modules/i18next": { + "version": "23.16.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.5.tgz", + "integrity": "sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA==", + "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" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.6.2.tgz", + "integrity": "sha512-Hp/kd8/VuoxIHmxsknJXjkTYYHzivAyAF15pzliKzk2TiXC25rZCEerb1pUFoxz4IVrG3fCvQSY51/Lu4ECV4A==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -12950,6 +12999,25 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", @@ -14524,6 +14592,27 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz", + "integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "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": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -16370,6 +16459,11 @@ "toxe": "^1.0.2" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tree-sitter-wasms": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.11.tgz", @@ -17078,6 +17172,14 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -17119,6 +17221,11 @@ "integrity": "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "5.93.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", @@ -17214,6 +17321,15 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 44bf8aa8..c7ce96de 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "twinny", "displayName": "twinny - AI Code Completion and Chat", "description": "Locally hosted AI code completion plugin for vscode", - "version": "3.19.2", + "version": "3.19.3", "icon": "assets/icon.png", "keywords": [ "code-inference", @@ -482,12 +482,15 @@ "handlebars-loader": "^1.7.3", "hypercore-crypto": "^3.4.2", "hyperswarm": "^4.7.15", + "i18next": "^23.16.5", + "i18next-http-backend": "^2.6.2", "ignore": "^6.0.2", "js-yaml": "^4.1.0", "node-polyfill-webpack-plugin": "^3.0.0", "onnxruntime-web": "^1.18.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^15.1.1", "react-markdown": "^9.0.1", "react-syntax-highlighter": "^15.5.0", "rehype-raw": "^7.0.0", diff --git a/src/webview/assets/locales/en.json b/src/webview/assets/locales/en.json new file mode 100644 index 00000000..e7c5132f --- /dev/null +++ b/src/webview/assets/locales/en.json @@ -0,0 +1,90 @@ +{ + "accept-solution": "Accept Solution", + "api-key-placeholder": "Enter your API key here", + "api-key": "API Key", + "api-path-placeholder": "Enter a hostname e.g 'localhost'", + "api-path": "API Path", + "applicable-ollama": "Applicable for some providers like Ollama", + "auto-connect-as-provider": "Auto connect as provider", + "automatic": "Automatic", + "cancel-edit": "Cancel edit", + "cancel": "Cancel", + "chat": "Chat", + "clear-conversations": "Clear conversations", + "connect": "Connect", + "connected": "Connected!", + "connecting": "Connecting...", + "connection-failed": "Connection failed! Please check your connection and try again.", + "consumer-connection": "Consumer Connection", + "conversation-history": "Conversation History", + "copy-code": "Copy Code", + "copy-provider": "Copy Provider", + "delete-message": "Delete message", + "delete-provider": "Delete Provider", + "disconnect": "Disconnect", + "edit-default-templates-description": "Edit the default templates used in the twinny extension.", + "edit-default-templates": "Edit default templates", + "edit-message": "Edit message", + "edit-provider": "Edit Provider", + "embed-documents": "Embed documents", + "embedding-provider": "Embedding provider", + "fim-template": "FIM Template", + "fim": "Fill-in-middle", + "hostname-placeholder": "Enter a hostname e.g 'localhost'", + "hostname": "Hostname", + "label-placeholder": "Enter a label for your provider.", + "label": "Label", + "loading-available-models": "Loading available models...", + "max-chunk-size": "Max chunk size", + "min-chunk-size": "Min chunk size", + "model-name-placeholder": "Enter a model name e.g 'llama3'", + "model-name": "Model Name", + "new-conversation": "New Conversation", + "new-document": "New Document", + "no-connections-found": "No connections found. Please add a new connection to get started.", + "no-result": "No result", + "nothing-to-see-here": "Nothing to see here.", + "number-code-filepaths": "The number of file paths to be used as context.", + "number-code-snippets": "The number of code snippets to be used as context.", + "open-diff": "Open Diff", + "open-template-editor": "Open template editor", + "overlap-size": "Overlap size", + "owner-repo-name": "This tab will help you review pull requests in your repository, enter the owner and repository name below to get started. For now only GitHub is supported, set your GitHub token in the settings tab to get started.", + "path": "Path", + "placeholder": "How can twinny help you today?", + "port-placeholder": "Enter a port number e.g '11434'", + "port": "Port", + "protocol": "Protocol", + "provider-connection": "Provider Connection", + "provider-name": "Provider Name", + "provider-placeholder": "Enter a provider name", + "provider-type": "Provider Type", + "provider": "Provider", + "providers": "Providers", + "pull-requests": "Pull Requests", + "regenerate-message": "Regenerate message", + "relevant-code-snippets": "Relevant code snippets", + "relevant-file-paths": "Relevant file paths", + "repository-level": "Repository level", + "rerank-probability-threshold": "Rerank probability threshold", + "rerank-threshold-description": "The lower the threshold, the more likely a result is to be included.", + "rerank-threshold": "Rerank threshold", + "reset-providers": "Reset Providers", + "reset-to-default": "Reset to default", + "review-pull-requests": "Review pull requests", + "save-edit": "Save edit", + "save": "Save", + "scroll-down": "Scroll down to the bottom", + "share-gpu-resources": "You can also share your GPU resources by connecting to Symmetry as a provider using your active twinny provider configuration. All connections are peer to peer, encrypted end-to-end and secure.", + "status": "Status", + "stop-generation": "Stop generation", + "symmetry-description": "Symmetry is a peer-to-peer AI inference network that allows secure, direct connections between users. When you connect as a consumer, Symmetry matches you with a provider based on your model selection.", + "symmetry-inference-network": "Symmetry Inference Network", + "template-settings-description": "Select the templates you want to use in the chat interface.", + "template-settings": "Template settings", + "thinking": "Thinking...", + "toggle-auto-scroll": "Toggle auto scroll on/off", + "toggle-embedding-options": "Toggle embedding options on/off", + "toggle-provider-selection": "Toggle provider selection", + "type": "Type" +} diff --git a/src/webview/chat.tsx b/src/webview/chat.tsx index 1584ad52..30665b38 100644 --- a/src/webview/chat.tsx +++ b/src/webview/chat.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useTranslation } from "react-i18next" import Mention from "@tiptap/extension-mention" import Placeholder from "@tiptap/extension-placeholder" import { Editor, EditorContent, JSONContent,useEditor } from "@tiptap/react" @@ -57,6 +58,7 @@ export const Chat = (props: ChatProps): JSX.Element => { const editorRef = useRef(null) const stopRef = useRef(false) const theme = useTheme() + const { t } = useTranslation() const [isLoading, setIsLoading] = useState(false) const [messages, setMessages] = useState() const [completion, setCompletion] = useState() @@ -416,7 +418,7 @@ export const Chat = (props: ChatProps): JSX.Element => { clearEditor, }), Placeholder.configure({ - placeholder: "How can twinny help you today?", + placeholder: t("placeholder") // "How can twinny help you today?", }), ], }, @@ -454,7 +456,7 @@ export const Chat = (props: ChatProps): JSX.Element => { @@ -516,7 +518,7 @@ export const Chat = (props: ChatProps): JSX.Element => {
{autoScrollContext ? ( @@ -526,7 +528,7 @@ export const Chat = (props: ChatProps): JSX.Element => { )} @@ -540,7 +542,7 @@ export const Chat = (props: ChatProps): JSX.Element => { type="button" appearance="icon" onClick={handleStopGeneration} - aria-label="Stop generation" + aria-label={t("stop-generation")} > @@ -548,14 +550,14 @@ export const Chat = (props: ChatProps): JSX.Element => { {!symmetryConnection && ( <> @@ -567,6 +569,7 @@ export const Chat = (props: ChatProps): JSX.Element => { + {/* TODO interpolate */} @@ -579,7 +582,6 @@ export const Chat = (props: ChatProps): JSX.Element => {
diff --git a/src/webview/code-block.tsx b/src/webview/code-block.tsx index 28cfce1f..4083a147 100644 --- a/src/webview/code-block.tsx +++ b/src/webview/code-block.tsx @@ -1,4 +1,5 @@ import React, { ReactNode } from "react" +import { useTranslation } from "react-i18next" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { vs,vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" @@ -22,6 +23,7 @@ interface CodeBlockProps { const global = globalThis as any export const CodeBlock = (props: CodeBlockProps) => { + const { t } = useTranslation() const { children, language, className, theme, role } = props const lang = getLanguageMatch(language, className) @@ -63,28 +65,28 @@ export const CodeBlock = (props: CodeBlockProps) => { <>
diff --git a/src/webview/conversation-history.tsx b/src/webview/conversation-history.tsx index 3c56d887..45d822cb 100644 --- a/src/webview/conversation-history.tsx +++ b/src/webview/conversation-history.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { EVENT_NAME } from "../common/constants" @@ -14,6 +15,7 @@ interface ConversationHistoryProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any const global = globalThis as any export const ConversationHistory = ({ onSelect }: ConversationHistoryProps) => { + const { t } = useTranslation() const { conversations: savedConversations, setActiveConversation, @@ -43,9 +45,11 @@ export const ConversationHistory = ({ onSelect }: ConversationHistoryProps) => { return (
-

Conversation history

+

+ {t("conversation-history")} ({conversations.length}) +

- Clear conversations + {t("clear-conversations")} {conversations.length ? ( conversations.map((conversation) => ( @@ -64,7 +68,9 @@ export const ConversationHistory = ({ onSelect }: ConversationHistoryProps) => {
)) ) : ( -

Nothing to see here...

+

+ {t("nothing-to-see-here")} +

)}
) diff --git a/src/webview/embedding-options.tsx b/src/webview/embedding-options.tsx index e41322ba..d05791d4 100644 --- a/src/webview/embedding-options.tsx +++ b/src/webview/embedding-options.tsx @@ -1,4 +1,5 @@ import { FormEvent } from "react" +import { useTranslation } from "react-i18next" import { TextFieldType } from "@vscode/webview-ui-toolkit" import { VSCodeButton, @@ -21,6 +22,7 @@ import styles from "./styles/index.module.css" // eslint-disable-next-line @typescript-eslint/no-explicit-any const global = globalThis as any export const EmbeddingOptions = () => { + const { t } = useTranslation() const { embeddingProvider, getProvidersByType, @@ -112,7 +114,7 @@ export const EmbeddingOptions = () => { return (
-
Embedding provider
+
{t("embedding-provider")}
{
-
Max chunk size
+
+ {t("max-chunk-size")} +
{ />
-
Min chunk size
+
+ {t("max-chunk-size")} +
{ />
-
Overlap
+
+ {t("overlap-size")} +
{ onClick={handleEmbedDocuments} className={styles.embedDocumentsButton} > - Embed workspace documents + {t("embed-documents")}
-
Code snippets
+
+ {t("relevant-code-snippets")} +
handleRelevantCodeSnippetsChange(e)} /> - The number code snippets to be used as context. + {t("number-code-snippets")}
@@ -180,13 +190,13 @@ export const EmbeddingOptions = () => { onChange={(e) => handleRelevantFilepathsChange(e)} /> - The number of filepaths to be used as context. + {t("number-code-filepaths")}
{ />
- The lower the threshold, the more likely a result is to be included. + {t("rerank-threshold-description")}
diff --git a/src/webview/i18n.ts b/src/webview/i18n.ts new file mode 100644 index 00000000..1784540b --- /dev/null +++ b/src/webview/i18n.ts @@ -0,0 +1,31 @@ +import { initReactI18next } from "react-i18next" +import i18n from "i18next" +import Backend from "i18next-http-backend" + +import en from "./assets/locales/en.json" + + +i18n + .use(Backend) + .use(initReactI18next) + .init({ + fallbackLng: "en", + resources: { + en: { translation: en }, + }, + backend: { + loadPath: (lng: string) => `"/assets/locales"/${lng}.json`, + }, + detection: { + order: ["localStorage"], + availableLanguages: ["en"], + }, + debug: true, + react: { + useSuspense: true, + transKeepBasicHtmlNodesFor: ["br", "strong", "i", "p", "u"], + transWrapTextNodes: "span", + }, + }) + +export default i18n diff --git a/src/webview/loader.tsx b/src/webview/loader.tsx index c3673811..83549895 100644 --- a/src/webview/loader.tsx +++ b/src/webview/loader.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react" +import { useTranslation } from "react-i18next" import { ASSISTANT } from "../common/constants" @@ -6,6 +7,7 @@ import { useLoading, useTheme } from "./hooks" import { Message } from "./message" export const ChatLoader = () => { + const { t } = useTranslation() const theme = useTheme() const loader = useLoading() const [dots, setDots] = useState("") @@ -35,7 +37,7 @@ export const ChatLoader = () => { isAssistant theme={theme} message={{ - content: `${loader || "Thinking"}${dots}`, + content: `${loader || t("thinking")}${dots}`, role: ASSISTANT, }} > diff --git a/src/webview/main.tsx b/src/webview/main.tsx index 272b1762..0c177157 100644 --- a/src/webview/main.tsx +++ b/src/webview/main.tsx @@ -1,5 +1,7 @@ import { useEffect, useState } from "react" +import "./i18n" + import { EVENT_NAME, WEBUI_TABS } from "../common/constants" import { ServerMessage } from "../common/types" diff --git a/src/webview/mention-list.tsx b/src/webview/mention-list.tsx index 35172de7..385a1119 100644 --- a/src/webview/mention-list.tsx +++ b/src/webview/mention-list.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { forwardRef, useEffect, useImperativeHandle, useState } from "react" +import { useTranslation } from "react-i18next" import { Editor } from "@tiptap/core" import { MentionNodeAttrs } from "@tiptap/extension-mention" import cx from "classnames" @@ -21,6 +22,7 @@ export interface MentionListRef { export const MentionList = forwardRef( (props, ref) => { + const { t } = useTranslation() const [selectedIndex, setSelectedIndex] = useState(0) const selectItem = (index: number) => { @@ -83,7 +85,7 @@ export const MentionList = forwardRef( )) ) : ( -
No result
+
{t("no-result")}
)}
) diff --git a/src/webview/message.tsx b/src/webview/message.tsx index 65ac7b6b..47bf1f16 100644 --- a/src/webview/message.tsx +++ b/src/webview/message.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useMemo } from "react" +import { useTranslation } from "react-i18next" import Markdown, { Components } from "react-markdown" import { EditorContent, Extension, useEditor } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" @@ -61,6 +62,7 @@ export const Message: React.FC = React.memo( onUpdate, theme, }) => { + const { t } = useTranslation() const [editing, setEditing] = React.useState(false) const handleToggleEditing = useCallback( @@ -152,14 +154,14 @@ export const Message: React.FC = React.memo( {editing && !isAssistant && ( <> @@ -171,7 +173,7 @@ export const Message: React.FC = React.memo( <> @@ -179,7 +181,7 @@ export const Message: React.FC = React.memo( @@ -190,7 +192,7 @@ export const Message: React.FC = React.memo( {!editing && isAssistant && ( @@ -201,7 +203,6 @@ export const Message: React.FC = React.memo(
{editing ? ( diff --git a/src/webview/provider-select.tsx b/src/webview/provider-select.tsx index a6295873..5ea08fa0 100644 --- a/src/webview/provider-select.tsx +++ b/src/webview/provider-select.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next" import { VSCodeDropdown, VSCodeOption, @@ -8,6 +9,7 @@ import { useProviders } from "./hooks" import styles from "./styles/providers.module.css" export const ProviderSelect = () => { + const { t } = useTranslation() const { getProvidersByType, setActiveChatProvider, @@ -34,7 +36,9 @@ export const ProviderSelect = () => { return (
-
Chat
+
+ {t("chat")} +
{
-
Fill-in-middle
+
+ {t("fim")} +
{ + const { t } = useTranslation() const [showForm, setShowForm] = React.useState(false) const [provider, setProvider] = React.useState() const { models } = useOllamaModels() @@ -76,7 +78,9 @@ export const Providers = () => { return (
-

Providers

+

+ {t("providers")} +

{showForm ? ( @@ -87,7 +91,7 @@ export const Providers = () => { Add Provider - Reset Providers + {t("reset-providers")}
{Object.values(providers).map((provider, index) => ( @@ -111,30 +115,30 @@ export const Providers = () => { handleChange(provider, e) }} value={provider.modelName} - placeholder='Applicable for some providers like "Ollama"' + placeholder={t("applicable-ollama")} > )}
handleEdit(provider)} > handleCopy(provider)} > handleDelete(provider)} > @@ -144,34 +148,34 @@ export const Providers = () => {
- Label: {provider.label} + {t("label")}: {provider.label}
- Provider: {provider.provider} + {t("provider")}: {provider.provider}
- Type: {provider.type} + {t("type")}: {provider.type}
{provider.type === "fim" && (
- Fim Template: {provider.fimTemplate} + {t("fim-template")}: {provider.fimTemplate}
)}
- Hostname: {provider.apiHostname} + {t("hostname")}: {provider.apiHostname}
- Path: {provider.apiPath} + {t("path")}: {provider.apiPath}
- Protocol: {provider.apiProtocol} + {t("protocol")}: {provider.apiProtocol}
- Port: {provider.apiPort} + {t("port")}: {provider.apiPort}
{provider.apiKey && (
- ApiKey: {provider.apiKey.substring(0, 12)}... + {t("api-key")}: {provider.apiKey.substring(0, 12)}...
)}
@@ -191,6 +195,7 @@ interface ProviderFormProps { } function ProviderForm({ onClose, provider }: ProviderFormProps) { + const { t } = useTranslation() const isEditing = provider !== undefined const { models } = useOllamaModels() const { saveProvider, updateProvider } = useProviders() @@ -238,7 +243,7 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) { const getModelInput = () => { if (formState.provider === apiProviders.Ollama && hasOllamaModels) { return ( - <> +
@@ -249,21 +254,21 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) { setFormState({ ...formState, modelName: model }) }} /> - +
) } return ( <>
- +
) @@ -275,20 +280,20 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) {
- +
- +
- +
{Object.values(FIM_TEMPLATE_FORMAT).map((type, index) => ( - + {type} ))} @@ -324,7 +329,7 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) {
- +
- +
- +
- +
- +
- +
@@ -417,17 +422,19 @@ function ProviderForm({ onClose, provider }: ProviderFormProps) { checked={formState.repositoryLevel} onClick={handleRepositoryLevelCheck} > - Repository Level + + {t("repository-level")} +
)}
- Save + {t("save")} - Cancel + {t("cancel")}
diff --git a/src/webview/review.tsx b/src/webview/review.tsx index e1808735..2d412c12 100644 --- a/src/webview/review.tsx +++ b/src/webview/review.tsx @@ -1,4 +1,5 @@ import React, { FormEvent, useEffect } from "react" +import { useTranslation } from "react-i18next" import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { WORKSPACE_STORAGE_KEY } from "../common/constants" @@ -8,6 +9,7 @@ import { useGithubPRs, useGlobalContext } from "./hooks" import styles from "./styles/index.module.css" export const Review = () => { + const { t } = useTranslation() const { prs, getPrs, startReview, isLoading } = useGithubPRs() const { context: owner, setContext: setOwner } = useGlobalContext( WORKSPACE_STORAGE_KEY.reviewOwner @@ -44,11 +46,11 @@ export const Review = () => { return ( <> -

Review Pull Requests

+

+ {t("review-pull-requests")} +

- This tab will help you review pull requests in your repository, enter - the owner and repository name below to get started. For now only GitHub - is supported, set your GitHub token in the settings tab to get started. + {t("owner-repo-name")}

{ {prs.length > 0 && (
-

Pull Requests:

+

+ {t("pull-requests")} +