// 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" "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. // Config contains configuration for the translation pipeline. type Config struct { SourceLanguage language.Tag // Supported indicates the languages for which data should be generated. // If unspecified, it will attempt to derive the set of supported languages // from the context. Supported []language.Tag Packages []string // TODO: // - Printf-style configuration // - Template-style configuration // - Extraction options // - Rewrite options // - Generation options } // A Locale is used to store all information for a single locale. This type is // used both for extraction and injection. type Locale 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"` 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"` // 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 } // 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 // (
). 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"` // }