Add format base class, docstrings, and related changes

Julien LepillerSat Nov 14 02:36:49+0100 2020

72274ce

Add format base class, docstrings, and related changes

offlate/formats/androidstrings.py

1-
#   Copyright (c) 2019 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2019, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

2020
from pathlib import Path
2121
2222
from .entry import AndroidStringsEntry
23+
from .format import Format
2324
24-
class AndroidStringsFormat:
25+
class AndroidStringsFormat(Format):
2526
    def __init__(self, conf):
2627
        self.conf = conf
2728
        self.translationfilename = conf["file"]

5960
                entry.dst = otranslated
6061
                continue
6162
            # otherwise, ntranslated and otranslated have a different value
62-
            entry.dst = callback(envalue, otranslated, ntranslated)
63+
            entry.dst = callback.mergeConflict(envalue, otranslated, ntranslated)
6364
        self.translation.save()
6465
6566
    def getExternalFiles(self):

offlate/formats/appstore.py

1-
#   Copyright (c) 2019 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2019, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

1919
from pathlib import Path
2020
2121
from .entry import AppstoreEntry
22+
from .format import Format
2223
2324
def findRecursive(directory):
2425
    ans = []

3031
            ans.extend(findRecursive(f))
3132
    return ans
3233
33-
class AppstoreFormat:
34+
class AppstoreFormat(Format):
3435
    def __init__(self, conf):
3536
        self.conf = conf
3637
        self.translationpath = conf["file"]

6465
                if ncontent == "":
6566
                    r.tr = oldr.tr
6667
                    continue
67-
                r.tr = callback(r.en, ocontent, ncontent)
68+
                r.tr = callback.mergeConflict(r.en, ocontent, ncontent)
6869
6970
    def getExternalFiles(self):
7071
        return findRecursive(self.enpath).extend(findRecursive(self.translationpath))

offlate/formats/callback.py unknown status 1

1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class FormatCallback:
18+
    """
19+
    The base class for a proper FormatCallback class.
20+
    """
21+
    def mergeConflict(self, base, oldTranslation, newTranslation):
22+
        """
23+
        Resolve a merge conflict.  This method is called when merging two
24+
        Format instances.  When a base string is translated in two different
25+
        (non-empty) ways, a conflict arrises.
26+
27+
        :returns: The translation that will be used for this entry.
28+
        :rtype: str
29+
        """
30+
        raise Exception("Unimplemented method in concrete class: mergeConflict")

offlate/formats/entry.py

1-
#   Copyright (c) 2018, 2019 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2018, 2019, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

1515
####
1616
1717
class Entry:
18+
    """
19+
    Set of very related pairs of base language strings and their translations.
20+
    Most of the time, and entry contains only one base language string and one
21+
    translation.  Some formats handle plurals, and each variant is saved as an
22+
    additional string in the same entry.
23+
    """
1824
    def __init__(self, msgids, msgstrs, fuzzy, obsolete):
25+
        """
26+
        Create an entry.
27+
28+
        :param strs msgids: the set of base language strings
29+
        :param strs msgstrs: the set of target language strings (translated)
30+
        :param bool fuzzy: whether the entry is fuzzy
31+
        :param bool obsolete: whether the entry is obsolete
32+
        """
1933
        self.msgids = msgids
2034
        self.msgstrs = msgstrs
2135
        self.fuzzy = fuzzy
2236
        self.obsolete = obsolete
2337
2438
    def isTranslated(self):
39+
        """
40+
        :returns: Whether the entry is fully translated
41+
        :rtype: bool
42+
        """
2543
        for msgstr in self.msgstrs:
2644
            if msgstr == '':
2745
                return False
2846
        return True
2947
3048
    def isFuzzy(self):
49+
        """
50+
        :returns: Whether the entry is fuzzy
51+
        :rtype: bool
52+
        """
3153
        return self.fuzzy
3254
3355
    def isObsolete(self):
56+
        """
57+
        :returns: Whether the entry is obsolete
58+
        :rtype: bool
59+
        """
3460
        return self.obsolete
3561
3662
    def update(self, index, content):
63+
        """
64+
        Update the entry by replacing the translation number index with content.
65+
66+
        :rtype: None
67+
        """
3768
        self.msgstrs[index] = content
3869
3970
    def get(self, index):
