增加多语言支持
This commit is contained in:
286
i18n/i18n.go
Normal file
286
i18n/i18n.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MessageInfo 消息信息结构
|
||||
// 包含业务错误码和消息内容
|
||||
type MessageInfo struct {
|
||||
Code int `json:"code"` // 业务错误码
|
||||
Message string `json:"message"` // 消息内容
|
||||
}
|
||||
|
||||
// I18n 国际化工具
|
||||
// 支持多语言内容管理,通过语言代码和消息代码获取对应语言的内容
|
||||
type I18n struct {
|
||||
messages map[string]map[string]MessageInfo // 存储格式:messages[语言][code] = MessageInfo
|
||||
defaultLang string // 默认语言代码
|
||||
mu sync.RWMutex // 读写锁,保证并发安全
|
||||
}
|
||||
|
||||
// NewI18n 创建国际化工具实例
|
||||
// defaultLang: 默认语言代码(如 "zh-CN", "en-US"),当指定语言不存在时使用
|
||||
func NewI18n(defaultLang string) *I18n {
|
||||
return &I18n{
|
||||
messages: make(map[string]map[string]MessageInfo),
|
||||
defaultLang: defaultLang,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFromFile 从单个语言文件加载内容
|
||||
// filePath: 语言文件路径(JSON格式)
|
||||
// lang: 语言代码(如 "zh-CN", "en-US")
|
||||
//
|
||||
// 文件格式示例(zh-CN.json):
|
||||
//
|
||||
// {
|
||||
// "user.not_found": {
|
||||
// "code": 1001,
|
||||
// "message": "用户不存在"
|
||||
// },
|
||||
// "user.login_success": {
|
||||
// "code": 0,
|
||||
// "message": "登录成功"
|
||||
// },
|
||||
// "user.welcome": {
|
||||
// "code": 0,
|
||||
// "message": "欢迎,%s"
|
||||
// }
|
||||
// }
|
||||
func (i *I18n) LoadFromFile(filePath, lang string) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
var messages map[string]MessageInfo
|
||||
if err := json.Unmarshal(data, &messages); err != nil {
|
||||
return fmt.Errorf("failed to parse JSON file %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
if i.messages[lang] == nil {
|
||||
i.messages[lang] = make(map[string]MessageInfo)
|
||||
}
|
||||
|
||||
// 合并到现有消息中(如果key已存在会被覆盖)
|
||||
for k, v := range messages {
|
||||
i.messages[lang][k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromDir 从目录加载多个语言文件
|
||||
// dirPath: 语言文件目录路径
|
||||
// 文件命名规则:{语言代码}.json(如 zh-CN.json, en-US.json)
|
||||
//
|
||||
// 示例目录结构:
|
||||
//
|
||||
// locales/
|
||||
// zh-CN.json
|
||||
// en-US.json
|
||||
// ja-JP.json
|
||||
func (i *I18n) LoadFromDir(dirPath string) error {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read directory %s: %w", dirPath, err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 只处理 .json 文件
|
||||
if !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从文件名提取语言代码(去掉 .json 后缀)
|
||||
lang := strings.TrimSuffix(entry.Name(), ".json")
|
||||
filePath := filepath.Join(dirPath, entry.Name())
|
||||
|
||||
if err := i.LoadFromFile(filePath, lang); err != nil {
|
||||
return fmt.Errorf("failed to load language file %s: %w", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromMap 从map加载语言内容(用于测试或动态加载)
|
||||
// lang: 语言代码
|
||||
// messages: 消息map,key为消息代码,value为消息信息
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// i18n.LoadFromMap("zh-CN", map[string]MessageInfo{
|
||||
// "user.not_found": {Code: 1001, Message: "用户不存在"},
|
||||
// "user.login_success": {Code: 0, Message: "登录成功"},
|
||||
// })
|
||||
func (i *I18n) LoadFromMap(lang string, messages map[string]MessageInfo) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
if i.messages[lang] == nil {
|
||||
i.messages[lang] = make(map[string]MessageInfo)
|
||||
}
|
||||
|
||||
// 合并到现有消息中
|
||||
for k, v := range messages {
|
||||
i.messages[lang][k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// GetMessage 获取指定语言和代码的消息内容
|
||||
// lang: 语言代码(如 "zh-CN", "en-US")
|
||||
// code: 消息代码(如 "user.not_found")
|
||||
// args: 可选参数,用于格式化消息(类似 fmt.Sprintf)
|
||||
//
|
||||
// 返回逻辑:
|
||||
// 1. 如果指定语言存在该code,返回对应内容
|
||||
// 2. 如果指定语言不存在,尝试使用默认语言
|
||||
// 3. 如果默认语言也不存在,返回code本身(作为fallback)
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// msg := i18n.GetMessage("zh-CN", "user.not_found")
|
||||
// // 返回: "用户不存在"
|
||||
//
|
||||
// msg := i18n.GetMessage("zh-CN", "user.welcome", "Alice")
|
||||
// // 如果消息内容是 "欢迎,%s",返回: "欢迎,Alice"
|
||||
func (i *I18n) GetMessage(lang, code string, args ...interface{}) string {
|
||||
info := i.GetMessageInfo(lang, code, args...)
|
||||
return info.Message
|
||||
}
|
||||
|
||||
// GetMessageInfo 获取指定语言和代码的完整消息信息(包含业务code)
|
||||
// lang: 语言代码(如 "zh-CN", "en-US")
|
||||
// code: 消息代码(如 "user.not_found")
|
||||
// args: 可选参数,用于格式化消息(类似 fmt.Sprintf)
|
||||
//
|
||||
// 返回逻辑:
|
||||
// 1. 如果指定语言存在该code,返回对应的MessageInfo
|
||||
// 2. 如果指定语言不存在,尝试使用默认语言
|
||||
// 3. 如果默认语言也不存在,返回code本身作为message,code为0
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// info := i18n.GetMessageInfo("zh-CN", "user.not_found")
|
||||
// // 返回: MessageInfo{Code: 1001, Message: "用户不存在"}
|
||||
func (i *I18n) GetMessageInfo(lang, code string, args ...interface{}) MessageInfo {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
// 尝试从指定语言获取
|
||||
if messages, ok := i.messages[lang]; ok {
|
||||
if msgInfo, ok := messages[code]; ok {
|
||||
// 格式化消息内容
|
||||
msgInfo.Message = i.formatMessage(msgInfo.Message, args...)
|
||||
return msgInfo
|
||||
}
|
||||
}
|
||||
|
||||
// 如果指定语言不存在该code,尝试使用默认语言
|
||||
if i.defaultLang != "" && i.defaultLang != lang {
|
||||
if messages, ok := i.messages[i.defaultLang]; ok {
|
||||
if msgInfo, ok := messages[code]; ok {
|
||||
// 格式化消息内容
|
||||
msgInfo.Message = i.formatMessage(msgInfo.Message, args...)
|
||||
return msgInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都不存在,返回code本身作为message,code为0(作为fallback)
|
||||
return MessageInfo{
|
||||
Code: 0,
|
||||
Message: code,
|
||||
}
|
||||
}
|
||||
|
||||
// formatMessage 格式化消息(支持参数替换)
|
||||
// 如果消息中包含 %s, %d 等格式化占位符,使用 args 进行替换
|
||||
func (i *I18n) formatMessage(msg string, args ...interface{}) string {
|
||||
if len(args) == 0 {
|
||||
return msg
|
||||
}
|
||||
|
||||
// 检查消息中是否包含格式化占位符
|
||||
if strings.Contains(msg, "%") {
|
||||
return fmt.Sprintf(msg, args...)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// SetDefaultLang 设置默认语言
|
||||
// lang: 默认语言代码
|
||||
func (i *I18n) SetDefaultLang(lang string) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
i.defaultLang = lang
|
||||
}
|
||||
|
||||
// GetDefaultLang 获取默认语言代码
|
||||
func (i *I18n) GetDefaultLang() string {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
return i.defaultLang
|
||||
}
|
||||
|
||||
// HasLang 检查是否已加载指定语言
|
||||
// lang: 语言代码
|
||||
func (i *I18n) HasLang(lang string) bool {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
_, ok := i.messages[lang]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetSupportedLangs 获取所有已加载的语言代码列表
|
||||
func (i *I18n) GetSupportedLangs() []string {
|
||||
i.mu.RLock()
|
||||
defer i.mu.RUnlock()
|
||||
|
||||
langs := make([]string, 0, len(i.messages))
|
||||
for lang := range i.messages {
|
||||
langs = append(langs, lang)
|
||||
}
|
||||
|
||||
return langs
|
||||
}
|
||||
|
||||
// ReloadFromFile 重新加载指定语言文件
|
||||
// filePath: 语言文件路径
|
||||
// lang: 语言代码
|
||||
func (i *I18n) ReloadFromFile(filePath, lang string) error {
|
||||
// 先清除该语言的所有消息
|
||||
i.mu.Lock()
|
||||
delete(i.messages, lang)
|
||||
i.mu.Unlock()
|
||||
|
||||
// 重新加载
|
||||
return i.LoadFromFile(filePath, lang)
|
||||
}
|
||||
|
||||
// ReloadFromDir 重新加载目录中的所有语言文件
|
||||
// dirPath: 语言文件目录路径
|
||||
func (i *I18n) ReloadFromDir(dirPath string) error {
|
||||
// 先清除所有消息
|
||||
i.mu.Lock()
|
||||
i.messages = make(map[string]map[string]MessageInfo)
|
||||
i.mu.Unlock()
|
||||
|
||||
// 重新加载
|
||||
return i.LoadFromDir(dirPath)
|
||||
}
|
||||
Reference in New Issue
Block a user