fix(i18n): map Norwegian locale alias to Bokmål

This commit is contained in:
Lars Tobias Skjong-Børsting
2026-08-28 19:31:21 +02:00
parent a3ede003a1
commit b34d22d78b
4 changed files with 68 additions and 20 deletions
+39
View File
@@ -0,0 +1,39 @@
import { afterEach, describe, expect, it } from 'vitest';
import { detectBrowserLocale } from '../detect-locale';
import { localeFromAcceptLanguage, normalizeLocaleTag } from '../locale-resolution';
const originalLanguages = Object.getOwnPropertyDescriptor(navigator, 'languages');
function setBrowserLanguages(languages: string[]) {
Object.defineProperty(navigator, 'languages', {
configurable: true,
value: languages,
});
}
afterEach(() => {
if (originalLanguages) {
Object.defineProperty(navigator, 'languages', originalLanguages);
} else {
Reflect.deleteProperty(navigator, 'languages');
}
});
describe('Norwegian locale aliases', () => {
it.each(['no', 'no-NO'])('normalizes %s to Bokmål', (tag) => {
expect(normalizeLocaleTag(tag)).toBe('nb');
});
it('keeps native Bokmål tags on nb', () => {
expect(normalizeLocaleTag('nb-NO')).toBe('nb');
});
it('resolves no-NO from Accept-Language using quality ranking', () => {
expect(localeFromAcceptLanguage('da;q=0.5, no-NO;q=0.9, en;q=0.8')).toBe('nb');
});
it.each(['no', 'no-NO'])('detects browser language %s as Bokmål', (tag) => {
setBrowserLanguages([tag]);
expect(detectBrowserLocale('en')).toBe('nb');
});
});
+2 -1
View File
@@ -1,5 +1,6 @@
import { routing } from './routing';
import { useLocaleStore } from '@/stores/locale-store';
import { normalizeLocaleTag } from './locale-resolution';
/**
* Pick the best supported locale for a first-time visitor from their browser
@@ -15,7 +16,7 @@ export function detectBrowserLocale(fallback: string): string {
const prefs = navigator.languages?.length ? navigator.languages : [navigator.language];
for (const tag of prefs) {
if (!tag) continue;
const base = tag.toLowerCase().split('-')[0];
const base = normalizeLocaleTag(tag);
if (base === 'en') return fallback; // English is the top preference -> keep default
if (supported.has(base)) return base; // first supported non-English preference wins
}
+25
View File
@@ -0,0 +1,25 @@
import { routing } from './routing';
/** Map a BCP 47 language tag to the locale code Bulwark supports. */
export function normalizeLocaleTag(tag: string): string {
const base = tag.trim().toLowerCase().split('-')[0];
return base === 'no' ? 'nb' : base;
}
/** Pick the highest-ranked supported locale from an Accept-Language header. */
export function localeFromAcceptLanguage(header: string | null): string | null {
if (!header) return null;
const supported = new Set<string>(routing.locales as readonly string[]);
const ranked = header
.split(',')
.map((part) => {
const [tag, q] = part.trim().split(';q=');
return { locale: normalizeLocaleTag(tag), q: q ? parseFloat(q) : 1 };
})
.sort((a, b) => b.q - a.q);
for (const { locale } of ranked) {
if (supported.has(locale)) return locale;
}
return null;
}
+2 -19
View File
@@ -1,29 +1,12 @@
import { getRequestConfig } from 'next-intl/server';
import { headers } from 'next/headers';
import { localeFromAcceptLanguage, normalizeLocaleTag } from './locale-resolution';
import { mergeMessages } from './merge-messages';
import { routing, type Locale } from './routing';
// Default a first-time visitor (no explicit stored choice) to the best supported
// language from their browser's Accept-Language header, so "Auto" is the real
// default and the first server render already matches the browser language.
function localeFromAcceptLanguage(header: string | null): string | null {
if (!header) return null;
const supported = new Set<string>(routing.locales as readonly string[]);
const ranked = header
.split(',')
.map((part) => {
const [tag, q] = part.trim().split(';q=');
return { base: tag.toLowerCase().split('-')[0], q: q ? parseFloat(q) : 1 };
})
.sort((a, b) => b.q - a.q);
for (const { base } of ranked) {
if (supported.has(base)) return base;
}
return null;
}
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (locale) locale = normalizeLocaleTag(locale);
if (!locale || !routing.locales.includes(locale as Locale)) {
const accept = (await headers()).get('accept-language');