summaryrefslogtreecommitdiffhomepage
path: root/vendor/golang.org/x/text/message/pipeline/message.go
blob: c83a8fd8780c41f568ae450eeed4aa53b4501187 (plain) (blame)
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
// 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 pipeline

import (
	"encoding/json"
	"errors"
	"strings"

	"golang.org/x/text/language"
)

// TODO: these definitions should be moved to a package so that the can be used
// by other tools.

// The file contains the structures used to define translations of a certain
// messages.
//
// A translation may have multiple translations strings, or messages, depending
// on the feature values of the various arguments. For instance, consider
// a hypothetical translation from English to English, where the source defines
// the format string "%d file(s) remaining".
// See the examples directory for examples of extracted messages.

// Messages is used to store translations for a single language.
type Messages struct {
	Language language.Tag    `json:"language"`
	Messages []Message       `json:"messages"`
	Macros   map[string]Text `json:"macros,omitempty"`
}

// A Message describes a message to be translated.
type Message struct {
	// ID contains a list of identifiers for the message.
	ID IDList `json:"id"`
	// Key is the string that is used to look up the message at runtime.
	Key         string `json:"key,omitempty"`
	Meaning     string `json:"meaning,omitempty"`
	Message     Text   `json:"message"`
	Translation Text   `json:"translation"`

	Comment           string `json:"comment,omitempty"`
	TranslatorComment string `json:"translatorComment,omitempty"`

	Placeholders []Placeholder `json:"placeholders,omitempty"`

	// Fuzzy indicates that the provide translation needs review by a
	// translator, for instance because it was derived from automated
	// translation.
	Fuzzy bool `json:"fuzzy,omitempty"`

	// TODO: default placeholder syntax is {foo}. Allow alternative escaping
	// like `foo`.

	// Extraction information.
	Position string `json:"position,omitempty"` // filePosition:line
}

// Placeholder reports the placeholder for the given ID if it is defined or nil
// otherwise.
func (m *Message) Placeholder(id string) *Placeholder {
	for _, p := range m.Placeholders {
		if p.ID == id {
			return &p
		}
	}
	return nil
}

// Substitute replaces placeholders in msg with their original value.
func (m *Message) Substitute(msg string) (sub string, err error) {
	last := 0
	for i := 0; i < len(msg); {
		pLeft := strings.IndexByte(msg[i:], '{')
		if pLeft == -1 {
			break
		}
		pLeft += i
		pRight := strings.IndexByte(msg[pLeft:], '}')
		if pRight == -1 {
			return "", errorf("unmatched '}'")
		}
		pRight += pLeft
		id := strings.TrimSpace(msg[pLeft+1 : pRight])
		i = pRight + 1
		if id != "" && id[0] == '$' {
			continue
		}
		sub += msg[last:pLeft]
		last = i
		ph := m.Placeholder(id)
		if ph == nil {
			return "", errorf("unknown placeholder %q in message %q", id, msg)
		}
		sub += ph.String
	}
	sub += msg[last:]
	return sub, err
}

var errIncompatibleMessage = errors.New("messages incompatible")

func checkEquivalence(a, b *Message) error {
	for _, v := range a.ID {
		for _, w := range b.ID {
			if v == w {
				return nil
			}
		}
	}
	// TODO: canonicalize placeholders and check for type equivalence.
	return errIncompatibleMessage
}

// A Placeholder is a part of the message that should not be changed by a
// translator. It can be used to hide or prettify format strings (e.g. %d or
// {{.Count}}), hide HTML, or mark common names that should not be translated.
type Placeholder struct {
	// ID is the placeholder identifier without the curly braces.
	ID string `json:"id"`

	// String is the string with which to replace the placeholder. This may be a
	// formatting string (for instance "%d" or "{{.Count}}") or a literal string
	// (<div>).
	String string `json:"string"`

	Type           string `json:"type"`
	UnderlyingType string `json:"underlyingType"`
	// ArgNum and Expr are set if the placeholder is a substitution of an
	// argument.
	ArgNum int    `json:"argNum,omitempty"`
	Expr   string `json:"expr,omitempty"`

	Comment string `json:"comment,omitempty"`
	Example string `json:"example,omitempty"`

	// Features contains the features that are available for the implementation
	// of this argument.
	Features []Feature `json:"features,omitempty"`
}

// An argument contains information about the arguments passed to a message.
type argument struct {
	// ArgNum corresponds to the number that should be used for explicit argument indexes (e.g.
	// "%[1]d").
	ArgNum int `json:"argNum,omitempty"`

	used           bool   // Used by Placeholder
	Type           string `json:"type"`
	UnderlyingType string `json:"underlyingType"`
	Expr           string `json:"expr"`
	Value          string `json:"value,omitempty"`
	Comment        string `json:"comment,omitempty"`
	Position       string `json:"position,omitempty"`
}

// Feature holds information about a feature that can be implemented by
// an Argument.
type Feature struct {
	Type string `json:"type"` // Right now this is only gender and plural.

	// TODO: possible values and examples for the language under consideration.

}

// Text defines a message to be displayed.
type Text struct {
	// Msg and Select contains the message to be displayed. Msg may be used as
	// a fallback value if none of the select cases match.
	Msg    string  `json:"msg,omitempty"`
	Select *Select `json:"select,omitempty"`

	// Var defines a map of variables that may be substituted in the selected
	// message.
	Var map[string]Text `json:"var,omitempty"`

	// Example contains an example message formatted with default values.
	Example string `json:"example,omitempty"`
}

// IsEmpty reports whether this Text can generate anything.
func (t *Text) IsEmpty() bool {
	return t.Msg == "" && t.Select == nil && t.Var == nil
}

// rawText erases the UnmarshalJSON method.
type rawText Text

// UnmarshalJSON implements json.Unmarshaler.
func (t *Text) UnmarshalJSON(b []byte) error {
	if b[0] == '"' {
		return json.Unmarshal(b, &t.Msg)
	}
	return json.Unmarshal(b, (*rawText)(t))
}

// MarshalJSON implements json.Marshaler.
func (t *Text) MarshalJSON() ([]byte, error) {
	if t.Select == nil && t.Var == nil && t.Example == "" {
		return json.Marshal(t.Msg)
	}
	return json.Marshal((*rawText)(t))
}

// IDList is a set identifiers that each may refer to possibly different
// versions of the same message. When looking up a messages, the first
// identifier in the list takes precedence.
type IDList []string

// UnmarshalJSON implements json.Unmarshaler.
func (id *IDList) UnmarshalJSON(b []byte) error {
	if b[0] == '"' {
		*id = []string{""}
		return json.Unmarshal(b, &((*id)[0]))
	}
	return json.Unmarshal(b, (*[]string)(id))
}

// MarshalJSON implements json.Marshaler.
func (id *IDList) MarshalJSON() ([]byte, error) {
	if len(*id) == 1 {
		return json.Marshal((*id)[0])
	}
	return json.Marshal((*[]string)(id))
}

// Select selects a Text based on the feature value associated with a feature of
// a certain argument.
type Select struct {
	Feature string          `json:"feature"` // Name of Feature type (e.g plural)
	Arg     string          `json:"arg"`     // The placeholder ID
	Cases   map[string]Text `json:"cases"`
}

// TODO: order matters, but can we derive the ordering from the case keys?
// type Case struct {
// 	Key   string `json:"key"`
// 	Value Text   `json:"value"`
// }