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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
PATH_ROOT := ../../..
PATH_INTERNAL := $(PATH_ROOT)/locale
VERSION := 2022.9.x
EXTERNAL_PKG := corteza-locale-$(VERSION).zip
EXTERNAL_DIR := corteza-locale-$(VERSION)
# Default task
copy-english-from-root-locale-dir: src/en
# Copy internal language files (english) to pkg/locale/src
#
# This way languages will be embedded into the build
src/en:
rm -rf $*
cp -r $(PATH_INTERNAL)/en ./src
# Catch-all for external locales
src/%: unpack-external
cp -r $(EXTERNAL_DIR)/src/$* ./src
unpack-external: $(EXTERNAL_DIR)
$(EXTERNAL_DIR): $(EXTERNAL_PKG)
unzip $@
$(EXTERNAL_PKG):
curl -L https://github.com/cortezaproject/corteza-locale/archive/refs/heads/$(VERSION).zip -o $@
clean:
rm -rf $(EXTERNAL_PKG) $(EXTERNAL_DIR)

View File

@@ -0,0 +1,35 @@
package locale
import (
"context"
"golang.org/x/text/language"
)
func SetAcceptLanguageToContext(ctx context.Context, code language.Tag) context.Context {
return context.WithValue(ctx, language.Tag{}, code)
}
// GetAcceptLanguageFromContext always returns language, either valid or default
func GetAcceptLanguageFromContext(ctx context.Context) language.Tag {
if tag, ok := ctx.Value(language.Tag{}).(language.Tag); ok {
return tag
} else {
return defaultLanguage
}
}
type contentLanguageCtx struct{}
func SetContentLanguageToContext(ctx context.Context, code language.Tag) context.Context {
return context.WithValue(ctx, contentLanguageCtx{}, code)
}
// GetContentLanguageFromContext always returns language, either valid or default
func GetContentLanguageFromContext(ctx context.Context) language.Tag {
if tag, ok := ctx.Value(contentLanguageCtx{}).(language.Tag); ok {
return tag
} else {
return language.Und
}
}

View File

@@ -0,0 +1,20 @@
package locale
// /////////////////////////////////////////////////////////////////////////////
// This helps us import translations into corteza-server as a module
// dependency
import (
"embed"
"io/fs"
)
//go:embed src/*
var languages embed.FS
// LoadEmbedded returns embedded translation files
// as a virtual filesystem
func LoadEmbedded() fs.FS {
sub, _ := fs.Sub(languages, "src")
return sub
}

132
server/pkg/locale/http.go Normal file
View File

