You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

371 lines
13 KiB

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