2021-01-05 16:05:40 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package translation
import (
2022-08-28 12:43:25 +03:00
"context"
2022-01-08 15:18:39 +03:00
"sort"
"strings"
2022-08-28 12:43:25 +03:00
"sync"
2022-01-08 15:18:39 +03:00
2021-01-05 16:05:40 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
2022-04-03 12:46:48 +03:00
"code.gitea.io/gitea/modules/translation/i18n"
2022-08-28 12:43:25 +03:00
"code.gitea.io/gitea/modules/watcher"
2021-01-05 16:05:40 +03:00
"golang.org/x/text/language"
)
// Locale represents an interface to translation
type Locale interface {
Language ( ) string
Tr ( string , ... interface { } ) string
2022-01-02 06:33:57 +03:00
TrN ( cnt interface { } , key1 , keyN string , args ... interface { } ) string
2021-01-05 16:05:40 +03:00
}
2021-01-26 18:36:53 +03:00
// LangType represents a lang type
type LangType struct {
2022-02-08 06:02:30 +03:00
Lang , Name string // these fields are used directly in templates: {{range .AllLangs}}{{.Lang}}{{.Name}}{{end}}
2021-01-26 18:36:53 +03:00
}
2021-01-05 16:05:40 +03:00
var (
2022-08-28 12:43:25 +03:00
lock * sync . RWMutex
2021-04-14 21:52:01 +03:00
matcher language . Matcher
2022-02-08 06:02:30 +03:00
allLangs [ ] * LangType
allLangMap map [ string ] * LangType
2021-04-14 21:52:01 +03:00
supportedTags [ ] language . Tag
2021-01-05 16:05:40 +03:00
)
2022-01-08 15:18:39 +03:00
// AllLangs returns all supported languages sorted by name
2022-02-08 06:02:30 +03:00
func AllLangs ( ) [ ] * LangType {
2021-01-26 18:36:53 +03:00
return allLangs
}
2021-01-05 16:05:40 +03:00
// InitLocales loads the locales
2022-08-28 12:43:25 +03:00
func InitLocales ( ctx context . Context ) {
if lock != nil {
lock . Lock ( )
defer lock . Unlock ( )
} else if ! setting . IsProd && lock == nil {
lock = & sync . RWMutex { }
2021-01-05 16:05:40 +03:00
}
2022-08-28 12:43:25 +03:00
refreshLocales := func ( ) {
i18n . ResetDefaultLocales ( )
localeNames , err := options . Dir ( "locale" )
if err != nil {
log . Fatal ( "Failed to list locale files: %v" , err )
}
2022-06-26 17:19:22 +03:00
2022-09-25 02:00:16 +03:00
localeData := make ( map [ string ] [ ] byte , len ( localeNames ) )
2022-08-28 12:43:25 +03:00
for _ , name := range localeNames {
2022-09-25 02:00:16 +03:00
localeData [ name ] , err = options . Locale ( name )
2022-06-26 17:19:22 +03:00
if err != nil {
log . Fatal ( "Failed to load %s locale file. %v" , name , err )
}
2021-01-05 16:05:40 +03:00
}
2022-08-28 12:43:25 +03:00
supportedTags = make ( [ ] language . Tag , len ( setting . Langs ) )
for i , lang := range setting . Langs {
supportedTags [ i ] = language . Raw . Make ( lang )
}
2021-01-26 18:36:53 +03:00
2022-08-28 12:43:25 +03:00
matcher = language . NewMatcher ( supportedTags )
for i := range setting . Names {
2022-09-25 02:00:16 +03:00
var localeDataBase [ ] byte
if i == 0 && setting . Langs [ 0 ] != "en-US" {
// Only en-US has complete translations. When use other language as default, the en-US should still be used as fallback.
localeDataBase = localeData [ "locale_en-US.ini" ]
if localeDataBase == nil {
log . Fatal ( "Failed to load locale_en-US.ini file." )
}
}
2022-06-26 17:19:22 +03:00
2022-09-25 02:00:16 +03:00
key := "locale_" + setting . Langs [ i ] + ".ini"
if err = i18n . DefaultLocales . AddLocaleByIni ( setting . Langs [ i ] , setting . Names [ i ] , localeDataBase , localeData [ key ] ) ; err != nil {
2022-08-28 12:43:25 +03:00
log . Error ( "Failed to set messages to %s: %v" , setting . Langs [ i ] , err )
}
2021-01-26 18:36:53 +03:00
}
2022-08-28 12:43:25 +03:00
if len ( setting . Langs ) != 0 {
defaultLangName := setting . Langs [ 0 ]
if defaultLangName != "en-US" {
log . Info ( "Use the first locale (%s) in LANGS setting option as default" , defaultLangName )
}
i18n . DefaultLocales . SetDefaultLang ( defaultLangName )
2022-04-03 12:46:48 +03:00
}
}
2021-01-26 18:36:53 +03:00
2022-08-28 12:43:25 +03:00
refreshLocales ( )
2022-04-03 12:46:48 +03:00
langs , descs := i18n . DefaultLocales . ListLangNameDesc ( )
allLangs = make ( [ ] * LangType , 0 , len ( langs ) )
2022-02-08 06:02:30 +03:00
allLangMap = map [ string ] * LangType { }
2021-01-26 18:36:53 +03:00
for i , v := range langs {
2022-02-08 06:02:30 +03:00
l := & LangType { v , descs [ i ] }
allLangs = append ( allLangs , l )
allLangMap [ v ] = l
2021-01-05 16:05:40 +03:00
}
2022-01-08 15:18:39 +03:00
2022-02-08 06:02:30 +03:00
// Sort languages case-insensitive according to their name - needed for the user settings
2022-01-08 15:18:39 +03:00
sort . Slice ( allLangs , func ( i , j int ) bool {
return strings . ToLower ( allLangs [ i ] . Name ) < strings . ToLower ( allLangs [ j ] . Name )
} )
2022-08-28 12:43:25 +03:00
if ! setting . IsProd {
watcher . CreateWatcher ( ctx , "Locales" , & watcher . CreateWatcherOpts {
PathsCallback : options . WalkLocales ,
BetweenCallback : func ( ) {
lock . Lock ( )
defer lock . Unlock ( )
refreshLocales ( )
} ,
} )
}
2021-01-05 16:05:40 +03:00
}
// Match matches accept languages
2021-04-14 21:52:01 +03:00
func Match ( tags ... language . Tag ) language . Tag {
_ , i , _ := matcher . Match ( tags ... )
return supportedTags [ i ]
2021-01-05 16:05:40 +03:00
}
// locale represents the information of localization.
type locale struct {
2022-08-28 12:43:25 +03:00
i18n . Locale
2022-02-08 06:02:30 +03:00
Lang , LangName string // these fields are used directly in templates: .i18n.Lang
2021-01-05 16:05:40 +03:00
}
// NewLocale return a locale
func NewLocale ( lang string ) Locale {
2022-08-28 12:43:25 +03:00
if lock != nil {
lock . RLock ( )
defer lock . RUnlock ( )
}
2022-02-08 06:02:30 +03:00
langName := "unknown"
if l , ok := allLangMap [ lang ] ; ok {
langName = l . Name
}
2022-08-28 12:43:25 +03:00
i18nLocale , _ := i18n . GetLocale ( lang )
2021-01-05 16:05:40 +03:00
return & locale {
2022-08-28 12:43:25 +03:00
Locale : i18nLocale ,
2022-02-08 06:02:30 +03:00
Lang : lang ,
LangName : langName ,
2021-01-05 16:05:40 +03:00
}
}
func ( l * locale ) Language ( ) string {
return l . Lang
}
2022-01-02 06:33:57 +03:00
// Language specific rules for translating plural texts
var trNLangRules = map [ string ] func ( int64 ) int {
// the default rule is "en-US" if a language isn't listed here
"en-US" : func ( cnt int64 ) int {
if cnt == 1 {
return 0
}
return 1
} ,
"lv-LV" : func ( cnt int64 ) int {
if cnt % 10 == 1 && cnt % 100 != 11 {
return 0
}
return 1
} ,
"ru-RU" : func ( cnt int64 ) int {
if cnt % 10 == 1 && cnt % 100 != 11 {
return 0
}
return 1
} ,
"zh-CN" : func ( cnt int64 ) int {
return 0
} ,
"zh-HK" : func ( cnt int64 ) int {
return 0
} ,
"zh-TW" : func ( cnt int64 ) int {
return 0
} ,
"fr-FR" : func ( cnt int64 ) int {
if cnt > - 2 && cnt < 2 {
return 0
}
return 1
} ,
}
// TrN returns translated message for plural text translation
func ( l * locale ) TrN ( cnt interface { } , key1 , keyN string , args ... interface { } ) string {
var c int64
if t , ok := cnt . ( int ) ; ok {
c = int64 ( t )
} else if t , ok := cnt . ( int16 ) ; ok {
c = int64 ( t )
} else if t , ok := cnt . ( int32 ) ; ok {
c = int64 ( t )
} else if t , ok := cnt . ( int64 ) ; ok {
c = t
} else {
return l . Tr ( keyN , args ... )
}
ruleFunc , ok := trNLangRules [ l . Lang ]
if ! ok {
ruleFunc = trNLangRules [ "en-US" ]
}
if ruleFunc ( c ) == 0 {
return l . Tr ( key1 , args ... )
}
return l . Tr ( keyN , args ... )
}