Skip to content

Commit

Permalink
Faster CLDR lookup for po (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
vorlif committed Dec 7, 2023
1 parent 6193a66 commit cf4c9e3
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 34 deletions.
4 changes: 4 additions & 0 deletions bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,15 @@ func WithFilesystemLoader(domain string, fsOpts ...FsOption) BundleOption {
}

// WithDomainPath loads a domain from a specified path.
//
// This is a shorthand for WithFilesystemLoader(domain, WithPath(path)).
func WithDomainPath(domain string, path string) BundleOption {
return WithFilesystemLoader(domain, WithPath(path))
}

// WithDomainFs loads a domain from a fs.FS.
//
// This is a shorthand for WithFilesystemLoader(domain, WithFs(fsys)).
func WithDomainFs(domain string, fsys fs.FS) BundleOption {
if fsys == nil {
return func(opts *bundleBuilder) error {
Expand Down
17 changes: 11 additions & 6 deletions catalog/po.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,20 @@ func buildGettextCatalog(file *po.File, lang language.Tag, domain string, useCLD
return catl, nil
}

func getCLDRPluralFunction(lang language.Tag) func(a interface{}) int {
func getCLDRPluralFunction(lang language.Tag) func(a any) int {
ruleSet, _ := cldrplural.ForLanguage(lang)
return func(a interface{}) int {

catToForm := make(map[cldrplural.Category]int, len(ruleSet.Categories))
for idx, cat := range ruleSet.Categories {
catToForm[cat] = idx
}

return func(a any) int {
cat := ruleSet.Evaluate(a)
for i := 0; i < len(ruleSet.Categories); i++ {
if ruleSet.Categories[i] == cat {
return i
}
if form, ok := catToForm[cat]; ok {
return form
}

return 0
}
}
Expand Down
61 changes: 48 additions & 13 deletions loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ import (
)

const (
// Deprecated: Will be removed in a future version.
// Has only been used for tests so far.
UnknownFile = "unknown"
PoFile = ".po"
MoFile = ".mo"
JSONFile = ".json"
// PoFile is the file extension of Po files.
// Deprecated: Will be removed in a future version.
// The string should be kept in your own program code.
PoFile = ".po"
// MoFile is the file extension of Mo files.
// Deprecated: Will be removed in a future version.
// The string should be kept in your own program code.
MoFile = ".mo"
// JSONFile is the file extension of JSON files.
// Deprecated: Will be removed in a future version.
// The string should be kept in your own program code.
JSONFile = ".json"
)

// Catalog represents a collection of messages (translations) for a language and a domain.
Expand All @@ -42,7 +53,7 @@ type Loader interface {
}

// A Resolver is used by the FilesystemLoader to resolve the appropriate path for a file.
// If a file was not found, os.ErrNotFound should be returned.
// If a file was not found, os.ErrNotExist should be returned.
// All other errors cause the loaders search to stop.
//
// fsys represents the file system from which the FilesystemLoader wants to load the file.
Expand Down Expand Up @@ -89,9 +100,9 @@ func NewFilesystemLoader(opts ...FsOption) (*FilesystemLoader, error) {
}

if len(l.decoder) == 0 {
l.addDecoder(PoFile, catalog.NewPoDecoder())
l.addDecoder(MoFile, catalog.NewMoDecoder())
l.addDecoder(JSONFile, catalog.NewJSONDecoder())
l.addDecoder(".po", catalog.NewPoDecoder())
l.addDecoder(".mo", catalog.NewMoDecoder())
l.addDecoder(".json", catalog.NewJSONDecoder())
}

if l.fsys == nil {
Expand Down Expand Up @@ -177,6 +188,8 @@ func WithPath(path string) FsOption {

// WithSystemFs stores the root path as filesystem.
// Lets the creation of the FilesystemLoader fail, if a filesystem was already deposited.
//
// Shorthand for WithPath("").
func WithSystemFs() FsOption { return WithPath("") }

// WithResolver stores the resolver of a FilesystemLoader.
Expand All @@ -191,7 +204,9 @@ func WithResolver(resolver Resolver) FsOption {
}
}

// WithDecoder stores a decoder for an extension.
// WithDecoder stores a decoder for a file extension.
//
// The file extension should begin with a dot. For example ".po" or ".json".
func WithDecoder(ext string, decoder catalog.Decoder) FsOption {
return func(r *FilesystemLoader) error {
r.addDecoder(ext, decoder)
Expand All @@ -200,17 +215,37 @@ func WithDecoder(ext string, decoder catalog.Decoder) FsOption {
}

// WithMoDecoder stores the mo file decoder.
func WithMoDecoder() FsOption { return WithDecoder(MoFile, catalog.NewMoDecoder()) }
//
// Shorthand for WithDecoder(".mo", catalog.NewMoDecoder()).
func WithMoDecoder() FsOption { return WithDecoder(".mo", catalog.NewMoDecoder()) }

// WithPoDecoder stores the mo file decoder.
func WithPoDecoder() FsOption { return WithDecoder(PoFile, catalog.NewPoDecoder()) }
//
// Shorthand for WithDecoder(".po", catalog.NewPoDecoder()).
func WithPoDecoder() FsOption { return WithDecoder(".po", catalog.NewPoDecoder()) }

// WithJSONDecoder stores the JSON file decoder.
//
// Shorthand for WithDecoder(".json", catalog.NewJSONDecoder()).
func WithJSONDecoder() FsOption { return WithDecoder(".json", catalog.NewJSONDecoder()) }

type defaultResolver struct {
search bool
category string
}

// NewDefaultResolver create a resolver which can be used for a FilesystemLoader.
// It is the Resolver that is used if no separate resolver has been set.
// He tries to find the files in different directories and returns the file that it found first.
//
// For example, if a Mo file is to be found, an attempt is made to resolve the following paths.
// - .../locale/category/domain.mo
// - .../locale/LC_MESSAGES/domain.mo
// - .../locale/domain.mo
// - .../domain/locale.mo
// - .../locale.mo
// - .../category/locale.mo
// - .../LC_MESSAGES/locale.mo
func NewDefaultResolver(opts ...ResolverOption) (Resolver, error) {
l := &defaultResolver{
search: true,
Expand All @@ -226,10 +261,10 @@ func NewDefaultResolver(opts ...ResolverOption) (Resolver, error) {

func WithDisabledSearch() ResolverOption { return func(r *defaultResolver) { r.search = false } }

// WithCategory defines an additional category which is included in the search.
// For Gettext files, LC_MESSAGES is often used for this.
func WithCategory(category string) ResolverOption {
return func(l *defaultResolver) {
l.category = category
}
return func(l *defaultResolver) { l.category = category }
}

func (r *defaultResolver) Resolve(fsys fs.FS, extension string, tag language.Tag, domain string) (string, error) {
Expand Down
43 changes: 28 additions & 15 deletions loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,23 @@ func TestLoadPo(t *testing.T) {
}{
{
language.German, "b", "my_category",
false, path.Join("de", "my_category", "b.po"), PoFile,
false, path.Join("de", "my_category", "b.po"), ".po",
},
{
language.German, "a", "",
false, path.Join("de", "LC_MESSAGES", "a.po"), PoFile,
false, path.Join("de", "LC_MESSAGES", "a.po"), ".po",
},
{
language.French, "a", "",
true, "", UnknownFile,
true, "", "unknown",
},
{
language.English, "domain", "cat",
true, "", UnknownFile,
true, "", "unknown",
},
{
language.MustParse("de_AT"), "", "",
false, "de_AT.po", PoFile,
false, "de_AT.po", ".po",
},
}

Expand Down Expand Up @@ -155,15 +155,15 @@ func TestReduceMoFiles(t *testing.T) {
}{
{
language.German, "b", "my_category",
true, "", UnknownFile,
true, "", "unknown",
},
{
language.French, "a", "",
true, "", UnknownFile,
true, "", "unknown",
},
{
language.English, "domain", "cat",
false, "en.mo", MoFile,
false, "en.mo", ".mo",
},
}

Expand Down Expand Up @@ -192,19 +192,19 @@ func TestDisableSearch(t *testing.T) {
}{
{
language.German, "other", "my_category",
true, "", UnknownFile,
true, "", "unknown",
},
{
language.German, "a", "LC_MESSAGES",
false, path.Join("de", "LC_MESSAGES", "a.po"), PoFile,
false, path.Join("de", "LC_MESSAGES", "a.po"), ".po",
},
{
language.German, "b", "my_category",
false, path.Join("de", "my_category", "b.po"), PoFile,
false, path.Join("de", "my_category", "b.po"), ".po",
},
{
language.English, "domain", "cat",
true, "", UnknownFile,
true, "", "unknown",
},
}

Expand Down Expand Up @@ -257,7 +257,7 @@ func TestWithDecoder(t *testing.T) {
)
assert.NoError(t, err)
require.NotNil(t, fl)
assert.Contains(t, fl.extensions, PoFile)
assert.Contains(t, fl.extensions, ".po")
if assert.Len(t, fl.decoder, 1) {
assert.IsType(t, catalog.NewPoDecoder(), fl.decoder[0])
}
Expand All @@ -270,12 +270,25 @@ func TestWithDecoder(t *testing.T) {
)
assert.NoError(t, err)
require.NotNil(t, fl)
assert.Contains(t, fl.extensions, MoFile)
assert.Contains(t, fl.extensions, ".mo")
if assert.Len(t, fl.decoder, 1) {
assert.IsType(t, catalog.NewMoDecoder(), fl.decoder[0])
}
})

t.Run("WithJSONDecoder disables fallback", func(t *testing.T) {
fl, err := NewFilesystemLoader(
WithJSONDecoder(),
WithSystemFs(),
)
assert.NoError(t, err)
require.NotNil(t, fl)
assert.Contains(t, fl.extensions, ".json")
if assert.Len(t, fl.decoder, 1) {
assert.IsType(t, catalog.NewJSONDecoder(), fl.decoder[0])
}
})

t.Run("WithDecoder sets decoder", func(t *testing.T) {
ext := ".json"
dec := &testDecoder{}
Expand Down Expand Up @@ -330,7 +343,7 @@ func TestWithFs(t *testing.T) {

for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
getPath, getErr := resolver.Resolve(testFS, PoFile, tt.lang, tt.domain)
getPath, getErr := resolver.Resolve(testFS, ".po", tt.lang, tt.domain)
if tt.wantErr {
assert.Error(t, getErr)
assert.Empty(t, getPath)
Expand Down

0 comments on commit cf4c9e3

Please sign in to comment.