gi18n_manager.go 8.5 KB

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