mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 02:10:15 +01:00
🎙️ a11y: update html lang attribute (#3636)
* refactor: remove duplicate localStorage lang call * refactor: use cookies to handle langcode * feat: override index.html lang w/ cookie pref * refactor: only read index on server start * refactor: rename lang cookie & localstorage as backup * refactor: use atomWithLocalStorage in language store * fix: forced reflow warning in language select
This commit is contained in:
parent
a0042317b2
commit
2ce4f66218
7 changed files with 71 additions and 19 deletions
|
|
@ -49,6 +49,7 @@
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-redis": "^7.1.0",
|
"connect-redis": "^7.1.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dedent": "^1.5.3",
|
"dedent": "^1.5.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ const express = require('express');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const mongoSanitize = require('express-mongo-sanitize');
|
const mongoSanitize = require('express-mongo-sanitize');
|
||||||
|
const fs = require('fs');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
const { jwtLogin, passportLogin } = require('~/strategies');
|
const { jwtLogin, passportLogin } = require('~/strategies');
|
||||||
const { connectDb, indexSync } = require('~/lib/db');
|
const { connectDb, indexSync } = require('~/lib/db');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
@ -37,6 +39,9 @@ const startServer = async () => {
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
await AppService(app);
|
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'));
|
app.get('/health', (_req, res) => res.status(200).send('OK'));
|
||||||
|
|
||||||
/* Middleware */
|
/* Middleware */
|
||||||
|
|
@ -50,6 +55,7 @@ const startServer = async () => {
|
||||||
app.use(staticCache(app.locals.paths.assets));
|
app.use(staticCache(app.locals.paths.assets));
|
||||||
app.set('trust proxy', 1); /* trust first proxy */
|
app.set('trust proxy', 1); /* trust first proxy */
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
if (!isEnabled(DISABLE_COMPRESSION)) {
|
if (!isEnabled(DISABLE_COMPRESSION)) {
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
@ -101,8 +107,12 @@ const startServer = async () => {
|
||||||
app.use('/api/roles', routes.roles);
|
app.use('/api/roles', routes.roles);
|
||||||
|
|
||||||
app.use('/api/tags', routes.tags);
|
app.use('/api/tags', routes.tags);
|
||||||
|
|
||||||
app.use((req, res) => {
|
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, () => {
|
app.listen(port, host, () => {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
"filenamify": "^6.0.0",
|
"filenamify": "^6.0.0",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"image-blob-reduce": "^4.1.0",
|
"image-blob-reduce": "^4.1.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"librechat-data-provider": "*",
|
"librechat-data-provider": "*",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.394.0",
|
"lucide-react": "^0.394.0",
|
||||||
|
|
@ -111,6 +112,7 @@
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20.3.0",
|
"@types/node": "^20.3.0",
|
||||||
"@types/react": "^18.2.11",
|
"@types/react": "^18.2.11",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import * as Tabs from '@radix-ui/react-tabs';
|
import * as Tabs from '@radix-ui/react-tabs';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
import { SettingsTabValues } from 'librechat-data-provider';
|
import { SettingsTabValues } from 'librechat-data-provider';
|
||||||
import React, { useContext, useCallback, useRef } from 'react';
|
import React, { useContext, useCallback, useRef } from 'react';
|
||||||
import type { TDangerButtonProps } from '~/common';
|
import type { TDangerButtonProps } from '~/common';
|
||||||
import { ThemeContext, useLocalize, useLocalStorage } from '~/hooks';
|
import { ThemeContext, useLocalize } from '~/hooks';
|
||||||
import HideSidePanelSwitch from './HideSidePanelSwitch';
|
import HideSidePanelSwitch from './HideSidePanelSwitch';
|
||||||
import AutoScrollSwitch from './AutoScrollSwitch';
|
import AutoScrollSwitch from './AutoScrollSwitch';
|
||||||
import ArchivedChats from './ArchivedChats';
|
import ArchivedChats from './ArchivedChats';
|
||||||
|
|
@ -123,7 +124,6 @@ function General() {
|
||||||
const { theme, setTheme } = useContext(ThemeContext);
|
const { theme, setTheme } = useContext(ThemeContext);
|
||||||
|
|
||||||
const [langcode, setLangcode] = useRecoilState(store.lang);
|
const [langcode, setLangcode] = useRecoilState(store.lang);
|
||||||
const [selectedLang, setSelectedLang] = useLocalStorage('selectedLang', langcode);
|
|
||||||
|
|
||||||
const contentRef = useRef(null);
|
const contentRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -136,17 +136,18 @@ function General() {
|
||||||
|
|
||||||
const changeLang = useCallback(
|
const changeLang = useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
setSelectedLang(value);
|
let userLang = value;
|
||||||
if (value === 'auto') {
|
if (value === 'auto') {
|
||||||
const userLang = navigator.language || navigator.languages[0];
|
userLang = navigator.language || navigator.languages[0];
|
||||||
setLangcode(userLang);
|
|
||||||
localStorage.setItem('lang', userLang);
|
|
||||||
} else {
|
|
||||||
setLangcode(value);
|
|
||||||
localStorage.setItem('lang', value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.documentElement.lang = userLang;
|
||||||
|
});
|
||||||
|
setLangcode(userLang);
|
||||||
|
Cookies.set('lang', userLang, { expires: 365 });
|
||||||
},
|
},
|
||||||
[setLangcode, setSelectedLang],
|
[setLangcode],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -161,7 +162,7 @@ function General() {
|
||||||
<ThemeSelector theme={theme} onChange={changeTheme} />
|
<ThemeSelector theme={theme} onChange={changeTheme} />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
<LangSelector langcode={selectedLang} onChange={changeLang} />
|
<LangSelector langcode={langcode} onChange={changeLang} />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
|
||||||
<AutoScrollSwitch />
|
<AutoScrollSwitch />
|
||||||
|
|
|
||||||
|
|
@ -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({
|
const lang = atomWithLocalStorage('lang', defaultLang());
|
||||||
key: 'lang',
|
|
||||||
default: localStorage.getItem('lang') || userLang,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default { lang };
|
export default { lang };
|
||||||
|
|
|
||||||
37
package-lock.json
generated
37
package-lock.json
generated
|
|
@ -58,6 +58,7 @@
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-redis": "^7.1.0",
|
"connect-redis": "^7.1.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dedent": "^1.5.3",
|
"dedent": "^1.5.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
|
@ -1156,6 +1157,7 @@
|
||||||
"filenamify": "^6.0.0",
|
"filenamify": "^6.0.0",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"image-blob-reduce": "^4.1.0",
|
"image-blob-reduce": "^4.1.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"librechat-data-provider": "*",
|
"librechat-data-provider": "*",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.394.0",
|
"lucide-react": "^0.394.0",
|
||||||
|
|
@ -1202,6 +1204,7 @@
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20.3.0",
|
"@types/node": "^20.3.0",
|
||||||
"@types/react": "^18.2.11",
|
"@types/react": "^18.2.11",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
|
|
@ -14452,6 +14455,12 @@
|
||||||
"pretty-format": "^29.0.0"
|
"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": {
|
"node_modules/@types/js-yaml": {
|
||||||
"version": "4.0.9",
|
"version": "4.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
|
@ -17266,6 +17275,26 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/cookie-signature": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz",
|
||||||
"integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ=="
|
"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": {
|
"node_modules/js-tiktoken": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.10.tgz",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue