Repository: learningequality/kolibri

Issue:
Strings in single group sub-tab not displaying localized
### Observed behavior

Some strings on group management page and modals do not display localized, despite being translated and approved on Crowdin:

![2019-03-18_15-17-23](https://user-images.githubusercontent.com/1457929/54568158-6a108b00-4993-11e9-8cbd-04aeed9455e4.jpg)

![2019-03-18_15-22-18](https://user-images.githubusercontent.com/1457929/54568165-6ed53f00-4993-11e9-90e2-6cc5ba5e7f21.jpg)

![2019-03-18_15-29-47](https://user-images.githubusercontent.com/1457929/54568170-73015c80-4993-11e9-9b7e-932a770d9261.jpg)

[String translated on Crowdin](https://crowdin.com/translate/kolibri/582/en-es#43232)

### Expected behavior
Localized strings displayed correctly.

### User-facing consequences
Lack of understanding the UI options.

### Errors and logs
…

### Steps to reproduce
Change UI language from English
Go to Coach > Plan > Group and select a group. 
Try to enroll and delete learners.

### Context
0.12.1 (kolibridemo server)


cc @indirectlylit @jonboiser 


Discussion:
It looks like the strings for GroupMembersPage didn't make it into the locale file. 
Notification for the newly created group also pops-up in English.
All instances of `Select all on page` are from the `facility_management_module-messages.json` file, are we sure that it gets applied to `coach_module-messages.json` too?

![2019-03-18_23-49-28](https://user-images.githubusercontent.com/1457929/54586744-378a8080-49db-11e9-89e9-ad03c35141f4.jpg)

Yeah, anything in the facility json will not be available to the coach plugin.
I was under the impression that it could be reused on Crowdin, and that was the reason for including it after the string freeze... 😕 
Yes - it could, but only if the strings were updated.
investigating now

Resolving patch:
diff --git a/build_tools/i18n/crowdin.py b/build_tools/i18n/crowdin.py
--- a/build_tools/i18n/crowdin.py
+++ b/build_tools/i18n/crowdin.py
@@ -88,9 +88,8 @@
     cmd="pre-translate",
     # perfect_match=0 - apply TM to all identical strings, regardless of ID
     # apply_untranslated_strings_only=1 - don't apply TM to strings that already have translations
-    # import_duplicates=1 - not sure? in the UI this is mutually exclusive with apply_untranslated_strings_only
-    # approve_translated=1 - only auto-approve "perfect" matches, i.e. those with the same ID
-    params="&method=tm&approve_translated=1&auto_approve_option=1&json&apply_untranslated_strings_only=1import_duplicates=1&perfect_match=0",
+    # approve_translated=1 - auto-approve
+    params="&method=tm&approve_translated=1&auto_approve_option={approve_option}&json&apply_untranslated_strings_only=1&perfect_match=0",
 )
 UPLOAD_TRANSLATION_URL = CROWDIN_API_URL.format(
     proj=CROWDIN_PROJECT,
@@ -133,9 +132,9 @@ def _format_json_files():
     re-print all json files to ensure consistent diffs with ordered keys
     """
     locale_paths = []
-    for lang_info in utils.supported_languages(include_in_context=True):
-        locale_paths.append(utils.local_locale_path(lang_info))
-        locale_paths.append(utils.local_perseus_locale_path(lang_info))
+    for lang_object in utils.supported_languages(include_in_context=True):
+        locale_paths.append(utils.local_locale_path(lang_object))
+        locale_paths.append(utils.local_perseus_locale_path(lang_object))
     for locale_path in locale_paths:
         for file_name in os.listdir(locale_path):
             if not file_name.endswith(".json"):
@@ -146,6 +145,10 @@ def _format_json_files():
             utils.json_dump_formatted(data, file_path)
 
 
+def _is_string_file(file_name):
+    return file_name.endswith(".json") or file_name.endswith(".po")
+
+
 """
 Rebuild command
 """
@@ -171,7 +174,7 @@ def command_rebuild(branch):
 """
 
 
-def command_pretranslate(branch):
+def command_pretranslate(branch, approve_all=False):
     """
     Applies pre-translation to the given branch on Crowdin
     """
@@ -184,8 +187,18 @@ def command_pretranslate(branch):
     codes = [lang[utils.KEY_CROWDIN_CODE] for lang in utils.supported_languages()]
     params.extend([("languages[]", code) for code in codes])
 
-    logging.info("Crowdin: pre-translating strings and pre-approving exact matches in '{}'...".format(branch))
-    r = requests.post(PRETRANSLATE_URL, params=params)
+    msg = (
+        "Crowdin: pre-translating and pre-approving untranslated matches in '{}'..."
+        if approve_all
+        else "Crowdin: pre-translating untranslated matches in '{}'..."
+    )
+    msg += "\n\tNote that this operation can take a long time and may time out."
+    msg += "\n\tYou should see the results on Crowdin eventually..."
+    logging.info(msg.format(branch))
+
+    r = requests.post(
+        PRETRANSLATE_URL.format(approve_option=0 if approve_all else 1), params=params
+    )
     r.raise_for_status()
     logging.info("Crowdin: succeeded!")
 
@@ -204,32 +217,15 @@ def _translation_upload_ref(file_name, lang_object):
     return ("files[{0}]".format(file_name), file_pointer)
 
 
-def command_upload_translation(branch, lang_code, file_name):
-    # make sure it's not a source
-    if lang_code == "en":
-        logging.error("'en' is a source, not a translation")
-        sys.exit(1)
+def _upload_translation(branch, lang_object):
 
-    # check branch
     if _no_crowdin_branch(branch, _get_crowdin_details()):
-        logging.error("Branch '{}' not found.".format(lang_code))
-        sys.exit(1)
-
-    # find language
-    try:
-        lang_object = next(
-            lang
-            for lang in utils.supported_languages()
-            if lang[utils.KEY_INTL_CODE] == lang_code
-        )
-    except StopIteration:
-        logging.error("Code '{}' not found in supported languages.".format(lang_code))
-        logging.error("Make sure to use an Intl code, not crowdin.")
+        logging.error("Branch '{}' not found.".format(branch))
         sys.exit(1)
 
     logging.info(
-        "Crowdin: uploading current translation for '{}' to '{}'...".format(
-            lang_code, branch
+        "Crowdin: uploading translation files for '{}' to '{}'...".format(
+            lang_object[utils.KEY_CROWDIN_CODE], branch
         )
     )
 
@@ -238,16 +234,14 @@ def command_upload_translation(branch, lang_code, file_name):
     )
 
     file_names = []
-    if file_name:
-        file_names.append(file_name)
-    else:
-        for name in os.listdir(utils.local_locale_path(lang_object)):
-            if name.endswith(".json"):
-                file_names.append(name)
+    for name in os.listdir(utils.local_locale_path(lang_object)):
+        if _is_string_file(name):
+            file_names.append(name)
+    for name in os.listdir(utils.local_perseus_locale_path(lang_object)):
+        if _is_string_file(name):
+            file_names.append(name)
 
     for chunk in _chunks(file_names):
-        logging.info("\t{}".format(", ".join(chunk)))
-
         references = [_translation_upload_ref(f, lang_object) for f in chunk]
         r = requests.post(url, files=references)
         r.raise_for_status()
@@ -257,6 +251,14 @@ def command_upload_translation(branch, lang_code, file_name):
     logging.info("Crowdin: upload succeeded!")
 
 
+def command_upload_translations(branch):
+    supported_languages = utils.supported_languages(
+        include_in_context=False, include_english=False
+    )
+    for lang_object in supported_languages:
+        _upload_translation(branch, lang_object)
+
+
 """
 Download command
 """
@@ -279,18 +281,18 @@ def command_download(branch):
     _wipe_translations(utils.LOCALE_PATH)
     _wipe_translations(utils.PERSEUS_LOCALE_PATH)
 
-    for lang in utils.supported_languages(include_in_context=True):
-        code = lang[utils.KEY_CROWDIN_CODE]
+    for lang_object in utils.supported_languages(include_in_context=True):
+        code = lang_object[utils.KEY_CROWDIN_CODE]
         url = DOWNLOAD_URL.format(language=code, branch=branch)
         r = requests.get(url)
         r.raise_for_status()
         z = zipfile.ZipFile(io.BytesIO(r.content))
-        target = utils.local_locale_path(lang)
+        target = utils.local_locale_path(lang_object)
         logging.info("\tExtracting {} to {}".format(code, target))
         z.extractall(target)
 
         # hack for perseus
-        perseus_target = utils.local_perseus_locale_path(lang)
+        perseus_target = utils.local_perseus_locale_path(lang_object)
         if not os.path.exists(perseus_target):
             os.makedirs(perseus_target)
         shutil.move(
@@ -307,10 +309,6 @@ def command_download(branch):
 """
 
 
-def _is_source(file_name):
-    return file_name.endswith(".json") or file_name.endswith(".po")
-
-
 def _source_upload_ref(file_name):
     if file_name == PERSEUS_FILE:  # hack for perseus, assumes the same file name
         file_pointer = open(os.path.join(utils.PERSEUS_SOURCE_PATH, file_name), "rb")
@@ -338,7 +336,7 @@ def _modify(url, file_names):
             ref[1].close()
 
 
-def command_upload(branch):
+def command_upload_sources(branch):
     """
     Uploads the English translation source files to the given branch on Crowdin
     """
@@ -352,7 +350,7 @@ def command_upload(branch):
     source_files = set(
         file_name
         for file_name in os.listdir(utils.SOURCE_PATH)
-        if _is_source(file_name)
+        if _is_string_file(file_name)
     )
 
     # hack for perseus
@@ -455,22 +453,14 @@ def _is_branch_node(node):
     avg_untranslated_strings = round(total_untranslated_strings / len(strings_table))
     avg_unapproved_strings = round(total_unapproved_strings / len(strings_table))
     strings_table.append(
-        (
-            "Average - all languages",
-            avg_untranslated_strings,
-            avg_unapproved_strings,
-        )
+        ("Average - all languages", avg_untranslated_strings, avg_unapproved_strings)
     )
     total_untranslated_words = sum([row[1] for row in words_table])
     total_unapproved_words = sum([row[2] for row in words_table])
     avg_untranslated_words = round(total_untranslated_words / len(words_table))
     avg_unapproved_words = round(total_unapproved_words / len(words_table))
     words_table.append(
-        (
-            "Average - all languages",
-            avg_untranslated_words,
-            avg_unapproved_words,
-        )
+        ("Average - all languages", avg_untranslated_words, avg_unapproved_words)
     )
     summary_table_headers = ["", "Untranslated", "Needs approval"]
     summary_table = [
@@ -505,11 +495,18 @@ def main():
     )
     parser_download.add_argument("branch", help="Branch name", type=str)
     parser_upload = subparsers.add_parser(
-        "upload", help="Upload English sources to Crowdin"
+        "upload-sources", help="Upload English sources to Crowdin"
     )
     parser_upload.add_argument("branch", help="Branch name", type=str)
     parser_pretranslate = subparsers.add_parser(
-        "pre-translate", help="Apply translation memory on Crowdin"
+        "pretranslate", help="Apply translation memory on Crowdin"
+    )
+    parser_pretranslate.add_argument(
+        "--approve-all",
+        dest="approve",
+        action="store_true",
+        default=False,
+        help="Automatically approve all string matches on Crowdin",
     )
     parser_pretranslate.add_argument("branch", help="Branch name", type=str)
     parser_rebuild = subparsers.add_parser(
@@ -520,32 +517,24 @@ def main():
         "stats", help="Stats for the translations on Crowdin"
     )
     parser_stats.add_argument("branch", help="Branch name", type=str)
-    parser_upload_translation = subparsers.add_parser(
-        "upload-translation", help="Upload a translation from a backup file"
-    )
-    parser_upload_translation.add_argument(
-        "--branch", help="Branch name", type=str, required=True
-    )
-    parser_upload_translation.add_argument(
-        "--language", help="Intl language code (not Crowdin)", type=str, required=True
-    )
-    parser_upload_translation.add_argument(
-        "--file", help="File name (not full path)", type=str
+    parser_upload_translations = subparsers.add_parser(
+        "upload-translations", help="Upload a translation from a backup file"
     )
+    parser_upload_translations.add_argument("branch", help="Branch name", type=str)
     args = parser.parse_args()
 
     if args.command == "download":
         command_download(args.branch)
-    elif args.command == "upload":
-        command_upload(args.branch)
+    elif args.command == "upload-sources":
+        command_upload_sources(args.branch)
     elif args.command == "rebuild":
         command_rebuild(args.branch)
-    elif args.command == "pre-translate":
-        command_pretranslate(args.branch)
+    elif args.command == "pretranslate":
+        command_pretranslate(args.branch, args.approve)
     elif args.command == "stats":
         command_stats(args.branch)
-    elif args.command == "upload-translation":
-        command_upload_translation(args.branch, args.language, args.file)
+    elif args.command == "upload-translations":
+        command_upload_translations(args.branch)
     else:
         logging.warning("Unknown command\n")
         parser.print_help(sys.stderr)


Test patch: