1# Copyright (c) 2019 Ultimaker B.V. 2# Cura is released under the terms of the LGPLv3 or higher. 3 4import argparse #To get the source directory from command line arguments. 5import io # To fix encoding issues in Windows 6import os #To find files from the source. 7import os.path #To find files from the source and the destination path. 8 9cura_files = {"cura", "fdmprinter.def.json", "fdmextruder.def.json"} 10uranium_files = {"uranium"} 11 12def lionbridge_import(source: str) -> None: 13 """Imports translation files from Lionbridge. 14 15 Lionbridge has a bit of a weird export feature. It exports it to the same 16 file type as what we imported, so that's a .pot file. However this .pot file 17 only contains the translations, so the header is completely empty. We need 18 to merge those translations into our existing files so that the header is 19 preserved. 20 """ 21 22 print("Importing from:", source) 23 print("Importing to Cura:", destination_cura()) 24 print("Importing to Uranium:", destination_uranium()) 25 26 for language in (directory for directory in os.listdir(source) if os.path.isdir(os.path.join(source, directory))): 27 print("================ Processing language:", language, "================") 28 directory = os.path.join(source, language) 29 for file_pot in (file for file in os.listdir(directory) if file.endswith(".pot")): 30 source_file = file_pot[:-4] #Strip extension. 31 if source_file in cura_files: 32 destination_file = os.path.join(destination_cura(), language.replace("-", "_"), source_file + ".po") 33 print("Merging", source_file, "(Cura) into", destination_file) 34 elif source_file in uranium_files: 35 destination_file = os.path.join(destination_uranium(), language.replace("-", "_"), source_file + ".po") 36 print("Merging", source_file, "(Uranium) into", destination_file) 37 else: 38 raise Exception("Unknown file: " + source_file + "... Is this Cura or Uranium?") 39 40 with io.open(os.path.join(directory, file_pot), encoding = "utf8") as f: 41 source_str = f.read() 42 with io.open(destination_file, encoding = "utf8") as f: 43 destination_str = f.read() 44 result = merge(source_str, destination_str) 45 with io.open(destination_file, "w", encoding = "utf8") as f: 46 f.write(result) 47 48 49def destination_cura() -> str: 50 """Gets the destination path to copy the translations for Cura to. 51 52 :return: Destination path for Cura. 53 """ 54 return os.path.abspath(os.path.join(__file__, "..", "..", "resources", "i18n")) 55 56 57def destination_uranium() -> str: 58 """Gets the destination path to copy the translations for Uranium to. 59 60 :return: Destination path for Uranium. 61 """ 62 try: 63 import UM 64 except ImportError: 65 relative_path = os.path.join(__file__, "..", "..", "..", "Uranium", "resources", "i18n", "uranium.pot") 66 absolute_path = os.path.abspath(relative_path) 67 if os.path.exists(absolute_path): 68 absolute_path = os.path.abspath(os.path.join(absolute_path, "..")) 69 print("Uranium is at:", absolute_path) 70 return absolute_path 71 else: 72 raise Exception("Can't find Uranium. Please put UM on the PYTHONPATH or put the Uranium folder next to the Cura folder. Looked for: " + absolute_path) 73 return os.path.abspath(os.path.join(UM.__file__, "..", "..", "resources", "i18n")) 74 75 76def merge(source: str, destination: str) -> str: 77 """Merges translations from the source file into the destination file if they 78 79 were missing in the destination file. 80 :param source: The contents of the source .po file. 81 :param destination: The contents of the destination .po file. 82 """ 83 result_lines = [] 84 last_destination = { 85 "msgctxt": "\"\"\n", 86 "msgid": "\"\"\n", 87 "msgstr": "\"\"\n", 88 "msgid_plural": "\"\"\n" 89 } 90 91 current_state = "none" 92 for line in destination.split("\n"): 93 if line.startswith("msgctxt \""): 94 current_state = "msgctxt" 95 line = line[8:] 96 last_destination[current_state] = "" 97 elif line.startswith("msgid \""): 98 current_state = "msgid" 99 line = line[6:] 100 last_destination[current_state] = "" 101 elif line.startswith("msgstr \""): 102 current_state = "msgstr" 103 line = line[7:] 104 last_destination[current_state] = "" 105 elif line.startswith("msgid_plural \""): 106 current_state = "msgid_plural" 107 line = line[13:] 108 last_destination[current_state] = "" 109 110 if line.startswith("\"") and line.endswith("\""): 111 last_destination[current_state] += line + "\n" 112 else: #White lines or comment lines trigger us to search for the translation in the source file. 113 if last_destination["msgstr"] == "\"\"\n" and last_destination["msgid"] != "\"\"\n": #No translation for this yet! 114 last_destination["msgstr"] = find_translation(source, last_destination["msgctxt"], last_destination["msgid"]) #Actually place the translation in. 115 if last_destination["msgctxt"] != "\"\"\n" or last_destination["msgid"] != "\"\"\n" or last_destination["msgid_plural"] != "\"\"\n" or last_destination["msgstr"] != "\"\"\n": 116 if last_destination["msgctxt"] != "\"\"\n": 117 result_lines.append("msgctxt {msgctxt}".format(msgctxt = last_destination["msgctxt"][:-1])) #The [:-1] to strip the last newline. 118 result_lines.append("msgid {msgid}".format(msgid = last_destination["msgid"][:-1])) 119 if last_destination["msgid_plural"] != "\"\"\n": 120 result_lines.append("msgid_plural {msgid_plural}".format(msgid_plural = last_destination["msgid_plural"][:-1])) 121 else: 122 result_lines.append("msgstr {msgstr}".format(msgstr = last_destination["msgstr"][:-1])) 123 last_destination = { 124 "msgctxt": "\"\"\n", 125 "msgid": "\"\"\n", 126 "msgstr": "\"\"\n", 127 "msgid_plural": "\"\"\n" 128 } 129 130 result_lines.append(line) #This line itself. 131 return "\n".join(result_lines) 132 133 134def find_translation(source: str, msgctxt: str, msgid: str) -> str: 135 """Finds a translation in the source file. 136 137 :param source: The contents of the source .po file. 138 :param msgctxt: The ctxt of the translation to find. 139 :param msgid: The id of the translation to find. 140 """ 141 last_source = { 142 "msgctxt": "\"\"\n", 143 "msgid": "\"\"\n", 144 "msgstr": "\"\"\n", 145 "msgid_plural": "\"\"\n" 146 } 147 148 current_state = "none" 149 for line in source.split("\n"): 150 if line.startswith("msgctxt \""): 151 current_state = "msgctxt" 152 line = line[8:] 153 last_source[current_state] = "" 154 elif line.startswith("msgid \""): 155 current_state = "msgid" 156 line = line[6:] 157 last_source[current_state] = "" 158 elif line.startswith("msgstr \""): 159 current_state = "msgstr" 160 line = line[7:] 161 last_source[current_state] = "" 162 elif line.startswith("msgid_plural \""): 163 current_state = "msgid_plural" 164 line = line[13:] 165 last_source[current_state] = "" 166 167 if line.startswith("\"") and line.endswith("\""): 168 last_source[current_state] += line + "\n" 169 else: #White lines trigger us to process this translation. Is it the correct one? 170 #Process the source and destination keys for comparison independent of newline technique. 171 source_ctxt = "".join((line.strip()[1:-1] for line in last_source["msgctxt"].split("\n"))) 172 source_id = "".join((line.strip()[1:-1] for line in last_source["msgid"].split("\n"))) 173 dest_ctxt = "".join((line.strip()[1:-1] for line in msgctxt.split("\n"))) 174 dest_id = "".join((line.strip()[1:-1] for line in msgid.split("\n"))) 175 176 if source_ctxt == dest_ctxt and source_id == dest_id: 177 if last_source["msgstr"] == "\"\"\n" and last_source["msgid_plural"] == "\"\"\n": 178 print("!!! Empty translation for {" + dest_ctxt + "}", dest_id, "!!!") 179 return last_source["msgstr"] 180 181 last_source = { 182 "msgctxt": "\"\"\n", 183 "msgid": "\"\"\n", 184 "msgstr": "\"\"\n", 185 "msgid_plural": "\"\"\n" 186 } 187 188 #Still here? Then the entire msgctxt+msgid combination was not found at all. 189 print("!!! Missing translation for {" + msgctxt.strip() + "}", msgid.strip(), "!!!") 190 return "\"\"\n" 191 192if __name__ == "__main__": 193 argparser = argparse.ArgumentParser(description = "Import translation files from Lionbridge.") 194 argparser.add_argument("source") 195 args = argparser.parse_args() 196 lionbridge_import(args.source) 197