123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- // 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.Selectf:
- //
- // catalog.Set(language.English, "You are %d minute(s) late.",
- // plural.Selectf(1, "",
- // plural.One, "You are 1 minute late.",
- // plural.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.Selectf(1, "", plural.One, "minute", plural.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.Selectf(1, "", plural.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.Selectf(1, ""
- // plural.One, gender.Select(1, "female", "her", "other", "his"))),
- // catalog.Var("invites", plural.Selectf(1, "", plural.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.Selectf(1, "",
- // plural.One, gender.Select(1,
- // "female", "%[1]v invites %[2]v to her party."
- // "other", "%[1]v invites %[2]v to his party."),
- // plural.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.Selectf(1, "",
- // plural.One, "invite"))
- // catalog.SetMacro(language.English, "their", plural.Selectf(1, "",
- // plural.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"
- "golang.org/x/text/internal/catmsg"
- "golang.org/x/text/language"
- )
- // A Catalog allows lookup of translated messages.
- type Catalog interface {
- // Languages returns all languages for which the Catalog contains variants.
- Languages() []language.Tag
- // Matcher returns a Matcher for languages from this Catalog.
- Matcher() language.Matcher
- // A Context is used for evaluating Messages.
- Context(tag language.Tag, r catmsg.Renderer) *Context
- // This method also makes Catalog a private interface.
- 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) {
- options := options{}
- for _, o := range opts {
- o(&options)
- }
- c := &catalog{
- dicts: map[language.Tag]Dictionary{},
- }
- _, hasFallback := dictionaries[options.fallback.String()]
- if hasFallback {
- // TODO: Should it be okay to not have a fallback language?
- // Catalog generators could enforce there is always a fallback.
- c.langs = append(c.langs, options.fallback)
- }
- 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
- if !hasFallback || tag != options.fallback {
- c.langs = append(c.langs, tag)
- }
- }
- if hasFallback {
- internal.SortTags(c.langs[1:])
- } else {
- internal.SortTags(c.langs)
- }
- c.matcher = language.NewMatcher(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
- matcher language.Matcher
- }
- func (c *catalog) Languages() []language.Tag { return c.langs }
- func (c *catalog) Matcher() language.Matcher { return c.matcher }
- 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
- matcher language.Matcher
- index store
- macros store
- }
- type options struct {
- fallback language.Tag
- }
- // An Option configures Catalog behavior.
- type Option func(*options)
- // Fallback specifies the default fallback language. The default is Und.
- func Fallback(tag language.Tag) Option {
- return func(o *options) { o.fallback = tag }
- }
- // 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
- // NewBuilder returns an empty mutable Catalog.
- func NewBuilder(opts ...Option) *Builder {
- c := &Builder{}
- for _, o := range opts {
- o(&c.options)
- }
- return c
- }
- // SetString is shorthand for Set(tag, key, String(msg)).
- 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 *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 *Builder) 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")
- // 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)}
- }
- // Context returns a Context for formatting messages.
- // Only one Message may be formatted per context at any given time.
- func (b *Builder) Context(tag language.Tag, r catmsg.Renderer) *Context {
- return &Context{
- cat: b,
- tag: 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 // 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.lookup(c.tag, key)
- if !ok {
- return ErrNotFound
- }
- return c.dec.Execute(data)
- }
|