github.com/gogf/gf/v2@v2.7.4/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 "strings" 13 "sync" 14 15 "github.com/gogf/gf/v2/encoding/gjson" 16 "github.com/gogf/gf/v2/errors/gcode" 17 "github.com/gogf/gf/v2/errors/gerror" 18 "github.com/gogf/gf/v2/internal/intlog" 19 "github.com/gogf/gf/v2/os/gfile" 20 "github.com/gogf/gf/v2/os/gfsnotify" 21 "github.com/gogf/gf/v2/os/gres" 22 "github.com/gogf/gf/v2/text/gregex" 23 "github.com/gogf/gf/v2/util/gconv" 24 ) 25 26 // pathType is the type for i18n file path. 27 type pathType string 28 29 const ( 30 pathTypeNone pathType = "none" 31 pathTypeNormal pathType = "normal" 32 pathTypeGres pathType = "gres" 33 ) 34 35 // Manager for i18n contents, it is concurrent safe, supporting hot reload. 36 type Manager struct { 37 mu sync.RWMutex 38 data map[string]map[string]string // Translating map. 39 pattern string // Pattern for regex parsing. 40 pathType pathType // Path type for i18n files. 41 options Options // configuration options. 42 } 43 44 // Options is used for i18n object configuration. 45 type Options struct { 46 Path string // I18n files storage path. 47 Language string // Default local language. 48 Delimiters []string // Delimiters for variable parsing. 49 Resource *gres.Resource // Resource for i18n files. 50 } 51 52 var ( 53 // defaultLanguage defines the default language if user does not specify in options. 54 defaultLanguage = "en" 55 56 // defaultDelimiters defines the default key variable delimiters. 57 defaultDelimiters = []string{"{#", "}"} 58 59 // i18n files searching folders. 60 searchFolders = []string{"manifest/i18n", "manifest/config/i18n", "i18n"} 61 ) 62 63 // New creates and returns a new i18n manager. 64 // The optional parameter `option` specifies the custom options for i18n manager. 65 // It uses a default one if it's not passed. 66 func New(options ...Options) *Manager { 67 var opts Options 68 var pathType = pathTypeNone 69 if len(options) > 0 { 70 opts = options[0] 71 pathType = opts.checkPathType(opts.Path) 72 } else { 73 opts = Options{} 74 for _, folder := range searchFolders { 75 pathType = opts.checkPathType(folder) 76 if pathType != pathTypeNone { 77 break 78 } 79 } 80 if opts.Path != "" { 81 // To avoid of the source path of GoFrame: github.com/gogf/i18n/gi18n 82 if gfile.Exists(opts.Path + gfile.Separator + "gi18n") { 83 opts.Path = "" 84 pathType = pathTypeNone 85 } 86 } 87 } 88 if len(opts.Language) == 0 { 89 opts.Language = defaultLanguage 90 } 91 if len(opts.Delimiters) == 0 { 92 opts.Delimiters = defaultDelimiters 93 } 94 m := &Manager{ 95 options: opts, 96 pattern: fmt.Sprintf( 97 `%s(.+?)%s`, 98 gregex.Quote(opts.Delimiters[0]), 99 gregex.Quote(opts.Delimiters[1]), 100 ), 101 pathType: pathType, 102 } 103 intlog.Printf(context.TODO(), `New: %#v`, m) 104 return m 105 } 106 107 // checkPathType checks and returns the path type for given directory path. 108 func (o *Options) checkPathType(dirPath string) pathType { 109 if dirPath == "" { 110 return pathTypeNone 111 } 112 113 if o.Resource == nil { 114 o.Resource = gres.Instance() 115 } 116 117 if o.Resource.Contains(dirPath) { 118 o.Path = dirPath 119 return pathTypeGres 120 } 121 122 realPath, _ := gfile.Search(dirPath) 123 if realPath != "" { 124 o.Path = realPath 125 return pathTypeNormal 126 } 127 128 return pathTypeNone 129 } 130 131 // SetPath sets the directory path storing i18n files. 132 func (m *Manager) SetPath(path string) error { 133 pathType := m.options.checkPathType(path) 134 if pathType == pathTypeNone { 135 return gerror.NewCodef(gcode.CodeInvalidParameter, `%s does not exist`, path) 136 } 137 138 m.pathType = pathType 139 intlog.Printf(context.TODO(), `SetPath[%s]: %s`, m.pathType, m.options.Path) 140 // Reset the manager after path changed. 141 m.reset() 142 return nil 143 } 144 145 // SetLanguage sets the language for translator. 146 func (m *Manager) SetLanguage(language string) { 147 m.options.Language = language 148 intlog.Printf(context.TODO(), `SetLanguage: %s`, m.options.Language) 149 } 150 151 // SetDelimiters sets the delimiters for translator. 152 func (m *Manager) SetDelimiters(left, right string) { 153 m.pattern = fmt.Sprintf(`%s(.+?)%s`, gregex.Quote(left), gregex.Quote(right)) 154 intlog.Printf(context.TODO(), `SetDelimiters: %v`, m.pattern) 155 } 156 157 // T is alias of Translate for convenience. 158 func (m *Manager) T(ctx context.Context, content string) string { 159 return m.Translate(ctx, content) 160 } 161 162 // Tf is alias of TranslateFormat for convenience. 163 func (m *Manager) Tf(ctx context.Context, format string, values ...interface{}) string { 164 return m.TranslateFormat(ctx, format, values...) 165 } 166 167 // TranslateFormat translates, formats and returns the `format` with configured language 168 // and given `values`. 169 func (m *Manager) TranslateFormat(ctx context.Context, format string, values ...interface{}) string { 170 return fmt.Sprintf(m.Translate(ctx, format), values...) 171 } 172 173 // Translate translates `content` with configured language. 174 func (m *Manager) Translate(ctx context.Context, content string) string { 175 m.init(ctx) 176 m.mu.RLock() 177 defer m.mu.RUnlock() 178 transLang := m.options.Language 179 if lang := LanguageFromCtx(ctx); lang != "" { 180 transLang = lang 181 } 182 data := m.data[transLang] 183 if data == nil { 184 return content 185 } 186 // Parse content as name. 187 if v, ok := data[content]; ok { 188 return v 189 } 190 // Parse content as variables container. 191 result, _ := gregex.ReplaceStringFuncMatch( 192 m.pattern, content, 193 func(match []string) string { 194 if v, ok := data[match[1]]; ok { 195 return v 196 } 197 // return match[1] will return the content between delimiters 198 // return match[0] will return the original content 199 return match[0] 200 }) 201 intlog.Printf(ctx, `Translate for language: %s`, transLang) 202 return result 203 } 204 205 // GetContent retrieves and returns the configured content for given key and specified language. 206 // It returns an empty string if not found. 207 func (m *Manager) GetContent(ctx context.Context, key string) string { 208 m.init(ctx) 209 m.mu.RLock() 210 defer m.mu.RUnlock() 211 transLang := m.options.Language 212 if lang := LanguageFromCtx(ctx); lang != "" { 213 transLang = lang 214 } 215 if data, ok := m.data[transLang]; ok { 216 return data[key] 217 } 218 return "" 219 } 220 221 // reset reset data of the manager. 222 func (m *Manager) reset() { 223 m.mu.Lock() 224 defer m.mu.Unlock() 225 m.data = nil 226 } 227 228 // init initializes the manager for lazy initialization design. 229 // The i18n manager is only initialized once. 230 func (m *Manager) init(ctx context.Context) { 231 m.mu.RLock() 232 // If the data is not nil, means it's already initialized. 233 if m.data != nil { 234 m.mu.RUnlock() 235 return 236 } 237 m.mu.RUnlock() 238 239 defer func() { 240 intlog.Printf(ctx, `Manager init finish: %#v`, m) 241 }() 242 243 intlog.Printf(ctx, `init path: %s`, m.options.Path) 244 245 m.mu.Lock() 246 defer m.mu.Unlock() 247 switch m.pathType { 248 case pathTypeGres: 249 files := m.options.Resource.ScanDirFile(m.options.Path, "*.*", true) 250 if len(files) > 0 { 251 var ( 252 path string 253 name string 254 lang string 255 array []string 256 ) 257 m.data = make(map[string]map[string]string) 258 for _, file := range files { 259 name = file.Name() 260 path = name[len(m.options.Path)+1:] 261 array = strings.Split(path, "/") 262 if len(array) > 1 { 263 lang = array[0] 264 } else if len(array) == 1 { 265 lang = gfile.Name(array[0]) 266 } 267 if m.data[lang] == nil { 268 m.data[lang] = make(map[string]string) 269 } 270 if j, err := gjson.LoadContent(file.Content()); err == nil { 271 for k, v := range j.Var().Map() { 272 m.data[lang][k] = gconv.String(v) 273 } 274 } else { 275 intlog.Errorf(ctx, "load i18n file '%s' failed: %+v", name, err) 276 } 277 } 278 } 279 case pathTypeNormal: 280 files, _ := gfile.ScanDirFile(m.options.Path, "*.*", true) 281 if len(files) == 0 { 282 return 283 } 284 var ( 285 path string 286 lang string 287 array []string 288 ) 289 m.data = make(map[string]map[string]string) 290 for _, file := range files { 291 path = file[len(m.options.Path)+1:] 292 array = strings.Split(path, gfile.Separator) 293 if len(array) > 1 { 294 lang = array[0] 295 } else if len(array) == 1 { 296 lang = gfile.Name(array[0]) 297 } 298 if m.data[lang] == nil { 299 m.data[lang] = make(map[string]string) 300 } 301 if j, err := gjson.LoadContent(gfile.GetBytes(file)); err == nil { 302 for k, v := range j.Var().Map() { 303 m.data[lang][k] = gconv.String(v) 304 } 305 } else { 306 intlog.Errorf(ctx, "load i18n file '%s' failed: %+v", file, err) 307 } 308 } 309 intlog.Printf(ctx, "i18n files loaded in path: %s", m.options.Path) 310 // Monitor changes of i18n files for hot reload feature. 311 _, _ = gfsnotify.Add(m.options.Path, func(event *gfsnotify.Event) { 312 intlog.Printf(ctx, `i18n file changed: %s`, event.Path) 313 // Any changes of i18n files, clear the data. 314 m.reset() 315 gfsnotify.Exit() 316 }) 317 } 318 }