@@ -0,0 +1,132 @@
package locale
import (
"net/http"
"go.uber.org/zap"
"golang.org/x/text/language"
)
const AcceptLanguageHeader = "Accept-Language"
const ContentLanguageHeader = "Content-Language"
func DetectLanguage(ll *service) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx = r.Context()
// resolve accept-language header
// Accept-Language specifies the language of the response payload.
ctx = SetAcceptLanguageToContext(ctx, resolveAcceptLanguageHeaders(r, ll))
// resolve content-language header
// Content-Language specifies the language of the request payload.
ctx = SetContentLanguageToContext(ctx, resolveContentLanguageHeaders(r.Header, ll))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// reads Content-Language headers from the request and returns
// parsed value as language.Tag
//
// There are 4 valid scenarios for :
// - lang == 'skip': returns und & services will (likely) ignore all translatable content
// - invalid language: (same as skip)
// - valid language: returns valid language; services will treat translatable content from the payload as translations
// - no header: returns default language; (same as valid language)
func resolveContentLanguageHeaders(h http.Header, ll *service) language.Tag {
if ll == nil || ll.Default() == nil {
return language.Und
}
var cLang = h.Get(ContentLanguageHeader)
if cLang == "" {
return ll.Default().Tag
}
if cLang == "skip" {
// more than 1 header or value equal to skip
return language.Und
}
if tag, err := language.Parse(cLang); err != nil {
return language.Und
} else {
return tag
}
}
func resolveAcceptLanguageHeaders(r *http.Request, ll *service) (tag language.Tag) {
if ll == nil || ll.Default() == nil {
// locale service does not have anything loaded...
return language.Und
}
if ll.opt.DevelopmentMode {
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
}
}
var (
raw string
)
// try to detect the language from the request's query string:
if ll.opt.QueryStringParam != "" {
raw = r.URL.Query().Get(ll.opt.QueryStringParam)
}
// try to detect the language from the request's headers:
if len(raw) == 0 {
raw = r.Header.Get(AcceptLanguageHeader)
}
if len(raw) == 0 {
// no need for lang detection
return ll.Default().Tag
}
// parse & ignore errors
var (
preferred = ll.Default().Tag
supported = ll.Tags()
accepted, _, err = language.ParseAcceptLanguage(raw)
)
if err == nil {
// ignoring index & confidence
preferred, _, _ = language.NewMatcher(supported).Match(accepted...)
var match bool
for _, s := range supported {
if s == preferred {
match = true
break
}
}
if !match {
base, _ := preferred.Base()
preferred = language.MustParse(base.String())
}
}
if ll.opt.DevelopmentMode {
ll.log.Debug(
"language detected",
zap.String("preferred", preferred.String()),
zap.String("raw", raw),
)
}
// new request with new context
return preferred
}

View File

@@ -0,0 +1,9 @@
package locale
import (
"github.com/cortezaproject/corteza/server/pkg/xss"
)
func SanitizeMessage(in string) string {
return xss.RichText(in)
}

View File

@@ -0,0 +1,28 @@
package locale
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_SanitizeMessage(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{"simple", "abc", "abc"},
{"accents", "čšž", "čšž"},
{"safe html", "<b>čšž</b>", "<b>čšž</b>"},
{"unsafe html", `<a href="javascript:document.location='https://cortezaproject.org/'">XSS</A>`, "XSS"},
{"regular link", `<a href="https://cortezaproject.org/">home</a>`, `<a href="https://cortezaproject.org/" rel="nofollow">home</a>`},
{"link with target", `<a href="https://cortezaproject.org/" target="_blank">home</a>`, `<a href="https://cortezaproject.org/" target="_blank" rel="nofollow noopener">home</a>`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.out, SanitizeMessage(tt.in))
})
}
}

248
server/pkg/locale/load.go Normal file
View File

