|
@@ -0,0 +1,138 @@
|
|
|
+package middleware
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io/ioutil"
|
|
|
+ "net/http"
|
|
|
+ "os"
|
|
|
+ "path"
|
|
|
+ "path/filepath"
|
|
|
+ "strings"
|
|
|
+
|
|
|
+ "gitee.com/xuyiping_admin/pkg/logger/zaplog"
|
|
|
+ "gitee.com/xuyiping_admin/pkg/xerr"
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+ "github.com/nicksnyder/go-i18n/v2/i18n"
|
|
|
+ "go.uber.org/zap"
|
|
|
+ "golang.org/x/text/language"
|
|
|
+)
|
|
|
+
|
|
|
+var (
|
|
|
+ localesDir = "./locales"
|
|
|
+ bundle *i18n.Bundle
|
|
|
+ defaultLanguage = language.Chinese
|
|
|
+ supported = map[string]bool{ // 支持的语言列表
|
|
|
+ "zh": true, // 中文
|
|
|
+ "en": true, // 英文
|
|
|
+ // 添加其他支持的语言...
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// I18N 初始化国际化中间件
|
|
|
+func I18N() gin.HandlerFunc {
|
|
|
+ bundle = i18n.NewBundle(defaultLanguage)
|
|
|
+ bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
|
+
|
|
|
+ // 加载翻译文件
|
|
|
+ if err := loadTranslations(); err != nil {
|
|
|
+ zaplog.Error("Failed to load translations", zap.Error(err))
|
|
|
+ }
|
|
|
+
|
|
|
+ return func(c *gin.Context) {
|
|
|
+ lang := detectLanguage(c.Request)
|
|
|
+ localize := i18n.NewLocalizer(bundle, lang)
|
|
|
+ c.Set(LanguageContent, localize)
|
|
|
+ c.Set(Language, lang)
|
|
|
+ c.Next()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// loadTranslations 加载所有翻译文件
|
|
|
+func loadTranslations() error {
|
|
|
+ absPath, err := filepath.Abs(localesDir)
|
|
|
+ if err != nil {
|
|
|
+ return xerr.WithStack(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ files, err := ioutil.ReadDir(absPath)
|
|
|
+ if err != nil {
|
|
|
+ return xerr.WithStack(fmt.Errorf("failed to read files dir: %w", err))
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(files) == 0 {
|
|
|
+ return xerr.Custom("no found in files directory")
|
|
|
+ }
|
|
|
+
|
|
|
+ loaded := false
|
|
|
+ for _, entry := range files {
|
|
|
+ if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ filePath := path.Join(absPath, entry.Name())
|
|
|
+ data, err := os.ReadFile(filePath)
|
|
|
+ if err != nil {
|
|
|
+ zaplog.Error("Failed to read translation file",
|
|
|
+ zap.String("file", filePath),
|
|
|
+ zap.Error(err))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ messageFile, err := bundle.ParseMessageFileBytes(data, filePath)
|
|
|
+ if err != nil {
|
|
|
+ zaplog.Error("Failed to parse translation file",
|
|
|
+ zap.String("file", filePath),
|
|
|
+ zap.Error(err))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ zaplog.Info("Loaded translation",
|
|
|
+ zap.String("language", messageFile.Tag.String()),
|
|
|
+ zap.Any("messageFile", messageFile))
|
|
|
+ loaded = true
|
|
|
+ }
|
|
|
+
|
|
|
+ if !loaded {
|
|
|
+ return xerr.Custom("no valid translation files were loaded")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// detectLanguage 检测客户端语言偏好
|
|
|
+func detectLanguage(r *http.Request) string {
|
|
|
+ /*// 1. 检查查询参数
|
|
|
+ if lang := r.URL.Query().Get("lang"); lang != "" {
|
|
|
+ return normalizeLanguage(lang)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查 Cookie
|
|
|
+ if langCookie, err := r.Cookie("lang"); err == nil && langCookie.Value != "" {
|
|
|
+ return normalizeLanguage(langCookie.Value)
|
|
|
+ }
|
|
|
+ */
|
|
|
+ // 3. 检查 Accept-Language 头
|
|
|
+ if acceptLang := r.Header.Get("Accept-Language"); acceptLang != "" {
|
|
|
+ if langs := strings.Split(acceptLang, ","); len(langs) > 0 {
|
|
|
+ return normalizeLanguage(langs[0])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 默认返回中文
|
|
|
+ return "zh"
|
|
|
+}
|
|
|
+
|
|
|
+// normalizeLanguage 规范化语言代码
|
|
|
+func normalizeLanguage(lang string) string {
|
|
|
+ // 去除权重值 (如 en;q=0.9 -> en)
|
|
|
+ if parts := strings.Split(lang, ";"); len(parts) > 0 {
|
|
|
+ lang = parts[0]
|
|
|
+ }
|
|
|
+
|
|
|
+ // 转换为小写并去除地区代码 (en-US -> en)
|
|
|
+ lang = strings.ToLower(strings.Split(lang, "-")[0])
|
|
|
+
|
|
|
+ if supported[lang] {
|
|
|
+ return lang
|
|
|
+ }
|
|
|
+ return "zh" // 默认中文
|
|
|
+}
|