github.com/gogf/gf@v1.16.9/i18n/gi18n/gi18n_manager.go (about) 1 // Copyright GoFrame Author(https://goframe.org). All Rights Reserved. 2 // 3 // This Source Code Form is subject to the terms of the MIT License. 4 // If a copy of the MIT was not distributed with this file, 5 // You can obtain one at https://github.com/gogf/gf. 6 7 package gi18n 8 9 import ( 10 "context" 11 "fmt" 12 "github.com/gogf/gf/errors/gcode" 13 "github.com/gogf/gf/errors/gerror" 14 "github.com/gogf/gf/internal/intlog" 15 "strings" 16 "sync" 17 18 "github.com/gogf/gf/os/gfsnotify" 19 20 "github.com/gogf/gf/text/gregex" 21 22 "github.com/gogf/gf/util/gconv" 23 24 "github.com/gogf/gf/encoding/gjson" 25 26 "github.com/gogf/gf/os/gfile" 27 "github.com/gogf/gf/os/gres" 28 ) 29 30 // Manager for i18n contents, it is concurrent safe, supporting hot reload. 31 type Manager struct { 32 mu sync.RWMutex 33 data map[string]map[string]string // Translating map. 34 pattern string // Pattern for regex parsing. 35 options Options // configuration options. 36 } 37 38 // Options is used for i18n object configuration. 39 type Options struct { 40 Path string // I18n files storage path. 41 Language string // Default local language. 42 Delimiters []string // Delimiters for variable parsing. 43 } 44 45 var ( 46 defaultLanguage = "en" // defaultDelimiters defines the default language if user does not specified in options. 47 defaultDelimiters = []string{"{#", "}"} // defaultDelimiters defines the default key variable delimiters. 48 ) 49 50 // New creates and returns a new i18n manager. 51 // The optional parameter <option> specifies the custom options for i18n manager. 52 // It uses a default one if it's not passed. 53 func New(options ...Options) *Manager { 54 var opts Options 55 if len(options) > 0 { 56 opts = options[0] 57 } else { 58 opts = DefaultOptions() 59 } 60 if len(opts.Language) == 0 { 61 opts.Language = defaultLanguage 62 } 63 if len(opts.Delimiters) == 0 { 64 opts.Delimiters = defaultDelimiters 65 } 66 m := &Manager{ 67 options: opts, 68 pattern: fmt.Sprintf( 69 `%s(\w+)%s`, 70 gregex.Quote(opts.Delimiters[0]), 71 gregex.Quote(opts.Delimiters[1]), 72 ), 73 } 74 intlog.Printf(context.TODO(), `New: %#v`, m) 75 return m 76 } 77 78 // DefaultOptions creates and returns a default options for i18n manager. 79 func DefaultOptions() Options { 80 var ( 81 path = "i18n" 82 realPath, _ = gfile.Search(path) 83 ) 84 if realPath != "" { 85 path = realPath 86 // To avoid of the source path of GF: github.com/gogf/i18n/gi18n 87 if gfile.Exists(path + gfile.Separator + "gi18n") { 88 path = "" 89 } 90 } 91 return Options{ 92 Path: path, 93 Language: "en", 94 Delimiters: defaultDelimiters, 95 } 96 } 97 98 // SetPath sets the directory path storing i18n files. 99 func (m *Manager) SetPath(path string) error { 100 if gres.Contains(path) { 101 m.options.Path = path 102 } else { 103 realPath, _ := gfile.Search(path) 104 if realPath == "" { 105 return gerror.NewCodef(gcode.CodeInvalidParameter, `%s does not exist`, path) 106 } 107 m.options.Path = realPath 108 } 109 intlog.Printf(context.TODO(), `SetPath: %s`, m.options.Path) 110 return nil 111 } 112 113 // SetLanguage sets the language for translator. 114 func (m *Manager) SetLanguage(language string) { 115 m.options.Language = language 116 intlog.Printf(context.TODO(), `SetLanguage: %s`, m.options.Language) 117 } 118 119 // SetDelimiters sets the delimiters for translator. 120 func (m *Manager) SetDelimiters(left, right string) { 121 m.pattern = fmt.Sprintf(`%s(\w+)%s`, gregex.Quote(left), gregex.Quote(right)) 122 intlog.Printf(context.TODO(), `SetDelimiters: %v`, m.pattern) 123 } 124 125 // T is alias of Translate for convenience. 126 func (m *Manager) T(ctx context.Context, content string) string { 127 return m.Translate(ctx, content) 128 } 129 130 // Tf is alias of TranslateFormat for convenience. 131 func (m *Manager) Tf(ctx context.Context, format string, values ...interface{}) string { 132 return m.TranslateFormat(ctx, format, values...) 133 } 134 135 // TranslateFormat translates, formats and returns the <format> with configured language 136 // and given <values>. 137 func (m *Manager) TranslateFormat(ctx context.Context, format string, values ...interface{}) string { 138 return fmt.Sprintf(m.Translate(ctx, format), values...) 139 } 140 141 // Translate translates <content> with configured language. 142 func (m *Manager) Translate(ctx context.Context, content string) string { 143 m.init(ctx) 144 m.mu.RLock() 145 defer m.mu.RUnlock() 146 transLang := m.options.Language 147 if lang := LanguageFromCtx(ctx); lang != "" { 148 transLang = lang 149 } 150 data := m.data[transLang] 151 if data == nil { 152 return content 153 } 154 // Parse content as name. 155 if v, ok := data[content]; ok { 156 return v 157 } 158 // Parse content as variables container. 159 result, _ := gregex.ReplaceStringFuncMatch( 160 m.pattern, content, 161 func(match []string) string { 162 if v, ok := data[match[1]]; ok { 163 return v 164 } 165 return match[0] 166 }) 167 intlog.Printf(ctx, `Translate for language: %s`, transLang) 168 return result 169 } 170 171 // GetContent retrieves and returns the configured content for given key and specified language. 172 // It returns an empty string if not found. 173 func (m *Manager) GetContent(ctx context.Context, key string) string { 174 m.init(ctx) 175 m.mu.RLock() 176 defer m.mu.RUnlock() 177 transLang := m.options.Language 178 if lang := LanguageFromCtx(ctx); lang != "" { 179 transLang = lang 180 } 181 if data, ok := m.data[transLang]; ok { 182 return data[key] 183 } 184 return "" 185 } 186 187 // init initializes the manager for lazy initialization design. 188 // The i18n manager is only initialized once. 189 func (m *Manager) init(ctx context.Context) { 190 m.mu.RLock() 191 // If the data is not nil, means it's already initialized. 192 if m.data != nil { 193 m.mu.RUnlock() 194 return 195 } 196 m.mu.RUnlock() 197 198 m.mu.Lock() 199 defer m.mu.Unlock() 200 if gres.Contains(m.options.Path) { 201 files := gres.ScanDirFile(m.options.Path, "*.*", true) 202 if len(files) > 0 { 203 var ( 204 path string 205 name string 206 lang string 207 array []string 208 ) 209 m.data = make(map[string]map[string]string) 210 for _, file := range files { 211 name = file.Name() 212 path = name[len(m.options.Path)+1:] 213 array = strings.Split(path, "/") 214 if len(array) > 1 { 215 lang = array[0] 216 } else { 217 lang = gfile.Name(array[0]) 218 } 219 if m.data[lang] == nil { 220 m.data[lang] = make(map[string]string) 221 } 222 if j, err := gjson.LoadContent(file.Content()); err == nil { 223 for k, v := range j.Map() { 224 m.data[lang][k] = gconv.String(v) 225 } 226 } else { 227 intlog.Errorf(ctx, "load i18n file '%s' failed: %v", name, err) 228 } 229 } 230 } 231 } else if m.options.Path != "" { 232 files, _ := gfile.ScanDirFile(m.options.Path, "*.*", true) 233 if len(files) == 0 { 234 return 235 } 236 var ( 237 path string 238 lang string 239 array []string 240 ) 241 m.data = make(map[string]map[string]string) 242 for _, file := range files { 243 path = file[len(m.options.Path)+1:] 244 array = strings.Split(path, gfile.Separator) 245 if len(array) > 1 { 246 lang = array[0] 247 } else { 248 lang = gfile.Name(array[0]) 249 } 250 if m.data[lang] == nil { 251 m.data[lang] = make(map[string]string) 252 } 253 if j, err := gjson.LoadContent(gfile.GetBytes(file)); err == nil { 254 for k, v := range j.Map() { 255 m.data[lang][k] = gconv.String(v) 256 } 257 } else { 258 intlog.Errorf(ctx, "load i18n file '%s' failed: %v", file, err) 259 } 260 } 261 // Monitor changes of i18n files for hot reload feature. 262 _, _ = gfsnotify.Add(path, func(event *gfsnotify.Event) { 263 // Any changes of i18n files, clear the data. 264 m.mu.Lock() 265 m.data = nil 266 m.mu.Unlock() 267 gfsnotify.Exit() 268 }) 269 } 270 }