From 3b0aab94ec1c6e33de16b2ef75aac7531619b1a4 Mon Sep 17 00:00:00 2001 From: shatfield4 Date: Wed, 23 Apr 2025 18:31:49 -0700 Subject: [PATCH] add i18n support and send message/reset chat data attributes --- README.md | 8 ++++ package.json | 5 +- src/App.jsx | 6 ++- .../ChatContainer/PromptInput/index.jsx | 8 +++- src/components/ResetChat/index.jsx | 6 ++- src/i18n.js | 31 +++++++++++++ src/locales/en/common.js | 8 ++++ src/locales/fr/common.js | 8 ++++ src/locales/resources.js | 11 +++++ src/main.jsx | 20 +++++--- yarn.lock | 46 +++++++++++++++++++ 11 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 src/i18n.js create mode 100644 src/locales/en/common.js create mode 100644 src/locales/fr/common.js create mode 100644 src/locales/resources.js diff --git a/README.md b/README.md index 1b4bccc..2d90ea2 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,10 @@ REQUIRED data attributes: - `data-temperature` — Override the chat model temperature. This must be a valid value for your AnythingLLM LLM provider. If unset it will use the embeds attached workspace model temperature or the system setting. +**Language & Localization** + +- `data-language` — Set the language for the chat interface. If not specified, it will default to English (en). Currently supported languages: en (English). + **Style Overrides** - `data-chat-icon` — The chat bubble icon show when chat is closed. Options are `plus`, `chatBubble`, `support`, `search2`, `search`, `magic`. @@ -97,6 +101,10 @@ REQUIRED data attributes: - `data-default-messages` - A string of comma-separated messages you want to display to the user when the chat widget has no history. Example: `"How are you?, What is so interesting about this project?, Tell me a joke."` +- `data-send-message-text` — Override the placeholder text in the message input field. If not specified, it will use the translated text based on the selected language. + +- `data-reset-chat-text` — Override the text shown on the reset chat button. If not specified, it will use the translated text based on the selected language. + **Behavior Overrides** - `data-open-on-load` — Once loaded, open the chat as default. It can still be closed by the user. To enable set this attribute to `on`. All other values will be ignored. diff --git a/package.json b/package.json index 111b68f..b831dd1 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "markdown-it": "^13.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "i18next": "^23.11.3", + "react-i18next": "^14.1.1", + "i18next-browser-languagedetector": "^7.2.1" }, "devDependencies": { "@rollup/plugin-image": "^3.0.3", diff --git a/src/App.jsx b/src/App.jsx index d60450d..1519184 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,8 @@ import Head from "@/components/Head"; import OpenButton from "@/components/OpenButton"; import ChatWindow from "./components/ChatWindow"; import { useEffect } from "react"; +import { I18nextProvider } from "react-i18next"; +import i18next from "@/i18n"; export default function App() { const { isChatOpen, toggleOpenChat } = useOpenChat(); @@ -31,7 +33,7 @@ export default function App() { const windowHeight = embedSettings.windowHeight ?? "700px"; return ( - <> +
)} - +
); } diff --git a/src/components/ChatWindow/ChatContainer/PromptInput/index.jsx b/src/components/ChatWindow/ChatContainer/PromptInput/index.jsx index 14961ca..e547db5 100644 --- a/src/components/ChatWindow/ChatContainer/PromptInput/index.jsx +++ b/src/components/ChatWindow/ChatContainer/PromptInput/index.jsx @@ -1,5 +1,7 @@ import { CircleNotch, PaperPlaneRight } from "@phosphor-icons/react"; import React, { useState, useRef, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { embedderSettings } from "@/main.jsx"; export default function PromptInput({ message, @@ -8,6 +10,7 @@ export default function PromptInput({ inputDisabled, buttonDisabled, }) { + const { t } = useTranslation(); const formRef = useRef(null); const textareaRef = useRef(null); const [_, setFocused] = useState(false); @@ -71,7 +74,10 @@ export default function PromptInput({ }} value={message} className="allm-font-sans allm-border-none allm-cursor-text allm-max-h-[100px] allm-text-[14px] allm-mx-2 allm-py-2 allm-w-full allm-text-black allm-bg-transparent placeholder:allm-text-slate-800/60 allm-resize-none active:allm-outline-none focus:allm-outline-none allm-flex-grow" - placeholder={"Send a message"} + placeholder={ + embedderSettings.settings.sendMessageText || + t("chat.send-message") + } id="message-input" /> ); diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..b784615 --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,31 @@ +import i18next from "i18next"; +import { initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { defaultNS, resources } from "./locales/resources.js"; + +export function initI18n(settings) { + const language = settings?.language || "en"; + + i18next + .use(initReactI18next) + .use(LanguageDetector) + .init({ + fallbackLng: "en", + lng: language, + debug: import.meta.env.DEV, + defaultNS, + resources, + load: "languageOnly", + detection: { + order: ["querystring", "navigator"], + lookupQuerystring: "lng", + }, + interpolation: { + escapeValue: false, + }, + }); + + return i18next; +} + +export default i18next; diff --git a/src/locales/en/common.js b/src/locales/en/common.js new file mode 100644 index 0000000..169785a --- /dev/null +++ b/src/locales/en/common.js @@ -0,0 +1,8 @@ +const TRANSLATIONS = { + chat: { + "send-message": "Send a message", + "reset-chat": "Reset Chat", + }, +}; + +export default TRANSLATIONS; diff --git a/src/locales/fr/common.js b/src/locales/fr/common.js new file mode 100644 index 0000000..c40df92 --- /dev/null +++ b/src/locales/fr/common.js @@ -0,0 +1,8 @@ +const TRANSLATIONS = { + chat: { + "send-message": "Envoyer un message", + "reset-chat": "Réinitialiser la conversation", + }, +}; + +export default TRANSLATIONS; diff --git a/src/locales/resources.js b/src/locales/resources.js new file mode 100644 index 0000000..1a6ec75 --- /dev/null +++ b/src/locales/resources.js @@ -0,0 +1,11 @@ +import English from "./en/common.js"; +import French from "./fr/common.js"; +export const defaultNS = "common"; +export const resources = { + en: { + common: English, + }, + fr: { + common: French, + }, +}; diff --git a/src/main.jsx b/src/main.jsx index 3ddfe7e..d2bef51 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,20 +3,16 @@ import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; import { parseStylesSrc } from "./utils/constants.js"; -const appElement = document.createElement("div"); +import { initI18n } from "./i18n.js"; +const appElement = document.createElement("div"); document.body.appendChild(appElement); -const root = ReactDOM.createRoot(appElement); -root.render( - - - -); const scriptSettings = Object.assign( {}, document?.currentScript?.dataset || {} ); + export const embedderSettings = { settings: scriptSettings, stylesSrc: parseStylesSrc(document?.currentScript?.src), @@ -29,3 +25,13 @@ export const embedderSettings = { base: `allm-text-[#222628] allm-rounded-t-[18px] allm-rounded-br-[18px] allm-rounded-bl-[4px] allm-mr-[37px] allm-ml-[9px]`, }, }; + +// Initialize i18n after settings are available +initI18n(scriptSettings); + +const root = ReactDOM.createRoot(appElement); +root.render( + + + +); diff --git a/yarn.lock b/yarn.lock index b5fd4fa..7ad4371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -184,6 +184,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" + integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.23.9": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" @@ -1788,11 +1795,32 @@ 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" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18next-browser-languagedetector@^7.2.1: + version "7.2.2" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz#748e7dc192847613911d8a79d9d9a6c2d266133e" + integrity sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next@^23.11.3: + version "23.16.8" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.16.8.tgz#3ae1373d344c2393f465556f394aba5a9233b93a" + integrity sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg== + dependencies: + "@babel/runtime" "^7.23.2" + ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" @@ -2694,6 +2722,14 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" +react-i18next@^14.1.1: + version "14.1.3" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.3.tgz#85525c4294ef870ddd3f5d184e793cae362f47cb" + integrity sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw== + dependencies: + "@babel/runtime" "^7.23.9" + html-parse-stringify "^3.0.1" + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -2737,6 +2773,11 @@ reflect.getprototypeof@^1.0.4: globalthis "^1.0.3" which-builtin-type "^1.1.3" +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" @@ -3327,6 +3368,11 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" +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"