message.go 7.2 KB

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