Skip to content

Make i18n live-reload work for default language and write-lock-free if no change #20165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions modules/translation/i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"reflect"
"sync"
"sync/atomic"
"time"

"code.gitea.io/gitea/modules/log"
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down