github.com/go-enjin/golang-org-x-text@v0.12.1-enjin.2/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  	"github.com/go-enjin/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  // }