Add translators to the credits dialog

- Add tooling at //tools/i18n to generate the file with the information
  about the translators: //src/json/i18n-credits.json.
- Change credits.json to remove an entry from a translator, who is now
  mentioned in the i18n credits.
- Change Makefile and release.bash to incorporate the i18n credit
  generation in the process of building the extension.
- Change options page to accommodate the translators section.

Change-Id: I7f3991f9c456c381832f4a7bebdfc5581ef9e4be
diff --git a/tools/i18n/generate-i18n-credits.go b/tools/i18n/generate-i18n-credits.go
new file mode 100644
index 0000000..93fa702
--- /dev/null
+++ b/tools/i18n/generate-i18n-credits.go
@@ -0,0 +1,319 @@
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"strings"
+	"time"
+)
+
+const blockFileName = "blocked-users.txt"
+const projectId = "191707"
+const checkAttempts = 5
+const checkWaitTime = 2 * time.Second
+const baseApiURL = "https://api.crowdin.com/api/v2/"
+const i18nCreditsFile = "../../src/json/i18n-credits.json"
+
+// Contributors who have sent translations before the Crowdin instance
+// was set up:
+var additionalContributors = []Contributor{
+	Contributor{
+		Name: "Alexander Simkin",
+		Languages: []Language{
+			Language{
+				Id:   "ru",
+				Name: "Russian",
+			}},
+	}}
+
+type User struct {
+	Id        string `json:"id"`
+	Username  string `json:"username"`
+	FullName  string `json:"fullName"`
+	AvatarUrl string `json:"avatarUrl"`
+}
+
+type Language struct {
+	Id   string `json:"id"`
+	Name string `json:"name"`
+}
+
+type TopUser struct {
+	User          User       `json:"user"`
+	Languages     []Language `json:"languages"`
+	Translated    int        `json:"translated"`
+	Target        int        `json:"target"`
+	Approved      int        `json:"approved"`
+	Voted         int        `json:"voted"`
+	PositiveVotes int        `json:"positiveVotes"`
+	NegativeVotes int        `json:"negativeVotes"`
+	Winning       int        `json:"winning"`
+}
+
+type DateRange struct {
+	from string `json:"from"`
+	to   string `json:"to"`
+}
+
+type Report struct {
+	Name      string    `json:"name"`
+	Url       string    `json:"url"`
+	Unit      string    `json:"unit"`
+	DateRange DateRange `json:"dateRange"`
+	Language  string    `json:"language"`
+	TopUsers  []TopUser `json:"data"`
+}
+
+type Contributor struct {
+	Name      string     `json:"name"`
+	Languages []Language `json:"languages"`
+}
+
+func isBlocked(blockedUsers []string, user string) bool {
+	for _, u := range blockedUsers {
+		if user == u {
+			return true
+		}
+	}
+	return false
+}
+
+func getBlockedUsers() []string {
+	blockFile, err := os.Open(blockFileName)
+	if err != nil {
+		log.Fatalf("Couldn't open blockfile, error: %v", err)
+	}
+	defer blockFile.Close()
+
+	blocked := make([]string, 0)
+
+	scanner := bufio.NewScanner(blockFile)
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if len(line) == 0 || line[0] == '#' {
+			continue
+		}
+		blocked = append(blocked, line)
+	}
+
+	return blocked
+}
+
+func getJSONFromResponseBody(body *io.ReadCloser) (map[string]interface{}, error) {
+	var responseJSON map[string]interface{}
+
+	responseRawBody, err := ioutil.ReadAll(*body)
+	if err != nil {
+		return responseJSON, err
+	}
+	if err := json.Unmarshal(responseRawBody, &responseJSON); err != nil {
+		return responseJSON, err
+	}
+
+	return responseJSON, nil
+}
+
+func apiCall(path string, method string, body string) (*http.Response, error) {
+	token, isTokenSet := os.LookupEnv("GTRANSLATE_CROWDIN_API_KEY")
+	if !isTokenSet {
+		return nil, fmt.Errorf("Environmental variable GTRANSLATE_CROWDIN_API_KEY is not set.")
+	}
+
+	if body == "" {
+		body = "{}"
+	}
+
+	client := &http.Client{
+		Timeout: 10 * time.Second,
+	}
+
+	url := baseApiURL + path
+	bodyReader := strings.NewReader(body)
+	request, err := http.NewRequest(method, url, bodyReader)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Add("Authorization", "Bearer "+token)
+	request.Header.Add("Content-Type", "application/json")
+
+	return client.Do(request)
+}
+
+func generateReport(projectId string) (string, error) {
+	response, err := apiCall("projects/"+projectId+"/reports", "POST", `
+    {
+      "name": "top-members",
+      "schema": {
+        "unit": "words",
+        "format": "json"
+      }
+    }
+  `)
+	if err != nil {
+		return "", fmt.Errorf("Error while requesting top users report: %v", err)
+	}
+
+	if response.StatusCode != 201 {
+		return "", fmt.Errorf("Error while requesting top users report (status code %d)", response.StatusCode)
+	}
+
+	responseJSON, err := getJSONFromResponseBody(&response.Body)
+	if err != nil {
+		return "", err
+	}
+
+	data := responseJSON["data"].(map[string]interface{})
+	return data["identifier"].(string), nil
+}
+
+func isReportGenerated(projectId string, reportId string) (bool, error) {
+	response, err := apiCall("projects/"+projectId+"/reports/"+reportId, "GET", "{}")
+	if err != nil {
+		return false, fmt.Errorf("Error while checking report generation: %v", err)
+	}
+
+	if response.StatusCode != 200 {
+		return false, fmt.Errorf("Error while checking report generation (status code %d)", response.StatusCode)
+	}
+
+	responseJSON, err := getJSONFromResponseBody(&response.Body)
+	if err != nil {
+		return false, err
+	}
+
+	data := responseJSON["data"].(map[string]interface{})
+	return data["status"].(string) == "finished", nil
+}
+
+func getReportUrl(projectId string, reportId string) (string, error) {
+	response, err := apiCall("projects/"+projectId+"/reports/"+reportId+"/download", "GET", "{}")
+	if err != nil {
+		return "", fmt.Errorf("Error while retrieving top users report download URL: %v", err)
+	}
+
+	if response.StatusCode != 200 {
+		return "", fmt.Errorf("Error while retrieving top users report download URL (status code %d)", response.StatusCode)
+	}
+
+	responseJSON, err := getJSONFromResponseBody(&response.Body)
+	if err != nil {
+		return "", err
+	}
+
+	data := responseJSON["data"].(map[string]interface{})
+	return data["url"].(string), nil
+}
+
+func getReport(projectId string, reportId string) (Report, error) {
+	reportUrl, err := getReportUrl(projectId, reportId)
+	if err != nil {
+		return Report{}, err
+	}
+
+	response, err := http.Get(reportUrl)
+	if err != nil {
+		return Report{}, fmt.Errorf("An error occurred while downloading the report: %v", err)
+	}
+
+	var report Report
+
+	responseRawBody, err := ioutil.ReadAll(response.Body)
+	if err != nil {
+		return Report{}, err
+	}
+	if err := json.Unmarshal(responseRawBody, &report); err != nil {
+		return Report{}, err
+	}
+
+	return report, nil
+}
+
+func getContributorsFromReport(report Report) []Contributor {
+	blockedUsers := getBlockedUsers()
+
+	contributors := make([]Contributor, 0)
+	for _, c := range additionalContributors {
+		contributors = append(contributors, c)
+	}
+	for _, u := range report.TopUsers {
+		if u.Translated <= 0 || isBlocked(blockedUsers, u.User.Username) {
+			continue
+		}
+		contributors = append(contributors, Contributor{
+			Name:      u.User.FullName,
+			Languages: u.Languages,
+		})
+	}
+	return contributors
+}
+
+func GetContributors(projectId string) ([]Contributor, error) {
+	id, err := generateReport(projectId)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Printf("Top users report requested successfully (assigned id: %v)", id)
+
+	reportGenerated := false
+	for i := 0; i < checkAttempts; i++ {
+		currReportGenerated, err := isReportGenerated(projectId, id)
+		if err != nil {
+			log.Printf("[Try %d] Couldn't check whether the top users report has been generated, error: %v", i+1, err)
+		} else if currReportGenerated {
+			log.Printf("[Try %d] The top users report has been generated.", i+1)
+			reportGenerated = true
+			break
+		} else {
+			log.Printf("[Try %d] Top users report hasn't still been generated.", i+1)
+		}
+		time.Sleep(checkWaitTime)
+	}
+
+	if !reportGenerated {
+		return nil, fmt.Errorf("After %d checks, the top users report hasn't still been generated. Aborting.", checkAttempts)
+	}
+
+	report, err := getReport(projectId, id)
+	if err != nil {
+		return nil, fmt.Errorf("Couldn't retrieve top users report, error: %v", err)
+	}
+
+	return getContributorsFromReport(report), nil
+}
+
+func main() {
+	log.SetPrefix("[generate-i18n-credits] ")
+	log.SetFlags(0)
+
+	log.Println("Starting to generate i18n credits")
+
+	contributors, err := GetContributors(projectId)
+	if err != nil {
+		log.Fatalf("%v", err)
+	}
+
+	creditsFile, err := os.Create(i18nCreditsFile)
+	if err != nil {
+		log.Fatalf("Couldn't create i18n credits file, error: %v", err)
+	}
+	defer creditsFile.Close()
+
+	JSONBytes, err := json.MarshalIndent(contributors, "", "  ")
+	if err != nil {
+		log.Fatalf("Couldn't marshal Contributors interface, error: %v", err)
+	}
+
+	if _, err := creditsFile.Write(JSONBytes); err != nil {
+		log.Fatalf("Couldn't write to i18n credits file, error: %v", err)
+	}
+
+	log.Println("Done!")
+}