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.4KB

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