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  }