@@ -0,0 +1,248 @@
package locale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/fs"
"path"
"path/filepath"
"strings"
"golang.org/x/text/language"
"gopkg.in/yaml.v3"
)
const serverApplication = "corteza-server"
var (
defaultLanguage = language.English
)
func loadConfigs(fsys fs.FS) (ll []*Language, err error) {
var (
sub fs.FS
lang *Language
configs = make([]string, 0)
)
configs, err = fs.Glob(fsys, "*/config.yaml")
if err != nil {
return nil, err
}
for _, config := range configs {
if sub, err = fs.Sub(fsys, filepath.Dir(config)); err != nil {
return nil, err
}
if lang, err = loadConfig(sub, filepath.Base(config)); err != nil {
return nil, err
}
lang.Tag = language.Make(filepath.Base(filepath.Dir(config)))
lang.src = filepath.Dir(config)
ll = append(ll, lang)
}
return ll, nil
}
func loadConfig(fsys fs.FS, p string) (lang *Language, err error) {
var f fs.File
lang = &Language{
Tag: language.Make(filepath.Base(filepath.Dir(p))),
fs: fsys,
}
f, err = fsys.Open(p)
if err != nil {
return nil, fmt.Errorf("could not read %s: %v", p, err)
}
defer f.Close()
if err = yaml.NewDecoder(f).Decode(&lang); err != nil {
return nil, fmt.Errorf("could not decode %s: %v", p, err)
}
return
}
func loadTranslations(lang *Language) (err error) {
lang.internal = make(internal)
lang.external = make(external)
var (
// this will help us collect structured keys from YAML files
// for external translations
auxExternal = make(map[string]map[string]map[string]interface{})
)
err = fs.WalkDir(lang.fs, ".", func(p string, entry fs.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return err
}
var (
f fs.File
firstSep = strings.Index(p, string(filepath.Separator))
)
if firstSep == -1 {
return nil
}
// From this point on, we're assuming we are at least 1 level deep
// and the name of the 1st directory is the name of the application
// that the content is for
var (
namespace string
keyPath string
ext = path.Ext(p)
appDir = p[:firstSep]
subpath = p[len(appDir)+1:]
nsDirIndex = strings.Index(subpath, string(filepath.Separator))
nsDotIndex = strings.LastIndex(subpath, ".")
isYAML = ext == ".yaml" || ext == ".yml"
isJSON = ext == ".json"
)
if !isYAML && !isJSON {
// unsupported format, skipping
return nil
}
if appDir == serverApplication && isJSON {
return fmt.Errorf("expecting YAML translation files for server translations")
}
// namespace is whatever we put on the 1st level - either filename or directory
if nsDirIndex > 0 {
namespace = subpath[:nsDirIndex]
keyPath = subpath[nsDirIndex+1:]
keyPath = keyPath[:len(keyPath)-len(ext)]
keyPath = strings.ReplaceAll(keyPath, string(filepath.Separator), ".")
} else if nsDotIndex > 0 {
namespace = subpath[:nsDotIndex]
}
if f, err = lang.fs.Open(p); err != nil {
return fmt.Errorf("could not open %s: %w", p, err)
}
defer f.Close()
if appDir == serverApplication {
if lang.internal[namespace] == nil {
lang.internal[namespace] = make(map[string]string)
}
if err = procInternal(lang.internal[namespace], keyPath, f); err != nil {
return fmt.Errorf("could not process %s: %v", p, err)
}
} else {
if isJSON {
if lang.external[appDir] != nil {
return fmt.Errorf("only one JSON file per namespace supported")
}
var buf = &bytes.Buffer{}
if _, err = io.Copy(buf, f); err != nil {
return fmt.Errorf("could not copy buffer: %v", err)
}
lang.external[appDir] = bytes.NewReader(buf.Bytes())
} else {
if auxExternal[appDir] == nil {
auxExternal[appDir] = make(map[string]map[string]interface{})
}
if auxExternal[appDir][namespace] == nil {
auxExternal[appDir][namespace] = make(map[string]interface{})
}
if err = procExternal(auxExternal[appDir][namespace], keyPath, f); err != nil {
return fmt.Errorf("could not process %s: %v", p, err)
}
}
}
return err
})
if err != nil {
return
}
// encode all external (for webapps, clientS) translations
// into buffers that will be copied directly into responses
for app, nss := range auxExternal {
var (
buf = &bytes.Buffer{}
)
if err = json.NewEncoder(buf).Encode(nss); err != nil {
return
}
lang.external[app] = bytes.NewReader(buf.Bytes())
}
return
}
func (svc *service) LoadResourceTranslations(ctx context.Context, tg language.Tag) (out map[string]map[string]*ResourceTranslation, err error) {
return svc.s.TransformResource(ctx, tg)
}
// 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) {
var (
aux = make(map[string]interface{})
// this allows us to do recursive calls to lambda
flatten func(prefix string, sub map[string]interface{})
)
if err = yaml.NewDecoder(f).Decode(&aux); err != nil {
return
}
flatten = func(prefix string, sub map[string]interface{}) {
if len(prefix) > 0 {
prefix += "."
}
for k, v := range sub {
switch val := v.(type) {
case string:
ns[prefix+k] = val
case map[string]interface{}:
flatten(prefix+k, val)
}
}
}
flatten(prefix, aux)
return
}
func procExternal(kv map[string]interface{}, prefix string, f io.Reader) error {
// get to the level we want to be:
if len(prefix) > 0 {
for _, p := range strings.Split(prefix, ".") {
if kv[p] == nil {
kv[p] = make(map[string]interface{})
kv = kv[p].(map[string]interface{})
}
}
}
return yaml.NewDecoder(f).Decode(&kv)
}

