...

Source file src/golang.org/x/text/message/catalog/catalog.go

Documentation: golang.org/x/text/message/catalog

     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: firstInSequence(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  }
   366  

View as plain text