Skip to content

Commit 2ce4f66

Browse files
authored
🎙️ a11y: update html lang attribute (danny-avila#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
1 parent a004231 commit 2ce4f66

File tree

7 files changed

+71
-19
lines changed

7 files changed

+71
-19
lines changed

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"compression": "^1.7.4",
5050
"connect-redis": "^7.1.0",
5151
"cookie": "^0.5.0",
52+
"cookie-parser": "^1.4.6",
5253
"cors": "^2.8.5",
5354
"dedent": "^1.5.3",
5455
"dotenv": "^16.0.3",

api/server/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const express = require('express');
77
const compression = require('compression');
88
const passport = require('passport');
99
const mongoSanitize = require('express-mongo-sanitize');
10+
const fs = require('fs');
11+
const cookieParser = require('cookie-parser');
1012
const { jwtLogin, passportLogin } = require('~/strategies');
1113
const { connectDb, indexSync } = require('~/lib/db');
1214
const { isEnabled } = require('~/server/utils');
@@ -37,6 +39,9 @@ const startServer = async () => {
3739
app.disable('x-powered-by');
3840
await AppService(app);
3941

42+
const indexPath = path.join(app.locals.paths.dist, 'index.html');
43+
const indexHTML = fs.readFileSync(indexPath, 'utf8');
44+
4045
app.get('/health', (_req, res) => res.status(200).send('OK'));
4146

4247
/* Middleware */
@@ -50,6 +55,7 @@ const startServer = async () => {
5055
app.use(staticCache(app.locals.paths.assets));
5156
app.set('trust proxy', 1); /* trust first proxy */
5257
app.use(cors());
58+
app.use(cookieParser());
5359

5460
if (!isEnabled(DISABLE_COMPRESSION)) {
5561
app.use(compression());
@@ -101,8 +107,12 @@ const startServer = async () => {
101107
app.use('/api/roles', routes.roles);
102108

103109
app.use('/api/tags', routes.tags);
110+
104111
app.use((req, res) => {
105-
res.sendFile(path.join(app.locals.paths.dist, 'index.html'));
112+
// Replace lang attribute in index.html with lang from cookies or accept-language header
113+
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
114+
const updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${lang}"`);
115+
res.send(updatedIndexHtml);
106116
});
107117

108118
app.listen(port, host, () => {

client/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
type="image/png"
2323
sizes="16x16"
2424
href="/assets/favicon-16x16.png"
25-
/>
25+
/>
2626
<link
2727
rel="apple-touch-icon"
2828
href="/assets/apple-touch-icon-180x180.png"

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"filenamify": "^6.0.0",
6666
"html-to-image": "^1.11.11",
6767
"image-blob-reduce": "^4.1.0",
68+
"js-cookie": "^3.0.5",
6869
"librechat-data-provider": "*",
6970
"lodash": "^4.17.21",
7071
"lucide-react": "^0.394.0",
@@ -111,6 +112,7 @@
111112
"@testing-library/react": "^14.0.0",
112113
"@testing-library/user-event": "^14.4.3",
113114
"@types/jest": "^29.5.2",
115+
"@types/js-cookie": "^3.0.6",
114116
"@types/node": "^20.3.0",
115117
"@types/react": "^18.2.11",
116118
"@types/react-dom": "^18.2.4",

client/src/components/Nav/SettingsTabs/General/General.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { useRecoilState } from 'recoil';
22
import * as Tabs from '@radix-ui/react-tabs';
3+
import Cookies from 'js-cookie';
34
import { SettingsTabValues } from 'librechat-data-provider';
45
import React, { useContext, useCallback, useRef } from 'react';
56
import type { TDangerButtonProps } from '~/common';
6-
import { ThemeContext, useLocalize, useLocalStorage } from '~/hooks';
7+
import { ThemeContext, useLocalize } from '~/hooks';
78
import HideSidePanelSwitch from './HideSidePanelSwitch';
89
import AutoScrollSwitch from './AutoScrollSwitch';
910
import ArchivedChats from './ArchivedChats';
@@ -123,7 +124,6 @@ function General() {
123124
const { theme, setTheme } = useContext(ThemeContext);
124125

125126
const [langcode, setLangcode] = useRecoilState(store.lang);
126-
const [selectedLang, setSelectedLang] = useLocalStorage('selectedLang', langcode);
127127

128128
const contentRef = useRef(null);
129129

@@ -136,17 +136,18 @@ function General() {
136136

137137
const changeLang = useCallback(
138138
(value: string) => {
139-
setSelectedLang(value);
139+
let userLang = value;
140140
if (value === 'auto') {
141-
const userLang = navigator.language || navigator.languages[0];
142-
setLangcode(userLang);
143-
localStorage.setItem('lang', userLang);
144-
} else {
145-
setLangcode(value);
146-
localStorage.setItem('lang', value);
141+
userLang = navigator.language || navigator.languages[0];
147142
}
143+
144+
requestAnimationFrame(() => {
145+
document.documentElement.lang = userLang;
146+
});
147+
setLangcode(userLang);
148+
Cookies.set('lang', userLang, { expires: 365 });
148149
},
149-
[setLangcode, setSelectedLang],
150+
[setLangcode],
150151
);
151152

152153
return (
@@ -161,7 +162,7 @@ function General() {
161162
<ThemeSelector theme={theme} onChange={changeTheme} />
162163
</div>
163164
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
164-
<LangSelector langcode={selectedLang} onChange={changeLang} />
165+
<LangSelector langcode={langcode} onChange={changeLang} />
165166
</div>
166167
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
167168
<AutoScrollSwitch />

client/src/store/language.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { atom } from 'recoil';
1+
import Cookies from 'js-cookie';
2+
import { atomWithLocalStorage } from './utils';
23

3-
const userLang = navigator.language || navigator.languages[0];
4+
const defaultLang = () => {
5+
const userLang = navigator.language || navigator.languages[0];
6+
return Cookies.get('lang') || localStorage.getItem('lang') || userLang;
7+
};
48

5-
const lang = atom({
6-
key: 'lang',
7-
default: localStorage.getItem('lang') || userLang,
8-
});
9+
const lang = atomWithLocalStorage('lang', defaultLang());
910

1011
export default { lang };

package-lock.json

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)