From 20778f3d10f16888c836e644b52a299cd7467cef Mon Sep 17 00:00:00 2001 From: Joey de l'Arago Date: Mon, 16 Jan 2023 15:46:25 +0100 Subject: [PATCH] Move translation scripts to Cura from Uranium CURA-9814 --- scripts/translations/createjsoncontext.py | 103 +++++++++++++++++++ scripts/translations/createkeypair.py | 47 +++++++++ scripts/translations/createplugincontext.py | 72 +++++++++++++ scripts/translations/extract-all | 13 +++ scripts/translations/extract-json | 42 ++++++++ scripts/translations/extract-messages | 42 ++++++++ scripts/translations/extract-plugins | 14 +++ scripts/translations/extract-python | 12 +++ scripts/translations/extract-tr-strings | 73 +++++++++++++ scripts/translations/pirate.py | 108 ++++++++++++++++++++ scripts/translations/pirateofdoom.py | 77 ++++++++++++++ 11 files changed, 603 insertions(+) create mode 100644 scripts/translations/createjsoncontext.py create mode 100644 scripts/translations/createkeypair.py create mode 100644 scripts/translations/createplugincontext.py create mode 100755 scripts/translations/extract-all create mode 100755 scripts/translations/extract-json create mode 100755 scripts/translations/extract-messages create mode 100755 scripts/translations/extract-plugins create mode 100755 scripts/translations/extract-python create mode 100755 scripts/translations/extract-tr-strings create mode 100644 scripts/translations/pirate.py create mode 100644 scripts/translations/pirateofdoom.py diff --git a/scripts/translations/createjsoncontext.py b/scripts/translations/createjsoncontext.py new file mode 100644 index 0000000000..a9f6019005 --- /dev/null +++ b/scripts/translations/createjsoncontext.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Copyright 2014 Burkhard Lück + +Permission to use, copy, modify, and distribute this software +and its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of the author not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +The author disclaim all warranties with regard to this +software, including all implied warranties of merchantability +and fitness. In no event shall the author be liable for any +special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether +in an action of contract, negligence or other tortious action, +arising out of or in connection with the use or performance of +this software. +""" + +# This script generates a POT file from a JSON settings file. It +# has been adapted from createjsoncontext.py of KDE's translation +# scripts. It extracts the "label" and "description" values of +# the JSON file using the structure as used by Uranium settings files. + +import sys +import os +import json +import time +import os.path +import collections + +debugoutput = False #set True to print debug output in scripty's logs + +basedir = sys.argv[-1] +pottxt = "" + +def appendMessage(file, setting, field, value): + global pottxt + pottxt += "#: {0}\nmsgctxt \"{1} {2}\"\nmsgid \"{3}\"\nmsgstr \"\"\n\n".format(file, setting, field, value.replace("\n", "\\n").replace("\"", "\\\"")) + +def processSettings(file, settings): + for name, value in settings.items(): + appendMessage(file, name, "label", value["label"]) + if "description" in value: + appendMessage(file, name, "description", value["description"]) + + if "warning_description" in value: + appendMessage(file, name, "warning_description", value["warning_description"]) + + if "error_description" in value: + appendMessage(file, name, "error_description", value["error_description"]) + + if "options" in value: + for item, description in value["options"].items(): + appendMessage(file, name, "option {0}".format(item), description) + + if "children" in value: + processSettings(file, value["children"]) + +def potheader(): + headertxt = "#, fuzzy\n" + headertxt += "msgid \"\"\n" + headertxt += "msgstr \"\"\n" + headertxt += "\"Project-Id-Version: Uranium json setting files\\n\"\n" + headertxt += "\"Report-Msgid-Bugs-To: plugins@ultimaker.com\\n\"\n" + headertxt += "\"POT-Creation-Date: %s+0000\\n\"\n" %time.strftime("%Y-%m-%d %H:%M") + headertxt += "\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n" + headertxt += "\"Last-Translator: FULL NAME \\n\"\n" + headertxt += "\"Language-Team: LANGUAGE\\n\"\n" + headertxt += "\"MIME-Version: 1.0\\n\"\n" + headertxt += "\"Content-Type: text/plain; charset=UTF-8\\n\"\n" + headertxt += "\"Content-Transfer-Encoding: 8bit\\n\"\n" + headertxt += "\n" + return headertxt + +if len(sys.argv) < 3: + print("wrong number of args: %s" % sys.argv) + print("\nUsage: python %s jsonfilenamelist basedir" % os.path.basename(sys.argv[0])) +else: + jsonfilename = sys.argv[1] + basedir = sys.argv[2] + outputfilename = sys.argv[3] + + with open(jsonfilename, "r", encoding = "utf-8") as data_file: + error = False + + jsondatadict = json.load(data_file, object_pairs_hook=collections.OrderedDict) + if "settings" not in jsondatadict: + print("No settings item found, nothing to translate") + exit(1) + + processSettings(jsonfilename.replace(basedir, ""), jsondatadict["settings"]) + + if pottxt != "": + with open(outputfilename, "w", encoding = "utf-8") as output_file: + output_file.write(potheader()) + output_file.write(pottxt) diff --git a/scripts/translations/createkeypair.py b/scripts/translations/createkeypair.py new file mode 100644 index 0000000000..e01c9c2a0b --- /dev/null +++ b/scripts/translations/createkeypair.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import argparse +from typing import Optional +import sys + +from UM.Trust import TrustBasics + +# Default arguments, if arguments to the script are omitted, these values are used: +DEFAULT_PRIVATE_KEY_PATH = "./private_key.pem" +DEFAULT_PUBLIC_KEY_PATH = "./public_key.pem" +DEFAULT_PASSWORD = "" + + +def createAndStoreNewKeyPair(private_filename: str, public_filename: str, optional_password: Optional[str]) -> None: + """Creates a new public and private key, and saves them to the provided filenames. + + See also 'Trust.py' in the main library and the related scripts; 'signfile.py', 'signfolder.py' in this folder. + + :param private_filename: Filename to save the private key to. + :param public_filename: Filename to save the public key to. + :param optional_password: Private keys can have a password (or not). + """ + + password = None if optional_password == "" else optional_password + private_key, public_key = TrustBasics.generateNewKeyPair() + TrustBasics.saveKeyPair(private_key, private_filename, public_filename, password) + + +def mainfunc(): + """Arguments: + + `-k ` or `--private ` will store the generated private key to + `-p ` or `--public ` will store the generated public key to + `-w ` or `--password ` will give the private key a password (none if omitted, which is default) + """ + + parser = argparse.ArgumentParser() + parser.add_argument("-k", "--private", type = str, default = DEFAULT_PRIVATE_KEY_PATH) + parser.add_argument("-p", "--public", type = str, default = DEFAULT_PUBLIC_KEY_PATH) + parser.add_argument("-w", "--password", type = str, default = DEFAULT_PASSWORD) + args = parser.parse_args() + createAndStoreNewKeyPair(args.private, args.public, args.password) + + +if __name__ == "__main__": + sys.exit(mainfunc()) diff --git a/scripts/translations/createplugincontext.py b/scripts/translations/createplugincontext.py new file mode 100644 index 0000000000..25a086357e --- /dev/null +++ b/scripts/translations/createplugincontext.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Copyright 2014 Burkhard Lück + +Permission to use, copy, modify, and distribute this software +and its documentation for any purpose and without fee is hereby +granted, provided that the above copyright notice appear in all +copies and that both that the copyright notice and this +permission notice and warranty disclaimer appear in supporting +documentation, and that the name of the author not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +The author disclaim all warranties with regard to this +software, including all implied warranties of merchantability +and fitness. In no event shall the author be liable for any +special, indirect or consequential damages or any damages +whatsoever resulting from loss of use, data or profits, whether +in an action of contract, negligence or other tortious action, +arising out of or in connection with the use or performance of +this software. +""" + +# This script generates a POT file from a JSON settings file. It +# has been adapted from createjsoncontext.py of KDE's translation +# scripts. It extracts the "label" and "description" values of +# the JSON file using the structure as used by Uranium settings files. + +import sys +import os.path +import collections +import json + +debugoutput = False #set True to print debug output in scripty's logs + +basedir = sys.argv[-1] +pottxt = "" + + +def appendMessage(file, field, value): + global pottxt + pottxt += "#: {0}\nmsgctxt \"{1}\"\nmsgid \"{2}\"\nmsgstr \"\"\n\n".format(file, field, value.replace("\n", "\\n").replace("\"", "\\\"")) + + +if len(sys.argv) < 3: + print("wrong number of args: %s" % sys.argv) + print("\nUsage: python %s jsonfilenamelist basedir" % os.path.basename(sys.argv[0])) +else: + json_filename = sys.argv[1] + basedir = sys.argv[2] + output_filename = sys.argv[3] + + with open(json_filename, "r", encoding = "utf-8") as data_file: + error = False + + jsondatadict = json.load(data_file, object_pairs_hook=collections.OrderedDict) + if "name" not in jsondatadict or ("api" not in jsondatadict and "supported_sdk_versions" not in jsondatadict) or "version" not in jsondatadict: + print("The plugin.json file found on %s is invalid, ignoring it" % json_filename) + exit(1) + + file = json_filename.replace(basedir, "") + + if "description" in jsondatadict: + appendMessage(file, "description", jsondatadict["description"]) + if "name" in jsondatadict: + appendMessage(file, "name", jsondatadict["name"]) + + if pottxt != "": + with open(output_filename, "a", encoding = "utf-8") as output_file: + output_file.write(pottxt) diff --git a/scripts/translations/extract-all b/scripts/translations/extract-all new file mode 100755 index 0000000000..ea0ac63f58 --- /dev/null +++ b/scripts/translations/extract-all @@ -0,0 +1,13 @@ +#!/bin/bash +# +# Use xgettext to extract all strings from a set of python files. +# Argument 1 is the directory to search for python files, argument 2 +# is the destination file. +# +# This script will extract strings marked using i18n or i18nc methods. +# See UM/i18n.py for the relevant methods. +# +dir=$1 +dest=$2 +xgettext --from-code=UTF-8 --language=python -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $(find -L "$dir" -name \*.py) +xgettext --from-code=UTF-8 --join-existing --language=javascript -ki18n:1 -ki18nc:1c,2 -ki18np:1,2 -ki18ncp:1c,2,3 -o $dest $(find -L "$dir" -name \*.qml) diff --git a/scripts/translations/extract-json b/scripts/translations/extract-json new file mode 100755 index 0000000000..01048bb5cb --- /dev/null +++ b/scripts/translations/extract-json @@ -0,0 +1,42 @@ +#! /bin/bash + +# Extract strings from a list of JSON files. +# +# This script will extract strings from all JSON files in the directory +# passed as first argument. The second argument is the destination +# directory for the extracted message file. +# +# This script uses createjsoncontext to generate the actual message file +# from the JSON file. +# +# This script is based on handle_json_files.sh from KDE's translation +# scripts. +# handle_json_files.sh is copyright 2014 Burkhard Lück +scriptdir=$(dirname $0) + +extract() { + basedir=$1 + dest=$2 + file=$3 + + python3 $scriptdir/createjsoncontext.py $file $basedir json.$$.tmp + if test $? -eq 1; then + return + fi + + echo "Extracted messages from $file" + + msguniq --to-code=UTF-8 -o json.$$ json.$$.tmp + if test -f json.$$; then + destfile="$dest/$(basename $file).pot" + mv json.$$ $destfile + fi + rm -f json.$$ json.$$.tmp +} + +dir=$1; shift +dest=$1; shift + +for file in $(find -L "$dir" -name *.json | grep -v 'tests'); do + extract $dir $dest $file +done diff --git a/scripts/translations/extract-messages b/scripts/translations/extract-messages new file mode 100755 index 0000000000..315009bcdf --- /dev/null +++ b/scripts/translations/extract-messages @@ -0,0 +1,42 @@ +#!/bin/bash + +scriptdir=$(dirname $0) +basedir=$1 +catalogname=$2 + +# This script processes the source files using several other scripts to extract strings. +# The strings are extracted to $basedir/resources/i18n/ and then post processed. After that +# It generates english translation files and testing files that are pre- and sufficed with +# xx. These can be used by setting the LANGUAGE environment variable to x-test. +# +# This script uses extract-tr-strings to extract strings from QML files, extract-json to +# extract strings from JSON files and extract-python to extract strings from Python files. +# +mkdir -p $basedir/resources/i18n +$scriptdir/extract-json $basedir/resources/definitions/ $basedir/resources/i18n +$scriptdir/extract-all $basedir $basedir/resources/i18n/$catalogname.pot +$scriptdir/extract-plugins $basedir/plugins/ $basedir/resources/i18n/$catalogname.pot +msgconv --to-code=UTF-8 $basedir/resources/i18n/$catalogname.pot -o $basedir/resources/i18n/$catalogname.pot + +for pot in $basedir/resources/i18n/*.pot; do + filename=$(basename $pot) + + dir=$basedir/resources/i18n/en_US + mkdir -p $dir + po=$dir/${filename/.pot/.po} + msginit --no-translator -l en_US -i $pot -o $po + + dir=$basedir/resources/i18n/x-test + mkdir -p $dir + po=$dir/${filename/.pot/.po} + msginit --no-translator -l en_US -i $pot -o $po + msgfilter --keep-header -i $po -o $po sed -e "s/.*/xx&xx/" + msgfilter -i $po -o $po sed -e "s/Language: en_US/Language: x-test/" + + #Auto-translate the translation files to Pirate. + dir=$basedir/resources/i18n/en_7S + mkdir -p $dir + po=$dir/${filename/.pot/.po} + python3 $scriptdir/pirate.py $pot $po + echo Created $po. +done diff --git a/scripts/translations/extract-plugins b/scripts/translations/extract-plugins new file mode 100755 index 0000000000..d5b3674968 --- /dev/null +++ b/scripts/translations/extract-plugins @@ -0,0 +1,14 @@ +#! /bin/bash + +# Extract strings from all plugins +# + +scriptdir=$(dirname $0) +dir=$1; shift +dest=$1; shift + +for file in $(find -L "$dir" -name plugin.json | grep -v 'tests'); do + python3 $scriptdir/createplugincontext.py $file $dir $dest +done + + diff --git a/scripts/translations/extract-python b/scripts/translations/extract-python new file mode 100755 index 0000000000..34cf332f99 --- /dev/null +++ b/scripts/translations/extract-python @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Use xgettext to extract all strings from a set of python files. +# Argument 1 is the directory to search for python files, argument 2 +# is the destination file. +# +# This script will extract strings marked using i18n or i18nc methods. +# See UM/i18n.py for the relevant methods. +# +dir=$1 +dest=$2 +xgettext --from-code=UTF-8 --language=python -ki18n:1 -ki18nc:1c,2 -o $dest $(find -L "$dir" -name \*.py) diff --git a/scripts/translations/extract-tr-strings b/scripts/translations/extract-tr-strings new file mode 100755 index 0000000000..378f50bb40 --- /dev/null +++ b/scripts/translations/extract-tr-strings @@ -0,0 +1,73 @@ +#!/bin/sh +# +# This script extracts strings from a set of files using Qt's translation system. +# It then converts the extracted .ts file in a .po file that can be used with +# tools that expect Gettext's po file format. +# +# This script was adapted from extract-tr-strings from KDE's translation scripts. +# extract-tr-strings is Copyright 2014 Aurélien Gateau +set -e + +OLD_PWD=$PWD +cd $(dirname $0) +SCRIPTS_DIR=$PWD +cd $OLD_PWD + +LUPDATE=${LUPDATE:-lupdate} +LCONVERT=${LCONVERT:-lconvert} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +usage() { + cat <= 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 new file mode 100644 index 0000000000..e8b8a28958 --- /dev/null +++ b/scripts/translations/pirateofdoom.py @@ -0,0 +1,77 @@ +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", +}