Skip to content

Commit 71147b4

Browse files
authored
fix: respect Accept-Language quality factors in language detection (#1380)
The Accept-Language header parsing was not correctly handling quality factors. When a browser sends "en-GB,de-DE;q=0.5", the expected behavior is to prefer English (q=1.0 by default) over German (q=0.5). The fix uses golang.org/x/text/language.ParseAcceptLanguage to properly parse and sort language preferences by quality factor. It also adds base language fallbacks (e.g., "en" for "en-GB") to ensure regional variants match their parent languages when no exact match exists. Fixes #1022 Signed-off-by: majiayu000 <1835304752@qq.com>
1 parent cee7871 commit 71147b4

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

lib/localization/localization.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,28 @@ func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Lo
8181
return i18n.NewLocalizer(bundle, "en")
8282
}
8383
acceptLanguage := r.Header.Get("Accept-Language")
84-
return i18n.NewLocalizer(ls.bundle, acceptLanguage, "en")
84+
85+
// Parse Accept-Language header to properly handle quality factors
86+
// The language.ParseAcceptLanguage function returns tags sorted by quality
87+
tags, _, err := language.ParseAcceptLanguage(acceptLanguage)
88+
if err != nil || len(tags) == 0 {
89+
return i18n.NewLocalizer(ls.bundle, "en")
90+
}
91+
92+
// Convert parsed tags to strings for the localizer
93+
// We include both the full tag and base language to ensure proper matching
94+
langs := make([]string, 0, len(tags)*2+1)
95+
for _, tag := range tags {
96+
langs = append(langs, tag.String())
97+
// Also add base language (e.g., "en" for "en-GB") to help matching
98+
base, _ := tag.Base()
99+
if base.String() != tag.String() {
100+
langs = append(langs, base.String())
101+
}
102+
}
103+
langs = append(langs, "en") // Always include English as fallback
104+
105+
return i18n.NewLocalizer(ls.bundle, langs...)
85106
}
86107

87108
// SimpleLocalizer wraps i18n.Localizer with a more convenient API

lib/localization/localization_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package localization
33
import (
44
"encoding/json"
55
"fmt"
6+
"net/http/httptest"
67
"sort"
78
"testing"
89

@@ -138,3 +139,40 @@ func TestComprehensiveTranslations(t *testing.T) {
138139
})
139140
}
140141
}
142+
143+
func TestAcceptLanguageQualityFactors(t *testing.T) {
144+
service := NewLocalizationService()
145+
146+
testCases := []struct {
147+
name string
148+
acceptLanguage string
149+
expectedLang string
150+
}{
151+
{"simple_en", "en", "en"},
152+
{"simple_de", "de", "de"},
153+
{"en_GB_with_lower_priority_de", "en-GB,de-DE;q=0.5", "en"},
154+
{"en_GB_only", "en-GB", "en"},
155+
{"de_with_lower_priority_en", "de,en;q=0.5", "de"},
156+
{"de_DE_with_lower_priority_en", "de-DE,en;q=0.5", "de"},
157+
{"fr_with_lower_priority_de", "fr,de;q=0.5", "fr"},
158+
{"zh_CN_regional", "zh-CN", "zh-CN"},
159+
{"zh_TW_regional", "zh-TW", "zh-TW"},
160+
{"pt_BR_regional", "pt-BR", "pt-BR"},
161+
{"complex_header", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,de;q=0.5", "fr"},
162+
}
163+
164+
for _, tc := range testCases {
165+
t.Run(tc.name, func(t *testing.T) {
166+
req := httptest.NewRequest("GET", "/", nil)
167+
req.Header.Set("Accept-Language", tc.acceptLanguage)
168+
169+
localizer := service.GetLocalizerFromRequest(req)
170+
sl := &SimpleLocalizer{Localizer: localizer}
171+
172+
gotLang := sl.GetLang()
173+
if gotLang != tc.expectedLang {
174+
t.Errorf("Accept-Language %q: expected %s, got %s", tc.acceptLanguage, tc.expectedLang, gotLang)
175+
}
176+
})
177+
}
178+
}

0 commit comments

Comments
 (0)