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

176
i18n/i18n.go Normal file
View File

@@ -0,0 +1,176 @@
package i18n
import (
"embed"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
)
const (
LangZh = "zh"
LangEn = "en"
DefaultLang = LangEn // Fallback to English if language not supported
)
//go:embed locales/*.yaml
var localeFS embed.FS
var (
bundle *i18n.Bundle
localizers = make(map[string]*i18n.Localizer)
mu sync.RWMutex
initOnce sync.Once
)
// Init initializes the i18n bundle and loads all translation files
func Init() error {
var initErr error
initOnce.Do(func() {
bundle = i18n.NewBundle(language.Chinese)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
// Load embedded translation files
files := []string{"locales/zh.yaml", "locales/en.yaml"}
for _, file := range files {
_, err := bundle.LoadMessageFileFS(localeFS, file)
if err != nil {
initErr = err
return
}
}
// Pre-create localizers for supported languages
localizers[LangZh] = i18n.NewLocalizer(bundle, LangZh)
localizers[LangEn] = i18n.NewLocalizer(bundle, LangEn)
// Set the TranslateMessage function in common package
common.TranslateMessage = T
})
return initErr
}
// GetLocalizer returns a localizer for the specified language
func GetLocalizer(lang string) *i18n.Localizer {
lang = normalizeLang(lang)
mu.RLock()
loc, ok := localizers[lang]
mu.RUnlock()
if ok {
return loc
}
// Create new localizer for unknown language (fallback to default)
mu.Lock()
defer mu.Unlock()
// Double-check after acquiring write lock
if loc, ok = localizers[lang]; ok {
return loc
}
loc = i18n.NewLocalizer(bundle, lang, DefaultLang)
localizers[lang] = loc
return loc
}
// T translates a message key using the language from gin context
func T(c *gin.Context, key string, args ...map[string]any) string {
lang := GetLangFromContext(c)
return Translate(lang, key, args...)
}
// Translate translates a message key for the specified language
func Translate(lang, key string, args ...map[string]any) string {
loc := GetLocalizer(lang)
config := &i18n.LocalizeConfig{
MessageID: key,
}
if len(args) > 0 && args[0] != nil {
config.TemplateData = args[0]
}
msg, err := loc.Localize(config)
if err != nil {
// Return key as fallback if translation not found
return key
}
return msg
}
// GetLangFromContext extracts the language setting from gin context
func GetLangFromContext(c *gin.Context) string {
if c == nil {
return DefaultLang
}
// Try to get language from context (set by middleware)
if lang := c.GetString(string(constant.ContextKeyLanguage)); lang != "" {
return normalizeLang(lang)
}
return DefaultLang
}
// ParseAcceptLanguage parses the Accept-Language header and returns the preferred language
func ParseAcceptLanguage(header string) string {
if header == "" {
return DefaultLang
}
// Simple parsing: take the first language tag
parts := strings.Split(header, ",")
if len(parts) == 0 {
return DefaultLang
}
// Get the first language and remove quality value
firstLang := strings.TrimSpace(parts[0])
if idx := strings.Index(firstLang, ";"); idx > 0 {
firstLang = firstLang[:idx]
}
return normalizeLang(firstLang)
}
// normalizeLang normalizes language code to supported format
func normalizeLang(lang string) string {
lang = strings.ToLower(strings.TrimSpace(lang))
// Handle common variations
switch {
case strings.HasPrefix(lang, "zh"):
return LangZh
case strings.HasPrefix(lang, "en"):
return LangEn
default:
return DefaultLang
}
}
// SupportedLanguages returns a list of supported language codes
func SupportedLanguages() []string {
return []string{LangZh, LangEn}
}
// IsSupported checks if a language code is supported
func IsSupported(lang string) bool {
lang = normalizeLang(lang)
for _, supported := range SupportedLanguages() {
if lang == supported {
return true
}
}
return false
}

270
i18n/keys.go Normal file
View File

