diff --git a/testdata/translator/d/invalid.json b/testdata/translator/d/invalid.json new file mode 100644 index 0000000..910ecaa --- /dev/null +++ b/testdata/translator/d/invalid.json @@ -0,0 +1 @@ +{"fk":"fv"} \ No newline at end of file diff --git a/testdata/translator/en.json b/testdata/translator/en.json new file mode 100644 index 0000000..2c57cff --- /dev/null +++ b/testdata/translator/en.json @@ -0,0 +1 @@ +{"1":"1","2":{"3":"3"}} \ No newline at end of file diff --git a/testdata/translator/fr.json b/testdata/translator/fr.json new file mode 100644 index 0000000..38213ca --- /dev/null +++ b/testdata/translator/fr.json @@ -0,0 +1 @@ +{"4":"4"} \ No newline at end of file diff --git a/testdata/translator/invalid.csv b/testdata/translator/invalid.csv new file mode 100644 index 0000000..e69de29 diff --git a/translator.go b/translator.go new file mode 100644 index 0000000..6287210 --- /dev/null +++ b/translator.go @@ -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) +} diff --git a/translator_test.go b/translator_test.go new file mode 100644 index 0000000..a8ebad2 --- /dev/null +++ b/translator_test.go @@ -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) + } + } +}