Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/middleware/language/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,82 @@ describe('languageDetector', () => {
expect(await res.text()).toBe('fr')
})

it('should fallback to language code when locale code is not in supportedLanguages', async () => {
const app = createTestApp({
supportedLanguages: ['en', 'ja'],
fallbackLanguage: 'en',
order: ['header'],
})

const res = await app.request('/', {
headers: {
'accept-language': 'ja-JP',
},
})
expect(await res.text()).toBe('ja')
})

it('should match after multiple truncations', async () => {
const app = createTestApp({
supportedLanguages: ['zh-Hant', 'en'],
fallbackLanguage: 'en',
order: ['header'],
})

const res = await app.request('/', {
headers: {
'accept-language': 'zh-Hant-CN',
},
})
expect(await res.text()).toBe('zh-Hant')
})

it('should fallback when truncation does not match any supported language', async () => {
const app = createTestApp({
supportedLanguages: ['en', 'ja'],
fallbackLanguage: 'en',
order: ['header'],
})

const res = await app.request('/', {
headers: {
'accept-language': 'ko-KR',
},
})
expect(await res.text()).toBe('en')
})

it('should prefer exact match over truncated match', async () => {
const app = createTestApp({
supportedLanguages: ['fr', 'fr-CA'],
fallbackLanguage: 'fr',
order: ['header'],
})

const res = await app.request('/', {
headers: {
'accept-language': 'fr-CA',
},
})
expect(await res.text()).toBe('fr-CA')
})

it('should handle case-insensitive truncation matching', async () => {
const app = createTestApp({
supportedLanguages: ['en', 'ja'],
fallbackLanguage: 'en',
order: ['header'],
ignoreCase: true,
})

const res = await app.request('/', {
headers: {
'accept-language': 'JA-JP',
},
})
expect(await res.text()).toBe('ja')
})

it('should handle malformed Accept-Language headers', async () => {
const app = createTestApp({
supportedLanguages: ['en', 'fr'],
Expand Down
19 changes: 17 additions & 2 deletions src/middleware/language/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,23 @@ export const normalizeLanguage = (
options.ignoreCase ? l.toLowerCase() : l
)

const matchedLang = compSupported.find((l) => l === compLang)
return matchedLang ? options.supportedLanguages[compSupported.indexOf(matchedLang)] : undefined
// Exact match
const exactIndex = compSupported.indexOf(compLang)
if (exactIndex !== -1) {
return options.supportedLanguages[exactIndex]
}

// Progressive truncation (RFC 4647 Lookup)
const parts = compLang.split('-')
for (let i = parts.length - 1; i > 0; i--) {
const candidate = parts.slice(0, i).join('-')
const prefixIndex = compSupported.indexOf(candidate)
if (prefixIndex !== -1) {
return options.supportedLanguages[prefixIndex]
}
}

return undefined
} catch {
return undefined
}
Expand Down
Loading