cmd/gotext: add update command

This is the recommended approach and will
likely replace export and generate.

Change-Id: Ie7dc0523c580715b8576de074f312b490a8727b6
Reviewed-on: https://go-review.googlesource.com/83776
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/cmd/gotext/examples/extract/catalog.go b/cmd/gotext/examples/extract/catalog.go
index 1a0d6d0..bc6130a 100644
--- a/cmd/gotext/examples/extract/catalog.go
+++ b/cmd/gotext/examples/extract/catalog.go
@@ -37,42 +37,48 @@
 }
 
 var messageKeyToIndex = map[string]int{
-	"%.2[1]f miles traveled (%[1]f)": 6,
-	"%[1]s is visiting %[3]s!\n":     3,
-	"%d more files remaining!":       4,
-	"%s is out of order!":            5,
-	"%s is visiting %s!\n":           2,
-	"Hello %s!\n":                    1,
-	"Hello world!\n":                 0,
+	"%.2[1]f miles traveled (%[1]f)":                 8,
+	"%[1]s is visiting %[3]s!\n":                     3,
+	"%d files remaining!":                            5,
+	"%d more files remaining!":                       4,
+	"%s is out of order!":                            7,
+	"%s is visiting %s!\n":                           2,
+	"Hello %s!\n":                                    1,
+	"Hello world!\n":                                 0,
+	"Use the following code for your discount: %d\n": 6,
 }
 
-var deIndex = []uint32{ // 8 elements
+var deIndex = []uint32{ // 10 elements
 	0x00000000, 0x00000011, 0x00000023, 0x0000003d,
 	0x00000057, 0x00000076, 0x00000076, 0x00000076,
-} // Size: 56 bytes
+	0x00000076, 0x00000076,
+} // Size: 64 bytes
 
 const deData string = "" + // Size: 118 bytes
 	"\x04\x00\x01\x0a\x0c\x02Hallo Welt!\x04\x00\x01\x0a\x0d\x02Hallo %[1]s!" +
 	"\x04\x00\x01\x0a\x15\x02%[1]s besucht %[2]s!\x04\x00\x01\x0a\x15\x02%[1]" +
 	"s besucht %[3]s!\x02Noch %[1]d Bestände zu gehen!"
 
-var en_USIndex = []uint32{ // 8 elements
+var en_USIndex = []uint32{ // 10 elements
 	0x00000000, 0x00000012, 0x00000024, 0x00000042,
-	0x00000060, 0x000000a3, 0x000000ba, 0x000000d9,
-} // Size: 56 bytes
+	0x00000060, 0x000000a3, 0x000000ba, 0x000000ef,
+	0x00000106, 0x00000125,
+} // Size: 64 bytes
 
-const en_USData string = "" + // Size: 217 bytes
-	"\x04\x00\x01\x0a\x0d\x02Hello world!\x04\x00\x01\x0a\x0d\x02Hello %[1]s!" +
+const en_USData string = "" + // Size: 293 bytes
+	"\x04\x00\x01\x0a\x0d\x02Hello world!\x04\x00\x01\x0a\x0d\x02Hello %[1]sn" +
 	"\x04\x00\x01\x0a\x19\x02%[1]s is visiting %[2]s!\x04\x00\x01\x0a\x19\x02" +
 	"%[1]s is visiting %[3]s!\x14\x01\x81\x01\x00\x02\x14\x02One file remaini" +
-	"ng!\x00&\x02There are %[1]d more files remaining!\x02%[1]s is out of ord" +
-	"er!\x02%.2[1]f miles traveled (%[1]f)"
+	"ng!\x00&\x02There are %[1]d more files remaining!\x02%[1]d files remaini" +
+	"ng!\x04\x00\x01\x0a0\x02Use the following code for your discount: %[1]d" +
+	"\x02%[1]s is out of order!\x02%.2[1]f miles traveled (%[1]f)"
 
-var zhIndex = []uint32{ // 8 elements
+var zhIndex = []uint32{ // 10 elements
 	0x00000000, 0x00000000, 0x00000000, 0x00000000,
 	0x00000000, 0x00000000, 0x00000000, 0x00000000,
-} // Size: 56 bytes
+	0x00000000, 0x00000000,
+} // Size: 64 bytes
 
 const zhData string = ""
 
-// Total table size 503 bytes (0KiB); checksum: A968BD6
+// Total table size 603 bytes (0KiB); checksum: 1D2754EE
diff --git a/cmd/gotext/examples/extract/main.go b/cmd/gotext/examples/extract/main.go
index cbbc370..414b453 100644
--- a/cmd/gotext/examples/extract/main.go
+++ b/cmd/gotext/examples/extract/main.go
@@ -4,8 +4,7 @@
 
 package main
 
