golang.org/x/text@v0.14.0/message/pipeline/message.go (about) 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 pipeline 6 7 import ( 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. 28 type 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. 35 type 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. 63 func (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. 73 func (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 103 var errIncompatibleMessage = errors.New("messages incompatible") 104 105 func 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. 120 type 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. 145 type 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. 161 type 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. 169 type 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. 184 func (t *Text) IsEmpty() bool { 185 return t.Msg == "" && t.Select == nil && t.Var == nil 186 } 187 188 // rawText erases the UnmarshalJSON method. 189 type rawText Text 190 191 // UnmarshalJSON implements json.Unmarshaler. 192 func (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. 200 func (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. 210 type IDList []string 211 212 // UnmarshalJSON implements json.Unmarshaler. 213 func (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. 222 func (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. 231 type 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 // }