3
0
2022-11-14 09:26:39 +01:00

249 lines
5.5 KiB
Go

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