242 lines
7.2 KiB
Go
242 lines
7.2 KiB
Go
|
// 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"`
|
||
|
// }
|