3
0

Update pkg/locale to support resource translations

This commit is contained in:
Tomaž Jerman 2021-09-14 12:51:41 +02:00
parent 2a07d0435f
commit 4da147ec1a
5 changed files with 214 additions and 7 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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
}

33
pkg/locale/resource.go Normal file
View File

@ -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]
}

View File

@ -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() {