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 }