fix(i18n): prioritize user settings over Accept-Language header

The i18n middleware runs before UserAuth, so user settings weren't
available when language was detected. Now GetLangFromContext checks
user settings first (set by UserAuth) before falling back to the
language set by middleware or Accept-Language header.
This commit is contained in:
CaIon
2026-02-05 00:20:47 +08:00
parent 42b5aeaae4
commit 67613e0642
8 changed files with 69 additions and 17 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
)
const (
@@ -109,15 +110,65 @@ func Translate(lang, key string, args ...map[string]any) string {
return msg
}
// userLangLoaderFunc is a function that loads user language from database/cache
// It's set by the model package to avoid circular imports
var userLangLoaderFunc func(userId int) string
// SetUserLangLoader sets the function to load user language (called from model package)
func SetUserLangLoader(loader func(userId int) string) {
userLangLoaderFunc = loader
}
// GetLangFromContext extracts the language setting from gin context
// It checks multiple sources in priority order:
// 1. User settings (ContextKeyUserSetting) - if already loaded (e.g., by TokenAuth)
// 2. Lazy load user language from cache/DB using user ID
// 3. Language set by middleware (ContextKeyLanguage) - from Accept-Language header
// 4. Default language (English)
func GetLangFromContext(c *gin.Context) string {
if c == nil {
return DefaultLang
}
// Try to get language from context (set by middleware)
// 1. Try to get language from user settings (if already loaded by TokenAuth or other middleware)
if userSetting, ok := common.GetContextKeyType[dto.UserSetting](c, constant.ContextKeyUserSetting); ok {
if userSetting.Language != "" {
normalized := normalizeLang(userSetting.Language)
if IsSupported(normalized) {
return normalized
}
}
}
// 2. Lazy load user language using user ID (for session-based auth where full settings aren't loaded)
if userLangLoaderFunc != nil {
if userId, exists := c.Get("id"); exists {
if uid, ok := userId.(int); ok && uid > 0 {
lang := userLangLoaderFunc(uid)
if lang != "" {
normalized := normalizeLang(lang)
if IsSupported(normalized) {
return normalized
}
}
}
}
}
// 3. Try to get language from context (set by I18n middleware from Accept-Language)
if lang := c.GetString(string(constant.ContextKeyLanguage)); lang != "" {
return normalizeLang(lang)
normalized := normalizeLang(lang)
if IsSupported(normalized) {
return normalized
}
}
// 4. Try Accept-Language header directly (fallback if middleware didn't run)
if acceptLang := c.GetHeader("Accept-Language"); acceptLang != "" {
lang := ParseAcceptLanguage(acceptLang)
if IsSupported(lang) {
return lang
}
}
return DefaultLang

View File

@@ -288,6 +288,8 @@ func InitResources() error {
} else {
common.SysLog("i18n initialized with languages: " + strings.Join(i18n.SupportedLanguages(), ", "))
}
// Register user language loader for lazy loading
i18n.SetUserLangLoader(model.GetUserLanguage)
return nil
}

View File

@@ -132,17 +132,6 @@ func authHelper(c *gin.Context, minRole int) {
c.Set("user_group", session.Get("group"))
c.Set("use_access_token", useAccessToken)
//userCache, err := model.GetUserCache(id.(int))
//if err != nil {
// c.JSON(http.StatusOK, gin.H{
// "success": false,
// "message": err.Error(),
// })
// c.Abort()
// return
//}
//userCache.WriteContext(c)
c.Next()
}

View File

@@ -221,3 +221,13 @@ func updateUserSettingCache(userId int, setting string) error {
}
return common.RedisHSetField(getUserCacheKey(userId), "Setting", setting)
}
// GetUserLanguage returns the user's language preference from cache
// Uses the existing GetUserCache mechanism for efficiency
func GetUserLanguage(userId int) string {
userCache, err := GetUserCache(userId)
if err != nil {
return ""
}
return userCache.GetSetting().Language
}

View File

@@ -491,7 +491,7 @@ export const useChannelsData = () => {
}
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let newChannels = [...channels];
for (let i = 0; i < newChannels.length; i++) {
if (newChannels[i].tag === tag) {

View File

@@ -145,7 +145,7 @@ export const useRedemptionsData = () => {
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let redemption = res.data.data;
let newRedemptions = [...redemptions];
if (action !== REDEMPTION_ACTIONS.DELETE) {

View File

@@ -174,7 +174,7 @@ export const useTokensData = (openFluentNotification) => {
}
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
let token = res.data.data;
let newTokens = [...tokens];
if (action !== 'delete') {

View File

@@ -132,7 +132,7 @@ export const useUsersData = () => {
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
showSuccess(t('操作成功完成!'));
const user = res.data.data;
// Create a new array and new object to ensure React detects changes