@@ -0,0 +1,270 @@
package i18n
// Message keys for i18n translations
// Use these constants instead of hardcoded strings
// Common error messages
const (
MsgInvalidParams = "common.invalid_params"
MsgDatabaseError = "common.database_error"
MsgRetryLater = "common.retry_later"
MsgGenerateFailed = "common.generate_failed"
MsgNotFound = "common.not_found"
MsgUnauthorized = "common.unauthorized"
MsgForbidden = "common.forbidden"
MsgInvalidId = "common.invalid_id"
MsgIdEmpty = "common.id_empty"
MsgFeatureDisabled = "common.feature_disabled"
MsgOperationSuccess = "common.operation_success"
MsgOperationFailed = "common.operation_failed"
MsgUpdateSuccess = "common.update_success"
MsgUpdateFailed = "common.update_failed"
MsgCreateSuccess = "common.create_success"
MsgCreateFailed = "common.create_failed"
MsgDeleteSuccess = "common.delete_success"
MsgDeleteFailed = "common.delete_failed"
MsgAlreadyExists = "common.already_exists"
MsgNameCannotBeEmpty = "common.name_cannot_be_empty"
)
// Token related messages
const (
MsgTokenNameTooLong = "token.name_too_long"
MsgTokenQuotaNegative = "token.quota_negative"
MsgTokenQuotaExceedMax = "token.quota_exceed_max"
MsgTokenGenerateFailed = "token.generate_failed"
MsgTokenGetInfoFailed = "token.get_info_failed"
MsgTokenExpiredCannotEnable = "token.expired_cannot_enable"
MsgTokenExhaustedCannotEable = "token.exhausted_cannot_enable"
MsgTokenInvalid = "token.invalid"
MsgTokenNotProvided = "token.not_provided"
MsgTokenExpired = "token.expired"
MsgTokenExhausted = "token.exhausted"
MsgTokenStatusUnavailable = "token.status_unavailable"
MsgTokenDbError = "token.db_error"
)
// Redemption related messages
const (
MsgRedemptionNameLength = "redemption.name_length"
MsgRedemptionCountPositive = "redemption.count_positive"
MsgRedemptionCountMax = "redemption.count_max"
MsgRedemptionCreateFailed = "redemption.create_failed"
MsgRedemptionInvalid = "redemption.invalid"
MsgRedemptionUsed = "redemption.used"
MsgRedemptionExpired = "redemption.expired"
MsgRedemptionFailed = "redemption.failed"
MsgRedemptionNotProvided = "redemption.not_provided"
MsgRedemptionExpireTimeInvalid = "redemption.expire_time_invalid"
)
// User related messages
const (
MsgUserPasswordLoginDisabled = "user.password_login_disabled"
MsgUserRegisterDisabled = "user.register_disabled"
MsgUserPasswordRegisterDisabled = "user.password_register_disabled"
MsgUserUsernameOrPasswordEmpty = "user.username_or_password_empty"
MsgUserUsernameOrPasswordError = "user.username_or_password_error"
MsgUserEmailOrPasswordEmpty = "user.email_or_password_empty"
MsgUserExists = "user.exists"
MsgUserNotExists = "user.not_exists"
MsgUserDisabled = "user.disabled"
MsgUserSessionSaveFailed = "user.session_save_failed"
MsgUserRequire2FA = "user.require_2fa"
MsgUserEmailVerificationRequired = "user.email_verification_required"
MsgUserVerificationCodeError = "user.verification_code_error"
MsgUserInputInvalid = "user.input_invalid"
MsgUserNoPermissionSameLevel = "user.no_permission_same_level"
MsgUserNoPermissionHigherLevel = "user.no_permission_higher_level"
MsgUserCannotCreateHigherLevel = "user.cannot_create_higher_level"
MsgUserCannotDeleteRootUser = "user.cannot_delete_root_user"
MsgUserCannotDisableRootUser = "user.cannot_disable_root_user"
MsgUserCannotDemoteRootUser = "user.cannot_demote_root_user"
MsgUserAlreadyAdmin = "user.already_admin"
MsgUserAlreadyCommon = "user.already_common"
MsgUserAdminCannotPromote = "user.admin_cannot_promote"
MsgUserOriginalPasswordError = "user.original_password_error"
MsgUserInviteQuotaInsufficient = "user.invite_quota_insufficient"
MsgUserTransferQuotaMinimum = "user.transfer_quota_minimum"
MsgUserTransferSuccess = "user.transfer_success"
MsgUserTransferFailed = "user.transfer_failed"
MsgUserTopUpProcessing = "user.topup_processing"
MsgUserRegisterFailed = "user.register_failed"
MsgUserDefaultTokenFailed = "user.default_token_failed"
MsgUserAffCodeEmpty = "user.aff_code_empty"
MsgUserEmailEmpty = "user.email_empty"
MsgUserGitHubIdEmpty = "user.github_id_empty"
MsgUserDiscordIdEmpty = "user.discord_id_empty"
MsgUserOidcIdEmpty = "user.oidc_id_empty"
MsgUserWeChatIdEmpty = "user.wechat_id_empty"
MsgUserTelegramIdEmpty = "user.telegram_id_empty"
MsgUserTelegramNotBound = "user.telegram_not_bound"
MsgUserLinuxDOIdEmpty = "user.linux_do_id_empty"
)
// Quota related messages
const (
MsgQuotaNegative = "quota.negative"
MsgQuotaExceedMax = "quota.exceed_max"
MsgQuotaInsufficient = "quota.insufficient"
MsgQuotaWarningInvalid = "quota.warning_invalid"
MsgQuotaThresholdGtZero = "quota.threshold_gt_zero"
)
// Subscription related messages
const (
MsgSubscriptionNotEnabled = "subscription.not_enabled"
MsgSubscriptionTitleEmpty = "subscription.title_empty"
MsgSubscriptionPriceNegative = "subscription.price_negative"
MsgSubscriptionPriceMax = "subscription.price_max"
MsgSubscriptionPurchaseLimitNeg = "subscription.purchase_limit_negative"
MsgSubscriptionQuotaNegative = "subscription.quota_negative"
MsgSubscriptionGroupNotExists = "subscription.group_not_exists"
MsgSubscriptionResetCycleGtZero = "subscription.reset_cycle_gt_zero"
MsgSubscriptionPurchaseMax = "subscription.purchase_max"
MsgSubscriptionInvalidId = "subscription.invalid_id"
MsgSubscriptionInvalidUserId = "subscription.invalid_user_id"
)
// Payment related messages
const (
MsgPaymentNotConfigured = "payment.not_configured"
MsgPaymentMethodNotExists = "payment.method_not_exists"
MsgPaymentCallbackError = "payment.callback_error"
MsgPaymentCreateFailed = "payment.create_failed"
MsgPaymentStartFailed = "payment.start_failed"
MsgPaymentAmountTooLow = "payment.amount_too_low"
MsgPaymentStripeNotConfig = "payment.stripe_not_configured"
MsgPaymentWebhookNotConfig = "payment.webhook_not_configured"
MsgPaymentPriceIdNotConfig = "payment.price_id_not_configured"
MsgPaymentCreemNotConfig = "payment.creem_not_configured"
)
// Topup related messages
const (
MsgTopupNotProvided = "topup.not_provided"
MsgTopupOrderNotExists = "topup.order_not_exists"
MsgTopupOrderStatus = "topup.order_status"
MsgTopupFailed = "topup.failed"
MsgTopupInvalidQuota = "topup.invalid_quota"
)
// Channel related messages
const (
MsgChannelNotExists = "channel.not_exists"
MsgChannelIdFormatError = "channel.id_format_error"
MsgChannelNoAvailableKey = "channel.no_available_key"
MsgChannelGetListFailed = "channel.get_list_failed"
MsgChannelGetTagsFailed = "channel.get_tags_failed"
MsgChannelGetKeyFailed = "channel.get_key_failed"
MsgChannelGetOllamaFailed = "channel.get_ollama_failed"
MsgChannelQueryFailed = "channel.query_failed"
MsgChannelNoValidUpstream = "channel.no_valid_upstream"
MsgChannelUpstreamSaturated = "channel.upstream_saturated"
MsgChannelGetAvailableFailed = "channel.get_available_failed"
)
// Model related messages
const (
MsgModelNameEmpty = "model.name_empty"
MsgModelNameExists = "model.name_exists"
MsgModelIdMissing = "model.id_missing"
MsgModelGetListFailed = "model.get_list_failed"
MsgModelGetFailed = "model.get_failed"
MsgModelResetSuccess = "model.reset_success"
)
// Vendor related messages
const (
MsgVendorNameEmpty = "vendor.name_empty"
MsgVendorNameExists = "vendor.name_exists"
MsgVendorIdMissing = "vendor.id_missing"
)
// Group related messages
const (
MsgGroupNameTypeEmpty = "group.name_type_empty"
MsgGroupNameExists = "group.name_exists"
MsgGroupIdMissing = "group.id_missing"
)
// Checkin related messages
const (
MsgCheckinDisabled = "checkin.disabled"
MsgCheckinAlreadyToday = "checkin.already_today"
MsgCheckinFailed = "checkin.failed"
MsgCheckinQuotaFailed = "checkin.quota_failed"
)
// Passkey related messages
const (
MsgPasskeyCreateFailed = "passkey.create_failed"
MsgPasskeyLoginAbnormal = "passkey.login_abnormal"
MsgPasskeyUpdateFailed = "passkey.update_failed"
MsgPasskeyInvalidUserId = "passkey.invalid_user_id"
MsgPasskeyVerifyFailed = "passkey.verify_failed"
)
// 2FA related messages
const (
MsgTwoFANotEnabled = "twofa.not_enabled"
MsgTwoFAUserIdEmpty = "twofa.user_id_empty"
MsgTwoFAAlreadyExists = "twofa.already_exists"
MsgTwoFARecordIdEmpty = "twofa.record_id_empty"
MsgTwoFACodeInvalid = "twofa.code_invalid"
)
// Rate limit related messages
const (
MsgRateLimitReached = "rate_limit.reached"
MsgRateLimitTotalReached = "rate_limit.total_reached"
)
// Setting related messages
const (
MsgSettingInvalidType = "setting.invalid_type"
MsgSettingWebhookEmpty = "setting.webhook_empty"
MsgSettingWebhookInvalid = "setting.webhook_invalid"
MsgSettingEmailInvalid = "setting.email_invalid"
MsgSettingBarkUrlEmpty = "setting.bark_url_empty"
MsgSettingBarkUrlInvalid = "setting.bark_url_invalid"
MsgSettingGotifyUrlEmpty = "setting.gotify_url_empty"
MsgSettingGotifyTokenEmpty = "setting.gotify_token_empty"
MsgSettingGotifyUrlInvalid = "setting.gotify_url_invalid"
MsgSettingUrlMustHttp = "setting.url_must_http"
MsgSettingSaved = "setting.saved"
)
// Deployment related messages (io.net)
const (
MsgDeploymentNotEnabled = "deployment.not_enabled"
MsgDeploymentIdRequired = "deployment.id_required"
MsgDeploymentContainerIdReq = "deployment.container_id_required"
MsgDeploymentNameEmpty = "deployment.name_empty"
MsgDeploymentNameTaken = "deployment.name_taken"
MsgDeploymentHardwareIdReq = "deployment.hardware_id_required"
MsgDeploymentHardwareInvId = "deployment.hardware_invalid_id"
MsgDeploymentApiKeyRequired = "deployment.api_key_required"
MsgDeploymentInvalidPayload = "deployment.invalid_payload"
MsgDeploymentNotFound = "deployment.not_found"
)
// Performance related messages
const (
MsgPerfDiskCacheCleared = "performance.disk_cache_cleared"
MsgPerfStatsReset = "performance.stats_reset"
MsgPerfGcExecuted = "performance.gc_executed"
)
// Ability related messages
const (
MsgAbilityDbCorrupted = "ability.db_corrupted"
MsgAbilityRepairRunning = "ability.repair_running"
)
// OAuth related messages
const (
MsgOAuthInvalidCode = "oauth.invalid_code"
MsgOAuthGetUserErr = "oauth.get_user_error"
MsgOAuthAccountUsed = "oauth.account_used"
)

