Skip to content

Commit 02346c6

Browse files
authored
feat(language): add progressive locale code truncation to normalizeLanguage (#4717)
* feat: lang locate code non all match trancation * fix: locate code trancate use lastIndexOf('-') * fix: back use for let
1 parent 7438ab9 commit 02346c6

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/middleware/language/index.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,82 @@ describe('languageDetector', () => {
7777
expect(await res.text()).toBe('fr')
7878
})
7979

80+
it('should fallback to language code when locale code is not in supportedLanguages', async () => {
81+
const app = createTestApp({
82+
supportedLanguages: ['en', 'ja'],
83+
fallbackLanguage: 'en',
84+
order: ['header'],
85+
})
86+
87+
const res = await app.request('/', {
88+
headers: {
89+
'accept-language': 'ja-JP',
90+
},
91+
})
92+
expect(await res.text()).toBe('ja')
93+
})
94+
95+
it('should match after multiple truncations', async () => {
96+
const app = createTestApp({
97+
supportedLanguages: ['zh-Hant', 'en'],
98+
fallbackLanguage: 'en',
99+
order: ['header'],
100+
})
101+
102+
const res = await app.request('/', {
103+
headers: {
104+
'accept-language': 'zh-Hant-CN',
105+
},
106+
})
107+
expect(await res.text()).toBe('zh-Hant')
108+
})
109+
110+
it('should fallback when truncation does not match any supported language', async () => {
111+
const app = createTestApp({
112+
supportedLanguages: ['en', 'ja'],
113+
fallbackLanguage: 'en',
114+
order: ['header'],
115+
})
116+
117+
const res = await app.request('/', {
118+
headers: {
119+
'accept-language': 'ko-KR',
120+
},
121+
})
122+
expect(await res.text()).toBe('en')
123+
})
124+
125+
it('should prefer exact match over truncated match', async () => {
126+
const app = createTestApp({
127+
supportedLanguages: ['fr', 'fr-CA'],
128+
fallbackLanguage: 'fr',
129+
order: ['header'],
130+
})
131+
132+
const res = await app.request('/', {
133+
headers: {
134+
'accept-language': 'fr-CA',
135+
},
136+
})
137+
expect(await res.text()).toBe('fr-CA')
138+
})
139+
140+
it('should handle case-insensitive truncation matching', async () => {
141+
const app = createTestApp({
142+
supportedLanguages: ['en', 'ja'],
143+
fallbackLanguage: 'en',
144+
order: ['header'],
145+
ignoreCase: true,
146+
})
147+
148+
const res = await app.request('/', {
149+
headers: {
150+
'accept-language': 'JA-JP',
151+
},
152+
})
153+
expect(await res.text()).toBe('ja')
154+
})
155+
80156
it('should handle malformed Accept-Language headers', async () => {
81157
const app = createTestApp({
82158
supportedLanguages: ['en', 'fr'],

src/middleware/language/language.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,23 @@ export const normalizeLanguage = (
100100
options.ignoreCase ? l.toLowerCase() : l
101101
)
102102

103-
const matchedLang = compSupported.find((l) => l === compLang)
104-
return matchedLang ? options.supportedLanguages[compSupported.indexOf(matchedLang)] : undefined
103+
// Exact match
104+
const exactIndex = compSupported.indexOf(compLang)
105+
if (exactIndex !== -1) {
106+
return options.supportedLanguages[exactIndex]
107+
}
108+
109+
// Progressive truncation (RFC 4647 Lookup)
110+
const parts = compLang.split('-')
111+
for (let i = parts.length - 1; i > 0; i--) {
112+
const candidate = parts.slice(0, i).join('-')
113+
const prefixIndex = compSupported.indexOf(candidate)
114+
if (prefixIndex !== -1) {
115+
return options.supportedLanguages[prefixIndex]
116+
}
117+
}
118+
119+
return undefined
105120
} catch {
106121
return undefined
107122
}

0 commit comments

Comments
 (0)