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:
@@ -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') ?? '');
|
||||
|
||||
32
frontend/src/lib/server/locale.spec.ts
Normal file
32
frontend/src/lib/server/locale.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
20
frontend/src/lib/server/locale.ts
Normal file
20
frontend/src/lib/server/locale.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user