|
|
package i18n
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"encoding/json"
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
"git.noahlan.cn/noahlan/ntool/nlog"
|
|
|
"github.com/BurntSushi/toml"
|
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
|
"github.com/radovskyb/watcher"
|
|
|
"golang.org/x/text/language"
|
|
|
"gopkg.in/yaml.v3"
|
|
|
"os"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
// Translator is a struct storing translating data.
|
|
|
type Translator struct {
|
|
|
BundleConfig
|
|
|
|
|
|
bundle *i18n.Bundle
|
|
|
localizerMap map[string]*i18n.Localizer
|
|
|
supportedFormat []string
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
}
|
|
|
|
|
|
type Option func(*Translator)
|
|
|
|
|
|
func NewTranslator(opts ...Option) *Translator {
|
|
|
ret := &Translator{
|
|
|
localizerMap: make(map[string]*i18n.Localizer),
|
|
|
BundleConfig: defaultBundleConfig,
|
|
|
}
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
opt(ret)
|
|
|
}
|
|
|
|
|
|
ret.bundle = i18n.NewBundle(ret.DefaultLanguage)
|
|
|
ret.RegisterUnmarshalFunc("json", json.Unmarshal)
|
|
|
ret.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
|
|
ret.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
|
|
|
|
|
// init and watcher
|
|
|
files := ret.watcher()
|
|
|
lngs := make([]string, len(files))
|
|
|
for i, file := range files {
|
|
|
lngs[i] = filenameWithoutExt(file.Name)
|
|
|
if err := ret.loadFileByPath(file.Path); err != nil {
|
|
|
nlog.Debugf("load locale file err: %v\n", err)
|
|
|
}
|
|
|
}
|
|
|
ret.configLocalizer(lngs...)
|
|
|
|
|
|
return ret
|
|
|
}
|
|
|
|
|
|
func (l *Translator) RegisterUnmarshalFunc(format string, fn i18n.UnmarshalFunc) {
|
|
|
if l.supportedFormat == nil {
|
|
|
l.supportedFormat = make([]string, 0)
|
|
|
}
|
|
|
l.supportedFormat = append(l.supportedFormat, format)
|
|
|
l.bundle.RegisterUnmarshalFunc(format, fn)
|
|
|
}
|
|
|
|
|
|
// Trans translates the message of message's ID
|
|
|
func (l *Translator) Trans(ctx context.Context, messageID string) string {
|
|
|
s, err := l.TransAny(ctx, messageID)
|
|
|
if err != nil || s == "" {
|
|
|
return messageID
|
|
|
}
|
|
|
|
|
|
return s
|
|
|
}
|
|
|
|
|
|
// TransAny translates the message of any param
|
|
|
func (l *Translator) TransAny(ctx context.Context, param any) (string, error) {
|
|
|
lngTag := l.TagGetter(ctx)
|
|
|
if lngTag == "" {
|
|
|
lngTag = l.DefaultLanguage.String()
|
|
|
}
|
|
|
localizer := l.getLocalizerByTag(lngTag)
|
|
|
|
|
|
config, err := l.getLocalizeConfig(param)
|
|
|
if err != nil {
|
|
|
return "", err
|
|
|
}
|
|
|
|
|
|
message, err := localizer.Localize(config)
|
|
|
if err != nil {
|
|
|
return "", err
|
|
|
}
|
|
|
|
|
|
return message, nil
|
|
|
}
|
|
|
|
|
|
func (l *Translator) loadFileByPath(path string) error {
|
|
|
buf, err := l.FileLoader.LoadMessage(path)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
if _, err = l.bundle.ParseMessageFileBytes(buf, path); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// watcher 监控给定Path的文件数据变化,实时更改localizer,以实现语言文件热更新
|
|
|
func (l *Translator) watcher() []struct {
|
|
|
Path string
|
|
|
Name string
|
|
|
} {
|
|
|
w := watcher.New()
|
|
|
w.FilterOps(watcher.Create, watcher.Remove, watcher.Write, watcher.Rename)
|
|
|
w.AddFilterHook(func(info os.FileInfo, fullPath string) error {
|
|
|
if info.IsDir() {
|
|
|
return watcher.ErrSkip
|
|
|
}
|
|
|
if info.Size() <= 0 {
|
|
|
return watcher.ErrSkip
|
|
|
}
|
|
|
name := info.Name()
|
|
|
|
|
|
for _, format := range l.supportedFormat {
|
|
|
if strings.HasSuffix(name, format) {
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 文件后缀不支持,直接跳过
|
|
|
return watcher.ErrSkip
|
|
|
})
|
|
|
|
|
|
go func() {
|
|
|
for {
|
|
|
select {
|
|
|
case event := <-w.Event:
|
|
|
if event.IsDir() {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
filePrefix := filenameWithoutExt(event.Name())
|
|
|
lang, err := language.Parse(filePrefix)
|
|
|
if err != nil {
|
|
|
nlog.Debugf("cannot parse filename:%s to language.Tag, process will continue.", filePrefix)
|
|
|
continue
|
|
|
}
|
|
|
path := event.Path
|
|
|
switch event.Op {
|
|
|
case watcher.Remove:
|
|
|
l.removeLocalizer(lang.String())
|
|
|
case watcher.Create, watcher.Write:
|
|
|
if err := l.loadFileByPath(path); err != nil {
|
|
|
nlog.Debugf("load locale file err [Create] or [Write], %+v", err)
|
|
|
continue
|
|
|
}
|
|
|
l.configLocalizer(lang.String())
|
|
|
case watcher.Rename:
|
|
|
l.removeLocalizer(lang.String())
|
|
|
|
|
|
newLangStr := filenameWithoutExt(path)
|
|
|
newLang, err := language.Parse(newLangStr)
|
|
|
if err != nil {
|
|
|
continue
|
|
|
}
|
|
|
if err := l.loadFileByPath(path); err != nil {
|
|
|
nlog.Debugf("load locale file err [Rename], %+v", err)
|
|
|
continue
|
|
|
}
|
|
|
l.configLocalizer(newLang.String())
|
|
|
}
|
|
|
case err := <-w.Error:
|
|
|
nlog.Errorf("watch locale file err: %+v", err)
|
|
|
case <-w.Closed:
|
|
|
nlog.Debugf("i18n watcher closed...")
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
if err := w.Add(l.RootPath); err != nil {
|
|
|
nlog.Errorf("[i18n] watching %s err:%v", l.RootPath, err)
|
|
|
w.Closed <- struct{}{}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
go func() {
|
|
|
_ = w.Start(time.Second * 5)
|
|
|
}()
|
|
|
|
|
|
watchedFiles := w.WatchedFiles()
|
|
|
|
|
|
ret := make([]struct {
|
|
|
Path string
|
|
|
Name string
|
|
|
}, 0, len(watchedFiles))
|
|
|
|
|
|
for path, info := range watchedFiles {
|
|
|
if info.IsDir() {
|
|
|
continue
|
|
|
}
|
|
|
if info.Size() <= 0 {
|
|
|
continue
|
|
|
}
|
|
|
name := info.Name()
|
|
|
|
|
|
for _, format := range l.supportedFormat {
|
|
|
if strings.HasSuffix(name, format) {
|
|
|
ret = append(ret, struct {
|
|
|
Path string
|
|
|
Name string
|
|
|
}{Path: path, Name: info.Name()})
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return ret
|
|
|
}
|
|
|
|
|
|
// getLocalizerByTag get localizer by language
|
|
|
func (l *Translator) getLocalizerByTag(tag string) *i18n.Localizer {
|
|
|
l.mu.RLock()
|
|
|
defer l.mu.RUnlock()
|
|
|
|
|
|
if localizer, ok := l.localizerMap[tag]; ok {
|
|
|
return localizer
|
|
|
}
|
|
|
return l.localizerMap[l.DefaultLanguage.String()]
|
|
|
}
|
|
|
|
|
|
// getLocalizerByTag get localizer by language
|
|
|
func (l *Translator) getLocalizeConfig(param any) (*i18n.LocalizeConfig, error) {
|
|
|
switch t := param.(type) {
|
|
|
case string:
|
|
|
return &i18n.LocalizeConfig{
|
|
|
DefaultMessage: &i18n.Message{ID: t},
|
|
|
}, nil
|
|
|
case *MessageWithParameter:
|
|
|
// 转化 template data
|
|
|
return &i18n.LocalizeConfig{
|
|
|
TemplateData: l.buildTemplateData(t.Params, t.ParamsWithName),
|
|
|
DefaultMessage: &i18n.Message{ID: t.MessageID},
|
|
|
}, nil
|
|
|
case *i18n.LocalizeConfig:
|
|
|
return t, nil
|
|
|
}
|
|
|
|
|
|
// extends localizerConfig getter
|
|
|
if ret := l.LocalizeConfigGetter(l.BundleConfig, param); ret != nil {
|
|
|
return ret, nil
|
|
|
}
|
|
|
|
|
|
return nil, errors.New(fmt.Sprintf("un supported localize param: %v", param))
|
|
|
}
|
|
|
|
|
|
// buildTemplateData 构建TemplateData
|
|
|
func (l *Translator) buildTemplateData(params []any, paramsWithName map[string]any) map[string]any {
|
|
|
if len(params) == 0 {
|
|
|
return paramsWithName
|
|
|
}
|
|
|
data := make(map[string]any)
|
|
|
for i, param := range params {
|
|
|
data[fmt.Sprintf("%s%d", l.SortedParameterPrefix, i)] = param
|
|
|
}
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// configLocalizer set localizer by language
|
|
|
func (l *Translator) configLocalizer(acceptLanguage ...string) {
|
|
|
l.mu.Lock()
|
|
|
defer l.mu.Unlock()
|
|
|
|
|
|
for _, lng := range acceptLanguage {
|
|
|
if lng == "" {
|
|
|
continue
|
|
|
}
|
|
|
if _, ok := l.localizerMap[lng]; !ok {
|
|
|
l.localizerMap[lng] = l.newLocalizer(lng)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// configure default
|
|
|
defaultLng := l.DefaultLanguage.String()
|
|
|
if _, ok := l.localizerMap[defaultLng]; !ok {
|
|
|
l.localizerMap[defaultLng] = l.newLocalizer(defaultLng)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// removeLocalizer remove localizer by tag string
|
|
|
func (l *Translator) removeLocalizer(lang ...string) {
|
|
|
l.mu.Lock()
|
|
|
defer l.mu.Unlock()
|
|
|
|
|
|
for _, lng := range lang {
|
|
|
delete(l.localizerMap, lng)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// newLocalizer create a localizer by language
|
|
|
func (l *Translator) newLocalizer(lng string) *i18n.Localizer {
|
|
|
lngDefault := l.DefaultLanguage.String()
|
|
|
lngs := []string{lng}
|
|
|
|
|
|
if lng != lngDefault {
|
|
|
lngs = append(lngs, lngDefault)
|
|
|
}
|
|
|
|
|
|
localizer := i18n.NewLocalizer(
|
|
|
l.bundle,
|
|
|
lngs...,
|
|
|
)
|
|
|
return localizer
|
|
|
}
|