feat: i18n (#723)

* feat: i18n

* fix: ci lint error

* fix: pnpm run pre script not trigger

---------

Co-authored-by: Jim Su <jimsu@protonmail.com>
This commit is contained in:
mashiro 2024-04-05 04:38:19 +08:00 committed by GitHub
parent baa981cda7
commit 0534c14279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 7301 additions and 3133 deletions

View File

@ -25,7 +25,7 @@ build:
rm -rf node_modules; \
fi
@which corepack > /dev/null || (echo "Installing corepack..." && npm install -g corepack)
@cd frontend && corepack enable && pnpm install
@cd frontend && corepack enable && pnpm install && pnpm run make-i18n
# Start backend
start-backend:

3
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# i18n translation files make by script using `make build`
public/locales/**/*
src/i18n/declaration.ts

View File

@ -1 +1,2 @@
public-hoist-pattern[]=*@nextui-org/*
enable-pre-post-scripts=true

10020
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,8 +23,12 @@
"@xterm/xterm": "^5.4.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^11.0.24",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^2.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"react-redux": "^9.1.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.2.2",
@ -39,6 +43,8 @@
"build": "tsc && vite build",
"test": "jest",
"preview": "vite preview",
"make-i18n": "node scripts/make-i18n-translations.cjs",
"prelint": "pnpm run make-i18n",
"lint": "eslint src/**/*.ts* && prettier --check src/**/*.ts*",
"prepare": "cd .. && husky install frontend/.husky"
},

View File

@ -53,12 +53,24 @@ dependencies:
framer-motion:
specifier: ^11.0.24
version: 11.0.24(react-dom@18.2.0)(react@18.2.0)
i18next:
specifier: ^23.10.1
version: 23.10.1
i18next-browser-languagedetector:
specifier: ^7.2.1
version: 7.2.1
i18next-http-backend:
specifier: ^2.5.0
version: 2.5.0
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-i18next:
specifier: ^14.1.0
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
react-redux:
specifier: ^9.1.0
version: 9.1.0(@types/react@18.2.74)(react@18.2.0)(redux@5.0.1)
@ -4465,6 +4477,14 @@ packages:
- ts-node
dev: true
/cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dev: false
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -5590,6 +5610,12 @@ packages:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
dev: true
/html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
dependencies:
void-elements: 3.1.0
dev: false
/http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
@ -5627,6 +5653,26 @@ packages:
hasBin: true
dev: true
/i18next-browser-languagedetector@7.2.1:
resolution: {integrity: sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==}
dependencies:
'@babel/runtime': 7.24.1
dev: false
/i18next-http-backend@2.5.0:
resolution: {integrity: sha512-Z/aQsGZk1gSxt2/DztXk92DuDD20J+rNudT7ZCdTrNOiK8uQppfvdjq9+DFQfpAnFPn3VZS+KQIr1S/W1KxhpQ==}
dependencies:
cross-fetch: 4.0.0
transitivePeerDependencies:
- encoding
dev: false
/i18next@23.10.1:
resolution: {integrity: sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==}
dependencies:
'@babel/runtime': 7.24.1
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -6806,6 +6852,18 @@ packages:
/natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
dev: true
@ -7229,6 +7287,26 @@ packages:
scheduler: 0.23.0
dev: false
/react-i18next@14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@babel/runtime': 7.24.1
html-parse-stringify: 3.0.1
i18next: 23.10.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
@ -7966,6 +8044,10 @@ packages:
url-parse: 1.5.10
dev: true
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/tr46@3.0.0:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
@ -8284,6 +8366,11 @@ packages:
fsevents: 2.3.3
dev: false
/void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
dev: false
/w3c-xmlserializer@4.0.0:
resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==}
engines: {node: '>=14'}
@ -8301,6 +8388,10 @@ packages:
resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@ -8326,6 +8417,13 @@ packages:
webidl-conversions: 7.0.0
dev: true
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false
/which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:

View File

