Skip to content

Commit

Permalink
🎙️ a11y: update html lang attribute (#3636)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jacobcolyvan authored Aug 30, 2024
1 parent a004231 commit 2ce4f66
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 19 deletions.
1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 */
Expand All @@ -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());
Expand Down Expand Up @@ -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, () => {
Expand Down
2 changes: 1 addition & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
type="image/png"
sizes="16x16"
href="/assets/favicon-16x16.png"
/>
/>
<link
rel="apple-touch-icon"
href="/assets/apple-touch-icon-180x180.png"
Expand Down
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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",
Expand Down Expand Up @@ -111,6 +112,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",
Expand Down
23 changes: 12 additions & 11 deletions client/src/components/Nav/SettingsTabs/General/General.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import Cookies from 'js-cookie';
import { SettingsTabValues } from 'librechat-data-provider';
import React, { useContext, useCallback, useRef } from 'react';
import type { TDangerButtonProps } from '~/common';
import { ThemeContext, useLocalize, useLocalStorage } from '~/hooks';
import { ThemeContext, useLocalize } from '~/hooks';
import HideSidePanelSwitch from './HideSidePanelSwitch';
import AutoScrollSwitch from './AutoScrollSwitch';
import ArchivedChats from './ArchivedChats';
Expand Down Expand Up @@ -123,7 +124,6 @@ function General() {
const { theme, setTheme } = useContext(ThemeContext);

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

const contentRef = useRef(null);

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

const changeLang = useCallback(
(value: string) => {
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 (
Expand All @@ -161,7 +162,7 @@ function General() {
<ThemeSelector theme={theme} onChange={changeTheme} />
</div>
<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 className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<AutoScrollSwitch />
Expand Down
13 changes: 7 additions & 6 deletions client/src/store/language.ts
Original file line number Diff line number Diff line change
@@ -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 };
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2ce4f66

Please sign in to comment.