code.gitea.io/gitea@v1.19.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 "sort" 9 "strings" 10 "sync" 11 12 "code.gitea.io/gitea/modules/log" 13 "code.gitea.io/gitea/modules/options" 14 "code.gitea.io/gitea/modules/setting" 15 "code.gitea.io/gitea/modules/translation/i18n" 16 "code.gitea.io/gitea/modules/watcher" 17 18 "golang.org/x/text/language" 19 ) 20 21 // Locale represents an interface to translation 22 type Locale interface { 23 Language() string 24 Tr(string, ...interface{}) string 25 TrN(cnt interface{}, key1, keyN string, args ...interface{}) string 26 } 27 28 // LangType represents a lang type 29 type LangType struct { 30 Lang, Name string // these fields are used directly in templates: {{range .AllLangs}}{{.Lang}}{{.Name}}{{end}} 31 } 32 33 var ( 34 lock *sync.RWMutex 35 matcher language.Matcher 36 allLangs []*LangType 37 allLangMap map[string]*LangType 38 supportedTags []language.Tag 39 ) 40 41 // AllLangs returns all supported languages sorted by name 42 func AllLangs() []*LangType { 43 return allLangs 44 } 45 46 // InitLocales loads the locales 47 func InitLocales(ctx context.Context) { 48 if lock != nil { 49 lock.Lock() 50 defer lock.Unlock() 51 } else if !setting.IsProd && lock == nil { 52 lock = &sync.RWMutex{} 53 } 54 55 refreshLocales := func() { 56 i18n.ResetDefaultLocales() 57 localeNames, err := options.Dir("locale") 58 if err != nil { 59 log.Fatal("Failed to list locale files: %v", err) 60 } 61 62 localeData := make(map[string][]byte, len(localeNames)) 63 for _, name := range localeNames { 64 localeData[name], err = options.Locale(name) 65 if err != nil { 66 log.Fatal("Failed to load %s locale file. %v", name, err) 67 } 68 } 69 70 supportedTags = make([]language.Tag, len(setting.Langs)) 71 for i, lang := range setting.Langs { 72 supportedTags[i] = language.Raw.Make(lang) 73 } 74 75 matcher = language.NewMatcher(supportedTags) 76 for i := range setting.Names { 77 var localeDataBase []byte 78 if i == 0 && setting.Langs[0] != "en-US" { 79 // Only en-US has complete translations. When use other language as default, the en-US should still be used as fallback. 80 localeDataBase = localeData["locale_en-US.ini"] 81 if localeDataBase == nil { 82 log.Fatal("Failed to load locale_en-US.ini file.") 83 } 84 } 85 86 key := "locale_" + setting.Langs[i] + ".ini" 87 if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil { 88 log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) 89 } 90 } 91 if len(setting.Langs) != 0 { 92 defaultLangName := setting.Langs[0] 93 if defaultLangName != "en-US" { 94 log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName) 95 } 96 i18n.DefaultLocales.SetDefaultLang(defaultLangName) 97 } 98 } 99 100 refreshLocales() 101 102 langs, descs := i18n.DefaultLocales.ListLangNameDesc() 103 allLangs = make([]*LangType, 0, len(langs)) 104 allLangMap = map[string]*LangType{} 105 for i, v := range langs { 106 l := &LangType{v, descs[i]} 107 allLangs = append(allLangs, l) 108 allLangMap[v] = l 109 } 110 111 // Sort languages case-insensitive according to their name - needed for the user settings 112 sort.Slice(allLangs, func(i, j int) bool { 113 return strings.ToLower(allLangs[i].Name) < strings.ToLower(allLangs[j].Name) 114 }) 115 116 if !setting.IsProd { 117 watcher.CreateWatcher(ctx, "Locales", &watcher.CreateWatcherOpts{ 118 PathsCallback: options.WalkLocales, 119 BetweenCallback: func() { 120 lock.Lock() 121 defer lock.Unlock() 122 refreshLocales() 123 }, 124 }) 125 } 126 } 127 128 // Match matches accept languages 129 func Match(tags ...language.Tag) language.Tag { 130 _, i, _ := matcher.Match(tags...) 131 return supportedTags[i] 132 } 133 134 // locale represents the information of localization. 135 type locale struct { 136 i18n.Locale 137 Lang, LangName string // these fields are used directly in templates: .i18n.Lang 138 } 139 140 // NewLocale return a locale 141 func NewLocale(lang string) Locale { 142 if lock != nil { 143 lock.RLock() 144 defer lock.RUnlock() 145 } 146 147 langName := "unknown" 148 if l, ok := allLangMap[lang]; ok { 149 langName = l.Name 150 } 151 i18nLocale, _ := i18n.GetLocale(lang) 152 return &locale{ 153 Locale: i18nLocale, 154 Lang: lang, 155 LangName: langName, 156 } 157 } 158 159 func (l *locale) Language() string { 160 return l.Lang 161 } 162 163 // Language specific rules for translating plural texts 164 var trNLangRules = map[string]func(int64) int{ 165 // the default rule is "en-US" if a language isn't listed here 166 "en-US": func(cnt int64) int { 167 if cnt == 1 { 168 return 0 169 } 170 return 1 171 }, 172 "lv-LV": func(cnt int64) int { 173 if cnt%10 == 1 && cnt%100 != 11 { 174 return 0 175 } 176 return 1 177 }, 178 "ru-RU": func(cnt int64) int { 179 if cnt%10 == 1 && cnt%100 != 11 { 180 return 0 181 } 182 return 1 183 }, 184 "zh-CN": func(cnt int64) int { 185 return 0 186 }, 187 "zh-HK": func(cnt int64) int { 188 return 0 189 }, 190 "zh-TW": func(cnt int64) int { 191 return 0 192 }, 193 "fr-FR": func(cnt int64) int { 194 if cnt > -2 && cnt < 2 { 195 return 0 196 } 197 return 1 198 }, 199 } 200 201 // TrN returns translated message for plural text translation 202 func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { 203 var c int64 204 if t, ok := cnt.(int); ok { 205 c = int64(t) 206 } else if t, ok := cnt.(int16); ok { 207 c = int64(t) 208 } else if t, ok := cnt.(int32); ok { 209 c = int64(t) 210 } else if t, ok := cnt.(int64); ok { 211 c = t 212 } else { 213 return l.Tr(keyN, args...) 214 } 215 216 ruleFunc, ok := trNLangRules[l.Lang] 217 if !ok { 218 ruleFunc = trNLangRules["en-US"] 219 } 220 221 if ruleFunc(c) == 0 { 222 return l.Tr(key1, args...) 223 } 224 return l.Tr(keyN, args...) 225 }