@ -0,0 +1,42 @@
const fs = require("fs");
const path = require("path");
const i18n = require("../src/i18n/translation.json");
// { [lang]: { [key]: content } }
const translationMap = {};
Object.entries(i18n).forEach(([key, transMap]) => {
Object.entries(transMap).forEach(([lang, content]) => {
if (!translationMap[lang]) {
translationMap[lang] = {};
}
translationMap[lang][key] = content;
})
});
// remove old locales directory
const localesPath = path.join(__dirname, "../public/locales");
if (fs.existsSync(localesPath)) {
fs.rmSync(localesPath, { recursive: true });
}
// write translation files
Object.entries(translationMap).forEach(([lang, transMap]) => {
const filePath = path.join(__dirname, `../public/locales/${lang}/translation.json`);
if (!fs.existsSync(filePath)) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(transMap, null, 2));
});
// write translation key enum
const transKeys = Object.keys(translationMap.en);
const transKeyDeclareFilePath = path.join(__dirname, "../src/i18n/declaration.ts");
if (!fs.existsSync(transKeyDeclareFilePath)) {
fs.mkdirSync(path.dirname(transKeyDeclareFilePath), { recursive: true });
}
fs.writeFileSync(transKeyDeclareFilePath, `
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
${transKeys.map(key => ` ${key} = "${key}",`).join('\n')}
}`.trim() + '\n');

View File

@ -1,6 +1,7 @@
import { Card, CardBody } from "@nextui-org/react";
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import assistantAvatar from "../assets/assistant-avatar.png";
import CogTooth from "../assets/cog-tooth";
import userAvatar from "../assets/user-avatar.png";
@ -14,6 +15,7 @@ import {
import { RootState } from "../store";
import { Message } from "../state/chatSlice";
import Input from "./Input";
import { I18nKey } from "../i18n/declaration";
interface IChatBubbleProps {
msg: Message;
@ -169,6 +171,8 @@ function MessageList(): JSX.Element {
}
function InitializingStatus(): JSX.Element {
const { t } = useTranslation();
return (
<div className="flex items-center m-auto h-full">
<img
@ -176,7 +180,7 @@ function InitializingStatus(): JSX.Element {
alt="assistant avatar"
className="w-[40px] h-[40px] mx-2.5"
/>
<div>Initializing agent (may take up to 10 seconds)...</div>
<div>{t(I18nKey.CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE)}</div>
</div>
);
}

View File

@ -2,11 +2,14 @@ import React, { ChangeEvent, useState, KeyboardEvent } from "react";
import { useSelector } from "react-redux";
import { Textarea } from "@nextui-org/react";
import { twMerge } from "tailwind-merge";
import { useTranslation } from "react-i18next";
import { RootState } from "../store";
import useInputComposition from "../hooks/useInputComposition";
import { sendChatMessage } from "../services/chatService";
import { I18nKey } from "../i18n/declaration";
function Input() {
const { t } = useTranslation();
const { initialized } = useSelector((state: RootState) => state.task);
const [inputMessage, setInputMessage] = useState("");
@ -54,7 +57,7 @@ function Input() {
onKeyDown={handleSendMessageOnEnter}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
placeholder="Send a message (won't interrupt the Assistant)"
placeholder={t(I18nKey.CHAT_INTERFACE$INPUT_PLACEHOLDER)}
/>
<button
type="button"
@ -65,7 +68,7 @@ function Input() {
onClick={handleSendMessage}
disabled={!initialized}
>
Send
{t(I18nKey.CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT)}
</button>
</div>
);

View File

@ -10,8 +10,12 @@ import {
Button,
Autocomplete,
AutocompleteItem,
Select,
SelectItem,
} from "@nextui-org/react";
import { KeyboardEvent } from "@react-types/shared/src/events";
import { useTranslation } from "react-i18next";
import i18next from "i18next";
import {
INITIAL_AGENTS,
fetchModels,
@ -24,9 +28,12 @@ import {
setModel,
setAgent,
setWorkspaceDirectory,
setLanguage,
} from "../state/settingsSlice";
import store, { RootState } from "../store";
import socket from "../socket/socket";
import { I18nKey } from "../i18n/declaration";
import { AvailableLanguages } from "../i18n";
interface Props {
isOpen: boolean;
@ -41,11 +48,13 @@ const cachedAgents = JSON.parse(
);
function SettingModal({ isOpen, onClose }: Props): JSX.Element {
const { t } = useTranslation();
const model = useSelector((state: RootState) => state.settings.model);
const agent = useSelector((state: RootState) => state.settings.agent);
const workspaceDirectory = useSelector(
(state: RootState) => state.settings.workspaceDirectory,
);
const language = useSelector((state: RootState) => state.settings.language);
const [supportedModels, setSupportedModels] = useState(
cachedModels.length > 0 ? cachedModels : INITIAL_MODELS,
@ -72,10 +81,12 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
}, []);
const handleSaveCfg = () => {
sendSettings(socket, { model, agent, workspaceDirectory });
sendSettings(socket, { model, agent, workspaceDirectory, language });
localStorage.setItem("model", model);
localStorage.setItem("workspaceDirectory", workspaceDirectory);
localStorage.setItem("agent", agent);
localStorage.setItem("language", language);
i18next.changeLanguage(language);
onClose();
};
@ -87,14 +98,18 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
<ModalContent>
<>
<ModalHeader className="flex flex-col gap-1">
Configuration
{t(I18nKey.CONFIGURATION$MODAL_TITLE)}
</ModalHeader>
<ModalBody>
<Input
type="text"
label="OpenDevin Workspace Directory"
label={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL,
)}
defaultValue={workspaceDirectory}
placeholder="Default: ./workspace"
placeholder={t(
I18nKey.CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER,
)}
onChange={(e) =>
store.dispatch(setWorkspaceDirectory(e.target.value))
}
@ -105,10 +120,9 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
label: v,
value: v,
}))}
label="Model"
placeholder="Select a model"
label={t(I18nKey.CONFIGURATION$MODEL_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER)}
selectedKey={model}
// className="max-w-xs"
onSelectionChange={(key) => {
store.dispatch(setModel(key as string));
}}
@ -127,10 +141,9 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
label: v,
value: v,
}))}
label="Agent"
placeholder="Select a agent"
label={t(I18nKey.CONFIGURATION$AGENT_SELECT_LABEL)}
placeholder={t(I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER)}
defaultSelectedKey={agent}
// className="max-w-xs"
onSelectionChange={(key) => {
store.dispatch(setAgent(key as string));
}}
@ -143,14 +156,28 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
</AutocompleteItem>
)}
</Autocomplete>
<Select
selectionMode="single"
onChange={(e) => {
store.dispatch(setLanguage(e.target.value));
}}
selectedKeys={[language]}
label={t(I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL)}
>
{AvailableLanguages.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
{lang.label}
</SelectItem>
))}
</Select>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
{t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL)}
</Button>
<Button color="primary" onPress={handleSaveCfg}>
Save
{t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL)}
</Button>
</ModalFooter>
</>

