message: allow user-defined dictionaries

Will be used by gotext tool.

Also fixes a bug in NewPrinter which did not
use DefaultCatalog.

Change-Id: If806c845c926ff467c1513b7dcda69d2e8235f49
Reviewed-on: https://go-review.googlesource.com/80675
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 2f65b4b..569e09f 100644
--- a/message/catalog.go
+++ b/message/catalog.go
@@ -13,9 +13,9 @@
 )
 
 // DefaultCatalog is used by SetString.
-var DefaultCatalog *catalog.Catalog = defaultCatalog
+var DefaultCatalog catalog.Catalog = defaultCatalog
 
-var defaultCatalog = catalog.New()
+var defaultCatalog = catalog.NewBuilder()
 
 // SetString calls SetString on the initial default Catalog.
 func SetString(tag language.Tag, key string, msg string) error {
diff --git a/message/catalog/catalog.go b/message/catalog/catalog.go
index d641d61..7933b66 100644
--- a/message/catalog/catalog.go
+++ b/message/catalog/catalog.go
@@ -154,13 +154,87 @@
 
 import (
 	"errors"
+	"fmt"
+
+	"golang.org/x/text/internal"
 
 	"golang.org/x/text/internal/catmsg"
 	"golang.org/x/text/language"
 )
 
-// A Catalog holds translations for messages for supported languages.
-type Catalog struct {
+// A Catalog allows lookup of translated messages.
+type Catalog interface {
+	// Languages returns all languages for which the Catalog contains variants.
+	Languages() []language.Tag
+
+	// A Context is used for evaluating Messages.
+	Context(tag language.Tag, r catmsg.Renderer) *Context
+
+	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) {
+	c := &catalog{
+		dicts: map[language.Tag]Dictionary{},
+	}
+	for lang, dict := range dictionaries {
+		tag, err := language.Parse(lang)
+		if err != nil {
+			return nil, fmt.Errorf("catalog: invalid language tag %q", lang)
+		}
+		if _, ok := c.dicts[tag]; ok {
+			return nil, fmt.Errorf("catalog: duplicate entry for tag %q after normalization", tag)
+		}
+		c.dicts[tag] = dict
+		c.langs = append(c.langs, tag)
+	}
+	internal.SortTags(c.langs)
+	return c, nil
+}
+
+// A Dictionary is a source of translations for a single language.
+type Dictionary interface {
+	// Lookup returns a message compiled with catmsg.Compile for the given key.
+	// It returns false for ok if such a message could not be found.
+	Lookup(key string) (data string, ok bool)
+}
+
+type catalog struct {
+	langs  []language.Tag
+	dicts  map[language.Tag]Dictionary
+	macros store
+}
+
+func (c *catalog) Languages() []language.Tag { return c.langs }
+
+func (c *catalog) lookup(tag language.Tag, key string) (data string, ok bool) {
+	for ; ; tag = tag.Parent() {
+		if dict, ok := c.dicts[tag]; ok {
+			if data, ok := dict.Lookup(key); ok {
+				return data, true
+			}
+		}
+		if tag == language.Und {
+			break
+		}
+	}
+	return "", false
+}
+
+// Context returns a Context for formatting messages.
+// Only one Message may be formatted per context at any given time.
+func (c *catalog) Context(tag language.Tag, r catmsg.Renderer) *Context {
+	return &Context{
+		cat: c,
+		tag: tag,
+		dec: catmsg.NewDecoder(tag, r, &dict{&c.macros, tag}),
+	}
+}
+
+// A Builder allows building a Catalog programmatically.
+type Builder struct {
 	options
 
 	index  store
@@ -185,9 +259,9 @@
 //
 // func Dict(tag language.Tag, d ...Dictionary) Option
 
-// New returns a new Catalog.
-func New(opts ...Option) *Catalog {
-	c := &Catalog{}
+// NewBuilder returns an empty mutable Catalog.
+func NewBuilder(opts ...Option) *Builder {
+	c := &Builder{}
 	for _, o := range opts {
 		o(&c.options)
 	}
@@ -195,12 +269,12 @@
 }
 
 // Languages returns all languages for which the Catalog contains variants.
-func (c *Catalog) Languages() []language.Tag {
+func (c *Builder) Languages() []language.Tag {
 	return c.index.languages()
 }
 
 // SetString is shorthand for Set(tag, key, String(msg)).
-func (c *Catalog) SetString(tag language.Tag, key string, msg string) error {
+func (c *Builder) SetString(tag language.Tag, key string, msg string) error {
 	return c.set(tag, key, &c.index, String(msg))
 }
 
@@ -208,14 +282,14 @@
 //
 // When evaluation this message, the first Message in the sequence to msgs to
 // evaluate to a string will be the message returned.
-func (c *Catalog) Set(tag language.Tag, key string, msg ...Message) error {
+func (c *Builder) Set(tag language.Tag, key string, msg ...Message) error {
 	return c.set(tag, key, &c.index, msg...)
 }
 
 // SetMacro defines a Message that may be substituted in another message.
 // The arguments to a macro Message are passed as arguments in the
 // placeholder the form "${foo(arg1, arg2)}".
-func (c *Catalog) SetMacro(tag language.Tag, name string, msg ...Message) error {
+func (c *Builder) SetMacro(tag language.Tag, name string, msg ...Message) error {
 	return c.set(tag, name, &c.macros, msg...)
 }
 
@@ -242,26 +316,26 @@
 
 // Context returns a Context for formatting messages.
 // Only one Message may be formatted per context at any given time.
-func (c *Catalog) Context(tag language.Tag, r catmsg.Renderer) *Context {
+func (b *Builder) Context(tag language.Tag, r catmsg.Renderer) *Context {
 	return &Context{
-		cat: c,
+		cat: b,
 		tag: tag,
-		dec: catmsg.NewDecoder(tag, r, &dict{&c.macros, tag}),
+		dec: catmsg.NewDecoder(tag, r, &dict{&b.macros, tag}),
 	}
 }
 
 // A Context is used for evaluating Messages.
 // Only one Message may be formatted per context at any given time.
 type Context struct {
-	cat *Catalog
-	tag language.Tag
+	cat Catalog
+	tag language.Tag // TODO: use compact index.
 	dec *catmsg.Decoder
 }
 
 // Execute looks up and executes the message with the given key.
 // It returns ErrNotFound if no message could be found in the index.
 func (c *Context) Execute(key string) error {
-	data, ok := c.cat.index.lookup(c.tag, key)
+	data, ok := c.cat.lookup(c.tag, key)
 	if !ok {
 		return ErrNotFound
 	}
diff --git a/message/catalog/catalog_test.go b/message/catalog/catalog_test.go
index 97ab4d8..7fc2ea7 100644
--- a/message/catalog/catalog_test.go
+++ b/message/catalog/catalog_test.go
@@ -124,14 +124,21 @@
 		{"en", "macroU", "Hello macroU!"},
 	}}}
 
-func initCat(entries []entry) (*Catalog, []language.Tag) {
+func setMacros(b *Builder) {
+	b.SetMacro(language.English, "macro1", String("Joe"))
+	b.SetMacro(language.Und, "macro2", String("${macro1(1)}"))
+	b.SetMacro(language.English, "macroU", noMatchMessage{})
+}
+
+func initBuilder(t *testing.T, entries []entry) (Catalog, []language.Tag) {
 	tags := []language.Tag{}
-	cat := New()
+	cat := NewBuilder()
 	for _, e := range entries {
 		tag := language.MustParse(e.tag)
 		tags = append(tags, tag)
 		switch msg := e.msg.(type) {
 		case string:
+
 			cat.SetString(tag, e.key, msg)
 		case Message:
 			cat.Set(tag, e.key, msg)
@@ -139,17 +146,53 @@
 			cat.Set(tag, e.key, msg...)
 		}
 	}
+	setMacros(cat)
 	return cat, internal.UniqueTags(tags)
 }
 
-func TestCatalog(t *testing.T) {
+type dictionary map[string]string
+
+func (d dictionary) Lookup(key string) (data string, ok bool) {
+	data, ok = d[key]
+	return data, ok
+}
+
+func initCatalog(t *testing.T, entries []entry) (Catalog, []language.Tag) {
+	m := map[string]Dictionary{}
+	for _, e := range entries {
+		m[e.tag] = dictionary{}
+	}
+	for _, e := range entries {
+		var msg Message
+		switch x := e.msg.(type) {
+		case string:
+			msg = String(x)
+		case Message:
+			msg = x
+		case []Message:
+			msg = firstInSequence(x)
+		}
+		data, _ := catmsg.Compile(language.MustParse(e.tag), nil, msg)
+		m[e.tag].(dictionary)[e.key] = data
+	}
+	c, err := NewFromMap(m)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// TODO: implement macros for fixed catalogs.
+	b := NewBuilder()
+	setMacros(b)
+	c.(*catalog).macros.index = b.macros.index
+	return c, c.Languages()
+}
+
+func TestCatalog(t *testing.T) { testCatalog(t, initCatalog) }
+func TestBuilder(t *testing.T) { testCatalog(t, initBuilder) }
+
+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 := initCat(tc.cat)
-			cat.SetMacro(language.English, "macro1", String("Joe"))
-			cat.SetMacro(language.Und, "macro2", String("${macro1(1)}"))
-			cat.SetMacro(language.English, "macroU", noMatchMessage{})
-
+			cat, wantTags := init(t, tc.cat)
 			if got := cat.Languages(); !reflect.DeepEqual(got, wantTags) {
 				t.Errorf("%s:Languages: got %v; want %v", tc.desc, got, wantTags)
 			}
diff --git a/message/catalog/dict.go b/message/catalog/dict.go
index 1810fab..74272c7 100644
--- a/message/catalog/dict.go
+++ b/message/catalog/dict.go
@@ -33,7 +33,11 @@
 	return d.s.lookup(d.tag, key)
 }
 
-func (c *Catalog) set(tag language.Tag, key string, s *store, msg ...Message) error {
+func (b *Builder) lookup(tag language.Tag, key string) (data string, ok bool) {
+	return b.index.lookup(tag, key)
+}
+
+func (c *Builder) set(tag language.Tag, key string, s *store, msg ...Message) error {
 	data, err := catmsg.Compile(tag, &dict{&c.macros, tag}, firstInSequence(msg))
 
 	s.mutex.Lock()
diff --git a/message/message.go b/message/message.go
index 363093a..841a98a 100644
--- a/message/message.go
+++ b/message/message.go
@@ -22,11 +22,11 @@
 	toDecimal    number.Formatter
 	toScientific number.Formatter
 
-	cat *catalog.Catalog
+	cat catalog.Catalog
 }
 
 type options struct {
-	cat *catalog.Catalog
+	cat catalog.Catalog
 	// TODO:
 	// - allow %s to print integers in written form (tables are likely too large
 	//   to enable this by default).
@@ -38,14 +38,14 @@
 type Option func(o *options)
 
 // Catalog defines the catalog to be used.
-func Catalog(c *catalog.Catalog) Option {
+func Catalog(c catalog.Catalog) Option {
 	return func(o *options) { o.cat = c }
 }
 
 // NewPrinter returns a Printer that formats messages tailored to language t.
 func NewPrinter(t language.Tag, opts ...Option) *Printer {
 	options := &options{
-		cat: defaultCatalog,
+		cat: DefaultCatalog,
 	}
 	for _, o := range opts {
 		o(options)
diff --git a/message/message_test.go b/message/message_test.go
index 091ed3b..326f716 100644
--- a/message/message_test.go
+++ b/message/message_test.go
@@ -169,9 +169,9 @@
 
 type entry struct{ tag, key, msg string }
 
-func initCat(entries []entry) (*catalog.Catalog, []language.Tag) {
+func initCat(entries []entry) (*catalog.Builder, []language.Tag) {
 	tags := []language.Tag{}
-	cat := catalog.New()
+	cat := catalog.NewBuilder()
 	for _, e := range entries {
 		tag := language.MustParse(e.tag)
 		tags = append(tags, tag)