Skip to content

Commit

Permalink
translation supports nested folder and key
Browse files Browse the repository at this point in the history
  • Loading branch information
kkumar-gcc committed Dec 16, 2023
1 parent ea46f55 commit f19e3ee
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 97 deletions.
2 changes: 1 addition & 1 deletion contracts/translation/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package translation

type Loader interface {
// Load the messages for the given locale.
Load(folder string, locale string) (map[string]map[string]string, error)
Load(folder string, locale string) (map[string]map[string]any, error)
}
10 changes: 5 additions & 5 deletions mocks/translation/Loader.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 5 additions & 14 deletions translation/file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/bytedance/sonic"
"github.com/bytedance/sonic/decoder"
Expand All @@ -22,19 +21,11 @@ func NewFileLoader(paths []string) *FileLoader {
}
}

func (f *FileLoader) Load(folder string, locale string) (map[string]map[string]string, error) {
translations := make(map[string]map[string]string)
func (f *FileLoader) Load(folder string, locale string) (map[string]map[string]any, error) {
translations := make(map[string]map[string]any)
for _, path := range f.paths {
var val map[string]string
fullPath := path
// Check if the folder is not "*", and if so, split it into subFolders
if folder != "*" {
subFolders := strings.Split(folder, ".")
for _, subFolder := range subFolders {
fullPath = filepath.Join(fullPath, subFolder)
}
}
fullPath = filepath.Join(fullPath, locale+".json")
var val map[string]any
fullPath := filepath.Join(path, locale, folder+".json")

if file.Exists(fullPath) {
data, err := os.ReadFile(fullPath)
Expand All @@ -51,7 +42,7 @@ func (f *FileLoader) Load(folder string, locale string) (map[string]map[string]s
}
// Initialize the map if it's a nil
if translations[locale] == nil {
translations[locale] = make(map[string]string)
translations[locale] = make(map[string]any)
}
mergeMaps(translations[locale], val)
} else {
Expand Down
25 changes: 13 additions & 12 deletions translation/file_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ type FileLoaderTestSuite struct {
}

func TestFileLoaderTestSuite(t *testing.T) {
assert.Nil(t, file.Create("lang/en.json", `{"foo": "bar"}`))
assert.Nil(t, file.Create("lang/another/en.json", `{"foo": "backagebar", "baz": "backagesplash"}`))
assert.Nil(t, file.Create("lang/invalid/en.json", `{"foo": "bar",}`))
restrictedFilePath := "lang/restricted/en.json"
assert.Nil(t, file.Create("lang/en/test.json", `{"foo": "bar", "baz": {"foo": "bar"}}`))
assert.Nil(t, file.Create("lang/en/another/test.json", `{"foo": "backagebar", "baz": "backagesplash"}`))
assert.Nil(t, file.Create("lang/another/en/test.json", `{"foo": "backagebar", "baz": "backagesplash"}`))
assert.Nil(t, file.Create("lang/en/invalid/test.json", `{"foo": "bar",}`))
restrictedFilePath := "lang/en/restricted/test.json"
assert.Nil(t, file.Create(restrictedFilePath, `{"foo": "restricted"}`))
assert.Nil(t, os.Chmod(restrictedFilePath, 0000))
suite.Run(t, &FileLoaderTestSuite{})
Expand All @@ -32,28 +33,28 @@ func (f *FileLoaderTestSuite) SetupTest() {
func (f *FileLoaderTestSuite) TestLoad() {
paths := []string{"./lang"}
loader := NewFileLoader(paths)
translations, err := loader.Load("*", "en")
translations, err := loader.Load("test", "en")
f.NoError(err)
f.NotNil(translations)
f.Equal("bar", translations["en"]["foo"])

paths = []string{"./lang/another", "./lang"}
loader = NewFileLoader(paths)
translations, err = loader.Load("*", "en")
translations, err = loader.Load("test", "en")
f.NoError(err)
f.NotNil(translations)
f.Equal("bar", translations["en"]["foo"])

paths = []string{"./lang"}
loader = NewFileLoader(paths)
translations, err = loader.Load("another", "en")
translations, err = loader.Load("another/test", "en")
f.NoError(err)
f.NotNil(translations)
f.Equal("backagebar", translations["en"]["foo"])

paths = []string{"./lang/restricted"}
paths = []string{"./lang"}
loader = NewFileLoader(paths)
translations, err = loader.Load("*", "en")
translations, err = loader.Load("restricted/test", "en")
if env.IsWindows() {
f.NoError(err)
f.NotNil(translations)
Expand All @@ -67,17 +68,17 @@ func (f *FileLoaderTestSuite) TestLoad() {
func (f *FileLoaderTestSuite) TestLoadNonExistentFile() {
paths := []string{"./lang"}
loader := NewFileLoader(paths)
translations, err := loader.Load("*", "hi")
translations, err := loader.Load("test", "hi")

f.Error(err)
f.Nil(translations)
f.Equal(ErrFileNotExist, err)
}

func (f *FileLoaderTestSuite) TestLoadInvalidJSON() {
paths := []string{"./lang/invalid"}
paths := []string{"./lang"}
loader := NewFileLoader(paths)
translations, err := loader.Load("*", "en")
translations, err := loader.Load("invalid/test", "en")

f.Error(err)
f.Nil(translations)
Expand Down
72 changes: 50 additions & 22 deletions translation/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"strconv"
"strings"

"github.com/bytedance/sonic"
"github.com/bytedance/sonic/ast"
"golang.org/x/text/cases"
"golang.org/x/text/language"

Expand All @@ -17,7 +19,7 @@ type Translator struct {
loader translationcontract.Loader
locale string
fallback string
loaded map[string]map[string]map[string]string
loaded map[string]map[string]map[string]any
selector *MessageSelector
key string
}
Expand All @@ -34,7 +36,7 @@ func NewTranslator(ctx context.Context, loader translationcontract.Loader, local
loader: loader,
locale: locale,
fallback: fallback,
loaded: make(map[string]map[string]map[string]string),
loaded: make(map[string]map[string]map[string]any),
selector: NewMessageSelector(),
}
}
Expand Down Expand Up @@ -85,24 +87,42 @@ func (t *Translator) Get(key string, options ...translationcontract.Option) (str
return "", err
}

line := t.loaded[folder][locale][keyPart]
if line == "" {
fallbackFolder, fallbackLocale := parseKey(t.GetFallback())
// If the fallback locale is different from the current locale, we will
// load in the lines for the fallback locale and try to retrieve the
// translation for the given key.If it is translated, we will return it.
// Otherwise, we can finally return the key as that will be the final
// fallback.
if (folder+locale != fallbackFolder+fallbackLocale) && fallback {
var fallbackOptions translationcontract.Option
if len(options) > 0 {
fallbackOptions = options[0]
marshal, err := sonic.Marshal(t.loaded[folder][locale])
if err != nil {
return "", err
}

Check warning on line 93 in translation/translator.go

View check run for this annotation

Codecov / codecov/patch

translation/translator.go#L92-L93

Added lines #L92 - L93 were not covered by tests
var keyParts []interface{}
for _, part := range strings.Split(keyPart, ".") {
keyParts = append(keyParts, part)
}

keyValue, err := sonic.Get(marshal, keyParts...)

if err != nil {
if err == ast.ErrNotExist {
fallbackLocale := t.GetFallback()
// If the fallback locale is different from the current locale, we will
// load in the lines for the fallback locale and try to retrieve the
// translation for the given key.If it is translated, we will return it.
// Otherwise, we can finally return the key as that will be the final
// fallback.
if (locale != fallbackLocale) && fallback {
var fallbackOptions translationcontract.Option
if len(options) > 0 {
fallbackOptions = options[0]
}
fallbackOptions.Fallback = translationcontract.Bool(false)
fallbackOptions.Locale = fallbackLocale
return t.Get(t.key, fallbackOptions)
}
fallbackOptions.Fallback = translationcontract.Bool(false)
fallbackOptions.Locale = fallbackLocale
return t.Get(fallbackFolder+"."+keyPart, fallbackOptions)
return t.key, nil
}
return t.key, nil
return "", err
}

line, err := keyValue.String()
if err != nil {
return "", err

Check warning on line 125 in translation/translator.go

View check run for this annotation

Codecov / codecov/patch

translation/translator.go#L125

Added line #L125 was not covered by tests
}

// If the line doesn't contain any placeholders, we can return it right
Expand Down Expand Up @@ -196,12 +216,20 @@ func makeReplacements(line string, replace map[string]string) string {
}

func parseKey(key string) (folder, keyPart string) {
parts := strings.Split(key, ".")
parts := strings.Split(key, "/")
keyParts := strings.Split(parts[len(parts)-1], ".")
folder = "*"
keyPart = key
keyPart = ""
if len(keyParts) > 1 {
keyPart = strings.Join(keyParts[1:], ".")
}
if len(parts) > 1 {
folder = strings.Join(parts[:len(parts)-1], ".")
keyPart = parts[len(parts)-1]
folder = strings.Join(parts[:len(parts)-1], "/")
}
if folder != "*" {
folder = folder + "/" + keyParts[0]
} else {
folder = keyParts[0]
}
return folder, keyPart
}
Loading

0 comments on commit f19e3ee

Please sign in to comment.