diff options
Diffstat (limited to 'vendor/golang.org/x/text/message/catalog')
-rw-r--r-- | vendor/golang.org/x/text/message/catalog/catalog.go | 292 | ||||
-rw-r--r-- | vendor/golang.org/x/text/message/catalog/catalog_test.go | 194 | ||||
-rw-r--r-- | vendor/golang.org/x/text/message/catalog/dict.go | 90 |
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 000000000..957444c10 --- /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 000000000..97ab4d88a --- /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 000000000..1810fabc6 --- /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 +} |