diff --git a/scripts/translations/extract-all b/scripts/translations/extract-all index 4c5fa09219..98e748f4ee 100755 --- a/scripts/translations/extract-all +++ b/scripts/translations/extract-all @@ -9,14 +9,15 @@ # dir=$1 dest=$2 +touch $dest for f in $(find -L "$dir" -name \*.py) do echo "Extracting strings from python file: $f" - xgettext --from-code=UTF-8 --language=python -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $f + xgettext --from-code=UTF-8 --join-existing --sort-by-file --language=python -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $f done for f in $(find -L "$dir" -name \*.qml) do echo "Extracting strings from qml file: $f" - xgettext --from-code=UTF-8 --join-existing --language=javascript -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $f + xgettext --from-code=UTF-8 --join-existing --sort-by-file --language=javascript -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $f done diff --git a/scripts/translations/extract_strings.py b/scripts/translations/extract_strings.py new file mode 100644 index 0000000000..30608eeeb4 --- /dev/null +++ b/scripts/translations/extract_strings.py @@ -0,0 +1,135 @@ +# Copyright (c) 2023 UltiMaker. +# Cura is released under the terms of the LGPLv3 or higher. + +import argparse +import os +import subprocess +from os.path import isfile + +from pathlib import Path + +def extract_all_strings(root_path: Path, script_path: Path, translations_root_path: Path, all_strings_pot_path: Path): + """ Extracts all strings into a pot file with empty translations. + + Strings are extracted everywhere that i18n is used in python and qml in the project. It also checks the project + for JSON files with 'settings' in the root node and extracts these for translation as well. + + @param root_path: The root path of the project. This is the root for string searching. + @param script_path: The location of the bash scripts used for translating. + @param translations_root_path: The root of the translations folder (resources/i18n). + @param all_strings_pot_path: The path of the pot file where all strings will be outputted (resources/i8n/cura.pot). + """ + + # # Extract the setting strings from any json file with settings at its root + # extract_json_arguments = [ + # script_path.joinpath("extract-json"), + # root_path.joinpath("resources", "definitions"), + # translations_root_path + # ] + # subprocess.run(extract_json_arguments) + # + # Extract all strings from qml and py files + extract_qml_py_arguments = [ + script_path.joinpath("extract-all"), + root_path, + all_strings_pot_path + ] + subprocess.run(extract_qml_py_arguments) + + # Extract all the name and description from all plugins + extract_plugin_arguments = [ + script_path.joinpath("extract-plugins"), + root_path.joinpath("plugins"), + all_strings_pot_path + ] + subprocess.run(extract_plugin_arguments) + + # Convert the output file to utf-8 + convert_encoding_arguments = [ + "msgconv", + "--to-code=UTF-8", + all_strings_pot_path, + "-o", + all_strings_pot_path + ] + subprocess.run(convert_encoding_arguments) + + +def update_po_files_all_languages(translation_root_path: Path) -> None: + """ Updates all po files in translation_root_path with new strings mapped to blank translations. + + This will take all newly generated po files in the root of the translations path (i18n/cura.pot, i18n/fdmextruder.json.def.pot) + and merge them with the existing po files for every language. This will create new po files with empty translations + for all new words added to the project. + + @param translation_root_path: Root of the translations folder (resources/i18n). + """ + new_pot_files = [] + + for file in os.listdir(translation_root_path): + path = translations_root_path.joinpath(file) + if path.suffix == ".pot": + new_pot_files.append(path) + print(new_pot_files) + + for directory, _, po_files in os.walk(translation_root_path): + print(directory) + print(po_files) + for pot in new_pot_files: + + po_filename = pot.name.rstrip("t") + if po_filename not in po_files: + continue # We only want to merge files that have matching names + + pot_file = pot + po_file = Path(directory, po_filename).absolute() + + # # Initialize the new po file + # init_files_arguments = [ + # "msginit", + # "--no-wrap", + # "--no-translator", + # "-l", language, + # "-i", pot_file, + # "-o", po_file + # ] + # + # subprocess.run(init_files_arguments) + + merge_files_arguments = [ + "msgmerge", + "--no-wrap", + "--no-fuzzy-matching", + "--update", + "--sort-by-file", # Sort by file location, this is better than pure sorting for translators + po_file, # po file that will be updated + pot_file # source of new strings + ] + + subprocess.run(merge_files_arguments) + + return + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract strings from project into .po files") + parser.add_argument("root_path", type=str, help="The root of the project to extract translatable strings from") + parser.add_argument("translation_file_name", type=str, help="The .pot file that all strings from python/qml files will be inserted into") + parser.add_argument("script_path", type=str, help="The path containing the scripts for translating files") + args = parser.parse_args() + + root_path = Path(args.root_path) # root of the project + script_path = Path(args.script_path) # location of bash scripts + + # Path where all translation file are + translations_root_path = root_path.joinpath("resources", "i18n") + translations_root_path.mkdir(parents=True, exist_ok=True) # Make sure we have an output path + + all_strings_pot_path = translations_root_path.joinpath(args.translation_file_name) # pot file containing all strings untranslated + + # Clear the output file, otherwise deleted strings will still be in the output. + if os.path.exists(all_strings_pot_path): + os.remove(all_strings_pot_path) + + extract_all_strings(root_path, script_path, translations_root_path, all_strings_pot_path) + update_po_files_all_languages(translations_root_path) diff --git a/scripts/translations/pirate.py b/scripts/translations/pirate.py deleted file mode 100644 index 6c0c170537..0000000000 --- a/scripts/translations/pirate.py +++ /dev/null @@ -1,108 +0,0 @@ -#Creates the Pirate translation files. - -import sys #To get command line arguments. -import pirateofdoom #Contains our translation dictionary. -import re #Case insensitive search and replace. -import random # Take random translation candidates - -pot_file = sys.argv[1] -po_file = sys.argv[2] - -#Translates English to Pirate. -def translate(english): - english = english.replace("&", "") #Pirates don't take shortcuts. - for eng, pir in pirateofdoom.pirate.items(): - matches = list(re.finditer(r"\b" + eng.lower() + r"\b", english.lower())) - matches = [match.start(0) for match in matches] - matches = reversed(sorted(matches)) - for position in matches: - #Make sure the case is correct. - uppercase = english[position].lower() != english[position] - - if isinstance(pir, list): - pir = random.choice(pir) - - first_character = pir[0] - rest_characters = pir[1:] - if uppercase: - first_character = first_character.upper() - else: - first_character = first_character.lower() - pir = first_character + rest_characters - - english = english[:position] + pir + english[position + len(eng):] - return english - -translations = {} - -last_id = "" -last_id_plural = "" -last_ctxt = "" -last_str = "" -state = "unknown" -with open(pot_file, encoding = "utf-8") as f: - for line in f: - if line.startswith("msgctxt"): - state = "ctxt" - if last_id != "": - translations[(last_ctxt, last_id, last_id_plural)] = last_str - last_ctxt = "" - last_id = "" - last_id_plural = "" - last_str = "" - elif line.startswith("msgid_plural"): - state = "idplural" - elif line.startswith("msgid"): - state = "id" - elif line.startswith("msgstr"): - state = "str" - - if line.count('"') >= 2: #There's an ID on this line! - line = line[line.find('"') + 1:] #Strip everything before the first ". - line = line[:line.rfind('"')] #And after the last ". - - if state == "ctxt": - last_ctxt += line #What's left is the context. - elif state == "idplural": - last_id_plural += line #Or the plural ID. - elif state == "id": - last_id += line #Or the ID. - elif state == "str": - last_str += line #Or the actual string. - -for key, _ in translations.items(): - context, english, english_plural = key - pirate = translate(english) - pirate_plural = translate(english_plural) - translations[key] = (pirate, pirate_plural) - -with open(po_file, "w", encoding = "utf-8") as f: - f.write("""msgid "" -msgstr "" -"Project-Id-Version: Pirate\\n" -"Report-Msgid-Bugs-To: plugins@ultimaker.com\\n" -"POT-Creation-Date: 1492\\n" -"PO-Revision-Date: 1492\\n" -"Last-Translator: Ghostkeeper and Awhiemstra\\n" -"Language-Team: Ghostkeeper and Awhiemstra\\n" -"Language: Pirate\\n" -"Lang-Code: en\\n" -"Country-Code: en_7S\\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=UTF-8\\n" -"Content-Transfer-Encoding: 8bit\\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\\n" -""") - for key, value in translations.items(): - context, english, english_plural = key - pirate, pirate_plural = value - f.write('msgctxt "{context}"\n'.format(context = context)) - if english_plural == "": #No plurals in this item. - f.write('msgid "{english}"\n'.format(english = english)) - f.write('msgstr "{pirate}"\n'.format(pirate = pirate)) - else: - f.write('msgid "{english}"\n'.format(english = english)) - f.write('msgid_plural "{english_plural}"\n'.format(english_plural = english_plural)) - f.write('msgstr[0] "{pirate}"\n'.format(pirate = pirate)) - f.write('msgstr[1] "{pirate_plural}"\n'.format(pirate_plural = pirate_plural)) - f.write("\n") #Empty line. \ No newline at end of file diff --git a/scripts/translations/pirateofdoom.py b/scripts/translations/pirateofdoom.py deleted file mode 100644 index e8b8a28958..0000000000 --- a/scripts/translations/pirateofdoom.py +++ /dev/null @@ -1,77 +0,0 @@ -pirate = { - "build plate": "deck", - "buildplate": "deck", - "quit": "abandon ship", - "back": "avast", - "nozzle": "muzzle", - "nozzles": "muzzles", - "extruder": "cannon", - "extruders": "cannons", - "yes": "aye", - "no": "nay", - "loading": "haulin'", - "you": "ye", - "you're": "ye are", - "ok": "aye", - "machine": "ship", - "machines": "ships", - "mm/s²": "knots/s", - "mm/s": "knots", - "printer": "ship", - "printers": "ships", - "view": "spyglass", - "support": "peg legs", - "fan": "wind", - "file": "treasure", - "file(s)": "treasure(s)", - "files": "treasures", - "profile": "map", - "profiles": "maps", - "setting": "knob", - "settings": "knobs", - "shield": "sail", - "your": "yer", - "the": "th'", - "travel": "journey", - "wireframe": "ropey", - "wire": "rope", - "are": "be", - "is": "be", - "there": "thar", - "not": "nay", - "delete": "send to Davy Jones' locker", - "remove": "send to Davy Jones' locker", - "print": "scribble", - "printing": "scribblin'", - "load": "haul", - "connect to": "board", - "connecting": "boarding", - "collects": "hoards", - "prime tower": "buoy", - "change log": "captain's log", - "my": "me", - "removable drive": "life boat", - "print core": "scribbler", - "printcore": "scribbler", - "abort": ["maroon", "abandon"], - "aborting": ["marooning", "abandoning"], - "aborted": ["marooned", "abandoned"], - "connected": ["anchored", "moored"], - "developer": "scurvy dog", - "wizard": "cap'n", - "active leveling": "keelhauling", - "download": "plunder", - "downloaded": "plundered", - "caution hot surface": "fire in the hole!", - "type": "sort", - "spool": "barrel", - "surface": "lacquer", - "zigzag": "heave-to", - "bottom": "bilge", - "top": "deck", - "ironing": "deck swabbing", - "adhesion": "anchorage", - "blob": "barnacle", - "blobs": "barnacles", - "slice": "set sail", -}