Skip to content

Commit

Permalink
Merge pull request #14 from fullpipe/external-dictionary
Browse files Browse the repository at this point in the history
External dictionary
  • Loading branch information
fullpipe authored Oct 30, 2024
2 parents 85df9e6 + e47f9b0 commit 222b9dc
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 221 deletions.
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

0 comments on commit 222b9dc

Please sign in to comment.