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