126
server/pkg/locale/locale.go Normal file
View File

@@ -0,0 +1,126 @@
package locale
import (
"fmt"
"io"
"io/fs"
"strings"
"sync"
"golang.org/x/text/language"
)
type (
// Internal translations (application == corteza-server)
// map contains namespace => key => text
internal map[string]map[string]string
// External translations (webapps)
// map structure:
// application => namespace => buffered JSON docs
external map[string]io.ReadSeeker
// resource => key => ResourceTranslation
resources map[string]map[string]*ResourceTranslation
Language struct {
l sync.RWMutex
// location of the language files
// this is mainly for logging/debugging purposes
//
// This value is empty when loading
// embedded languages
src string
// pointer to the place we loaded files from
fs fs.FS
Tag language.Tag `json:"tag,string"`
Name string `json:"name"`
LocalizedName string `json:"localizedName"`
// Language (tag) that this language is extending
// @todo
Extends language.Tag `json:"-"`
// Points to extended language
// @todo
extends *Language
// Internal translations (application == corteza-server)
internal internal
// External translations (webapps)
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()
msg, has := l.internal[ns][key]
if has {
for r := 0; r < len(rr); r += 2 {
rr[r] = fmt.Sprintf("{{%s}}", rr[r])
}
return strings.NewReplacer(rr...).Replace(msg)
}
// If translation is not found, try with language
// this language extends
if l.extends != nil {
return l.extends.t(ns, key, rr...)
}
// In case of untranslated string / missing translations,
// translation key is returned
return key
}
// tResource returns the translated string for resource translations
func (l *Language) tResource(ns, key string, rr ...string) string {
l.l.RLock()
defer l.l.RUnlock()
rt, has := l.resources[ns][key]
if has {
for r := 0; r < len(rr); r += 2 {
rr[r] = fmt.Sprintf("{{%s}}", rr[r])
}
return strings.NewReplacer(rr...).Replace(rt.Msg)
}
// When translating resources we do not utilize
// language extensions like with static translations
// In case of untranslated string / missing translations,
// only empty string is returned (in contrast to static translations
// where a string with key is returned
return ""
}
// resourceTranslations returns all resource translations for the specified resource
func (l *Language) resourceTranslations(resource string) ResourceTranslationIndex {
l.l.RLock()
defer l.l.RUnlock()
out := make(ResourceTranslationIndex)
if l.resources == nil {
return out
}
for k, rt := range l.resources[resource] {
out[k] = rt
}
return out
}

View File

@@ -0,0 +1,47 @@
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:"message"`
}
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) {
str := tag.String()
for _, r := range rr {
r.Lang = str
}
}
// Returns true if resource translation set contains foreign (non-native) languages
func (rr ResourceTranslationSet) ContainsForeign(native language.Tag) bool {
str := native.String()
for _, r := range rr {
if r.Lang != str {
return true
}
}
return false
}
func (rx ResourceTranslationIndex) FindByKey(k string) *ResourceTranslation {
return rx[k]
}

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

View File

@@ -0,0 +1,28 @@
package locale
import (
"testing"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/text/language"
)
// tested with
// go test -count 10 -race -run TestServiceReloadAndTranslate ./pkg/locale/...
func TestServiceReloadAndTranslate(t *testing.T) {
var (
req = require.New(t)
svc, err = Service(zap.NewNop(), options.LocaleOpt{
Languages: "en",
})
tag = language.English
)
req.NoError(err)
req.NotNil(svc)
go svc.ResourceTranslations(tag, "resource")
go svc.ReloadStatic()
}

3
server/pkg/locale/src/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!Readme.md