message/catalog: add Matcher method to Catalog
Matchers pretty much exclusively will have to be created
through a Catalog, if one uses one.
One of the main goals of this, though, is to facilitate
the usage of automatically generated catalogs, which is
otherwise tricky.
Change-Id: I008254ce8b3e93c84b2a26d53117e2221c0f027e
Reviewed-on: https://go-review.googlesource.com/81835
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/message/catalog.go b/message/catalog.go
index 569e09f..068271d 100644
--- a/message/catalog.go
+++ b/message/catalog.go
@@ -12,6 +12,14 @@
"golang.org/x/text/message/catalog"
)
+// MatchLanguage reports the matched tag obtained from language.MatchStrings for
+// the Matcher of the DefaultCatalog.
+func MatchLanguage(preferred ...string) language.Tag {
+ c := DefaultCatalog
+ tag, _ := language.MatchStrings(c.Matcher(), preferred...)
+ return tag
+}
+
// DefaultCatalog is used by SetString.
var DefaultCatalog catalog.Catalog = defaultCatalog
diff --git a/message/catalog/catalog.go b/message/catalog/catalog.go
index 7933b66..34a30d3 100644
--- a/message/catalog/catalog.go
+++ b/message/catalog/catalog.go
@@ -167,18 +167,32 @@
// Languages returns all languages for which the Catalog contains variants.
Languages() []language.Tag
+ // Matcher returns a Matcher for languages from this Catalog.
+ Matcher() language.Matcher
+
// A Context is used for evaluating Messages.
Context(tag language.Tag, r catmsg.Renderer) *Context
+ // This method also makes Catalog a private interface.
lookup(tag language.Tag, key string) (data string, ok bool)
}
// NewFromMap creates a Catalog from the given map. If a Dictionary is
// underspecified the entry is retrieved from a parent language.
func NewFromMap(dictionaries map[string]Dictionary, opts ...Option) (Catalog, error) {
+ options := options{}
+ for _, o := range opts {
+ o(&options)
+ }
c := &catalog{
dicts: map[language.Tag]Dictionary{},
}
+ _, hasFallback := dictionaries[options.fallback.String()]
+ if hasFallback {
+ // TODO: Should it be okay to not have a fallback language?
+ // Catalog generators could enforce there is always a fallback.
+ c.langs = append(c.langs, options.fallback)
+ }
for lang, dict := range dictionaries {
tag, err := language.Parse(lang)
if err != nil {
@@ -188,9 +202,16 @@
return nil, fmt.Errorf("catalog: duplicate entry for tag %q after normalization", tag)
}
c.dicts[tag] = dict
- c.langs = append(c.langs, tag)
+ if !hasFallback || tag != options.fallback {
+ c.langs = append(c.langs, tag)
+ }
}
- internal.SortTags(c.langs)
+ if hasFallback {
+ internal.SortTags(c.langs[1:])
+ } else {
+ internal.SortTags(c.langs)
+ }
+ c.matcher = language.NewMatcher(c.langs)
return c, nil
}
@@ -202,12 +223,14 @@
}
type catalog struct {
- langs []language.Tag
- dicts map[language.Tag]Dictionary
- macros store
+ langs []language.Tag
+ dicts map[language.Tag]Dictionary
+ macros store
+ matcher language.Matcher
}
func (c *catalog) Languages() []language.Tag { return c.langs }
+func (c *catalog) Matcher() language.Matcher { return c.matcher }
func (c *catalog) lookup(tag language.Tag, key string) (data string, ok bool) {
for ; ; tag = tag.Parent() {
@@ -236,16 +259,24 @@
// A Builder allows building a Catalog programmatically.
type Builder struct {
options
+ matcher language.Matcher
index store
macros store
}
-type options struct{}
+type options struct {
+ fallback language.Tag
+}
// An Option configures Catalog behavior.
type Option func(*options)
+// Fallback specifies the default fallback language. The default is Und.
+func Fallback(tag language.Tag) Option {
+ return func(o *options) { o.fallback = tag }
+}
+
// TODO:
// // Catalogs specifies one or more sources for a Catalog.
// // Lookups are in order.
@@ -268,11 +299,6 @@
return c
}
-// Languages returns all languages for which the Catalog contains variants.
-func (c *Builder) Languages() []language.Tag {
- return c.index.languages()
-}
-
// SetString is shorthand for Set(tag, key, String(msg)).
func (c *Builder) SetString(tag language.Tag, key string, msg string) error {
return c.set(tag, key, &c.index, String(msg))
diff --git a/message/catalog/catalog_test.go b/message/catalog/catalog_test.go
index 7fc2ea7..08bfdc7 100644
--- a/message/catalog/catalog_test.go
+++ b/message/catalog/catalog_test.go
@@ -6,11 +6,11 @@
import (
"bytes"
- "fmt"
+ "path"
"reflect"
+ "strings"
"testing"
- "golang.org/x/text/internal"
"golang.org/x/text/internal/catmsg"
"golang.org/x/text/language"
)
@@ -20,17 +20,33 @@
msg interface{}
}
-var testCases = []struct {
- desc string
- cat []entry
- lookup []entry
-}{{
+func langs(s string) []language.Tag {
+ t, _, _ := language.ParseAcceptLanguage(s)
+ return t
+}
+
+type testCase struct {
+ desc string
+ cat []entry
+ lookup []entry
+ fallback string
+ match []string
+ tags []language.Tag
+}
+
+var testCases = []testCase{{
desc: "empty catalog",
lookup: []entry{
{"en", "key", ""},
{"en", "", ""},
{"nl", "", ""},
},
+ match: []string{
+ "gr -> und",
+ "en-US -> und",
+ "af -> und",
+ },
+ tags: nil, // not an empty list.
}, {
desc: "one entry",
cat: []entry{
@@ -45,6 +61,11 @@
{"en-oxendict", "hello", "Hello!"},
{"en-oxendict-u-ms-metric", "hello", "Hello!"},
},
+ match: []string{
+ "gr -> en",
+ "en-US -> en",
+ },
+ tags: langs("en"),
}, {
desc: "hierarchical languages",
cat: []entry{
@@ -52,6 +73,7 @@
{"en-GB", "hello", "Hellø!"},
{"en-US", "hello", "Howdy!"},
{"en", "greetings", "Greetings!"},
+ {"gsw", "hello", "Grüetzi!"},
},
lookup: []entry{
{"und", "hello", ""},
@@ -70,6 +92,12 @@
{"en-oxendict", "greetings", "Greetings!"},
{"en-US-oxendict-u-ms-metric", "greetings", "Greetings!"},
},
+ fallback: "gsw",
+ match: []string{
+ "gr -> gsw",
+ "en-US -> en-US",
+ },
+ tags: langs("gsw, en, en-GB, en-US"),
}, {
desc: "variables",
cat: []entry{
@@ -103,6 +131,7 @@
{"en", "scopes", "Hello Joe and Jane."},
{"en", "missing var", "Hello missing."},
},
+ tags: langs("en"),
}, {
desc: "macros",
cat: []entry{
@@ -122,7 +151,9 @@
{"en", "badnum", "Hello $!(BADNUM)."},
{"en", "undefined", "Hello undefined."},
{"en", "macroU", "Hello macroU!"},
- }}}
+ },
+ tags: langs("en"),
+}}
func setMacros(b *Builder) {
b.SetMacro(language.English, "macro1", String("Joe"))
@@ -130,12 +161,16 @@
b.SetMacro(language.English, "macroU", noMatchMessage{})
}
-func initBuilder(t *testing.T, entries []entry) (Catalog, []language.Tag) {
- tags := []language.Tag{}
- cat := NewBuilder()
- for _, e := range entries {
+type buildFunc func(t *testing.T, tc testCase) Catalog
+
+func initBuilder(t *testing.T, tc testCase) Catalog {
+ options := []Option{}
+ if tc.fallback != "" {
+ options = append(options, Fallback(language.MustParse(tc.fallback)))
+ }
+ cat := NewBuilder(options...)
+ for _, e := range tc.cat {
tag := language.MustParse(e.tag)
- tags = append(tags, tag)
switch msg := e.msg.(type) {
case string:
@@ -147,7 +182,7 @@
}
}
setMacros(cat)
- return cat, internal.UniqueTags(tags)
+ return cat
}
type dictionary map[string]string
@@ -157,12 +192,12 @@
return data, ok
}
-func initCatalog(t *testing.T, entries []entry) (Catalog, []language.Tag) {
+func initCatalog(t *testing.T, tc testCase) Catalog {
m := map[string]Dictionary{}
- for _, e := range entries {
+ for _, e := range tc.cat {
m[e.tag] = dictionary{}
}
- for _, e := range entries {
+ for _, e := range tc.cat {
var msg Message
switch x := e.msg.(type) {
case string:
@@ -175,7 +210,11 @@
data, _ := catmsg.Compile(language.MustParse(e.tag), nil, msg)
m[e.tag].(dictionary)[e.key] = data
}
- c, err := NewFromMap(m)
+ options := []Option{}
+ if tc.fallback != "" {
+ options = append(options, Fallback(language.MustParse(tc.fallback)))
+ }
+ c, err := NewFromMap(m, options...)
if err != nil {
t.Fatal(err)
}
@@ -183,22 +222,40 @@
b := NewBuilder()
setMacros(b)
c.(*catalog).macros.index = b.macros.index
- return c, c.Languages()
+ return c
}
-func TestCatalog(t *testing.T) { testCatalog(t, initCatalog) }
-func TestBuilder(t *testing.T) { testCatalog(t, initBuilder) }
+func TestMatcher(t *testing.T) {
+ test := func(t *testing.T, init buildFunc) {
+ for _, tc := range testCases {
+ for _, s := range tc.match {
+ a := strings.Split(s, "->")
+ t.Run(path.Join(tc.desc, a[0]), func(t *testing.T) {
+ cat := init(t, tc)
+ got, _ := language.MatchStrings(cat.Matcher(), a[0])
+ want := language.MustParse(strings.TrimSpace(a[1]))
+ if got != want {
+ t.Errorf("got %q; want %q", got, want)
+ }
+ })
+ }
+ }
+ }
+ t.Run("Builder", func(t *testing.T) { test(t, initBuilder) })
+ t.Run("Catalog", func(t *testing.T) { test(t, initCatalog) })
+}
-func testCatalog(t *testing.T, init func(*testing.T, []entry) (Catalog, []language.Tag)) {
- for _, tc := range testCases {
- t.Run(fmt.Sprintf("%s", tc.desc), func(t *testing.T) {
- cat, wantTags := init(t, tc.cat)
+func TestCatalog(t *testing.T) {
+ test := func(t *testing.T, init buildFunc) {
+ for _, tc := range testCases {
+ cat := init(t, tc)
+ wantTags := tc.tags
if got := cat.Languages(); !reflect.DeepEqual(got, wantTags) {
t.Errorf("%s:Languages: got %v; want %v", tc.desc, got, wantTags)
}
for _, e := range tc.lookup {
- t.Run(fmt.Sprintf("%s/%s", e.tag, e.key), func(t *testing.T) {
+ t.Run(path.Join(tc.desc, e.tag, e.key), func(t *testing.T) {
tag := language.MustParse(e.tag)
buf := testRenderer{}
ctx := cat.Context(tag, &buf)
@@ -214,8 +271,10 @@
}
})
}
- })
+ }
}
+ t.Run("Builder", func(t *testing.T) { test(t, initBuilder) })
+ t.Run("Catalog", func(t *testing.T) { test(t, initCatalog) })
}
type testRenderer struct {
diff --git a/message/catalog/dict.go b/message/catalog/dict.go
index 74272c7..a0eb818 100644
--- a/message/catalog/dict.go
+++ b/message/catalog/dict.go
@@ -49,6 +49,7 @@
if s.index == nil {
s.index = map[language.Tag]msgMap{}
}
+ c.matcher = nil
s.index[tag] = m
}
@@ -56,6 +57,23 @@
return err
}
+func (c *Builder) Matcher() language.Matcher {
+ c.index.mutex.RLock()
+ m := c.matcher
+ c.index.mutex.RUnlock()
+ if m != nil {
+ return m
+ }
+
+ c.index.mutex.Lock()
+ if c.matcher == nil {
+ c.matcher = language.NewMatcher(c.unlockedLanguages())
+ }
+ m = c.matcher
+ c.index.mutex.Unlock()
+ return m
+}
+
type store struct {
mutex sync.RWMutex
index map[language.Tag]msgMap
@@ -80,15 +98,32 @@
return "", false
}
-// Languages returns all languages for which the store contains variants.
-func (s *store) languages() []language.Tag {
+// Languages returns all languages for which the Catalog contains variants.
+func (b *Builder) Languages() []language.Tag {
+ s := &b.index
s.mutex.RLock()
defer s.mutex.RUnlock()
- tags := make([]language.Tag, 0, len(s.index))
- for t := range s.index {
- tags = append(tags, t)
+ return b.unlockedLanguages()
+}
+
+func (b *Builder) unlockedLanguages() []language.Tag {
+ s := &b.index
+ if len(s.index) == 0 {
+ return nil
}
- internal.SortTags(tags)
+ tags := make([]language.Tag, 0, len(s.index))
+ _, hasFallback := s.index[b.options.fallback]
+ offset := 0
+ if hasFallback {
+ tags = append(tags, b.options.fallback)
+ offset = 1
+ }
+ for t := range s.index {
+ if t != b.options.fallback {
+ tags = append(tags, t)
+ }
+ }
+ internal.SortTags(tags[offset:])
return tags
}
diff --git a/message/catalog_test.go b/message/catalog_test.go
new file mode 100644
index 0000000..7a2301c
--- /dev/null
+++ b/message/catalog_test.go
@@ -0,0 +1,43 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package message
+
+import (
+ "strings"
+ "testing"
+
+ "golang.org/x/text/language"
+ "golang.org/x/text/message/catalog"
+)
+
+func TestMatchLanguage(t *testing.T) {
+ c := catalog.NewBuilder(catalog.Fallback(language.English))
+ c.SetString(language.Bengali, "", "")
+ c.SetString(language.English, "", "")
+ c.SetString(language.German, "", "")
+
+ testCases := []struct {
+ args string // '|'-separated list
+ want string
+ }{{
+ args: "de-CH",
+ want: "de",
+ }, {
+ args: "bn-u-nu-latn|en-US,en;q=0.9,de;q=0.8,nl;q=0.7",
+ want: "bn-u-nu-latn",
+ }, {
+ args: "gr",
+ want: "en",
+ }}
+ for _, tc := range testCases {
+ DefaultCatalog = c
+ t.Run(tc.args, func(t *testing.T) {
+ got := MatchLanguage(strings.Split(tc.args, "|")...)
+ if got != language.Make(tc.want) {
+ t.Errorf("got %q; want %q", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/message/doc.go b/message/doc.go
index 89c1592..2f7effd 100644
--- a/message/doc.go
+++ b/message/doc.go
@@ -11,15 +11,15 @@
// A format string can be localized by replacing any of the print functions of
// fmt with an equivalent call to a Printer.
//
-// p := message.NewPrinter(language.English)
+// p := message.NewPrinter(message.MatchLanguage("en"))
// p.Println(123456.78) // Prints 123,456.78
//
// p.Printf("%d ducks in a row", 4331) // Prints 4,331 ducks in a row
//
-// p := message.NewPrinter(language.Dutch)
+// p := message.NewPrinter(message.MatchLanguage("nl"))
// p.Println("Hoogte: %f meter", 1244.9) // Prints Hoogte: 1.244,9 meter
//
-// p := message.NewPrinter(language.Bengali)
+// p := message.NewPrinter(message.MatchLanguage("bn"))
// p.Println(123456.78) // Prints ১,২৩,৪৫৬.৭৮
//
// Printer currently supports numbers and specialized types for which packages