View File

@ -1,5 +1,6 @@
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { Tab, Tabs } from "@nextui-org/react";
import { useTranslation } from "react-i18next";
import Terminal from "./Terminal";
import Planner from "./Planner";
import CodeEditor from "./CodeEditor";
@ -9,37 +10,42 @@ import CmdLine from "../assets/cmd-line";
import Calendar from "../assets/calendar";
import Earth from "../assets/earth";
import Pencil from "../assets/pencil";
const tabData = {
[TabOption.TERMINAL]: {
name: "Terminal",
icon: <CmdLine />,
component: <Terminal key="terminal" />,
},
[TabOption.PLANNER]: {
name: "Planner",
icon: <Calendar />,
component: <Planner key="planner" />,
},
[TabOption.CODE]: {
name: "Code Editor",
icon: <Pencil />,
component: <CodeEditor key="code" />,
},
[TabOption.BROWSER]: {
name: "Browser",
icon: <Earth />,
component: <Browser key="browser" />,
},
};
import { I18nKey } from "../i18n/declaration";
function Workspace() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabType>(TabOption.TERMINAL);
const tabData = useMemo(
() => ({
[TabOption.TERMINAL]: {
name: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL),
icon: <CmdLine />,
component: <Terminal key="terminal" />,
},
[TabOption.PLANNER]: {
name: t(I18nKey.WORKSPACE$PLANNER_TAB_LABEL),
icon: <Calendar />,
component: <Planner key="planner" />,
},
[TabOption.CODE]: {
name: t(I18nKey.WORKSPACE$CODE_EDITOR_TAB_LABEL),
icon: <Pencil />,
component: <CodeEditor key="code" />,
},
[TabOption.BROWSER]: {
name: t(I18nKey.WORKSPACE$BROWSER_TAB_LABEL),
icon: <Earth />,
component: <Browser key="browser" />,
},
}),
[t],
);
return (
<>
<div className="w-full p-4 text-2xl font-bold select-none">
OpenDevin Workspace
{t(I18nKey.WORKSPACE$TITLE)}
</div>
<div role="tablist" className="tabs tabs-bordered tabs-lg ">
<Tabs

View File

@ -0,0 +1,61 @@
import i18n from "i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
export const AvailableLanguages = [
{ label: "English", value: "en" },
{ label: "简体中文", value: "zh-CN" },
];
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: "en",
debug: process.env.NODE_ENV === "development",
})
.then(() => {
// assume all detected languages are available
const detectLanguage = i18n.language;
// cannot trust browser language setting
const settingLanguage = localStorage.getItem("language");
// if setting is not initialized, but detected language is available, use detected language and update language setting
if (
!settingLanguage &&
AvailableLanguages.some((lang) => detectLanguage === lang.value)
) {
localStorage.setItem("language", detectLanguage);
i18n.changeLanguage(detectLanguage);
return;
}
// if setting is not initialized and detected language is not available, use en and update language setting
if (
!settingLanguage &&
!AvailableLanguages.some((lang) => detectLanguage === lang.value)
) {
localStorage.setItem("language", "en");
i18n.changeLanguage("en");
return;
}
// if setting is initialized and setting language is not available, use en and update language setting
if (
settingLanguage &&
!AvailableLanguages.some((lang) => settingLanguage === lang.value)
) {
localStorage.setItem("language", "en");
i18n.changeLanguage("en");
return;
}
// if setting is initialized and setting language is available, use setting language
if (settingLanguage && settingLanguage !== detectLanguage) {
i18n.changeLanguage(settingLanguage);
}
});
export default i18n;

