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)