internal/{frontend,middleware}: group import count in search

- Add a middleware that extracts the preferred i18n information from the
  client.

- Use that information to format the imported-by count displayed in
  search.

Change-Id: Id6676c135cb6b094c53a039206ef4702256120d6
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/347553
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go
index e1e6e62..f8100b4 100644
--- a/cmd/frontend/main.go
+++ b/cmd/frontend/main.go
@@ -174,6 +174,7 @@
 		middleware.Panic(panicHandler),
 		ermw,
 		middleware.Timeout(54*time.Second),
+		middleware.Language,
 	)
 	addr := cfg.HostAddr(*hostAddr)
 	log.Infof(ctx, "Listening on addr %s", addr)
diff --git a/go.mod b/go.mod
index a3e9e88..de77081 100644
--- a/go.mod
+++ b/go.mod
@@ -43,6 +43,7 @@
 	golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449
 	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
 	golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
+	golang.org/x/text v0.3.6
 	golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c
 	google.golang.org/api v0.32.0
 	google.golang.org/genproto v0.0.0-20200923140941-5646d36feee1
diff --git a/internal/frontend/search.go b/internal/frontend/search.go
index 1b58297..dc09c6a 100644
--- a/internal/frontend/search.go
+++ b/internal/frontend/search.go
@@ -23,9 +23,11 @@
 	"golang.org/x/pkgsite/internal/derrors"
 	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/log"
+	"golang.org/x/pkgsite/internal/middleware"
 	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/stdlib"
 	"golang.org/x/pkgsite/internal/version"
+	"golang.org/x/text/message"
 )
 
 const defaultSearchLimit = 10
@@ -48,7 +50,7 @@
 	DisplayVersion string
 	Licenses       []string
 	CommitTime     string
-	NumImportedBy  int
+	NumImportedBy  string
 	Approximate    bool
 	Symbols        *subResult
 	SameModule     *subResult // package paths in the same module
@@ -88,7 +90,8 @@
 
 	var results []*SearchResult
 	for _, r := range dbresults {
-		results = append(results, newSearchResult(r, searchSymbols))
+		sr := newSearchResult(r, searchSymbols, message.NewPrinter(middleware.LanguageTag(ctx)))
+		results = append(results, sr)
 	}
 
 	var (
@@ -124,7 +127,7 @@
 	return sp, nil
 }
 
-func newSearchResult(r *postgres.SearchResult, searchSymbols bool) *SearchResult {
+func newSearchResult(r *postgres.SearchResult, searchSymbols bool, pr *message.Printer) *SearchResult {
 	// For commands, change the name from "main" to the last component of the import path.
 	chipText := ""
 	name := r.Name
@@ -146,7 +149,7 @@
 		DisplayVersion: displayVersion(r.ModulePath, r.Version, r.Version),
 		Licenses:       r.Licenses,
 		CommitTime:     elapsedTime(r.CommitTime),
-		NumImportedBy:  int(r.NumImportedBy),
+		NumImportedBy:  pr.Sprint(r.NumImportedBy),
 		SameModule:     packagePaths(moduleDesc+":", r.SameModule),
 		// Say "other" instead of "lower" because at some point we may
 		// prefer to show a tagged, lower major version over an untagged
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index 5a3a45b..35f1cfc 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -18,6 +18,8 @@
 	"golang.org/x/pkgsite/internal/licenses"
 	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/testing/sample"
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
 )
 
 func TestSearchQuery(t *testing.T) {
@@ -246,31 +248,37 @@
 func TestNewSearchResult(t *testing.T) {
 	for _, test := range []struct {
 		name string
+		tag  language.Tag
 		in   postgres.SearchResult
 		want SearchResult
 	}{
 		{
 			name: "basic",
+			tag:  language.English,
 			in: postgres.SearchResult{
-				Name:        "pkg",
-				PackagePath: "m.com/pkg",
-				ModulePath:  "m.com",
-				Version:     "v1.0.0",
+				Name:          "pkg",
+				PackagePath:   "m.com/pkg",
+				ModulePath:    "m.com",
+				Version:       "v1.0.0",
+				NumImportedBy: 3,
 			},
 			want: SearchResult{
 				Name:           "pkg",
 				PackagePath:    "m.com/pkg",
 				ModulePath:     "m.com",
 				DisplayVersion: "v1.0.0",
+				NumImportedBy:  "3",
 			},
 		},
 		{
 			name: "command",
+			tag:  language.English,
 			in: postgres.SearchResult{
-				Name:        "main",
-				PackagePath: "m.com/cmd",
-				ModulePath:  "m.com",
-				Version:     "v1.0.0",
+				Name:          "main",
+				PackagePath:   "m.com/cmd",
+				ModulePath:    "m.com",
+				Version:       "v1.0.0",
+				NumImportedBy: 1234,
 			},
 			want: SearchResult{
 				Name:           "cmd",
@@ -278,10 +286,12 @@
 				ModulePath:     "m.com",
 				DisplayVersion: "v1.0.0",
 				ChipText:       "command",
+				NumImportedBy:  "1,234",
 			},
 		},
 		{
 			name: "stdlib",
+			tag:  language.English,
 			in: postgres.SearchResult{
 				Name:        "math",
 				PackagePath: "math",
@@ -294,11 +304,31 @@
 				ModulePath:     "std",
 				DisplayVersion: "go1.14",
 				ChipText:       "standard library",
+				NumImportedBy:  "0",
+			},
+		},
+		{
+			name: "German",
+			tag:  language.German,
+			in: postgres.SearchResult{
+				Name:          "pkg",
+				PackagePath:   "m.com/pkg",
+				ModulePath:    "m.com",
+				Version:       "v1.0.0",
+				NumImportedBy: 3456,
+			},
+			want: SearchResult{
+				Name:           "pkg",
+				PackagePath:    "m.com/pkg",
+				ModulePath:     "m.com",
+				DisplayVersion: "v1.0.0",
+				NumImportedBy:  "3.456",
 			},
 		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			got := newSearchResult(&test.in, false)
+			pr := message.NewPrinter(test.tag)
+			got := newSearchResult(&test.in, false, pr)
 			test.want.CommitTime = "Jan  1, 0001"
 			if diff := cmp.Diff(&test.want, got); diff != "" {
 				t.Errorf("mimatch (-want, +got):\n%s", diff)
diff --git a/internal/middleware/language.go b/internal/middleware/language.go
new file mode 100644
index 0000000..e038e0a
--- /dev/null
+++ b/internal/middleware/language.go
@@ -0,0 +1,34 @@
+// Copyright 2021 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 middleware
+
+import (
+	"context"
+	"net/http"
+
+	"golang.org/x/text/language"
+	"golang.org/x/text/message"
+)
+
+type tagKey struct{}
+
+var matcher = language.NewMatcher(message.DefaultCatalog.Languages())
+
+// Language is a middleware that provides browser i18n information to handlers,
+// in the form of a golang.org/x/text/language.Tag.
+func Language(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tag, _ := language.MatchStrings(matcher, r.Header.Get("Accept-Language"))
+		h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), tagKey{}, tag)))
+	})
+}
+
+// LanguageTag returns the language.Tag from the context, or language.English if none is set.
+func LanguageTag(ctx context.Context) language.Tag {
+	if tag, ok := ctx.Value(tagKey{}).(language.Tag); ok {
+		return tag
+	}
+	return language.English
+}