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