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
5package pipeline
6
7import (
8	"encoding/json"
9	"errors"
10	"strings"
11
12	"golang.org/x/text/language"
13)
14
15// TODO: these definitions should be moved to a package so that the can be used
16// by other tools.
17
18// The file contains the structures used to define translations of a certain
19// messages.
20//
21// A translation may have multiple translations strings, or messages, depending
22// on the feature values of the various arguments. For instance, consider
23// a hypothetical translation from English to English, where the source defines
24// the format string "%d file(s) remaining".
25// See the examples directory for examples of extracted messages.
26
27// Messages is used to store translations for a single language.
28type Messages struct {
29	Language language.Tag    `json:"language"`
30	Messages []Message       `json:"messages"`
31	Macros   map[string]Text `json:"macros,omitempty"`
32}
33
34// A Message describes a message to be translated.
35type Message struct {
36	// ID contains a list of identifiers for the message.
37	ID IDList `json:"id"`
38	// Key is the string that is used to look up the message at runtime.
39	Key         string `json:"key,omitempty"`
40	Meaning     string `json:"meaning,omitempty"`
41	Message     Text   `json:"message"`
42	Translation Text   `json:"translation"`
43
44	Comment           string `json:"comment,omitempty"`
45	TranslatorComment string `json:"translatorComment,omitempty"`
46
47	Placeholders []Placeholder `json:"placeholders,omitempty"`
48
49	// Fuzzy indicates that the provide translation needs review by a
50	// translator, for instance because it was derived from automated
51	// translation.
52	Fuzzy bool `json:"fuzzy,omitempty"`
53
54	// TODO: default placeholder syntax is {foo}. Allow alternative escaping
55	// like `foo`.
56
57	// Extraction information.
58	Position string `json:"position,omitempty"` // filePosition:line
59}
60
61// Placeholder reports the placeholder for the given ID if it is defined or nil
62// otherwise.
63func (m *Message) Placeholder(id string) *Placeholder {
64	for _, p := range m.Placeholders {
65		if p.ID == id {
66			return &p
67		}
68	}
69	return nil
70}
71
72// Substitute replaces placeholders in msg with their original value.
73func (m *Message) Substitute(msg string) (sub string, err error) {
74	last := 0
75	for i := 0; i < len(msg); {
76		pLeft := strings.IndexByte(msg[i:], '{')
77		if pLeft == -1 {
78			break
79		}
80		pLeft += i
81		pRight := strings.IndexByte(msg[pLeft:], '}')
82		if pRight == -1 {
83			return "", errorf("unmatched '}'")
84		}
85		pRight += pLeft
86		id := strings.TrimSpace(msg[pLeft+1 : pRight])
87		i = pRight + 1
88		if id != "" && id[0] == '$' {
89			continue
90		}
91		sub += msg[last:pLeft]
92		last = i
93		ph := m.Placeholder(id)
94		if ph == nil {
95			return "", errorf("unknown placeholder %q in message %q", id, msg)
96		}
97		sub += ph.String
98	}
99	sub += msg[last:]
100	return sub, err
101}
102
103var errIncompatibleMessage = errors.New("messages incompatible")
104
105func checkEquivalence(a, b *Message) error {
106	for _, v := range a.ID {
107		for _, w := range b.ID {
108			if v == w {
109				return nil
110			}
111		}
112	}
113	// TODO: canonicalize placeholders and check for type equivalence.
114	return errIncompatibleMessage
115}
116
117// A Placeholder is a part of the message that should not be changed by a
118// translator. It can be used to hide or prettify format strings (e.g. %d or
119// {{.Count}}), hide HTML, or mark common names that should not be translated.
120type Placeholder struct {
121	// ID is the placeholder identifier without the curly braces.
122	ID string `json:"id"`
123
124	// String is the string with which to replace the placeholder. This may be a
125	// formatting string (for instance "%d" or "{{.Count}}") or a literal string
126	// (<div>).
127	String string `json:"string"`
128
129	Type           string `json:"type"`
130	UnderlyingType string `json:"underlyingType"`
131	// ArgNum and Expr are set if the placeholder is a substitution of an
132	// argument.
133	ArgNum int    `json:"argNum,omitempty"`
134	Expr   string `json:"expr,omitempty"`
135
136	Comment string `json:"comment,omitempty"`
137	Example string `json:"example,omitempty"`
138
139	// Features contains the features that are available for the implementation
140	// of this argument.
141	Features []Feature `json:"features,omitempty"`
142}
143
144// An argument contains information about the arguments passed to a message.
145type argument struct {
146	// ArgNum corresponds to the number that should be used for explicit argument indexes (e.g.
147	// "%[1]d").
148	ArgNum int `json:"argNum,omitempty"`
149
150	used           bool   // Used by Placeholder
151	Type           string `json:"type"`
152	UnderlyingType string `json:"underlyingType"`
153	Expr           string `json:"expr"`
154	Value          string `json:"value,omitempty"`
155	Comment        string `json:"comment,omitempty"`
156	Position       string `json:"position,omitempty"`
157}
158
159// Feature holds information about a feature that can be implemented by
160// an Argument.
161type Feature struct {
162	Type string `json:"type"` // Right now this is only gender and plural.
163
164	// TODO: possible values and examples for the language under consideration.
165
166}
167
168// Text defines a message to be displayed.
169type Text struct {
170	// Msg and Select contains the message to be displayed. Msg may be used as
171	// a fallback value if none of the select cases match.
172	Msg    string  `json:"msg,omitempty"`
173	Select *Select `json:"select,omitempty"`
174
175	// Var defines a map of variables that may be substituted in the selected
176	// message.
177	Var map[string]Text `json:"var,omitempty"`
178
179	// Example contains an example message formatted with default values.
180	Example string `json:"example,omitempty"`
181}
182
183// IsEmpty reports whether this Text can generate anything.
184func (t *Text) IsEmpty() bool {
185	return t.Msg == "" && t.Select == nil && t.Var == nil
186}
187
188// rawText erases the UnmarshalJSON method.
189type rawText Text
190
191// UnmarshalJSON implements json.Unmarshaler.
192func (t *Text) UnmarshalJSON(b []byte) error {
193	if b[0] == '"' {
194		return json.Unmarshal(b, &t.Msg)
195	}
196	return json.Unmarshal(b, (*rawText)(t))
197}
198
199// MarshalJSON implements json.Marshaler.
200func (t *Text) MarshalJSON() ([]byte, error) {
201	if t.Select == nil && t.Var == nil && t.Example == "" {
202		return json.Marshal(t.Msg)
203	}
204	return json.Marshal((*rawText)(t))
205}
206
207// IDList is a set identifiers that each may refer to possibly different
208// versions of the same message. When looking up a messages, the first
209// identifier in the list takes precedence.
210type IDList []string
211
212// UnmarshalJSON implements json.Unmarshaler.
213func (id *IDList) UnmarshalJSON(b []byte) error {
214	if b[0] == '"' {
215		*id = []string{""}
216		return json.Unmarshal(b, &((*id)[0]))
217	}
218	return json.Unmarshal(b, (*[]string)(id))
219}
220
221// MarshalJSON implements json.Marshaler.
222func (id *IDList) MarshalJSON() ([]byte, error) {
223	if len(*id) == 1 {
224		return json.Marshal((*id)[0])
225	}
226	return json.Marshal((*[]string)(id))
227}
228
229// Select selects a Text based on the feature value associated with a feature of
230// a certain argument.
231type Select struct {
232	Feature string          `json:"feature"` // Name of Feature type (e.g plural)
233	Arg     string          `json:"arg"`     // The placeholder ID
234	Cases   map[string]Text `json:"cases"`
235}
236
237// TODO: order matters, but can we derive the ordering from the case keys?
238// type Case struct {
239// 	Key   string `json:"key"`
240// 	Value Text   `json:"value"`
241// }
242