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) }