feat(i18n): add backend multi-language support with user language preference

- Add go-i18n library for internationalization
- Create i18n package with translation keys and YAML locale files (zh/en)
- Implement i18n middleware for language detection from user settings and Accept-Language header
- Add Language field to UserSetting DTO
- Update API response helpers with i18n support (ApiErrorI18n, ApiSuccessI18n)
- Migrate hardcoded messages in token, redemption, and user controllers
- Add frontend language preference settings component
- Sync language preference across header selector and user settings
- Auto-restore user language preference on login
This commit is contained in:
CaIon
2026-02-05 00:07:54 +08:00
parent ded79c7684
commit f60fce6584
20 changed files with 1393 additions and 317 deletions

View File

@@ -1,12 +1,12 @@
package controller
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
@@ -108,10 +108,7 @@ func GetTokenUsage(c *gin.Context) {
token, err := model.GetTokenByKey(strings.TrimPrefix(tokenKey, "sk-"), false)
if err != nil {
common.SysError("failed to get token by key: " + err.Error())
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "获取令牌信息失败,请稍后重试",
})
common.ApiErrorI18n(c, i18n.MsgTokenGetInfoFailed)
return
}
@@ -145,36 +142,24 @@ func AddToken(c *gin.Context) {
return
}
if len(token.Name) > 50 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌名称过长",
})
common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong)
return
}
// 非无限额度时,检查额度值是否超出有效范围
if !token.UnlimitedQuota {
if token.RemainQuota < 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "额度值不能为负数",
})
common.ApiErrorI18n(c, i18n.MsgTokenQuotaNegative)
return
}
maxQuotaValue := int((1000000000 * common.QuotaPerUnit))
if token.RemainQuota > maxQuotaValue {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": fmt.Sprintf("额度值超出有效范围,最大值为 %d", maxQuotaValue),
})
common.ApiErrorI18n(c, i18n.MsgTokenQuotaExceedMax, map[string]any{"Max": maxQuotaValue})
return
}
}
key, err := common.GenerateKey()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "生成令牌失败",
})
common.ApiErrorI18n(c, i18n.MsgTokenGenerateFailed)
common.SysLog("failed to generate token key: " + err.Error())
return
}
@@ -230,26 +215,17 @@ func UpdateToken(c *gin.Context) {
return
}
if len(token.Name) > 50 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌名称过长",
})
common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong)
return
}
if !token.UnlimitedQuota {
if token.RemainQuota < 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "额度值不能为负数",
})
common.ApiErrorI18n(c, i18n.MsgTokenQuotaNegative)
return
}
maxQuotaValue := int((1000000000 * common.QuotaPerUnit))
if token.RemainQuota > maxQuotaValue {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": fmt.Sprintf("额度值超出有效范围,最大值为 %d", maxQuotaValue),
})
common.ApiErrorI18n(c, i18n.MsgTokenQuotaExceedMax, map[string]any{"Max": maxQuotaValue})
return
}
}
@@ -260,17 +236,11 @@ func UpdateToken(c *gin.Context) {
}
if token.Status == common.TokenStatusEnabled {
if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期",
})
common.ApiErrorI18n(c, i18n.MsgTokenExpiredCannotEnable)
return
}
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度",
})
common.ApiErrorI18n(c, i18n.MsgTokenExhaustedCannotEable)
return
}
}
@@ -307,10 +277,7 @@ type TokenBatch struct {
func DeleteTokenBatch(c *gin.Context) {
tokenBatch := TokenBatch{}
if err := c.ShouldBindJSON(&tokenBatch); err != nil || len(tokenBatch.Ids) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
userId := c.GetInt("id")