From 71147b4857b103ec94eaa3ff69148fa77f765778 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Fri, 2 Jan 2026 21:01:43 +0800 Subject: [PATCH] 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> --- lib/localization/localization.go | 23 +++++++++++++++- lib/localization/localization_test.go | 38 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/localization/localization.go b/lib/localization/localization.go index 6b8d2a6..02189bf 100644 --- a/lib/localization/localization.go +++ b/lib/localization/localization.go @@ -81,7 +81,28 @@ func (ls *LocalizationService) GetLocalizerFromRequest(r *http.Request) *i18n.Lo return i18n.NewLocalizer(bundle, "en") } acceptLanguage := r.Header.Get("Accept-Language") - return i18n.NewLocalizer(ls.bundle, acceptLanguage, "en") + + // Parse Accept-Language header to properly handle quality factors + // The language.ParseAcceptLanguage function returns tags sorted by quality + tags, _, err := language.ParseAcceptLanguage(acceptLanguage) + if err != nil || len(tags) == 0 { + return i18n.NewLocalizer(ls.bundle, "en") + } + + // Convert parsed tags to strings for the localizer + // We include both the full tag and base language to ensure proper matching + langs := make([]string, 0, len(tags)*2+1) + for _, tag := range tags { + langs = append(langs, tag.String()) + // Also add base language (e.g., "en" for "en-GB") to help matching + base, _ := tag.Base() + if base.String() != tag.String() { + langs = append(langs, base.String()) + } + } + langs = append(langs, "en") // Always include English as fallback + + return i18n.NewLocalizer(ls.bundle, langs...) } // SimpleLocalizer wraps i18n.Localizer with a more convenient API diff --git a/lib/localization/localization_test.go b/lib/localization/localization_test.go index 47442f1..006e76d 100644 --- a/lib/localization/localization_test.go +++ b/lib/localization/localization_test.go @@ -3,6 +3,7 @@ package localization import ( "encoding/json" "fmt" + "net/http/httptest" "sort" "testing" @@ -138,3 +139,40 @@ func TestComprehensiveTranslations(t *testing.T) { }) } } + +func TestAcceptLanguageQualityFactors(t *testing.T) { + service := NewLocalizationService() + + testCases := []struct { + name string + acceptLanguage string + expectedLang string + }{ + {"simple_en", "en", "en"}, + {"simple_de", "de", "de"}, + {"en_GB_with_lower_priority_de", "en-GB,de-DE;q=0.5", "en"}, + {"en_GB_only", "en-GB", "en"}, + {"de_with_lower_priority_en", "de,en;q=0.5", "de"}, + {"de_DE_with_lower_priority_en", "de-DE,en;q=0.5", "de"}, + {"fr_with_lower_priority_de", "fr,de;q=0.5", "fr"}, + {"zh_CN_regional", "zh-CN", "zh-CN"}, + {"zh_TW_regional", "zh-TW", "zh-TW"}, + {"pt_BR_regional", "pt-BR", "pt-BR"}, + {"complex_header", "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7,de;q=0.5", "fr"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("Accept-Language", tc.acceptLanguage) + + localizer := service.GetLocalizerFromRequest(req) + sl := &SimpleLocalizer{Localizer: localizer} + + gotLang := sl.GetLang() + if gotLang != tc.expectedLang { + t.Errorf("Accept-Language %q: expected %s, got %s", tc.acceptLanguage, tc.expectedLang, gotLang) + } + }) + } +}