️(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 - ♻️(frontend) redirect to doc after duplicate #1175
- 🔧(project) change env.d system by using local files #1200 - 🔧(project) change env.d system by using local files #1200
- ⚡️(frontend) improve tree stability #1207 - ⚡️(frontend) improve tree stability #1207
- ⚡️(frontend) improve accessibility #1232 - ⚡️(frontend) improve accessibility
- #1232
- #1248
- 🛂(frontend) block drag n drop when not desktop #1239 - 🛂(frontend) block drag n drop when not desktop #1239
### Fixed ### Fixed

View File

@@ -26,6 +26,8 @@ test.describe.serial('Language', () => {
test('checks language switching', async ({ page }) => { test('checks language switching', async ({ page }) => {
const header = page.locator('header').first(); const header = page.locator('header').first();
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
// initial language should be english // initial language should be english
await expect( await expect(
page.getByRole('button', { page.getByRole('button', {
@@ -36,6 +38,8 @@ test.describe.serial('Language', () => {
// switch to french // switch to french
await waitForLanguageSwitch(page, TestLanguage.French); await waitForLanguageSwitch(page, TestLanguage.French);
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect( await expect(
header.getByRole('button').getByText('Français'), header.getByRole('button').getByText('Français'),
).toBeVisible(); ).toBeVisible();
@@ -47,6 +51,8 @@ test.describe.serial('Language', () => {
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible(); await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
await expect(page.getByLabel('Abmelden')).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 ({ 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 LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import { fallbackLng } from './config';
import resources from './translations.json'; import resources from './translations.json';
// Add an initialization guard // Add an initialization guard
@@ -16,7 +17,7 @@ if (!isInitialized && !i18next.isInitialized) {
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources, resources,
fallbackLng: 'en', fallbackLng,
debug: false, debug: false,
detection: { detection: {
order: ['cookie', 'navigator'], order: ['cookie', 'navigator'],
@@ -35,6 +36,17 @@ if (!isInitialized && !i18next.isInitialized) {
nsSeparator: false, nsSeparator: false,
keySeparator: 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)); .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() { import { fallbackLng } from '../i18n/config';
return (
<Html> class MyDocument extends Document<{ locale: string }> {
<Head /> static async getInitialProps(ctx: DocumentContext) {
<body suppressHydrationWarning={process.env.NODE_ENV === 'development'}> const initialProps = await Document.getInitialProps(ctx);
<Main /> return {
<NextScript /> ...initialProps,
</body> locale: ctx.locale || fallbackLng,
</Html> };
); }
render() {
return (
<Html lang={this.props.locale}>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
} }
export default MyDocument;