aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/golang.org/x/text/message/catalog
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/golang.org/x/text/message/catalog')
-rw-r--r--vendor/golang.org/x/text/message/catalog/catalog.go292
-rw-r--r--vendor/golang.org/x/text/message/catalog/catalog_test.go194
-rw-r--r--vendor/golang.org/x/text/message/catalog/dict.go90
3 files changed, 576 insertions, 0 deletions
diff --git a/vendor/golang.org/x/text/message/catalog/catalog.go b/vendor/golang.org/x/text/message/catalog/catalog.go
new file mode 100644
index 0000000..957444c
--- /dev/null
+++ b/vendor/golang.org/x/text/message/catalog/catalog.go
@@ -0,0 +1,292 @@
+// 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 catalog defines collections of translated format strings.
+//
+// This package mostly defines types for populating catalogs with messages. The
+// catmsg package contains further definitions for creating custom message and
+// dictionary types as well as packages that use Catalogs.
+//
+// Package catalog defines various interfaces: Dictionary, Loader, and Message.
+// A Dictionary maintains a set of translations of format strings for a single
+// language. The Loader interface defines a source of dictionaries. A
+// translation of a format string is represented by a Message.
+//
+//
+// Catalogs
+//
+// A Catalog defines a programmatic interface for setting message translations.
+// It maintains a set of per-language dictionaries with translations for a set
+// of keys. For message translation to function properly, a translation should
+// be defined for each key for each supported language. A dictionary may be
+// underspecified, though, if there is a parent language that already defines
+// the key. For example, a Dictionary for "en-GB" could leave out entries that
+// are identical to those in a dictionary for "en".
+//
+//
+// Messages
+//
+// A Message is a format string which varies on the value of substitution
+// variables. For instance, to indicate the number of results one could want "no
+// results" if there are none, "1 result" if there is 1, and "%d results" for
+// any other number. Catalog is agnostic to the kind of format strings that are
+// used: for instance, messages can follow either the printf-style substitution
+// from package fmt or use templates.
+//
+// A Message does not substitute arguments in the format string. This job is
+// reserved for packages that render strings, such as message, that use Catalogs
+// to selected string. This separation of concerns allows Catalog to be used to
+// store any kind of formatting strings.
+//
+//
+// Selecting messages based on linguistic features of substitution arguments
+//
+// Messages may vary based on any linguistic features of the argument values.
+// The most common one is plural form, but others exist.
+//
+// Selection messages are provided in packages that provide support for a
+// specific linguistic feature. The following snippet uses plural.Select:
+//
+// catalog.Set(language.English, "You are %d minute(s) late.",
+// plural.Select(1,
+// "one", "You are 1 minute late.",
+// "other", "You are %d minutes late."))
+//
+// In this example, a message is stored in the Catalog where one of two messages
+// is selected based on the first argument, a number. The first message is
+// selected if the argument is singular (identified by the selector "one") and
+// the second message is selected in all other cases. The selectors are defined
+// by the plural rules defined in CLDR. The selector "other" is special and will
+// always match. Each language always defines one of the linguistic categories
+// to be "other." For English, singular is "one" and plural is "other".
+//
+// Selects can be nested. This allows selecting sentences based on features of
+// multiple arguments or multiple linguistic properties of a single argument.
+//
+//
+// String interpolation
+//
+// There is often a lot of commonality between the possible variants of a
+// message. For instance, in the example above the word "minute" varies based on
+// the plural catogory of the argument, but the rest of the sentence is
+// identical. Using interpolation the above message can be rewritten as:
+//
+// catalog.Set(language.English, "You are %d minute(s) late.",
+// catalog.Var("minutes",
+// plural.Select(1, "one", "minute", "other", "minutes")),
+// catalog.String("You are %[1]d ${minutes} late."))
+//
+// Var is defined to return the variable name if the message does not yield a
+// match. This allows us to further simplify this snippet to
+//
+// catalog.Set(language.English, "You are %d minute(s) late.",
+// catalog.Var("minutes", plural.Select(1, "one", "minute")),
+// catalog.String("You are %d ${minutes} late."))
+//
+// Overall this is still only a minor improvement, but things can get a lot more
+// unwieldy if more than one linguistic feature is used to determine a message
+// variant. Consider the following example:
+//
+// // argument 1: list of hosts, argument 2: list of guests
+// catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
+// catalog.Var("their",
+// plural.Select(1,
+// "one", gender.Select(1, "female", "her", "other", "his"))),
+// catalog.Var("invites", plural.Select(1, "one", "invite"))
+// catalog.String("%[1]v ${invites} %[2]v to ${their} party.")),
+//
+// Without variable substitution, this would have to be written as
+//
+// // argument 1: list of hosts, argument 2: list of guests
+// catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
+// plural.Select(1,
+// "one", gender.Select(1,
+// "female", "%[1]v invites %[2]v to her party."
+// "other", "%[1]v invites %[2]v to his party."),
+// "other", "%[1]v invites %[2]v to their party.")
+//
+// Not necessarily shorter, but using variables there is less duplication and
+// the messages are more maintenance friendly. Moreover, languages may have up
+// to six plural forms. This makes the use of variables more welcome.
+//
+// Different messages using the same inflections can reuse variables by moving
+// them to macros. Using macros we can rewrite the message as:
+//
+// // argument 1: list of hosts, argument 2: list of guests
+// catalog.SetString(language.English, "%[1]v invite(s) %[2]v to their party.",
+// "%[1]v ${invites(1)} %[2]v to ${their(1)} party.")
+//
+// Where the following macros were defined separately.
+//
+// catalog.SetMacro(language.English, "invites", plural.Select(1, "one", "invite"))
+// catalog.SetMacro(language.English, "their", plural.Select(1,
+// "one", gender.Select(1, "female", "her", "other", "his"))),
+//
+// Placeholders use parentheses and the arguments to invoke a macro.
+//
+//
+// Looking up messages
+//
+// Message lookup using Catalogs is typically only done by specialized packages
+// and is not something the user should be concerned with. For instance, to
+// express the tardiness of a user using the related message we defined earlier,
+// the user may use the package message like so:
+//
+// p := message.NewPrinter(language.English)
+// p.Printf("You are %d minute(s) late.", 5)
+//
+// Which would print:
+// You are 5 minutes late.
+//
+//
+// This package is UNDER CONSTRUCTION and its API may change.
+package catalog // import "golang.org/x/text/message/catalog"
+
+// TODO:
+// Some way to freeze a catalog.
+// - Locking on each lockup turns out to be about 50% of the total running time
+// for some of the benchmarks in the message package.
+// Consider these:
+// - Sequence type to support sequences in user-defined messages.
+// - Garbage collection: Remove dictionaries that can no longer be reached
+// as other dictionaries have been added that cover all possible keys.
+
+import (
+ "errors"
+ "fmt"
+
+ "golang.org/x/text/internal/catmsg"
+ "golang.org/x/text/language"
+)
+
+// A Catalog holds translations for messages for supported languages.
+type Catalog struct {
+ options
+
+ index store
+ macros store
+}
+
+type options struct{}
+
+// An Option configures Catalog behavior.
+type Option func(*options)
+
+// TODO:
+// // Catalogs specifies one or more sources for a Catalog.
+// // Lookups are in order.
+// // This can be changed inserting a Catalog used for setting, which implements
+// // Loader, used for setting in the chain.
+// func Catalogs(d ...Loader) Option {
+// return nil
+// }
+//
+// func Delims(start, end string) Option {}
+//
+// func Dict(tag language.Tag, d ...Dictionary) Option
+
+// New returns a new Catalog.
+func New(opts ...Option) *Catalog {
+ c := &Catalog{}
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ return c.set(tag, name, &c.macros, msg...)
+}
+
+// ErrNotFound indicates there was no message for the given key.
+var ErrNotFound = errors.New("catalog: message not found")
+
+// A Message holds a collection of translations for the same phrase that may
+// vary based on the values of substitution arguments.
+type Message interface {
+ catmsg.Message
+}
+
+// String specifies a plain message string. It can be used as fallback if no
+// other strings match or as a simple standalone message.
+//
+// It is an error to pass more than one String in a message sequence.
+func String(name string) Message {
+ return catmsg.String(name)
+}
+
+// Var sets a variable that may be substituted in formatting patterns using
+// named substitution of the form "${name}". The name argument is used as a
+// fallback if the statements do not produce a match. The statement sequence may
+// not contain any Var calls.
+//
+// The name passed to a Var must be unique within message sequence.
+func Var(name string, msg ...Message) Message {
+ return &catmsg.Var{Name: name, Message: firstInSequence(msg)}
+}
+
+// firstInSequence is a message type that prints the first message in the
+// sequence that resolves to a match for the given substitution arguments.
+type firstInSequence []Message
+
+func (s firstInSequence) Compile(e *catmsg.Encoder) error {
+ e.EncodeMessageType(catmsg.First)
+ err := catmsg.ErrIncomplete
+ for i, m := range s {
+ if err == nil {
+ return fmt.Errorf("catalog: message argument %d is complete and blocks subsequent messages", i-1)
+ }
+ err = e.EncodeMessage(m)
+ }
+ return err
+}
+
+// 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 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
+ 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)
+ if !ok {
+ return ErrNotFound
+ }
+ return c.dec.Execute(data)
+}
diff --git a/vendor/golang.org/x/text/message/catalog/catalog_test.go b/vendor/golang.org/x/text/message/catalog/catalog_test.go
new file mode 100644
index 0000000..97ab4d8
--- /dev/null
+++ b/vendor/golang.org/x/text/message/catalog/catalog_test.go
@@ -0,0 +1,194 @@
+// 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 catalog
+
+import (
+ "bytes"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "golang.org/x/text/internal"
+ "golang.org/x/text/internal/catmsg"
+ "golang.org/x/text/language"
+)
+
+type entry struct {
+ tag, key string
+ msg interface{}
+}
+
+var testCases = []struct {
+ desc string
+ cat []entry
+ lookup []entry
+}{{
+ desc: "empty catalog",
+ lookup: []entry{
+ {"en", "key", ""},
+ {"en", "", ""},
+ {"nl", "", ""},
+ },
+}, {
+ desc: "one entry",
+ cat: []entry{
+ {"en", "hello", "Hello!"},
+ },
+ lookup: []entry{
+ {"und", "hello", ""},
+ {"nl", "hello", ""},
+ {"en", "hello", "Hello!"},
+ {"en-US", "hello", "Hello!"},
+ {"en-GB", "hello", "Hello!"},
+ {"en-oxendict", "hello", "Hello!"},
+ {"en-oxendict-u-ms-metric", "hello", "Hello!"},
+ },
+}, {
+ desc: "hierarchical languages",
+ cat: []entry{
+ {"en", "hello", "Hello!"},
+ {"en-GB", "hello", "Hellø!"},
+ {"en-US", "hello", "Howdy!"},
+ {"en", "greetings", "Greetings!"},
+ },
+ lookup: []entry{
+ {"und", "hello", ""},
+ {"nl", "hello", ""},
+ {"en", "hello", "Hello!"},
+ {"en-US", "hello", "Howdy!"},
+ {"en-GB", "hello", "Hellø!"},
+ {"en-oxendict", "hello", "Hello!"},
+ {"en-US-oxendict-u-ms-metric", "hello", "Howdy!"},
+
+ {"und", "greetings", ""},
+ {"nl", "greetings", ""},
+ {"en", "greetings", "Greetings!"},
+ {"en-US", "greetings", "Greetings!"},
+ {"en-GB", "greetings", "Greetings!"},
+ {"en-oxendict", "greetings", "Greetings!"},
+ {"en-US-oxendict-u-ms-metric", "greetings", "Greetings!"},
+ },
+}, {
+ desc: "variables",
+ cat: []entry{
+ {"en", "hello %s", []Message{
+ Var("person", String("Jane")),
+ String("Hello ${person}!"),
+ }},
+ {"en", "hello error", []Message{
+ Var("person", String("Jane")),
+ noMatchMessage{}, // trigger sequence path.
+ String("Hello ${person."),
+ }},
+ {"en", "fallback to var value", []Message{
+ Var("you", noMatchMessage{}, noMatchMessage{}),
+ String("Hello ${you}."),
+ }},
+ {"en", "scopes", []Message{
+ Var("person1", String("Mark")),
+ Var("person2", String("Jane")),
+ Var("couple",
+ Var("person1", String("Joe")),
+ String("${person1} and ${person2}")),
+ String("Hello ${couple}."),
+ }},
+ {"en", "missing var", String("Hello ${missing}.")},
+ },
+ lookup: []entry{
+ {"en", "hello %s", "Hello Jane!"},
+ {"en", "hello error", "Hello $!(MISSINGBRACE)"},
+ {"en", "fallback to var value", "Hello you."},
+ {"en", "scopes", "Hello Joe and Jane."},
+ {"en", "missing var", "Hello missing."},
+ },
+}, {
+ desc: "macros",
+ cat: []entry{
+ {"en", "macro1", String("Hello ${macro1(1)}.")},
+ {"en", "macro2", String("Hello ${ macro1(2) }!")},
+ {"en", "macroWS", String("Hello ${ macro1( 2 ) }!")},
+ {"en", "missing", String("Hello ${ missing(1 }.")},
+ {"en", "badnum", String("Hello ${ badnum(1b) }.")},
+ {"en", "undefined", String("Hello ${ undefined(1) }.")},
+ {"en", "macroU", String("Hello ${ macroU(2) }!")},
+ },
+ lookup: []entry{
+ {"en", "macro1", "Hello Joe."},
+ {"en", "macro2", "Hello Joe!"},
+ {"en-US", "macroWS", "Hello Joe!"},
+ {"en-NL", "missing", "Hello $!(MISSINGPAREN)."},
+ {"en", "badnum", "Hello $!(BADNUM)."},
+ {"en", "undefined", "Hello undefined."},
+ {"en", "macroU", "Hello macroU!"},
+ }}}
+
+func initCat(entries []entry) (*Catalog, []language.Tag) {
+ tags := []language.Tag{}
+ cat := New()
+ 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...)
+ }
+ }
+ return cat, internal.UniqueTags(tags)
+}
+
+func TestCatalog(t *testing.T) {
+ 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{})
+
+ 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) {
+ tag := language.MustParse(e.tag)
+ buf := testRenderer{}
+ ctx := cat.Context(tag, &buf)
+ want := e.msg.(string)
+ err := ctx.Execute(e.key)
+ gotFound := err != ErrNotFound
+ wantFound := want != ""
+ if gotFound != wantFound {
+ t.Fatalf("err: got %v (%v); want %v", gotFound, err, wantFound)
+ }
+ if got := buf.buf.String(); got != want {
+ t.Errorf("Lookup:\ngot %q\nwant %q", got, want)
+ }
+ })
+ }
+ })
+ }
+}
+
+type testRenderer struct {
+ buf bytes.Buffer
+}
+
+func (f *testRenderer) Arg(i int) interface{} { return nil }
+func (f *testRenderer) Render(s string) { f.buf.WriteString(s) }
+
+var msgNoMatch = catmsg.Register("no match", func(d *catmsg.Decoder) bool {
+ return false // no match
+})
+
+type noMatchMessage struct{}
+
+func (noMatchMessage) Compile(e *catmsg.Encoder) error {
+ e.EncodeMessageType(msgNoMatch)
+ return catmsg.ErrIncomplete
+}
diff --git a/vendor/golang.org/x/text/message/catalog/dict.go b/vendor/golang.org/x/text/message/catalog/dict.go
new file mode 100644
index 0000000..1810fab
--- /dev/null
+++ b/vendor/golang.org/x/text/message/catalog/dict.go
@@ -0,0 +1,90 @@
+// 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 catalog
+
+import (
+ "sync"
+
+ "golang.org/x/text/internal"
+ "golang.org/x/text/internal/catmsg"
+ "golang.org/x/text/language"
+)
+
+// TODO:
+// Dictionary returns a Dictionary that returns the first Message, using the
+// given language tag, that matches:
+// 1. the last one registered by one of the Set methods
+// 2. returned by one of the Loaders
+// 3. repeat from 1. using the parent language
+// This approach allows messages to be underspecified.
+// func (c *Catalog) Dictionary(tag language.Tag) (Dictionary, error) {
+// // TODO: verify dictionary exists.
+// return &dict{&c.index, tag}, nil
+// }
+
+type dict struct {
+ s *store
+ tag language.Tag // TODO: make compact tag.
+}
+
+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 {
+ data, err := catmsg.Compile(tag, &dict{&c.macros, tag}, firstInSequence(msg))
+
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ m := s.index[tag]
+ if m == nil {
+ m = msgMap{}
+ if s.index == nil {
+ s.index = map[language.Tag]msgMap{}
+ }
+ s.index[tag] = m
+ }
+
+ m[key] = data
+ return err
+}
+
+type store struct {
+ mutex sync.RWMutex
+ index map[language.Tag]msgMap
+}
+
+type msgMap map[string]string
+
+func (s *store) lookup(tag language.Tag, key string) (data string, ok bool) {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ for ; ; tag = tag.Parent() {
+ if msgs, ok := s.index[tag]; ok {
+ if msg, ok := msgs[key]; ok {
+ return msg, true
+ }
+ }
+ if tag == language.Und {
+ break
+ }
+ }
+ return "", false
+}
+
+// Languages returns all languages for which the store contains variants.
+func (s *store) languages() []language.Tag {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ tags := make([]language.Tag, 0, len(s.index))
+ for t := range s.index {
+ tags = append(tags, t)
+ }
+ internal.SortTags(tags)
+ return tags
+}