225
i18n/locales/en.yaml Normal file
View File

@@ -0,0 +1,225 @@
# English translations
# Common messages
common.invalid_params: "Invalid parameters"
common.database_error: "Database error, please try again later"
common.retry_later: "Please try again later"
common.generate_failed: "Generation failed"
common.not_found: "Not found"
common.unauthorized: "Unauthorized"
common.forbidden: "Forbidden"
common.invalid_id: "Invalid ID"
common.id_empty: "ID is empty!"
common.feature_disabled: "This feature is not enabled"
common.operation_success: "Operation successful"
common.operation_failed: "Operation failed"
common.update_success: "Update successful"
common.update_failed: "Update failed"
common.create_success: "Creation successful"
common.create_failed: "Creation failed"
common.delete_success: "Deletion successful"
common.delete_failed: "Deletion failed"
common.already_exists: "Already exists"
common.name_cannot_be_empty: "Name cannot be empty"
# Token messages
token.name_too_long: "Token name is too long"
token.quota_negative: "Quota value cannot be negative"
token.quota_exceed_max: "Quota value exceeds valid range, maximum is {{.Max}}"
token.generate_failed: "Failed to generate token"
token.get_info_failed: "Failed to get token info, please try again later"
token.expired_cannot_enable: "Token has expired and cannot be enabled. Please modify the expiration time or set it to never expire"
token.exhausted_cannot_enable: "Token quota is exhausted and cannot be enabled. Please modify the remaining quota or set it to unlimited"
token.invalid: "Invalid token"
token.not_provided: "Token not provided"
token.expired: "This token has expired"
token.exhausted: "This token quota is exhausted TokenStatusExhausted[sk-{{.Prefix}}***{{.Suffix}}]"
token.status_unavailable: "This token status is unavailable"
token.db_error: "Invalid token, database query error, please contact administrator"
# Redemption messages
redemption.name_length: "Redemption code name length must be between 1-20"
redemption.count_positive: "Redemption code count must be greater than 0"
redemption.count_max: "Maximum 100 redemption codes can be generated at once"
redemption.create_failed: "Failed to create redemption code, please try again later"
redemption.invalid: "Invalid redemption code"
redemption.used: "This redemption code has been used"
redemption.expired: "This redemption code has expired"
redemption.failed: "Redemption failed, please try again later"
redemption.not_provided: "Redemption code not provided"
redemption.expire_time_invalid: "Expiration time cannot be earlier than current time"
# User messages
user.password_login_disabled: "Password login has been disabled by administrator"
user.register_disabled: "New user registration has been disabled by administrator"
user.password_register_disabled: "Password registration has been disabled by administrator, please use third-party account verification"
user.username_or_password_empty: "Username or password is empty"
user.username_or_password_error: "Username or password is incorrect, or user has been banned"
user.email_or_password_empty: "Email or password is empty!"
user.exists: "Username already exists or has been deleted"
user.not_exists: "User does not exist"
user.disabled: "This user has been disabled"
user.session_save_failed: "Failed to save session, please try again"
user.require_2fa: "Please enter two-factor authentication code"
user.email_verification_required: "Email verification is enabled, please enter email address and verification code"
user.verification_code_error: "Verification code is incorrect or has expired"
user.input_invalid: "Invalid input {{.Error}}"
user.no_permission_same_level: "No permission to access users of same or higher level"
user.no_permission_higher_level: "No permission to update users of same or higher permission level"
user.cannot_create_higher_level: "Cannot create users with permission level equal to or higher than yourself"
user.cannot_delete_root_user: "Cannot delete super administrator account"
user.cannot_disable_root_user: "Cannot disable super administrator user"
user.cannot_demote_root_user: "Cannot demote super administrator user"
user.already_admin: "This user is already an administrator"
user.already_common: "This user is already a common user"
user.admin_cannot_promote: "Regular administrators cannot promote other users to administrator"
user.original_password_error: "Original password is incorrect"
user.invite_quota_insufficient: "Invitation quota is insufficient!"
user.transfer_quota_minimum: "Minimum transfer quota is {{.Min}}!"
user.transfer_success: "Transfer successful"
user.transfer_failed: "Transfer failed {{.Error}}"
user.topup_processing: "Top-up is processing, please try again later"
user.register_failed: "User registration failed or user ID retrieval failed"
user.default_token_failed: "Failed to generate default token"
user.aff_code_empty: "Affiliate code is empty!"
user.email_empty: "Email is empty!"
user.github_id_empty: "GitHub ID is empty!"
user.discord_id_empty: "Discord ID is empty!"
user.oidc_id_empty: "OIDC ID is empty!"
user.wechat_id_empty: "WeChat ID is empty!"
user.telegram_id_empty: "Telegram ID is empty!"
user.telegram_not_bound: "This Telegram account is not bound"
user.linux_do_id_empty: "Linux DO ID is empty!"
# Quota messages
quota.negative: "Quota cannot be negative!"
quota.exceed_max: "Quota value exceeds valid range"
quota.insufficient: "Insufficient quota"
quota.warning_invalid: "Invalid warning type"
quota.threshold_gt_zero: "Warning threshold must be greater than 0"
# Subscription messages
subscription.not_enabled: "Subscription plan is not enabled"
subscription.title_empty: "Subscription plan title cannot be empty"
subscription.price_negative: "Price cannot be negative"
subscription.price_max: "Price cannot exceed 9999"
subscription.purchase_limit_negative: "Purchase limit cannot be negative"
subscription.quota_negative: "Total quota cannot be negative"
subscription.group_not_exists: "Upgrade group does not exist"
subscription.reset_cycle_gt_zero: "Custom reset cycle must be greater than 0 seconds"
subscription.purchase_max: "Purchase limit for this plan has been reached"
subscription.invalid_id: "Invalid subscription ID"
subscription.invalid_user_id: "Invalid user ID"
# Payment messages
payment.not_configured: "Payment information has not been configured by administrator"
payment.method_not_exists: "Payment method does not exist"
payment.callback_error: "Callback URL configuration error"
payment.create_failed: "Failed to create order"
payment.start_failed: "Failed to start payment"
payment.amount_too_low: "Plan amount is too low"
payment.stripe_not_configured: "Stripe is not configured or key is invalid"
payment.webhook_not_configured: "Webhook is not configured"
payment.price_id_not_configured: "StripePriceId is not configured for this plan"
payment.creem_not_configured: "CreemProductId is not configured for this plan"
# Topup messages
topup.not_provided: "Payment order number not provided"
topup.order_not_exists: "Top-up order does not exist"
topup.order_status: "Top-up order status error"
topup.failed: "Top-up failed, please try again later"
topup.invalid_quota: "Invalid top-up quota"
# Channel messages
channel.not_exists: "Channel does not exist"
channel.id_format_error: "Channel ID format error"
channel.no_available_key: "No available channel keys"
channel.get_list_failed: "Failed to get channel list, please try again later"
channel.get_tags_failed: "Failed to get tags, please try again later"
channel.get_key_failed: "Failed to get channel key"
channel.get_ollama_failed: "Failed to get Ollama models"
channel.query_failed: "Failed to query channel"
channel.no_valid_upstream: "No valid upstream channel"
channel.upstream_saturated: "Current group upstream load is saturated, please try again later"
channel.get_available_failed: "Failed to get available channels for model {{.Model}} under group {{.Group}}"
# Model messages
model.name_empty: "Model name cannot be empty"
model.name_exists: "Model name already exists"
model.id_missing: "Model ID is missing"
model.get_list_failed: "Failed to get model list, please try again later"
model.get_failed: "Failed to get upstream models"
model.reset_success: "Model ratio reset successful"
# Vendor messages
vendor.name_empty: "Vendor name cannot be empty"
vendor.name_exists: "Vendor name already exists"
vendor.id_missing: "Vendor ID is missing"
# Group messages
group.name_type_empty: "Group name and type cannot be empty"
group.name_exists: "Group name already exists"
group.id_missing: "Group ID is missing"
# Checkin messages
checkin.disabled: "Check-in feature is not enabled"
checkin.already_today: "Already checked in today"
checkin.failed: "Check-in failed, please try again later"
checkin.quota_failed: "Check-in failed: quota update error"
# Passkey messages
passkey.create_failed: "Unable to create Passkey credential"
passkey.login_abnormal: "Passkey login status is abnormal"
passkey.update_failed: "Passkey credential update failed"
passkey.invalid_user_id: "Invalid user ID"
passkey.verify_failed: "Passkey verification failed, please try again or contact administrator"
# 2FA messages
twofa.not_enabled: "User has not enabled 2FA"
twofa.user_id_empty: "User ID cannot be empty"
twofa.already_exists: "User already has 2FA configured"
twofa.record_id_empty: "2FA record ID cannot be empty"
twofa.code_invalid: "Verification code or backup code is incorrect"
# Rate limit messages
rate_limit.reached: "You have reached the request limit: maximum {{.Max}} requests in {{.Minutes}} minutes"
rate_limit.total_reached: "You have reached the total request limit: maximum {{.Max}} requests in {{.Minutes}} minutes, including failed attempts"
# Setting messages
setting.invalid_type: "Invalid warning type"
setting.webhook_empty: "Webhook URL cannot be empty"
setting.webhook_invalid: "Invalid Webhook URL"
setting.email_invalid: "Invalid email address"
setting.bark_url_empty: "Bark push URL cannot be empty"
setting.bark_url_invalid: "Invalid Bark push URL"
setting.gotify_url_empty: "Gotify server URL cannot be empty"
setting.gotify_token_empty: "Gotify token cannot be empty"
setting.gotify_url_invalid: "Invalid Gotify server URL"
setting.url_must_http: "URL must start with http:// or https://"
setting.saved: "Settings updated"
# Deployment messages (io.net)
deployment.not_enabled: "io.net model deployment is not enabled or API key is missing"
deployment.id_required: "Deployment ID is required"
deployment.container_id_required: "Container ID is required"
deployment.name_empty: "Deployment name cannot be empty"
deployment.name_taken: "Deployment name is not available, please choose a different name"
deployment.hardware_id_required: "hardware_id parameter is required"
deployment.hardware_invalid_id: "Invalid hardware_id parameter"
deployment.api_key_required: "api_key is required"
deployment.invalid_payload: "Invalid request payload"
deployment.not_found: "Container details not found"
# Performance messages
performance.disk_cache_cleared: "Inactive disk cache has been cleared"
performance.stats_reset: "Statistics have been reset"
performance.gc_executed: "GC has been executed"
# Ability messages
ability.db_corrupted: "Database consistency has been compromised"
ability.repair_running: "A repair task is already running, please try again later"
# OAuth messages
oauth.invalid_code: "Invalid authorization code"
oauth.get_user_error: "Failed to get user information"
oauth.account_used: "This account has been bound to another user"

