Welcome to mirror list, hosted at ThFree Co, Russian Federation.

backportTranslations.py « scripts - github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 6f66e61f726402142771a28b9cee4a46398f2c01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python3
#
# Copyright 2021-2022 The Mumble Developers. All rights reserved.
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file at the root of the
# Mumble source tree or at <https://www.mumble.info/LICENSE>.

# This script is meant to be used for backporting translations from a target branch to a different branch
# Only translations that already exist in the base branch will be updated to match the corresponding
# translations in the target branch


import xml.etree.ElementTree as xml
import argparse
import os
import subprocess
import platform
from io import BytesIO
import re

htmlEscapeCodeEscape = "__(-ThisOnceWasAnAmpersand-)__"


def cmd(args):
    shell = platform.system() == 'Windows'
    p = subprocess.Popen(args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = p.communicate()
    if p.returncode != 0:
        raise Exception('cmd(): {0} failed with status {1}: {2}'.format(args, p.returncode, stderr))
    return stdout.decode('utf-8')


def assertNoPendingChanges(filePath):
    if len(cmd(["git", "status", "--porcelain", "--", filePath])) > 0:
        raise RuntimeError("File \"%s\" has pending changes" % filePath)


def findTranslationFor(source, context, comment, translationRoot):
    for currentCtx in translationRoot.findall("context"):
        if currentCtx.find("name").text == context:
            for currentMsg in currentCtx.findall("message"):
                if currentMsg.find("source").text == source:
                    currentComment = ""
                    if not currentMsg.find("comment") is None:
                        currentComment = currentMsg.find("comment").text

                    if comment == currentComment:
                        return currentMsg.find("translation").text
            
            # Translation not found
            return ""

    return ""


def parseFile(filePath):
    with open(filePath, "r") as openFile:
        contents = openFile.read()

        # Replace the ampersand in all HTML codes in order to prevent automatic un-escaping
        pattern = re.compile(r"&((?:#(?:\d|\w)+|\w+);)")
        contents = re.sub(pattern, r"%s\1" % htmlEscapeCodeEscape, contents)

        return xml.fromstring(contents)


def main():
    parser = argparse.ArgumentParser("Backports translation changes from the given branch to the current one")
    parser.add_argument("FROM_BRANCH", help="Branch to fetch the translations from")
    parser.add_argument("--translation-dir", help="The directory in which the translation files are located", metavar="PATH", default="")

    args = parser.parse_args()

    # Get the name of the branch we're currently on in order to print a helpful message
    currentBranchName = cmd(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip()
    print("Backporting translations from \"%s\" to \"%s\"" % (args.FROM_BRANCH, currentBranchName))


    if not args.translation_dir:
        # Assume translation files are located at ../src/mumble relative to this script's location
        scriptDir = os.path.dirname(os.path.realpath(__file__))
        args.translation_dir = os.path.join(scriptDir, "..", "src", "mumble")


    # Get all translation files in the source directory
    tsFiles = [os.path.join(args.translation_dir, current) for current in os.listdir(args.translation_dir) if current.endswith(".ts")]

    for currentTranslationFile in tsFiles:
        # First make sure there are no pending changes for these files
        assertNoPendingChanges(currentTranslationFile)

        # Parse the current state of the file
        currentTranslationRoot = parseFile(currentTranslationFile)

        if not "language" in currentTranslationRoot.attrib:
            # This is the source translation file that only contains source strings
            continue

        language = currentTranslationRoot.attrib["language"]
        print("Processing \"{}\"...".format(language))


        try:
            # Checkout the file in its state in the target branch and parse the state from there
            cmd(["git", "checkout", args.FROM_BRANCH, "--", currentTranslationFile])
            updatedTranslationRoot = parseFile(currentTranslationFile) 
        finally:
            pass
            # Always restore to the current branch's state afterwards
            cmd(["git", "reset", "--", currentTranslationFile])
            cmd(["git", "restore", "--", currentTranslationFile])

        # Go through all translations in the current state, locate the translation for any particular source string
        # in the state from the target branch and of the translations differ, use the one from the target branch
        changedTranslations = 0
        for currentCtx in currentTranslationRoot.findall("context"):
            context = currentCtx.find("name").text

            for currentMsg in currentCtx.findall("message"):
                source = currentMsg.find("source").text
                translationElement = currentMsg.find("translation")
                translation = translationElement.text

                comment = ""
                if not currentMsg.find("comment") is None:
                    comment = currentMsg.find("comment").text

                newTranslation = findTranslationFor(source, context, comment, updatedTranslationRoot)

                if not newTranslation == "" and translation != newTranslation:
                    # Overtake new translation
                    translationElement.text = newTranslation
                    translationElement.attrib = {}

                    changedTranslations += 1


        if changedTranslations > 0:
            # The translations have changed and thus we need to update our TS file
            contents = xml.tostring(currentTranslationRoot, method="xml", encoding="utf-8", xml_declaration=True, short_empty_elements=False)

            contents = contents.decode("UTF-8")
            # Use double quotes for the XML header as Qt Linguist does (but ElementTree does not)
            # Also add the "<!DOCTYPE TS>" comment after that (again just as Qt Linguist does)
            contents = contents.replace("<?xml version='1.0' encoding='utf-8'?>", "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>")

            # We have previously replaced the ampersand in all HTML escape codes in order to prevent Python from unescaping these for us. This is
            # because ElementTree unescapes them all but only re-escapes the ones that are strictly necessary. Thus that would create a huge diff
            # where nothing actually changed.
            # By having replaced the escape code's ampersands before we can now undo that and be sure to only replace at the appropriate
            # places.
            contents = contents.replace(htmlEscapeCodeEscape, "&")

            # Make sure the translation files end with a newline (as Qt Linguist does)
            if not contents.endswith("\n"):
                contents += "\n"

            # Actually write the file's new content to disk
            with open(currentTranslationFile, "w") as targetFile:
                targetFile.write(contents)


            print("  -> {} translation(s) changed".format(changedTranslations))


if __name__ == "__main__":
    main()