2019-08-31 18:04:12 +08:00
|
|
|
// Copyright 2019 gf Author(https://github.com/gogf/gf). 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 (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-02-28 23:00:05 +08:00
|
|
|
"github.com/gogf/gf/internal/intlog"
|
2019-08-31 18:04:12 +08:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
2019-09-06 15:43:57 +08:00
|
|
|
"github.com/gogf/gf/os/glog"
|
|
|
|
|
2019-08-31 18:04:12 +08:00
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2019-09-01 00:30:01 +08:00
|
|
|
// Manager, it is concurrent safe, supporting hot reload.
|
|
|
|
type Manager struct {
|
2019-08-31 18:04:12 +08:00
|
|
|
mu sync.RWMutex
|
|
|
|
data map[string]map[string]string // Translating map.
|
|
|
|
pattern string // Pattern for regex parsing.
|
|
|
|
options Options // configuration options.
|
|
|
|
}
|
|
|
|
|
2020-02-28 23:00:05 +08:00
|
|
|
// Options is used for i18n object configuration.
|
2019-08-31 18:04:12 +08:00
|
|
|
type Options struct {
|
|
|
|
Path string // I18n files storage path.
|
|
|
|
Language string // Local language.
|
|
|
|
Delimiters []string // Delimiters for variable parsing.
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2020-05-10 10:56:11 +08:00
|
|
|
// defaultDelimiters defines the default key variable delimiters.
|
2019-08-31 18:04:12 +08:00
|
|
|
defaultDelimiters = []string{"{#", "}"}
|
|
|
|
)
|
|
|
|
|
2019-09-09 22:56:54 +08:00
|
|
|
// New creates and returns a new i18n manager.
|
2020-05-10 10:56:11 +08:00
|
|
|
// The optional parameter <option> specifies the custom options for i18n manager.
|
|
|
|
// It uses a default one if it's not passed.
|
2019-09-01 00:30:01 +08:00
|
|
|
func New(options ...Options) *Manager {
|
2019-08-31 18:04:12 +08:00
|
|
|
var opts Options
|
|
|
|
if len(options) > 0 {
|
|
|
|
opts = options[0]
|
|
|
|
} else {
|
|
|
|
opts = DefaultOptions()
|
|
|
|
}
|
|
|
|
if len(opts.Delimiters) == 0 {
|
|
|
|
opts.Delimiters = defaultDelimiters
|
|
|
|
}
|
2020-02-28 23:00:05 +08:00
|
|
|
m := &Manager{
|
2019-08-31 18:04:12 +08:00
|
|
|
options: opts,
|
|
|
|
pattern: fmt.Sprintf(
|
|
|
|
`%s(\w+)%s`,
|
|
|
|
gregex.Quote(opts.Delimiters[0]),
|
|
|
|
gregex.Quote(opts.Delimiters[1]),
|
|
|
|
),
|
|
|
|
}
|
2020-06-06 15:31:04 +08:00
|
|
|
intlog.Printf(`New: %#v`, m)
|
2020-02-28 23:00:05 +08:00
|
|
|
return m
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
|
2020-05-10 10:56:11 +08:00
|
|
|
// DefaultOptions creates and returns a default options for i18n manager.
|
2019-08-31 18:04:12 +08:00
|
|
|
func DefaultOptions() Options {
|
2020-05-10 10:56:11 +08:00
|
|
|
var (
|
|
|
|
path = "i18n"
|
|
|
|
realPath, _ = gfile.Search(path)
|
|
|
|
)
|
2019-08-31 18:04:12 +08:00
|
|
|
if realPath != "" {
|
|
|
|
path = realPath
|
2020-05-10 10:56:11 +08:00
|
|
|
// To avoid of the source path of GF: github.com/gogf/i18n/gi18n
|
2019-09-09 22:56:54 +08:00
|
|
|
if gfile.Exists(path + gfile.Separator + "gi18n") {
|
|
|
|
path = ""
|
|
|
|
}
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
return Options{
|
|
|
|
Path: path,
|
2020-05-10 10:56:11 +08:00
|
|
|
Language: "en",
|
|
|
|
Delimiters: defaultDelimiters,
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPath sets the directory path storing i18n files.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) SetPath(path string) error {
|
2019-08-31 18:04:12 +08:00
|
|
|
if gres.Contains(path) {
|
2019-09-01 00:30:01 +08:00
|
|
|
m.options.Path = path
|
2019-08-31 18:04:12 +08:00
|
|
|
} else {
|
|
|
|
realPath, _ := gfile.Search(path)
|
|
|
|
if realPath == "" {
|
|
|
|
return errors.New(fmt.Sprintf(`%s does not exist`, path))
|
|
|
|
}
|
2019-09-01 00:30:01 +08:00
|
|
|
m.options.Path = realPath
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
2020-02-28 23:00:05 +08:00
|
|
|
intlog.Printf(`SetPath: %s`, m.options.Path)
|
2019-08-31 18:04:12 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetLanguage sets the language for translator.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) SetLanguage(language string) {
|
|
|
|
m.options.Language = language
|
2020-02-28 23:00:05 +08:00
|
|
|
intlog.Printf(`SetLanguage: %s`, m.options.Language)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetDelimiters sets the delimiters for translator.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) SetDelimiters(left, right string) {
|
|
|
|
m.pattern = fmt.Sprintf(`%s(\w+)%s`, gregex.Quote(left), gregex.Quote(right))
|
2020-02-28 23:00:05 +08:00
|
|
|
intlog.Printf(`SetDelimiters: %v`, m.pattern)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
|
2020-05-10 10:56:11 +08:00
|
|
|
// T is alias of Translate for convenience.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) T(content string, language ...string) string {
|
|
|
|
return m.Translate(content, language...)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
|
2020-08-08 16:46:52 +08:00
|
|
|
// TF is alias of TranslateFormat for convenience.
|
|
|
|
func (m *Manager) TF(format string, values ...interface{}) string {
|
|
|
|
return m.TranslateFormat(format, values...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TFL is alias of TranslateFormatLang for convenience.
|
|
|
|
func (m *Manager) TFL(format string, language string, values ...interface{}) string {
|
|
|
|
return m.TranslateFormatLang(format, language, values...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TranslateFormat translates, formats and returns the <format> with configured language
|
|
|
|
// and given <values>.
|
|
|
|
func (m *Manager) TranslateFormat(format string, values ...interface{}) string {
|
|
|
|
return fmt.Sprintf(m.Translate(format), values...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TranslateFormatLang translates, formats and returns the <format> with configured language
|
|
|
|
// and given <values>. The parameter <language> specifies custom translation language ignoring
|
|
|
|
// configured language. If <language> is given empty string, it uses the default configured
|
|
|
|
// language for the translation.
|
|
|
|
func (m *Manager) TranslateFormatLang(format string, language string, values ...interface{}) string {
|
|
|
|
return fmt.Sprintf(m.Translate(format, language), values...)
|
|
|
|
}
|
|
|
|
|
2019-08-31 18:04:12 +08:00
|
|
|
// Translate translates <content> with configured language.
|
|
|
|
// The parameter <language> specifies custom translation language ignoring configured language.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) Translate(content string, language ...string) string {
|
|
|
|
m.init()
|
|
|
|
m.mu.RLock()
|
|
|
|
defer m.mu.RUnlock()
|
2020-02-28 23:00:05 +08:00
|
|
|
transLang := m.options.Language
|
2019-10-17 20:31:03 +08:00
|
|
|
if len(language) > 0 && language[0] != "" {
|
2020-02-28 23:00:05 +08:00
|
|
|
transLang = language[0]
|
2019-08-31 18:04:12 +08:00
|
|
|
} else {
|
2020-02-28 23:00:05 +08:00
|
|
|
transLang = m.options.Language
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
2020-02-28 23:00:05 +08:00
|
|
|
data := m.data[transLang]
|
2019-08-31 18:04:12 +08:00
|
|
|
if data == nil {
|
|
|
|
return content
|
|
|
|
}
|
|
|
|
// Parse content as name.
|
|
|
|
if v, ok := data[content]; ok {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
// Parse content as variables container.
|
2020-05-10 10:56:11 +08:00
|
|
|
result, _ := gregex.ReplaceStringFuncMatch(
|
|
|
|
m.pattern, content,
|
|
|
|
func(match []string) string {
|
|
|
|
if v, ok := data[match[1]]; ok {
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
return match[0]
|
|
|
|
})
|
2020-02-28 23:00:05 +08:00
|
|
|
intlog.Printf(`Translate for language: %s`, transLang)
|
2019-08-31 18:04:12 +08:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-05-10 10:56:11 +08:00
|
|
|
// GetValue retrieves and returns the configured content for given key and specified language.
|
|
|
|
// It returns an empty string if not found.
|
|
|
|
func (m *Manager) GetContent(key string, language ...string) string {
|
|
|
|
m.init()
|
|
|
|
m.mu.RLock()
|
|
|
|
defer m.mu.RUnlock()
|
|
|
|
transLang := m.options.Language
|
|
|
|
if len(language) > 0 && language[0] != "" {
|
|
|
|
transLang = language[0]
|
|
|
|
} else {
|
|
|
|
transLang = m.options.Language
|
|
|
|
}
|
|
|
|
if data, ok := m.data[transLang]; ok {
|
|
|
|
return data[key]
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2020-02-28 23:00:05 +08:00
|
|
|
// init initializes the manager for lazy initialization design.
|
|
|
|
// The i18n manager is only initialized once.
|
2019-09-01 00:30:01 +08:00
|
|
|
func (m *Manager) init() {
|
|
|
|
m.mu.RLock()
|
2020-05-10 10:56:11 +08:00
|
|
|
// If the data is not nil, means it's already initialized.
|
2019-09-01 00:30:01 +08:00
|
|
|
if m.data != nil {
|
|
|
|
m.mu.RUnlock()
|
2019-08-31 18:04:12 +08:00
|
|
|
return
|
|
|
|
}
|
2019-09-01 00:30:01 +08:00
|
|
|
m.mu.RUnlock()
|
2019-08-31 18:04:12 +08:00
|
|
|
|
2019-09-01 00:30:01 +08:00
|
|
|
m.mu.Lock()
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
if gres.Contains(m.options.Path) {
|
|
|
|
files := gres.ScanDirFile(m.options.Path, "*.*", true)
|
2019-08-31 18:04:12 +08:00
|
|
|
if len(files) > 0 {
|
2020-05-10 10:56:11 +08:00
|
|
|
var (
|
|
|
|
path string
|
|
|
|
name string
|
|
|
|
lang string
|
|
|
|
array []string
|
|
|
|
)
|
2019-09-01 00:30:01 +08:00
|
|
|
m.data = make(map[string]map[string]string)
|
2019-08-31 18:04:12 +08:00
|
|
|
for _, file := range files {
|
|
|
|
name = file.Name()
|
2019-09-01 00:30:01 +08:00
|
|
|
path = name[len(m.options.Path)+1:]
|
2019-08-31 18:04:12 +08:00
|
|
|
array = strings.Split(path, "/")
|
|
|
|
if len(array) > 1 {
|
|
|
|
lang = array[0]
|
|
|
|
} else {
|
|
|
|
lang = gfile.Name(array[0])
|
|
|
|
}
|
2019-09-01 00:30:01 +08:00
|
|
|
if m.data[lang] == nil {
|
|
|
|
m.data[lang] = make(map[string]string)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
2019-09-06 15:43:57 +08:00
|
|
|
if j, err := gjson.LoadContent(file.Content()); err == nil {
|
2019-08-31 18:04:12 +08:00
|
|
|
for k, v := range j.ToMap() {
|
2019-09-01 00:30:01 +08:00
|
|
|
m.data[lang][k] = gconv.String(v)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
2019-09-06 15:43:57 +08:00
|
|
|
} else {
|
2019-09-06 16:59:38 +08:00
|
|
|
glog.Errorf("load i18n file '%s' failed: %v", name, err)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-09-15 13:47:44 +08:00
|
|
|
} else if m.options.Path != "" {
|
2019-09-01 00:30:01 +08:00
|
|
|
files, _ := gfile.ScanDirFile(m.options.Path, "*.*", true)
|
2020-05-10 10:56:11 +08:00
|
|
|
if len(files) == 0 {
|
2020-07-25 14:09:03 +08:00
|
|
|
//intlog.Printf(
|
|
|
|
// "no i18n files found in configured directory: %s",
|
|
|
|
// m.options.Path,
|
|
|
|
//)
|
2020-05-10 10:56:11 +08:00
|
|
|
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.ToMap() {
|
|
|
|
m.data[lang][k] = gconv.String(v)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
2020-05-10 10:56:11 +08:00
|
|
|
} else {
|
|
|
|
glog.Errorf("load i18n file '%s' failed: %v", file, err)
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
}
|
2020-05-10 10:56:11 +08:00
|
|
|
// 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()
|
|
|
|
})
|
2019-08-31 18:04:12 +08:00
|
|
|
}
|
|
|
|
}
|