diff --git a/.github/workflows/check-translations.yaml b/.github/workflows/check-translations.yaml new file mode 100644 index 00000000..1dae4881 --- /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 84c27166..e584d9a3 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 627b8341..40d5c9c9 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() { - - } /> - } /> - } - /> - } - /> - } - /> - } /> + + + } /> + } /> + } + /> + } + /> + } + /> + } /> - {/* Admin */} - } - /> - - } - /> - } - /> - } - /> - - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - {/* Manager */} - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - } - /> - {/* Onboarding Flow */} - } /> - } /> - + {/* Admin */} + } + /> + + } + /> + } + /> + + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + {/* Manager */} + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + {/* Onboarding Flow */} + } /> + } /> + + diff --git a/frontend/src/components/EditingChatBubble/index.jsx b/frontend/src/components/EditingChatBubble/index.jsx index 0aa2a078..38eeb4e8 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 (

- {isUser ? "User" : "AnythingLLM Chat Assistant"} + {isUser ? t("common.user") : t("appearance.message.assistant")}

- There is no set up required when using AnythingLLM's native embedding - engine. + {t("embedding.provider.description")}

); diff --git a/frontend/src/components/Modals/Password/MultiUserAuth.jsx b/frontend/src/components/Modals/Password/MultiUserAuth.jsx index 66b40e06..c9a6536e 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() {

- Welcome to + {t("login.multi-user.welcome")}

{customAppName || "AnythingLLM"}

- Sign in to your {customAppName || "AnythingLLM"} account. + {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "} + {t("login.sign-in.end")}

@@ -296,7 +299,7 @@ export default function MultiUserAuth() { - {loading ? "Validating..." : "Login"} + {loading + ? t("login.multi-user.validating") + : t("login.multi-user.login")} diff --git a/frontend/src/components/Modals/Password/SingleUserAuth.jsx b/frontend/src/components/Modals/Password/SingleUserAuth.jsx index f976c634..66dcdb8f 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() {

- Welcome to + {t("login.multi-user.welcome")}

{customAppName || "AnythingLLM"}

- Sign in to your {customAppName || "AnythingLLM"} instance. + {t("login.sign-in.start")} {customAppName || "AnythingLLM"}{" "} + {t("login.sign-in.end")}

diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 6049d83f..9dada953 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() {
- +
@@ -146,12 +148,12 @@ export default function SettingsSidebar() { >
- Instance Settings + {t("settings.title")}
- +
@@ -221,39 +223,39 @@ const Option = ({ ); }; -const SidebarOptions = ({ user = null }) => ( +const SidebarOptions = ({ user = null, t }) => ( <>
@@ -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")} - Remove + {t("appearance.logo.remove")} diff --git a/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/CustomMessages/index.jsx index 7e026ac8..6165501a 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() {

- Custom Messages + {t("appearance.message.title")}

- Customize the automatic messages displayed to your users. + {t("appearance.message.description")}

@@ -93,8 +95,11 @@ export default function CustomMessages() {
- New system{" "} - message + {t("appearance.message.new")}{" "} + + {t("appearance.message.system")} + {" "} + {t("appearance.message.message")}
@@ -105,7 +110,11 @@ export default function CustomMessages() {
- New user message + {t("appearance.message.new")}{" "} + + {t("appearance.message.user")} + {" "} + {t("appearance.message.message")}
@@ -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")}
)} diff --git a/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/FooterCustomization/index.jsx index bd78861a..e7f6c75e 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() {

- Custom Footer Icons + {t("appearance.icons.title")}

- Customize the footer icons displayed on the bottom of the sidebar. + {t("appearance.icons.description")}

-
Icon
-
Link
+
{t("appearance.icons.icon")}
+
{t("appearance.icons.link")}
{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 00000000..8e58706b --- /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 ( + <> +
+

+ Display Language +

+

+ Select the preferred language to render AnythingLLM's UI in, when + applicable. +

+
+
+ +
+ + ); +} diff --git a/frontend/src/pages/GeneralSettings/Appearance/index.jsx b/frontend/src/pages/GeneralSettings/Appearance/index.jsx index d7352998..5894a642 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 (
@@ -18,13 +21,14 @@ export default function Appearance() {

- Appearance + {t("appearance.title")}

- Customize the appearance settings of your platform. + {t("appearance.description")}

+ diff --git a/frontend/src/pages/GeneralSettings/Chats/index.jsx b/frontend/src/pages/GeneralSettings/Chats/index.jsx index 3cc77804..3631c8c3 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() {

- Workspace Chats + {t("recorded.title")}

- These are all the recorded chats and messages that have been sent - by users ordered by their creation date. + {t("recorded.description")}

@@ -192,6 +194,7 @@ function ChatsContainer({ offset, setOffset, canNext, + t, }) { const handlePrevious = () => { setOffset(Math.max(offset - 1, 0)); @@ -225,22 +228,22 @@ function ChatsContainer({ - Id + {t("recorded.table.id")} - Sent By + {t("recorded.table.by")} - Workspace + {t("recorded.table.workspace")} - Prompt + {t("recorded.table.prompt")} - Response + {t("recorded.table.response")} - Sent At + {t("recorded.table.at")} {" "} diff --git a/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx b/frontend/src/pages/GeneralSettings/EmbedChats/index.jsx index a2605154..39b013c7 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 (
@@ -20,12 +22,11 @@ export default function EmbedChats() {

- Embed Chats + {t("embed-chats.title")}

- These are all the recorded chats and messages from any embed that - you have published. + {t("embed-chats.description")}

@@ -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() { - Embed + {t("embed-chats.table.embed")} - Sender + {t("embed-chats.table.sender")} - Message + {t("embed-chats.table.message")} - Response + {t("embed-chats.table.response")} - Sent At + {t("embed-chats.table.at")} {" "} @@ -116,14 +118,14 @@ function ChatsContainer() { disabled={offset === 0} > {" "} - Previous Page + {t("common.previous")}
diff --git a/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx b/frontend/src/pages/GeneralSettings/EmbedConfigs/index.jsx index 4d65e0d0..442a03d9 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 (
@@ -25,18 +26,17 @@ export default function EmbedConfigs() {

- Embeddable Chat Widgets + {t("embeddable.title")}

- 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")}

- Create embed + {" "} + {t("embeddable.create")}
@@ -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() { - Workspace + {t("embeddable.table.workspace")} - Sent Chats + {t("embeddable.table.chats")} - Active Domains + {t("embeddable.table.Active")} {" "} diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index 9c52c271..2563aaad 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() {

- Embedding Preference + {t("embedding.title")}

- 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")}
- 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")}

@@ -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")} )}
- Embedding Provider + {t("embedding.provider.title")}
{searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx index 5ee1197f..1f30f71a 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() {

- Text splitting & Chunking Preferences + {t("text.title")}

- Sometimes, you may want to change the default way that new - documents are split and chunked before being inserted into - your vector database.
- You should only modify this setting if you understand how text - splitting works and it's side effects. + {t("text.desc-start")}
+ {t("text.desc-end")}

- Changes here will only apply to{" "} - newly embedded documents, not existing documents. + {t("text.warn-start")} {t("text.warn-center")} + {t("text.warn-end")}

{hasChanges && ( - {saving ? "Saving..." : "Save changes"} + {saving ? t("common.saving") : t("common.save")} )}
@@ -113,11 +112,10 @@ export default function EmbeddingTextSplitterPreference() {

- This is the maximum length of characters that can be - present in a single vector. + {t("text.size.description")}

- Embed model maximum length is{" "} + {t("text.size.recommend")}{" "} {numberWithCommas(settings?.max_embed_chunk_size || 1000)}.

@@ -147,11 +145,10 @@ export default function EmbeddingTextSplitterPreference() {

- This is the maximum overlap of characters that occurs - during chunking between two adjacent text chunks. + {t("text.overlap.description")}

{ e.preventDefault(); @@ -310,14 +312,11 @@ export default function GeneralLLMPreference() {

- LLM Preference + {t("llm.title")}

- 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")}

@@ -331,7 +330,7 @@ export default function GeneralLLMPreference() { )}
- LLM Provider + {t("llm.provider")}
{searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx b/frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx index 28347089..12d5a3e5 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() {

- Privacy & Data-Handling + {t("privacy.title")}

- This is your configuration for how connected third party providers - and AnythingLLM handle your data. + {t("privacy.description")}

{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 (
-
LLM Selection
+
+ {t("privacy.llm")} +
- Embedding Preference + {t("privacy.embedding")}
-
Vector Database
+
+ {t("privacy.vector")} +
)} @@ -116,8 +117,8 @@ function MultiUserMode() {
- Admin account password + {t("multi.enable.password")}

- 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")}

@@ -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() {

- Password Protection + {t("multi.password.title")}

- 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")}

{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")}
)} @@ -294,7 +293,7 @@ function PasswordProtection() {

- 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")}

diff --git a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx index 35a0622e..07c5ecae 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() {

- Transcription Model Preference + {t("transcription.title")}

- 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")}

@@ -139,7 +138,7 @@ export default function TranscriptionModelPreference() { )}
- Transcription Provider + {t("transcription.provider")}
{searchMenuOpen && ( diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 15dad464..48c1c971 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() {

- Vector Database + {t("vector.title")}

- 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")}

@@ -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")} )}
- Vector Database Provider + {t("vector.provider.title")}
{searchMenuOpen && ( diff --git a/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx index bd4615d0..fb28cc98 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")}
-

- 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. -

+

{t("agent.performance-warning")}

)}

- The specific LLM provider & model that will be used for this - workspace's @agent agent. + {t("agent.provider.description")}

diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx index bf51cb87..270f22ef 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({

- The specific chat model that will be used for this workspace's - @agent agent. + {t("agent.mode.chat.description")}

@@ -51,11 +53,10 @@ export default function AgentModelSelection({

- The specific LLM model that will be used for this workspace's @agent - agent. + {t("agent.mode.description")}

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 00000000..09eba16b --- /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: () => , + 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) => , + 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) => , + 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) => , + 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 ( +
+
+
+ + +
+

+ {t("agent.skill.web.desc-start")} +
+ {t("agent.skill.web.desc-end")} +

+
+ + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx index 092c055c..faa62103 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 (

- The number of previous chats that will be included in the - response's short-term memory. - Recommend 20. - Anything more than 45 is likely to lead to continuous chat failures - depending on message size. + {t("chat.history.desc-start")} + {t("chat.history.recommend")} + {t("chat.history.desc-end")}

@@ -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")}

{chatMode === "chat" ? ( <> - Chat will provide answers with the LLM's general knowledge{" "} - and document context that is - found. + {t("chat.mode.chat.title")}{" "} + {t("chat.mode.chat.desc-start")}{" "} + {t("chat.mode.chat.and")}{" "} + {t("chat.mode.chat.desc-end")} ) : ( <> - Query will provide answers{" "} - only if document context is - found. + {t("chat.mode.query.title")}{" "} + {t("chat.mode.query.desc-start")}{" "} + {t("chat.mode.query.only")}{" "} + {t("chat.mode.query.desc-end")} )}

diff --git a/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModelSelection/index.jsx index 9ed42429..71d943e5 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({

- The specific chat model that will be used for this workspace. If - empty, will use the system LLM preference. + {t("chat.model.description")}