You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ntool-biz/core/i18n/translator.go

317 lines
7.1 KiB
Go

1 year ago
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
}