snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
503
server/pkg/locale/service.go
Normal file
503
server/pkg/locale/service.go
Normal file
@@ -0,0 +1,503 @@
|
||||
package locale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/options"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/language/display"
|
||||
)
|
||||
|
||||
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 {
|
||||
TResource(ctx context.Context, ns, key string, rr ...string) string
|
||||
TResourceFor(tag language.Tag, ns, key string, rr ...string) string
|
||||
Tags() []language.Tag
|
||||
SupportedLang(language.Tag) bool
|
||||
ResourceTranslations(code language.Tag, resource string) ResourceTranslationIndex
|
||||
Default() *Language
|
||||
}
|
||||
|
||||
service struct {
|
||||
l sync.RWMutex
|
||||
s resourceStore
|
||||
|
||||
// logger facility for the locale service
|
||||
log *zap.Logger
|
||||
|
||||
// options
|
||||
opt options.LocaleOpt
|
||||
|
||||
// configured sources where we can load languages from
|
||||
src []string
|
||||
|
||||
// all configured tags
|
||||
tags []language.Tag
|
||||
|
||||
// set of all known languages
|
||||
set map[language.Tag]*Language
|
||||
|
||||
// default language
|
||||
def *Language
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// Global locales service
|
||||
global *service
|
||||
)
|
||||
|
||||
// Global returns global RBAC service
|
||||
func Global() *service {
|
||||
return global
|
||||
}
|
||||
|
||||
// SetGlobal re-sets global service
|
||||
func SetGlobal(ll *service) {
|
||||
global = ll
|
||||
}
|
||||
|
||||
func Static(ll ...*Language) (svc *service) {
|
||||
svc = &service{
|
||||
log: zap.NewNop(),
|
||||
set: make(map[language.Tag]*Language),
|
||||
}
|
||||
|
||||
for _, l := range ll {
|
||||
svc.set[l.Tag] = l
|
||||
svc.tags = append(svc.tags, l.Tag)
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func Service(log *zap.Logger, opt options.LocaleOpt) (*service, error) {
|
||||
svc := &service{
|
||||
opt: opt,
|
||||
src: strings.Split(opt.Path, ":"),
|
||||
log: log.Named("locale"),
|
||||
}
|
||||
for _, lang := range strings.Split(opt.Languages, ",") {
|
||||
lang = strings.TrimSpace(lang)
|
||||
tag := language.Make(lang)
|
||||
if tag.IsRoot() {
|
||||
return nil, fmt.Errorf("failed to parse language '%s' (did you use comma to separate them?)", lang)
|
||||
}
|
||||
|
||||
svc.tags = append(svc.tags, tag)
|
||||
}
|
||||
|
||||
return svc, svc.ReloadStatic()
|
||||
}
|
||||
|
||||
func (svc *service) BindStore(s resourceStore) {
|
||||
svc.s = s
|
||||
}
|
||||
|
||||
// Default language
|
||||
func (svc *service) Default() *Language {
|
||||
return svc.def
|
||||
}
|
||||
|
||||
// Tags of all loaded languages
|
||||
func (svc *service) Tags() (tt []language.Tag) {
|
||||
svc.l.RLock()
|
||||
defer svc.l.RUnlock()
|
||||
return svc.tags
|
||||
}
|
||||
|
||||
func (svc *service) SupportedLang(tag language.Tag) bool {
|
||||
return svc.set[tag] != nil
|
||||
}
|
||||
|
||||
// ReloadStatic all language configurations (as configured via path options) and
|
||||
// all translation files
|
||||
func (svc *service) ReloadStatic() (err error) {
|
||||
var (
|
||||
ll, aux []*Language
|
||||
|
||||
logFields = make([]zap.Field, 0)
|
||||
)
|
||||
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
if len(svc.tags) == 0 {
|
||||
return fmt.Errorf("no supported languages (LOCALE_LANGUAGES is empty)")
|
||||
}
|
||||
|
||||
svc.log.Info("reloading static",
|
||||
zap.Strings("path", svc.src),
|
||||
zap.Strings("tags", tagsToStrings(svc.tags)),
|
||||
)
|
||||
|
||||
// In case where this is already initialized, we need to make sure to
|
||||
// preserve any resource translations.
|
||||
// The preservation is handled below, before we update svc.set.
|
||||
if svc.set == nil {
|
||||
svc.set = make(map[language.Tag]*Language)
|
||||
}
|
||||
|
||||
// load embedded locales from the corteza-locale package
|
||||
if ll, err = loadConfigs(LoadEmbedded()); err != nil {
|
||||
return fmt.Errorf("could not load embedded locales: %w", err)
|
||||
} else {
|
||||
// cleanup src and effectively mark the
|
||||
// loaded languages as embedded
|
||||
for _, l := range ll {
|
||||
l.src = ""
|
||||
}
|
||||
}
|
||||
|
||||
// load imported locales from the configured (LOCALE_PATH) paths
|
||||
for _, p := range svc.src {
|
||||
aux, err = loadConfigs(os.DirFS(p))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load imported locales: %w", err)
|
||||
}
|
||||
|
||||
if len(aux) == 0 {
|
||||
svc.log.Warn(
|
||||
"no languages found in path, future versions of Corteza will require at least one language to be present",
|
||||
zap.String("path", p),
|
||||
)
|
||||
}
|
||||
|
||||
for _, l := range aux {
|
||||
l.src = p + "/" + l.src
|
||||
}
|
||||
|
||||
ll = append(ll, aux...)
|
||||
}
|
||||
|
||||
for _, lang := range ll {
|
||||
logFields = []zap.Field{
|
||||
zap.Stringer("tag", lang.Tag),
|
||||
}
|
||||
|
||||
if !hasTag(lang.Tag, svc.tags) {
|
||||
svc.log.Debug(
|
||||
"language skipped (not in LOCALE_LANGUAGES)",
|
||||
zap.Stringer("tag", lang.Tag),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if !lang.Extends.IsRoot() {
|
||||
logFields = append(logFields, zap.Stringer("extends", lang.Extends))
|
||||
}
|
||||
|
||||
if lang.src != "" {
|
||||
logFields = append(logFields, zap.String("imported", lang.src))
|
||||
} else {
|
||||
logFields = append(logFields, zap.Bool("embedded", true))
|
||||
}
|
||||
|
||||
if err = loadTranslations(lang); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if svc.set[lang.Tag] != nil {
|
||||
svc.log.Info(
|
||||
"language overloaded",
|
||||
logFields...,
|
||||
)
|
||||
} else {
|
||||
svc.log.Info(
|
||||
"language loaded",
|
||||
logFields...,
|
||||
)
|
||||
}
|
||||
|
||||
// Preserve current resource translations.
|
||||
// Resource translations are updated when they change so these are
|
||||
// already up to date.
|
||||
if existing, ok := svc.set[lang.Tag]; ok {
|
||||
lang.resources = existing.resources
|
||||
}
|
||||
|
||||
svc.set[lang.Tag] = lang
|
||||
}
|
||||
|
||||
svc.log.Debug("languages loaded",
|
||||
zap.Strings("path", svc.src),
|
||||
zap.Int("count", len(svc.set)),
|
||||
)
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
if svc.def == nil {
|
||||
first := svc.tags[0]
|
||||
|
||||
if svc.set[first] == nil {
|
||||
return fmt.Errorf("could not find %s in loaded languages", first)
|
||||
}
|
||||
|
||||
svc.def = svc.set[first]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadResourceTranslations all language configurations (as configured via path options) and
|
||||
// all translation files
|
||||
func (svc *service) ReloadResourceTranslations(ctx context.Context) (err error) {
|
||||
if svc.s == nil {
|
||||
return fmt.Errorf("store for locale service not set")
|
||||
}
|
||||
|
||||
svc.l.RLock()
|
||||
defer svc.l.RUnlock()
|
||||
|
||||
svc.log.Info("reloading resource translations",
|
||||
zap.Strings("tags", tagsToStrings(svc.tags)),
|
||||
)
|
||||
|
||||
for _, tag := range svc.tags {
|
||||
lang, ok := svc.set[tag]
|
||||
if !ok {
|
||||
lang = &Language{
|
||||
Tag: tag,
|
||||
// @todo find a better name for this, because it's not the
|
||||
// language that it's loaded from the store, the resource translations are
|
||||
src: "store",
|
||||
}
|
||||
|
||||
svc.set[tag] = lang
|
||||
}
|
||||
|
||||
if lang.resources, err = svc.LoadResourceTranslations(ctx, lang.Tag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.log.Info(
|
||||
"resource translations loaded",
|
||||
zap.Stringer("tag", lang.Tag),
|
||||
zap.String("src", lang.src),
|
||||
zap.Int("translations", len(lang.resources)),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns list of all languages
|
||||
func (svc *service) List() []*Language {
|
||||
svc.l.RLock()
|
||||
defer svc.l.RUnlock()
|
||||
|
||||
ll := make([]*Language, 0, len(svc.set))
|
||||
for _, tag := range svc.tags {
|
||||
if svc.set[tag] != nil {
|
||||
ll = append(ll, svc.set[tag])
|
||||
}
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
// List localized list of all languages
|
||||
func (svc *service) LocalizedList(ctx context.Context) []*Language {
|
||||
svc.l.RLock()
|
||||
defer svc.l.RUnlock()
|
||||
|
||||
var (
|
||||
l *Language
|
||||
ll = make([]*Language, 0, len(svc.set))
|
||||
reqLang = GetAcceptLanguageFromContext(ctx)
|
||||
)
|
||||
|
||||
for _, tag := range svc.tags {
|
||||
l = svc.set[tag]
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
l = &Language{
|
||||
Tag: l.Tag,
|
||||
Name: l.Name,
|
||||
}
|
||||
|
||||
if nl := display.Languages(reqLang); nl != nil {
|
||||
l.LocalizedName = nl.Name(l.Tag)
|
||||
}
|
||||
|
||||
if l.Name == "" {
|
||||
if nl := display.Languages(l.Tag); nl != nil {
|
||||
// for cases when language name is not configured
|
||||
l.Name = nl.Name(l.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
ll = append(ll, l)
|
||||
}
|
||||
|
||||
return ll
|
||||
}
|
||||
|
||||
func (svc *service) HasLanguage(lang language.Tag) bool {
|
||||
return svc.set[lang] != nil
|
||||
}
|
||||
|
||||
func (svc *service) HasApplication(lang language.Tag, app string) bool {
|
||||
return svc.set[lang] != nil && svc.set[lang].external[app] != nil
|
||||
}
|
||||
|
||||
// EncodeExternal writes into
|
||||
func (svc *service) EncodeExternal(w io.Writer, app string, ll ...language.Tag) (err error) {
|
||||
svc.l.RLock()
|
||||
defer svc.l.RUnlock()
|
||||
|
||||
_, _ = fmt.Fprint(w, "{")
|
||||
for i, lang := range ll {
|
||||
if i > 0 {
|
||||
_, _ = fmt.Fprint(w, ",")
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%q:", lang)
|
||||
if svc.HasApplication(lang, app) {
|
||||
svc.set[lang].external[app].Seek(0, 0)
|
||||
_, _ = io.Copy(w, svc.set[lang].external[app])
|
||||
} else {
|
||||
_, _ = fmt.Fprint(w, "null")
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "}")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// NS returns translator function for the current language (from context) and a specific namespace
|
||||
//
|
||||
// Language is picked from the context
|
||||
func (svc *service) NS(ctx context.Context, ns string) func(key string, rr ...string) string {
|
||||
var (
|
||||
tag = GetAcceptLanguageFromContext(ctx)
|
||||
)
|
||||
|
||||
return func(key string, rr ...string) string {
|
||||
return svc.t(tag, ns, key, rr...)
|
||||
}
|
||||
}
|
||||
|
||||
// T returns translated key from namespaces using list of replacement pairs
|
||||
//
|
||||
// Language is picked from the context
|
||||
func (svc *service) T(ctx context.Context, ns, key string, rr ...string) string {
|
||||
return svc.t(GetAcceptLanguageFromContext(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...)
|
||||
}
|
||||
|
||||
// TResource returns translated key for resource using list of replacement pairs
|
||||
//
|
||||
// Language is picked from the context
|
||||
func (svc *service) TResource(ctx context.Context, ns, key string, rr ...string) string {
|
||||
|
||||
return svc.tResource(GetAcceptLanguageFromContext(ctx), ns, key, rr...)
|
||||
}
|
||||
|
||||
// TResourceFor returns translated key for resource using list of replacement pairs
|
||||
//
|
||||
// Language is picked from the context
|
||||
func (svc *service) TResourceFor(tag language.Tag, ns, key string, rr ...string) string {
|
||||
return svc.tResource(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(tag language.Tag, resource string) ResourceTranslationIndex {
|
||||
out := make(ResourceTranslationIndex)
|
||||
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
if svc != nil && svc.set != nil {
|
||||
if l, has := svc.set[tag]; has {
|
||||
return l.resourceTranslations(resource)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Finds language and uses it to translate the given key
|
||||
func (svc *service) t(tag language.Tag, ns, key string, rr ...string) string {
|
||||
if svc != nil && svc.set != nil {
|
||||
if l, has := svc.set[tag]; has {
|
||||
return l.t(ns, key, rr...)
|
||||
}
|
||||
}
|
||||
|
||||
// In case of a missing language, translation
|
||||
// key is returned
|
||||
return key
|
||||
}
|
||||
|
||||
// Finds language and uses it to translate the given key for resource
|
||||
func (svc *service) tResource(tag language.Tag, ns, key string, rr ...string) string {
|
||||
if svc != nil && svc.set != nil {
|
||||
if l, has := svc.set[tag]; has {
|
||||
return l.tResource(ns, key, rr...)
|
||||
}
|
||||
}
|
||||
|
||||
// In case of a missing language, only empty string is
|
||||
// returned (in contrast to static translations
|
||||
// where a string with key is returned)
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasTag(t language.Tag, tt []language.Tag) bool {
|
||||
for _, tag := range tt {
|
||||
if tag.String() == t.String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func tagsToStrings(tt []language.Tag) []string {
|
||||
out := make([]string, len(tt))
|
||||
for t := range tt {
|
||||
out[t] = tt[t].String()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user