View File

@ -0,0 +1,74 @@
{
"WORKSPACE$TITLE": {
"en": "OpenDevin Workspace",
"zh-CN": "OpenDevin 工作区"
},
"WORKSPACE$TERMINAL_TAB_LABEL": {
"en": "Terminal",
"zh-CN": "终端"
},
"WORKSPACE$PLANNER_TAB_LABEL": {
"en": "Planner",
"zh-CN": "规划器"
},
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
"en": "Code Editor",
"zh-CN": "代码编辑器"
},
"WORKSPACE$BROWSER_TAB_LABEL": {
"en": "Browser",
"zh-CN": "浏览器"
},
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_LABEL": {
"en": "OpenDevin Workspace directory",
"zh-CN": "OpenDevin 工作区目录"
},
"CONFIGURATION$OPENDEVIN_WORKSPACE_DIRECTORY_INPUT_PLACEHOLDER": {
"en": "Default: ./workspace",
"zh-CN": "默认:./workspace"
},
"CONFIGURATION$MODAL_TITLE": {
"en": "Configuration",
"zh-CN": "配置"
},
"CONFIGURATION$MODEL_SELECT_LABEL": {
"en": "Model",
"zh-CN": "模型"
},
"CONFIGURATION$MODEL_SELECT_PLACEHOLDER": {
"en": "Select a model",
"zh-CN": "选择一个模型"
},
"CONFIGURATION$AGENT_SELECT_LABEL": {
"en": "Agent",
"zh-CN": "代理"
},
"CONFIGURATION$AGENT_SELECT_PLACEHOLDER": {
"en": "Select a agent",
"zh-CN": "选择一个代理"
},
"CONFIGURATION$LANGUAGE_SELECT_LABEL": {
"en": "Language",
"zh-CN": "语言"
},
"CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL": {
"en": "Close",
"zh-CN": "关闭"
},
"CONFIGURATION$MODAL_SAVE_BUTTON_LABEL": {
"en": "Save",
"zh-CN": "保存"
},
"CHAT_INTERFACE$INITIALZING_AGENT_LOADING_MESSAGE": {
"en": "Initializing agent (may take up to 10 seconds)...",
"zh-CN": "初始化代理(可能需要 10 秒以上时间)"
},
"CHAT_INTERFACE$INPUT_PLACEHOLDER": {
"en": "Send a message (won't interrupt the Assistant)",
"zh-CN": "发送消息(不会打断助理)"
},
"CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
"en": "Send",
"zh-CN": "发送"
}
}

View File

@ -7,6 +7,7 @@ import { NextUIProvider } from "@nextui-org/react";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import store from "./store";
import "./i18n";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,

View File

@ -7,6 +7,7 @@ export const settingsSlice = createSlice({
agent: localStorage.getItem("agent") || "MonologueAgent",
workspaceDirectory:
localStorage.getItem("workspaceDirectory") || "./workspace",
language: localStorage.getItem("language") || "en",
},
reducers: {
setModel: (state, action) => {
@ -18,10 +19,13 @@ export const settingsSlice = createSlice({
setWorkspaceDirectory: (state, action) => {
state.workspaceDirectory = action.payload;
},
setLanguage: (state, action) => {
state.language = action.payload;
},
},
});
export const { setModel, setAgent, setWorkspaceDirectory } =
export const { setModel, setAgent, setWorkspaceDirectory, setLanguage } =
settingsSlice.actions;
export default settingsSlice.reducer;