-//go:generate gotext extract --lang=de,zh
-//go:generate gotext generate -out catalog.go
+//go:generate gotext update -out catalog.go
 
 import (
 	"golang.org/x/text/language"
diff --git a/cmd/gotext/examples/extract_http/catalog_gen.go b/cmd/gotext/examples/extract_http/catalog_gen.go
new file mode 100644
index 0000000..2c410dc
--- /dev/null
+++ b/cmd/gotext/examples/extract_http/catalog_gen.go
@@ -0,0 +1,57 @@
+// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
+
+package main
+
+import (
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+	"golang.org/x/text/message/catalog"
+)
+
+type dictionary struct {
+	index []uint32
+	data  string
+}
+
+func (d *dictionary) Lookup(key string) (data string, ok bool) {
+	p := messageKeyToIndex[key]
+	start, end := d.index[p], d.index[p+1]
+	if start == end {
+		return "", false
+	}
+	return d.data[start:end], true
+}
+
+func init() {
+	dict := map[string]catalog.Dictionary{
+		"en": &dictionary{index: enIndex, data: enData},
+		"zh": &dictionary{index: zhIndex, data: zhData},
+	}
+	fallback := language.MustParse("en")
+	cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
+	if err != nil {
+		panic(err)
+	}
+	message.DefaultCatalog = cat
+}
+
+var messageKeyToIndex = map[string]int{
+	"Do you like your browser (%s)?\n": 1,
+	"Hello %s!\n":                      0,
+}
+
+var enIndex = []uint32{ // 3 elements
+	0x00000000, 0x00000012, 0x00000039,
+} // Size: 36 bytes
+
+const enData string = "" + // Size: 57 bytes
+	"\x04\x00\x01\x0a\x0d\x02Hello %[1]s!\x04\x00\x01\x0a\x22\x02Do you like " +
+	"your browser (%[1]s)?"
+
+var zhIndex = []uint32{ // 3 elements
+	0x00000000, 0x00000000, 0x00000000,
+} // Size: 36 bytes
+
+const zhData string = ""
+
+// Total table size 129 bytes (0KiB); checksum: 9C146C82
diff --git a/cmd/gotext/examples/extract_http/locales/en/out.gotext.json b/cmd/gotext/examples/extract_http/locales/en/out.gotext.json
new file mode 100644
index 0000000..1391e58
--- /dev/null
+++ b/cmd/gotext/examples/extract_http/locales/en/out.gotext.json
@@ -0,0 +1,39 @@
+{
+    "language": "en",
+    "messages": [
+        {
+            "id": "Hello {From}!",
+            "message": "Hello {From}!",
+            "translation": "Hello {From}!",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "From",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "r.Header.Get(\"From\")"
+                }
+            ],
+            "fuzzy": true
+        },
+        {
+            "id": "Do you like your browser ({User_Agent})?",
+            "message": "Do you like your browser ({User_Agent})?",
+            "translation": "Do you like your browser ({User_Agent})?",
+            "translatorComment": "Copied from source.",
+            "placeholders": [
+                {
+                    "id": "User_Agent",
+                    "string": "%[1]s",
+                    "type": "string",
+                    "underlyingType": "string",
+                    "argNum": 1,
+                    "expr": "r.Header.Get(\"User-Agent\")"
+                }
+            ],
+            "fuzzy": true
+        }
+    ]
+}
\ No newline at end of file
diff --git a/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json b/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json
index 0bab4a7..7b26974 100755
--- a/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json
+++ b/cmd/gotext/examples/extract_http/locales/zh/out.gotext.json
@@ -3,7 +3,6 @@
     "messages": [
         {
             "id": "Hello {From}!",
-            "key": "Hello %s!\n",
             "message": "Hello {From}!",
             "translation": "",
             "placeholders": [
@@ -15,12 +14,10 @@
                     "argNum": 1,
                     "expr": "r.Header.Get(\"From\")"
                 }
-            ],
-            "position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:22:11"
+            ]
         },
         {
             "id": "Do you like your browser ({User_Agent})?",
-            "key": "Do you like your browser (%s)?\n",
             "message": "Do you like your browser ({User_Agent})?",
             "translation": "",
             "placeholders": [
@@ -32,8 +29,7 @@
                     "argNum": 1,
                     "expr": "r.Header.Get(\"User-Agent\")"
                 }
-            ],
-            "position": "golang.org/x/text/cmd/gotext/examples/extract_http/pkg/pkg.go:24:11"
+            ]
         }
     ]
 }
\ No newline at end of file
diff --git a/cmd/gotext/examples/extract_http/main.go b/cmd/gotext/examples/extract_http/main.go
index c1ac449..b5eb3b33 100644
--- a/cmd/gotext/examples/extract_http/main.go
+++ b/cmd/gotext/examples/extract_http/main.go
@@ -4,7 +4,7 @@
 
 package main
 
-//go:generate gotext extract --lang=de,zh
+//go:generate gotext -srclang=en update -out=catalog_gen.go -lang=en,zh
 
 import (
 	"net/http"
diff --git a/cmd/gotext/extract.go b/cmd/gotext/extract.go
index b59097b..103d7e6 100644
--- a/cmd/gotext/extract.go
+++ b/cmd/gotext/extract.go
@@ -14,10 +14,6 @@
 // - handle features (gender, plural)
 // - message rewriting
 
-var (
-	lang *string
-)
-
 func init() {
 	lang = cmdExtract.Flag.String("lang", "en-US", "comma-separated list of languages to process")
 }
diff --git a/cmd/gotext/generate.go b/cmd/gotext/generate.go
index c8d5d79..36820df 100644
--- a/cmd/gotext/generate.go
+++ b/cmd/gotext/generate.go
@@ -12,10 +12,6 @@
 	out = cmdGenerate.Flag.String("out", "", "output file to write to")
 }
 
