github.com/erda-project/erda-infra@v1.0.9/providers/i18n/provider.go (about)

     1  // Copyright (c) 2021 Terminus, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package i18n
    16  
    17  import (
    18  	"bytes"
    19  	"embed"
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"reflect"
    24  	"regexp"
    25  	"strings"
    26  
    27  	"github.com/recallsong/go-utils/reflectx"
    28  
    29  	"github.com/erda-project/erda-infra/base/logs"
    30  	"github.com/erda-project/erda-infra/base/servicehub"
    31  	cfg "github.com/erda-project/erda-infra/pkg/config"
    32  )
    33  
    34  // Internationalizable .
    35  type Internationalizable interface {
    36  	Translate(t Translator, langs LanguageCodes) string
    37  }
    38  
    39  // Translator .
    40  type Translator interface {
    41  	Get(lang LanguageCodes, key, def string) string
    42  	Text(lang LanguageCodes, key string) string
    43  	Sprintf(lang LanguageCodes, key string, args ...interface{}) string
    44  }
    45  
    46  // I18n .
    47  type I18n interface {
    48  	Get(namespace string, lang LanguageCodes, key, def string) string
    49  	Text(namespace string, lang LanguageCodes, key string) string
    50  	Sprintf(namespace string, lang LanguageCodes, key string, args ...interface{}) string
    51  	Translator(namespace string) Translator
    52  	RegisterFilesFromFS(fsPrefix string, rootFS embed.FS) error
    53  }
    54  
    55  var (
    56  	i18nType       = reflect.TypeOf((*I18n)(nil)).Elem()
    57  	translatorType = reflect.TypeOf((*Translator)(nil)).Elem()
    58  )
    59  
    60  // NopTranslator .
    61  type NopTranslator struct{}
    62  
    63  // Get .
    64  func (t *NopTranslator) Get(lang LanguageCodes, key, def string) string { return def }
    65  
    66  // Text .
    67  func (t *NopTranslator) Text(lang LanguageCodes, key string) string { return key }
    68  
    69  // Sprintf .
    70  func (t *NopTranslator) Sprintf(lang LanguageCodes, key string, args ...interface{}) string {
    71  	return fmt.Sprintf(key, args...)
    72  }
    73  
    74  type config struct {
    75  	Files  []string `file:"files"`
    76  	Common []string `file:"common"`
    77  }
    78  
    79  type provider struct {
    80  	Cfg    *config
    81  	Log    logs.Logger
    82  	common map[string]map[string]string
    83  	dic    map[string]map[string]map[string]string
    84  }
    85  
    86  func (p *provider) Init(ctx servicehub.Context) error {
    87  	for _, file := range p.Cfg.Common {
    88  		f, err := os.Stat(file)
    89  		if err != nil {
    90  			return fmt.Errorf("fail to load i18n file: %s", err)
    91  		}
    92  		if f.IsDir() {
    93  			err := filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
    94  				if skipFile(path, info, err) {
    95  					return nil
    96  				}
    97  				return p.loadToDic(file, p.common)
    98  			})
    99  			if err != nil {
   100  				return err
   101  			}
   102  			continue
   103  		}
   104  		err = p.loadToDic(file, p.common)
   105  		if err != nil {
   106  			return err
   107  		}
   108  	}
   109  	for _, file := range p.Cfg.Files {
   110  		f, err := os.Stat(file)
   111  		if err != nil {
   112  			return fmt.Errorf("fail to load i18n file: %s", err)
   113  		}
   114  		if f.IsDir() {
   115  			err := filepath.Walk(file, func(path string, info os.FileInfo, err error) error {
   116  				if skipFile(path, info, err) {
   117  					return nil
   118  				}
   119  				return p.loadI18nFile(path)
   120  			})
   121  			if err != nil {
   122  				return err
   123  			}
   124  			continue
   125  		}
   126  		err = p.loadI18nFile(file)
   127  		if err != nil {
   128  			return err
   129  		}
   130  	}
   131  	p.Log.Infof("load i18n files: %v, %v", p.Cfg.Common, p.Cfg.Files)
   132  	return nil
   133  }
   134  
   135  func skipFile(path string, info os.FileInfo, err error) bool {
   136  	if err != nil || info == nil || info.IsDir() {
   137  		return true
   138  	}
   139  	if strings.HasPrefix(filepath.Base(path), ".") {
   140  		return true
   141  	}
   142  	return false
   143  }
   144  
   145  func (p *provider) loadI18nFile(file string) error {
   146  	base := filepath.Base(file)
   147  	name := base[0 : len(base)-len(filepath.Ext(base))]
   148  	dic := p.dic[name]
   149  	if dic == nil {
   150  		dic = make(map[string]map[string]string)
   151  		p.dic[name] = dic
   152  	}
   153  	err := p.loadToDic(file, dic)
   154  	if err != nil {
   155  		return err
   156  	}
   157  	return nil
   158  }
   159  
   160  func (p *provider) loadToDic(file string, dic map[string]map[string]string) error {
   161  	m := make(map[string]interface{})
   162  	err := cfg.LoadToMap(file, m)
   163  	if err != nil {
   164  		return fmt.Errorf("fail to load i18n file: %s", err)
   165  	}
   166  	for lang, v := range m {
   167  		text := dic[lang]
   168  		if text == nil {
   169  			text = make(map[string]string)
   170  			dic[lang] = text
   171  		}
   172  		switch m := v.(type) {
   173  		case map[string]string:
   174  			for k, v := range m {
   175  				text[strings.ToLower(k)] = fmt.Sprint(v)
   176  			}
   177  		case map[string]interface{}:
   178  			for k, v := range m {
   179  				text[strings.ToLower(k)] = fmt.Sprint(v)
   180  			}
   181  		case map[interface{}]interface{}:
   182  			for k, v := range m {
   183  				text[strings.ToLower(fmt.Sprint(k))] = fmt.Sprint(v)
   184  			}
   185  		default:
   186  			return fmt.Errorf("invalid i18n file format: %s", file)
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  func (p *provider) Text(namespace string, lang LanguageCodes, key string) string {
   193  	return p.Translator(namespace).Text(lang, key)
   194  }
   195  
   196  func (p *provider) Sprintf(namespace string, lang LanguageCodes, key string, args ...interface{}) string {
   197  	return p.Translator(namespace).Sprintf(lang, key, args...)
   198  }
   199  
   200  func (p *provider) Get(namespace string, lang LanguageCodes, key, def string) string {
   201  	return p.Translator(namespace).Get(lang, key, def)
   202  }
   203  
   204  func (p *provider) Translator(namespace string) Translator {
   205  	return &translator{
   206  		common: p.common,
   207  		dic:    p.dic[namespace],
   208  	}
   209  }
   210  
   211  func (p *provider) Provide(ctx servicehub.DependencyContext, options ...interface{}) interface{} {
   212  	trans, ok := ctx.Tags().Lookup("translator")
   213  	if ok {
   214  		return p.Translator(trans)
   215  	}
   216  	if ctx.Type() == translatorType {
   217  		return p.Translator("")
   218  	}
   219  	return p
   220  }
   221  
   222  type translator struct {
   223  	common map[string]map[string]string
   224  	dic    map[string]map[string]string
   225  }
   226  
   227  func (t *translator) Text(lang LanguageCodes, key string) string {
   228  	text := t.getText(lang, key)
   229  	if len(text) > 0 {
   230  		return text
   231  	}
   232  	return key
   233  }
   234  
   235  func (t *translator) Sprintf(lang LanguageCodes, key string, args ...interface{}) string {
   236  	return fmt.Sprintf(t.escape(lang, key), args...)
   237  }
   238  
   239  func (t *translator) Get(lang LanguageCodes, key, def string) string {
   240  	text := t.getText(lang, key)
   241  	if len(text) > 0 {
   242  		return text
   243  	}
   244  	return def
   245  }
   246  
   247  func (t *translator) getText(langs LanguageCodes, key string) string {
   248  	key = strings.ToLower(key)
   249  	for _, lang := range langs {
   250  		if t.dic != nil {
   251  			text := t.dic[lang.Code]
   252  			if text != nil {
   253  				if value, ok := text[key]; ok {
   254  					return value
   255  				}
   256  			}
   257  			text = t.dic[lang.RestrictedCode()]
   258  			if text != nil {
   259  				if value, ok := text[key]; ok {
   260  					return value
   261  				}
   262  			}
   263  		}
   264  		text := t.common[lang.Code]
   265  		if text != nil {
   266  			if value, ok := text[key]; ok {
   267  				return value
   268  			}
   269  		}
   270  		text = t.common[lang.RestrictedCode()]
   271  		if text != nil {
   272  			if value, ok := text[key]; ok {
   273  				return value
   274  			}
   275  		}
   276  	}
   277  	return ""
   278  }
   279  
   280  var regExp = regexp.MustCompile(`\$\{([^:}]*)(:[^}]*)?\}`)
   281  
   282  func (t *translator) escape(lang LanguageCodes, text string) string {
   283  	contents := reflectx.StringToBytes(text)
   284  	params := regExp.FindAllSubmatch(contents, -1)
   285  	for _, param := range params {
   286  		if len(param) != 3 {
   287  			continue
   288  		}
   289  		var key, defval []byte = param[1], nil
   290  		if len(param[2]) > 0 {
   291  			defval = param[2][1:]
   292  		}
   293  		k := reflectx.BytesToString(key)
   294  		val := t.getText(lang, k)
   295  		if len(val) <= 0 {
   296  			val = strings.Trim(reflectx.BytesToString(defval), `"`)
   297  		}
   298  		if len(val) <= 0 {
   299  			val = k
   300  		}
   301  		contents = bytes.Replace(contents, param[0], reflectx.StringToBytes(val), 1)
   302  	}
   303  	return reflectx.BytesToString(contents)
   304  }
   305  
   306  func init() {
   307  	servicehub.Register("i18n", &servicehub.Spec{
   308  		Services:    []string{"i18n"},
   309  		Types:       []reflect.Type{i18nType, translatorType},
   310  		Description: "i18n",
   311  		ConfigFunc:  func() interface{} { return &config{} },
   312  		Creator: func() servicehub.Provider {
   313  			return &provider{
   314  				common: make(map[string]map[string]string),
   315  				dic:    make(map[string]map[string]map[string]string),
   316  			}
   317  		},
   318  	})
   319  }