Skip to content

Commit

Permalink
Move messages to message provider
Browse files Browse the repository at this point in the history
  • Loading branch information
fullpipe committed Oct 30, 2024
1 parent b253d7c commit 5c1c110
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 176 deletions.
7 changes: 2 additions & 5 deletions example/base/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ func main() {
mf.WithLangFallback(language.BritishEnglish, language.English),
mf.WithLangFallback(language.Portuguese, language.Spanish),

mf.WithFSProvider(messagesDir),

mf.WithErrorHandler(func(err error, id string, ctx map[string]any) {
slog.Error(err.Error(), slog.String("id", id), slog.Any("ctx", ctx))

Expand All @@ -32,11 +34,6 @@ func main() {
log.Fatal(err)
}

err = bundle.LoadDir(messagesDir)
if err != nil {
log.Fatal(err)
}

tr := bundle.Translator("en")

slog.Info(tr.Trans("title", mf.Arg("lang", "en")))
Expand Down
140 changes: 42 additions & 98 deletions mf/bundle.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
package mf

import (
"fmt"
"io"
"io/fs"
"path"
"strings"

"github.com/pkg/errors"
"golang.org/x/text/language"
)

type Bundle interface {
LoadMessages(rd fs.FS, path string, lang language.Tag) error
LoadDir(dir fs.FS) error
Translator(lang string) Translator
}

type bundle struct {
fallbacks map[language.Tag]language.Tag
translators map[language.Tag]Translator
dictionaries map[language.Tag]Dictionary
provider Provider

defaultLang language.Tag
defaultErrorHandler ErrorHandler
}

type ErrorHandler func(err error, id string, ctx map[string]any)

type BundleOption func(b *bundle)
type BundleOption func(b *bundle) error

func NewBundle(options ...BundleOption) (Bundle, error) {
bundle := &bundle{
Expand All @@ -41,64 +36,19 @@ func NewBundle(options ...BundleOption) (Bundle, error) {
}

for _, option := range options {
option(bundle)
}

// TODO: check fallbacks for cicles en -> es -> en -> ...

return bundle, nil
}

func (b *bundle) LoadMessages(rd fs.FS, path string, lang language.Tag) error {
yamlFile, err := rd.Open(path)
if err != nil {
return errors.Wrap(err, "unable to open file")
}

yamlData, err := io.ReadAll(yamlFile)
if err != nil {
return errors.Wrap(err, "unable to read file")
}

_, hasDictionary := b.dictionaries[lang]
if hasDictionary {
return fmt.Errorf("unable to load %s: language %s already has messages loaded", path, lang)
}

b.dictionaries[lang], err = NewDictionary(yamlData)
if err != nil {
return errors.Wrap(err, "unable to create dictionary")
}

return nil
}

func (b *bundle) LoadDir(dir fs.FS) error {
return fs.WalkDir(dir, ".", func(p string, f fs.DirEntry, err error) error {
err := option(bundle)
if err != nil {
return err
}

if f.IsDir() {
return nil
}

if path.Ext(f.Name()) != ".yaml" && path.Ext(f.Name()) != ".yml" {
return nil
return nil, err
}
}

nameParts := strings.Split(f.Name(), ".")
if len(nameParts) < 2 {
return fmt.Errorf("no lang in file %s", f.Name())
}
if bundle.provider == nil {
return nil, errors.New("you have add message provider with WithFSProvider or WithProvider")
}

tag, err := language.Parse(nameParts[len(nameParts)-2])
if err != nil {
return errors.Wrap(err, "unable to parse language from filename")
}
// TODO: check fallbacks for cicles en -> es -> en -> ...

return b.LoadMessages(dir, p, tag)
})
return bundle, nil
}

func (b *bundle) Translator(lang string) Translator {
Expand All @@ -123,73 +73,67 @@ func (b *bundle) getTranlator(tag language.Tag) Translator {
return tr
}

dictionary, hasDictionary := b.dictionaries[tag]
var fallback Translator
fallbackTag, hasFallback := b.fallbacks[tag]

if hasDictionary {
var fallback Translator

if hasFallback {
fallback = b.getTranlator(fallbackTag)
} else if tag != b.defaultLang {
fallback = b.getTranlator(b.defaultLang)
}

return &translator{
dictionary: dictionary,
fallback: fallback,
errorHandler: b.defaultErrorHandler,
lang: tag,
}
}

if hasFallback {
return b.getTranlator(fallbackTag)
}

tr, ok = b.translators[b.defaultLang]
if ok {
return tr
}

dictionary, hasDictionary = b.dictionaries[b.defaultLang]
if !hasDictionary {
dictionary = &dummyDictionary{}
fallback = b.getTranlator(fallbackTag)
} else if tag != b.defaultLang {
fallback = b.getTranlator(b.defaultLang)
}

return &translator{
dictionary: dictionary,
provider: b.provider,
fallback: fallback,
errorHandler: b.defaultErrorHandler,
lang: tag,
}
}

func WithDefaulLangFallback(l language.Tag) BundleOption {
return func(b *bundle) {
return func(b *bundle) error {
b.defaultLang = l

return nil
}
}

func LoadDictionariesFromFS(dir fs.FS) BundleOption {
return func(b *bundle) {
b.LoadDir(dir)
func WithFSProvider(dir fs.FS) BundleOption {
return func(b *bundle) error {
provider, err := NewFSProvider(dir)
b.provider = provider

return err
}
}

func WithProvider(provider Provider) BundleOption {
return func(b *bundle) error {
b.provider = provider

return nil
}
}

func WithDictionary(lang language.Tag, d Dictionary) BundleOption {
return func(b *bundle) {
return func(b *bundle) error {
b.dictionaries[lang] = d

return nil
}
}

func WithLangFallback(from language.Tag, to language.Tag) BundleOption {
return func(b *bundle) {
return func(b *bundle) error {
b.fallbacks[from] = to

return nil
}
}

func WithErrorHandler(handler ErrorHandler) BundleOption {
return func(b *bundle) {
return func(b *bundle) error {
b.defaultErrorHandler = handler

return nil
}
}
83 changes: 14 additions & 69 deletions mf/bundle_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package mf

import (
"io/fs"
"reflect"
"runtime"
"testing"
"testing/fstest"

"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
Expand All @@ -16,6 +16,7 @@ func TestNewBundle(t *testing.T) {
b, err := NewBundle(
WithDefaulLangFallback(language.English),
WithLangFallback(language.Portuguese, language.Spanish),
WithProvider(new(MockedProvider)),
)

require.NoError(t, err)
Expand Down Expand Up @@ -71,81 +72,25 @@ func TestWithErrorHandler(t *testing.T) {
assert.Equal(t, funcName1, funcName2)
}

func TestBundle_LoadMessages(t *testing.T) {
b := &bundle{
fallbacks: map[language.Tag]language.Tag{},
translators: map[language.Tag]Translator{},
dictionaries: map[language.Tag]Dictionary{},

defaultLang: language.Und,
defaultErrorHandler: func(_ error, _ string, _ map[string]any) {},
}

fs := fstest.MapFS{
"non_readable": {
Mode: fs.ModeDir,
},
"foo.en.yaml": {
Data: []byte("foo: bar"),
},
}

require.Error(t, b.LoadMessages(fs, "file_not_exists.yaml", language.English))
assert.Nil(t, b.dictionaries[language.English])

require.Error(t, b.LoadMessages(fs, "non_readable", language.English))
assert.Nil(t, b.dictionaries[language.English])

require.NoError(t, b.LoadMessages(fs, "foo.en.yaml", language.English))
assert.NotNil(t, b.dictionaries[language.English])

require.Error(t, b.LoadMessages(fs, "foo.en.yaml", language.English), "error when lang already loaded")
assert.NotNil(t, b.dictionaries[language.English])
}

func TestBundle_LoadDir(t *testing.T) {
b := &bundle{
fallbacks: map[language.Tag]language.Tag{},
translators: map[language.Tag]Translator{},
dictionaries: map[language.Tag]Dictionary{},

defaultLang: language.Und,
defaultErrorHandler: func(_ error, _ string, _ map[string]any) {},
}

require.NoError(t, b.LoadDir(fstest.MapFS{"dir.en": {Mode: fs.ModeDir}}), "no error on empty fs")
assert.Nil(t, b.dictionaries[language.English], "does not loads dirs")

require.NoError(t, b.LoadDir(fstest.MapFS{"messages.en.toml": {Data: []byte("foo=bar")}}), "no error on non yaml files")
assert.Nil(t, b.dictionaries[language.English], "but does not loads them")

require.Error(t, b.LoadDir(fstest.MapFS{".yaml": {Data: []byte("foo: bar")}}), "error on invalid filename")
require.Error(t, b.LoadDir(fstest.MapFS{"messages.FOO.yaml": {Data: []byte("foo: bar")}}), "error on invalid lang in filename")

require.NoError(t, b.LoadDir(fstest.MapFS{
"messages.en.yaml": {Data: []byte("foo: bar")},
"messages.es.yaml": {Data: []byte("foo: bar")},
"messages.ru.yml": {Data: []byte("foo: bar")},
}), "no error on normal yaml files")
assert.NotNil(t, b.dictionaries[language.English])
assert.NotNil(t, b.dictionaries[language.Spanish])
assert.NotNil(t, b.dictionaries[language.Russian])
}

func TestBundle_Translator(t *testing.T) {
b, _ := NewBundle()
provider := new(MockedProvider)
b, err := NewBundle(WithProvider(provider))
require.NoError(t, err)
assert.NotNil(t, b.Translator("ru"), "even empty bundle returns some translator")

provider.On("Get", language.Russian, "msg_id").Return("", errors.New("no message")) // call from actual translator
provider.On("Get", language.Und, "msg_id").Return("", errors.New("no message")) // call from fallback translator
assert.Equal(t, "msg_id", b.Translator("ru").Trans("msg_id"), "even empty bundle returns some translator")

b, _ = NewBundle(
b, err = NewBundle(
WithDefaulLangFallback(language.English),
WithLangFallback(language.Portuguese, language.Spanish),
WithFSProvider(fstest.MapFS{
"messages.en.yaml": {Data: []byte("foo: en\nbar_id: enbar")},
"messages.es.yaml": {Data: []byte("foo: es")},
}),
)

require.NoError(t, b.LoadDir(fstest.MapFS{
"messages.en.yaml": {Data: []byte("foo: en\nbar_id: enbar")},
"messages.es.yaml": {Data: []byte("foo: es")},
}))
require.NoError(t, err)

assert.Equal(t, "en", b.Translator("en").Trans("foo"), "en loaded from file")
assert.Equal(t, "en", b.Translator("pl").Trans("foo"), "fallback to defalt lang")
Expand Down
Loading

0 comments on commit 5c1c110

Please sign in to comment.