Skip to content

Commit cf95cad

Browse files
fix(translations): fall back to base language tag for Accept-Language matching (#15076)
### Why? Some browsers send only regional tags like `ja-JP` in the `Accept-Language` header. When no exact match exists, the current behavior can fail to detect a supported base language, leading to missed localization. ### What? Allow language detection to fall back from a regional tag (e.g., `ja-JP`) to its base tag (`ja`) when the exact match is not supported. ### How? While parsing `Accept-Language`, check for an exact match in `acceptedLanguages`, and if none is found, split on `-` and try the base language tag before giving up. --------- Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
1 parent d9b3c07 commit cf95cad

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { extractHeaderLanguage } from './languages.js'
4+
5+
describe('extractHeaderLanguage', () => {
6+
it('should return exact match when base language tag is supported', () => {
7+
expect(extractHeaderLanguage('ja')).toBe('ja')
8+
expect(extractHeaderLanguage('en')).toBe('en')
9+
expect(extractHeaderLanguage('de')).toBe('de')
10+
})
11+
12+
it('should fall back to base tag when regional tag has no exact match', () => {
13+
expect(extractHeaderLanguage('ja-JP')).toBe('ja')
14+
expect(extractHeaderLanguage('de-AT')).toBe('de')
15+
expect(extractHeaderLanguage('fr-BE')).toBe('fr')
16+
})
17+
18+
it('should match exact regional tag when it is supported', () => {
19+
// zh-TW is in acceptedLanguages, so it should match directly
20+
expect(extractHeaderLanguage('zh-TW')).toBe('zh-TW')
21+
// bn-BD is in acceptedLanguages
22+
expect(extractHeaderLanguage('bn-BD')).toBe('bn-BD')
23+
})
24+
25+
it('should fall back to base tag for unsupported regional variant of a supported regional language', () => {
26+
// zh-CN is not in acceptedLanguages, but zh is
27+
expect(extractHeaderLanguage('zh-CN')).toBe('zh')
28+
})
29+
30+
it('should return undefined for unsupported languages', () => {
31+
expect(extractHeaderLanguage('xx')).toBeUndefined()
32+
expect(extractHeaderLanguage('xx-XX')).toBeUndefined()
33+
})
34+
35+
it('should respect quality ordering and return highest quality supported language', () => {
36+
expect(extractHeaderLanguage('ja-JP;q=0.9, en;q=1.0')).toBe('en')
37+
expect(extractHeaderLanguage('ja-JP, en;q=0.9')).toBe('ja')
38+
})
39+
40+
it('should skip unsupported languages and find the next supported one', () => {
41+
expect(extractHeaderLanguage('xx-XX, ja-JP')).toBe('ja')
42+
expect(extractHeaderLanguage('xx, yy, de-DE')).toBe('de')
43+
})
44+
45+
it('should handle empty string', () => {
46+
// empty string produces one entry with empty language, which won't match
47+
expect(extractHeaderLanguage('')).toBeUndefined()
48+
})
49+
})

packages/translations/src/utilities/languages.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,19 @@ export function extractHeaderLanguage(acceptLanguageHeader: string): AcceptedLan
157157
let matchedLanguage: AcceptedLanguages | undefined
158158

159159
for (const { language } of parsedHeader) {
160-
if (!matchedLanguage && acceptedLanguages.includes(language)) {
160+
if (matchedLanguage) {
161+
break
162+
}
163+
164+
if (acceptedLanguages.includes(language)) {
161165
matchedLanguage = language
166+
break
167+
}
168+
169+
const baseLanguage = language.split('-')[0] as AcceptedLanguages
170+
if (acceptedLanguages.includes(baseLanguage)) {
171+
matchedLanguage = baseLanguage
172+
break
162173
}
163174
}
164175

0 commit comments

Comments
 (0)