From 610c87ce19e1238070b24050196680cb8e741422 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Wed, 19 Jun 2024 14:48:19 -0700 Subject: [PATCH] Init support of i18n and English, Mandarin, Spanish, French (#1317) * Init support of i18n and English and mandarin * Update common.js (#1320) * add General Appearance and Chat setting zh translate (#1414) * add config zh translate (#1461) * patch some translation pages * Update locality fixes * update: complete login page Mandarin translation. (#1709) update: complete Mandarin translation. * complete translation * update github to run validator * bump to test workflow failure * bump to fix tests * update workflow * refactor lang selector support * add Spanish and French * add dictionaries --------- Co-authored-by: GetOffer.help <13744916+getofferhelp@users.noreply.github.com> Co-authored-by: AIR <129256286+KochabStar@users.noreply.github.com> Co-authored-by: Ezio T <ezio5600@gmail.com> --- .github/workflows/check-translations.yaml | 37 ++ frontend/package.json | 5 +- frontend/src/App.jsx | 210 ++++---- .../components/EditingChatBubble/index.jsx | 4 +- .../NativeEmbeddingOptions/index.jsx | 6 +- .../Modals/Password/MultiUserAuth.jsx | 18 +- .../Modals/Password/SingleUserAuth.jsx | 7 +- .../src/components/SettingsSidebar/index.jsx | 44 +- .../NativeTranscriptionOptions/index.jsx | 29 +- .../UserMenu/AccountModal/index.jsx | 36 ++ .../LanceDBOptions/index.jsx | 4 +- frontend/src/hooks/useLanguageOptions.js | 20 + frontend/src/i18n.js | 21 + frontend/src/locales/en/common.js | 448 +++++++++++++++++ frontend/src/locales/es/common.js | 440 +++++++++++++++++ frontend/src/locales/fr/common.js | 456 ++++++++++++++++++ frontend/src/locales/resources.js | 32 ++ frontend/src/locales/verifyTranslations.mjs | 96 ++++ frontend/src/locales/zh/common.js | 424 ++++++++++++++++ frontend/src/pages/Admin/Logging/index.jsx | 20 +- .../Workspaces/NewWorkspaceModal/index.jsx | 4 +- .../pages/GeneralSettings/ApiKeys/index.jsx | 23 +- .../Appearance/CustomLogo/index.jsx | 14 +- .../Appearance/CustomMessages/index.jsx | 25 +- .../Appearance/FooterCustomization/index.jsx | 11 +- .../Appearance/LanguagePreference/index.jsx | 40 ++ .../GeneralSettings/Appearance/index.jsx | 8 +- .../src/pages/GeneralSettings/Chats/index.jsx | 23 +- .../GeneralSettings/EmbedChats/index.jsx | 22 +- .../GeneralSettings/EmbedConfigs/index.jsx | 19 +- .../EmbeddingPreference/index.jsx | 16 +- .../EmbeddingTextSplitterPreference/index.jsx | 29 +- .../GeneralSettings/LLMPreference/index.jsx | 11 +- .../GeneralSettings/PrivacyAndData/index.jsx | 22 +- .../pages/GeneralSettings/Security/index.jsx | 37 +- .../TranscriptionPreference/index.jsx | 11 +- .../GeneralSettings/VectorDatabase/index.jsx | 12 +- .../Steps/CreateWorkspace/index.jsx | 4 +- .../AgentConfig/AgentLLMSelection/index.jsx | 14 +- .../AgentConfig/AgentModelSelection/index.jsx | 15 +- .../AgentConfig/WebSearchSelection/index.jsx | 204 ++++++++ .../ChatHistorySettings/index.jsx | 12 +- .../ChatSettings/ChatModeSelection/index.jsx | 23 +- .../ChatSettings/ChatModelSelection/index.jsx | 13 +- .../ChatSettings/ChatPromptSettings/index.jsx | 10 +- .../ChatQueryRefusalResponse/index.jsx | 13 +- .../ChatTemperatureSettings/index.jsx | 15 +- .../WorkspaceLLMSelection/index.jsx | 10 +- .../DeleteWorkspace/index.jsx | 9 +- .../SuggestedChatMessages/index.jsx | 24 +- .../GeneralAppearance/WorkspaceName/index.jsx | 7 +- .../GeneralAppearance/WorkspacePfp/index.jsx | 11 +- .../DocumentSimilarityThreshold/index.jsx | 17 +- .../MaxContextSnippets/index.jsx | 10 +- .../VectorDatabase/ResetDatabase/index.jsx | 34 +- .../VectorDatabase/VectorCount/index.jsx | 8 +- .../VectorDBIdentifier/index.jsx | 5 +- .../src/pages/WorkspaceSettings/index.jsx | 12 +- frontend/yarn.lock | 41 ++ package.json | 3 +- 60 files changed, 2786 insertions(+), 412 deletions(-) create mode 100644 .github/workflows/check-translations.yaml create mode 100644 frontend/src/hooks/useLanguageOptions.js create mode 100644 frontend/src/i18n.js create mode 100644 frontend/src/locales/en/common.js create mode 100644 frontend/src/locales/es/common.js create mode 100644 frontend/src/locales/fr/common.js create mode 100644 frontend/src/locales/resources.js create mode 100644 frontend/src/locales/verifyTranslations.mjs create mode 100644 frontend/src/locales/zh/common.js create mode 100644 frontend/src/pages/GeneralSettings/Appearance/LanguagePreference/index.jsx create mode 100644 frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx diff --git a/.github/workflows/check-translations.yaml b/.github/workflows/check-translations.yaml new file mode 100644 index 000000000..1dae48814 --- /dev/null +++ b/.github/workflows/check-translations.yaml @@ -0,0 +1,37 @@ +# This Github action is for validation of all languages which translations are offered for +# in the locales folder in `frontend/src`. All languages are compared to the EN translation +# schema since that is the fallback language setting. This workflow will run on all PRs that +# modify any files in the translation directory +name: Verify translations files + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "frontend/src/locales/**.js" + +jobs: + run-script: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + + - name: Run verifyTranslations.mjs script + run: | + cd frontend/src/locales + node verifyTranslations.mjs + + - name: Fail job on error + if: failure() + run: exit 1 diff --git a/frontend/package.json b/frontend/package.json index 84c27166a..e584d9a39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,8 @@ "file-saver": "^2.0.5", "he": "^1.2.0", "highlight.js": "^11.9.0", + "i18next": "^23.11.3", + "i18next-browser-languagedetector": "^7.2.1", "js-levenshtein": "^1.1.6", "lodash.debounce": "^4.0.8", "markdown-it": "^13.0.1", @@ -27,6 +29,7 @@ "react-device-detect": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-i18next": "^14.1.1", "react-loading-skeleton": "^3.1.0", "react-router-dom": "^6.3.0", "react-speech-recognition": "^3.10.0", @@ -64,4 +67,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 627b8341e..40d5c9c98 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import React, { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; +import { I18nextProvider } from "react-i18next"; import { ContextWrapper } from "@/AuthContext"; import PrivateRoute, { AdminRoute, @@ -9,6 +10,7 @@ import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import Login from "@/pages/Login"; import OnboardingFlow from "@/pages/OnboardingFlow"; +import i18n from "./i18n"; import { PfpProvider } from "./PfpContext"; import { LogoProvider } from "./LogoContext"; @@ -61,109 +63,113 @@ export default function App() { <ContextWrapper> <LogoProvider> <PfpProvider> - <Routes> - <Route path="/" element={<PrivateRoute Component={Main} />} /> - <Route path="/login" element={<Login />} /> - <Route - path="/workspace/:slug/settings/:tab" - element={<ManagerRoute Component={WorkspaceSettings} />} - /> - <Route - path="/workspace/:slug" - element={<PrivateRoute Component={WorkspaceChat} />} - /> - <Route - path="/workspace/:slug/t/:threadSlug" - element={<PrivateRoute Component={WorkspaceChat} />} - /> - <Route path="/accept-invite/:code" element={<InvitePage />} /> + <I18nextProvider i18n={i18n}> + <Routes> + <Route path="/" element={<PrivateRoute Component={Main} />} /> + <Route path="/login" element={<Login />} /> + <Route + path="/workspace/:slug/settings/:tab" + element={<ManagerRoute Component={WorkspaceSettings} />} + /> + <Route + path="/workspace/:slug" + element={<PrivateRoute Component={WorkspaceChat} />} + /> + <Route + path="/workspace/:slug/t/:threadSlug" + element={<PrivateRoute Component={WorkspaceChat} />} + /> + <Route path="/accept-invite/:code" element={<InvitePage />} /> - {/* Admin */} - <Route - path="/settings/llm-preference" - element={<AdminRoute Component={GeneralLLMPreference} />} - /> - <Route - path="/settings/transcription-preference" - element={ - <AdminRoute Component={GeneralTranscriptionPreference} /> - } - /> - <Route - path="/settings/audio-preference" - element={<AdminRoute Component={GeneralAudioPreference} />} - /> - <Route - path="/settings/embedding-preference" - element={<AdminRoute Component={GeneralEmbeddingPreference} />} - /> - <Route - path="/settings/text-splitter-preference" - element={ - <AdminRoute Component={EmbeddingTextSplitterPreference} /> - } - /> - <Route - path="/settings/vector-database" - element={<AdminRoute Component={GeneralVectorDatabase} />} - /> - <Route - path="/settings/agents" - element={<AdminRoute Component={AdminAgents} />} - /> - <Route - path="/settings/event-logs" - element={<AdminRoute Component={AdminLogs} />} - /> - <Route - path="/settings/embed-config" - element={<AdminRoute Component={EmbedConfigSetup} />} - /> - <Route - path="/settings/embed-chats" - element={<AdminRoute Component={EmbedChats} />} - /> - {/* Manager */} - <Route - path="/settings/security" - element={<ManagerRoute Component={GeneralSecurity} />} - /> - <Route - path="/settings/privacy" - element={<AdminRoute Component={PrivacyAndData} />} - /> - <Route - path="/settings/appearance" - element={<ManagerRoute Component={GeneralAppearance} />} - /> - <Route - path="/settings/api-keys" - element={<AdminRoute Component={GeneralApiKeys} />} - /> - <Route - path="/settings/workspace-chats" - element={<ManagerRoute Component={GeneralChats} />} - /> - <Route - path="/settings/system-preferences" - element={<ManagerRoute Component={AdminSystem} />} - /> - <Route - path="/settings/invites" - element={<ManagerRoute Component={AdminInvites} />} - /> - <Route - path="/settings/users" - element={<ManagerRoute Component={AdminUsers} />} - /> - <Route - path="/settings/workspaces" - element={<ManagerRoute Component={AdminWorkspaces} />} - /> - {/* Onboarding Flow */} - <Route path="/onboarding" element={<OnboardingFlow />} /> - <Route path="/onboarding/:step" element={<OnboardingFlow />} /> - </Routes> + {/* Admin */} + <Route + path="/settings/llm-preference" + element={<AdminRoute Component={GeneralLLMPreference} />} + /> + <Route + path="/settings/transcription-preference" + element={ + <AdminRoute Component={GeneralTranscriptionPreference} /> + } + /> + <Route + path="/settings/audio-preference" + element={<AdminRoute Component={GeneralAudioPreference} />} + /> + <Route + path="/settings/embedding-preference" + element={ + <AdminRoute Component={GeneralEmbeddingPreference} /> + } + /> + <Route + path="/settings/text-splitter-preference" + element={ + <AdminRoute Component={EmbeddingTextSplitterPreference} /> + } + /> + <Route + path="/settings/vector-database" + element={<AdminRoute Component={GeneralVectorDatabase} />} + /> + <Route + path="/settings/agents" + element={<AdminRoute Component={AdminAgents} />} + /> + <Route + path="/settings/event-logs" + element={<AdminRoute Component={AdminLogs} />} + /> + <Route + path="/settings/embed-config" + element={<AdminRoute Component={EmbedConfigSetup} />} + /> + <Route + path="/settings/embed-chats" + element={<AdminRoute Component={EmbedChats} />} + /> + {/* Manager */} + <Route + path="/settings/security" + element={<ManagerRoute Component={GeneralSecurity} />} + /> + <Route + path="/settings/privacy" + element={<AdminRoute Component={PrivacyAndData} />} + /> + <Route + path="/settings/appearance" + element={<ManagerRoute Component={GeneralAppearance} />} + /> + <Route + path="/settings/api-keys" + element={<AdminRoute Component={GeneralApiKeys} />} + /> + <Route + path="/settings/workspace-chats" + element={<ManagerRoute Component={GeneralChats} />} + /> + <Route + path="/settings/system-preferences" + element={<ManagerRoute Component={AdminSystem} />} + /> + <Route + path="/settings/invites" + element={<ManagerRoute Component={AdminInvites} />} + /> + <Route + path="/settings/users" + element={<ManagerRoute Component={AdminUsers} />} + /> + <Route + path="/settings/workspaces" + element={<ManagerRoute Component={AdminWorkspaces} />} + /> + {/* Onboarding Flow */} + <Route path="/onboarding" element={<OnboardingFlow />} /> + <Route path="/onboarding/:step" element={<OnboardingFlow />} /> + </Routes> + </I18nextProvider> <ToastContainer /> </PfpProvider> </LogoProvider> diff --git a/frontend/src/components/EditingChatBubble/index.jsx b/frontend/src/components/EditingChatBubble/index.jsx index 0aa2a0784..38eeb4e83 100644 --- a/frontend/src/components/EditingChatBubble/index.jsx +++ b/frontend/src/components/EditingChatBubble/index.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { X } from "@phosphor-icons/react"; +import { useTranslation } from "react-i18next"; export default function EditingChatBubble({ message, @@ -11,11 +12,12 @@ export default function EditingChatBubble({ const [isEditing, setIsEditing] = useState(false); const [tempMessage, setTempMessage] = useState(message[type]); const isUser = type === "user"; + const { t } = useTranslation(); return ( <div> <p className={`text-xs text-[#D3D4D4] ${isUser ? "text-right" : ""}`}> - {isUser ? "User" : "AnythingLLM Chat Assistant"} + {isUser ? t("common.user") : t("appearance.message.assistant")} </p> <div className={`relative flex w-full mt-2 items-start ${ diff --git a/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx b/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx index 1cbfc689f..e3f974b8b 100644 --- a/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx @@ -1,9 +1,11 @@ +import { useTranslation } from "react-i18next"; + export default function NativeEmbeddingOptions() { + const { t } = useTranslation(); return ( <div className="w-full h-10 items-center flex"> <p className="text-sm font-base text-white text-opacity-60"> - There is no set up required when using AnythingLLM's native embedding - engine. + {t("embedding.provider.description")} </p> </div> ); diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx index 66b40e064..c9a6536e4 100644 --- a/frontend/src/components/Modals/Password/MultiUserAuth.jsx +++ b/frontend/src/components/Modals/Password/MultiUserAuth.jsx @@ -6,6 +6,7 @@ import showToast from "@/utils/toast"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; +import { useTranslation } from "react-i18next"; const RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => { const [username, setUsername] = useState(""); @@ -160,6 +161,7 @@ const ResetPasswordForm = ({ onSubmit }) => { }; export default function MultiUserAuth() { + const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [recoveryCodes, setRecoveryCodes] = useState([]); @@ -279,14 +281,15 @@ export default function MultiUserAuth() { <div className="flex items-center flex-col gap-y-4"> <div className="flex gap-x-1"> <h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block"> - Welcome to + {t("login.multi-user.welcome")} </h3> <p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent"> {customAppName || "AnythingLLM"} </p> </div> <p className="text-sm text-white/90 text-center"> - Sign in to your {customAppName || "AnythingLLM"} account. + {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "} + {t("login.sign-in.end")} </p> </div> </div> @@ -296,7 +299,7 @@ export default function MultiUserAuth() { <input name="username" type="text" - placeholder="Username" + placeholder={t("login.multi-user.placeholder-username")} className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]" required={true} autoComplete="off" @@ -306,7 +309,7 @@ export default function MultiUserAuth() { <input name="password" type="password" - placeholder="Password" + placeholder={t("login.multi-user.placeholder-password")} className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-full h-[48px] md:w-[300px] md:h-[34px]" required={true} autoComplete="off" @@ -321,14 +324,17 @@ export default function MultiUserAuth() { type="submit" className="md:text-primary-button md:bg-transparent text-dark-text text-sm font-bold focus:ring-4 focus:outline-none rounded-md border-[1.5px] border-[#46C8FF] md:h-[34px] h-[48px] md:hover:text-white md:hover:bg-primary-button bg-primary-button focus:z-10 w-full" > - {loading ? "Validating..." : "Login"} + {loading + ? t("login.multi-user.validating") + : t("login.multi-user.login")} </button> <button type="button" className="text-white text-sm flex gap-x-1 hover:text-primary-button hover:underline" onClick={handleResetPassword} > - Forgot password?<b>Reset</b> + {t("login.multi-user.forgot-pass")}? + <b>{t("login.multi-user.reset")}</b> </button> </div> </div> diff --git a/frontend/src/components/Modals/Password/SingleUserAuth.jsx b/frontend/src/components/Modals/Password/SingleUserAuth.jsx index f976c6344..66dcdb8f3 100644 --- a/frontend/src/components/Modals/Password/SingleUserAuth.jsx +++ b/frontend/src/components/Modals/Password/SingleUserAuth.jsx @@ -5,8 +5,10 @@ import paths from "../../../utils/paths"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal"; +import { useTranslation } from "react-i18next"; export default function SingleUserAuth() { + const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [recoveryCodes, setRecoveryCodes] = useState([]); @@ -73,14 +75,15 @@ export default function SingleUserAuth() { <div className="flex items-center flex-col gap-y-4"> <div className="flex gap-x-1"> <h3 className="text-md md:text-2xl font-bold text-white text-center white-space-nowrap hidden md:block"> - Welcome to + {t("login.multi-user.welcome")} </h3> <p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent"> {customAppName || "AnythingLLM"} </p> </div> <p className="text-sm text-white/90 text-center"> - Sign in to your {customAppName || "AnythingLLM"} instance. + {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "} + {t("login.sign-in.end")} </p> </div> </div> diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 6049d83fb..9dada9537 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -29,8 +29,10 @@ import { USER_BACKGROUND_COLOR } from "@/utils/constants"; import { isMobile } from "react-device-detect"; import Footer from "../Footer"; import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; export default function SettingsSidebar() { + const { t } = useTranslation(); const { logo } = useLogo(); const { user } = useUser(); const sidebarRef = useRef(null); @@ -113,7 +115,7 @@ export default function SettingsSidebar() { <div className="h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll"> <div className="h-auto md:sidebar-items md:dark:sidebar-items"> <div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll"> - <SidebarOptions user={user} /> + <SidebarOptions user={user} t={t} /> </div> </div> </div> @@ -146,12 +148,12 @@ export default function SettingsSidebar() { > <div className="w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]"> <div className="text-white text-opacity-60 text-sm font-medium uppercase mt-[4px] mb-0 ml-2"> - Instance Settings + {t("settings.title")} </div> <div className="relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll"> <div className="h-auto sidebar-items"> <div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll"> - <SidebarOptions user={user} /> + <SidebarOptions user={user} t={t} /> </div> </div> </div> @@ -221,39 +223,39 @@ const Option = ({ ); }; -const SidebarOptions = ({ user = null }) => ( +const SidebarOptions = ({ user = null, t }) => ( <> <Option href={paths.settings.system()} - btnText="System Preferences" + btnText={t("settings.system")} icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} user={user} allowedRole={["admin", "manager"]} /> <Option href={paths.settings.invites()} - btnText="Invitation" + btnText={t("settings.invites")} icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} user={user} allowedRole={["admin", "manager"]} /> <Option href={paths.settings.users()} - btnText="Users" + btnText={t("settings.users")} icon={<Users className="h-5 w-5 flex-shrink-0" />} user={user} allowedRole={["admin", "manager"]} /> <Option href={paths.settings.workspaces()} - btnText="Workspaces" + btnText={t("settings.workspaces")} icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} user={user} allowedRole={["admin", "manager"]} /> <Option href={paths.settings.chats()} - btnText="Workspace Chat" + btnText={t("settings.workspace-chats")} icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -270,7 +272,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.appearance()} - btnText="Appearance" + btnText={t("settings.appearance")} icon={<Eye className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -278,7 +280,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.apiKeys()} - btnText="API Keys" + btnText={t("settings.api-keys")} icon={<Key className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -286,7 +288,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.llmPreference()} - btnText="LLM Preference" + btnText={t("settings.llm")} icon={<ChatText className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -302,7 +304,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.transcriptionPreference()} - btnText="Transcription Model" + btnText={t("settings.transcription")} icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -311,7 +313,7 @@ const SidebarOptions = ({ user = null }) => ( <Option href={paths.settings.embedder.modelPreference()} childLinks={[paths.settings.embedder.chunkingPreference()]} - btnText="Embedder Preferences" + btnText={t("settings.embedder")} icon={<FileCode className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -320,7 +322,7 @@ const SidebarOptions = ({ user = null }) => ( <> <Option href={paths.settings.embedder.chunkingPreference()} - btnText="Text Splitter & Chunking" + btnText={t("settings.text-splitting")} icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -331,7 +333,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.vectorDatabase()} - btnText="Vector Database" + btnText={t("settings.vector-database")} icon={<Database className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -340,7 +342,7 @@ const SidebarOptions = ({ user = null }) => ( <Option href={paths.settings.embedSetup()} childLinks={[paths.settings.embedChats()]} - btnText="Chat Embed Widgets" + btnText={t("settings.embeds")} icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -349,7 +351,7 @@ const SidebarOptions = ({ user = null }) => ( <> <Option href={paths.settings.embedChats()} - btnText="Chat Embed History" + btnText={t("settings.embed-chats")} icon={<Barcode className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -360,7 +362,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.security()} - btnText="Security" + btnText={t("settings.security")} icon={<Lock className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -369,7 +371,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.logs()} - btnText="Event Logs" + btnText={t("settings.event-logs")} icon={<Notepad className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} @@ -377,7 +379,7 @@ const SidebarOptions = ({ user = null }) => ( /> <Option href={paths.settings.privacy()} - btnText="Privacy & Data" + btnText={t("settings.privacy")} icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} diff --git a/frontend/src/components/TranscriptionSelection/NativeTranscriptionOptions/index.jsx b/frontend/src/components/TranscriptionSelection/NativeTranscriptionOptions/index.jsx index d2e81a68a..1ffad8dde 100644 --- a/frontend/src/components/TranscriptionSelection/NativeTranscriptionOptions/index.jsx +++ b/frontend/src/components/TranscriptionSelection/NativeTranscriptionOptions/index.jsx @@ -1,7 +1,9 @@ -import { Gauge } from "@phosphor-icons/react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Gauge } from "@phosphor-icons/react"; export default function NativeTranscriptionOptions({ settings }) { + const { t } = useTranslation(); const [model, setModel] = useState(settings?.WhisperModelPref); return ( @@ -10,7 +12,7 @@ export default function NativeTranscriptionOptions({ settings }) { <div className="w-full flex items-center gap-4"> <div className="flex flex-col w-60"> <label className="text-white text-sm font-semibold block mb-4"> - Model Selection + {t("common.selection")} </label> <select name="WhisperModelPref" @@ -46,20 +48,19 @@ function LocalWarning({ model }) { } function WhisperSmall() { + const { t } = useTranslation(); + return ( <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="gap-x-2 flex items-center"> <Gauge size={25} /> <p className="text-sm"> - Running the <b>whisper-small</b> model on a machine with limited RAM - or CPU can stall AnythingLLM when processing media files. + {t("transcription.warn-start")} <br /> - We recommend at least 2GB of RAM and upload files <10Mb. + {t("transcription.warn-recommend")} <br /> <br /> - <i> - This model will automatically download on the first use. (250mb) - </i> + <i>{t("transcription.warn-end")} (250mb)</i> </p> </div> </div> @@ -67,21 +68,19 @@ function WhisperSmall() { } function WhisperLarge() { + const { t } = useTranslation(); + return ( <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="gap-x-2 flex items-center"> <Gauge size={25} /> <p className="text-sm"> - Using the <b>whisper-large</b> model on machines with limited RAM or - CPU can stall AnythingLLM when processing media files. This model is - substantially larger than the whisper-small. + {t("transcription.warn-start")} <br /> - We recommend at least 8GB of RAM and upload files <10Mb. + {t("transcription.warn-recommend")} <br /> <br /> - <i> - This model will automatically download on the first use. (1.56GB) - </i> + <i>{t("transcription.warn-end")} (1.56GB)</i> </p> </div> </div> diff --git a/frontend/src/components/UserMenu/AccountModal/index.jsx b/frontend/src/components/UserMenu/AccountModal/index.jsx index 39c67d5c8..cb39fdd59 100644 --- a/frontend/src/components/UserMenu/AccountModal/index.jsx +++ b/frontend/src/components/UserMenu/AccountModal/index.jsx @@ -1,3 +1,4 @@ +import { useLanguageOptions } from "@/hooks/useLanguageOptions"; import usePfp from "@/hooks/usePfp"; import System from "@/models/system"; import { AUTH_USER } from "@/utils/constants"; @@ -147,6 +148,7 @@ export default function AccountModal({ user, hideModal }) { placeholder={`${user.username}'s new password`} /> </div> + <LanguagePreference /> </div> <div className="flex justify-between items-center border-t border-gray-500/50 pt-4 p-6"> <button @@ -168,3 +170,37 @@ export default function AccountModal({ user, hideModal }) { </div> ); } + +function LanguagePreference() { + const { + currentLanguage, + supportedLanguages, + getLanguageName, + changeLanguage, + } = useLanguageOptions(); + + return ( + <div> + <label + htmlFor="userLang" + className="block mb-2 text-sm font-medium text-white" + > + Preferred language + </label> + <select + name="userLang" + className="bg-zinc-900 w-fit mt-2 px-4 border-gray-500 text-white text-sm rounded-lg block py-2" + defaultValue={currentLanguage || "en"} + onChange={(e) => changeLanguage(e.target.value)} + > + {supportedLanguages.map((lang) => { + return ( + <option key={lang} value={lang}> + {getLanguageName(lang)} + </option> + ); + })} + </select> + </div> + ); +} diff --git a/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx index e78f571a0..d8845bb25 100644 --- a/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx +++ b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx @@ -1,8 +1,10 @@ +import { useTranslation } from "react-i18next"; export default function LanceDBOptions() { + const { t } = useTranslation(); return ( <div className="w-full h-10 items-center flex"> <p className="text-sm font-base text-white text-opacity-60"> - There is no configuration needed for LanceDB. + {t("vector.provider.description")} </p> </div> ); diff --git a/frontend/src/hooks/useLanguageOptions.js b/frontend/src/hooks/useLanguageOptions.js new file mode 100644 index 000000000..2337afae7 --- /dev/null +++ b/frontend/src/hooks/useLanguageOptions.js @@ -0,0 +1,20 @@ +import i18n from "@/i18n"; +import { resources as languages } from "@/locales/resources"; + +export function useLanguageOptions() { + const supportedLanguages = Object.keys(languages); + const languageNames = new Intl.DisplayNames(supportedLanguages, { + type: "language", + }); + const changeLanguage = (newLang = "en") => { + if (!Object.keys(languages).includes(newLang)) return false; + i18n.changeLanguage(newLang); + }; + + return { + currentLanguage: i18n.language || "en", + supportedLanguages, + getLanguageName: (lang = "en") => languageNames.of(lang), + changeLanguage, + }; +} diff --git a/frontend/src/i18n.js b/frontend/src/i18n.js new file mode 100644 index 000000000..2f5ca580c --- /dev/null +++ b/frontend/src/i18n.js @@ -0,0 +1,21 @@ +import i18next from "i18next"; +import { initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { defaultNS, resources } from "./locales/resources"; + +i18next + // https://github.com/i18next/i18next-browser-languageDetector/blob/9efebe6ca0271c3797bc09b84babf1ba2d9b4dbb/src/index.js#L11 + .use(initReactI18next) // Initialize i18n for React + .use(LanguageDetector) + .init({ + fallbackLng: "en", + debug: true, + defaultNS, + resources, + lowerCaseLng: true, + interpolation: { + escapeValue: false, + }, + }); + +export default i18next; diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js new file mode 100644 index 000000000..5254e3bd7 --- /dev/null +++ b/frontend/src/locales/en/common.js @@ -0,0 +1,448 @@ +const TRANSLATIONS = { + common: { + "workspaces-name": "Workspaces Name", + error: "error", + success: "success", + user: "User", + selection: "Model Selection", + saving: "Saving...", + save: "Save changes", + previous: "Previous Page", + next: "Next Page", + }, + + // Setting Sidebar menu items. + settings: { + title: "Instance Settings", + system: "System Preferences", + invites: "Invitation", + users: "Users", + workspaces: "Workspaces", + "workspace-chats": "Workspace Chat", + appearance: "Appearance", + "api-keys": "API Keys", + llm: "LLM Preference", + transcription: "Transcription Model", + embedder: "Embedding Preferences", + "text-splitting": "Text Splitter & Chunking", + "vector-database": "Vector Database", + embeds: "Chat Embed Widgets", + "embed-chats": "Chat Embed History", + security: "Security", + "event-logs": "Event Logs", + privacy: "Privacy & Data", + }, + + // Page Definitions + login: { + "multi-user": { + welcome: "Welcome to", + "placeholder-username": "Username", + "placeholder-password": "Password", + login: "Login", + validating: "Validating...", + "forgot-pass": "Forgot password", + reset: "Reset", + }, + "sign-in": { + start: "Sign in to your", + end: "account.", + }, + }, + + // Workspace Settings menu items + "workspaces—settings": { + general: "General Settings", + chat: "Chat Settings", + vector: "Vector Database", + members: "Members", + agent: "Agent Configuration", + }, + + // General Appearance + general: { + vector: { + title: "Vector Count", + description: "Total number of vectors in your vector database.", + }, + names: { + description: "This will only change the display name of your workspace.", + }, + message: { + title: "Suggested Chat Messages", + description: + "Customize the messages that will be suggested to your workspace users.", + add: "Add new message", + save: "Save Messages", + heading: "Explain to me", + body: "the benefits of AnythingLLM", + }, + pfp: { + title: "Assistant Profile Image", + description: + "Customize the profile image of the assistant for this workspace.", + image: "Workspace Image", + remove: "Remove Workspace Image", + }, + delete: { + delete: "Delete Workspace", + deleting: "Deleting Workspace...", + "confirm-start": "You are about to delete your entire", + "confirm-end": + "workspace. This will remove all vector embeddings in your vector database.\n\nThe original source files will remain untouched. This action is irreversible.", + }, + }, + + // Chat Settings + chat: { + llm: { + title: "Workspace LLM Provider", + description: + "The specific LLM provider & model that will be used for this workspace. By default, it uses the system LLM provider and settings.", + search: "Search all LLM providers", + }, + model: { + title: "Workspace Chat model", + description: + "The specific chat model that will be used for this workspace. If empty, will use the system LLM preference.", + wait: "-- waiting for models --", + }, + mode: { + title: "Chat mode", + chat: { + title: "Chat", + "desc-start": "will provide answers with the LLM's general knowledge", + and: "and", + "desc-end": "document context that is found.", + }, + query: { + title: "Query", + "desc-start": "will provide answers", + only: "only", + "desc-end": "if document context is found.", + }, + }, + history: { + title: "Chat History", + "desc-start": + "The number of previous chats that will be included in the response's short-term memory.", + recommend: "Recommend 20. ", + "desc-end": + "AAnything more than 45 is likely to lead to continuous chat failures depending on message size.", + }, + prompt: { + title: "Prompt", + description: + "The prompt that will be used on this workspace. Define the context and instructions for the AI to generate a response. You should to provide a carefully crafted prompt so the AI can generate a relevant and accurate response.", + }, + refusal: { + title: "Query mode refusal response", + "desc-start": "When in", + query: "query", + "desc-end": + "mode, you may want to return a custom refusal response when no context is found.", + }, + temperature: { + title: "LLM Temperature", + "desc-start": + 'This setting controls how "creative" your LLM responses will be.', + "desc-end": + "The higher the number the more creative. For some models this can lead to incoherent responses when set too high.", + hint: "Most LLMs have various acceptable ranges of valid values. Consult your LLM provider for that information.", + }, + }, + + // Vector Database + "vector-workspace": { + identifier: "Vector database identifier", + snippets: { + title: "Max Context Snippets", + description: + "This setting controls the maximum amount of context snippets the will be sent to the LLM for per chat or query.", + recommend: "Recommended: 4", + }, + doc: { + title: "Document similarity threshold", + description: + "The minimum similarity score required for a source to be considered related to the chat. The higher the number, the more similar the source must be to the chat.", + zero: "No restriction", + low: "Low (similarity score ≥ .25)", + medium: "Medium (similarity score ≥ .50)", + high: "High (similarity score ≥ .75)", + }, + reset: { + reset: "Reset Vector Database", + resetting: "Clearing vectors...", + confirm: + "You are about to reset this workspace's vector database. This will remove all vector embeddings currently embedded.\n\nThe original source files will remain untouched. This action is irreversible.", + error: "Workspace vector database could not be reset!", + success: "Workspace vector database was reset!", + }, + }, + + // Agent Configuration + agent: { + "performance-warning": + "Performance of LLMs that do not explicitly support tool-calling is highly dependent on the model's capabilities and accuracy. Some abilities may be limited or non-functional.", + provider: { + title: "Workspace Agent LLM Provider", + description: + "The specific LLM provider & model that will be used for this workspace's @agent agent.", + }, + mode: { + chat: { + title: "Workspace Agent Chat model", + description: + "The specific chat model that will be used for this workspace's @agent agent.", + }, + title: "Workspace Agent model", + description: + "The specific LLM model that will be used for this workspace's @agent agent.", + wait: "-- waiting for models --", + }, + + skill: { + title: "Default agent skills", + description: + "Improve the natural abilities of the default agent with these pre-built skills. This set up applies to all workspaces.", + rag: { + title: "RAG & long-term memory", + description: + 'Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.', + }, + view: { + title: "View & summarize documents", + description: + "Allow the agent to list and summarize the content of workspace files currently embedded.", + }, + scrape: { + title: "Scrape websites", + description: + "Allow the agent to visit and scrape the content of websites.", + }, + generate: { + title: "Generate charts", + description: + "Enable the default agent to generate various types of charts from data provided or given in chat.", + }, + save: { + title: "Generate & save files to browser", + description: + "Enable the default agent to generate and write to files that save and can be downloaded in your browser.", + }, + web: { + title: "Live web search and browsing", + "desc-start": + "Enable your agent to search the web to answer your questions by connecting to a web-search (SERP) provider.", + "desc-end": + "Web search during agent sessions will not work until this is set up.", + }, + }, + }, + + // Workspace Chats + recorded: { + title: "Workspace Chats", + description: + "These are all the recorded chats and messages that have been sent by users ordered by their creation date.", + export: "Export", + table: { + id: "Id", + by: "Sent By", + workspace: "Workspace", + prompt: "Prompt", + response: "Response", + at: "Sent At", + }, + }, + + // Appearance + appearance: { + title: "Appearance", + description: "Customize the appearance settings of your platform.", + logo: { + title: "Customize Logo", + description: "Upload your custom logo to make your chatbot yours.", + add: "Add a custom logo", + recommended: "Recommended size: 800 x 200", + remove: "Remove", + replace: "Replace", + }, + message: { + title: "Customize Messages", + description: "Customize the automatic messages displayed to your users.", + new: "New", + system: "system", + user: "user", + message: "message", + assistant: "AnythingLLM Chat Assistant", + "double-click": "Double click to edit...", + save: "Save Messages", + }, + icons: { + title: "Custom Footer Icons", + description: + "Customize the footer icons displayed on the bottom of the sidebar.", + icon: "Icon", + link: "Link", + }, + }, + + // API Keys + api: { + title: "API Keys", + description: + "API keys allow the holder to programmatically access and manage this AnythingLLM instance.", + link: "Read the API documentation", + generate: "Generate New API Key", + table: { + key: "API Key", + by: "Created By", + created: "Created", + }, + }, + + llm: { + title: "LLM Preference", + description: + "These are the credentials and settings for your preferred LLM chat & embedding provider. Its important these keys are current and correct or else AnythingLLM will not function properly.", + provider: "LLM Provider", + }, + + transcription: { + title: "Transcription Model Preference", + description: + "These are the credentials and settings for your preferred transcription model provider. Its important these keys are current and correct or else media files and audio will not transcribe.", + provider: "Transcription Provider", + "warn-start": + "Using the local whisper model on machines with limited RAM or CPU can stall AnythingLLM when processing media files.", + "warn-recommend": + "We recommend at least 2GB of RAM and upload files <10Mb.", + "warn-end": + "The built-in model will automatically download on the first use.", + }, + + embedding: { + title: "Embedding Preference", + "desc-start": + "When using an LLM that does not natively support an embedding engine - you may need to additionally specify credentials to for embedding text.", + "desc-end": + "Embedding is the process of turning text into vectors. These credentials are required to turn your files and prompts into a format which AnythingLLM can use to process.", + provider: { + title: "Embedding Provider", + description: + "There is no set up required when using AnythingLLM's native embedding engine.", + }, + }, + + text: { + title: "Text splitting & Chunking Preferences", + "desc-start": + "Sometimes, you may want to change the default way that new documents are split and chunked before being inserted into your vector database.", + "desc-end": + "You should only modify this setting if you understand how text splitting works and it's side effects.", + "warn-start": "Changes here will only apply to", + "warn-center": "newly embedded documents", + "warn-end": ", not existing documents.", + size: { + title: "Text Chunk Size", + description: + "This is the maximum length of characters that can be present in a single vector.", + recommend: "Embed model maximum length is", + }, + + overlap: { + title: "Text Chunk Overlap", + description: + "This is the maximum overlap of characters that occurs during chunking between two adjacent text chunks.", + }, + }, + + // Vector Database + vector: { + title: "Vector Database", + description: + "These are the credentials and settings for how your AnythingLLM instance will function. It's important these keys are current and correct.", + provider: { + title: "Vector Database Provider", + description: "There is no configuration needed for LanceDB.", + }, + }, + + // Embeddable Chat Widgets + embeddable: { + title: "Embeddable Chat Widgets", + description: + "Embeddable chat widgets are public facing chat interfaces that are tied to a single workspace. These allow you to build workspaces that then you can publish to the world.", + create: "Create embed", + table: { + workspace: "Workspace", + chats: "Sent Chats", + Active: "Active Domains", + }, + }, + + "embed-chats": { + title: "Embed Chats", + description: + "These are all the recorded chats and messages from any embed that you have published.", + table: { + embed: "Embed", + sender: "Sender", + message: "Message", + response: "Response", + at: "Sent At", + }, + }, + + multi: { + title: "Multi-User Mode", + description: + "Set up your instance to support your team by activating Multi-User Mode.", + enable: { + "is-enable": "Multi-User Mode is Enabled", + enable: "Enable Multi-User Mode", + description: + "By default, you will be the only admin. As an admin you will need to create accounts for all new users or admins. Do not lose your password as only an Admin user can reset passwords.", + username: "Admin account username", + password: "Admin account password", + }, + password: { + title: "Password Protection", + description: + "Protect your AnythingLLM instance with a password. If you forget this there is no recovery method so ensure you save this password.", + }, + instance: { + title: "Password Protect Instance", + description: + "By default, you will be the only admin. As an admin you will need to create accounts for all new users or admins. Do not lose your password as only an Admin user can reset passwords.", + password: "Instance password", + }, + }, + + // Event Logs + event: { + title: "Event Logs", + description: + "View all actions and events happening on this instance for monitoring.", + clear: "Clear Event Logs", + table: { + type: "Event Type", + user: "User", + occurred: "Occurred At", + }, + }, + + // Privacy & Data-Handling + privacy: { + title: "Privacy & Data-Handling", + description: + "This is your configuration for how connected third party providers and AnythingLLM handle your data.", + llm: "LLM Selection", + embedding: "Embedding Preference", + vector: "Vector Database", + anonymous: "Anonymous Telemetry Enabled", + }, +}; + +export default TRANSLATIONS; diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js new file mode 100644 index 000000000..d0609e9a3 --- /dev/null +++ b/frontend/src/locales/es/common.js @@ -0,0 +1,440 @@ +const TRANSLATIONS = { + common: { + "workspaces-name": "Nombre de espacios de trabajo", + error: "error", + success: "éxito", + user: "Usuario", + selection: "Selección de modelo", + saving: "Guardando...", + save: "Guardar cambios", + previous: "Página anterior", + next: "Página siguiente", + }, + + settings: { + title: "Configuración de instancia", + system: "Preferencias del sistema", + invites: "Invitación", + users: "Usuarios", + workspaces: "Espacios de trabajo", + "workspace-chats": "Chat del espacio de trabajo", + appearance: "Apariencia", + "api-keys": "Claves API", + llm: "Preferencia de LLM", + transcription: "Modelo de transcripción", + embedder: "Preferencias de incrustación", + "text-splitting": "Divisor y fragmentación de texto", + "vector-database": "Base de datos de vectores", + embeds: "Widgets de chat incrustados", + "embed-chats": "Historial de chats incrustados", + security: "Seguridad", + "event-logs": "Registros de eventos", + privacy: "Privacidad y datos", + }, + + login: { + "multi-user": { + welcome: "Bienvenido a", + "placeholder-username": "Nombre de usuario", + "placeholder-password": "Contraseña", + login: "Iniciar sesión", + validating: "Validando...", + "forgot-pass": "Olvidé mi contraseña", + reset: "Restablecer", + }, + "sign-in": { + start: "Iniciar sesión en tu", + end: "cuenta.", + }, + }, + + "workspaces—settings": { + general: "Configuración general", + chat: "Configuración de chat", + vector: "Base de datos de vectores", + members: "Miembros", + agent: "Configuración del agente", + }, + + general: { + vector: { + title: "Conteo de vectores", + description: "Número total de vectores en tu base de datos de vectores.", + }, + names: { + description: + "Esto solo cambiará el nombre de visualización de tu espacio de trabajo.", + }, + message: { + title: "Mensajes de chat sugeridos", + description: + "Personaliza los mensajes que se sugerirán a los usuarios de tu espacio de trabajo.", + add: "Agregar nuevo mensaje", + save: "Guardar mensajes", + heading: "ExplÃcame", + body: "los beneficios de AnythingLLM", + }, + pfp: { + title: "Imagen de perfil del asistente", + description: + "Personaliza la imagen de perfil del asistente para este espacio de trabajo.", + image: "Imagen del espacio de trabajo", + remove: "Eliminar imagen del espacio de trabajo", + }, + delete: { + delete: "Eliminar espacio de trabajo", + deleting: "Eliminando espacio de trabajo...", + "confirm-start": "Estás a punto de eliminar tu", + "confirm-end": + "espacio de trabajo. Esto eliminará todas las incrustaciones de vectores en tu base de datos de vectores.\n\nLos archivos de origen originales permanecerán intactos. Esta acción es irreversible.", + }, + }, + + chat: { + llm: { + title: "Proveedor LLM del espacio de trabajo", + description: + "El proveedor y modelo LLM especÃfico que se utilizará para este espacio de trabajo. Por defecto, utiliza el proveedor y configuración del sistema LLM.", + search: "Buscar todos los proveedores LLM", + }, + model: { + title: "Modelo de chat del espacio de trabajo", + description: + "El modelo de chat especÃfico que se utilizará para este espacio de trabajo. Si está vacÃo, se utilizará la preferencia LLM del sistema.", + wait: "-- esperando modelos --", + }, + mode: { + title: "Modo de chat", + chat: { + title: "Chat", + "desc-start": + "proporcionará respuestas con el conocimiento general del LLM", + and: "y", + "desc-end": "el contexto del documento que se encuentre.", + }, + query: { + title: "Consulta", + "desc-start": "proporcionará respuestas", + only: "solo", + "desc-end": "si se encuentra el contexto del documento.", + }, + }, + history: { + title: "Historial de chat", + "desc-start": + "El número de chats anteriores que se incluirán en la memoria a corto plazo de la respuesta.", + recommend: "Recomendar 20. ", + "desc-end": + "Cualquier cosa más de 45 probablemente conducirá a fallos continuos en el chat dependiendo del tamaño del mensaje.", + }, + prompt: { + title: "Prompt", + description: + "El prompt que se utilizará en este espacio de trabajo. Define el contexto y las instrucciones para que la IA genere una respuesta. Debes proporcionar un prompt cuidadosamente elaborado para que la IA pueda generar una respuesta relevante y precisa.", + }, + refusal: { + title: "Respuesta de rechazo en modo consulta", + "desc-start": "Cuando esté en", + query: "consulta", + "desc-end": + "modo, es posible que desees devolver una respuesta de rechazo personalizada cuando no se encuentre contexto.", + }, + temperature: { + title: "Temperatura de LLM", + "desc-start": + 'Esta configuración controla cuán "creativas" serán las respuestas de tu LLM.', + "desc-end": + "Cuanto mayor sea el número, más creativas serán las respuestas. Para algunos modelos, esto puede llevar a respuestas incoherentes cuando se establece demasiado alto.", + hint: "La mayorÃa de los LLM tienen varios rangos aceptables de valores válidos. Consulta a tu proveedor de LLM para obtener esa información.", + }, + }, + + "vector-workspace": { + identifier: "Identificador de la base de datos de vectores", + snippets: { + title: "Máximo de fragmentos de contexto", + description: + "Esta configuración controla la cantidad máxima de fragmentos de contexto que se enviarán al LLM por chat o consulta.", + recommend: "Recomendado: 4", + }, + doc: { + title: "Umbral de similitud de documentos", + description: + "La puntuación mÃnima de similitud requerida para que una fuente se considere relacionada con el chat. Cuanto mayor sea el número, más similar debe ser la fuente al chat.", + zero: "Sin restricción", + low: "Bajo (puntuación de similitud ≥ .25)", + medium: "Medio (puntuación de similitud ≥ .50)", + high: "Alto (puntuación de similitud ≥ .75)", + }, + reset: { + reset: "Restablecer la base de datos de vectores", + resetting: "Borrando vectores...", + confirm: + "Estás a punto de restablecer la base de datos de vectores de este espacio de trabajo. Esto eliminará todas las incrustaciones de vectores actualmente incrustadas.\n\nLos archivos de origen originales permanecerán intactos. Esta acción es irreversible.", + error: + "¡No se pudo restablecer la base de datos de vectores del espacio de trabajo!", + success: + "¡La base de datos de vectores del espacio de trabajo fue restablecida!", + }, + }, + + agent: { + "performance-warning": + "El rendimiento de los LLM que no admiten explÃcitamente la llamada de herramientas depende en gran medida de las capacidades y la precisión del modelo. Algunas habilidades pueden estar limitadas o no funcionar.", + provider: { + title: "Proveedor de LLM del agente del espacio de trabajo", + description: + "El proveedor y modelo LLM especÃfico que se utilizará para el agente @agent de este espacio de trabajo.", + }, + mode: { + chat: { + title: "Modelo de chat del agente del espacio de trabajo", + description: + "El modelo de chat especÃfico que se utilizará para el agente @agent de este espacio de trabajo.", + }, + title: "Modelo del agente del espacio de trabajo", + description: + "El modelo LLM especÃfico que se utilizará para el agente @agent de este espacio de trabajo.", + wait: "-- esperando modelos --", + }, + + skill: { + title: "Habilidades predeterminadas del agente", + description: + "Mejora las habilidades naturales del agente predeterminado con estas habilidades preconstruidas. Esta configuración se aplica a todos los espacios de trabajo.", + rag: { + title: "RAG y memoria a largo plazo", + description: + 'Permitir que el agente aproveche tus documentos locales para responder a una consulta o pedirle al agente que "recuerde" piezas de contenido para la recuperación de memoria a largo plazo.', + }, + view: { + title: "Ver y resumir documentos", + description: + "Permitir que el agente enumere y resuma el contenido de los archivos del espacio de trabajo actualmente incrustados.", + }, + scrape: { + title: "Rastrear sitios web", + description: + "Permitir que el agente visite y rastree el contenido de sitios web.", + }, + generate: { + title: "Generar gráficos", + description: + "Habilitar al agente predeterminado para generar varios tipos de gráficos a partir de datos proporcionados o dados en el chat.", + }, + save: { + title: "Generar y guardar archivos en el navegador", + description: + "Habilitar al agente predeterminado para generar y escribir archivos que se guarden y puedan descargarse en tu navegador.", + }, + web: { + title: "Búsqueda en vivo en la web y navegación", + "desc-start": + "Permitir que tu agente busque en la web para responder tus preguntas conectándose a un proveedor de búsqueda en la web (SERP).", + "desc-end": + "La búsqueda en la web durante las sesiones del agente no funcionará hasta que esto esté configurado.", + }, + }, + }, + + recorded: { + title: "Chats del espacio de trabajo", + description: + "Estos son todos los chats y mensajes grabados que han sido enviados por los usuarios ordenados por su fecha de creación.", + export: "Exportar", + table: { + id: "Id", + by: "Enviado por", + workspace: "Espacio de trabajo", + prompt: "Prompt", + response: "Respuesta", + at: "Enviado a", + }, + }, + + appearance: { + title: "Apariencia", + description: "Personaliza la configuración de apariencia de tu plataforma.", + logo: { + title: "Personalizar logotipo", + description: + "Sube tu logotipo personalizado para hacer que tu chatbot sea tuyo.", + add: "Agregar un logotipo personalizado", + recommended: "Tamaño recomendado: 800 x 200", + remove: "Eliminar", + replace: "Reemplazar", + }, + message: { + title: "Personalizar mensajes", + description: + "Personaliza los mensajes automáticos que se muestran a tus usuarios.", + new: "Nuevo", + system: "sistema", + user: "usuario", + message: "mensaje", + assistant: "Asistente de chat AnythingLLM", + "double-click": "Haz doble clic para editar...", + save: "Guardar mensajes", + }, + icons: { + title: "Iconos de pie de página personalizados", + description: + "Personaliza los iconos de pie de página que se muestran en la parte inferior de la barra lateral.", + icon: "Icono", + link: "Enlace", + }, + }, + + api: { + title: "Claves API", + description: + "Las claves API permiten al titular acceder y gestionar programáticamente esta instancia de AnythingLLM.", + link: "Leer la documentación de la API", + generate: "Generar nueva clave API", + table: { + key: "Clave API", + by: "Creado por", + created: "Creado", + }, + }, + + llm: { + title: "Preferencia de LLM", + description: + "Estas son las credenciales y configuraciones para tu proveedor preferido de chat y incrustación de LLM. Es importante que estas claves estén actualizadas y correctas, de lo contrario AnythingLLM no funcionará correctamente.", + provider: "Proveedor de LLM", + }, + + transcription: { + title: "Preferencia de modelo de transcripción", + description: + "Estas son las credenciales y configuraciones para tu proveedor preferido de modelo de transcripción. Es importante que estas claves estén actualizadas y correctas, de lo contrario los archivos multimedia y de audio no se transcribirán.", + provider: "Proveedor de transcripción", + "warn-start": + "El uso del modelo local Whisper en máquinas con RAM o CPU limitadas puede bloquear AnythingLLM al procesar archivos multimedia.", + "warn-recommend": + "Recomendamos al menos 2GB de RAM y subir archivos <10Mb.", + "warn-end": + "El modelo incorporado se descargará automáticamente en el primer uso.", + }, + + embedding: { + title: "Preferencia de incrustación", + "desc-start": + "Cuando uses un LLM que no admita de forma nativa un motor de incrustación, es posible que necesites especificar credenciales adicionales para incrustar texto.", + "desc-end": + "La incrustación es el proceso de convertir texto en vectores. Estas credenciales son necesarias para convertir tus archivos y prompts en un formato que AnythingLLM pueda usar para procesar.", + provider: { + title: "Proveedor de incrustación", + description: + "No se requiere configuración cuando se utiliza el motor de incrustación nativo de AnythingLLM.", + }, + }, + + text: { + title: "Preferencias de división y fragmentación de texto", + "desc-start": + "A veces, es posible que desees cambiar la forma predeterminada en que los nuevos documentos se dividen y fragmentan antes de ser insertados en tu base de datos de vectores.", + "desc-end": + "Solo debes modificar esta configuración si entiendes cómo funciona la división de texto y sus efectos secundarios.", + "warn-start": "Los cambios aquà solo se aplicarán a", + "warn-center": "documentos recién incrustados", + "warn-end": ", no a los documentos existentes.", + size: { + title: "Tamaño del fragmento de texto", + description: + "Esta es la longitud máxima de caracteres que puede estar presente en un solo vector.", + recommend: "La longitud máxima del modelo de incrustación es", + }, + + overlap: { + title: "Superposición de fragmentos de texto", + description: + "Esta es la superposición máxima de caracteres que ocurre durante la fragmentación entre dos fragmentos de texto adyacentes.", + }, + }, + + vector: { + title: "Base de datos de vectores", + description: + "Estas son las credenciales y configuraciones para cómo funcionará tu instancia de AnythingLLM. Es importante que estas claves estén actualizadas y correctas.", + provider: { + title: "Proveedor de base de datos de vectores", + description: "No se necesita configuración para LanceDB.", + }, + }, + + embeddable: { + title: "Widgets de chat incrustables", + description: + "Los widgets de chat incrustables son interfaces de chat de cara al público que están vinculadas a un solo espacio de trabajo. Esto te permite crear espacios de trabajo que luego puedes publicar al mundo.", + create: "Crear incrustación", + table: { + workspace: "Espacio de trabajo", + chats: "Chats enviados", + Active: "Dominios activos", + }, + }, + + "embed-chats": { + title: "Incrustar chats", + description: + "Estos son todos los chats y mensajes grabados de cualquier incrustación que hayas publicado.", + table: { + embed: "Incrustar", + sender: "Remitente", + message: "Mensaje", + response: "Respuesta", + at: "Enviado a", + }, + }, + + multi: { + title: "Modo multiusuario", + description: + "Configura tu instancia para admitir a tu equipo activando el modo multiusuario.", + enable: { + "is-enable": "El modo multiusuario está habilitado", + enable: "Habilitar modo multiusuario", + description: + "Por defecto, serás el único administrador. Como administrador, necesitarás crear cuentas para todos los nuevos usuarios o administradores. No pierdas tu contraseña ya que solo un usuario administrador puede restablecer las contraseñas.", + username: "Nombre de usuario de la cuenta de administrador", + password: "Contraseña de la cuenta de administrador", + }, + password: { + title: "Protección con contraseña", + description: + "Protege tu instancia de AnythingLLM con una contraseña. Si olvidas esta contraseña, no hay método de recuperación, asà que asegúrate de guardar esta contraseña.", + }, + instance: { + title: "Proteger instancia con contraseña", + description: + "Por defecto, serás el único administrador. Como administrador, necesitarás crear cuentas para todos los nuevos usuarios o administradores. No pierdas tu contraseña ya que solo un usuario administrador puede restablecer las contraseñas.", + password: "Contraseña de la instancia", + }, + }, + + event: { + title: "Registros de eventos", + description: + "Ver todas las acciones y eventos que ocurren en esta instancia para monitoreo.", + clear: "Borrar registros de eventos", + table: { + type: "Tipo de evento", + user: "Usuario", + occurred: "Ocurrido a", + }, + }, + + privacy: { + title: "Privacidad y manejo de datos", + description: + "Esta es tu configuración para cómo los proveedores de terceros conectados y AnythingLLM manejan tus datos.", + llm: "Selección de LLM", + embedding: "Preferencia de incrustación", + vector: "Base de datos de vectores", + anonymous: "TelemetrÃa anónima habilitada", + }, +}; + +export default TRANSLATIONS; diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js new file mode 100644 index 000000000..9e7a82e81 --- /dev/null +++ b/frontend/src/locales/fr/common.js @@ -0,0 +1,456 @@ +const TRANSLATIONS = { + common: { + "workspaces-name": "Nom des espaces de travail", + error: "erreur", + success: "succès", + user: "Utilisateur", + selection: "Sélection du modèle", + saving: "Enregistrement...", + save: "Enregistrer les modifications", + previous: "Page précédente", + next: "Page suivante", + }, + + // Setting Sidebar menu items. + settings: { + title: "Paramètres de l'instance", + system: "Préférences système", + invites: "Invitation", + users: "Utilisateurs", + workspaces: "Espaces de travail", + "workspace-chats": "Chat de l'espace de travail", + appearance: "Apparence", + "api-keys": "Clés API", + llm: "Préférence LLM", + transcription: "Modèle de transcription", + embedder: "Préférences d'intégration", + "text-splitting": "Diviseur de texte et découpage", + "vector-database": "Base de données vectorielle", + embeds: "Widgets de chat intégrés", + "embed-chats": "Historique des chats intégrés", + security: "Sécurité", + "event-logs": "Journaux d'événements", + privacy: "Confidentialité et données", + }, + + // Page Definitions + login: { + "multi-user": { + welcome: "Bienvenue à ", + "placeholder-username": "Nom d'utilisateur", + "placeholder-password": "Mot de passe", + login: "Connexion", + validating: "Validation...", + "forgot-pass": "Mot de passe oublié", + reset: "Réinitialiser", + }, + "sign-in": { + start: "Connectez-vous à votre", + end: "compte.", + }, + }, + + // Workspace Settings menu items + "workspaces—settings": { + general: "Paramètres généraux", + chat: "Paramètres de chat", + vector: "Base de données vectorielle", + members: "Membres", + agent: "Configuration de l'agent", + }, + + // General Appearance + general: { + vector: { + title: "Nombre de vecteurs", + description: + "Nombre total de vecteurs dans votre base de données vectorielle.", + }, + names: { + description: + "Cela ne changera que le nom d'affichage de votre espace de travail.", + }, + message: { + title: "Messages de chat suggérés", + description: + "Personnalisez les messages qui seront suggérés aux utilisateurs de votre espace de travail.", + add: "Ajouter un nouveau message", + save: "Enregistrer les messages", + heading: "Expliquez-moi", + body: "les avantages de AnythingLLM", + }, + pfp: { + title: "Image de profil de l'assistant", + description: + "Personnalisez l'image de profil de l'assistant pour cet espace de travail.", + image: "Image de l'espace de travail", + remove: "Supprimer l'image de l'espace de travail", + }, + delete: { + delete: "Supprimer l'espace de travail", + deleting: "Suppression de l'espace de travail...", + "confirm-start": "Vous êtes sur le point de supprimer votre", + "confirm-end": + "espace de travail. Cela supprimera toutes les intégrations vectorielles dans votre base de données vectorielle.\n\nLes fichiers source originaux resteront intacts. Cette action est irréversible.", + }, + }, + + // Chat Settings + chat: { + llm: { + title: "Fournisseur LLM de l'espace de travail", + description: + "Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour cet espace de travail. Par défaut, il utilise le fournisseur et les paramètres LLM du système.", + search: "Rechercher tous les fournisseurs LLM", + }, + model: { + title: "Modèle de chat de l'espace de travail", + description: + "Le modèle de chat spécifique qui sera utilisé pour cet espace de travail. Si vide, utilisera la préférence LLM du système.", + wait: "-- en attente des modèles --", + }, + mode: { + title: "Mode de chat", + chat: { + title: "Chat", + "desc-start": + "fournira des réponses avec les connaissances générales du LLM", + and: "et", + "desc-end": "le contexte du document trouvé.", + }, + query: { + title: "Requête", + "desc-start": "fournira des réponses", + only: "uniquement", + "desc-end": "si un contexte de document est trouvé.", + }, + }, + history: { + title: "Historique des chats", + "desc-start": + "Le nombre de chats précédents qui seront inclus dans la mémoire à court terme de la réponse.", + recommend: "Recommandé: 20.", + "desc-end": + "Tout nombre supérieur à 45 risque de provoquer des échecs de chat continus en fonction de la taille du message.", + }, + prompt: { + title: "Invite", + description: + "L'invite qui sera utilisée sur cet espace de travail. Définissez le contexte et les instructions pour que l'IA génère une réponse. Vous devez fournir une invite soigneusement conçue pour que l'IA puisse générer une réponse pertinente et précise.", + }, + refusal: { + title: "Réponse de refus en mode requête", + "desc-start": "En mode", + query: "requête", + "desc-end": + ", vous pouvez souhaiter retourner une réponse de refus personnalisée lorsque aucun contexte n'est trouvé.", + }, + temperature: { + title: "Température LLM", + "desc-start": + "Ce paramètre contrôle le niveau de créativité des réponses de votre LLM.", + "desc-end": + "Plus le nombre est élevé, plus la réponse sera créative. Pour certains modèles, cela peut entraîner des réponses incohérentes si la valeur est trop élevée.", + hint: "La plupart des LLM ont diverses plages acceptables de valeurs valides. Consultez votre fournisseur LLM pour cette information.", + }, + }, + + // Vector Database + "vector-workspace": { + identifier: "Identifiant de la base de données vectorielle", + snippets: { + title: "Nombre maximum de contextes", + description: + "Ce paramètre contrôle le nombre maximum de contextes qui seront envoyés au LLM par chat ou requête.", + recommend: "Recommandé: 4", + }, + doc: { + title: "Seuil de similarité des documents", + description: + "Le score de similarité minimum requis pour qu'une source soit considérée comme liée au chat. Plus le nombre est élevé, plus la source doit être similaire au chat.", + zero: "Aucune restriction", + low: "Bas (score de similarité ≥ .25)", + medium: "Moyen (score de similarité ≥ .50)", + high: "Élevé (score de similarité ≥ .75)", + }, + reset: { + reset: "Réinitialiser la base de données vectorielle", + resetting: "Effacement des vecteurs...", + confirm: + "Vous êtes sur le point de réinitialiser la base de données vectorielle de cet espace de travail. Cela supprimera toutes les intégrations vectorielles actuellement intégrées.\n\nLes fichiers source originaux resteront intacts. Cette action est irréversible.", + error: + "La base de données vectorielle de l'espace de travail n'a pas pu être réinitialisée !", + success: + "La base de données vectorielle de l'espace de travail a été réinitialisée !", + }, + }, + + // Agent Configuration + agent: { + "performance-warning": + "La performance des LLM qui ne supportent pas explicitement l'appel d'outils dépend fortement des capacités et de la précision du modèle. Certaines capacités peuvent être limitées ou non fonctionnelles.", + provider: { + title: "Fournisseur LLM de l'agent de l'espace de travail", + description: + "Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour l'agent @agent de cet espace de travail.", + }, + mode: { + chat: { + title: "Modèle de chat de l'agent de l'espace de travail", + description: + "Le modèle de chat spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.", + }, + title: "Modèle de l'agent de l'espace de travail", + description: + "Le modèle LLM spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.", + wait: "-- en attente des modèles --", + }, + + skill: { + title: "Compétences par défaut de l'agent", + description: + "Améliorez les capacités naturelles de l'agent par défaut avec ces compétences préconstruites. Cette configuration s'applique à tous les espaces de travail.", + rag: { + title: "RAG et mémoire à long terme", + description: + "Permettez à l'agent de s'appuyer sur vos documents locaux pour répondre à une requête ou demandez à l'agent de se souvenir de morceaux de contenu pour la récupération de mémoire à long terme.", + }, + view: { + title: "Voir et résumer des documents", + description: + "Permettez à l'agent de lister et de résumer le contenu des fichiers de l'espace de travail actuellement intégrés.", + }, + scrape: { + title: "Récupérer des sites web", + description: + "Permettez à l'agent de visiter et de récupérer le contenu des sites web.", + }, + generate: { + title: "Générer des graphiques", + description: + "Activez l'agent par défaut pour générer différents types de graphiques à partir des données fournies ou données dans le chat.", + }, + save: { + title: "Générer et sauvegarder des fichiers dans le navigateur", + description: + "Activez l'agent par défaut pour générer et écrire des fichiers qui peuvent être sauvegardés et téléchargés dans votre navigateur.", + }, + web: { + title: "Recherche web en direct et navigation", + "desc-start": + "Permettez à votre agent de rechercher sur le web pour répondre à vos questions en se connectant à un fournisseur de recherche web (SERP).", + "desc-end": + "La recherche web pendant les sessions d'agent ne fonctionnera pas tant que cela ne sera pas configuré.", + }, + }, + }, + + // Workspace Chats + recorded: { + title: "Chats de l'espace de travail", + description: + "Voici tous les chats et messages enregistrés qui ont été envoyés par les utilisateurs, classés par date de création.", + export: "Exporter", + table: { + id: "Id", + by: "Envoyé par", + workspace: "Espace de travail", + prompt: "Invite", + response: "Réponse", + at: "Envoyé à ", + }, + }, + + // Appearance + appearance: { + title: "Apparence", + description: + "Personnalisez les paramètres d'apparence de votre plateforme.", + logo: { + title: "Personnaliser le logo", + description: + "Téléchargez votre logo personnalisé pour rendre votre chatbot unique.", + add: "Ajouter un logo personnalisé", + recommended: "Taille recommandée: 800 x 200", + remove: "Supprimer", + replace: "Remplacer", + }, + message: { + title: "Personnaliser les messages", + description: + "Personnalisez les messages automatiques affichés à vos utilisateurs.", + new: "Nouveau", + system: "système", + user: "utilisateur", + message: "message", + assistant: "Assistant de chat AnythingLLM", + "double-click": "Double-cliquez pour modifier...", + save: "Enregistrer les messages", + }, + icons: { + title: "Icônes de pied de page personnalisées", + description: + "Personnalisez les icônes de pied de page affichées en bas de la barre latérale.", + icon: "Icône", + link: "Lien", + }, + }, + + // API Keys + api: { + title: "Clés API", + description: + "Les clés API permettent au titulaire d'accéder et de gérer de manière programmatique cette instance AnythingLLM.", + link: "Lisez la documentation de l'API", + generate: "Générer une nouvelle clé API", + table: { + key: "Clé API", + by: "Créé par", + created: "Créé", + }, + }, + + llm: { + title: "Préférence LLM", + description: + "Voici les identifiants et les paramètres de votre fournisseur LLM de chat et d'intégration préféré. Il est important que ces clés soient actuelles et correctes, sinon AnythingLLM ne fonctionnera pas correctement.", + provider: "Fournisseur LLM", + }, + + transcription: { + title: "Préférence du modèle de transcription", + description: + "Voici les identifiants et les paramètres de votre fournisseur de modèle de transcription préféré. Il est important que ces clés soient actuelles et correctes, sinon les fichiers multimédias et audio ne seront pas transcrits.", + provider: "Fournisseur de transcription", + "warn-start": + "L'utilisation du modèle local whisper sur des machines avec une RAM ou un CPU limités peut bloquer AnythingLLM lors du traitement des fichiers multimédias.", + "warn-recommend": + "Nous recommandons au moins 2 Go de RAM et des fichiers téléchargés <10 Mo.", + "warn-end": + "Le modèle intégré se téléchargera automatiquement lors de la première utilisation.", + }, + + embedding: { + title: "Préférence d'intégration", + "desc-start": + "Lorsque vous utilisez un LLM qui ne supporte pas nativement un moteur d'intégration - vous devrez peut-être spécifier en plus des identifiants pour intégrer le texte.", + "desc-end": + "L'intégration est le processus de transformation du texte en vecteurs. Ces identifiants sont nécessaires pour transformer vos fichiers et invites en un format que AnythingLLM peut utiliser pour traiter.", + provider: { + title: "Fournisseur d'intégration", + description: + "Aucune configuration n'est nécessaire lors de l'utilisation du moteur d'intégration natif de AnythingLLM.", + }, + }, + + text: { + title: "Préférences de division et de découpage du texte", + "desc-start": + "Parfois, vous voudrez peut-être changer la façon dont les nouveaux documents sont divisés et découpés avant d'être insérés dans votre base de données vectorielle.", + "desc-end": + "Vous ne devez modifier ce paramètre que si vous comprenez comment fonctionne la division du texte et ses effets secondaires.", + "warn-start": "Les changements ici s'appliqueront uniquement aux", + "warn-center": "nouveaux documents intégrés", + "warn-end": ", pas aux documents existants.", + size: { + title: "Taille des segments de texte", + description: + "C'est la longueur maximale de caractères pouvant être présents dans un seul vecteur.", + recommend: "Longueur maximale du modèle d'intégration est", + }, + + overlap: { + title: "Chevauchement des segments de texte", + description: + "C'est le chevauchement maximal de caractères qui se produit pendant le découpage entre deux segments de texte adjacents.", + }, + }, + + // Vector Database + vector: { + title: "Base de données vectorielle", + description: + "Voici les identifiants et les paramètres de fonctionnement de votre instance AnythingLLM. Il est important que ces clés soient actuelles et correctes.", + provider: { + title: "Fournisseur de base de données vectorielle", + description: "Aucune configuration n'est nécessaire pour LanceDB.", + }, + }, + + // Embeddable Chat Widgets + embeddable: { + title: "Widgets de chat intégrables", + description: + "Les widgets de chat intégrables sont des interfaces de chat publiques associées à un espace de travail unique. Ils vous permettent de créer des espaces de travail que vous pouvez ensuite publier dans le monde entier.", + create: "Créer un widget intégré", + table: { + workspace: "Espace de travail", + chats: "Chats envoyés", + Active: "Domaines actifs", + }, + }, + + "embed-chats": { + title: "Chats intégrés", + description: + "Voici tous les chats et messages enregistrés de tout widget intégré que vous avez publié.", + table: { + embed: "Intégration", + sender: "Expéditeur", + message: "Message", + response: "Réponse", + at: "Envoyé à ", + }, + }, + + multi: { + title: "Mode multi-utilisateurs", + description: + "Configurez votre instance pour prendre en charge votre équipe en activant le mode multi-utilisateurs.", + enable: { + "is-enable": "Le mode multi-utilisateurs est activé", + enable: "Activer le mode multi-utilisateurs", + description: + "Par défaut, vous serez le seul administrateur. En tant qu'administrateur, vous devrez créer des comptes pour tous les nouveaux utilisateurs ou administrateurs. Ne perdez pas votre mot de passe car seul un utilisateur administrateur peut réinitialiser les mots de passe.", + username: "Nom d'utilisateur du compte administrateur", + password: "Mot de passe du compte administrateur", + }, + password: { + title: "Protection par mot de passe", + description: + "Protégez votre instance AnythingLLM avec un mot de passe. Si vous oubliez ce mot de passe, il n'y a pas de méthode de récupération, donc assurez-vous de le sauvegarder.", + }, + instance: { + title: "Protéger l'instance par mot de passe", + description: + "Par défaut, vous serez le seul administrateur. En tant qu'administrateur, vous devrez créer des comptes pour tous les nouveaux utilisateurs ou administrateurs. Ne perdez pas votre mot de passe car seul un utilisateur administrateur peut réinitialiser les mots de passe.", + password: "Mot de passe de l'instance", + }, + }, + + // Event Logs + event: { + title: "Journaux d'événements", + description: + "Consultez toutes les actions et événements se produisant sur cette instance pour la surveillance.", + clear: "Effacer les journaux d'événements", + table: { + type: "Type d'événement", + user: "Utilisateur", + occurred: "Survenu à ", + }, + }, + + // Privacy & Data-Handling + privacy: { + title: "Confidentialité et gestion des données", + description: + "Voici votre configuration pour la gestion des données et des fournisseurs tiers connectés avec AnythingLLM.", + llm: "Sélection LLM", + embedding: "Préférence d'intégration", + vector: "Base de données vectorielle", + anonymous: "Télémétrie anonyme activée", + }, +}; + +export default TRANSLATIONS; diff --git a/frontend/src/locales/resources.js b/frontend/src/locales/resources.js new file mode 100644 index 000000000..d2072d34a --- /dev/null +++ b/frontend/src/locales/resources.js @@ -0,0 +1,32 @@ +// Looking for a language to translate AnythingLLM to? +// Create a `common.js` file in the language's ISO code https://www.w3.org/International/O-charset-lang.html +// eg: Spanish => es/common.js +// eg: French => fr/common.js +// You should copy the en/common.js file as your template and just translate every string in there. +// By default, we try to see what the browsers native language is set to and use that. If a string +// is not defined or is null in the translation file, it will fallback to the value in the en/common.js file +// RULES: +// The EN translation file is the ground-truth for what keys and options are available. DO NOT add a special key +// to a specific language file as this will break the other languages. Any new keys should be added to english +// and the language file you are working on. + +import English from "./en/common.js"; +import Spanish from "./es/common.js"; +import French from "./fr/common.js"; +import Mandarin from "./zh/common.js"; + +export const defaultNS = "common"; +export const resources = { + en: { + common: English, + }, + zh: { + common: Mandarin, + }, + es: { + common: Spanish, + }, + fr: { + common: French, + }, +}; diff --git a/frontend/src/locales/verifyTranslations.mjs b/frontend/src/locales/verifyTranslations.mjs new file mode 100644 index 000000000..dccec76d3 --- /dev/null +++ b/frontend/src/locales/verifyTranslations.mjs @@ -0,0 +1,96 @@ +import { resources } from "./resources.js"; +const languageNames = new Intl.DisplayNames(Object.keys(resources), { + type: "language", +}); + +function langDisplayName(lang) { + return languageNames.of(lang); +} + +function compareStructures(lang, a, b, subdir = null) { + //if a and b aren't the same type, they can't be equal + if (typeof a !== typeof b) { + console.log("Invalid type comparison", [ + { + lang, + a: typeof a, + b: typeof b, + ...(!!subdir ? { subdir } : {}), + }, + ]); + return false; + } + + // Need the truthy guard because + // typeof null === 'object' + if (a && typeof a === "object") { + var keysA = Object.keys(a).sort(), + keysB = Object.keys(b).sort(); + + //if a and b are objects with different no of keys, unequal + if (keysA.length !== keysB.length) { + console.log("Keys are missing!", { + [lang]: keysA, + en: keysB, + ...(!!subdir ? { subdir } : {}), + diff: { + added: keysB.filter((key) => !keysA.includes(key)), + removed: keysA.filter((key) => !keysB.includes(key)), + }, + }); + return false; + } + + //if keys aren't all the same, unequal + if ( + !keysA.every(function (k, i) { + return k === keysB[i]; + }) + ) { + console.log("Keys are not equal!", { + [lang]: keysA, + en: keysB, + ...(!!subdir ? { subdir } : {}), + }); + return false; + } + + //recurse on the values for each key + return keysA.every(function (key) { + //if we made it here, they have identical keys + return compareStructures(lang, a[key], b[key], key); + }); + + //for primitives just ignore since we don't check values. + } else { + return true; + } +} + +const failed = []; +const TRANSLATIONS = {}; +for (const [lang, { common }] of Object.entries(resources)) + TRANSLATIONS[lang] = common; +const PRIMARY = { ...TRANSLATIONS["en"] }; +delete TRANSLATIONS["en"]; + +console.log( + `The following translation files will be verified: [${Object.keys( + TRANSLATIONS + ).join(",")}]` +); +for (const [lang, translations] of Object.entries(TRANSLATIONS)) { + const passed = compareStructures(lang, translations, PRIMARY); + console.log(`${langDisplayName(lang)} (${lang}): ${passed ? "✅" : "âŒ"}`); + !passed && failed.push(lang); +} + +if (failed.length !== 0) + throw new Error( + `The following translations files are INVALID and need fixing. Please see logs`, + failed + ); +console.log( + `👠All translation files located match the schema defined by the English file!` +); +process.exit(0); diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js new file mode 100644 index 000000000..0f30c4f74 --- /dev/null +++ b/frontend/src/locales/zh/common.js @@ -0,0 +1,424 @@ +// Anything with "null" requires a translation. Contribute to translation via a PR! +const TRANSLATIONS = { + common: { + "workspaces-name": "工作区å称", + error: "错误", + success: "æˆåŠŸ", + user: "用户", + selection: "模型选择", + save: "ä¿å˜æ›´æ”¹", + saving: "ä¿å˜ä¸...", + previous: "上一页", + next: "下一页", + }, + + // Setting Sidebar menu items. + settings: { + title: "设置", + system: "系统", + invites: "邀请", + users: "用户", + workspaces: "工作区", + "workspace-chats": "对è¯åŽ†å²è®°å½•", // "workspace-chats" should be "对è¯åŽ†å²è®°å½•", means "chat history",or "chat history records" + appearance: "外观", + "api-keys": "API 密钥", + llm: "LLM 首选项", + transcription: "Transcription 模型", + embedder: "Embedder 首选项", + "text-splitting": "文本分割", + "vector-database": "å‘é‡æ•°æ®åº“", + embeds: "嵌入å¼å¯¹è¯", + "embed-chats": "嵌入å¼å¯¹è¯åŽ†å²", + security: "用户与安全", + "event-logs": "事件日志", + privacy: "éšç§ä¸Žæ•°æ®", + }, + + // Page Definitions + login: { + "multi-user": { + welcome: "欢迎ï¼", + "placeholder-username": "请输入用户å", + "placeholder-password": "请输入密ç ", + login: "登录", + validating: "登录", + "forgot-pass": "忘记密ç ", + reset: "é‡ç½®", + }, + "sign-in": { + start: "ç™»å½•ä½ çš„", + end: "账户", + }, + }, + + // Workspace Settings menu items + "workspaces—settings": { + general: "通用设置", + chat: "èŠå¤©è®¾ç½®", + vector: "å‘é‡æ•°æ®åº“", + members: "æˆå‘˜", + agent: "代ç†é…ç½®", + }, + + // General Appearance + general: { + vector: { + title: "å‘é‡æ•°é‡", + description: "å‘é‡æ•°æ®åº“ä¸çš„总å‘é‡æ•°ã€‚", + }, + names: { + description: "è¿™åªä¼šæ›´æ”¹å·¥ä½œåŒºçš„显示å称。", + }, + message: { + title: "建议的èŠå¤©æ¶ˆæ¯", + description: "自定义将å‘您的工作区用户建议的消æ¯ã€‚", + add: "æ·»åŠ æ–°æ¶ˆæ¯", + save: "ä¿å˜æ¶ˆæ¯", + heading: "å‘我解释", + body: "AnythingLLM的好处", + }, + pfp: { + title: "助ç†å¤´åƒ", + description: "为æ¤å·¥ä½œåŒºè‡ªå®šä¹‰åŠ©æ‰‹çš„个人资料图åƒã€‚", + image: "工作区图åƒ", + remove: "移除工作区图åƒ", + }, + delete: { + delete: "åˆ é™¤å·¥ä½œåŒº", + deleting: "æ£åœ¨åˆ 除工作区...", + "confirm-start": "您å³å°†åˆ 除整个", + "confirm-end": + "å·¥ä½œåŒºã€‚è¿™å°†åˆ é™¤çŸ¢é‡æ•°æ®åº“ä¸çš„所有矢é‡åµŒå…¥ã€‚\n\n原始æºæ–‡ä»¶å°†ä¿æŒä¸å˜ã€‚æ¤æ“作是ä¸å¯é€†è½¬çš„。", + }, + }, + + // Chat Settings + chat: { + llm: { + title: "工作区LLMæ供者", + description: + "将用于æ¤å·¥ä½œåŒºçš„特定 LLM æ供商和模型。默认情况下,它使用系统 LLM æ供程åºå’Œè®¾ç½®ã€‚", + search: "æœç´¢æ‰€æœ‰ LLM æ供商", + }, + model: { + title: "工作区èŠå¤©æ¨¡åž‹", + description: + "将用于æ¤å·¥ä½œåŒºçš„特定èŠå¤©æ¨¡åž‹ã€‚如果为空,将使用系统LLM首选项。", + wait: "-- ç‰å¾…模型 --", + }, + mode: { + title: "èŠå¤©æ¨¡å¼", + chat: { + title: "èŠå¤©", + "desc-start": "å°†æ供法å¦ç¡•å£«çš„一般知识", + and: "å’Œ", + "desc-end": "找到的文档上下文的ç”案。", + }, + query: { + title: "查询", + "desc-start": "å°†", + only: "ä»…", + "desc-end": "æ供找到的文档上下文的ç”案。", + }, + }, + history: { + title: "èŠå¤©åŽ†å²è®°å½•", + "desc-start": "将包å«åœ¨å“应的çŸæœŸè®°å¿†ä¸çš„å…ˆå‰èŠå¤©çš„æ•°é‡ã€‚", + recommend: "推è 20。", + "desc-end": + "任何超过 45 的值都å¯èƒ½å¯¼è‡´è¿žç»èŠå¤©å¤±è´¥ï¼Œå…·ä½“å–决于消æ¯å¤§å°ã€‚", + }, + prompt: { + title: "èŠå¤©æ示", + description: + "将在æ¤å·¥ä½œåŒºä¸Šä½¿ç”¨çš„æ示。定义 AI 生æˆå“应的上下文和指令。您应该æ供精心设计的æ示,以便人工智能å¯ä»¥ç”Ÿæˆç›¸å…³ä¸”准确的å“应。", + }, + refusal: { + title: "查询模å¼æ‹’ç»å“应", + "desc-start": "当处于", + query: "查询", + "desc-end": "模å¼æ—¶ï¼Œå½“未找到上下文时,您å¯èƒ½å¸Œæœ›è¿”回自定义拒ç»å“应。", + }, + temperature: { + title: "LLM Temperature", + "desc-start": "æ¤è®¾ç½®æŽ§åˆ¶æ‚¨çš„ LLM 回ç”的“创æ„â€ç¨‹åº¦", + "desc-end": + "æ•°å—越高越有创æ„。对于æŸäº›æ¨¡åž‹ï¼Œå¦‚果设置得太高,å¯èƒ½ä¼šå¯¼è‡´å“应ä¸ä¸€è‡´ã€‚", + hint: "大多数法å¦ç¡•å£«éƒ½æœ‰å„ç§å¯æŽ¥å—的有效值范围。请咨询您的法å¦ç¡•å£«æ供商以获å–该信æ¯ã€‚", + }, + }, + + // Vector Database Settings + "vector-workspace": { + identifier: "å‘é‡æ•°æ®åº“æ ‡è¯†ç¬¦", + snippets: { + title: "最大上下文片段", + description: + "æ¤è®¾ç½®æŽ§åˆ¶æ¯æ¬¡èŠå¤©æˆ–查询将å‘é€åˆ° LLM 的上下文片段的最大数é‡ã€‚", + recommend: "推è: 4", + }, + doc: { + title: "文档相似性阈值", + description: + "æºè¢«è§†ä¸ºä¸ŽèŠå¤©ç›¸å…³æ‰€éœ€çš„最低相似度分数。数å—越高,æ¥æºä¸ŽèŠå¤©å°±è¶Šç›¸ä¼¼ã€‚", + zero: "æ— é™åˆ¶", + low: "低(相似度分数 ≥ .25)", + medium: "ä¸ï¼ˆç›¸ä¼¼åº¦åˆ†æ•° ≥ .50)", + high: "高(相似度分数 ≥ .75)", + }, + reset: { + reset: "é‡ç½®å‘é‡æ•°æ®åº“", + resetting: "清除å‘é‡...", + confirm: + "您将é‡ç½®æ¤å·¥ä½œåŒºçš„矢é‡æ•°æ®åº“ã€‚è¿™å°†åˆ é™¤å½“å‰åµŒå…¥çš„所有矢é‡åµŒå…¥ã€‚\n\n原始æºæ–‡ä»¶å°†ä¿æŒä¸å˜ã€‚æ¤æ“作是ä¸å¯é€†è½¬çš„。", + success: "å‘é‡æ•°æ®åº“å·²é‡ç½®ã€‚", + error: "æ— æ³•é‡ç½®å·¥ä½œåŒºå‘é‡æ•°æ®åº“ï¼", + }, + }, + + // Agent Configuration + agent: { + "performance-warning": + "ä¸æ˜Žç¡®æ”¯æŒå·¥å…·è°ƒç”¨çš„ LLMs 的性能高度ä¾èµ–于模型的功能和准确性。有些能力å¯èƒ½å—到é™åˆ¶æˆ–ä¸èµ·ä½œç”¨ã€‚", + provider: { + title: "å·¥ä½œåŒºä»£ç† LLM æ供商", + description: "将用于æ¤å·¥ä½œåŒºçš„ @agent 代ç†çš„特定 LLM æ供商和模型。", + }, + mode: { + chat: { + title: "工作区代ç†èŠå¤©æ¨¡åž‹", + description: "将用于æ¤å·¥ä½œåŒºçš„ @agent 代ç†çš„特定èŠå¤©æ¨¡åž‹ã€‚", + }, + title: "工作区代ç†æ¨¡åž‹", + description: "将用于æ¤å·¥ä½œåŒºçš„ @agent 代ç†çš„特定 LLM 模型。", + wait: "-- ç‰å¾…模型 --", + }, + skill: { + title: "默认代ç†æŠ€èƒ½", + description: + "使用这些预构建的技能æ高默认代ç†çš„自然能力。æ¤è®¾ç½®é€‚用于所有工作区。", + rag: { + title: "RAG和长期记忆", + description: + 'å…许代ç†åˆ©ç”¨æ‚¨çš„本地文档æ¥å›žç”查询,或è¦æ±‚代ç†"è®°ä½"长期记忆检索的内容片段。', + }, + view: { + title: "查看和总结文档", + description: "å…许代ç†åˆ—出和总结当å‰åµŒå…¥çš„工作区文件的内容。", + }, + scrape: { + title: "抓å–网站", + description: "å…许代ç†è®¿é—®å’ŒæŠ“å–网站的内容。", + }, + generate: { + title: "生æˆå›¾è¡¨", + description: "使默认代ç†èƒ½å¤Ÿä»Žæ供的数æ®æˆ–èŠå¤©ä¸ç”Ÿæˆå„ç§ç±»åž‹çš„图表。", + }, + save: { + title: "生æˆå¹¶ä¿å˜æ–‡ä»¶åˆ°æµè§ˆå™¨", + description: + "使默认代ç†èƒ½å¤Ÿç”Ÿæˆå¹¶å†™å…¥æ–‡ä»¶ï¼Œè¿™äº›æ–‡ä»¶å¯ä»¥ä¿å˜å¹¶åœ¨æ‚¨çš„æµè§ˆå™¨ä¸ä¸‹è½½ã€‚", + }, + web: { + title: "实时网络æœç´¢å’Œæµè§ˆ", + "desc-start": + "通过连接到网络æœç´¢ï¼ˆSERP)æ供者,使您的代ç†èƒ½å¤Ÿæœç´¢ç½‘络以回ç”您的问题。", + "desc-end": "在代ç†ä¼šè¯æœŸé—´ï¼Œç½‘络æœç´¢å°†ä¸èµ·ä½œç”¨ï¼Œç›´åˆ°æ¤è®¾ç½®å®Œæˆã€‚", + }, + }, + }, + + // Workspace Chat + recorded: { + title: "工作区èŠå¤©åŽ†å²è®°å½•", + description: "这些是用户å‘é€çš„所有èŠå¤©è®°å½•å’Œæ¶ˆæ¯ï¼ŒæŒ‰åˆ›å»ºæ—¥æœŸæŽ’åºã€‚", + export: "导出", + table: { + id: "Id", + by: "Sent By", + workspace: "Workspace", + prompt: "Prompt", + response: "Response", + at: "Sent At", + }, + }, + + appearance: { + title: "外观", + description: "自定义平å°çš„外观设置。", + logo: { + title: "è‡ªå®šä¹‰å›¾æ ‡", + description: "ä¸Šä¼ æ‚¨çš„è‡ªå®šä¹‰å›¾æ ‡ï¼Œè®©æ‚¨çš„èŠå¤©æœºå™¨äººæˆä¸ºæ‚¨çš„。", + add: "æ·»åŠ è‡ªå®šä¹‰å›¾æ ‡", + recommended: "建议尺寸:800 x 200", + remove: "移除", + replace: "替æ¢", + }, + message: { + title: "自定义消æ¯", + description: "自定义å‘用户显示的自动消æ¯ã€‚", + new: "新建", + system: "系统", + user: "用户", + message: "消æ¯", + assistant: "AnythingLLM èŠå¤©åŠ©æ‰‹", + "double-click": "åŒå‡»ä»¥ç¼–辑...", + save: "ä¿å˜æ¶ˆæ¯", + }, + icons: { + title: "è‡ªå®šä¹‰é¡µè„šå›¾æ ‡", + description: "自定义侧边æ åº•éƒ¨æ˜¾ç¤ºçš„é¡µè„šå›¾æ ‡ã€‚", + icon: "å›¾æ ‡", + link: "链接", + }, + }, + + // API Keys + api: { + title: "API 密钥", + description: "API 密钥å…许æŒæœ‰è€…以编程方å¼è®¿é—®å’Œç®¡ç†æ¤ AnythingLLM 实例。", + link: "阅读 API 文档", + generate: "生æˆæ–°çš„ API 密钥", + table: { + key: "API 密钥", + by: "创建者", + created: "创建", + }, + }, + + // LLM Preferences + llm: { + title: "LLM å好", + description: + "这些是您首选的 LLM èŠå¤©å’ŒåµŒå…¥æ供商的å‡æ®å’Œè®¾ç½®ã€‚é‡è¦çš„是,这些密钥是最新的和æ£ç¡®çš„,å¦åˆ™ AnythingLLM å°†æ— æ³•æ£å¸¸è¿è¡Œã€‚", + provider: "LLM æ供商", + }, + + transcription: { + title: "转录模型å好", + description: + "这些是您的首选转录模型æ供商的å‡æ®å’Œè®¾ç½®ã€‚é‡è¦çš„是这些密钥是最新且æ£ç¡®çš„,å¦åˆ™åª’ä½“æ–‡ä»¶å’ŒéŸ³é¢‘å°†æ— æ³•è½¬å½•ã€‚", + provider: "转录æ供商", + "warn-start": + "在 RAM 或 CPU 有é™çš„计算机上使用本地耳è¯æ¨¡åž‹å¯èƒ½ä¼šåœ¨å¤„ç†åª’体文件时åœæ¢ AnythingLLM。", + "warn-recommend": "我们建议至少 2GB RAM å¹¶ä¸Šä¼ <10Mb 的文件。", + "warn-end": "内置模型将在首次使用时自动下载。", + }, + + embedding: { + title: "嵌入首选项", + "desc-start": + "当使用本身ä¸æ”¯æŒåµŒå…¥å¼•æ“Žçš„ LLM 时,您å¯èƒ½éœ€è¦é¢å¤–指定用于嵌入文本的å‡æ®ã€‚", + "desc-end": + "嵌入是将文本转æ¢ä¸ºçŸ¢é‡çš„过程。需è¦è¿™äº›å‡æ®æ‰èƒ½å°†æ‚¨çš„文件和æ示转æ¢ä¸º AnythingLLM å¯ä»¥ç”¨æ¥å¤„ç†çš„æ ¼å¼ã€‚", + provider: { + title: "嵌入引擎æ供商", + description: "使用 AnythingLLM 的本机嵌入引擎时ä¸éœ€è¦è®¾ç½®ã€‚", + }, + }, + + text: { + title: "文本拆分和分å—首选项", + "desc-start": + "有时,您å¯èƒ½å¸Œæœ›æ›´æ”¹æ–°æ–‡æ¡£åœ¨æ’入到矢é‡æ•°æ®åº“之å‰æ‹†åˆ†å’Œåˆ†å—的默认方å¼ã€‚", + "desc-end": "åªæœ‰åœ¨äº†è§£æ–‡æœ¬æ‹†åˆ†çš„工作原ç†åŠå…¶å‰¯ä½œç”¨æ—¶ï¼Œæ‰åº”修改æ¤è®¾ç½®ã€‚", + "warn-start": "æ¤å¤„的更改仅适用于", + "warn-center": "新嵌入的文档", + "warn-end": ",而ä¸æ˜¯çŽ°æœ‰æ–‡æ¡£ã€‚", + size: { + title: "文本å—大å°", + description: "这是å•ä¸ªå‘é‡ä¸å¯ä»¥å˜åœ¨çš„å—符的最大长度。", + recommend: "嵌入模型的最大长度为", + }, + overlap: { + title: "文本å—é‡å ", + description: "这是在两个相邻文本å—之间分å—期间å‘生的最大å—符é‡å 。", + }, + }, + + // Vector Database + vector: { + title: "å‘é‡æ•°æ®åº“", + description: + "这些是 AnythingLLM 实例如何è¿è¡Œçš„å‡æ®å’Œè®¾ç½®ã€‚é‡è¦çš„是,这些密钥是最新的和æ£ç¡®çš„。", + provider: { + title: "å‘é‡æ•°æ®åº“æ供商", + description: "LanceDB ä¸éœ€è¦ä»»ä½•é…置。", + }, + }, + + // Embeddable Chats + embeddable: { + title: "å¯åµŒå…¥çš„èŠå¤©å°éƒ¨ä»¶", + description: + "å¯åµŒå…¥çš„èŠå¤©å°éƒ¨ä»¶æ˜¯ä¸Žå•ä¸ªå·¥ä½œåŒºç»‘定的é¢å‘公众的èŠå¤©ç•Œé¢ã€‚这些å…许您构建工作区,然åŽæ‚¨å¯ä»¥å°†å…¶å‘布到全世界。", + create: "创建嵌入å¼å¯¹è¯", + table: { + workspace: "工作区", + chats: "å·²å‘é€èŠå¤©", + Active: "活动域", + }, + }, + + // Embeddable Chat History + "embed-chats": { + title: "嵌入èŠå¤©", + description: "这些是您å‘布的任何嵌入的所有记录的èŠå¤©å’Œæ¶ˆæ¯ã€‚", + table: { + embed: "嵌入", + sender: "å‘é€è€…", + message: "消æ¯", + response: "å“应", + at: "å‘é€äºŽ", + }, + }, + + multi: { + title: "多用户模å¼", + description: "通过激活多用户模å¼æ¥è®¾ç½®æ‚¨çš„实例以支æŒæ‚¨çš„团队。", + enable: { + "is-enable": "多用户模å¼å·²å¯ç”¨", + enable: "å¯ç”¨å¤šç”¨æˆ·æ¨¡å¼", + description: + "默认情况下,您将是唯一的管ç†å‘˜ã€‚作为管ç†å‘˜ï¼Œæ‚¨éœ€è¦ä¸ºæ‰€æœ‰æ–°ç”¨æˆ·æˆ–管ç†å‘˜åˆ›å»ºè´¦æˆ·ã€‚ä¸è¦ä¸¢å¤±æ‚¨çš„密ç ï¼Œå› ä¸ºåªæœ‰ç®¡ç†å‘˜ç”¨æˆ·å¯ä»¥é‡ç½®å¯†ç 。", + username: "管ç†å‘˜è´¦æˆ·ç”¨æˆ·å", + password: "管ç†å‘˜è´¦æˆ·å¯†ç ", + }, + password: { + title: "密ç ä¿æŠ¤", + description: + "用密ç ä¿æŠ¤æ‚¨çš„AnythingLLM实例。如果您忘记了密ç ,那么没有æ¢å¤æ–¹æ³•ï¼Œæ‰€ä»¥è¯·ç¡®ä¿ä¿å˜è¿™ä¸ªå¯†ç 。", + }, + instance: { + title: "实例密ç ä¿æŠ¤", + description: + "默认情况下,您将是唯一的管ç†å‘˜ã€‚作为管ç†å‘˜ï¼Œæ‚¨éœ€è¦ä¸ºæ‰€æœ‰æ–°ç”¨æˆ·æˆ–管ç†å‘˜åˆ›å»ºè´¦æˆ·ã€‚ä¸è¦ä¸¢å¤±æ‚¨çš„密ç ï¼Œå› ä¸ºåªæœ‰ç®¡ç†å‘˜ç”¨æˆ·å¯ä»¥é‡ç½®å¯†ç 。", + password: "实例密ç ", + }, + }, + + // Event Logs + event: { + title: "事件日志", + description: "查看æ¤å®žä¾‹ä¸Šå‘生的所有æ“作和事件以进行监控。", + clear: "清除事件日志", + table: { + type: "事件类型", + user: "用户", + occurred: "å‘生时间", + }, + }, + + // Privacy & Data-Handling + privacy: { + title: "éšç§å’Œæ•°æ®å¤„ç†", + description: + "这是您对如何处ç†è¿žæŽ¥çš„第三方æ供商和AnythingLLMçš„æ•°æ®çš„é…置。", + llm: "LLM选择", + embedding: "嵌入å好", + vector: "å‘é‡æ•°æ®åº“", + anonymous: "å¯ç”¨åŒ¿åé¥æµ‹", + }, +}; + +export default TRANSLATIONS; diff --git a/frontend/src/pages/Admin/Logging/index.jsx b/frontend/src/pages/Admin/Logging/index.jsx index 498247849..389263f4b 100644 --- a/frontend/src/pages/Admin/Logging/index.jsx +++ b/frontend/src/pages/Admin/Logging/index.jsx @@ -7,6 +7,7 @@ import * as Skeleton from "react-loading-skeleton"; import LogRow from "./LogRow"; import showToast from "@/utils/toast"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; export default function AdminLogs() { const query = useQuery(); @@ -14,6 +15,7 @@ export default function AdminLogs() { const [logs, setLogs] = useState([]); const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [canNext, setCanNext] = useState(false); + const { t } = useTranslation(); useEffect(() => { async function fetchLogs() { @@ -62,12 +64,11 @@ export default function AdminLogs() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Event Logs + {t("event.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - View all actions and events happening on this instance for - monitoring. + {t("event.description")} </p> </div> <div className="w-full justify-end flex"> @@ -75,7 +76,7 @@ export default function AdminLogs() { onClick={handleResetLogs} className="mt-3 mr-0 -mb-14 z-10" > - Clear Event Logs + {t("event.clear")} </CTAButton> </div> <LogsContainer @@ -100,6 +101,7 @@ function LogsContainer({ handleNext, handlePrevious, }) { + const { t } = useTranslation(); if (loading) { return ( <Skeleton.default @@ -120,13 +122,13 @@ function LogsContainer({ <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <tr> <th scope="col" className="px-6 py-3 rounded-tl-lg"> - Event Type + {t("event.table.type")} </th> <th scope="col" className="px-6 py-3"> - User + {t("event.table.user")} </th> <th scope="col" className="px-6 py-3"> - Occurred At + {t("event.table.occurred")} </th> <th scope="col" className="px-6 py-3 rounded-tr-lg"> {" "} @@ -143,14 +145,14 @@ function LogsContainer({ className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" disabled={offset === 0} > - Previous Page + {t("common.previous")} </button> <button onClick={handleNext} className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" disabled={!canNext} > - Next Page + {t("common.next")} </button> </div> </> diff --git a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx index 0667809a5..53f5c98d7 100644 --- a/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx +++ b/frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx @@ -1,9 +1,11 @@ import React, { useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; +import { useTranslation } from "react-i18next"; export default function NewWorkspaceModal({ closeModal }) { const [error, setError] = useState(null); + const { t } = useTranslation(); const handleCreate = async (e) => { setError(null); e.preventDefault(); @@ -37,7 +39,7 @@ export default function NewWorkspaceModal({ closeModal }) { htmlFor="name" className="block mb-2 text-sm font-medium text-white" > - Workspace name + {t("common.workspaces-name")} </label> <input name="name" diff --git a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx index 4cf3fa397..3aa17b4bf 100644 --- a/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx +++ b/frontend/src/pages/GeneralSettings/ApiKeys/index.jsx @@ -13,10 +13,11 @@ import System from "@/models/system"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; export default function AdminApiKeys() { const { isOpen, openModal, closeModal } = useModal(); - + const { t } = useTranslation(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <Sidebar /> @@ -27,11 +28,12 @@ export default function AdminApiKeys() { <div className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16"> <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="items-center flex gap-x-4"> - <p className="text-lg leading-6 font-bold text-white">API Keys</p> + <p className="text-lg leading-6 font-bold text-white"> + {t("api.title")} + </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - API keys allow the holder to programmatically access and manage - this AnythingLLM instance. + {t("api.description")} </p> <a href={paths.apiDocs()} @@ -39,13 +41,13 @@ export default function AdminApiKeys() { rel="noreferrer" className="text-xs leading-[18px] font-base text-blue-300 hover:underline" > - Read the API documentation → + {t("api.link")} → </a> </div> <div className="w-full justify-end flex"> <CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10"> - <PlusCircle className="h-4 w-4" weight="bold" /> Generate New API - Key + <PlusCircle className="h-4 w-4" weight="bold" />{" "} + {t("api.generate")} </CTAButton> </div> <ApiKeysContainer /> @@ -61,6 +63,7 @@ export default function AdminApiKeys() { function ApiKeysContainer() { const [loading, setLoading] = useState(true); const [apiKeys, setApiKeys] = useState([]); + const { t } = useTranslation(); useEffect(() => { async function fetchExistingKeys() { @@ -92,13 +95,13 @@ function ApiKeysContainer() { <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <tr> <th scope="col" className="px-6 py-3 rounded-tl-lg"> - API Key + {t("api.table.key")} </th> <th scope="col" className="px-6 py-3"> - Created By + {t("api.table.by")} </th> <th scope="col" className="px-6 py-3"> - Created + {t("api.table.created")} </th> <th scope="col" className="px-6 py-3 rounded-tr-lg"> {" "} diff --git a/frontend/src/pages/GeneralSettings/Appearance/CustomLogo/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/CustomLogo/index.jsx index 5de37e3fe..c0d230d5a 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/CustomLogo/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/CustomLogo/index.jsx @@ -3,6 +3,7 @@ import System from "@/models/system"; import showToast from "@/utils/toast"; import { useEffect, useRef, useState } from "react"; import { Plus } from "@phosphor-icons/react"; +import { useTranslation } from "react-i18next"; export default function CustomLogo() { const { logo: _initLogo, setLogo: _setLogo } = useLogo(); @@ -65,15 +66,16 @@ export default function CustomLogo() { const triggerFileInputClick = () => { fileInputRef.current?.click(); }; + const { t } = useTranslation(); return ( <div className="mt-6 mb-8"> <div className="flex flex-col gap-y-1"> <h2 className="text-base leading-6 font-bold text-white"> - Custom Logo + {t("appearance.logo.title")} </h2> <p className="text-xs leading-[18px] font-base text-white/60"> - Upload your custom logo to make your chatbot yours. + {t("appearance.logo.description")} </p> </div> {isDefaultLogo ? ( @@ -99,10 +101,10 @@ export default function CustomLogo() { <Plus className="w-6 h-6 text-black/80 m-2" /> </div> <div className="text-white text-opacity-80 text-sm font-semibold py-1"> - Add a custom logo + {t("appearance.logo.add")} </div> <div className="text-white text-opacity-60 text-xs font-medium py-1"> - Recommended size: 800 x 200 + {t("appearance.logo.recommended")} </div> </div> </div> @@ -123,7 +125,7 @@ export default function CustomLogo() { onClick={triggerFileInputClick} className="text-white text-base font-medium hover:text-opacity-60 mx-2" > - Replace + {t("appearance.logo.replace")} </button> <input @@ -138,7 +140,7 @@ export default function CustomLogo() { onClick={handleRemoveLogo} className="text-white text-base font-medium hover:text-opacity-60 mx-2" > - Remove + {t("appearance.logo.remove")} </button> </div> </div> diff --git a/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx index 7e026ac83..6165501a2 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx @@ -3,10 +3,12 @@ import System from "@/models/system"; import showToast from "@/utils/toast"; import { Plus } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; export default function CustomMessages() { const [hasChanges, setHasChanges] = useState(false); const [messages, setMessages] = useState([]); + const { t } = useTranslation(); useEffect(() => { async function fetchMessages() { @@ -20,12 +22,12 @@ export default function CustomMessages() { if (type === "user") { setMessages([ ...messages, - { user: "Double click to edit...", response: "" }, + { user: t("appearance.message.double-click"), response: "" }, ]); } else { setMessages([ ...messages, - { user: "", response: "Double click to edit..." }, + { user: "", response: t("appearance.message.double-click") }, ]); } }; @@ -56,10 +58,10 @@ export default function CustomMessages() { <div className="mb-8"> <div className="flex flex-col gap-y-1"> <h2 className="text-base leading-6 font-bold text-white"> - Custom Messages + {t("appearance.message.title")} </h2> <p className="text-xs leading-[18px] font-base text-white/60"> - Customize the automatic messages displayed to your users. + {t("appearance.message.description")} </p> </div> <div className="mt-3 flex flex-col gap-y-6 bg-dark-highlight rounded-lg pr-[31px] pl-[12px] pt-4 max-w-[700px]"> @@ -93,8 +95,11 @@ export default function CustomMessages() { <div className="flex items-center justify-start text-sm font-normal -ml-2"> <Plus className="m-2" size={16} weight="bold" /> <span className="leading-5"> - New <span className="font-bold italic mr-1">system</span>{" "} - message + {t("appearance.message.new")}{" "} + <span className="font-bold italic mr-1"> + {t("appearance.message.system")} + </span>{" "} + {t("appearance.message.message")} </span> </div> </button> @@ -105,7 +110,11 @@ export default function CustomMessages() { <div className="flex items-center justify-start text-sm font-normal"> <Plus className="m-2" size={16} weight="bold" /> <span className="leading-5"> - New <span className="font-bold italic mr-1">user</span> message + {t("appearance.message.new")}{" "} + <span className="font-bold italic mr-1"> + {t("appearance.message.user")} + </span>{" "} + {t("appearance.message.message")} </span> </div> </button> @@ -117,7 +126,7 @@ export default function CustomMessages() { className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" onClick={handleMessageSave} > - Save Messages + {t("appearance.message.save")} </button> </div> )} diff --git a/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx index bd78861a7..e7f6c75ec 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx @@ -4,10 +4,11 @@ import { safeJsonParse } from "@/utils/request"; import NewIconForm from "./NewIconForm"; import Admin from "@/models/admin"; import System from "@/models/system"; +import { useTranslation } from "react-i18next"; export default function FooterCustomization() { const [footerIcons, setFooterIcons] = useState(Array(3).fill(null)); - + const { t } = useTranslation(); useEffect(() => { async function fetchFooterIcons() { const settings = (await Admin.systemPreferences())?.settings; @@ -52,15 +53,15 @@ export default function FooterCustomization() { <div className="mb-8"> <div className="flex flex-col gap-y-1"> <h2 className="text-base leading-6 font-bold text-white"> - Custom Footer Icons + {t("appearance.icons.title")} </h2> <p className="text-xs leading-[18px] font-base text-white/60"> - Customize the footer icons displayed on the bottom of the sidebar. + {t("appearance.icons.description")} </p> </div> <div className="mt-3 flex gap-x-3 font-bold text-white text-sm"> - <div>Icon</div> - <div>Link</div> + <div>{t("appearance.icons.icon")}</div> + <div>{t("appearance.icons.link")}</div> </div> <div className="mt-2 flex flex-col gap-y-[10px]"> {footerIcons.map((icon, index) => ( diff --git a/frontend/src/pages/GeneralSettings/Appearance/LanguagePreference/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/LanguagePreference/index.jsx new file mode 100644 index 000000000..8e58706b0 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/Appearance/LanguagePreference/index.jsx @@ -0,0 +1,40 @@ +import { useLanguageOptions } from "@/hooks/useLanguageOptions"; + +export default function LanguagePreference() { + const { + currentLanguage, + supportedLanguages, + getLanguageName, + changeLanguage, + } = useLanguageOptions(); + + return ( + <> + <div className="flex flex-col gap-y-1"> + <h2 className="text-base leading-6 font-bold text-white"> + Display Language + </h2> + <p className="text-xs leading-[18px] font-base text-white/60"> + Select the preferred language to render AnythingLLM's UI in, when + applicable. + </p> + </div> + <div className="flex items-center gap-x-4"> + <select + name="userLang" + className="bg-zinc-900 w-fit mt-2 px-4 border-gray-500 text-white text-sm rounded-lg block py-2" + defaultValue={currentLanguage || "en"} + onChange={(e) => changeLanguage(e.target.value)} + > + {supportedLanguages.map((lang) => { + return ( + <option key={lang} value={lang}> + {getLanguageName(lang)} + </option> + ); + })} + </select> + </div> + </> + ); +} diff --git a/frontend/src/pages/GeneralSettings/Appearance/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/index.jsx index d73529987..5894a642c 100644 --- a/frontend/src/pages/GeneralSettings/Appearance/index.jsx +++ b/frontend/src/pages/GeneralSettings/Appearance/index.jsx @@ -4,9 +4,12 @@ import FooterCustomization from "./FooterCustomization"; import SupportEmail from "./SupportEmail"; import CustomLogo from "./CustomLogo"; import CustomMessages from "./CustomMessages"; +import { useTranslation } from "react-i18next"; import CustomAppName from "./CustomAppName"; +import LanguagePreference from "./LanguagePreference"; export default function Appearance() { + const { t } = useTranslation(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <Sidebar /> @@ -18,13 +21,14 @@ export default function Appearance() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="items-center"> <p className="text-lg leading-6 font-bold text-white"> - Appearance + {t("appearance.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - Customize the appearance settings of your platform. + {t("appearance.description")} </p> </div> + <LanguagePreference /> <CustomLogo /> <CustomAppName /> <CustomMessages /> diff --git a/frontend/src/pages/GeneralSettings/Chats/index.jsx b/frontend/src/pages/GeneralSettings/Chats/index.jsx index 3cc778044..3631c8c3e 100644 --- a/frontend/src/pages/GeneralSettings/Chats/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/index.jsx @@ -9,6 +9,7 @@ import showToast from "@/utils/toast"; import System from "@/models/system"; import { CaretDown, Download, Trash } from "@phosphor-icons/react"; import { saveAs } from "file-saver"; +import { useTranslation } from "react-i18next"; const exportOptions = { csv: { @@ -54,6 +55,7 @@ export default function WorkspaceChats() { const [chats, setChats] = useState([]); const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [canNext, setCanNext] = useState(false); + const { t } = useTranslation(); const handleDumpChats = async (exportType) => { const chats = await System.exportChats(exportType); @@ -122,7 +124,7 @@ export default function WorkspaceChats() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Workspace Chats + {t("recorded.title")} </p> <div className="relative"> <button @@ -131,7 +133,7 @@ export default function WorkspaceChats() { className="flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:text-white text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit" > <Download size={18} weight="bold" /> - Export + {t("recorded.export")} <CaretDown size={18} weight="bold" /> </button> <div @@ -167,8 +169,7 @@ export default function WorkspaceChats() { )} </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are all the recorded chats and messages that have been sent - by users ordered by their creation date. + {t("recorded.description")} </p> </div> <ChatsContainer @@ -178,6 +179,7 @@ export default function WorkspaceChats() { offset={offset} setOffset={setOffset} canNext={canNext} + t={t} /> </div> </div> @@ -192,6 +194,7 @@ function ChatsContainer({ offset, setOffset, canNext, + t, }) { const handlePrevious = () => { setOffset(Math.max(offset - 1, 0)); @@ -225,22 +228,22 @@ function ChatsContainer({ <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <tr> <th scope="col" className="px-6 py-3 rounded-tl-lg"> - Id + {t("recorded.table.id")} </th> <th scope="col" className="px-6 py-3"> - Sent By + {t("recorded.table.by")} </th> <th scope="col" className="px-6 py-3"> - Workspace + {t("recorded.table.workspace")} </th> <th scope="col" className="px-6 py-3"> - Prompt + {t("recorded.table.prompt")} </th> <th scope="col" className="px-6 py-3"> - Response + {t("recorded.table.response")} </th> <th scope="col" className="px-6 py-3"> - Sent At + {t("recorded.table.at")} </th> <th scope="col" className="px-6 py-3 rounded-tr-lg"> {" "} diff --git a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx index a2605154f..39b013c75 100644 --- a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx @@ -6,9 +6,11 @@ import "react-loading-skeleton/dist/skeleton.css"; import useQuery from "@/hooks/useQuery"; import ChatRow from "./ChatRow"; import Embed from "@/models/embed"; +import { useTranslation } from "react-i18next"; export default function EmbedChats() { // TODO [FEAT]: Add export of embed chats + const { t } = useTranslation(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <Sidebar /> @@ -20,12 +22,11 @@ export default function EmbedChats() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Embed Chats + {t("embed-chats.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are all the recorded chats and messages from any embed that - you have published. + {t("embed-chats.description")} </p> </div> <ChatsContainer /> @@ -41,6 +42,7 @@ function ChatsContainer() { const [chats, setChats] = useState([]); const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [canNext, setCanNext] = useState(false); + const { t } = useTranslation(); const handlePrevious = () => { setOffset(Math.max(offset - 1, 0)); @@ -83,19 +85,19 @@ function ChatsContainer() { <thead className="text-white text-opacity-80 text-sm font-bold uppercase border-white border-b border-opacity-60"> <tr> <th scope="col" className="px-6 py-3 rounded-tl-lg"> - Embed + {t("embed-chats.table.embed")} </th> <th scope="col" className="px-6 py-3"> - Sender + {t("embed-chats.table.sender")} </th> <th scope="col" className="px-6 py-3"> - Message + {t("embed-chats.table.message")} </th> <th scope="col" className="px-6 py-3"> - Response + {t("embed-chats.table.response")} </th> <th scope="col" className="px-6 py-3"> - Sent At + {t("embed-chats.table.at")} </th> <th scope="col" className="px-6 py-3 rounded-tr-lg"> {" "} @@ -116,14 +118,14 @@ function ChatsContainer() { disabled={offset === 0} > {" "} - Previous Page + {t("common.previous")} </button> <button onClick={handleNext} className="px-4 py-2 rounded-lg border border-slate-200 text-slate-200 text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible" disabled={!canNext} > - Next Page + {t("common.next")} </button> </div> </> diff --git a/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx b/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx index 4d65e0d03..442a03d93 100644 --- a/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import Sidebar from "@/components/SettingsSidebar"; import { isMobile } from "react-device-detect"; import * as Skeleton from "react-loading-skeleton"; @@ -13,7 +14,7 @@ import CTAButton from "@/components/lib/CTAButton"; export default function EmbedConfigs() { const { isOpen, openModal, closeModal } = useModal(); - + const { t } = useTranslation(); return ( <div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <Sidebar /> @@ -25,18 +26,17 @@ export default function EmbedConfigs() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="items-center flex gap-x-4"> <p className="text-lg leading-6 font-bold text-white"> - Embeddable Chat Widgets + {t("embeddable.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - Embeddable chat widgets are public facing chat interfaces that are - tied to a single workspace. These allow you to build workspaces - that then you can publish to the world. + {t("embeddable.description")} </p> </div> <div className="w-full justify-end flex"> <CTAButton onClick={openModal} className="mt-3 mr-0 -mb-14 z-10"> - <CodeBlock className="h-4 w-4" weight="bold" /> Create embed + <CodeBlock className="h-4 w-4" weight="bold" />{" "} + {t("embeddable.create")} </CTAButton> </div> <EmbedContainer /> @@ -52,6 +52,7 @@ export default function EmbedConfigs() { function EmbedContainer() { const [loading, setLoading] = useState(true); const [embeds, setEmbeds] = useState([]); + const { t } = useTranslation(); useEffect(() => { async function fetchUsers() { @@ -81,13 +82,13 @@ function EmbedContainer() { <thead className="text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white border-b border-opacity-60"> <tr> <th scope="col" className="px-6 py-3 rounded-tl-lg"> - Workspace + {t("embeddable.table.workspace")} </th> <th scope="col" className="px-6 py-3"> - Sent Chats + {t("embeddable.table.chats")} </th> <th scope="col" className="px-6 py-3"> - Active Domains + {t("embeddable.table.Active")} </th> <th scope="col" className="px-6 py-3 rounded-tr-lg"> {" "} diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index 9c52c2714..2563aaadb 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -30,6 +30,7 @@ import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import { useModal } from "@/hooks/useModal"; import ModalWrapper from "@/components/ModalWrapper"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; const EMBEDDERS = [ { @@ -112,6 +113,7 @@ export default function GeneralEmbeddingPreference() { const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); const { isOpen, openModal, closeModal } = useModal(); + const { t } = useTranslation(); function embedderModelChanged(formEl) { try { @@ -223,17 +225,13 @@ export default function GeneralEmbeddingPreference() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Embedding Preference + {t("embedding.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - When using an LLM that does not natively support an embedding - engine - you may need to additionally specify credentials to - for embedding text. + {t("embedding.desc-start")} <br /> - Embedding is the process of turning text into vectors. These - credentials are required to turn your files and prompts into a - format which AnythingLLM can use to process. + {t("embedding.desc-end")} </p> </div> <div className="w-full justify-end flex"> @@ -242,12 +240,12 @@ export default function GeneralEmbeddingPreference() { onClick={() => handleSubmit()} className="mt-3 mr-0 -mb-14 z-10" > - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} </CTAButton> )} </div> <div className="text-base font-bold text-white mt-6 mb-4"> - Embedding Provider + {t("embedding.provider.title")} </div> <div className="relative"> {searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx index 5ee1197f1..1f30f71a2 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx @@ -6,6 +6,7 @@ import CTAButton from "@/components/lib/CTAButton"; import Admin from "@/models/admin"; import showToast from "@/utils/toast"; import { nFormatter, numberWithCommas } from "@/utils/numbers"; +import { useTranslation } from "react-i18next"; function isNullOrNaN(value) { if (value === null) return true; @@ -17,6 +18,7 @@ export default function EmbeddingTextSplitterPreference() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [hasChanges, setHasChanges] = useState(false); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -86,25 +88,22 @@ export default function EmbeddingTextSplitterPreference() { <div className="w-full flex flex-col gap-y-1 pb-4 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Text splitting & Chunking Preferences + {t("text.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - Sometimes, you may want to change the default way that new - documents are split and chunked before being inserted into - your vector database. <br /> - You should only modify this setting if you understand how text - splitting works and it's side effects. + {t("text.desc-start")} <br /> + {t("text.desc-end")} </p> <p className="text-xs leading-[18px] font-semibold text-white/80"> - Changes here will only apply to{" "} - <i>newly embedded documents</i>, not existing documents. + {t("text.warn-start")} <i>{t("text.warn-center")}</i> + {t("text.warn-end")} </p> </div> <div className="w-full justify-end flex"> {hasChanges && ( <CTAButton className="mt-3 mr-0 -mb-14 z-10"> - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} </CTAButton> )} </div> @@ -113,11 +112,10 @@ export default function EmbeddingTextSplitterPreference() { <div className="flex flex-col max-w-[300px]"> <div className="flex flex-col gap-y-2 mb-4"> <label className="text-white text-sm font-semibold block"> - Text Chunk Size + {t("text.size.title")} </label> <p className="text-xs text-white/60"> - This is the maximum length of characters that can be - present in a single vector. + {t("text.size.description")} </p> </div> <input @@ -137,7 +135,7 @@ export default function EmbeddingTextSplitterPreference() { autoComplete="off" /> <p className="text-xs text-white/40"> - Embed model maximum length is{" "} + {t("text.size.recommend")}{" "} {numberWithCommas(settings?.max_embed_chunk_size || 1000)}. </p> </div> @@ -147,11 +145,10 @@ export default function EmbeddingTextSplitterPreference() { <div className="flex flex-col max-w-[300px]"> <div className="flex flex-col gap-y-2 mb-4"> <label className="text-white text-sm font-semibold block"> - Text Chunk Overlap + {t("text.overlap.title")} </label> <p className="text-xs text-white/60"> - This is the maximum overlap of characters that occurs - during chunking between two adjacent text chunks. + {t("text.overlap.description")} </p> </div> <input diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index 407c33099..d38a99383 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import Sidebar from "@/components/SettingsSidebar"; import { isMobile } from "react-device-detect"; import System from "@/models/system"; @@ -232,6 +233,7 @@ export default function GeneralLLMPreference() { const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); const isHosted = window.location.hostname.includes("useanything.com"); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -310,14 +312,11 @@ export default function GeneralLLMPreference() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - LLM Preference + {t("llm.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are the credentials and settings for your preferred LLM - chat & embedding provider. Its important these keys are - current and correct or else AnythingLLM will not function - properly. + {t("llm.description")} </p> </div> <div className="w-full justify-end flex"> @@ -331,7 +330,7 @@ export default function GeneralLLMPreference() { )} </div> <div className="text-base font-bold text-white mt-6 mb-4"> - LLM Provider + {t("llm.provider")} </div> <div className="relative"> {searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx b/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx index 283470897..12d5a3e52 100644 --- a/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx +++ b/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx @@ -9,11 +9,12 @@ import { LLM_SELECTION_PRIVACY, VECTOR_DB_PRIVACY, } from "@/pages/OnboardingFlow/Steps/DataHandling"; +import { useTranslation } from "react-i18next"; export default function PrivacyAndDataHandling() { const [settings, setSettings] = useState({}); const [loading, setLoading] = useState(true); - + const { t } = useTranslation(); useEffect(() => { async function fetchSettings() { setLoading(true); @@ -35,12 +36,11 @@ export default function PrivacyAndDataHandling() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="items-center flex gap-x-4"> <p className="text-lg leading-6 font-bold text-white"> - Privacy & Data-Handling + {t("privacy.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - This is your configuration for how connected third party providers - and AnythingLLM handle your data. + {t("privacy.description")} </p> </div> {loading ? ( @@ -65,12 +65,15 @@ function ThirdParty({ settings }) { const llmChoice = settings?.LLMProvider || "openai"; const embeddingEngine = settings?.EmbeddingEngine || "openai"; const vectorDb = settings?.VectorDB || "lancedb"; + const { t } = useTranslation(); return ( <div className="py-8 w-full flex items-start justify-center flex-col gap-y-6 border-b-2 border-white/10"> <div className="flex flex-col gap-8"> <div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4"> - <div className="text-white text-base font-bold">LLM Selection</div> + <div className="text-white text-base font-bold"> + {t("privacy.llm")} + </div> <div className="flex items-center gap-2.5"> <img src={LLM_SELECTION_PRIVACY[llmChoice].logo} @@ -89,7 +92,7 @@ function ThirdParty({ settings }) { </div> <div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4"> <div className="text-white text-base font-bold"> - Embedding Preference + {t("privacy.embedding")} </div> <div className="flex items-center gap-2.5"> <img @@ -111,7 +114,9 @@ function ThirdParty({ settings }) { </div> <div className="flex flex-col gap-y-2 pb-4"> - <div className="text-white text-base font-bold">Vector Database</div> + <div className="text-white text-base font-bold"> + {t("privacy.vector")} + </div> <div className="flex items-center gap-2.5"> <img src={VECTOR_DB_PRIVACY[vectorDb].logo} @@ -137,6 +142,7 @@ function TelemetryLogs({ settings }) { const [telemetry, setTelemetry] = useState( settings?.DisableTelemetry !== "true" ); + const { t } = useTranslation(); async function toggleTelemetry() { await System.updateSystem({ DisableTelemetry: !telemetry ? "false" : "true", @@ -157,7 +163,7 @@ function TelemetryLogs({ settings }) { <div className="w-full flex flex-col gap-y-4"> <div className=""> <label className="mb-2.5 block font-medium text-white"> - Anonymous Telemetry Enabled + {t("privacy.anonymous")} </label> <label className="relative inline-flex cursor-pointer items-center"> <input diff --git a/frontend/src/pages/GeneralSettings/Security/index.jsx b/frontend/src/pages/GeneralSettings/Security/index.jsx index 94655fea6..9cab96509 100644 --- a/frontend/src/pages/GeneralSettings/Security/index.jsx +++ b/frontend/src/pages/GeneralSettings/Security/index.jsx @@ -7,6 +7,7 @@ import paths from "@/utils/paths"; import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants"; import PreLoader from "@/components/Preloader"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; export default function GeneralSecurity() { return ( @@ -29,6 +30,7 @@ function MultiUserMode() { const [useMultiUserMode, setUseMultiUserMode] = useState(false); const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false); const [loading, setLoading] = useState(true); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -90,12 +92,11 @@ function MultiUserMode() { <div className="w-full flex flex-col gap-y-1"> <div className="items-center flex gap-x-4"> <p className="text-lg leading-6 font-bold text-white"> - Multi-User Mode + {t("multi.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - Set up your instance to support your team by activating Multi-User - Mode. + {t("multi.description")} </p> </div> {hasChanges && ( @@ -104,7 +105,7 @@ function MultiUserMode() { onClick={() => handleSubmit()} className="mt-3 mr-0 -mb-20 z-10" > - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} </CTAButton> </div> )} @@ -116,8 +117,8 @@ function MultiUserMode() { <div className=""> <label className="mb-2.5 block font-medium text-white"> {multiUserModeEnabled - ? "Multi-User Mode is Enabled" - : "Enable Multi-User Mode"} + ? t("multi.enable.is-enable") + : t("multi.enable.enable")} </label> <label className="relative inline-flex cursor-pointer items-center"> @@ -140,7 +141,7 @@ function MultiUserMode() { htmlFor="username" className="block mb-3 font-medium text-white" > - Admin account username + {t("multi.enable.username")} </label> <input name="username" @@ -159,7 +160,7 @@ function MultiUserMode() { htmlFor="password" className="block mb-3 font-medium text-white" > - Admin account password + {t("multi.enable.password")} </label> <input name="password" @@ -178,9 +179,7 @@ function MultiUserMode() { </div> <div className="flex items-center justify-between space-x-14"> <p className="text-white/80 text-xs rounded-lg w-96"> - By default, you will be the only admin. As an admin you will - need to create accounts for all new users or admins. Do not lose - your password as only an Admin user can reset passwords. + {t("multi.enable.description")} </p> </div> </div> @@ -197,6 +196,7 @@ function PasswordProtection() { const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false); const [usePassword, setUsePassword] = useState(false); const [loading, setLoading] = useState(true); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -269,12 +269,11 @@ function PasswordProtection() { <div className="w-full flex flex-col gap-y-1"> <div className="items-center flex gap-x-4"> <p className="text-lg leading-6 font-bold text-white"> - Password Protection + {t("multi.password.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - Protect your AnythingLLM instance with a password. If you forget - this there is no recovery method so ensure you save this password. + {t("multi.password.description")} </p> </div> {hasChanges && ( @@ -283,7 +282,7 @@ function PasswordProtection() { onClick={() => handleSubmit()} className="mt-3 mr-0 -mb-20 z-10" > - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} </CTAButton> </div> )} @@ -294,7 +293,7 @@ function PasswordProtection() { <div className="w-full flex flex-col gap-y-4"> <div className=""> <label className="mb-2.5 block font-medium text-white"> - Password Protect Instance + {t("multi.instance.title")} </label> <label className="relative inline-flex cursor-pointer items-center"> @@ -314,7 +313,7 @@ function PasswordProtection() { htmlFor="password" className="block mb-3 font-medium text-white" > - Instance password + {t("multi.instance.password")} </label> <input name="password" @@ -333,9 +332,7 @@ function PasswordProtection() { </div> <div className="flex items-center justify-between space-x-14"> <p className="text-white/80 text-xs rounded-lg w-96"> - By default, anyone with this password can log into the instance. - Do not lose this password as only the instance maintainer is - able to retrieve or reset the password once set. + {t("multi.instance.description")} </p> </div> </div> diff --git a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx index 35a0622e7..07c5ecae8 100644 --- a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx @@ -11,6 +11,7 @@ import NativeTranscriptionOptions from "@/components/TranscriptionSelection/Nati import LLMItem from "@/components/LLMSelection/LLMItem"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; const PROVIDERS = [ { @@ -39,6 +40,7 @@ export default function TranscriptionModelPreference() { const [selectedProvider, setSelectedProvider] = useState(null); const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -118,14 +120,11 @@ export default function TranscriptionModelPreference() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Transcription Model Preference + {t("transcription.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are the credentials and settings for your preferred - transcription model provider. Its important these keys are - current and correct or else media files and audio will not - transcribe. + {t("transcription.description")} </p> </div> <div className="w-full justify-end flex"> @@ -139,7 +138,7 @@ export default function TranscriptionModelPreference() { )} </div> <div className="text-base font-bold text-white mt-6 mb-4"> - Transcription Provider + {t("transcription.provider")} </div> <div className="relative"> {searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 15dad4645..48c1c971f 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -26,6 +26,7 @@ import { useModal } from "@/hooks/useModal"; import ModalWrapper from "@/components/ModalWrapper"; import AstraDBOptions from "@/components/VectorDBSelection/AstraDBOptions"; import CTAButton from "@/components/lib/CTAButton"; +import { useTranslation } from "react-i18next"; export default function GeneralVectorDatabase() { const [saving, setSaving] = useState(false); @@ -39,6 +40,7 @@ export default function GeneralVectorDatabase() { const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); const { isOpen, openModal, closeModal } = useModal(); + const { t } = useTranslation(); const handleSubmit = async (e) => { e.preventDefault(); @@ -194,13 +196,11 @@ export default function GeneralVectorDatabase() { <div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10"> <div className="flex gap-x-4 items-center"> <p className="text-lg leading-6 font-bold text-white"> - Vector Database + {t("vector.title")} </p> </div> <p className="text-xs leading-[18px] font-base text-white text-opacity-60"> - These are the credentials and settings for how your - AnythingLLM instance will function. It's important these keys - are current and correct. + {t("vector.description")} </p> </div> <div className="w-full justify-end flex"> @@ -209,12 +209,12 @@ export default function GeneralVectorDatabase() { onClick={() => handleSubmit()} className="mt-3 mr-0 -mb-14 z-10" > - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} </CTAButton> )} </div> <div className="text-base font-bold text-white mt-6 mb-4"> - Vector Database Provider + {t("vector.provider.title")} </div> <div className="relative"> {searchMenuOpen && ( diff --git a/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx index bd4615d0b..fb28cc984 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx @@ -4,6 +4,7 @@ import paths from "@/utils/paths"; import showToast from "@/utils/toast"; import { useNavigate } from "react-router-dom"; import Workspace from "@/models/workspace"; +import { useTranslation } from "react-i18next"; const TITLE = "Create your first workspace"; const DESCRIPTION = @@ -17,6 +18,7 @@ export default function CreateWorkspace({ const [workspaceName, setWorkspaceName] = useState(""); const navigate = useNavigate(); const createWorkspaceRef = useRef(); + const { t } = useTranslation(); useEffect(() => { setHeader({ title: TITLE, description: DESCRIPTION }); @@ -71,7 +73,7 @@ export default function CreateWorkspace({ htmlFor="name" className="block mb-3 text-sm font-medium text-white" > - Workspace Name + {t("common.workspaces-name")} </label> <input name="name" diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx index e5d226a07..ac507e929 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx @@ -4,6 +4,7 @@ import AgentLLMItem from "./AgentLLMItem"; import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { CaretUpDown, Gauge, MagnifyingGlass, X } from "@phosphor-icons/react"; import AgentModelSelection from "../AgentModelSelection"; +import { useTranslation } from "react-i18next"; const ENABLED_PROVIDERS = [ "openai", @@ -65,7 +66,7 @@ export default function AgentLLMSelection({ const [searchQuery, setSearchQuery] = useState(""); const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); - + const { t } = useTranslation(); function updateLLMChoice(selection) { setSearchQuery(""); setSelectedLLM(selection); @@ -96,22 +97,17 @@ export default function AgentLLMSelection({ <div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2"> <div className="gap-x-2 flex items-center"> <Gauge className="shrink-0" size={25} /> - <p className="text-sm"> - Performance of LLMs that do not explicitly support tool-calling is - highly dependent on the model's capabilities and accuracy. Some - abilities may be limited or non-functional. - </p> + <p className="text-sm">{t("agent.performance-warning")}</p> </div> </div> )} <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Agent LLM Provider + {t("agent.provider.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific LLM provider & model that will be used for this - workspace's @agent agent. + {t("agent.provider.description")} </p> </div> diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx index bf51cb87e..270f22ef9 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx @@ -1,6 +1,7 @@ import useGetProviderModels, { DISABLED_PROVIDERS, } from "@/hooks/useGetProvidersModels"; +import { useTranslation } from "react-i18next"; // These models do NOT support function calling function supportedModel(provider, model = "") { @@ -19,6 +20,8 @@ export default function AgentModelSelection({ }) { const { defaultModels, customModels, loading } = useGetProviderModels(provider); + + const { t } = useTranslation(); if (DISABLED_PROVIDERS.includes(provider)) return null; if (loading) { @@ -26,11 +29,10 @@ export default function AgentModelSelection({ <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Agent Chat model + {t("agent.mode.chat.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific chat model that will be used for this workspace's - @agent agent. + {t("agent.mode.chat.description")} </p> </div> <select @@ -40,7 +42,7 @@ export default function AgentModelSelection({ className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" > <option disabled={true} selected={true}> - -- waiting for models -- + {t("agent.mode.wait")} </option> </select> </div> @@ -51,11 +53,10 @@ export default function AgentModelSelection({ <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Agent model + {t("agent.mode.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific LLM model that will be used for this workspace's @agent - agent. + {t("agent.mode.description")} </p> </div> diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx new file mode 100644 index 000000000..09eba16b5 --- /dev/null +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx @@ -0,0 +1,204 @@ +import React, { useEffect, useRef, useState } from "react"; +import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; +import GoogleSearchIcon from "./icons/google.png"; +import SerperDotDevIcon from "./icons/serper.png"; +import BingSearchIcon from "./icons/bing.png"; +import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; +import SearchProviderItem from "./SearchProviderItem"; +import { + SerperDotDevOptions, + GoogleSearchOptions, + BingSearchOptions, +} from "./SearchProviderOptions"; +import { useTranslation } from "react-i18next"; + +const SEARCH_PROVIDERS = [ + { + name: "Please make a selection", + value: "none", + logo: AnythingLLMIcon, + options: () => <React.Fragment />, + description: + "Web search will be disabled until a provider and keys are provided.", + }, + { + name: "Google Search Engine", + value: "google-search-engine", + logo: GoogleSearchIcon, + options: (settings) => <GoogleSearchOptions settings={settings} />, + description: + "Web search powered by a custom Google Search Engine. Free for 100 queries per day.", + }, + { + name: "Serper.dev", + value: "serper-dot-dev", + logo: SerperDotDevIcon, + options: (settings) => <SerperDotDevOptions settings={settings} />, + description: + "Serper.dev web-search. Free account with a 2,500 calls, but then paid.", + }, + { + name: "Bing Search", + value: "bing-search", + logo: BingSearchIcon, + options: (settings) => <BingSearchOptions settings={settings} />, + description: + "Web search powered by the Bing Search API. Free for 1000 queries per month.", + }, +]; + +export default function AgentWebSearchSelection({ + skill, + settings, + toggleSkill, + enabled = false, +}) { + const searchInputRef = useRef(null); + const [filteredResults, setFilteredResults] = useState([]); + const [selectedProvider, setSelectedProvider] = useState("none"); + const [searchQuery, setSearchQuery] = useState(""); + const [searchMenuOpen, setSearchMenuOpen] = useState(false); + const { t } = useTranslation(); + function updateChoice(selection) { + setSearchQuery(""); + setSelectedProvider(selection); + setSearchMenuOpen(false); + } + + function handleXButton() { + if (searchQuery.length > 0) { + setSearchQuery(""); + if (searchInputRef.current) searchInputRef.current.value = ""; + } else { + setSearchMenuOpen(!searchMenuOpen); + } + } + + useEffect(() => { + const filtered = SEARCH_PROVIDERS.filter((provider) => + provider.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredResults(filtered); + }, [searchQuery, selectedProvider]); + + useEffect(() => { + setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none"); + }, [settings?.preferences?.agent_search_provider]); + + const selectedSearchProviderObject = SEARCH_PROVIDERS.find( + (provider) => provider.value === selectedProvider + ); + + return ( + <div className="border-b border-white/40 pb-4"> + <div className="flex flex-col"> + <div className="flex w-full justify-between items-center"> + <label htmlFor="name" className="block input-label"> + {t("agent.skill.web.title")} + </label> + <label className="border-none relative inline-flex cursor-pointer items-center mt-2"> + <input + type="checkbox" + className="peer sr-only" + checked={enabled} + onClick={() => toggleSkill(skill)} + /> + <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> + <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> + </label> + </div> + <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> + {t("agent.skill.web.desc-start")} + <br /> + {t("agent.skill.web.desc-end")} + </p> + </div> + <div hidden={!enabled}> + <div className="relative"> + <input + type="hidden" + name="system::agent_search_provider" + value={selectedProvider} + /> + {searchMenuOpen && ( + <div + className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10" + onClick={() => setSearchMenuOpen(false)} + /> + )} + {searchMenuOpen ? ( + <div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20"> + <div className="w-full flex flex-col gap-y-1"> + <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]"> + <MagnifyingGlass + size={20} + weight="bold" + className="absolute left-4 z-30 text-white -ml-4 my-2" + /> + <input + type="text" + name="web-provider-search" + autoComplete="off" + placeholder="Search available web-search providers" + className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium" + onChange={(e) => setSearchQuery(e.target.value)} + ref={searchInputRef} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); + }} + /> + <X + size={20} + weight="bold" + className="cursor-pointer text-white hover:text-[#9CA3AF]" + onClick={handleXButton} + /> + </div> + <div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4"> + {filteredResults.map((provider) => { + return ( + <SearchProviderItem + provider={provider} + key={provider.name} + checked={selectedProvider === provider.value} + onClick={() => updateChoice(provider.value)} + /> + ); + })} + </div> + </div> + </div> + ) : ( + <button + className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300" + type="button" + onClick={() => setSearchMenuOpen(true)} + > + <div className="flex gap-x-4 items-center"> + <img + src={selectedSearchProviderObject.logo} + alt={`${selectedSearchProviderObject.name} logo`} + className="w-10 h-10 rounded-md" + /> + <div className="flex flex-col text-left"> + <div className="text-sm font-semibold text-white"> + {selectedSearchProviderObject.name} + </div> + <div className="mt-1 text-xs text-[#D2D5DB]"> + {selectedSearchProviderObject.description} + </div> + </div> + </div> + <CaretUpDown size={24} weight="bold" className="text-white" /> + </button> + )} + </div> + {selectedProvider !== "none" && ( + <div className="mt-4 flex flex-col gap-y-1"> + {selectedSearchProviderObject.options(settings)} + </div> + )} + </div> + </div> + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx index 092c055c4..faa621031 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx @@ -1,16 +1,16 @@ +import { useTranslation } from "react-i18next"; export default function ChatHistorySettings({ workspace, setHasChanges }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col gap-y-1 mb-4"> <label htmlFor="name" className="block mb-2 input-label"> - Chat History + {t("chat.history.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium"> - The number of previous chats that will be included in the - response's short-term memory. - <i>Recommend 20. </i> - Anything more than 45 is likely to lead to continuous chat failures - depending on message size. + {t("chat.history.desc-start")} + <i> {t("chat.history.recommend")} </i> + {t("chat.history.desc-end")} </p> </div> <input diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModeSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModeSelection/index.jsx index 0b3bb3b71..135cbf9e6 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModeSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModeSelection/index.jsx @@ -1,12 +1,13 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; export default function ChatModeSelection({ workspace, setHasChanges }) { const [chatMode, setChatMode] = useState(workspace?.chatMode || "chat"); - + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="chatMode" className="block input-label"> - Chat mode + {t("chat.mode.title")} </label> </div> @@ -22,7 +23,7 @@ export default function ChatModeSelection({ workspace, setHasChanges }) { }} className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md" > - Chat + {t("chat.mode.chat.title")} </button> <button type="button" @@ -33,21 +34,23 @@ export default function ChatModeSelection({ workspace, setHasChanges }) { }} className="transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md" > - Query + {t("chat.mode.query.title")} </button> </div> <p className="text-sm text-white/60"> {chatMode === "chat" ? ( <> - <b>Chat</b> will provide answers with the LLM's general knowledge{" "} - <i className="font-semibold">and</i> document context that is - found. + <b>{t("chat.mode.chat.title")}</b>{" "} + {t("chat.mode.chat.desc-start")}{" "} + <i className="font-semibold">{t("chat.mode.chat.and")}</i>{" "} + {t("chat.mode.chat.desc-end")} </> ) : ( <> - <b>Query</b> will provide answers{" "} - <i className="font-semibold">only</i> if document context is - found. + <b>{t("chat.mode.query.title")}</b>{" "} + {t("chat.mode.query.desc-start")}{" "} + <i className="font-semibold">{t("chat.mode.query.only")}</i>{" "} + {t("chat.mode.query.desc-end")} </> )} </p> diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx index 9ed424294..71d943e5e 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx @@ -1,7 +1,7 @@ import useGetProviderModels, { DISABLED_PROVIDERS, } from "@/hooks/useGetProvidersModels"; - +import { useTranslation } from "react-i18next"; export default function ChatModelSelection({ provider, workspace, @@ -9,6 +9,7 @@ export default function ChatModelSelection({ }) { const { defaultModels, customModels, loading } = useGetProviderModels(provider); + const { t } = useTranslation(); if (DISABLED_PROVIDERS.includes(provider)) return null; if (loading) { @@ -16,11 +17,10 @@ export default function ChatModelSelection({ <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Chat model + {t("chat.model.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific chat model that will be used for this workspace. If - empty, will use the system LLM preference. + {t("chat.model.description")} </p> </div> <select @@ -41,11 +41,10 @@ export default function ChatModelSelection({ <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Chat model + {t("chat.model.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific chat model that will be used for this workspace. If - empty, will use the system LLM preference. + {t("chat.model.description")} </p> </div> diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx index 48ad46309..0b50e941d 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx @@ -1,17 +1,15 @@ import { chatPrompt } from "@/utils/chat"; - +import { useTranslation } from "react-i18next"; export default function ChatPromptSettings({ workspace, setHasChanges }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Prompt + {t("chat.prompt.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The prompt that will be used on this workspace. Define the context and - instructions for the AI to generate a response. You should to provide - a carefully crafted prompt so the AI can generate a relevant and - accurate response. + {t("chat.prompt.description")} </p> </div> <textarea diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatQueryRefusalResponse/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatQueryRefusalResponse/index.jsx index 2b8570dba..f540c3bcb 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatQueryRefusalResponse/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatQueryRefusalResponse/index.jsx @@ -1,16 +1,19 @@ import { chatQueryRefusalResponse } from "@/utils/chat"; - +import { useTranslation } from "react-i18next"; export default function ChatQueryRefusalResponse({ workspace, setHasChanges }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Query mode refusal response + {t("chat.refusal.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - When in <code className="bg-zinc-900 p-0.5 rounded-sm">query</code>{" "} - mode, you may want to return a custom refusal response when no context - is found. + {t("chat.refusal.desc-start")}{" "} + <code className="bg-zinc-900 p-0.5 rounded-sm"> + {t("chat.refusal.query")} + </code>{" "} + {t("chat.refusal.desc-end")} </p> </div> <textarea diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx index 08565f588..a4478e318 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; function recommendedSettings(provider = null) { switch (provider) { case "mistral": @@ -13,24 +14,20 @@ export default function ChatTemperatureSettings({ setHasChanges, }) { const defaults = recommendedSettings(settings?.LLMProvider); + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - LLM Temperature + {t("chat.temperature.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - This setting controls how "creative" your LLM responses will - be. + {t("chat.temperature.desc-start")} <br /> - The higher the number the more creative. For some models this can lead - to incoherent responses when set too high. + {t("chat.temperature.desc-end")} <br /> <br /> - <i> - Most LLMs have various acceptable ranges of valid values. Consult - your LLM provider for that information. - </i> + <i>{t("chat.temperature.hint")}</i> </p> </div> <input diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/index.jsx index 2d82307ed..7542f2d76 100644 --- a/frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/index.jsx @@ -4,6 +4,7 @@ import WorkspaceLLMItem from "./WorkspaceLLMItem"; import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; import ChatModelSelection from "../ChatModelSelection"; +import { useTranslation } from "react-i18next"; // Some providers can only be associated with a single model. // In that case there is no selection to be made so we can just move on. @@ -34,7 +35,7 @@ export default function WorkspaceLLMSelection({ const [searchQuery, setSearchQuery] = useState(""); const [searchMenuOpen, setSearchMenuOpen] = useState(false); const searchInputRef = useRef(null); - + const { t } = useTranslation(); function updateLLMChoice(selection) { setSearchQuery(""); setSelectedLLM(selection); @@ -63,11 +64,10 @@ export default function WorkspaceLLMSelection({ <div className="border-b border-white/40 pb-8"> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace LLM Provider + {t("chat.llm.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The specific LLM provider & model that will be used for this - workspace. By default, it uses the system LLM provider and settings. + {t("chat.llm.description")} </p> </div> @@ -92,7 +92,7 @@ export default function WorkspaceLLMSelection({ type="text" name="llm-search" autoComplete="off" - placeholder="Search all LLM providers" + placeholder={t("chat.llm.search")} className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium" onChange={(e) => setSearchQuery(e.target.value)} ref={searchInputRef} diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/DeleteWorkspace/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/DeleteWorkspace/index.jsx index 56fef1ad9..32a3eaa4e 100644 --- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/DeleteWorkspace/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/DeleteWorkspace/index.jsx @@ -3,12 +3,13 @@ import { useParams } from "react-router-dom"; import Workspace from "@/models/workspace"; import paths from "@/utils/paths"; import System from "@/models/system"; +import { useTranslation } from "react-i18next"; export default function DeleteWorkspace({ workspace }) { const { slug } = useParams(); const [deleting, setDeleting] = useState(false); const [canDelete, setCanDelete] = useState(false); - + const { t } = useTranslation(); useEffect(() => { async function fetchKeys() { const canDelete = await System.getCanDeleteWorkspaces(); @@ -20,7 +21,9 @@ export default function DeleteWorkspace({ workspace }) { const deleteWorkspace = async () => { if ( !window.confirm( - `You are about to delete your entire ${workspace.name} workspace. This will remove all vector embeddings on your vector database.\n\nThe original source files will remain untouched. This action is irreversible.` + `${t("general.delete.confirm-start")} ${workspace.name} ${t( + "general.delete.confirm-end" + )}` ) ) return false; @@ -46,7 +49,7 @@ export default function DeleteWorkspace({ workspace }) { type="button" className="w-60 mt-[40px] transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse" > - {deleting ? "Deleting Workspace..." : "Delete Workspace"} + {deleting ? t("general.delete.deleting") : t("general.delete.delete")} </button> ); } diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx index 5ac15f218..23fc77c26 100644 --- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx @@ -3,6 +3,7 @@ import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; import { useEffect, useState } from "react"; import { Plus, X } from "@phosphor-icons/react"; +import { useTranslation } from "react-i18next"; export default function SuggestedChatMessages({ slug }) { const [suggestedMessages, setSuggestedMessages] = useState([]); @@ -10,7 +11,7 @@ export default function SuggestedChatMessages({ slug }) { const [newMessage, setNewMessage] = useState({ heading: "", message: "" }); const [hasChanges, setHasChanges] = useState(false); const [loading, setLoading] = useState(true); - + const { t } = useTranslation(); useEffect(() => { async function fetchWorkspace() { if (!slug) return; @@ -45,8 +46,8 @@ export default function SuggestedChatMessages({ slug }) { return; } const defaultMessage = { - heading: "Explain to me", - message: "the benefits of AnythingLLM", + heading: t("general.message.heading"), + message: t("general.message.body"), }; setNewMessage(defaultMessage); setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]); @@ -91,9 +92,11 @@ export default function SuggestedChatMessages({ slug }) { if (loading) return ( <div className="flex flex-col"> - <label className="block input-label">Suggested Chat Messages</label> + <label className="block input-label"> + {t("general.message.title")} + </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Customize the messages that will be suggested to your workspace users. + {t("general.message.description")} </p> <p className="text-white text-opacity-60 text-sm font-medium mt-6"> <PreLoader size="4" /> @@ -103,9 +106,11 @@ export default function SuggestedChatMessages({ slug }) { return ( <div className="w-screen mt-6"> <div className="flex flex-col"> - <label className="block input-label">Suggested Chat Messages</label> + <label className="block input-label"> + {t("general.message.title")} + </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Customize the messages that will be suggested to your workspace users. + {t("general.message.description")} </p> </div> @@ -169,7 +174,8 @@ export default function SuggestedChatMessages({ slug }) { onClick={addMessage} className="flex gap-x-2 items-center justify-center mt-6 text-white text-sm hover:text-sky-400 transition-all duration-300" > - Add new message <Plus className="" size={24} weight="fill" /> + {t("general.message.add")}{" "} + <Plus className="" size={24} weight="fill" /> </button> )} @@ -180,7 +186,7 @@ export default function SuggestedChatMessages({ slug }) { className="transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" onClick={handleSaveSuggestedMessages} > - Save Messages + {t("general.message.save")} </button> </div> )} diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspaceName/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspaceName/index.jsx index d1a929810..efa2e9ae8 100644 --- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspaceName/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspaceName/index.jsx @@ -1,12 +1,15 @@ +import { useTranslation } from "react-i18next"; + export default function WorkspaceName({ workspace, setHasChanges }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Workspace Name + {t("common.workspaces-name")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - This will only change the display name of your workspace. + {t("general.names.description")} </p> </div> <input diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx index e9fb8303a..e652c734c 100644 --- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspacePfp/index.jsx @@ -2,10 +2,11 @@ import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; import { Plus } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; export default function WorkspacePfp({ workspace, slug }) { const [pfp, setPfp] = useState(null); - + const { t } = useTranslation(); useEffect(() => { async function fetchWorkspace() { const pfpUrl = await Workspace.fetchPfp(slug); @@ -47,9 +48,9 @@ export default function WorkspacePfp({ workspace, slug }) { return ( <div className="mt-6"> <div className="flex flex-col"> - <label className="block input-label">Assistant Profile Image</label> + <label className="block input-label">{t("general.pfp.title")}</label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Customize the profile image of the assistant for this workspace. + {t("general.pfp.description")} </p> </div> <div className="flex flex-col md:flex-row items-center gap-8"> @@ -72,7 +73,7 @@ export default function WorkspacePfp({ workspace, slug }) { <div className="flex flex-col items-center justify-center p-3"> <Plus className="w-8 h-8 text-white/80 m-2" /> <span className="text-white text-opacity-80 text-xs font-semibold"> - Workspace Image + {t("general.pfp.image")} </span> <span className="text-white text-opacity-60 text-xs"> 800 x 800 @@ -86,7 +87,7 @@ export default function WorkspacePfp({ workspace, slug }) { onClick={handleRemovePfp} className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline" > - Remove Workspace Image + {t("general.pfp.remove")} </button> )} </div> diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/DocumentSimilarityThreshold/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/DocumentSimilarityThreshold/index.jsx index 122e1144b..c14d86bb4 100644 --- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/DocumentSimilarityThreshold/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/DocumentSimilarityThreshold/index.jsx @@ -1,17 +1,18 @@ +import { useTranslation } from "react-i18next"; + export default function DocumentSimilarityThreshold({ workspace, setHasChanges, }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Document similarity threshold + {t("vector-workspace.doc.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - The minimum similarity score required for a source to be considered - related to the chat. The higher the number, the more similar the - source must be to the chat. + {t("vector-workspace.doc.description")} </p> </div> <select @@ -21,10 +22,10 @@ export default function DocumentSimilarityThreshold({ onChange={() => setHasChanges(true)} required={true} > - <option value={0.0}>No restriction</option> - <option value={0.25}>Low (similarity score ≥ .25)</option> - <option value={0.5}>Medium (similarity score ≥ .50)</option> - <option value={0.75}>High (similarity score ≥ .75)</option> + <option value={0.0}>{t("vector-workspace.doc.zero")}</option> + <option value={0.25}>{t("vector-workspace.doc.low")}</option> + <option value={0.5}>{t("vector-workspace.doc.medium")}</option> + <option value={0.75}>{t("vector-workspace.doc.high")}</option> </select> </div> ); diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/MaxContextSnippets/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/MaxContextSnippets/index.jsx index c66ccfd71..80aecdc9c 100644 --- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/MaxContextSnippets/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/MaxContextSnippets/index.jsx @@ -1,15 +1,17 @@ +import { useTranslation } from "react-i18next"; + export default function MaxContextSnippets({ workspace, setHasChanges }) { + const { t } = useTranslation(); return ( <div> <div className="flex flex-col"> <label htmlFor="name" className="block input-label"> - Max Context Snippets + {t("vector-workspace.snippets.title")} </label> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - This setting controls the maximum amount of context snippets the will - be sent to the LLM for per chat or query. + {t("vector-workspace.snippets.description")} <br /> - <i>Recommended: 4</i> + <i>{t("vector-workspace.snippets.recommend")}</i> </p> </div> <input diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/ResetDatabase/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/ResetDatabase/index.jsx index e3e2730cd..44bf12d64 100644 --- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/ResetDatabase/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/ResetDatabase/index.jsx @@ -1,31 +1,35 @@ import { useState } from "react"; import Workspace from "@/models/workspace"; import showToast from "@/utils/toast"; +import { useTranslation } from "react-i18next"; export default function ResetDatabase({ workspace }) { const [deleting, setDeleting] = useState(false); - + const { t } = useTranslation(); const resetVectorDatabase = async () => { - if ( - !window.confirm( - `You are about to reset this workspace's vector database. This will remove all vector embeddings currently embedded.\n\nThe original source files will remain untouched. This action is irreversible.` - ) - ) - return false; + if (!window.confirm(`${t("vector-workspace.reset.confirm")}`)) return false; setDeleting(true); const success = await Workspace.wipeVectorDb(workspace.slug); if (!success) { - showToast("Workspace vector database could not be reset!", "error", { - clear: true, - }); + showToast( + t("vector-workspace.reset.error"), + t("vector-workspace.common.error"), + { + clear: true, + } + ); setDeleting(false); return; } - showToast("Workspace vector database was reset!", "success", { - clear: true, - }); + showToast( + t("vector-workspace.reset.success"), + t("vector-workspace.common.success"), + { + clear: true, + } + ); setDeleting(false); }; @@ -36,7 +40,9 @@ export default function ResetDatabase({ workspace }) { type="button" className="border-none w-fit transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 hover:text-white hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse" > - {deleting ? "Clearing vectors..." : "Reset Workspace Vector Database"} + {deleting + ? t("vector-workspace.reset.resetting") + : t("vector-workspace.reset.reset")} </button> ); } diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorCount/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorCount/index.jsx index 173acf8e3..b8ea84c5a 100644 --- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorCount/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorCount/index.jsx @@ -1,9 +1,11 @@ import PreLoader from "@/components/Preloader"; import System from "@/models/system"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; export default function VectorCount({ reload, workspace }) { const [totalVectors, setTotalVectors] = useState(null); + const { t } = useTranslation(); useEffect(() => { async function fetchVectorCount() { @@ -16,9 +18,9 @@ export default function VectorCount({ reload, workspace }) { if (totalVectors === null) return ( <div> - <h3 className="input-label">Number of vectors</h3> + <h3 className="input-label">{t("general.vector.title")}</h3> <p className="text-white text-opacity-60 text-xs font-medium py-1"> - Total number of vectors in your vector database. + {t("general.vector.description")} </p> <p className="text-white text-opacity-60 text-sm font-medium"> <PreLoader size="4" /> @@ -27,7 +29,7 @@ export default function VectorCount({ reload, workspace }) { ); return ( <div> - <h3 className="input-label">Number of vectors</h3> + <h3 className="input-label">{t("general.vector.title")}</h3> <p className="text-white text-opacity-60 text-sm font-medium"> {totalVectors} </p> diff --git a/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorDBIdentifier/index.jsx b/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorDBIdentifier/index.jsx index 9140d7fc0..110129942 100644 --- a/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorDBIdentifier/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorDBIdentifier/index.jsx @@ -1,7 +1,10 @@ +import { useTranslation } from "react-i18next"; + export default function VectorDBIdentifier({ workspace }) { + const { t } = useTranslation(); return ( <div> - <h3 className="input-label">Vector database identifier</h3> + <h3 className="input-label">{t("vector-workspace.identifier")}</h3> <p className="text-white/60 text-xs font-medium py-1"> </p> <p className="text-white/60 text-sm">{workspace?.slug}</p> </div> diff --git a/frontend/src/pages/WorkspaceSettings/index.jsx b/frontend/src/pages/WorkspaceSettings/index.jsx index 01643174e..888e59566 100644 --- a/frontend/src/pages/WorkspaceSettings/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/index.jsx @@ -22,6 +22,7 @@ import VectorDatabase from "./VectorDatabase"; import Members from "./Members"; import WorkspaceAgentConfiguration from "./AgentConfig"; import useUser from "@/hooks/useUser"; +import { useTranslation } from "react-i18next"; const TABS = { "general-appearance": GeneralAppearance, @@ -43,6 +44,7 @@ export default function WorkspaceSettings() { } function ShowWorkspaceChat() { + const { t } = useTranslation(); const { slug, tab } = useParams(); const { user } = useUser(); const [workspace, setWorkspace] = useState(null); @@ -85,28 +87,28 @@ function ShowWorkspaceChat() { <ArrowUUpLeft className="h-5 w-5" weight="fill" /> </Link> <TabItem - title="General Settings" + title={t("workspaces—settings.general")} icon={<Wrench className="h-6 w-6" />} to={paths.workspace.settings.generalAppearance(slug)} /> <TabItem - title="Chat Settings" + title={t("workspaces—settings.chat")} icon={<ChatText className="h-6 w-6" />} to={paths.workspace.settings.chatSettings(slug)} /> <TabItem - title="Vector Database" + title={t("workspaces—settings.vector")} icon={<Database className="h-6 w-6" />} to={paths.workspace.settings.vectorDatabase(slug)} /> <TabItem - title="Members" + title={t("workspaces—settings.members")} icon={<User className="h-6 w-6" />} to={paths.workspace.settings.members(slug)} visible={["admin", "manager"].includes(user?.role)} /> <TabItem - title="Agent Configuration" + title={t("workspaces—settings.agent")} icon={<Robot className="h-6 w-6" />} to={paths.workspace.settings.agentConfig(slug)} /> diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d5bdc0d6f..82d68d027 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -191,6 +191,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9": + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1956,6 +1963,13 @@ highlight.js@^11.9.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0" integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw== +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html2canvas@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543" @@ -1974,6 +1988,20 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== +i18next-browser-languagedetector@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f" + integrity sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@^23.11.3: + version "23.11.3" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.3.tgz#d269c9c15bae9d90ab291055cfc433089ca5f77b" + integrity sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg== + dependencies: + "@babel/runtime" "^7.23.2" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -2807,6 +2835,14 @@ react-dropzone@^14.2.3: file-selector "^0.6.0" prop-types "^15.8.1" +react-i18next@^14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.1.tgz#3d942a99866555ae3552c40f9fddfa061e29d7f3" + integrity sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ== + dependencies: + "@babel/runtime" "^7.23.9" + html-parse-stringify "^3.0.1" + react-is@^16.10.2, react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -3626,6 +3662,11 @@ vlq@^0.2.1: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" diff --git a/package.json b/package.json index 1edb96807..43fc24cfb 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "prod:server": "cd server && yarn start", "prod:frontend": "cd frontend && yarn build", "generate:cloudformation": "node cloud-deployments/aws/cloudformation/generate.mjs", - "generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs" + "generate::gcp_deployment": "node cloud-deployments/gcp/deployment/generate.mjs", + "verify:translations": "cd frontend/src/locales && node verifyTranslations.mjs" }, "private": false } \ No newline at end of file -- GitLab