️(frontend) set html lang attribute dynamically based on current loc

ensures proper language tag is set for accessibility and SEO compliance

Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
Cyril
2025-07-31 11:19:34 +02:00
parent 7813219b86
commit ec84f31bc7
5 changed files with 54 additions and 13 deletions

View File

@@ -25,7 +25,9 @@ and this project adheres to
- ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility #1232
- ⚡️(frontend) improve accessibility
- #1232
- #1248
- 🛂(frontend) block drag n drop when not desktop #1239
### Fixed

View File

@@ -26,6 +26,8 @@ test.describe.serial('Language', () => {
test('checks language switching', async ({ page }) => {
const header = page.locator('header').first();
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english
await expect(
page.getByRole('button', {
@@ -36,6 +38,8 @@ test.describe.serial('Language', () => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(
header.getByRole('button').getByText('Français'),
).toBeVisible();
@@ -47,6 +51,8 @@ test.describe.serial('Language', () => {
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await expect(page.getByLabel('Abmelden')).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
});
test('checks that backend uses the same language as the frontend', async ({

View File

@@ -0,0 +1 @@
export const fallbackLng = 'en';

View File

@@ -2,6 +2,7 @@ import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import { fallbackLng } from './config';
import resources from './translations.json';
// Add an initialization guard
@@ -16,7 +17,7 @@ if (!isInitialized && !i18next.isInitialized) {
.use(initReactI18next)
.init({
resources,
fallbackLng: 'en',
fallbackLng,
debug: false,
detection: {
order: ['cookie', 'navigator'],
@@ -35,6 +36,17 @@ if (!isInitialized && !i18next.isInitialized) {
nsSeparator: false,
keySeparator: false,
})
.then(() => {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute(
'lang',
i18next.language || fallbackLng,
);
i18next.on('languageChanged', (lang) => {
document.documentElement.setAttribute('lang', lang);
});
}
})
.catch((e) => console.error('i18n initialization failed:', e));
}

View File

@@ -1,13 +1,33 @@
import { Head, Html, Main, NextScript } from 'next/document';
import Document, {
DocumentContext,
Head,
Html,
Main,
NextScript,
} from 'next/document';
export default function RootLayout() {
return (
<Html>
<Head />
<body suppressHydrationWarning={process.env.NODE_ENV === 'development'}>
<Main />
<NextScript />
</body>
</Html>
);
import { fallbackLng } from '../i18n/config';
class MyDocument extends Document<{ locale: string }> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
locale: ctx.locale || fallbackLng,
};
}
render() {
return (
<Html lang={this.props.locale}>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;