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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}