From c3edab4387bc6967029543a5965395ba9233118d Mon Sep 17 00:00:00 2001
From: Stanislav Chzhen <s.chzhen@adguard.com>
Date: Tue, 21 Mar 2023 17:43:48 +0300
Subject: [PATCH] Pull request 1763: AG-20200-translation-script

Merge in DNS/adguard-home from AG-20200-translation-script to master

Squashed commit of the following:

commit 3113b77c0312219f8134324caa232a53c42a3988
Merge: bbd784ab f736d85e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 21 17:41:08 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit bbd784ab817955f3342d140644a3199d558c22b8
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 21 13:23:48 2023 +0300

    scripts: imp code

commit 7d379ab1fc2ae9858f8e7e3754de9be3d23153b6
Merge: 4f6278ad 1daabb97
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 21 12:01:16 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit 4f6278adb28287205a4fc89239e7ba776a15ff7a
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Fri Mar 17 14:27:32 2023 +0300

    scripts: imp error handling

commit 64e307a591cfeac1986d477a55bcc714636663bc
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 15:41:39 2023 +0300

    scripts: imp code

commit fe06df88f2bb3fc0de83f83deea26652485a22d4
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 15:23:20 2023 +0300

    scripts: add docs

commit 15d65a075373586fc31a595d7c831b80752d7cf2
Merge: ddd3cacd 9f7a582d
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 13:04:16 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit ddd3cacd507ca861c4d9d5f7600bdcc2c3068315
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Thu Mar 16 13:03:11 2023 +0300

    scripts: imp code

commit 4e8ebdc199f0c0ff4e7c7b8ae71483cca6c4d428
Merge: 73fedefa c6706445
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 15 12:25:36 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit 73fedefa4ceaf2273648afe5816f1903d96ba213
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Wed Mar 15 12:25:04 2023 +0300

    scripts: fix chlog

commit 780b0a257b6b2e684cfe8a49e4c4d22bcd4056ec
Merge: ef70d192 595484e0
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 14 14:58:24 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit ef70d192555a9ef2acd6dd1caeb4f05a10a1de63
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 14 14:57:36 2023 +0300

    scripts: upd readme

commit e28655826c8c5ce6ddf3cc904201681286a6be87
Merge: cae3b769 a2053526
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 13 14:28:47 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit cae3b769dfa6906653b32104169ae4a08a2c3723
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Mon Mar 13 14:27:29 2023 +0300

    scripts: rm translations js

commit 77e2e3480b52a70b8ef9be8f1edf581cca2a1a3a
Merge: 99e2382a c11a52d6
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 7 16:04:06 2023 +0300

    Merge branch 'master' into AG-20200-translation-script

commit 99e2382a161dee0bff30ae488d8b42565330d82e
Author: Stanislav Chzhen <s.chzhen@adguard.com>
Date:   Tue Mar 7 16:03:19 2023 +0300

    scripts: add translations go
---
 scripts/README.md                      |  24 +-
 scripts/make/go-lint.sh                |   1 +
 scripts/translations/.gitignore        |   1 -
 scripts/translations/count.js          |  41 --
 scripts/translations/download.js       | 125 ----
 scripts/translations/main.go           | 464 ++++++++++++++
 scripts/translations/package-lock.json | 838 -------------------------
 scripts/translations/package.json      |  14 -
 scripts/translations/unused.js         |  63 --
 scripts/translations/upload.js         |  47 --
 10 files changed, 480 insertions(+), 1138 deletions(-)
 delete mode 100644 scripts/translations/.gitignore
 delete mode 100644 scripts/translations/count.js
 delete mode 100644 scripts/translations/download.js
 create mode 100644 scripts/translations/main.go
 delete mode 100644 scripts/translations/package-lock.json
 delete mode 100644 scripts/translations/package.json
 delete mode 100644 scripts/translations/unused.js
 delete mode 100644 scripts/translations/upload.js

diff --git a/scripts/README.md b/scripts/README.md
index 5b7475bb..4821849f 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -178,22 +178,28 @@ manifest file templates, and helper scripts.
 
  ###  Usage
 
- *  `npm install`: install dependencies.  Run this first.
- *  `npm run locales:download`: download and save all translations.
- *  `npm run locales:upload`: upload the base `en` locale.
- *  `npm run locales:summary`: show the current locales summary.
- *  `npm run locales:unused`: show the list of unused strings.
+ *  `go run main.go help`: print usage.
+
+ *  `go run main.go download [-n <count>]`: download and save all translations.
+    `n` is optional flag where count is a number of concurrent downloads.
+
+ *  `go run main.go upload`: upload the base `en` locale.
+
+ *  `go run main.go summary`: show the current locales summary.
+
+ *  `go run main.go unused`: show the list of unused strings.
 
 After the download you'll find the output locales in the `client/src/__locales/`
 directory.
 
 Optional environment:
 
- *  `SLEEP_TIME`: set the sleep time between downloads for `locales:download`,
-    in milliseconds.  The default is 250 ms.
+ *  `UPLOAD_LANGUAGE`: set an alternative language for `upload`.
 
