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