github.com/gogf/gf@v1.16.9/i18n/gi18n/gi18n_manager.go (about)

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