249 lines
5.5 KiB
Go
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)
|
|
}
|