cmd/gotext: move to Placeholder model
This more general model fits better with template-
style substitution, while still fitting well with
printf-style. It also allows hiding HTML and the like.
Modifies printf-substitution to be position-independent.
Change-Id: Ie8bd64c4fec9b8833bf8952bd02a8f3f56139e59
Reviewed-on: https://go-review.googlesource.com/79916
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/cmd/gotext/examples/main.go b/cmd/gotext/examples/main.go
index e1a84e0..08ac143 100644
--- a/cmd/gotext/examples/main.go
+++ b/cmd/gotext/examples/main.go
@@ -45,13 +45,16 @@
pp := struct {
Person string // The person of matter. // TODO: get this comment.
Place string
+ extra int
}{
- person, place,
+ person, place, 4,
}
// extract will drop this comment in favor of the one below.
- p.Printf("%s is visiting %s!\n", // Person visiting a place.
+ // argument is added as a placeholder.
+ p.Printf("%[1]s is visiting %[3]s!\n", // Person visiting a place.
pp.Person,
+ pp.extra,
pp.Place, // Place the person is visiting.
)
@@ -76,4 +79,8 @@
const msgOutOfOrder = "%s is out of order!" // FOO
const device = "Soda machine"
p.Printf(msgOutOfOrder, device)
+
+ // Double arguments.
+ miles := 1.2345
+ p.Printf("%.2[1]f miles traveled (%[1]f)", miles)
}
diff --git a/cmd/gotext/examples/textdata/gotext_en.out.json b/cmd/gotext/examples/textdata/gotext_en.out.json
index e6ebe5d..3dd943a 100755
--- a/cmd/gotext/examples/textdata/gotext_en.out.json
+++ b/cmd/gotext/examples/textdata/gotext_en.out.json
@@ -15,17 +15,14 @@
"message": {
"msg": "Hello {City}!\n"
},
- "args": [
+ "placeholders": [
{
"id": "City",
- "argNum": 1,
- "format": [
- "%s"
- ],
+ "string": "%[1]s",
"type": "string",
"underlyingType": "string",
- "expr": "city",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:31:26"
+ "argNum": 1,
+ "expr": "city"
}
],
"position": "golang.org/x/text/cmd/gotext/examples/main.go:31:10"
@@ -37,18 +34,15 @@
"message": {
"msg": "Hello {Town}!\n"
},
- "args": [
+ "placeholders": [
{
"id": "Town",
- "argNum": 1,
- "format": [
- "%s"
- ],
+ "string": "%[1]s",
"type": "string",
"underlyingType": "string",
+ "argNum": 1,
"expr": "town",
- "comment": "Town",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:36:3"
+ "comment": "Town"
}
],
"position": "golang.org/x/text/cmd/gotext/examples/main.go:35:10"
@@ -60,68 +54,64 @@
"message": {
"msg": "{Person} is visiting {Place}!\n"
},
- "args": [
+ "placeholders": [
{
"id": "Person",
- "argNum": 1,
- "format": [
- "%s"
- ],
+ "string": "%[1]s",
"type": "string",
"underlyingType": "string",
+ "argNum": 1,
"expr": "person",
- "comment": "The person of matter.",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:41:3"
+ "comment": "The person of matter."
},
{
"id": "Place",
- "argNum": 2,
- "format": [
- "%s"
- ],
+ "string": "%[2]s",
"type": "string",
"underlyingType": "string",
+ "argNum": 2,
"expr": "place",
- "comment": "Place the person is visiting.",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:42:3"
+ "comment": "Place the person is visiting."
}
],
"position": "golang.org/x/text/cmd/gotext/examples/main.go:40:10"
},
{
"key": [
- "%s is visiting %s!\n"
+ "%[1]s is visiting %[3]s!\n"
],
"message": {
"msg": "{Person} is visiting {Place}!\n"
},
"comment": "Person visiting a place.",
- "args": [
+ "placeholders": [
{
"id": "Person",
- "argNum": 1,
- "format": [
- "%s"
- ],
+ "string": "%[1]s",
"type": "string",
"underlyingType": "string",
- "expr": "pp.Person",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:54:3"
+ "argNum": 1,
+ "expr": "pp.Person"
},
{
"id": "Place",
- "argNum": 2,
- "format": [
- "%s"
- ],
+ "string": "%[3]s",
"type": "string",
"underlyingType": "string",
+ "argNum": 3,
"expr": "pp.Place",
- "comment": "Place the person is visiting.",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:55:3"
+ "comment": "Place the person is visiting."
+ },
+ {
+ "id": "Extra",
+ "string": "%[2]v",
+ "type": "int",
+ "underlyingType": "int",
+ "argNum": 2,
+ "expr": "pp.extra"
}
],
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:53:10"
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:55:10"
},
{
"key": [
@@ -130,21 +120,17 @@
"message": {
"msg": "{2} files remaining!"
},
- "args": [
+ "placeholders": [
{
"id": "2",
- "argNum": 1,
- "format": [
- "%d"
- ],
+ "string": "%[1]d",
"type": "int",
"underlyingType": "int",
- "expr": "2",
- "value": "2",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:59:34"
+ "argNum": 1,
+ "expr": "2"
}
],
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:59:10"
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:62:10"
},
{
"key": [
@@ -153,21 +139,17 @@
"message": {
"msg": "{N} more files remaining!"
},
- "args": [
+ "placeholders": [
{
"id": "N",
- "argNum": 1,
- "format": [
- "%d"
- ],
+ "string": "%[1]d",
"type": "int",
"underlyingType": "int",
- "expr": "n",
- "value": "2",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:64:39"
+ "argNum": 1,
+ "expr": "n"
}
],
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:64:10"
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:67:10"
},
{
"key": [
@@ -176,21 +158,17 @@
"message": {
"msg": "Use the following code for your discount: {ReferralCode}\n"
},
- "args": [
+ "placeholders": [
{
"id": "ReferralCode",
- "argNum": 1,
- "format": [
- "%d"
- ],
+ "string": "%[1]d",
"type": "golang.org/x/text/cmd/gotext/examples.referralCode",
"underlyingType": "int",
- "expr": "c",
- "value": "5",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:70:61"
+ "argNum": 1,
+ "expr": "c"
}
],
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:70:10"
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:73:10"
},
{
"key": [
@@ -201,20 +179,43 @@
"msg": "{Device} is out of order!"
},
"comment": "FOO\n",
- "args": [
+ "placeholders": [
{
"id": "Device",
- "argNum": 1,
- "format": [
- "%s"
- ],
+ "string": "%[1]s",
"type": "string",
"underlyingType": "string",
- "expr": "device",
- "value": "\"Soda machine\"",
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:78:26"
+ "argNum": 1,
+ "expr": "device"
}
],
- "position": "golang.org/x/text/cmd/gotext/examples/main.go:78:10"
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:81:10"
+ },
+ {
+ "key": [
+ "%.2[1]f miles traveled (%[1]f)"
+ ],
+ "message": {
+ "msg": "{Miles} miles traveled ({Miles_1})"
+ },
+ "placeholders": [
+ {
+ "id": "Miles",
+ "string": "%.2[1]f",
+ "type": "float64",
+ "underlyingType": "float64",
+ "argNum": 1,
+ "expr": "miles"
+ },
+ {
+ "id": "Miles_1",
+ "string": "%[1]f",
+ "type": "float64",
+ "underlyingType": "float64",
+ "argNum": 1,
+ "expr": "miles"
+ }
+ ],
+ "position": "golang.org/x/text/cmd/gotext/examples/main.go:85:10"
}
]
\ No newline at end of file
diff --git a/cmd/gotext/extract.go b/cmd/gotext/extract.go
index ac318d1..3b6120d 100644
--- a/cmd/gotext/extract.go
+++ b/cmd/gotext/extract.go
@@ -21,6 +21,7 @@
"path/filepath"
"strings"
"unicode"
+ "unicode/utf8"
fmtparser "golang.org/x/text/internal/format"
"golang.org/x/tools/go/loader"
@@ -135,7 +136,7 @@
}
key = append(key, fmtMsg)
- arguments := []Argument{}
+ arguments := []argument{}
args = args[1:]
simArgs := make([]interface{}, len(args))
for i, arg := range args {
@@ -149,7 +150,7 @@
expr = val
}
}
- arguments = append(arguments, Argument{
+ arguments = append(arguments, argument{
ArgNum: i + 1,
Type: info.Types[arg].Type.String(),
UnderlyingType: info.Types[arg].Type.Underlying().String(),
@@ -164,6 +165,8 @@
}
msg := ""
+ ph := placeholders{index: map[string]string{}}
+
p := fmtparser.Parser{}
p.Reset(simArgs)
for p.SetFormat(fmtMsg); p.Scan(); {
@@ -173,28 +176,37 @@
case fmtparser.StatusSubstitution,
fmtparser.StatusBadWidthSubstitution,
fmtparser.StatusBadPrecSubstitution:
+ arguments[p.ArgNum-1].used = true
arg := arguments[p.ArgNum-1]
- id := getID(&arg)
- arguments[p.ArgNum-1].ID = id
- // TODO: do we allow the same entry to be formatted
- // differently within the same string, do we give
- // a warning, or is this an error?
- arguments[p.ArgNum-1].Format = append(arguments[p.ArgNum-1].Format, p.Text())
- msg += fmt.Sprintf("{%s}", id)
+ sub := p.Text()
+ if !p.HasIndex {
+ r, sz := utf8.DecodeLastRuneInString(sub)
+ sub = fmt.Sprintf("%s[%d]%c", sub[:len(sub)-sz], p.ArgNum, r)
+ }
+ msg += fmt.Sprintf("{%s}", ph.addArg(&arg, sub))
}
}
+ // Add additional Placeholders that can be used in translations
+ // that are not present in the string.
+ for _, arg := range arguments {
+ if arg.used {
+ continue
+ }
+ ph.addArg(&arg, fmt.Sprintf("%%[%d]v", arg.ArgNum))
+ }
+
if c := getComment(call.Args[0]); c != "" {
comment = c
}
messages = append(messages, Message{
- Key: key,
- Position: posString(conf, info, call.Lparen),
- Message: Text{Msg: msg},
+ Key: key,
+ Message: Text{Msg: msg},
// TODO(fix): this doesn't get the before comment.
- Comment: comment,
- Args: arguments,
+ Comment: comment,
+ Placeholders: ph.slice,
+ Position: posString(conf, info, call.Lparen),
})
return true
})
@@ -245,7 +257,7 @@
arg int
}
-func getID(arg *Argument) string {
+func getID(arg *argument) string {
s := getLastComponent(arg.Expr)
s = strings.Replace(s, " ", "", -1)
// For small variable names, use user-defined types for more info.
@@ -255,6 +267,32 @@
return strings.Title(s)
}
+type placeholders struct {
+ index map[string]string
+ slice []Placeholder
+}
+
+func (p *placeholders) addArg(arg *argument, sub string) (id string) {
+ id = getID(arg)
+ id1 := id
+ alt, ok := p.index[id1]
+ for i := 1; ok && alt != sub; i++ {
+ id1 = fmt.Sprintf("%s_%d", id, i)
+ alt, ok = p.index[id1]
+ }
+ p.index[id1] = sub
+ p.slice = append(p.slice, Placeholder{
+ ID: id1,
+ String: sub,
+ Type: arg.Type,
+ UnderlyingType: arg.UnderlyingType,
+ ArgNum: arg.ArgNum,
+ Expr: arg.Expr,
+ Comment: arg.Comment,
+ })
+ return id1
+}
+
func getLastComponent(s string) string {
return s[1+strings.LastIndexByte(s, '.'):]
}
diff --git a/cmd/gotext/message.go b/cmd/gotext/message.go
index 7344f8d..567cf5d 100644
--- a/cmd/gotext/message.go
+++ b/cmd/gotext/message.go
@@ -28,36 +28,55 @@
Comment string `json:"comment,omitempty"`
TranslatorComment string `json:"translatorComment,omitempty"`
- // TODO: have a separate placeholder list, mapping placeholders
- // to arguments or constant strings.
- // TODO: default placeholder syntax is {foo}. Allow alternatives
- // like `foo`.
+ Placeholders []Placeholder `json:"placeholders,omitempty"`
- Args []Argument `json:"args,omitempty"`
+ // TODO: default placeholder syntax is {foo}. Allow alternative escaping
+ // like `foo`.
// Extraction information.
Position string `json:"position,omitempty"` // filePosition:line
}
-// An Argument contains information about the arguments passed to a message.
-type Argument struct {
- ID string `json:"id"` // An int for printf-style calls, but could be a string.
- // Argument position for printf-style format strings. ArgNum corresponds to
- // the number that should be used for explicit argument indexes (e.g.
- // "%[1]d").
- ArgNum int `json:"argNum,omitempty"`
- Format []string `json:"format,omitempty"`
+// 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"`
-
- // Features contains the features that are available for the implementation
- // of this argument.
- Features []Feature `json:"features,omitempty"`
}
// Feature holds information about a feature that can be implemented by
@@ -71,13 +90,15 @@
// Text defines a message to be displayed.
type Text struct {
- // Msg and Select contains the message to be displayed. Within a Text value
- // either Msg or Select is defined.
+ // 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"`
}
@@ -86,6 +107,12 @@
// a certain argument.
type Select struct {
Feature string `json:"feature"` // Name of variable or Feature type
- Arg interface{} `json:"arg"` // The argument ID.
+ 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"`
+// }
diff --git a/internal/format/parser.go b/internal/format/parser.go
index 68a8e8e..f4f37f4 100644
--- a/internal/format/parser.go
+++ b/internal/format/parser.go
@@ -28,6 +28,8 @@
PlusV bool
SharpV bool
+ HasIndex bool
+
Width int
Prec int // precision
@@ -94,6 +96,8 @@
p.PlusV = false
p.SharpV = false
+
+ p.HasIndex = false
}
// Scan scans the next part of the format string and sets the status to
@@ -220,6 +224,7 @@
if !afterIndex {
i, afterIndex = p.updateArgNumber(format, i)
}
+ p.HasIndex = afterIndex
if i >= end {
p.endPos = i