diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59d4c01..fde04df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,10 +10,6 @@ Instructions: ## Testing -### vcrpy - -In addition to [`pytest`](https://docs.pytest.org/), we also use the [`vcrpy`](https://vcrpy.readthedocs.io/) library when writing our tests. - ### tox To run the tests, install the project dependencies in a [virtual environment](https://docs.python.org/3/library/venv.html#module-venv) @@ -41,6 +37,17 @@ pip install "" pip freeze > requirements.txt ``` +### vcrpy + +In addition to [`pytest`](https://docs.pytest.org/), we also use the [`vcrpy`](https://vcrpy.readthedocs.io/) library when writing our tests. + +If you need to update or regenerate a cassette for a test, i.e. [`tests/cassettes/test_translate_missing_messages_without_sorting.yml`](https://github.com/hypercision/i18ntools/blob/main/tests/cassettes/test_translate_missing_messages_without_sorting.yml), then: + +- delete the cassette yml file +- update the `os.environ["TRANSLATOR_API_SUBSCRIPTION_KEY"]` line in the test so it is set to a real API key (but do not commit this change) +- run the tests with `tox`. This will regenerate the cassette yml file +- revert the `os.environ["TRANSLATOR_API_SUBSCRIPTION_KEY"]` line in the test so it is no longer a real API key + ### Editable installation Alternatively, you can perform an [editable installation](https://setuptools.pypa.io/en/latest/userguide/development_mode.html) diff --git a/src/i18ntools/parse_i18n_file.py b/src/i18ntools/parse_i18n_file.py index aaef3c7..eb40e4c 100644 --- a/src/i18ntools/parse_i18n_file.py +++ b/src/i18ntools/parse_i18n_file.py @@ -1,20 +1,23 @@ #!/usr/bin/env python """Parses an i18n Java properties file and returns the data as a dictionary. -The benefit of this method over using configparser is that the whitespace in +If called with remove_backslashes=False, then the whitespace in multiline values is preserved. +If called with remove_backslashes=True, then configparser is used +and the whitespace and backslashes in multiline values are removed. + Note that this method does not work properly for multiline translations with an "=" character in them. - -See related question: https://stackoverflow.com/questions/76047202 """ import argparse +import configparser +import tempfile from pathlib import Path -def parse_i18n_file(file_path): +def parse_i18n_file(file_path, remove_backslashes=False): """Parses an i18n Java properties file and returns the data as a dictionary. Note that this method does not work properly for multiline translations @@ -22,6 +25,9 @@ def parse_i18n_file(file_path): Keyword arguments: file_path -- filepath of the i18n Java properties file to parse + remove_backslashes -- when true, the data returned will not have the + backslashes used in multiline values. + Multiline values will be transformed into single line values. """ if not Path(file_path).exists(): raise FileNotFoundError(f"File {file_path} does not exist") @@ -60,6 +66,82 @@ def parse_i18n_file(file_path): f"It has at least one duplicate key: {duplicate_keys}" ) + if remove_backslashes: + # Now that we've ensured the file has no duplicate properties, return + # the data as a dictionary with multiline values transformed into + # single line values. + return parse_i18n_file_without_backslashes(file_path) + + return data + + +def convert_properties_to_ini(input_path, ini_path): + """Reads a properties file and writes it as an .ini file with a [DEFAULT] section + header to make it compatible with configparser. + + Keyword arguments: + input_path -- filepath of the i18n Java properties file to convert + ini_path -- filepath of the output .ini file + """ + with ( + open(input_path, "r", encoding="utf-8") as infile, + open(ini_path, "w", encoding="utf-8") as outfile, + ): + # Add a dummy section header + outfile.write("[DEFAULT]\n") + outfile.writelines(infile.readlines()) + + +def merge_multiline_string(multiline_string: str) -> str: + """Takes a multiline string as input, removes the backslashes at the + end of each line, and returns a single line string. + + Keyword arguments: + multiline_string -- the input string potentially containing multiple lines + with backslash continuations. + """ + # Split the string into lines and strip any leading/trailing whitespace + # from each line + lines = multiline_string.splitlines() + # Remove the backslash from the end of each line + processed_lines = [line.rstrip("\\").strip() for line in lines] + # Join the lines into a single string, filtering out any empty lines + # to avoid leading/trailing spaces + merged_string = " ".join(line for line in processed_lines if line) + return merged_string + + +def parse_i18n_file_without_backslashes(file_path): + """Parses an i18n Java properties file and returns the data as a dictionary. + Multiline values will be transformed into single line values with the + backslashes removed. + + Note that this method does not work properly for multiline translations + with an "=" character in them. + + Keyword arguments: + file_path -- filepath of the i18n Java properties file to parse + """ + # Use a temporary file that is automatically cleaned up + with tempfile.NamedTemporaryFile( + mode="w", suffix=".ini", encoding="utf-8", delete=True + ) as temp_ini: + # Convert the properties file into a temporary .ini file + convert_properties_to_ini(file_path, temp_ini.name) + + # Parse the temporary .ini file + # Use RawConfigParser to avoid any interpolation or automatic conversions + config = configparser.RawConfigParser(empty_lines_in_values=False) + # Override the optionxform method to prevent lowercase conversion of the keys + config.optionxform = str # type: ignore + + config.read(temp_ini.name, encoding="utf-8") + + data = {} + for key, value in config["DEFAULT"].items(): + merged_string = merge_multiline_string(value) + data[key] = merged_string + return data @@ -80,8 +162,17 @@ def main(): "Can be specified as a relative or absolute file path." ), ) + parser.add_argument( + "-r", + "--remove_backslashes", + action="store_true", + help=( + "the data returned will not have the " + "backslashes used in multiline values." + ), + ) args = parser.parse_args() - result = parse_i18n_file(args.input_file) + result = parse_i18n_file(args.input_file, args.remove_backslashes) for key, value in result.items(): print("key", key) print("value", value) diff --git a/src/i18ntools/translate.py b/src/i18ntools/translate.py index 09ed912..d12e7d7 100644 --- a/src/i18ntools/translate.py +++ b/src/i18ntools/translate.py @@ -109,6 +109,7 @@ def translate_file( output_file_path=None, input_lang=default_lang, translator_region=default_region, + remove_backslashes=False, ): if not Path(input_file_path).exists(): raise FileNotFoundError(f"File {input_file_path} does not exist") @@ -120,7 +121,7 @@ def translate_file( output_file_path = get_default_filepath(input_file_path, output_lang) # Parse the input file into a dictionary - input_data = parse_i18n_file(input_file_path) + input_data = parse_i18n_file(input_file_path, remove_backslashes) # Open the input file in read mode to read its contents with open(input_file_path, "r", encoding="utf-8") as f: @@ -214,9 +215,23 @@ def main(): default=default_region, help="region of the Azure translator resource. Defaults to eastus2", ) + parser.add_argument( + "-rbs", + "--remove_backslashes", + action="store_true", + help=( + "any backslashes from multiline values in the input file " + "will not be included in the text that gets translated." + ), + ) args = parser.parse_args() translate_file( - args.input_file, args.to, args.output_file, args.from_lang, args.region + args.input_file, + args.to, + args.output_file, + args.from_lang, + args.region, + args.remove_backslashes, ) diff --git a/src/i18ntools/translate_missing.py b/src/i18ntools/translate_missing.py index b89d481..8f20cff 100644 --- a/src/i18ntools/translate_missing.py +++ b/src/i18ntools/translate_missing.py @@ -30,6 +30,7 @@ def translate_missing_messages( output_file_path=None, input_lang=default_lang, translator_region=default_region, + remove_backslashes=False, ): if not Path(input_file_path).exists(): raise FileNotFoundError(f"File {input_file_path} does not exist") @@ -46,8 +47,8 @@ def translate_missing_messages( raise FileNotFoundError(f"File {output_file_path} does not exist") # Parse the input file and output file into a dictionary - input_data = parse_i18n_file(input_file_path) - output_data = parse_i18n_file(output_file_path) + input_data = parse_i18n_file(input_file_path, remove_backslashes) + output_data = parse_i18n_file(output_file_path, remove_backslashes) # Find any i18n messages missing from the output file # and put those keys and values in the payload_data dictionary @@ -148,6 +149,15 @@ def main(): default=default_region, help="region of the Azure translator resource. Defaults to eastus2", ) + parser.add_argument( + "-rbs", + "--remove_backslashes", + action="store_true", + help=( + "any backslashes from multiline values in the input file " + "will not be included in the text that gets translated." + ), + ) parser.add_argument( "-s", "--sort", @@ -166,6 +176,7 @@ def main(): args.output_file, args.from_lang, args.region, + args.remove_backslashes, ) diff --git a/tests/cassettes/test_translate_file_without_backslashes.yml b/tests/cassettes/test_translate_file_without_backslashes.yml new file mode 100644 index 0000000..ede8455 --- /dev/null +++ b/tests/cassettes/test_translate_file_without_backslashes.yml @@ -0,0 +1,67 @@ +interactions: +- request: + body: '[{"text": "Property [{0}] of class [{1}] with value [{2}] is less than + minimum value [{3}]"}, {"text": "I want to see you knocking at the door. I wanna + leave you out there waiting in the downpour. Singing that you\u2019re sorry, + dripping on the hall floor."}, {"text": "The customSubmitTS parameter is missing. + It must be present and of type Date."}, {"text": "{0} session removed."}, {"text": + "The trial period has ended for your account and you can no longer use the application."}, + {"text": "Instructor is disabled"}, {"text": " Attendance actions made on this + page will also be made for every session in this group."}, {"text": "Errors: + {0}. \\n\\n Sessions successfully removed: {1}"}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '690' + Content-Type: + - application/json + Ocp-Apim-Subscription-Region: + - eastus2 + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=de + response: + body: + string: "[{\"translations\":[{\"text\":\"Die Eigenschaft [{0}] der Klasse [{1}] + mit Wert [{2}] ist kleiner als der Mindestwert [{3}]\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Ich + will dich an der T\xFCr klopfen sehen. Ich will dich drau\xDFen im Wolkenbruch + warten lassen. Singend, dass es dir leid tut, tropfend auf den Flurboden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + customSubmitTS-Parameter fehlt. Es muss anwesend und vom Typ Datum sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} + Sitzung entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Die Testphase + f\xFCr dein Konto ist beendet und du kannst die Anwendung nicht mehr nutzen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + Ausbilder ist behindert\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"F\xFCr jede Sitzung in dieser Gruppe werden auch die auf dieser Seite vorgenommenen + Anwesenheitsma\xDFnahmen vorgenommen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Fehler: + {0}. \\\\n\\\\n Sitzungen erfolgreich entfernt: {1}\",\"to\":\"de\"}]}]" + headers: + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Jan 2026 20:16:28 GMT + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + access-control-expose-headers: + - X-RequestId,X-Metered-Usage,X-MT-System + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '438' + x-metered-usage: + - '571' + x-mt-system: + - Microsoft + x-requestid: + - 5e1ef095-341f-4bca-b80b-b07d6089bf28.EUWE.0114T2016 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_translate_missing_messages.yml b/tests/cassettes/test_translate_missing_messages.yml index fb3a22a..62d0a57 100644 --- a/tests/cassettes/test_translate_missing_messages.yml +++ b/tests/cassettes/test_translate_missing_messages.yml @@ -5,7 +5,10 @@ interactions: dripping on the hall floor."}, {"text": "The customSubmitTS parameter is missing. \\\n It must be present and of type Date."}, {"text": "{0} session removed."}, {"text": "The trial period has ended for your account \\\n and you can no - longer use the application."}, {"text": "Instructor is disabled"}]' + longer use the application."}, {"text": "Instructor is disabled"}, {"text": + "\\\n Attendance actions made on this page will also be made for every session + in this group."}, {"text": "Errors: {0}. \\n\\n Sessions successfully removed: + {1}"}]' headers: Accept: - '*/*' @@ -14,49 +17,51 @@ interactions: Connection: - keep-alive Content-Length: - - '459' + - '636' Content-Type: - application/json Ocp-Apim-Subscription-Region: - eastus2 User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.5 method: POST uri: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=de response: body: string: "[{\"translations\":[{\"text\":\"Ich will dich an der T\xFCr klopfen - sehen. \\\\\\n Ich m\xF6chte dich da drau\xDFen im Regenguss warten lassen. - \\\\\\n Singt, dass es dir leid tut, tropft auf den Boden des Flurs.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der - Parameter customSubmitTS fehlt. \\\\\\n Er muss vorhanden sein und vom - Typ Date sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} Sitzung - wurde entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der Testzeitraum - f\xFCr Ihr Konto ist abgelaufen \\\\\\n und Sie k\xF6nnen die Anwendung + sehen. \\\\\\n Ich will dich drau\xDFen im Wolkenbruch warten lassen. \\\\\\n + \ Singend, dass es dir leid tut, tropfend auf den Flurboden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + customSubmitTS-Parameter fehlt. \\\\\\n Es muss anwesend und vom Typ Datum + sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} Sitzung entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Die + Testphase f\xFCr dein Konto ist beendet \\\\\\n Und Sie k\xF6nnen die Anwendung nicht mehr verwenden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der - Kursleiter ist deaktiviert\",\"to\":\"de\"}]}]" + Ausbilder ist behindert\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"\\\\\\n + \ F\xFCr jede Sitzung in dieser Gruppe werden auch die auf dieser Seite + vorgenommenen Anwesenheitsma\xDFnahmen vorgenommen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Fehler: + {0}. \\\\n\\\\n Sitzungen erfolgreich entfernt: {1}\",\"to\":\"de\"}]}]" headers: - Access-Control-Expose-Headers: - - X-RequestId - - X-Metered-Usage - - X-MT-System Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - - Tue, 30 Jul 2024 20:07:23 GMT + - Wed, 14 Jan 2026 19:36:47 GMT Strict-Transport-Security: - max-age=31536000; includeSubDomains Transfer-Encoding: - chunked - X-Content-Type-Options: + access-control-expose-headers: + - X-RequestId,X-Metered-Usage,X-MT-System + x-content-type-options: - nosniff - X-MT-System: + x-envoy-upstream-service-time: + - '734' + x-metered-usage: + - '521' + x-mt-system: - Microsoft - X-Metered-Usage: - - '376' - X-RequestId: - - 4a67fa69-cb62-4299-9305-a0558026866f.USE2.0730T2007 + x-requestid: + - c49e2ff1-99a1-4a2d-b6f9-16660e39dd42.EUWE.0114T1936 status: code: 200 message: OK diff --git a/tests/cassettes/test_translate_missing_messages_remove_backslashes.yml b/tests/cassettes/test_translate_missing_messages_remove_backslashes.yml new file mode 100644 index 0000000..80c0da7 --- /dev/null +++ b/tests/cassettes/test_translate_missing_messages_remove_backslashes.yml @@ -0,0 +1,65 @@ +interactions: +- request: + body: '[{"text": "I want to see you knocking at the door. I wanna leave you out + there waiting in the downpour. Singing that you\u2019re sorry, dripping on the + hall floor."}, {"text": "The customSubmitTS parameter is missing. It must be + present and of type Date."}, {"text": "{0} session removed."}, {"text": "The + trial period has ended for your account and you can no longer use the application."}, + {"text": "Instructor is disabled"}, {"text": " Attendance actions made on this + page will also be made for every session in this group."}, {"text": "Errors: + {0}. \\n\\n Sessions successfully removed: {1}"}]' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '597' + Content-Type: + - application/json + Ocp-Apim-Subscription-Region: + - eastus2 + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=de + response: + body: + string: "[{\"translations\":[{\"text\":\"Ich will dich an der T\xFCr klopfen + sehen. Ich will dich drau\xDFen im Wolkenbruch warten lassen. Singend, dass + es dir leid tut, tropfend auf den Flurboden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + customSubmitTS-Parameter fehlt. Es muss anwesend und vom Typ Datum sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} + Sitzung entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Die Testphase + f\xFCr dein Konto ist beendet und du kannst die Anwendung nicht mehr nutzen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + Ausbilder ist behindert\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"F\xFCr jede Sitzung in dieser Gruppe werden auch die auf dieser Seite vorgenommenen + Anwesenheitsma\xDFnahmen vorgenommen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Fehler: + {0}. \\\\n\\\\n Sitzungen erfolgreich entfernt: {1}\",\"to\":\"de\"}]}]" + headers: + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Wed, 14 Jan 2026 19:36:49 GMT + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + Transfer-Encoding: + - chunked + access-control-expose-headers: + - X-RequestId,X-Metered-Usage,X-MT-System + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '488' + x-metered-usage: + - '492' + x-mt-system: + - Microsoft + x-requestid: + - 234b31a1-6f98-4ff2-9091-7b6a409ecf58.EUWE.0114T1936 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_translate_missing_messages_without_sorting.yml b/tests/cassettes/test_translate_missing_messages_without_sorting.yml index c03b489..d7ccc9a 100644 --- a/tests/cassettes/test_translate_missing_messages_without_sorting.yml +++ b/tests/cassettes/test_translate_missing_messages_without_sorting.yml @@ -5,7 +5,10 @@ interactions: dripping on the hall floor."}, {"text": "The customSubmitTS parameter is missing. \\\n It must be present and of type Date."}, {"text": "{0} session removed."}, {"text": "The trial period has ended for your account \\\n and you can no - longer use the application."}, {"text": "Instructor is disabled"}]' + longer use the application."}, {"text": "Instructor is disabled"}, {"text": + "\\\n Attendance actions made on this page will also be made for every session + in this group."}, {"text": "Errors: {0}. \\n\\n Sessions successfully removed: + {1}"}]' headers: Accept: - '*/*' @@ -14,49 +17,51 @@ interactions: Connection: - keep-alive Content-Length: - - '459' + - '636' Content-Type: - application/json Ocp-Apim-Subscription-Region: - eastus2 User-Agent: - - python-requests/2.32.3 + - python-requests/2.32.5 method: POST uri: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&to=de response: body: string: "[{\"translations\":[{\"text\":\"Ich will dich an der T\xFCr klopfen - sehen. \\\\\\n Ich m\xF6chte dich da drau\xDFen im Regenguss warten lassen. - \\\\\\n Singt, dass es dir leid tut, tropft auf den Boden des Flurs.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der - Parameter customSubmitTS fehlt. \\\\\\n Er muss vorhanden sein und vom - Typ Date sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} Sitzung - wurde entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der Testzeitraum - f\xFCr Ihr Konto ist abgelaufen \\\\\\n und Sie k\xF6nnen die Anwendung + sehen. \\\\\\n Ich will dich drau\xDFen im Wolkenbruch warten lassen. \\\\\\n + \ Singend, dass es dir leid tut, tropfend auf den Flurboden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der + customSubmitTS-Parameter fehlt. \\\\\\n Es muss anwesend und vom Typ Datum + sein.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"{0} Sitzung entfernt.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Die + Testphase f\xFCr dein Konto ist beendet \\\\\\n Und Sie k\xF6nnen die Anwendung nicht mehr verwenden.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Der - Kursleiter ist deaktiviert\",\"to\":\"de\"}]}]" + Ausbilder ist behindert\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"\\\\\\n + \ F\xFCr jede Sitzung in dieser Gruppe werden auch die auf dieser Seite + vorgenommenen Anwesenheitsma\xDFnahmen vorgenommen.\",\"to\":\"de\"}]},{\"translations\":[{\"text\":\"Fehler: + {0}. \\\\n\\\\n Sitzungen erfolgreich entfernt: {1}\",\"to\":\"de\"}]}]" headers: - Access-Control-Expose-Headers: - - X-RequestId - - X-Metered-Usage - - X-MT-System Connection: - keep-alive Content-Type: - application/json; charset=utf-8 Date: - - Tue, 30 Jul 2024 20:33:01 GMT + - Wed, 14 Jan 2026 19:36:48 GMT Strict-Transport-Security: - max-age=31536000; includeSubDomains Transfer-Encoding: - chunked - X-Content-Type-Options: + access-control-expose-headers: + - X-RequestId,X-Metered-Usage,X-MT-System + x-content-type-options: - nosniff - X-MT-System: + x-envoy-upstream-service-time: + - '604' + x-metered-usage: + - '521' + x-mt-system: - Microsoft - X-Metered-Usage: - - '376' - X-RequestId: - - 7a7eb71f-bb78-4366-941d-266b3eab605a.USE2.0730T2033 + x-requestid: + - a9ce065f-4a23-4d02-98f9-d3acaefbb1cf.EUWE.0114T1936 status: code: 200 message: OK diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9ea0693 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import pytest + + +@pytest.fixture +def fake_german_i18n_data_without_backslashes(): + """Fixture that returns static German i18n file data with newlines and + backslashes not preserved when input file was translated.""" + with open( + "tests/resources/translations_de_no_backslashes.properties", + "r", + encoding="utf-8", + ) as f: + return f.read() diff --git a/tests/resources/example.properties b/tests/resources/example.properties index 3a00001..5ea2275 100644 --- a/tests/resources/example.properties +++ b/tests/resources/example.properties @@ -13,3 +13,6 @@ instructorService.removeSession.success={0} session removed. handshake.register.suspended.error=The trial period has ended for your account \ and you can no longer use the application. handshake.register.disabledException.error=Instructor is disabled +associatedGroupMessageText=\ + Attendance actions made on this page will also be made for every session in this group. +removedMultipleSessionsErrorsMessage=Errors: {0}. \n\n Sessions successfully removed: {1} diff --git a/tests/resources/translations_de.properties b/tests/resources/translations_de.properties index 1f026f0..fc273c9 100644 --- a/tests/resources/translations_de.properties +++ b/tests/resources/translations_de.properties @@ -1,15 +1,18 @@ -default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] ist kleiner als der Mindestwert [{3}] +default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] mit Wert [{2}] ist kleiner als der Mindestwert [{3}] # Track 4 on Expert In A Dying Field TheBeths.YourSide.lyrics=Ich will dich an der Tür klopfen sehen. \ - Ich möchte dich da draußen im Regenguss warten lassen. \ - Singt, dass es dir leid tut, tropft auf den Boden des Flurs. -instructor.submitWithCustomTime.customSubmitTS.missing.error=Der Parameter customSubmitTS fehlt. \ - Er muss vorhanden sein und vom Typ Date sein. + Ich will dich draußen im Wolkenbruch warten lassen. \ + Singend, dass es dir leid tut, tropfend auf den Flurboden. +instructor.submitWithCustomTime.customSubmitTS.missing.error=Der customSubmitTS-Parameter fehlt. \ + Es muss anwesend und vom Typ Datum sein. # SessionItem.itemID is the first parameter -instructorService.removeSession.success={0} Sitzung wurde entfernt. +instructorService.removeSession.success={0} Sitzung entfernt. -handshake.register.suspended.error=Der Testzeitraum für Ihr Konto ist abgelaufen \ - und Sie können die Anwendung nicht mehr verwenden. -handshake.register.disabledException.error=Der Kursleiter ist deaktiviert +handshake.register.suspended.error=Die Testphase für dein Konto ist beendet \ + Und Sie können die Anwendung nicht mehr verwenden. +handshake.register.disabledException.error=Der Ausbilder ist behindert +associatedGroupMessageText=\ + Für jede Sitzung in dieser Gruppe werden auch die auf dieser Seite vorgenommenen Anwesenheitsmaßnahmen vorgenommen. +removedMultipleSessionsErrorsMessage=Fehler: {0}. \n\n Sitzungen erfolgreich entfernt: {1} diff --git a/tests/resources/translations_de2.properties b/tests/resources/translations_de2.properties index 86a0fa1..a918829 100644 --- a/tests/resources/translations_de2.properties +++ b/tests/resources/translations_de2.properties @@ -1,10 +1,13 @@ -default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] mit dem Wert [{2}] ist kleiner als der Mindestwert [{3}] +default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] mit Wert [{2}] ist kleiner als der Mindestwert [{3}] TheBeths.YourSide.lyrics=Ich will dich an der Tür klopfen sehen. \ - Ich möchte dich da draußen im Regenguss warten lassen. \ - Singt, dass es dir leid tut, tropft auf den Boden des Flurs. -instructor.submitWithCustomTime.customSubmitTS.missing.error=Der Parameter customSubmitTS fehlt. \ - Er muss vorhanden sein und vom Typ Date sein. -instructorService.removeSession.success={0} Sitzung wurde entfernt. -handshake.register.suspended.error=Der Testzeitraum für Ihr Konto ist abgelaufen \ - und Sie können die Anwendung nicht mehr verwenden. -handshake.register.disabledException.error=Der Kursleiter ist deaktiviert + Ich will dich draußen im Wolkenbruch warten lassen. \ + Singend, dass es dir leid tut, tropfend auf den Flurboden. +instructor.submitWithCustomTime.customSubmitTS.missing.error=Der customSubmitTS-Parameter fehlt. \ + Es muss anwesend und vom Typ Datum sein. +instructorService.removeSession.success={0} Sitzung entfernt. +handshake.register.suspended.error=Die Testphase für dein Konto ist beendet \ + Und Sie können die Anwendung nicht mehr verwenden. +handshake.register.disabledException.error=Der Ausbilder ist behindert +associatedGroupMessageText=\ + Für jede Sitzung in dieser Gruppe werden auch die auf dieser Seite vorgenommenen Anwesenheitsmaßnahmen vorgenommen. +removedMultipleSessionsErrorsMessage=Fehler: {0}. \n\n Sitzungen erfolgreich entfernt: {1} diff --git a/tests/resources/translations_de_no_backslashes.properties b/tests/resources/translations_de_no_backslashes.properties new file mode 100644 index 0000000..d765d7c --- /dev/null +++ b/tests/resources/translations_de_no_backslashes.properties @@ -0,0 +1,13 @@ +default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] mit Wert [{2}] ist kleiner als der Mindestwert [{3}] + +# Track 4 on Expert In A Dying Field +TheBeths.YourSide.lyrics=Ich will dich an der Tür klopfen sehen. Ich will dich draußen im Wolkenbruch warten lassen. Singend, dass es dir leid tut, tropfend auf den Flurboden. +instructor.submitWithCustomTime.customSubmitTS.missing.error=Der customSubmitTS-Parameter fehlt. Es muss anwesend und vom Typ Datum sein. + +# SessionItem.itemID is the first parameter +instructorService.removeSession.success={0} Sitzung entfernt. + +handshake.register.suspended.error=Die Testphase für dein Konto ist beendet und du kannst die Anwendung nicht mehr nutzen. +handshake.register.disabledException.error=Der Ausbilder ist behindert +associatedGroupMessageText=Für jede Sitzung in dieser Gruppe werden auch die auf dieser Seite vorgenommenen Anwesenheitsmaßnahmen vorgenommen. +removedMultipleSessionsErrorsMessage=Fehler: {0}. \n\n Sitzungen erfolgreich entfernt: {1} diff --git a/tests/test_parse_i18n_file.py b/tests/test_parse_i18n_file.py index 9514137..94e8a98 100644 --- a/tests/test_parse_i18n_file.py +++ b/tests/test_parse_i18n_file.py @@ -4,7 +4,7 @@ def test_parse_file(): parsed_data = parse_i18n_file("tests/resources/example.properties") - assert len(parsed_data.keys()) == 6 + assert len(parsed_data.keys()) == 8 assert ( parsed_data["instructorService.removeSession.success"] == "{0} session removed." ) @@ -26,6 +26,15 @@ def test_parse_file(): "\n Singing that you’re sorry, dripping on the hall floor." ) + assert parsed_data["associatedGroupMessageText"] == ( + "\\\n Attendance actions made on this page will " + "also be made for every session in this group." + ) + + assert parsed_data["removedMultipleSessionsErrorsMessage"] == ( + "Errors: {0}. \\n\\n Sessions successfully removed: {1}" + ) + def test_parse_file_2(): """Test for parsing a file with a value that has two consecutive @@ -49,9 +58,67 @@ def test_parse_file_2(): "\n \\n\\" "\n \\ Email: tina.herring@yourcompany.com" ) + # Now test parsing the file when removing backslashes + parsed_data = parse_i18n_file( + "tests/resources/example2.properties", remove_backslashes=True + ) + assert len(parsed_data.keys()) == 4 + assert parsed_data["handshake.register.mobileDeviceLimitReached.error"] == ( + "The device limit of {0} devices has been reached " + "for your company''s account and new devices cannot use the application. " + "Please contact your administrator." + ) + assert parsed_data["recordAttendance.segment.notFound.error"] == ( + "Segment with segmentID {0} not found in the SessionItem's segments" + ) + assert parsed_data["me"] == "first!" + assert parsed_data["clientHelpText"] == ( + "For issues with the software, tablets, and card scanners " + "only, please contact Tina Herring at \\n \\n " + "\\ Email: tina.herring@yourcompany.com" + ) + + +def test_parse_file_and_remove_slashes(): + parsed_data = parse_i18n_file( + "tests/resources/example.properties", remove_backslashes=True + ) + assert len(parsed_data.keys()) == 8 + assert "instructorService.removeSession.success" in parsed_data + assert ( + parsed_data["instructorService.removeSession.success"] == "{0} session removed." + ) + assert parsed_data["default.invalid.min.message"] == ( + "Property [{0}] of class [{1}] with value " + "[{2}] is less than minimum value [{3}]" + ) + + assert parsed_data[ + "instructor.submitWithCustomTime.customSubmitTS.missing.error" + ] == ( + "The customSubmitTS parameter is missing. " + "It must be present and of type Date." + ) + + assert parsed_data["TheBeths.YourSide.lyrics"] == ( + "I want to see you knocking at the door. " + "I wanna leave you out there waiting in the downpour. " + "Singing that you’re sorry, dripping on the hall floor." + ) + + assert parsed_data["associatedGroupMessageText"] == ( + "Attendance actions made on this page will " + "also be made for every session in this group." + ) + + assert parsed_data["removedMultipleSessionsErrorsMessage"] == ( + "Errors: {0}. \\n\\n Sessions successfully removed: {1}" + ) def test_parse_file_with_duplicate_keys(): """SyntaxWarning is raised for files with duplicate keys""" with pytest.raises(SyntaxWarning): parse_i18n_file("tests/resources/duplicate.properties") + with pytest.raises(SyntaxWarning): + parse_i18n_file("tests/resources/duplicate.properties", remove_backslashes=True) diff --git a/tests/test_translate.py b/tests/test_translate.py index a23dc64..76855cb 100644 --- a/tests/test_translate.py +++ b/tests/test_translate.py @@ -81,6 +81,25 @@ def test_translate_file(tmp_path, fake_portuguese_i18n_data): assert output_file.read_text() == fake_portuguese_i18n_data +@vcr.use_cassette( + "tests/cassettes/test_translate_file_without_backslashes.yml", + filter_headers=filter_headers, +) # type: ignore +def test_translate_file_without_backslashes( + tmp_path, fake_german_i18n_data_without_backslashes +): + """translate_file successfully writes translations to an output file""" + os.environ["TRANSLATOR_API_SUBSCRIPTION_KEY"] = "not-an-actual-api-key" + output_file = tmp_path / "example_de.properties" + translate_file( + "tests/resources/example.properties", + "de", + remove_backslashes=True, + output_file_path=output_file, + ) + assert output_file.read_text() == fake_german_i18n_data_without_backslashes + + @vcr.use_cassette( "tests/cassettes/test_make_api_call.yml", filter_headers=filter_headers ) # type: ignore diff --git a/tests/test_translate_missing.py b/tests/test_translate_missing.py index 9531e04..3798dba 100644 --- a/tests/test_translate_missing.py +++ b/tests/test_translate_missing.py @@ -49,7 +49,7 @@ def test_translate_missing_messages_without_api_key(tmp_path): """KeyError is raised when environment variable TRANSLATOR_API_SUBSCRIPTION_KEY is not set """ - # Set the environment variable and then unset it it. + # Set the environment variable and then unset it. os.environ["TRANSLATOR_API_SUBSCRIPTION_KEY"] = "three_little_ducks" os.environ.pop("TRANSLATOR_API_SUBSCRIPTION_KEY") output_file = tmp_path / "messages_de.properties" @@ -77,7 +77,7 @@ def test_translate_missing_messages_with_invalid_api_key(tmp_path): output_file = tmp_path / "messages_de.properties" output_file.write_text( "default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] " - "mit dem Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" + "mit Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" ) with pytest.raises(requests.exceptions.HTTPError): translate_missing_messages( @@ -99,7 +99,7 @@ def test_translate_missing_messages(tmp_path, fake_german_i18n_data): output_file = tmp_path / "messages_de.properties" output_file.write_text( "default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] " - "mit dem Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" + "mit Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" ) translate_missing_messages( "tests/resources/example.properties", @@ -125,7 +125,7 @@ def test_translate_missing_messages_without_sorting( output_file = tmp_path / "messages_de.properties" output_file.write_text( "default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] " - "mit dem Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" + "mit Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" ) translate_missing_messages( "tests/resources/example.properties", @@ -134,3 +134,29 @@ def test_translate_missing_messages_without_sorting( output_file_path=str(output_file), ) assert output_file.read_text() == fake_german_i18n_data_unsorted + + +@vcr.use_cassette( + "tests/cassettes/test_translate_missing_messages_remove_backslashes.yml", + filter_headers=filter_headers, +) # type: ignore +def test_translate_missing_messages_remove_backslashes( + tmp_path, fake_german_i18n_data_without_backslashes +): + """translate_missing_messages fills an i18n file with the + translations it was missing + """ + os.environ["TRANSLATOR_API_SUBSCRIPTION_KEY"] = "not-an-actual-api-key" + output_file = tmp_path / "messages_de.properties" + output_file.write_text( + "default.invalid.min.message=Die Eigenschaft [{0}] der Klasse [{1}] " + "mit Wert [{2}] ist kleiner als der Mindestwert [{3}]\n" + ) + translate_missing_messages( + "tests/resources/example.properties", + "de", + sort_file=True, + remove_backslashes=True, + output_file_path=str(output_file), + ) + assert output_file.read_text() == fake_german_i18n_data_without_backslashes