Added translator

This commit is contained in:
Quentin Renard
2020-02-03 17:21:31 +01:00
parent 2d5f7518f4
commit 3833bbfde2
6 changed files with 249 additions and 0 deletions

1
testdata/translator/d/invalid.json vendored Normal file
View File

@@ -0,0 +1 @@
{"fk":"fv"}

1
testdata/translator/en.json vendored Normal file
View File

@@ -0,0 +1 @@
{"1":"1","2":{"3":"3"}}

1
testdata/translator/fr.json vendored Normal file
View File

@@ -0,0 +1 @@
{"4":"4"}

0
testdata/translator/invalid.csv vendored Normal file
View File

171
translator.go Normal file
View File

@@ -0,0 +1,171 @@
package astikit
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
)
// Translator represents an object capable of translating stuff
type Translator struct {
m *sync.RWMutex // Lock p
o TranslatorOptions
p map[string]string
}
// TranslatorOptions represents Translator options
type TranslatorOptions struct {
DefaultLanguage string
}
// NewTranslator creates a new Translator
func NewTranslator(o TranslatorOptions) *Translator {
return &Translator{
m: &sync.RWMutex{},
o: o,
p: make(map[string]string),
}
}
// ParseDir adds translations located in ".json" files in the specified dir
func (t *Translator) ParseDir(dirPath string) (err error) {
// Default dir path
if dirPath == "" {
if dirPath, err = os.Getwd(); err != nil {
err = fmt.Errorf("astikit: getwd failed: %w", err)
return
}
}
// Walk through dir
if err = filepath.Walk(dirPath, func(path string, info os.FileInfo, e error) (err error) {
// Check input error
if e != nil {
err = fmt.Errorf("astikit: walking %s has an input error for path %s: %w", dirPath, path, e)
return
}
// Only process first level files
if info.IsDir() {
if path != dirPath {
err = filepath.SkipDir
}
return
}
// Only process ".json" files
if filepath.Ext(path) != ".json" {
return
}
// Parse file
if err = t.ParseFile(path); err != nil {
err = fmt.Errorf("astikit: parsing %s failed: %w", path, err)
return
}
return
}); err != nil {
err = fmt.Errorf("astikit: walking %s failed: %w", dirPath, err)
return
}
return
}
// ParseFile adds translation located in the provided path
func (t *Translator) ParseFile(path string) (err error) {
// Lock
t.m.Lock()
defer t.m.Unlock()
// Open file
var f *os.File
if f, err = os.Open(path); err != nil {
err = fmt.Errorf("astikit: opening %s failed: %w", path, err)
return
}
defer f.Close()
// Unmarshal
var p map[string]interface{}
if err = json.NewDecoder(f).Decode(&p); err != nil {
err = fmt.Errorf("astikit: unmarshaling %s failed: %w", path, err)
return
}
// Parse
t.parse(p, strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)))
return
}
func (t *Translator) key(prefix, key string) string {
return prefix + "." + key
}
func (t *Translator) parse(i map[string]interface{}, prefix string) {
for k, v := range i {
p := t.key(prefix, k)
switch a := v.(type) {
case string:
t.p[p] = a
case map[string]interface{}:
t.parse(a, p)
}
}
}
// HTTPMiddleware is the Translator HTTP middleware
func (t *Translator) HTTPMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// Store language in context
if l := r.Header.Get("Accept-Language"); l != "" {
*r = *r.WithContext(contextWithTranslatorLanguage(r.Context(), l))
}
// Next handler
h.ServeHTTP(rw, r)
})
}
const contextKeyTranslatorLanguage = "astikit.translator.language"
func contextWithTranslatorLanguage(ctx context.Context, language string) context.Context {
return context.WithValue(ctx, contextKeyTranslatorLanguage, language)
}
func translatorLanguageFromContext(ctx context.Context) string {
v, ok := ctx.Value(contextKeyTranslatorLanguage).(string)
if !ok {
return ""
}
return v
}
// Translate translates a key into a specific language
func (t *Translator) Translate(language, key string) string {
// Lock
t.m.RLock()
defer t.m.RUnlock()
// Default language
if language == "" {
language = t.o.DefaultLanguage
}
// Get translation
k := t.key(language, key)
v, ok := t.p[k]
if !ok {
return k
}
return v
}
// TranslateCtx translates a key using the language specified in the context
func (t *Translator) TranslateCtx(ctx context.Context, key string) string {
return t.Translate(translatorLanguageFromContext(ctx), key)
}

75
translator_test.go Normal file
View File

@@ -0,0 +1,75 @@
package astikit
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
func TestTranslator(t *testing.T) {
// Setup
tl := NewTranslator(TranslatorOptions{DefaultLanguage: "fr"})
// Parse dir
err := tl.ParseDir("testdata/translator")
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if e := map[string]string{
"en.1": "1",
"en.2.3": "3",
"fr.4": "4",
}; !reflect.DeepEqual(e, tl.p) {
t.Errorf("expected %+v, got %+v", e, tl.p)
}
// Middleware
var o string
s := httptest.NewServer(ChainHTTPMiddlewares(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
o = tl.TranslateCtx(r.Context(), r.Header.Get("key"))
}), tl.HTTPMiddleware))
defer s.Close()
// Translate
for _, v := range []struct {
expected string
key string
language string
}{
{
expected: "4",
key: "4",
},
{
expected: "fr.1",
key: "1",
},
{
expected: "3",
key: "2.3",
language: "en",
},
{
expected: "en.4",
key: "4",
language: "en",
},
} {
r, err := http.NewRequest(http.MethodGet, s.URL, nil)
if err != nil {
t.Errorf("expected no error, got %+v", err)
}
r.Header.Set("key", v.key)
if v.language != "" {
r.Header.Set("Accept-Language", v.language)
}
_, err = http.DefaultClient.Do(r)
if err != nil {
t.Errorf("expected no error, got %+v", err)
}
if !reflect.DeepEqual(v.expected, o) {
t.Errorf("expected %+v, got %+v", v.expected, o)
}
}
}