Skip to content

Commit

Permalink
message: allow user-defined dictionaries
Browse files Browse the repository at this point in the history
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 <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-by: Nigel Tao <[email protected]>
  • Loading branch information
mpvl committed Nov 30, 2017
1 parent ded9dd9 commit 75cc3ca
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 32 deletions.
4 changes: 2 additions & 2 deletions message/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
)

// 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 {
Expand Down
104 changes: 89 additions & 15 deletions message/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,87 @@ package catalog // import "golang.org/x/text/message/catalog"

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
Expand All @@ -185,37 +259,37 @@ type Option func(*options)
//
// 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)
}
return c
}

// 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))
}

// Set sets the translation for the given language and key.
//
// 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...)
}

Expand All @@ -242,26 +316,26 @@ func Var(name string, msg ...Message) Message {

// 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
}
Expand Down
59 changes: 51 additions & 8 deletions message/catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,32 +124,75 @@ var testCases = []struct {
{"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)
case []Message:
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)
}
Expand Down
6 changes: 5 additions & 1 deletion message/catalog/dict.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ func (d *dict) Lookup(key string) (data string, ok bool) {
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()
Expand Down
8 changes: 4 additions & 4 deletions message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ type Printer struct {
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).
Expand All @@ -38,14 +38,14 @@ type options struct {
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)
Expand Down
4 changes: 2 additions & 2 deletions message/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ func TestLocalization(t *testing.T) {

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)
Expand Down

0 comments on commit 75cc3ca

Please sign in to comment.