summaryrefslogtreecommitdiff
path: root/vendor/golang.org/x/text/message/catalog/catalog.go
blob: 34a30d3c8da79d62d2a533402356dbec6015e281 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
// 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.Select:
//
//   catalog.Set(language.English, "You are %d minute(s) late.",
//       plural.Select(1,
//           "one", "You are 1 minute late.",
//           "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.Select(1, "one", "minute", "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.Select(1, "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.Select(1,
//             "one", gender.Select(1, "female", "her", "other", "his"))),
//     catalog.Var("invites", plural.Select(1, "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.Select(1,
//         "one", gender.Select(1,
//             "female", "%[1]v invites %[2]v to her party."
//             "other", "%[1]v invites %[2]v to his party."),
//         "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.Select(1, "one", "invite"))
//   catalog.SetMacro(language.English, "their", plural.Select(1,
//      "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)
}