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