-var (
-	out *string
-)
-
 var cmdGenerate = &Command{
 	Run:       runGenerate,
 	UsageLine: "generate <package>",
diff --git a/cmd/gotext/main.go b/cmd/gotext/main.go
index 1e6c3aa..73f6d91 100644
--- a/cmd/gotext/main.go
+++ b/cmd/gotext/main.go
@@ -47,6 +47,7 @@
 	}
 	return &pipeline.Config{
 		SourceLanguage:      tag,
+		Supported:           getLangs(),
 		TranslationsPattern: `messages\.(.*)\.json`,
 		GenFile:             *out,
 	}, nil
@@ -100,6 +101,7 @@
 // Commands lists the available commands and help topics.
 // The order here is the order in which they are printed by 'go help'.
 var commands = []*Command{
+	cmdUpdate,
 	cmdExtract,
 	cmdRewrite,
 	cmdGenerate,
diff --git a/cmd/gotext/update.go b/cmd/gotext/update.go
new file mode 100644
index 0000000..1260750
--- /dev/null
+++ b/cmd/gotext/update.go
@@ -0,0 +1,52 @@
+// Copyright 2016 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 main
+
+import (
+	"golang.org/x/text/message/pipeline"
+)
+
+// TODO:
+// - merge information into existing files
+// - handle different file formats (PO, XLIFF)
+// - handle features (gender, plural)
+// - message rewriting
+
+var (
+	lang *string
+	out  *string
+)
+
+func init() {
+	lang = cmdUpdate.Flag.String("lang", "en-US", "comma-separated list of languages to process")
+	out = cmdUpdate.Flag.String("out", "", "output file to write to")
+}
+
+var cmdUpdate = &Command{
+	Run:       runUpdate,
+	UsageLine: "update <package>* [-out <gofile>]",
+	Short:     "merge translations and generate catalog",
+}
+
+func runUpdate(cmd *Command, config *pipeline.Config, args []string) error {
+	config.Packages = args
+	state, err := pipeline.Extract(config)
+	if err != nil {
+		return wrap(err, "extract failed")
+	}
+	if err := state.Import(); err != nil {
+		return wrap(err, "import failed")
+	}
+	if err := state.Merge(); err != nil {
+		return wrap(err, "merge failed")
+	}
+	if err := state.Export(); err != nil {
+		return wrap(err, "export failed")
+	}
+	if *out != "" {
+		return wrap(state.Generate(), "generation failed")
+	}
+	return nil
+}
diff --git a/message/pipeline/generate.go b/message/pipeline/generate.go
index cb62c25..5d329b2 100644
--- a/message/pipeline/generate.go
+++ b/message/pipeline/generate.go
@@ -26,6 +26,8 @@
 var transRe = regexp.MustCompile(`messages\.(.*)\.json`)
 
 // Generate writes a Go file that defines a Catalog with translated messages.
+// Translations are retrieved from s.Messages, not s.Translations, so it
+// is assumed Merge has been called.
 func (s *State) Generate() error {
 	path := s.Config.GenPackage
 	if path == "" {
@@ -56,7 +58,8 @@
 }
 
 // WriteGen writes a Go file with the given package name to w that defines a
-// Catalog with translated messages.
+// Catalog with translated messages. Translations are retrieved from s.Messages,
+// not s.Translations, so it is assumed Merge has been called.
 func (s *State) WriteGen(w io.Writer, pkg string) error {
 	cw, err := s.generate()
 	if err != nil {
@@ -80,15 +83,12 @@
 }
 
 func (s *State) generate() (*gen.CodeWriter, error) {
-	// TODO: add in external input. Right now we assume that all files are
-	// manually created and stored in the textdata directory.
-
 	// Build up index of translations and original messages.
 	translations := map[language.Tag]map[string]Message{}
 	languages := []language.Tag{}
 	usedKeys := map[string]int{}
 
-	for _, loc := range s.Translations {
+	for _, loc := range s.Messages {
 		tag := loc.Language
 		if _, ok := translations[tag]; !ok {
 			translations[tag] = map[string]Message{}
diff --git a/message/pipeline/pipeline.go b/message/pipeline/pipeline.go
index 93d2602..cafd6f2 100644
--- a/message/pipeline/pipeline.go
+++ b/message/pipeline/pipeline.go
@@ -289,7 +289,7 @@
 
 	// Build index of translations.
 	translations := map[language.Tag]map[string]Message{}
-	languages := []language.Tag{}
+	languages := append([]language.Tag{}, s.Config.Supported...)
 
 	for _, t := range s.Translations {
 		tag := t.Language
@@ -308,7 +308,7 @@
 			}
 		}
 	}
-	internal.SortTags(languages)
+	languages = internal.UniqueTags(languages)
 
 	for _, tag := range languages {
 		ms := Messages{Language: tag}