71+
        """
72+
        :returns: The indexth translation in this entry.
73+
        :rtype: str
74+
        """
4075
        if isinstance(self.msgstrs, list):
4176
            return self.msgstrs[index]
4277
        else:
4378
            return list(self.msgstrs.items())[index][1]
4479
4580
    def isPlural(self):
81+
        """
82+
        :retuns: Whether the entry is an entry for a plural form.
83+
        :rtype: bool
84+
        """
4685
        return len(self.msgstrs) > 1
4786
4887
class POEntry(Entry):
88+
    """
89+
    An entry for the gettext format.
90+
    """
4991
    def __init__(self, entry):
5092
        msgids = [entry.msgid]
5193
        msgstrs = [entry.msgstr]

67109
            self.entry.msgstr = content
68110
69111
class AndroidStringsEntry(Entry):
112+
    """
113+
    An entry for the android translation format.
114+
    """
70115
    def __init__(self, entry, parent=None, index=None):
71116
        if entry.type == 'string':
72117
            msgids = [entry.orig]

93138
        return isinstance(self.msgstrs, dict)
94139
95140
class AppstoreEntry(Entry):
141+
    """
142+
    An entry for the android app description format.
143+
    """
96144
    def __init__(self, filename, en, tr):
97145
        Entry.__init__(self, [en], [tr], False, False)
98146
        self.en = en

105153
106154
107155
class JSONEntry(Entry):
156+
    """
157+
    An entry for the json format.
158+
    """
108159
    def __init__(self, entry):
109160
        Entry.__init__(self, [entry['source_string']], [entry['translation']], False, False)
110161
        self.entry = entry

114165
        self.entry['translation'] = content
115166
116167
class YAMLEntry(Entry):
168+
    """
169+
    An entry for the yaml format.
170+
    """
117171
    def __init__(self, entry):
118172
        self.entry = entry
