diff --git a/api/package.json b/api/package.json index 6f4861bf5..43ccda33e 100644 --- a/api/package.json +++ b/api/package.json @@ -49,6 +49,7 @@ "compression": "^1.7.4", "connect-redis": "^7.1.0", "cookie": "^0.5.0", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dedent": "^1.5.3", "dotenv": "^16.0.3", diff --git a/api/server/index.js b/api/server/index.js index b39116fbb..3cb969ddc 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -7,6 +7,8 @@ const express = require('express'); const compression = require('compression'); const passport = require('passport'); const mongoSanitize = require('express-mongo-sanitize'); +const fs = require('fs'); +const cookieParser = require('cookie-parser'); const { jwtLogin, passportLogin } = require('~/strategies'); const { connectDb, indexSync } = require('~/lib/db'); const { isEnabled } = require('~/server/utils'); @@ -37,6 +39,9 @@ const startServer = async () => { app.disable('x-powered-by'); await AppService(app); + const indexPath = path.join(app.locals.paths.dist, 'index.html'); + const indexHTML = fs.readFileSync(indexPath, 'utf8'); + app.get('/health', (_req, res) => res.status(200).send('OK')); /* Middleware */ @@ -50,6 +55,7 @@ const startServer = async () => { app.use(staticCache(app.locals.paths.assets)); app.set('trust proxy', 1); /* trust first proxy */ app.use(cors()); + app.use(cookieParser()); if (!isEnabled(DISABLE_COMPRESSION)) { app.use(compression()); @@ -101,8 +107,12 @@ const startServer = async () => { app.use('/api/roles', routes.roles); app.use('/api/tags', routes.tags); + app.use((req, res) => { - res.sendFile(path.join(app.locals.paths.dist, 'index.html')); + // Replace lang attribute in index.html with lang from cookies or accept-language header + const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US'; + const updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${lang}"`); + res.send(updatedIndexHtml); }); app.listen(port, host, () => { diff --git a/client/index.html b/client/index.html index 0523894cf..e70cc7678 100644 --- a/client/index.html +++ b/client/index.html @@ -22,7 +22,7 @@ type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" - /> + /> { - setSelectedLang(value); + let userLang = value; if (value === 'auto') { - const userLang = navigator.language || navigator.languages[0]; - setLangcode(userLang); - localStorage.setItem('lang', userLang); - } else { - setLangcode(value); - localStorage.setItem('lang', value); + userLang = navigator.language || navigator.languages[0]; } + + requestAnimationFrame(() => { + document.documentElement.lang = userLang; + }); + setLangcode(userLang); + Cookies.set('lang', userLang, { expires: 365 }); }, - [setLangcode, setSelectedLang], + [setLangcode], ); return ( @@ -161,7 +162,7 @@ function General() {
- +
diff --git a/client/src/store/language.ts b/client/src/store/language.ts index 0df191fb3..50e98fb12 100644 --- a/client/src/store/language.ts +++ b/client/src/store/language.ts @@ -1,10 +1,11 @@ -import { atom } from 'recoil'; +import Cookies from 'js-cookie'; +import { atomWithLocalStorage } from './utils'; -const userLang = navigator.language || navigator.languages[0]; +const defaultLang = () => { + const userLang = navigator.language || navigator.languages[0]; + return Cookies.get('lang') || localStorage.getItem('lang') || userLang; +}; -const lang = atom({ - key: 'lang', - default: localStorage.getItem('lang') || userLang, -}); +const lang = atomWithLocalStorage('lang', defaultLang()); export default { lang }; diff --git a/package-lock.json b/package-lock.json index c683624b5..d54fa86a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "compression": "^1.7.4", "connect-redis": "^7.1.0", "cookie": "^0.5.0", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dedent": "^1.5.3", "dotenv": "^16.0.3", @@ -1156,6 +1157,7 @@ "filenamify": "^6.0.0", "html-to-image": "^1.11.11", "image-blob-reduce": "^4.1.0", + "js-cookie": "^3.0.5", "librechat-data-provider": "*", "lodash": "^4.17.21", "lucide-react": "^0.394.0", @@ -1202,6 +1204,7 @@ "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.2", + "@types/js-cookie": "^3.0.6", "@types/node": "^20.3.0", "@types/react": "^18.2.11", "@types/react-dom": "^18.2.4", @@ -14452,6 +14455,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -17266,6 +17275,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -23346,6 +23375,14 @@ "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==" }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tiktoken": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.10.tgz",