- *  `UPLOAD_LANGUAGE`: set an alternative language for `locales:upload` to
-    upload.
+ *  `TWOSKY_URI`: set an alternative URL for `download` or `upload`.
+
+ *  `TWOSKY_PROJECT_ID`: set an alternative project ID for `download` or
+    `upload`.
 
 
 
diff --git a/scripts/make/go-lint.sh b/scripts/make/go-lint.sh
index 3216b40a..35200f49 100644
--- a/scripts/make/go-lint.sh
+++ b/scripts/make/go-lint.sh
@@ -182,6 +182,7 @@ run_linter gocyclo --over 10\
 	./internal/version/\
 	./scripts/blocked-services/\
 	./scripts/vetted-filters/\
+	./scripts/translations/\
 	./main.go\
 	;
 
diff --git a/scripts/translations/.gitignore b/scripts/translations/.gitignore
deleted file mode 100644
index 3c3629e6..00000000
--- a/scripts/translations/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules
diff --git a/scripts/translations/count.js b/scripts/translations/count.js
deleted file mode 100644
index c3369f96..00000000
--- a/scripts/translations/count.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const path = require('path');
-const twoskyConfig = require('../../.twosky.json')[0];
-
-const {languages} = twoskyConfig;
-const LOCALES_DIR = '../../client/src/__locales';
-const LOCALES_LIST = Object.keys(languages);
-const BASE_FILE = 'en.json';
-
-const main = () => {
-    const pathToBaseFile = path.join(LOCALES_DIR, BASE_FILE);
-    const baseLanguageJson = require(pathToBaseFile);
-
-    const summary = {};
-
-    LOCALES_LIST.forEach((locale) => {
-        const pathToFile = path.join(LOCALES_DIR, `${locale}.json`);
-        if (pathToFile === pathToBaseFile) {
-            return;
-        }
-
-        let total = 0;
-        let translated = 0;
-
-        const languageJson = require(pathToFile);
-        for (let key in baseLanguageJson) {
-            total += 1;
-            if (key in languageJson) {
-                translated += 1;
-            }
-        }
-
-        summary[locale] = Math.round(translated / total * 10000) / 100;
-    });
-
-    console.log('Translations summary:');
-    for (let key in summary) {
-        console.log(`${key}, translated ${summary[key]}%`);
-    }
-}
-
-main();
diff --git a/scripts/translations/download.js b/scripts/translations/download.js
deleted file mode 100644
index 6cb65072..00000000
--- a/scripts/translations/download.js
+++ /dev/null
@@ -1,125 +0,0 @@
-// TODO(a.garipov): Rewrite this in Go; add better concurrency controls; add
-// features for easier maintenance.
-
-const fs = require('fs');
-const path = require('path');
-const requestPromise = require('request-promise');
-const twoskyConfig = require('../../.twosky.json')[0];
-
-const { project_id: TWOSKY_PROJECT_ID, languages } = twoskyConfig;
-const LOCALES_DIR = '../../client/src/__locales';
-const LOCALES_LIST = Object.keys(languages);
-const BASE_FILE = 'en.json';
-const TWOSKY_URI = process.env.TWOSKY_URI;
-
-/**
- * Prepare params to get translations from twosky
- * @param {string} locale language shortcut
- * @param {object} twosky config twosky
- */
-const getRequestUrl = (locale, url, projectId) => {
-    return `${url}/download?format=json&language=${locale}&filename=${BASE_FILE}&project=${projectId}`;
-};
-
-/**
- * Promise wrapper for writing in file
- * @param {string} filename
- * @param {any} body
- */
-function writeInFile(filename, body) {
-    let normalizedBody = removeEmpty(JSON.parse(body));
-
-    return new Promise((resolve, reject) => {
-        if (typeof normalizedBody !== 'string') {
-            try {
-                normalizedBody = JSON.stringify(normalizedBody, null, 4) + '\n'; // eslint-disable-line
-            } catch (err) {
-                reject(err);
-            }
-        }
-
-        fs.writeFile(filename, normalizedBody, (err) => {
-            if (err) reject(err);
-            resolve('Ok');
-        });
-    });
-}
-
-/**
- * Clear initial from empty value keys
- * @param {object} initialObject
- */
-function removeEmpty(initialObject) {
-    let processedObject = {};
-    Object.keys(initialObject).forEach(prop => {
-        if (initialObject[prop]) {
-            processedObject[prop] = initialObject[prop];
-        }
-    });
-    return processedObject;
-}
-
-/**
- * Request twosky
- * @param {string} url
- * @param {string} locale
- */
-const request = (url, locale) => (
-    requestPromise.get(url)
-        .then((res) => {
-            if (res.length) {
-                const pathToFile = path.join(LOCALES_DIR, `${locale}.json`);
-                return writeInFile(pathToFile, res);
-            }
-            return null;
-        })
-        .then((res) => {
-            let result = locale;
-            result += res ? ' - OK' : ' - Empty';
-            return result;
-        })
-        .catch((err) => {
-            console.log(err);
-            return `${locale} - Not OK`;
-        }));
-
-/**
- * Sleep.
- * @param {number} ms
- */
-const sleep = (ms) => new Promise((resolve) => {
-    setTimeout(resolve, ms);
-});
-
-/**
- * Download locales
- */
-const download = async () => {
-    const locales = LOCALES_LIST;
-
-    if (!TWOSKY_URI) {
-        console.error('No credentials');
-        return;
-    }
-
-    const requests = [];
-    for (let i = 0; i < locales.length; i++) {
-        const locale = locales[i];
-        const url = getRequestUrl(locale, TWOSKY_URI, TWOSKY_PROJECT_ID);
-        requests.push(request(url, locale));
-
-        // Don't request the Crowdin API too aggressively to prevent spurious
-        // 400 errors.
-        const sleepTime = process.env.SLEEP_TIME || 250;
-        await sleep(sleepTime);
-    }
-
-    Promise
-        .all(requests)
-        .then((res) => {
-            res.forEach(item => console.log(item));
-        })
-        .catch(err => console.log(err));
-};
-
-download();
diff --git a/scripts/translations/main.go b/scripts/translations/main.go
new file mode 100644
index 00000000..1922f614
--- /dev/null
+++ b/scripts/translations/main.go
@@ -0,0 +1,464 @@
+// translations downloads translations, uploads translations, prints summary
+// for translations, prints unused strings.
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/AdguardTeam/AdGuardHome/internal/aghio"
+	"github.com/AdguardTeam/golibs/errors"
+	"github.com/AdguardTeam/golibs/log"
+	"golang.org/x/exp/maps"
+	"golang.org/x/exp/slices"
+)
+
+const (
+	twoskyConfFile   = "./.twosky.json"
+	localesDir       = "./client/src/__locales"
+	defaultBaseFile  = "en.json"
+	defaultProjectID = "home"
+	srcDir           = "./client/src"
+	twoskyURI        = "https://twosky.int.agrd.dev/api/v1"
+
+	readLimit = 1 * 1024 * 1024
+)
+
+// langCode is a language code.
+type langCode string
+
+// languages is a map, where key is language code and value is display name.
+type languages map[langCode]string
+
+// textlabel is a text label of localization.
+type textLabel string
+
+// locales is a map, where key is text label and value is translation.
+type locales map[textLabel]string
+
+func main() {
+	if len(os.Args) == 1 {
+		usage("need a command")
+	}
+
+	if os.Args[1] == "help" {
+		usage("")
+	}
+
+	uriStr := os.Getenv("TWOSKY_URI")
+	if uriStr == "" {
+		uriStr = twoskyURI
+	}
+
+	uri, err := url.Parse(uriStr)
+	check(err)
+
+	projectID := os.Getenv("TWOSKY_PROJECT_ID")
+	if projectID == "" {
+		projectID = defaultProjectID
+	}
+
+	conf, err := readTwoskyConf()
+	check(err)
+
+	switch os.Args[1] {
+	case "summary":
+		err = summary(conf.Languages)
+		check(err)
+	case "download":
+		err = download(uri, projectID, conf.Languages)
+		check(err)
+	case "unused":
+		err = unused()
+		check(err)
+	case "upload":
+		err = upload(uri, projectID, conf.BaseLangcode)
+		check(err)
+	default:
+		usage("unknown command")
+	}
+}
+
+// check is a simple error-checking helper for scripts.
+func check(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+// usage prints usage.  If addStr is not empty print addStr and exit with code
+// 1, otherwise exit with code 0.
+func usage(addStr string) {
+	const usageStr = `Usage: go run main.go <command> [<args>]
+Commands:
+  help
+        Print usage.
+  summary
+        Print summary.
+  download [-n <count>]
+        Download translations. count is a number of concurrent downloads.
+  unused
+        Print unused strings.
+  upload
+        Upload translations.`
+
+	if addStr != "" {
+		fmt.Printf("%s\n%s\n", addStr, usageStr)
+
+		os.Exit(1)
+	}
+
+	fmt.Println(usageStr)
+
+	os.Exit(0)
+}
+
+// twoskyConf is the configuration structure for localization.
+type twoskyConf struct {
+	Languages        languages `json:"languages"`
+	ProjectID        string    `json:"project_id"`
+	BaseLangcode     langCode  `json:"base_locale"`
+	LocalizableFiles []string  `json:"localizable_files"`
+}
+
+// readTwoskyConf returns configuration.
+func readTwoskyConf() (t twoskyConf, err error) {
+	b, err := os.ReadFile(twoskyConfFile)
+	if err != nil {
+		// Don't wrap the error since it's informative enough as is.
+		return twoskyConf{}, err
+	}
+
+	var tsc []twoskyConf
+	err = json.Unmarshal(b, &tsc)
+	if err != nil {
+		err = fmt.Errorf("unmarshalling %q: %w", twoskyConfFile, err)
+
+		return twoskyConf{}, err
+	}
+
+	if len(tsc) == 0 {
+		err = fmt.Errorf("%q is empty", twoskyConfFile)
+
+		return twoskyConf{}, err
+	}
+
+	conf := tsc[0]
+
+	for _, lang := range conf.Languages {
+		if lang == "" {
+			return twoskyConf{}, errors.Error("language is empty")
+		}
+	}
+
+	return conf, nil
+}
+
+// readLocales reads file with name fn and returns a map, where key is text
+// label and value is localization.
+func readLocales(fn string) (loc locales, err error) {
+	b, err := os.ReadFile(fn)
+	if err != nil {
+		// Don't wrap the error since it's informative enough as is.
+		return nil, err
+	}
+
+	loc = make(locales)
+	err = json.Unmarshal(b, &loc)
+	if err != nil {
+		err = fmt.Errorf("unmarshalling %q: %w", fn, err)
+
+		return nil, err
+	}
+
+	return loc, nil
+}
+
+// summary prints summary for translations.
+func summary(langs languages) (err error) {
+	basePath := filepath.Join(localesDir, defaultBaseFile)
+	baseLoc, err := readLocales(basePath)
+	if err != nil {
+		return fmt.Errorf("summary: %w", err)
+	}
+
+	size := float64(len(baseLoc))
+
+	keys := maps.Keys(langs)
+	slices.Sort(keys)
+
+	for _, lang := range keys {
+		name := filepath.Join(localesDir, string(lang)+".json")
+		if name == basePath {
+			continue
+		}
+
+		var loc locales
+		loc, err = readLocales(name)
+		if err != nil {
+			return fmt.Errorf("summary: reading locales: %w", err)
+		}
+
+		f := float64(len(loc)) * 100 / size
+
+		fmt.Printf("%s\t %6.2f %%\n", lang, f)
+	}
+
+	return nil
+}
+
+// download and save all translations.  uri is the base URL.  projectID is the
+// name of the project.
+func download(uri *url.URL, projectID string, langs languages) (err error) {
+	var numWorker int
+
+	flagSet := flag.NewFlagSet("download", flag.ExitOnError)
+	flagSet.Usage = func() {
+		usage("download command error")
+	}
+	flagSet.IntVar(&numWorker, "n", 1, "number of concurrent downloads")
+
+	err = flagSet.Parse(os.Args[2:])
+	if err != nil {
+		// Don't wrap the error since there is exit on error.
+		return err
+	}
+
+	if numWorker < 1 {
+		usage("count must be positive")
+	}
+
+	downloadURI := uri.JoinPath("download")
+
+	client := &http.Client{
+		Timeout: 10 * time.Second,
+	}
+
+	var wg sync.WaitGroup
+	uriCh := make(chan *url.URL, len(langs))
+
+	for i := 0; i < numWorker; i++ {
+		wg.Add(1)
+		go downloadWorker(&wg, client, uriCh)
+	}
+
+	for lang := range langs {
+		uri = translationURL(downloadURI, defaultBaseFile, projectID, lang)
+
+		uriCh <- uri
+	}
+
+	close(uriCh)
+	wg.Wait()
+
+	return nil
+}
+
+// downloadWorker downloads translations by received urls and saves them.
+func downloadWorker(wg *sync.WaitGroup, client *http.Client, uriCh <-chan *url.URL) {
+	defer wg.Done()
+
+	for uri := range uriCh {
+		data, err := getTranslation(client, uri.String())
+		if err != nil {
+			log.Error("download worker: getting translation: %s", err)
+
+			continue
+		}
+
+		q := uri.Query()
+		code := q.Get("language")
+
+		name := filepath.Join(localesDir, code+".json")
+		err = os.WriteFile(name, data, 0o664)
+		if err != nil {
+			log.Error("download worker: writing file: %s", err)
+
+			continue
+		}
+
+		fmt.Println(name)
+	}
+}
+
+// getTranslation returns received translation data or error.
+func getTranslation(client *http.Client, url string) (data []byte, err error) {
+	resp, err := client.Get(url)
+	if err != nil {
+		return nil, fmt.Errorf("requesting: %w", err)
+	}
+
+	defer log.OnCloserError(resp.Body, log.ERROR)
+
+	if resp.StatusCode != http.StatusOK {
+		err = fmt.Errorf("url: %q; status code: %s", url, http.StatusText(resp.StatusCode))
+
+		return nil, err
+	}
+
+	limitReader, err := aghio.LimitReader(resp.Body, readLimit)
+	if err != nil {
+		err = fmt.Errorf("limit reading: %w", err)
+
+		return nil, err
+	}
+
+	data, err = io.ReadAll(limitReader)
+	if err != nil {
+		err = fmt.Errorf("reading all: %w", err)
+
+		return nil, err
+	}
+
+	return data, nil
+}
+
+// translationURL returns a new url.URL with provided query parameters.
+func translationURL(oldURL *url.URL, baseFile, projectID string, lang langCode) (uri *url.URL) {
+	uri = &url.URL{}
+	*uri = *oldURL
+
+	q := uri.Query()
+	q.Set("format", "json")
+	q.Set("filename", baseFile)
+	q.Set("project", projectID)
+	q.Set("language", string(lang))
+
+	uri.RawQuery = q.Encode()
+
+	return uri
+}
+
+// unused prints unused text labels.
+func unused() (err error) {
+	fileNames := []string{}
+	basePath := filepath.Join(localesDir, defaultBaseFile)
+	baseLoc, err := readLocales(basePath)
+	if err != nil {
+		return fmt.Errorf("unused: %w", err)
+	}
+
+	locDir := filepath.Clean(localesDir)
+
+	err = filepath.Walk(srcDir, func(name string, info os.FileInfo, err error) error {
+		if err != nil {
+			log.Info("accessing a path %q: %s", name, err)
+
+			return nil
+		}
+
+		if info.IsDir() {
+			return nil
+		}
+
+		if strings.HasPrefix(name, locDir) {
+			return nil
+		}
+
+		ext := filepath.Ext(name)
+		if ext == ".js" || ext == ".json" {
+			fileNames = append(fileNames, name)
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return fmt.Errorf("filepath walking %q: %w", srcDir, err)
+	}
+
+	err = removeUnused(fileNames, baseLoc)
+
+	return errors.Annotate(err, "removing unused: %w")
+}
+
+func removeUnused(fileNames []string, loc locales) (err error) {
+	knownUsed := []textLabel{
+		"blocking_mode_refused",
+		"blocking_mode_nxdomain",
+		"blocking_mode_custom_ip",
+	}
+
+	for _, v := range knownUsed {
+		delete(loc, v)
+	}
+
+	for _, fn := range fileNames {
+		var buf []byte
+		buf, err = os.ReadFile(fn)
+		if err != nil {
+			// Don't wrap the error since it's informative enough as is.
+			return err
+		}
+
+		for k := range loc {
+			if bytes.Contains(buf, []byte(k)) {
+				delete(loc, k)
+			}
+		}
+	}
+
+	printUnused(loc)
+
+	return nil
+}
+
+// printUnused text labels to stdout.
+func printUnused(loc locales) {
+	keys := maps.Keys(loc)
+	slices.Sort(keys)
+
+	for _, v := range keys {
+		fmt.Println(v)
+	}
+}
+
+// upload base translation.  uri is the base URL.  projectID is the name of the
+// project.  baseLang is the base language code.
+func upload(uri *url.URL, projectID string, baseLang langCode) (err error) {
+	uploadURI := uri.JoinPath("upload")
+
+	lang := baseLang
+
+	langStr := os.Getenv("UPLOAD_LANGUAGE")
+	if langStr != "" {
+		lang = langCode(langStr)
+	}
+
+	basePath := filepath.Join(localesDir, defaultBaseFile)
+	b, err := os.ReadFile(basePath)
+	if err != nil {
+		return fmt.Errorf("upload: %w", err)
+	}
+
+	var buf bytes.Buffer
+	buf.Write(b)
+
+	uri = translationURL(uploadURI, defaultBaseFile, projectID, lang)
+
+	var client http.Client
+	resp, err := client.Post(uri.String(), "application/json", &buf)
+	if err != nil {
+		return fmt.Errorf("upload: client post: %w", err)
+	}
+
+	defer func() {
+		err = errors.WithDeferred(err, resp.Body.Close())
+	}()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("status code is not ok: %q", http.StatusText(resp.StatusCode))
+	}
+
+	return nil
+}
diff --git a/scripts/translations/package-lock.json b/scripts/translations/package-lock.json
deleted file mode 100644
index 42f5d23a..00000000
--- a/scripts/translations/package-lock.json
+++ /dev/null
@@ -1,838 +0,0 @@
-{
-  "name": "translations",
-  "version": "0.2.0",
-  "lockfileVersion": 2,
-  "requires": true,
-  "packages": {
-    "": {
-      "version": "0.2.0",
-      "dependencies": {
-        "request": "^2.88.0",
-        "request-promise": "^4.2.2"
-      }
-    },
-    "node_modules/ajv": {
-      "version": "6.5.5",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
-      "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
-      "dependencies": {
-        "fast-deep-equal": "^2.0.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      }
-    },
-    "node_modules/asn1": {
-      "version": "0.2.4",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
-      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
-      "dependencies": {
-        "safer-buffer": "~2.1.0"
-      }
-    },
-    "node_modules/assert-plus": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
-    },
-    "node_modules/aws-sign2": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/aws4": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
-      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
-    },
-    "node_modules/bcrypt-pbkdf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
-      "dependencies": {
-        "tweetnacl": "^0.14.3"
-      }
-    },
-    "node_modules/bluebird": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
-      "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
-    },
-    "node_modules/caseless": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
-    },
-    "node_modules/combined-stream": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
-      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/core-util-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-    },
-    "node_modules/dashdash": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-      "dependencies": {
-        "assert-plus": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/ecc-jsbn": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
-      "dependencies": {
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.1.0"
-      }
-    },
-    "node_modules/extend": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
-    },
-    "node_modules/extsprintf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
-      "engines": [
-        "node >=0.6.0"
-      ]
-    },
-    "node_modules/fast-deep-equal": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
-    },
-    "node_modules/fast-json-stable-stringify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
-    },
-    "node_modules/forever-agent": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/form-data": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.6",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 0.12"
-      }
-    },
-    "node_modules/getpass": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
-      "dependencies": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "node_modules/har-schema": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/har-validator": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
-      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
-      "dependencies": {
-        "ajv": "^6.5.5",
-        "har-schema": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/http-signature": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
-      "dependencies": {
-        "assert-plus": "^1.0.0",
-        "jsprim": "^1.2.2",
-        "sshpk": "^1.7.0"
-      },
-      "engines": {
-        "node": ">=0.8",
-        "npm": ">=1.3.7"
-      }
-    },
-    "node_modules/is-typedarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
-    },
-    "node_modules/isstream": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
-    },
-    "node_modules/jsbn": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
-    },
-    "node_modules/json-schema": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
-      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
-    },
-    "node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
-    },
-    "node_modules/json-stringify-safe": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
-    },
-    "node_modules/jsprim": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
-      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
-      "engines": [
-        "node >=0.6.0"
-      ],
-      "dependencies": {
-        "assert-plus": "1.0.0",
-        "extsprintf": "1.3.0",
-        "json-schema": "0.2.3",
-        "verror": "1.10.0"
-      }
-    },
-    "node_modules/lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
-    },
-    "node_modules/mime-db": {
-      "version": "1.37.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
-      "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.21",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
-      "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
-      "dependencies": {
-        "mime-db": "~1.37.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/oauth-sign": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/performance-now": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
-    },
-    "node_modules/psl": {
-      "version": "1.1.29",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
-      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
-    },
-    "node_modules/punycode": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
-    },
-    "node_modules/qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
-      "engines": {
-        "node": ">=0.6"
-      }
-    },
-    "node_modules/request": {
-      "version": "2.88.0",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
-      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
-      "dependencies": {
-        "aws-sign2": "~0.7.0",
-        "aws4": "^1.8.0",
-        "caseless": "~0.12.0",
-        "combined-stream": "~1.0.6",
-        "extend": "~3.0.2",
-        "forever-agent": "~0.6.1",
-        "form-data": "~2.3.2",
-        "har-validator": "~5.1.0",
-        "http-signature": "~1.2.0",
-        "is-typedarray": "~1.0.0",
-        "isstream": "~0.1.2",
-        "json-stringify-safe": "~5.0.1",
-        "mime-types": "~2.1.19",
-        "oauth-sign": "~0.9.0",
-        "performance-now": "^2.1.0",
-        "qs": "~6.5.2",
-        "safe-buffer": "^5.1.2",
-        "tough-cookie": "~2.4.3",
-        "tunnel-agent": "^0.6.0",
-        "uuid": "^3.3.2"
-      },
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/request-promise": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
-      "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
-      "dependencies": {
-        "bluebird": "^3.5.0",
-        "request-promise-core": "1.1.1",
-        "stealthy-require": "^1.1.0",
-        "tough-cookie": ">=2.3.3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/request-promise-core": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
-      "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
-      "dependencies": {
-        "lodash": "^4.13.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-    },
-    "node_modules/safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
-    },
-    "node_modules/sshpk": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
-      "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
-      "dependencies": {
-        "asn1": "~0.2.3",
-        "assert-plus": "^1.0.0",
-        "bcrypt-pbkdf": "^1.0.0",
-        "dashdash": "^1.12.0",
-        "ecc-jsbn": "~0.1.1",
-        "getpass": "^0.1.1",
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.0.2",
-        "tweetnacl": "~0.14.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/stealthy-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
-      "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/tough-cookie": {
-      "version": "2.4.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
-      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
-      "dependencies": {
-        "psl": "^1.1.24",
-        "punycode": "^1.4.1"
-      },
-      "engines": {
-        "node": ">=0.8"
-      }
-    },
-    "node_modules/tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "dependencies": {
-        "safe-buffer": "^5.0.1"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
-    },
-    "node_modules/uri-js": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
-      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
-      "dependencies": {
-        "punycode": "^2.1.0"
-      }
-    },
-    "node_modules/uri-js/node_modules/punycode": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/uuid": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
-      "bin": {
-        "uuid": "bin/uuid"
-      }
-    },
-    "node_modules/verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "engines": [
-        "node >=0.6.0"
-      ],
-      "dependencies": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    }
-  },
-  "dependencies": {
-    "ajv": {
-      "version": "6.5.5",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz",
-      "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==",
-      "requires": {
-        "fast-deep-equal": "^2.0.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      }
-    },
-    "asn1": {
-      "version": "0.2.4",
-      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
-      "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
-      "requires": {
-        "safer-buffer": "~2.1.0"
-      }
-    },
-    "assert-plus": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
-      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
-    },
-    "asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
-    },
-    "aws-sign2": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
-      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
-    },
-    "aws4": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
-      "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
-    },
-    "bcrypt-pbkdf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
-      "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
-      "requires": {
-        "tweetnacl": "^0.14.3"
-      }
-    },
-    "bluebird": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
-      "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
-    },
-    "caseless": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
-    },
-    "combined-stream": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
-      "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
-      "requires": {
-        "delayed-stream": "~1.0.0"
-      }
-    },
-    "core-util-is": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
-    },
-    "dashdash": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
-      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
-      "requires": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
-    },
-    "ecc-jsbn": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
-      "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
-      "requires": {
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.1.0"
-      }
-    },
-    "extend": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
-    },
-    "extsprintf": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
-      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
-    },
-    "fast-deep-equal": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
-      "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
-    },
-    "fast-json-stable-stringify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
-      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
-    },
-    "forever-agent": {
-      "version": "0.6.1",
-      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
-      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
-    },
-    "form-data": {
-      "version": "2.3.3",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
-      "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
-      "requires": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.6",
-        "mime-types": "^2.1.12"
-      }
-    },
-    "getpass": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
-      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
-      "requires": {
-        "assert-plus": "^1.0.0"
-      }
-    },
-    "har-schema": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
-      "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
-    },
-    "har-validator": {
-      "version": "5.1.3",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
-      "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
-      "requires": {
-        "ajv": "^6.5.5",
-        "har-schema": "^2.0.0"
-      }
-    },
-    "http-signature": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
-      "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "jsprim": "^1.2.2",
-        "sshpk": "^1.7.0"
-      }
-    },
-    "is-typedarray": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
-    },
-    "isstream": {
-      "version": "0.1.2",
-      "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
-      "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
-    },
-    "jsbn": {
-      "version": "0.1.1",
-      "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
-      "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
-    },
-    "json-schema": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
-      "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
-    },
-    "json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
-    },
-    "json-stringify-safe": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
-      "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
-    },
-    "jsprim": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
-      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
-      "requires": {
-        "assert-plus": "1.0.0",
-        "extsprintf": "1.3.0",
-        "json-schema": "0.2.3",
-        "verror": "1.10.0"
-      }
-    },
-    "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
-    },
-    "mime-db": {
-      "version": "1.37.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
-      "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
-    },
-    "mime-types": {
-      "version": "2.1.21",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
-      "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
-      "requires": {
-        "mime-db": "~1.37.0"
-      }
-    },
-    "oauth-sign": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
-      "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
-    },
-    "performance-now": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
-      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
-    },
-    "psl": {
-      "version": "1.1.29",
-      "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
-      "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
-    },
-    "punycode": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
-      "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
-    },
-    "qs": {
-      "version": "6.5.2",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
-      "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
-    },
-    "request": {
-      "version": "2.88.0",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
-      "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
-      "requires": {
-        "aws-sign2": "~0.7.0",
-        "aws4": "^1.8.0",
-        "caseless": "~0.12.0",
-        "combined-stream": "~1.0.6",
-        "extend": "~3.0.2",
-        "forever-agent": "~0.6.1",
-        "form-data": "~2.3.2",
-        "har-validator": "~5.1.0",
-        "http-signature": "~1.2.0",
-        "is-typedarray": "~1.0.0",
-        "isstream": "~0.1.2",
-        "json-stringify-safe": "~5.0.1",
-        "mime-types": "~2.1.19",
-        "oauth-sign": "~0.9.0",
-        "performance-now": "^2.1.0",
-        "qs": "~6.5.2",
-        "safe-buffer": "^5.1.2",
-        "tough-cookie": "~2.4.3",
-        "tunnel-agent": "^0.6.0",
-        "uuid": "^3.3.2"
-      }
-    },
-    "request-promise": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
-      "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
-      "requires": {
-        "bluebird": "^3.5.0",
-        "request-promise-core": "1.1.1",
-        "stealthy-require": "^1.1.0",
-        "tough-cookie": ">=2.3.3"
-      }
-    },
-    "request-promise-core": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
-      "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
-      "requires": {
-        "lodash": "^4.13.1"
-      }
-    },
-    "safe-buffer": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
-    },
-    "safer-buffer": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
-    },
-    "sshpk": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz",
-      "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==",
-      "requires": {
-        "asn1": "~0.2.3",
-        "assert-plus": "^1.0.0",
-        "bcrypt-pbkdf": "^1.0.0",
-        "dashdash": "^1.12.0",
-        "ecc-jsbn": "~0.1.1",
-        "getpass": "^0.1.1",
-        "jsbn": "~0.1.0",
-        "safer-buffer": "^2.0.2",
-        "tweetnacl": "~0.14.0"
-      }
-    },
-    "stealthy-require": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
-      "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
-    },
-    "tough-cookie": {
-      "version": "2.4.3",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
-      "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
-      "requires": {
-        "psl": "^1.1.24",
-        "punycode": "^1.4.1"
-      }
-    },
-    "tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "requires": {
-        "safe-buffer": "^5.0.1"
-      }
-    },
-    "tweetnacl": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
-      "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
-    },
-    "uri-js": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
-      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
-      "requires": {
-        "punycode": "^2.1.0"
-      },
-      "dependencies": {
-        "punycode": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-          "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
-        }
-      }
-    },
-    "uuid": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
-      "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
-    },
-    "verror": {
-      "version": "1.10.0",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
-      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
-      "requires": {
-        "assert-plus": "^1.0.0",
-        "core-util-is": "1.0.2",
-        "extsprintf": "^1.2.0"
-      }
-    }
-  }
-}
diff --git a/scripts/translations/package.json b/scripts/translations/package.json
deleted file mode 100644
index 110db720..00000000
--- a/scripts/translations/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "name": "translations",
-  "version": "0.2.0",
-  "scripts": {
-    "locales:download": "TWOSKY_URI=https://twosky.int.agrd.dev/api/v1 TWOSKY_PROJECT_ID=home node download.js ; node count.js",
-    "locales:upload": "TWOSKY_URI=https://twosky.int.agrd.dev/api/v1 TWOSKY_PROJECT_ID=home node upload.js",
-    "locales:summary": "node count.js",
-    "locales:unused": "node unused.js"
-  },
-  "dependencies": {
-    "request": "^2.88.0",
-    "request-promise": "^4.2.2"
-  }
-}
diff --git a/scripts/translations/unused.js b/scripts/translations/unused.js
deleted file mode 100644
index 7a4ec0e9..00000000
--- a/scripts/translations/unused.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-
-const SRC_DIR = '../../client/src/'
-const LOCALES_DIR = '../../client/src/__locales';
-const BASE_FILE = path.join(LOCALES_DIR, 'en.json');
-
-// Strings that may be found by the algorithm,
-// but in fact they are used.
-const KNOWN_USED_STRINGS = {
-    'blocking_mode_refused': true,
-    'blocking_mode_nxdomain': true,
-    'blocking_mode_custom_ip': true,
-}
-
-function traverseDir(dir, callback) {
-    fs.readdirSync(dir).forEach(file => {
-        let fullPath = path.join(dir, file);
-        if (fs.lstatSync(fullPath).isDirectory()) {
-            traverseDir(fullPath, callback);
-        } else {
-            callback(fullPath);
-        }
-    });
-}
-
-const contains = (key, files) => {
-    for (let file of files) {
-        if (file.includes(key)) {
-            return true;
-        }
-    }
-
-    return false;
-}
-
-const main = () => {
-    const baseLanguage = require(BASE_FILE);
-    const files = [];
-
-    traverseDir(SRC_DIR, (path) => {
-        const canContain = (path.endsWith('.js') || path.endsWith('.json')) &&
-            !path.includes(LOCALES_DIR);
-
-        if (canContain) {
-            files.push(fs.readFileSync(path).toString());
-        }
-    });
-
-    const unused = [];
-    for (let key in baseLanguage) {
-        if (!contains(key, files) && !KNOWN_USED_STRINGS[key]) {
-            unused.push(key);
-        }
-    }
-
-    console.log('Unused keys:');
-    for (let key of unused) {
-        console.log(key);
-    }
-}
-
-main();
diff --git a/scripts/translations/upload.js b/scripts/translations/upload.js
deleted file mode 100644
index 702b251b..00000000
--- a/scripts/translations/upload.js
+++ /dev/null
@@ -1,47 +0,0 @@
-const path = require('path');
-const fs = require('fs');
-const request = require('request-promise');
-const twoskyConfig = require('../../.twosky.json')[0];
-
-const { project_id: TWOSKY_PROJECT_ID, base_locale: DEFAULT_LANGUAGE } = twoskyConfig;
-const LOCALES_DIR = '../../client/src/__locales';
-const BASE_FILE = 'en.json';
-const TWOSKY_URI = process.env.TWOSKY_URI;
-
-/**
- * Prepare post params
- */
-const getRequestData = (url, projectId) => {
-    const language = process.env.UPLOAD_LANGUAGE || DEFAULT_LANGUAGE;
-    const formData = {
-        format: 'json',
-        language: language,
-        filename: BASE_FILE,
-        project: projectId,
-        file: fs.createReadStream(path.resolve(LOCALES_DIR, `${language}.json`)),
-    };
-
-    console.log(`uploading ${language}`);
-
-    return {
-        url: `${url}/upload`,
-        formData
-    };
-};
-
-/**
- * Make request to twosky to upload new json
- */
-const upload = () => {
-    if (!TWOSKY_URI) {
-        console.error('No credentials');
-        return;
-    }
-
-    const { url, formData } = getRequestData(TWOSKY_URI, TWOSKY_PROJECT_ID);
-    request
-        .post({ url, formData })
-        .catch(err => console.log(err));
-};
-
-upload();