gi18n_manager.go 7.3 KB

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