diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index acce5f19fb0dc..268aca3ae8c43 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -10,6 +10,7 @@ import ( "os" "reflect" "sync" + "sync/atomic" "time" "code.gitea.io/gitea/modules/log" @@ -31,11 +32,11 @@ type locale struct { sourceFileName string sourceFileInfo os.FileInfo - lastReloadCheckTime time.Time + lastReloadCheckTime atomic.Value } type LocaleStore struct { - reloadMu *sync.Mutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload. + reloadMu *sync.RWMutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload. langNames []string langDescs []string @@ -49,7 +50,7 @@ type LocaleStore struct { func NewLocaleStore(isProd bool) *LocaleStore { ls := &LocaleStore{localeMap: make(map[string]*locale), textIdxMap: make(map[string]int)} if !isProd { - ls.reloadMu = &sync.Mutex{} + ls.reloadMu = &sync.RWMutex{} } return ls } @@ -66,6 +67,7 @@ func (ls *LocaleStore) AddLocaleByIni(langName, langDesc string, source interfac if fileName, ok := source.(string); ok { lc.sourceFileName = fileName lc.sourceFileInfo, _ = os.Stat(fileName) // live-reload only works for regular files. the error can be ignored + lc.lastReloadCheckTime.Store(&time.Time{}) } ls.langNames = append(ls.langNames, langName) @@ -136,25 +138,39 @@ func (ls *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string { // Tr translates content to locale language. fall back to default language. func (l *locale) Tr(trKey string, trArgs ...interface{}) string { if l.store.reloadMu != nil { - l.store.reloadMu.Lock() - defer l.store.reloadMu.Unlock() + l.store.reloadMu.RLock() + defer l.store.reloadMu.RUnlock() + } + msg, _ := l.tryTr(trKey, trArgs...) + return msg +} + +func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { + if l.store.reloadMu != nil { now := time.Now() - if now.Sub(l.lastReloadCheckTime) >= time.Second && l.sourceFileInfo != nil && l.sourceFileName != "" { - l.lastReloadCheckTime = now - if sourceFileInfo, err := os.Stat(l.sourceFileName); err == nil && !sourceFileInfo.ModTime().Equal(l.sourceFileInfo.ModTime()) { + lastCheckTime, ok := l.lastReloadCheckTime.Load().(*time.Time) + if ok && now.Sub(*lastCheckTime) >= time.Second && l.sourceFileInfo != nil { + lastModTime := l.sourceFileInfo.ModTime() + l.store.reloadMu.RUnlock() // the file may need to be reloaded with a write-lock, release the read-lock first + + sourceFileInfo, err := os.Stat(l.sourceFileName) + casOk := l.lastReloadCheckTime.CompareAndSwap(lastCheckTime, &now) + if err == nil && casOk && !sourceFileInfo.ModTime().Equal(lastModTime) { + l.store.reloadMu.Lock() // acquire the write-lock only if the file need to be reloaded if err = l.store.reloadLocaleByIni(l.langName, l.sourceFileName); err == nil { l.sourceFileInfo = sourceFileInfo + log.Info("reloaded locale file %q", l.sourceFileName) } else { log.Error("unable to live-reload the locale file %q, err: %v", l.sourceFileName, err) } + l.store.reloadMu.Unlock() // release the write-lock + } else if err != nil { + log.Error("unable to stat the locale file %q, err: %v", l.sourceFileName, err) } + + l.store.reloadMu.RLock() // re-acquire the read-lock, which was managed by outer Tr function } } - msg, _ := l.tryTr(trKey, trArgs...) - return msg -} - -func (l *locale) tryTr(trKey string, trArgs ...interface{}) (msg string, found bool) { trMsg := trKey textIdx, ok := l.store.textIdxMap[trKey] if ok {