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  }