code.gitea.io/gitea@v1.22.3/modules/translation/translation.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package translation 5 6 import ( 7 "context" 8 "html/template" 9 "sort" 10 "strings" 11 "sync" 12 13 "code.gitea.io/gitea/modules/log" 14 "code.gitea.io/gitea/modules/options" 15 "code.gitea.io/gitea/modules/setting" 16 "code.gitea.io/gitea/modules/translation/i18n" 17 "code.gitea.io/gitea/modules/util" 18 19 "golang.org/x/text/language" 20 "golang.org/x/text/message" 21 "golang.org/x/text/number" 22 ) 23 24 type contextKey struct{} 25 26 var ContextKey any = &contextKey{} 27 28 // Locale represents an interface to translation 29 type Locale interface { 30 Language() string 31 TrString(string, ...any) string 32 33 Tr(key string, args ...any) template.HTML 34 TrN(cnt any, key1, keyN string, args ...any) template.HTML 35 36 PrettyNumber(v any) string 37 } 38 39 // LangType represents a lang type 40 type LangType struct { 41 Lang, Name string // these fields are used directly in templates: {{range .AllLangs}}{{.Lang}}{{.Name}}{{end}} 42 } 43 44 var ( 45 lock *sync.RWMutex 46 47 allLangs []*LangType 48 allLangMap map[string]*LangType 49 50 matcher language.Matcher 51 supportedTags []language.Tag 52 ) 53 54 // AllLangs returns all supported languages sorted by name 55 func AllLangs() []*LangType { 56 return allLangs 57 } 58 59 // InitLocales loads the locales 60 func InitLocales(ctx context.Context) { 61 if lock != nil { 62 lock.Lock() 63 defer lock.Unlock() 64 } else if !setting.IsProd && lock == nil { 65 lock = &sync.RWMutex{} 66 } 67 68 refreshLocales := func() { 69 i18n.ResetDefaultLocales() 70 localeNames, err := options.AssetFS().ListFiles("locale", true) 71 if err != nil { 72 log.Fatal("Failed to list locale files: %v", err) 73 } 74 75 localeData := make(map[string][]byte, len(localeNames)) 76 for _, name := range localeNames { 77 localeData[name], err = options.Locale(name) 78 if err != nil { 79 log.Fatal("Failed to load %s locale file. %v", name, err) 80 } 81 } 82 83 supportedTags = make([]language.Tag, len(setting.Langs)) 84 for i, lang := range setting.Langs { 85 supportedTags[i] = language.Raw.Make(lang) 86 } 87 88 matcher = language.NewMatcher(supportedTags) 89 for i := range setting.Names { 90 var localeDataBase []byte 91 if i == 0 && setting.Langs[0] != "en-US" { 92 // Only en-US has complete translations. When use other language as default, the en-US should still be used as fallback. 93 localeDataBase = localeData["locale_en-US.ini"] 94 if localeDataBase == nil { 95 log.Fatal("Failed to load locale_en-US.ini file.") 96 } 97 } 98 99 key := "locale_" + setting.Langs[i] + ".ini" 100 if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil { 101 log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) 102 } 103 } 104 if len(setting.Langs) != 0 { 105 defaultLangName := setting.Langs[0] 106 if defaultLangName != "en-US" { 107 log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName) 108 } 109 i18n.DefaultLocales.SetDefaultLang(defaultLangName) 110 } 111 } 112 113 refreshLocales() 114 115 langs, descs := i18n.DefaultLocales.ListLangNameDesc() 116 allLangs = make([]*LangType, 0, len(langs)) 117 allLangMap = map[string]*LangType{} 118 for i, v := range langs { 119 l := &LangType{v, descs[i]} 120 allLangs = append(allLangs, l) 121 allLangMap[v] = l 122 } 123 124 // Sort languages case-insensitive according to their name - needed for the user settings 125 sort.Slice(allLangs, func(i, j int) bool { 126 return strings.ToLower(allLangs[i].Name) < strings.ToLower(allLangs[j].Name) 127 }) 128 129 if !setting.IsProd { 130 go options.AssetFS().WatchLocalChanges(ctx, func() { 131 lock.Lock() 132 defer lock.Unlock() 133 refreshLocales() 134 }) 135 } 136 } 137 138 // Match matches accept languages 139 func Match(tags ...language.Tag) language.Tag { 140 _, i, _ := matcher.Match(tags...) 141 return supportedTags[i] 142 } 143 144 // locale represents the information of localization. 145 type locale struct { 146 i18n.Locale 147 Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang 148 msgPrinter *message.Printer 149 } 150 151 var _ Locale = (*locale)(nil) 152 153 // NewLocale return a locale 154 func NewLocale(lang string) Locale { 155 if lock != nil { 156 lock.RLock() 157 defer lock.RUnlock() 158 } 159 160 langName := "unknown" 161 if l, ok := allLangMap[lang]; ok { 162 langName = l.Name 163 } else if len(setting.Langs) > 0 { 164 lang = setting.Langs[0] 165 langName = setting.Names[0] 166 } 167 168 i18nLocale, _ := i18n.GetLocale(lang) 169 l := &locale{ 170 Locale: i18nLocale, 171 Lang: lang, 172 LangName: langName, 173 } 174 if langTag, err := language.Parse(lang); err != nil { 175 log.Error("Failed to parse language tag from name %q: %v", l.Lang, err) 176 l.msgPrinter = message.NewPrinter(language.English) 177 } else { 178 l.msgPrinter = message.NewPrinter(langTag) 179 } 180 return l 181 } 182 183 func (l *locale) Language() string { 184 return l.Lang 185 } 186 187 // Language specific rules for translating plural texts 188 var trNLangRules = map[string]func(int64) int{ 189 // the default rule is "en-US" if a language isn't listed here 190 "en-US": func(cnt int64) int { 191 if cnt == 1 { 192 return 0 193 } 194 return 1 195 }, 196 "lv-LV": func(cnt int64) int { 197 if cnt%10 == 1 && cnt%100 != 11 { 198 return 0 199 } 200 return 1 201 }, 202 "ru-RU": func(cnt int64) int { 203 if cnt%10 == 1 && cnt%100 != 11 { 204 return 0 205 } 206 return 1 207 }, 208 "zh-CN": func(cnt int64) int { 209 return 0 210 }, 211 "zh-HK": func(cnt int64) int { 212 return 0 213 }, 214 "zh-TW": func(cnt int64) int { 215 return 0 216 }, 217 "fr-FR": func(cnt int64) int { 218 if cnt > -2 && cnt < 2 { 219 return 0 220 } 221 return 1 222 }, 223 } 224 225 func (l *locale) Tr(s string, args ...any) template.HTML { 226 return l.TrHTML(s, args...) 227 } 228 229 // TrN returns translated message for plural text translation 230 func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { 231 var c int64 232 if t, ok := cnt.(int); ok { 233 c = int64(t) 234 } else if t, ok := cnt.(int16); ok { 235 c = int64(t) 236 } else if t, ok := cnt.(int32); ok { 237 c = int64(t) 238 } else if t, ok := cnt.(int64); ok { 239 c = t 240 } else { 241 return l.Tr(keyN, args...) 242 } 243 244 ruleFunc, ok := trNLangRules[l.Lang] 245 if !ok { 246 ruleFunc = trNLangRules["en-US"] 247 } 248 249 if ruleFunc(c) == 0 { 250 return l.Tr(key1, args...) 251 } 252 return l.Tr(keyN, args...) 253 } 254 255 func (l *locale) PrettyNumber(v any) string { 256 // TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format 257 if s, ok := v.(string); ok { 258 if num, err := util.ToInt64(s); err == nil { 259 v = num 260 } else if num, err := util.ToFloat64(s); err == nil { 261 v = num 262 } 263 } 264 return l.msgPrinter.Sprintf("%v", number.Decimal(v)) 265 } 266 267 func init() { 268 // prepare a default matcher, especially for tests 269 supportedTags = []language.Tag{language.English} 270 matcher = language.NewMatcher(supportedTags) 271 }