You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

translation.go 6.2KB

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