gi18n_manager.go 7.1 KB

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