taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

catalog.go (13504B)


      1 // Copyright 2017 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 // Package catalog defines collections of translated format strings.
      6 //
      7 // This package mostly defines types for populating catalogs with messages. The
      8 // catmsg package contains further definitions for creating custom message and
      9 // dictionary types as well as packages that use Catalogs.
     10 //
     11 // Package catalog defines various interfaces: Dictionary, Loader, and Message.
     12 // A Dictionary maintains a set of translations of format strings for a single
     13 // language. The Loader interface defines a source of dictionaries. A
     14 // translation of a format string is represented by a Message.
     15 //
     16 // # Catalogs
     17 //
     18 // A Catalog defines a programmatic interface for setting message translations.
     19 // It maintains a set of per-language dictionaries with translations for a set
     20 // of keys. For message translation to function properly, a translation should
     21 // be defined for each key for each supported language. A dictionary may be
     22 // underspecified, though, if there is a parent language that already defines
     23 // the key. For example, a Dictionary for "en-GB" could leave out entries that
     24 // are identical to those in a dictionary for "en".
     25 //
     26 // # Messages
     27 //
     28 // A Message is a format string which varies on the value of substitution
     29 // variables. For instance, to indicate the number of results one could want "no
     30 // results" if there are none, "1 result" if there is 1, and "%d results" for
     31 // any other number. Catalog is agnostic to the kind of format strings that are
     32 // used: for instance, messages can follow either the printf-style substitution
     33 // from package fmt or use templates.
     34 //
     35 // A Message does not substitute arguments in the format string. This job is
     36 // reserved for packages that render strings, such as message, that use Catalogs
     37 // to selected string. This separation of concerns allows Catalog to be used to
     38 // store any kind of formatting strings.
     39 //
     40 // # Selecting messages based on linguistic features of substitution arguments
     41 //
     42 // Messages may vary based on any linguistic features of the argument values.
     43 // The most common one is plural form, but others exist.
     44 //
     45 // Selection messages are provided in packages that provide support for a
     46 // specific linguistic feature. The following snippet uses plural.Selectf:
     47 //
     48 //	catalog.Set(language.English, "You are %d minute(s) late.",
     49 //		plural.Selectf(1, "",
     50 //			plural.One, "You are 1 minute late.",
     51 //			plural.Other, "You are %d minutes late."))
     52 //
     53 // In this example, a message is stored in the Catalog where one of two messages
     54 // is selected based on the first argument, a number. The first message is
     55 // selected if the argument is singular (identified by the selector "one") and
     56 // the second message is selected in all other cases. The selectors are defined
     57 // by the plural rules defined in CLDR. The selector "other" is special and will
     58 // always match. Each language always defines one of the linguistic categories
     59 // to be "other." For English, singular is "one" and plural is "other".
     60 //
     61 // Selects can be nested. This allows selecting sentences based on features of
     62 // multiple arguments or multiple linguistic properties of a single argument.
     63 //
     64 // # String interpolation
     65 //
     66 // There is often a lot of commonality between the possible variants of a
     67 // message. For instance, in the example above the word "minute" varies based on
     68 // the plural catogory of the argument, but the rest of the sentence is
     69 // identical. Using interpolation the above message can be rewritten as:
     70 //
     71 //	catalog.Set(language.English, "You are %d minute(s) late.",
     72 //		catalog.Var("minutes",
     73 //			plural.Selectf(1, "", plural.One, "minute", plural.Other, "minutes")),
     74 //		catalog.String("You are %[1]d ${minutes} late."))
     75 //
     76 // Var is defined to return the variable name if the message does not yield a
     77 // match. This allows us to further simplify this snippet to
     78 //
     79 //	catalog.Set(language.English, "You are %d minute(s) late.",
     80 //		catalog.Var("minutes", plural.Selectf(1, "", plural.One, "minute")),
     81 //		catalog.String("You are %d ${minutes} late."))
     82 //
     83 // Overall this is still only a minor improvement, but things can get a lot more
     84 // unwieldy if more than one linguistic feature is used to determine a message
     85 // variant. Consider the following example:
     86 //
     87 //	// argument 1: list of hosts, argument 2: list of guests
     88 //	catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
     89 //		catalog.Var("their",
     90 //			plural.Selectf(1, ""
     91 //				plural.One, gender.Select(1, "female", "her", "other", "his"))),
     92 //		catalog.Var("invites", plural.Selectf(1, "", plural.One, "invite"))
     93 //		catalog.String("%[1]v ${invites} %[2]v to ${their} party.")),
     94 //
     95 // Without variable substitution, this would have to be written as
     96 //
     97 //	// argument 1: list of hosts, argument 2: list of guests
     98 //	catalog.Set(language.English, "%[1]v invite(s) %[2]v to their party.",
     99 //		plural.Selectf(1, "",
    100 //			plural.One, gender.Select(1,
    101 //				"female", "%[1]v invites %[2]v to her party."
    102 //				"other", "%[1]v invites %[2]v to his party."),
    103 //			plural.Other, "%[1]v invites %[2]v to their party."))
    104 //
    105 // Not necessarily shorter, but using variables there is less duplication and
    106 // the messages are more maintenance friendly. Moreover, languages may have up
    107 // to six plural forms. This makes the use of variables more welcome.
    108 //
    109 // Different messages using the same inflections can reuse variables by moving
    110 // them to macros. Using macros we can rewrite the message as:
    111 //
    112 //	// argument 1: list of hosts, argument 2: list of guests
    113 //	catalog.SetString(language.English, "%[1]v invite(s) %[2]v to their party.",
    114 //		"%[1]v ${invites(1)} %[2]v to ${their(1)} party.")
    115 //
    116 // Where the following macros were defined separately.
    117 //
    118 //	catalog.SetMacro(language.English, "invites", plural.Selectf(1, "",
    119 //		plural.One, "invite"))
    120 //	catalog.SetMacro(language.English, "their", plural.Selectf(1, "",
    121 //		plural.One, gender.Select(1, "female", "her", "other", "his"))),
    122 //
    123 // Placeholders use parentheses and the arguments to invoke a macro.
    124 //
    125 // # Looking up messages
    126 //
    127 // Message lookup using Catalogs is typically only done by specialized packages
    128 // and is not something the user should be concerned with. For instance, to
    129 // express the tardiness of a user using the related message we defined earlier,
    130 // the user may use the package message like so:
    131 //
    132 //	p := message.NewPrinter(language.English)
    133 //	p.Printf("You are %d minute(s) late.", 5)
    134 //
    135 // Which would print:
    136 //
    137 //	You are 5 minutes late.
    138 //
    139 // This package is UNDER CONSTRUCTION and its API may change.
    140 package catalog // import "golang.org/x/text/message/catalog"
    141 
    142 // TODO:
    143 // Some way to freeze a catalog.
    144 // - Locking on each lockup turns out to be about 50% of the total running time
    145 //   for some of the benchmarks in the message package.
    146 // Consider these:
    147 // - Sequence type to support sequences in user-defined messages.
    148 // - Garbage collection: Remove dictionaries that can no longer be reached
    149 //   as other dictionaries have been added that cover all possible keys.
    150 
    151 import (
    152 	"errors"
    153 	"fmt"
    154 
    155 	"golang.org/x/text/internal"
    156 
    157 	"golang.org/x/text/internal/catmsg"
    158 	"golang.org/x/text/language"
    159 )
    160 
    161 // A Catalog allows lookup of translated messages.
    162 type Catalog interface {
    163 	// Languages returns all languages for which the Catalog contains variants.
    164 	Languages() []language.Tag
    165 
    166 	// Matcher returns a Matcher for languages from this Catalog.
    167 	Matcher() language.Matcher
    168 
    169 	// A Context is used for evaluating Messages.
    170 	Context(tag language.Tag, r catmsg.Renderer) *Context
    171 
    172 	// This method also makes Catalog a private interface.
    173 	lookup(tag language.Tag, key string) (data string, ok bool)
    174 }
    175 
    176 // NewFromMap creates a Catalog from the given map. If a Dictionary is
    177 // underspecified the entry is retrieved from a parent language.
    178 func NewFromMap(dictionaries map[string]Dictionary, opts ...Option) (Catalog, error) {
    179 	options := options{}
    180 	for _, o := range opts {
    181 		o(&options)
    182 	}
    183 	c := &catalog{
    184 		dicts: map[language.Tag]Dictionary{},
    185 	}
    186 	_, hasFallback := dictionaries[options.fallback.String()]
    187 	if hasFallback {
    188 		// TODO: Should it be okay to not have a fallback language?
    189 		// Catalog generators could enforce there is always a fallback.
    190 		c.langs = append(c.langs, options.fallback)
    191 	}
    192 	for lang, dict := range dictionaries {
    193 		tag, err := language.Parse(lang)
    194 		if err != nil {
    195 			return nil, fmt.Errorf("catalog: invalid language tag %q", lang)
    196 		}
    197 		if _, ok := c.dicts[tag]; ok {
    198 			return nil, fmt.Errorf("catalog: duplicate entry for tag %q after normalization", tag)
    199 		}
    200 		c.dicts[tag] = dict
    201 		if !hasFallback || tag != options.fallback {
    202 			c.langs = append(c.langs, tag)
    203 		}
    204 	}
    205 	if hasFallback {
    206 		internal.SortTags(c.langs[1:])
    207 	} else {
    208 		internal.SortTags(c.langs)
    209 	}
    210 	c.matcher = language.NewMatcher(c.langs)
    211 	return c, nil
    212 }
    213 
    214 // A Dictionary is a source of translations for a single language.
    215 type Dictionary interface {
    216 	// Lookup returns a message compiled with catmsg.Compile for the given key.
    217 	// It returns false for ok if such a message could not be found.
    218 	Lookup(key string) (data string, ok bool)
    219 }
    220 
    221 type catalog struct {
    222 	langs   []language.Tag
    223 	dicts   map[language.Tag]Dictionary
    224 	macros  store
    225 	matcher language.Matcher
    226 }
    227 
    228 func (c *catalog) Languages() []language.Tag { return c.langs }
    229 func (c *catalog) Matcher() language.Matcher { return c.matcher }
    230 
    231 func (c *catalog) lookup(tag language.Tag, key string) (data string, ok bool) {
    232 	for ; ; tag = tag.Parent() {
    233 		if dict, ok := c.dicts[tag]; ok {
    234 			if data, ok := dict.Lookup(key); ok {
    235 				return data, true
    236 			}
    237 		}
    238 		if tag == language.Und {
    239 			break
    240 		}
    241 	}
    242 	return "", false
    243 }
    244 
    245 // Context returns a Context for formatting messages.
    246 // Only one Message may be formatted per context at any given time.
    247 func (c *catalog) Context(tag language.Tag, r catmsg.Renderer) *Context {
    248 	return &Context{
    249 		cat: c,
    250 		tag: tag,
    251 		dec: catmsg.NewDecoder(tag, r, &dict{&c.macros, tag}),
    252 	}
    253 }
    254 
    255 // A Builder allows building a Catalog programmatically.
    256 type Builder struct {
    257 	options
    258 	matcher language.Matcher
    259 
    260 	index  store
    261 	macros store
    262 }
    263 
    264 type options struct {
    265 	fallback language.Tag
    266 }
    267 
    268 // An Option configures Catalog behavior.
    269 type Option func(*options)
    270 
    271 // Fallback specifies the default fallback language. The default is Und.
    272 func Fallback(tag language.Tag) Option {
    273 	return func(o *options) { o.fallback = tag }
    274 }
    275 
    276 // TODO:
    277 // // Catalogs specifies one or more sources for a Catalog.
    278 // // Lookups are in order.
    279 // // This can be changed inserting a Catalog used for setting, which implements
    280 // // Loader, used for setting in the chain.
    281 // func Catalogs(d ...Loader) Option {
    282 // 	return nil
    283 // }
    284 //
    285 // func Delims(start, end string) Option {}
    286 //
    287 // func Dict(tag language.Tag, d ...Dictionary) Option
    288 
    289 // NewBuilder returns an empty mutable Catalog.
    290 func NewBuilder(opts ...Option) *Builder {
    291 	c := &Builder{}
    292 	for _, o := range opts {
    293 		o(&c.options)
    294 	}
    295 	return c
    296 }
    297 
    298 // SetString is shorthand for Set(tag, key, String(msg)).
    299 func (c *Builder) SetString(tag language.Tag, key string, msg string) error {
    300 	return c.set(tag, key, &c.index, String(msg))
    301 }
    302 
    303 // Set sets the translation for the given language and key.
    304 //
    305 // When evaluation this message, the first Message in the sequence to msgs to
    306 // evaluate to a string will be the message returned.
    307 func (c *Builder) Set(tag language.Tag, key string, msg ...Message) error {
    308 	return c.set(tag, key, &c.index, msg...)
    309 }
    310 
    311 // SetMacro defines a Message that may be substituted in another message.
    312 // The arguments to a macro Message are passed as arguments in the
    313 // placeholder the form "${foo(arg1, arg2)}".
    314 func (c *Builder) SetMacro(tag language.Tag, name string, msg ...Message) error {
    315 	return c.set(tag, name, &c.macros, msg...)
    316 }
    317 
    318 // ErrNotFound indicates there was no message for the given key.
    319 var ErrNotFound = errors.New("catalog: message not found")
    320 
    321 // String specifies a plain message string. It can be used as fallback if no
    322 // other strings match or as a simple standalone message.
    323 //
    324 // It is an error to pass more than one String in a message sequence.
    325 func String(name string) Message {
    326 	return catmsg.String(name)
    327 }
    328 
    329 // Var sets a variable that may be substituted in formatting patterns using
    330 // named substitution of the form "${name}". The name argument is used as a
    331 // fallback if the statements do not produce a match. The statement sequence may
    332 // not contain any Var calls.
    333 //
    334 // The name passed to a Var must be unique within message sequence.
    335 func Var(name string, msg ...Message) Message {
    336 	return &catmsg.Var{Name: name, Message: catmsg.FirstOf(msg)}
    337 }
    338 
    339 // Context returns a Context for formatting messages.
    340 // Only one Message may be formatted per context at any given time.
    341 func (b *Builder) Context(tag language.Tag, r catmsg.Renderer) *Context {
    342 	return &Context{
    343 		cat: b,
    344 		tag: tag,
    345 		dec: catmsg.NewDecoder(tag, r, &dict{&b.macros, tag}),
    346 	}
    347 }
    348 
    349 // A Context is used for evaluating Messages.
    350 // Only one Message may be formatted per context at any given time.
    351 type Context struct {
    352 	cat Catalog
    353 	tag language.Tag // TODO: use compact index.
    354 	dec *catmsg.Decoder
    355 }
    356 
    357 // Execute looks up and executes the message with the given key.
    358 // It returns ErrNotFound if no message could be found in the index.
    359 func (c *Context) Execute(key string) error {
    360 	data, ok := c.cat.lookup(c.tag, key)
    361 	if !ok {
    362 		return ErrNotFound
    363 	}
    364 	return c.dec.Execute(data)
    365 }