gi18n_manager.go 7.6 KB

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