226
i18n/locales/zh.yaml Normal file
View File

@@ -0,0 +1,226 @@
# Chinese (Simplified) translations
# 中文(简体)翻译文件
# Common messages
common.invalid_params: "无效的参数"
common.database_error: "数据库错误,请稍后重试"
common.retry_later: "请稍后重试"
common.generate_failed: "生成失败"
common.not_found: "未找到"
common.unauthorized: "未授权"
common.forbidden: "无权限"
common.invalid_id: "无效的ID"
common.id_empty: "ID 为空!"
common.feature_disabled: "该功能未启用"
common.operation_success: "操作成功"
common.operation_failed: "操作失败"
common.update_success: "更新成功"
common.update_failed: "更新失败"
common.create_success: "创建成功"
common.create_failed: "创建失败"
common.delete_success: "删除成功"
common.delete_failed: "删除失败"
common.already_exists: "已存在"
common.name_cannot_be_empty: "名称不能为空"
# Token messages
token.name_too_long: "令牌名称过长"
token.quota_negative: "额度值不能为负数"
token.quota_exceed_max: "额度值超出有效范围,最大值为 {{.Max}}"
token.generate_failed: "生成令牌失败"
token.get_info_failed: "获取令牌信息失败,请稍后重试"
token.expired_cannot_enable: "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期"
token.exhausted_cannot_enable: "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度"
token.invalid: "无效的令牌"
token.not_provided: "未提供令牌"
token.expired: "该令牌已过期"
token.exhausted: "该令牌额度已用尽 TokenStatusExhausted[sk-{{.Prefix}}***{{.Suffix}}]"
token.status_unavailable: "该令牌状态不可用"
token.db_error: "无效的令牌,数据库查询出错,请联系管理员"
# Redemption messages
redemption.name_length: "兑换码名称长度必须在1-20之间"
redemption.count_positive: "兑换码个数必须大于0"
redemption.count_max: "一次兑换码批量生成的个数不能大于 100"
redemption.create_failed: "创建兑换码失败,请稍后重试"
redemption.invalid: "无效的兑换码"
redemption.used: "该兑换码已被使用"
redemption.expired: "该兑换码已过期"
redemption.failed: "兑换失败,请稍后重试"
redemption.not_provided: "未提供兑换码"
redemption.expire_time_invalid: "过期时间不能早于当前时间"
# User messages
user.password_login_disabled: "管理员关闭了密码登录"
user.register_disabled: "管理员关闭了新用户注册"
user.password_register_disabled: "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册"
user.username_or_password_empty: "用户名或密码为空"
user.username_or_password_error: "用户名或密码错误,或用户已被封禁"
user.email_or_password_empty: "邮箱地址或密码为空!"
user.exists: "用户名已存在,或已注销"
user.not_exists: "用户不存在"
user.disabled: "该用户已被禁用"
user.session_save_failed: "无法保存会话信息,请重试"
user.require_2fa: "请输入两步验证码"
user.email_verification_required: "管理员开启了邮箱验证,请输入邮箱地址和验证码"
user.verification_code_error: "验证码错误或已过期"
user.input_invalid: "输入不合法 {{.Error}}"
user.no_permission_same_level: "无权获取同级或更高等级用户的信息"
user.no_permission_higher_level: "无权更新同权限等级或更高权限等级的用户信息"
user.cannot_create_higher_level: "无法创建权限大于等于自己的用户"
user.cannot_delete_root_user: "不能删除超级管理员账户"
user.cannot_disable_root_user: "无法禁用超级管理员用户"
user.cannot_demote_root_user: "无法降级超级管理员用户"
user.already_admin: "该用户已经是管理员"
user.already_common: "该用户已经是普通用户"
user.admin_cannot_promote: "普通管理员用户无法提升其他用户为管理员"
user.original_password_error: "原密码错误"
user.invite_quota_insufficient: "邀请额度不足!"
user.transfer_quota_minimum: "转移额度最小为{{.Min}}"
user.transfer_success: "划转成功"
user.transfer_failed: "划转失败 {{.Error}}"
user.topup_processing: "充值处理中,请稍后重试"
user.register_failed: "用户注册失败或用户ID获取失败"
user.default_token_failed: "生成默认令牌失败"
user.aff_code_empty: "affCode 为空!"
user.email_empty: "email 为空!"
user.github_id_empty: "GitHub id 为空!"
user.discord_id_empty: "discord id 为空!"
user.oidc_id_empty: "oidc id 为空!"
user.wechat_id_empty: "WeChat id 为空!"
user.telegram_id_empty: "Telegram id 为空!"
user.telegram_not_bound: "该 Telegram 账户未绑定"
user.linux_do_id_empty: "Linux DO id 为空!"
# Quota messages
quota.negative: "额度不能为负数!"
quota.exceed_max: "额度值超出有效范围"
quota.insufficient: "额度不足"
quota.warning_invalid: "无效的预警类型"
quota.threshold_gt_zero: "预警阈值必须大于0"
# Subscription messages
subscription.not_enabled: "套餐未启用"
subscription.title_empty: "套餐标题不能为空"
subscription.price_negative: "价格不能为负数"
subscription.price_max: "价格不能超过9999"
subscription.purchase_limit_negative: "购买上限不能为负数"
subscription.quota_negative: "总额度不能为负数"
subscription.group_not_exists: "升级分组不存在"
subscription.reset_cycle_gt_zero: "自定义重置周期需大于0秒"
subscription.purchase_max: "已达到该套餐购买上限"
subscription.invalid_id: "无效的订阅ID"
subscription.invalid_user_id: "无效的用户ID"
# Payment messages
payment.not_configured: "当前管理员未配置支付信息"
payment.method_not_exists: "支付方式不存在"
payment.callback_error: "回调地址配置错误"
payment.create_failed: "创建订单失败"
payment.start_failed: "拉起支付失败"
payment.amount_too_low: "套餐金额过低"
payment.stripe_not_configured: "Stripe 未配置或密钥无效"
payment.webhook_not_configured: "Webhook 未配置"
payment.price_id_not_configured: "该套餐未配置 StripePriceId"
payment.creem_not_configured: "该套餐未配置 CreemProductId"
# Topup messages
topup.not_provided: "未提供支付单号"
topup.order_not_exists: "充值订单不存在"
topup.order_status: "充值订单状态错误"
topup.failed: "充值失败,请稍后重试"
topup.invalid_quota: "无效的充值额度"
# Channel messages
channel.not_exists: "渠道不存在"
channel.id_format_error: "渠道ID格式错误"
channel.no_available_key: "没有可用的渠道密钥"
channel.get_list_failed: "获取渠道列表失败,请稍后重试"
channel.get_tags_failed: "获取标签失败,请稍后重试"
channel.get_key_failed: "获取渠道密钥失败"
channel.get_ollama_failed: "获取Ollama模型失败"
channel.query_failed: "查询渠道失败"
channel.no_valid_upstream: "无有效上游渠道"
channel.upstream_saturated: "当前分组上游负载已饱和,请稍后再试"
channel.get_available_failed: "获取分组 {{.Group}} 下模型 {{.Model}} 的可用渠道失败"
# Model messages
model.name_empty: "模型名称不能为空"
model.name_exists: "模型名称已存在"
model.id_missing: "缺少模型 ID"
model.get_list_failed: "获取模型列表失败,请稍后重试"
model.get_failed: "获取上游模型失败"
model.reset_success: "重置模型倍率成功"
# Vendor messages
vendor.name_empty: "供应商名称不能为空"
vendor.name_exists: "供应商名称已存在"
vendor.id_missing: "缺少供应商 ID"
# Group messages
group.name_type_empty: "组名称和类型不能为空"
group.name_exists: "组名称已存在"
group.id_missing: "缺少组 ID"
# Checkin messages
checkin.disabled: "签到功能未启用"
checkin.already_today: "今日已签到"
checkin.failed: "签到失败,请稍后重试"
checkin.quota_failed: "签到失败:更新额度出错"
# Passkey messages
passkey.create_failed: "无法创建 Passkey 凭证"
passkey.login_abnormal: "Passkey 登录状态异常"
passkey.update_failed: "Passkey 凭证更新失败"
passkey.invalid_user_id: "无效的用户 ID"
passkey.verify_failed: "Passkey 验证失败,请重试或联系管理员"
# 2FA messages
twofa.not_enabled: "用户未启用2FA"
twofa.user_id_empty: "用户ID不能为空"
twofa.already_exists: "用户已存在2FA设置"
twofa.record_id_empty: "2FA记录ID不能为空"
twofa.code_invalid: "验证码或备用码不正确"
# Rate limit messages
rate_limit.reached: "您已达到请求数限制:{{.Minutes}}分钟内最多请求{{.Max}}次"
rate_limit.total_reached: "您已达到总请求数限制:{{.Minutes}}分钟内最多请求{{.Max}}次,包括失败次数"
# Setting messages
setting.invalid_type: "无效的预警类型"
setting.webhook_empty: "Webhook地址不能为空"
setting.webhook_invalid: "无效的Webhook地址"
setting.email_invalid: "无效的邮箱地址"
setting.bark_url_empty: "Bark推送URL不能为空"
setting.bark_url_invalid: "无效的Bark推送URL"
setting.gotify_url_empty: "Gotify服务器地址不能为空"
setting.gotify_token_empty: "Gotify令牌不能为空"
setting.gotify_url_invalid: "无效的Gotify服务器地址"
setting.url_must_http: "URL必须以http://或https://开头"
setting.saved: "设置已更新"
# Deployment messages (io.net)
deployment.not_enabled: "io.net 模型部署功能未启用或 API 密钥缺失"
deployment.id_required: "deployment ID 为必填项"
deployment.container_id_required: "container ID 为必填项"
deployment.name_empty: "deployment 名称不能为空"
deployment.name_taken: "deployment 名称已被使用,请选择其他名称"
deployment.hardware_id_required: "hardware_id 参数为必填项"
deployment.hardware_invalid_id: "无效的 hardware_id 参数"
deployment.api_key_required: "api_key 为必填项"
deployment.invalid_payload: "无效的请求内容"
deployment.not_found: "未找到容器详情"
# Performance messages
performance.disk_cache_cleared: "不活跃的磁盘缓存已清理"
performance.stats_reset: "统计信息已重置"
performance.gc_executed: "GC 已执行"
# Ability messages
ability.db_corrupted: "数据库一致性被破坏"
ability.repair_running: "已经有一个修复任务在运行中,请稍后再试"
# OAuth messages
oauth.invalid_code: "无效的授权码"
oauth.get_user_error: "获取用户信息失败"
oauth.account_used: "该账户已被其他用户绑定"