From a78c1c9be9b0e83ef6e92e4babcfcd01f3152b3e Mon Sep 17 00:00:00 2001 From: CaIon Date: Thu, 5 Feb 2026 00:20:47 +0800 Subject: [PATCH] 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. --- i18n/i18n.go | 55 ++++++++++++++++++- main.go | 2 + middleware/auth.go | 11 ---- model/user_cache.go | 10 ++++ web/src/hooks/channels/useChannelsData.jsx | 2 +- .../hooks/redemptions/useRedemptionsData.jsx | 2 +- web/src/hooks/tokens/useTokensData.jsx | 2 +- web/src/hooks/users/useUsersData.jsx | 2 +- 8 files changed, 69 insertions(+), 17 deletions(-) diff --git a/i18n/i18n.go b/i18n/i18n.go index 45c5c38c..99505ee4 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -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 diff --git a/main.go b/main.go index 1c737b04..4f9cf84e 100644 --- a/main.go +++ b/main.go @@ -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 } diff --git a/middleware/auth.go b/middleware/auth.go index a5d283d2..0bb27ead 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -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() } diff --git a/model/user_cache.go b/model/user_cache.go index 7a6af098..2ba1f18e 100644 --- a/model/user_cache.go +++ b/model/user_cache.go @@ -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 +} diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 1ce0785f..12a757ab 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -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) { diff --git a/web/src/hooks/redemptions/useRedemptionsData.jsx b/web/src/hooks/redemptions/useRedemptionsData.jsx index 23c5b8d8..26b0f3a1 100644 --- a/web/src/hooks/redemptions/useRedemptionsData.jsx +++ b/web/src/hooks/redemptions/useRedemptionsData.jsx @@ -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) { diff --git a/web/src/hooks/tokens/useTokensData.jsx b/web/src/hooks/tokens/useTokensData.jsx index a34508f4..fb86c0cf 100644 --- a/web/src/hooks/tokens/useTokensData.jsx +++ b/web/src/hooks/tokens/useTokensData.jsx @@ -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') { diff --git a/web/src/hooks/users/useUsersData.jsx b/web/src/hooks/users/useUsersData.jsx index f906be54..96e1a194 100644 --- a/web/src/hooks/users/useUsersData.jsx +++ b/web/src/hooks/users/useUsersData.jsx @@ -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