Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

External dictionary #14

Merged
merged 7 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ bundle, err := mf.NewBundle(
mf.WithLangFallback(language.BritishEnglish, language.English),
mf.WithLangFallback(language.Portuguese, language.Spanish),

// Load all yaml files in directory as messages
mf.WithYamlProvider(messagesDir),

// or you could use your own custom message provider
// mf.WithProvider(sqlMessageProvider),

// We assume that the translated messages are mostly correct.
// However, if any errors occur during translation,
Expand All @@ -77,12 +82,6 @@ bundle, err := mf.NewBundle(
}),
)

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

// Load all yaml files in directory as messages
err = bundle.LoadDir(messagesDir)
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -131,6 +130,8 @@ func main() {
mf.WithLangFallback(language.BritishEnglish, language.English),
mf.WithLangFallback(language.Portuguese, language.Spanish),

mf.WithYamlProvider(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 @@ -140,11 +141,6 @@ func main() {
log.Fatal(err)
}

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

tr := bundle.Translator("es")

slog.Info(tr.Trans("say_hello", mf.Arg("name", "Bob")))
Expand Down
10 changes: 3 additions & 7 deletions example/base/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"embed"
"log"
"log/slog"
"math/rand/v2"

Expand All @@ -20,6 +19,8 @@ func main() {
mf.WithLangFallback(language.BritishEnglish, language.English),
mf.WithLangFallback(language.Portuguese, language.Spanish),

mf.WithYamlProvider(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 @@ -29,12 +30,7 @@ func main() {
)

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

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

tr := bundle.Translator("en")
Expand Down
4 changes: 2 additions & 2 deletions example/base/var/messages.es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ subtitle: >-
}

say:
hello: Hello, {name}!
goodbye: Goodbye, {name}!
hello: Hola, {name}!
goodbye: Adiós, {name}!

user:
profile:
Expand Down
12 changes: 6 additions & 6 deletions message/plural.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (p *Plural) Eval(ctx Context) (string, error) {
}

if p.Offset > 0 {
offset := uint64(p.Offset)
offset := uint64(p.Offset) //nolint: gosec
if offset < np.i {
np.i -= offset
} else {
Expand Down Expand Up @@ -124,27 +124,27 @@ func toPluralForm(num any) (pm, error) {
if i < 0 {
i = -i
}
return pm{i: uint64(i)}, nil
return pm{i: uint64(i)}, nil //nolint: gosec
case int8:
if i < 0 {
i = -i
}
return pm{i: uint64(i)}, nil
return pm{i: uint64(i)}, nil //nolint: gosec
case int16:
if i < 0 {
i = -i
}
return pm{i: uint64(i)}, nil
return pm{i: uint64(i)}, nil //nolint: gosec
case int32:
if i < 0 {
i = -i
}
return pm{i: uint64(i)}, nil
return pm{i: uint64(i)}, nil //nolint: gosec
case int64:
if i < 0 {
i = -i
}
return pm{i: uint64(i)}, nil
return pm{i: uint64(i)}, nil //nolint: gosec
case uint:
return pm{i: uint64(i)}, nil
case uint8:
Expand Down
146 changes: 46 additions & 100 deletions mf/bundle.go
Original file line number Diff line number Diff line change
@@ -1,104 +1,52 @@
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
fallbacks map[language.Tag]language.Tag
translators map[language.Tag]Translator
provider MessageProvider

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{
fallbacks: map[language.Tag]language.Tag{},
translators: map[language.Tag]Translator{},
dictionaries: map[language.Tag]Dictionary{},
fallbacks: map[language.Tag]language.Tag{},
translators: map[language.Tag]Translator{},

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

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,61 +71,59 @@ 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 WithYamlProvider(dir fs.FS) BundleOption {
return func(b *bundle) error {
provider, err := NewYamlMessageProvider(dir)
b.provider = provider

return err
}
}

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

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