github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/translation/translation.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package translation 7 8 import ( 9 "sort" 10 "strings" 11 12 "github.com/gitbundle/modules/log" 13 "github.com/gitbundle/modules/setting" 14 "github.com/gitbundle/modules/translation/i18n" 15 16 "golang.org/x/text/language" 17 ) 18 19 // Locale represents an interface to translation 20 type Locale interface { 21 Language() string 22 Tr(string, ...interface{}) string 23 TrN(cnt interface{}, key1, keyN string, args ...interface{}) string 24 } 25 26 // LangType represents a lang type 27 type LangType struct { 28 Lang, Name string // these fields are used directly in templates: {{range .AllLangs}}{{.Lang}}{{.Name}}{{end}} 29 } 30 31 var ( 32 matcher language.Matcher 33 allLangs []*LangType 34 allLangMap map[string]*LangType 35 supportedTags []language.Tag 36 ) 37 38 // AllLangs returns all supported languages sorted by name 39 func AllLangs() []*LangType { 40 return allLangs 41 } 42 43 // TryTr tries to do the translation, if no translation, it returns (format, false) 44 func TryTr(lang, format string, args ...interface{}) (string, bool) { 45 s := i18n.Tr(lang, format, args...) 46 // now the i18n library is not good enough and we can only use this hacky method to detect whether the transaction exists 47 idx := strings.IndexByte(format, '.') 48 defaultText := format 49 if idx > 0 { 50 defaultText = format[idx+1:] 51 } 52 return s, s != defaultText 53 } 54 55 type OptionsDir func(name string) ([]string, error) 56 type OptionsLocale func(name string) ([]byte, error) 57 58 // InitLocales loads the locales 59 func InitLocales(optionsDir OptionsDir, optionsLocale OptionsLocale) { 60 i18n.ResetDefaultLocales() 61 localeNames, err := optionsDir("locale") 62 if err != nil { 63 log.Fatal("Failed to list locale files: %v", err) 64 } 65 66 localFiles := make(map[string][]byte, len(localeNames)) 67 for _, name := range localeNames { 68 localFiles[name], err = optionsLocale(name) 69 if err != nil { 70 log.Fatal("Failed to load %s locale file. %v", name, err) 71 } 72 } 73 74 supportedTags = make([]language.Tag, len(setting.Langs)) 75 for i, lang := range setting.Langs { 76 supportedTags[i] = language.Raw.Make(lang) 77 } 78 79 matcher = language.NewMatcher(supportedTags) 80 for i := range setting.Names { 81 key := "locale_" + setting.Langs[i] + ".ini" 82 if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil { 83 log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) 84 } 85 } 86 if len(setting.Langs) != 0 { 87 defaultLangName := setting.Langs[0] 88 if defaultLangName != "en-US" { 89 log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName) 90 } 91 i18n.DefaultLocales.SetDefaultLang(defaultLangName) 92 } 93 94 langs, descs := i18n.DefaultLocales.ListLangNameDesc() 95 allLangs = make([]*LangType, 0, len(langs)) 96 allLangMap = map[string]*LangType{} 97 for i, v := range langs { 98 l := &LangType{v, descs[i]} 99 allLangs = append(allLangs, l) 100 allLangMap[v] = l 101 } 102 103 // Sort languages case-insensitive according to their name - needed for the user settings 104 sort.Slice(allLangs, func(i, j int) bool { 105 return strings.ToLower(allLangs[i].Name) < strings.ToLower(allLangs[j].Name) 106 }) 107 } 108 109 // Match matches accept languages 110 func Match(tags ...language.Tag) language.Tag { 111 _, i, _ := matcher.Match(tags...) 112 return supportedTags[i] 113 } 114 115 // locale represents the information of localization. 116 type locale struct { 117 Lang, LangName string // these fields are used directly in templates: .i18n.Lang 118 } 119 120 // NewLocale return a locale 121 func NewLocale(lang string) Locale { 122 langName := "unknown" 123 if l, ok := allLangMap[lang]; ok { 124 langName = l.Name 125 } 126 return &locale{ 127 Lang: lang, 128 LangName: langName, 129 } 130 } 131 132 func (l *locale) Language() string { 133 return l.Lang 134 } 135 136 // Tr translates content to target language. 137 func (l *locale) Tr(format string, args ...interface{}) string { 138 if setting.IsProd || setting.IsBeta { 139 return i18n.Tr(l.Lang, format, args...) 140 } 141 142 // in development, we should show an error if a translation key is missing 143 s, ok := TryTr(l.Lang, format, args...) 144 if !ok { 145 log.Error("missing i18n translation key: %q", format) 146 } 147 return s 148 } 149 150 // Language specific rules for translating plural texts 151 var trNLangRules = map[string]func(int64) int{ 152 // the default rule is "en-US" if a language isn't listed here 153 "en-US": func(cnt int64) int { 154 if cnt == 1 { 155 return 0 156 } 157 return 1 158 }, 159 "lv-LV": func(cnt int64) int { 160 if cnt%10 == 1 && cnt%100 != 11 { 161 return 0 162 } 163 return 1 164 }, 165 "ru-RU": func(cnt int64) int { 166 if cnt%10 == 1 && cnt%100 != 11 { 167 return 0 168 } 169 return 1 170 }, 171 "zh-CN": func(cnt int64) int { 172 return 0 173 }, 174 "zh-HK": func(cnt int64) int { 175 return 0 176 }, 177 "zh-TW": func(cnt int64) int { 178 return 0 179 }, 180 "fr-FR": func(cnt int64) int { 181 if cnt > -2 && cnt < 2 { 182 return 0 183 } 184 return 1 185 }, 186 } 187 188 // TrN returns translated message for plural text translation 189 func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { 190 var c int64 191 if t, ok := cnt.(int); ok { 192 c = int64(t) 193 } else if t, ok := cnt.(int16); ok { 194 c = int64(t) 195 } else if t, ok := cnt.(int32); ok { 196 c = int64(t) 197 } else if t, ok := cnt.(int64); ok { 198 c = t 199 } else { 200 return l.Tr(keyN, args...) 201 } 202 203 ruleFunc, ok := trNLangRules[l.Lang] 204 if !ok { 205 ruleFunc = trNLangRules["en-US"] 206 } 207 208 if ruleFunc(c) == 0 { 209 return l.Tr(key1, args...) 210 } 211 return l.Tr(keyN, args...) 212 }