test(i18n): add unit tests for locale detection + extract to module

Extract detectLocale() from hooks.server.ts into src/lib/server/locale.ts
so it can be tested in isolation. Add 7 unit tests covering:
- German, English, Spanish browser preferences
- Fallback when primary language is unsupported
- Quality value (q=) ordering
- Fully unsupported language → null
- Empty Accept-Language header → null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-19 18:59:02 +01:00
committed by marcel
parent c4be2eb46e
commit a998ef4550
3 changed files with 54 additions and 15 deletions

View File

@@ -2,24 +2,11 @@ import { redirect, type Handle, type HandleFetch } from '@sveltejs/kit';
import { paraglideMiddleware } from '$lib/paraglide/server';
import { sequence } from '@sveltejs/kit/hooks';
import { env } from 'process';
import { cookieName, cookieMaxAge, locales } from '$lib/paraglide/runtime';
import { cookieName, cookieMaxAge } from '$lib/paraglide/runtime';
import { detectLocale } from '$lib/server/locale';
const PUBLIC_PATHS = ['/login', '/logout'];
function detectLocale(acceptLanguage: string): string | null {
const preferred = acceptLanguage
.split(',')
.map((part) => {
const [lang, q] = part.trim().split(';q=');
return { lang: lang.trim().split('-')[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
})
.sort((a, b) => b.q - a.q);
for (const { lang } of preferred) {
if ((locales as readonly string[]).includes(lang)) return lang;
}
return null;
}
const handleLocaleDetection: Handle = ({ event, resolve }) => {
if (!event.cookies.get(cookieName)) {
const locale = detectLocale(event.request.headers.get('accept-language') ?? '');

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { detectLocale } from './locale';
describe('detectLocale', () => {
it('returns de for a German browser', () => {
expect(detectLocale('de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7')).toBe('de');
});
it('returns en for an English browser', () => {
expect(detectLocale('en-US,en;q=0.9')).toBe('en');
});
it('returns es for a Spanish browser', () => {
expect(detectLocale('es-MX,es;q=0.9,en-US;q=0.8')).toBe('es');
});
it('falls back to a supported language when the primary is unsupported', () => {
expect(detectLocale('fr-FR,fr;q=0.9,en;q=0.8')).toBe('en');
});
it('respects quality values — picks the highest-priority supported locale', () => {
expect(detectLocale('en-US;q=0.7,de-DE;q=0.9')).toBe('de');
});
it('returns null for a completely unsupported language', () => {
expect(detectLocale('ja-JP,ja;q=0.9,zh-CN;q=0.8')).toBeNull();
});
it('returns null for an empty header', () => {
expect(detectLocale('')).toBeNull();
});
});

View File

@@ -0,0 +1,20 @@
import { locales } from '$lib/paraglide/runtime';
/**
* Picks the best supported locale from an Accept-Language header value.
* Returns null when no supported locale is found.
*/
export function detectLocale(acceptLanguage: string): string | null {
const preferred = acceptLanguage
.split(',')
.map((part) => {
const [lang, q] = part.trim().split(';q=');
return { lang: lang.trim().split('-')[0].toLowerCase(), q: q ? parseFloat(q) : 1 };
})
.sort((a, b) => b.q - a.q);
for (const { lang } of preferred) {
if ((locales as readonly string[]).includes(lang)) return lang;
}
return null;
}