123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
- //
- // This Source Code Form is subject to the terms of the MIT License.
- // If a copy of the MIT was not distributed with this file,
- // You can obtain one at https://github.com/gogf/gf.
- package gi18n
- import (
- "context"
- "fmt"
- "github.com/gogf/gf/errors/gcode"
- "github.com/gogf/gf/errors/gerror"
- "github.com/gogf/gf/internal/intlog"
- "strings"
- "sync"
- "github.com/gogf/gf/os/gfsnotify"
- "github.com/gogf/gf/text/gregex"
- "github.com/gogf/gf/util/gconv"
- "github.com/gogf/gf/encoding/gjson"
- "github.com/gogf/gf/os/gfile"
- "github.com/gogf/gf/os/gres"
- )
- // Manager for i18n contents, it is concurrent safe, supporting hot reload.
- type Manager struct {
- mu sync.RWMutex
- data map[string]map[string]string // Translating map.
- pattern string // Pattern for regex parsing.
- options Options // configuration options.
- }
- // Options is used for i18n object configuration.
- type Options struct {
- Path string // I18n files storage path.
- Language string // Default local language.
- Delimiters []string // Delimiters for variable parsing.
- }
- var (
- defaultLanguage = "en" // defaultDelimiters defines the default language if user does not specified in options.
- defaultDelimiters = []string{"{#", "}"} // defaultDelimiters defines the default key variable delimiters.
- )
- // New creates and returns a new i18n manager.
- // The optional parameter <option> specifies the custom options for i18n manager.
- // It uses a default one if it's not passed.
- func New(options ...Options) *Manager {
- var opts Options
- if len(options) > 0 {
- opts = options[0]
- } else {
- opts = DefaultOptions()
- }
- if len(opts.Language) == 0 {
- opts.Language = defaultLanguage
- }
- if len(opts.Delimiters) == 0 {
- opts.Delimiters = defaultDelimiters
- }
- m := &Manager{
- options: opts,
- pattern: fmt.Sprintf(
- `%s(\w+)%s`,
- gregex.Quote(opts.Delimiters[0]),
- gregex.Quote(opts.Delimiters[1]),
- ),
- }
- intlog.Printf(context.TODO(), `New: %#v`, m)
- return m
- }
- // DefaultOptions creates and returns a default options for i18n manager.
- func DefaultOptions() Options {
- var (
- path = "i18n"
- realPath, _ = gfile.Search(path)
- )
- if realPath != "" {
- path = realPath
- // To avoid of the source path of GF: github.com/gogf/i18n/gi18n
- if gfile.Exists(path + gfile.Separator + "gi18n") {
- path = ""
- }
- }
- return Options{
- Path: path,
- Language: "en",
- Delimiters: defaultDelimiters,
- }
- }
- // SetPath sets the directory path storing i18n files.
- func (m *Manager) SetPath(path string) error {
- if gres.Contains(path) {
- m.options.Path = path
- } else {
- realPath, _ := gfile.Search(path)
- if realPath == "" {
- return gerror.NewCodef(gcode.CodeInvalidParameter, `%s does not exist`, path)
- }
- m.options.Path = realPath
- }
- intlog.Printf(context.TODO(), `SetPath: %s`, m.options.Path)
- return nil
- }
- // SetLanguage sets the language for translator.
- func (m *Manager) SetLanguage(language string) {
- m.options.Language = language
- intlog.Printf(context.TODO(), `SetLanguage: %s`, m.options.Language)
- }
- // SetDelimiters sets the delimiters for translator.
- func (m *Manager) SetDelimiters(left, right string) {
- m.pattern = fmt.Sprintf(`%s(\w+)%s`, gregex.Quote(left), gregex.Quote(right))
- intlog.Printf(context.TODO(), `SetDelimiters: %v`, m.pattern)
- }
- // T is alias of Translate for convenience.
- func (m *Manager) T(ctx context.Context, content string) string {
- return m.Translate(ctx, content)
- }
- // Tf is alias of TranslateFormat for convenience.
- func (m *Manager) Tf(ctx context.Context, format string, values ...interface{}) string {
- return m.TranslateFormat(ctx, format, values...)
- }
- // TranslateFormat translates, formats and returns the <format> with configured language
- // and given <values>.
- func (m *Manager) TranslateFormat(ctx context.Context, format string, values ...interface{}) string {
- return fmt.Sprintf(m.Translate(ctx, format), values...)
- }
- // Translate translates <content> with configured language.
- func (m *Manager) Translate(ctx context.Context, content string) string {
- m.init(ctx)
- m.mu.RLock()
- defer m.mu.RUnlock()
- transLang := m.options.Language
- if lang := LanguageFromCtx(ctx); lang != "" {
- transLang = lang
- }
- data := m.data[transLang]
- if data == nil {
- return content
- }
- // Parse content as name.
- if v, ok := data[content]; ok {
- return v
- }
- // Parse content as variables container.
- result, _ := gregex.ReplaceStringFuncMatch(
- m.pattern, content,
- func(match []string) string {
- if v, ok := data[match[1]]; ok {
- return v
- }
- return match[0]
- })
- intlog.Printf(ctx, `Translate for language: %s`, transLang)
- return result
- }
- // GetContent retrieves and returns the configured content for given key and specified language.
- // It returns an empty string if not found.
- func (m *Manager) GetContent(ctx context.Context, key string) string {
- m.init(ctx)
- m.mu.RLock()
- defer m.mu.RUnlock()
- transLang := m.options.Language
- if lang := LanguageFromCtx(ctx); lang != "" {
- transLang = lang
- }
- if data, ok := m.data[transLang]; ok {
- return data[key]
- }
- return ""
- }
- // init initializes the manager for lazy initialization design.
- // The i18n manager is only initialized once.
- func (m *Manager) init(ctx context.Context) {
- m.mu.RLock()
- // If the data is not nil, means it's already initialized.
- if m.data != nil {
- m.mu.RUnlock()
- return
- }
- m.mu.RUnlock()
- m.mu.Lock()
- defer m.mu.Unlock()
- if gres.Contains(m.options.Path) {
- files := gres.ScanDirFile(m.options.Path, "*.*", true)
- if len(files) > 0 {
- var (
- path string
- name string
- lang string
- array []string
- )
- m.data = make(map[string]map[string]string)
- for _, file := range files {
- name = file.Name()
- path = name[len(m.options.Path)+1:]
- array = strings.Split(path, "/")
- if len(array) > 1 {
- lang = array[0]
- } else {
- lang = gfile.Name(array[0])
- }
- if m.data[lang] == nil {
- m.data[lang] = make(map[string]string)
- }
- if j, err := gjson.LoadContent(file.Content()); err == nil {
- for k, v := range j.Map() {
- m.data[lang][k] = gconv.String(v)
- }
- } else {
- intlog.Errorf(ctx, "load i18n file '%s' failed: %v", name, err)
- }
- }
- }
- } else if m.options.Path != "" {
- files, _ := gfile.ScanDirFile(m.options.Path, "*.*", true)
- if len(files) == 0 {
- return
- }
- var (
- path string
- lang string
- array []string
- )
- m.data = make(map[string]map[string]string)
- for _, file := range files {
- path = file[len(m.options.Path)+1:]
- array = strings.Split(path, gfile.Separator)
- if len(array) > 1 {
- lang = array[0]
- } else {
- lang = gfile.Name(array[0])
- }
- if m.data[lang] == nil {
- m.data[lang] = make(map[string]string)
- }
- if j, err := gjson.LoadContent(gfile.GetBytes(file)); err == nil {
- for k, v := range j.Map() {
- m.data[lang][k] = gconv.String(v)
- }
- } else {
- intlog.Errorf(ctx, "load i18n file '%s' failed: %v", file, err)
- }
- }
- // Monitor changes of i18n files for hot reload feature.
- _, _ = gfsnotify.Add(path, func(event *gfsnotify.Event) {
- // Any changes of i18n files, clear the data.
- m.mu.Lock()
- m.data = nil
- m.mu.Unlock()
- gfsnotify.Exit()
- })
- }
- }
|