diff --git a/pkg/locale/http.go b/pkg/locale/http.go index 319ca7fbe..a3c8bd84c 100644 --- a/pkg/locale/http.go +++ b/pkg/locale/http.go @@ -23,7 +23,7 @@ func upgradeRequest(ll *service, r *http.Request) *http.Request { func detectLanguage(ll *service, r *http.Request) (tag language.Tag) { if ll.opt.DevelopmentMode { - if err := ll.Reload(); err != nil { + if err := ll.ReloadStatic(); err != nil { // when in development mode, refresh languages for every request ll.log.Error("failed to load locales", zap.Error(err)) return diff --git a/pkg/locale/load.go b/pkg/locale/load.go index 53baff34f..9538d2062 100644 --- a/pkg/locale/load.go +++ b/pkg/locale/load.go @@ -2,6 +2,7 @@ package locale import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -194,6 +195,15 @@ func loadTranslations(lang *Language) (err error) { return } +func (svc *service) loadResourceTranslations(ctx context.Context, lang *Language, tg language.Tag) (err error) { + lang.resources, err = svc.s.TransformResource(ctx, tg) + if err != nil { + return err + } + + return +} + // procInternal reads internal YAML translation files and converts it into // simple key/value structure func procInternal(ns map[string]string, prefix string, f io.Reader) (err error) { diff --git a/pkg/locale/locale.go b/pkg/locale/locale.go index fa24e0c10..cea3f6054 100644 --- a/pkg/locale/locale.go +++ b/pkg/locale/locale.go @@ -20,6 +20,9 @@ type ( // application => namespace => buffered JSON docs external map[string]io.ReadSeeker + // resource => key => ResourceTranslation + resources map[string]map[string]*ResourceTranslation + Language struct { l sync.RWMutex @@ -49,13 +52,15 @@ type ( internal internal // External translations (webapps) - external external + external external + resources resources } ErrorMetaNamespace struct{} ErrorMetaKey struct{} ) +// t returns the translated string for internal content func (l *Language) t(ns, key string, rr ...string) string { l.l.RLock() defer l.l.RUnlock() @@ -75,3 +80,38 @@ func (l *Language) t(ns, key string, rr ...string) string { return key } + +// tr returns the translated string for resource translations +func (l *Language) tr(ns, key string, rr ...string) string { + l.l.RLock() + defer l.l.RUnlock() + + for r := 0; r < len(rr); r += 2 { + rr[r] = fmt.Sprintf("{{%s}}", rr[r]) + } + + rt, has := l.resources[ns][key] + if has { + return strings.NewReplacer(rr...).Replace(rt.Msg) + } + + if l.extends != nil { + return l.extends.tr(ns, key, rr...) + } + + return key +} + +// resourceTranslations returns all resource translations for the specified resource +func (l *Language) resourceTranslations(resource string) ResourceTranslationIndex { + out := make(ResourceTranslationIndex) + if l.resources == nil { + return out + } + + for k, rt := range l.resources[resource] { + out[k] = rt + } + + return out +} diff --git a/pkg/locale/resource.go b/pkg/locale/resource.go new file mode 100644 index 000000000..590f7547f --- /dev/null +++ b/pkg/locale/resource.go @@ -0,0 +1,33 @@ +package locale + +import "golang.org/x/text/language" + +type ( + ResourceTranslation struct { + Resource string `json:"resource"` + Lang string `json:"lang"` + Key string `json:"key"` + Msg string `json:"msg"` + } + + ResourceTranslationIndex map[string]*ResourceTranslation + + ResourceTranslationSet []*ResourceTranslation +) + +func ContentID(cID uint64, i int) uint64 { + if cID == 0 && i > 0 { + return uint64(i) + } + return cID +} + +func (rr ResourceTranslationSet) SetLanguage(tag language.Tag) { + for _, r := range rr { + r.Lang = tag.String() + } +} + +func (rx ResourceTranslationIndex) FindByKey(k string) *ResourceTranslation { + return rx[k] +} diff --git a/pkg/locale/service.go b/pkg/locale/service.go index ee1a334e7..2daf7ea82 100644 --- a/pkg/locale/service.go +++ b/pkg/locale/service.go @@ -16,8 +16,26 @@ import ( ) type ( + resourceStore interface { + TransformResource(context.Context, language.Tag) (map[string]map[string]*ResourceTranslation, error) + } + + Locale interface { + T(ctx context.Context, ns, key string, rr ...string) string + TFor(tag language.Tag, ns, key string, rr ...string) string + Tags() []language.Tag + } + + Resource interface { + TR(ctx context.Context, ns, key string, rr ...string) string + TRFor(tag language.Tag, ns, key string, rr ...string) string + Tags() []language.Tag + ResourceTranslations(code language.Tag, resource string) ResourceTranslationIndex + } + service struct { l sync.RWMutex + s resourceStore // logger facility for the locale service log *zap.Logger @@ -75,7 +93,11 @@ func Service(log *zap.Logger, opt options.LocaleOpt) (*service, error) { svc.tags = append(svc.tags, tag) } - return svc, svc.Reload() + return svc, svc.ReloadStatic() +} + +func (svc *service) BindStore(s resourceStore) { + svc.s = s } // Default language @@ -93,9 +115,9 @@ func (svc *service) Tags() (tt []language.Tag) { return } -// Reload all embedded (via github.com/cortezaproject/corteza-locale package) -// and imported (from one or more paths found in LOCALE_PATH) translations -func (svc *service) Reload() (err error) { +// ReloadStatic all language configurations (as configured via path options) and +// all translation files +func (svc *service) ReloadStatic() (err error) { var ( i int lang *Language @@ -107,7 +129,7 @@ func (svc *service) Reload() (err error) { svc.l.RLock() defer svc.l.RUnlock() - svc.log.Info("reloading", + svc.log.Info("reloading static", zap.Strings("path", svc.src), zap.Strings("tags", tagsToStrings(svc.tags)), ) @@ -203,6 +225,60 @@ func (svc *service) Reload() (err error) { return nil } +// ReloadResourceTranslations all language configurations (as configured via path options) and +// all translation files +func (svc *service) ReloadResourceTranslations(ctx context.Context) (err error) { + svc.l.RLock() + defer svc.l.RUnlock() + + svc.log.Info("reloading resource translations", + zap.Strings("tags", tagsToStrings(svc.tags)), + ) + + for i, tag := range svc.tags { + lang, ok := svc.set[tag] + if !ok { + lang = &Language{ + Tag: tag, + src: "store", + } + + svc.set[tag] = lang + } + + if err = svc.loadResourceTranslations(ctx, lang, lang.Tag); err != nil { + return err + } + + svc.log.Info( + "language loaded", + zap.Stringer("tag", lang.Tag), + zap.String("src", lang.src), + zap.Stringer("extends", lang.Extends), + ) + + if i == 0 && svc.def == nil { + // set first one as default + svc.def = lang + } + } + + // Do another pass and link all extended languages + for _, lang := range svc.set { + if lang.Extends.IsRoot() { + continue + } + + if svc.set[lang.Extends] == nil { + return fmt.Errorf("could not extend langage %q from an unknown language %q", lang.Tag, lang.Extends) + } + + lang.extends = svc.set[lang.Extends] + } + + return nil +} + // List returns list of all languages func (svc *service) List() []*Language { svc.l.RLock() @@ -304,6 +380,43 @@ func (svc *service) T(ctx context.Context, ns, key string, rr ...string) string return svc.t(GetLanguageFromContext(ctx), ns, key, rr...) } +// T returns translated key from namespaces using list of replacement pairs +// +// Language is specified +func (svc *service) TFor(tag language.Tag, ns, key string, rr ...string) string { + return svc.t(tag, ns, key, rr...) +} + +// TR returns translated key for resource using list of replacement pairs +// +// Language is picked from the context +func (svc *service) TR(ctx context.Context, ns, key string, rr ...string) string { + return svc.tr(GetLanguageFromContext(ctx), ns, key, rr...) +} + +// TRFor returns translated key for resource using list of replacement pairs +// +// Language is picked from the context +func (svc *service) TRFor(tag language.Tag, ns, key string, rr ...string) string { + return svc.tr(tag, ns, key, rr...) +} + +// ResourceTranslations returns all translations for the given language for the +// given resource. +// +// The response is indexed by translation key for nicer lookups. +func (svc *service) ResourceTranslations(code language.Tag, resource string) ResourceTranslationIndex { + out := make(ResourceTranslationIndex) + + if svc != nil && svc.set != nil { + if l, has := svc.set[code]; has { + return l.resourceTranslations(resource) + } + } + + return out +} + // Finds language and uses it to translate the given key func (svc *service) t(code language.Tag, ns, key string, rr ...string) string { if svc != nil && svc.set != nil { @@ -315,6 +428,17 @@ func (svc *service) t(code language.Tag, ns, key string, rr ...string) string { return key } +// Finds language and uses it to translate the given key for resource +func (svc *service) tr(code language.Tag, ns, key string, rr ...string) string { + if svc != nil && svc.set != nil { + if l, has := svc.set[code]; has { + return l.tr(ns, key, rr...) + } + } + + return key +} + func hasTag(t language.Tag, tt []language.Tag) bool { for _, tag := range tt { if tag.String() == t.String() {