119173
        Entry.__init__(self, [entry['source_string']],

offlate/formats/exception.py unknown status 1

1+
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class UnsupportedFormatException(Exception):
18+
    def __init__(self, message):
19+
        super().__init__('Unsupported format: '+message)
20+
        self.unsupportedFormat = message

offlate/formats/format.py unknown status 1

1+
#   Copyright (c) 2020 Julien Lepiller <julien@lepiller.eu>
2+
#
3+
#   This program is free software: you can redistribute it and/or modify
4+
#   it under the terms of the GNU Affero General Public License as
5+
#   published by the Free Software Foundation, either version 3 of the
6+
#   License, or (at your option) any later version.
7+
#
8+
#   This program is distributed in the hope that it will be useful,
9+
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11+
#   GNU Affero General Public License for more details.
12+
#
13+
#   You should have received a copy of the GNU Affero General Public License
14+
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15+
####
16+
17+
class Format:
18+
    """
19+
    The base class for translation formats.
20+
21+
    A format is a class that is linked to a project. Each instance of this
22+
    class represents the files that contain the source and target strings
23+
    in a specific format (gettext, yaml, json, ...).
24+
    """
25+
26+
    def __init__(self, conf):
27+
        self.conf = conf
28+
29+
    def content(self):
30+
        """
31+
        Return the content of this set of source and target strings.
32+
33+
        You may use these as read-write object.  Any change to the content
34+
        can later be saved using the save method.
35+
36+
        :returns: List of entries
37+
        :rtype: Entry list
38+
        """
39+
        raise Exception("Unimplemented method in concrete class: content")
40+
41+
    def save(self):
42+
        """
43+
        Saves the content to files.
44+
45+
        This method assumes that the content (Entry objects) is modified
46+
        in-place, and will save their content.
47+
48+
        :rtype: None
49+
        """
50+
        raise Exception("Unimplemented method in concrete class: save")
51+
52+
    def merge(self, older, callback):
53+
        """
54+
        Merge two versions of the same translation into this translation set
55+
        and save the result.
56+
57+
        :param Format older: The previous version of the translation
58+
        :param FormatCallback callback: A format callback to help deal with
59+
            merge conflicts.
60+
        :rtype: None
61+
        """
62+
        raise Exception("Unimplemented method in concrete class: merge")
63+
64+
    def reload(self):
65+
        """
66+
        Reloads the content from the files without saving existing changes.
67+
68+
        :rtype: None
69+
        """
70+
        raise Exception("Unimplemented method in concrete class: reload")
71+
72+
    def getExternalFiles(self):
73+
        """
74+
        :returns: The list of files this instance may read or write
75+
        :rtype: String list
76+
        """
77+
        raise Exception("Unimplemented method in concrete class: getExternalFiles")
78+
79+
    def getTranslationFiles(self):
80+
        """
81+
        A format can be composed of different files.  For instance, a file
82+
        that contains the English (or base) language version of the strings,
83+
        and a file that contains the translated strings.
84+
85+
        :returns: The list of files that contain the actual translation
86+
        :rtype: String list
87+
        """
88+
        raise Exception("Unimplemented method in concrete class: getTranslationFiles")

offlate/formats/formatException.py unknown status 2

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
2-
#
3-
#   This program is free software: you can redistribute it and/or modify
4-
#   it under the terms of the GNU Affero General Public License as
5-
#   published by the Free Software Foundation, either version 3 of the
6-
#   License, or (at your option) any later version.
7-
#
8-
#   This program is distributed in the hope that it will be useful,
9-
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11-
#   GNU Affero General Public License for more details.
12-
#
13-
#   You should have received a copy of the GNU Affero General Public License
14-
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15-
####
16-
17-
class UnsupportedFormatException(Exception):
18-
    def __init__(self, message):
19-
        super().__init__('Unsupported format: '+message)
20-
        self.unsupportedFormat = message

offlate/formats/gettext.py

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2018, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

2020
import os.path
2121
from dateutil.tz import tzlocal
2222
from .entry import POEntry
23+
from .format import Format
2324
24-
class GettextFormat:
25+
class GettextFormat(Format):
2526
    def __init__(self, conf):
2627
        self.pofilename = conf["file"]
2728
        self.pot = polib.pofile(conf["pot"])

5455
                        nentry.msgstr = oentry.msgstr
5556
                        break
5657
                    # otherwise, nentry and oentry have a different msgstr
57-
                    nentry.msgstr = callback(nentry.msgid, oentry.msgstr, nentry.msgstr)
58+
                    nentry.msgstr = callback.mergeConflict(nentry.msgid,
59+
                            oentry.msgstr, nentry.msgstr)
5860
                    break
5961
        self.po.save()
6062

offlate/formats/list.py unknown status 2

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
2-
#
3-
#   This program is free software: you can redistribute it and/or modify
4-
#   it under the terms of the GNU Affero General Public License as
5-
#   published by the Free Software Foundation, either version 3 of the
6-
#   License, or (at your option) any later version.
7-
#
8-
#   This program is distributed in the hope that it will be useful,
9-
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10-
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11-
#   GNU Affero General Public License for more details.
12-
#
13-
#   You should have received a copy of the GNU Affero General Public License
14-
#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
15-
####
16-
""" The list of available formats. """
17-
18-
GETTEXT = 0
19-
YAML = 1
20-
21-
format_list = [
22-
        "Gettext (.po)",
23-
        "YAML (.yml)",
24-
        "Qt (.ts)",
25-
        "Android (strings.xml)"
26-
]

offlate/formats/ts.py

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2018, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

2020
import xml.etree.ElementTree as ET
2121
import time
2222
from .entry import TSEntry
23+
from .format import Format
2324
2425
from PyQt5.QtCore import *
2526

194195
            return nplural
195196
    return 0
196197
197-
class TSFormat:
198+
class TSFormat(Format):
198199
    def __init__(self, conf):
199200
        self.conf = conf
200201
        self.tsfilename = conf["file"]
201202
        if not os.path.isfile(conf["file"]):
202-
            self.createNewTS()
203+
            self._createNewTS()
203204
        self.reload()
204205
205-
    def createNewTS(self):
206+
    def _createNewTS(self):
206207
        template = ET.parse(self.conf["template"])
207208
        content = template.getroot()
208209
        root = ET.Element('TS')

300301
                            elif oentry.msgstrs[i] == '':
301302
                                break
302303
                            else:
303-
                                entry.update(i, callback(entry.msgids[0],
304+
                                entry.update(i, callback.mergeConflict(entry.msgids[0],
304305
                                    oentry.msgstrs[i], entry.msgstrs[i]))
305306
                    break
306307
        self.save()

offlate/formats/yaml.py

1-
#   Copyright (c) 2018 Julien Lepiller <julien@lepiller.eu>
1+
#   Copyright (c) 2018, 2020 Julien Lepiller <julien@lepiller.eu>
22
#
33
#   This program is free software: you can redistribute it and/or modify
44
#   it under the terms of the GNU Affero General Public License as

1717
1818
from ruamel import yaml
1919
from .entry import YAMLEntry
20+
from .format import Format
2021
2122
def yaml_rec_load(path, source, dest):
2223
    ans = []

5152
            elif n == '':
5253
                ans[i] = o
5354
            else:
54-
                ans[i] = callback(s, o, n)
55+
                ans[i] = callback.mergeConflict(s, o, n)
5556
        else:
5657
            ans[i] = yaml_rec_update(callback, s, o, n)
5758
    return ans
5859
59-
class YamlFormat:
60+
class YamlFormat(Format):
6061
    def __init__(self, conf):
6162
        self.conf = conf
6263
        self.source = conf['source']

offlate/manager.py

1919
from .systems.transifex import TransifexProject
2020
from .systems.gitlab import GitlabProject
2121
from .systems.github import GithubProject
22-
from .formats.formatException import UnsupportedFormatException
22+
from .formats.exception import UnsupportedFormatException
2323
from .systems.list import *
2424
2525
import json

offlate/systems/git.py

2525
from ..formats.yaml import YamlFormat
2626
from ..formats.androidstrings import AndroidStringsFormat
2727
from ..formats.appstore import AppstoreFormat
28-
from ..formats.formatException import UnsupportedFormatException
28+
from ..formats.exception import UnsupportedFormatException
2929
from .systemException import ProjectNotFoundSystemException
3030
3131
def rmdir(dir):

offlate/systems/transifex.py

2020
import requests
2121
from requests.auth import HTTPBasicAuth
2222
from ..formats.yaml import YamlFormat
23-
from ..formats.formatException import UnsupportedFormatException
23+
from ..formats.exception import UnsupportedFormatException
2424
from .systemException import ProjectNotFoundSystemException
2525
2626
class TransifexProject:

offlate/ui/editor.py

2121
from .spellcheckedit import SpellCheckEdit
2222
from .tagclickedit import TagClickEdit
2323
from .parallel import RunnableCallback, RunnableSignals
24+
from ..formats.callback import FormatCallback
2425
2526
import math
2627
import platform

3536
    def __init__(self, parent = None):
3637
        super(ProjectTab, self).__init__(parent)
3738
39+
class MergeCallback(FormatCallback):
40+
    def __init__(self):
41+
        pass
42+
43+
    def mergeConflict(self, base, oldTranslation, newTranslation):
44+
        # TODO: Actually do something more intelligent
45+
        return newTranslation
46+
3847
class Interface:
3948
    def __init__(self):
4049
        self.value = None

366375
        self.project.save()
367376
        self.project.send(Interface())
368377
369-
    def askmerge(self, msgid, oldstr, newstr):
370-
        # TODO: Actually do something more intelligent
371-
        return newstr
372-
373378
    def update(self, callback=None):
374379
        self.project.save()
375-
        self.project.update(self.askmerge, callback)
380+
        self.project.update(MergeCallback(), callback)
376381
        self.content = self.project.content()
377382
        self.updateContent()
378383

offlate/ui/manager.py

3030
3131
from ..manager import ProjectManager
3232
33-
from ..formats.formatException import UnsupportedFormatException
33+
from ..formats.exception import UnsupportedFormatException
3434
from ..systems.systemException import ProjectNotFoundSystemException
3535
3636
class ProjectManagerWindow(QMainWindow):

offlate/ui/new.py

2424
from PyQt5.QtCore import *
2525
2626
from ..systems.list import *
27-
from ..formats.list import *
2827
from .multiplelineedit import MultipleLineEdit
2928
3029
class NewWindow(QDialog):

offlate/ui/parallel.py

1515
####
1616
1717
from PyQt5.QtCore import *
18-
from ..formats.formatException import UnsupportedFormatException
18+
from ..formats.exception import UnsupportedFormatException
1919
from ..systems.systemException import ProjectNotFoundSystemException
2020
2121
class RunnableCallback: