#!/usr/bin/env python3 # This script generates a temporary copy of all translations including suggestions. # We have created a local git clone before running this script. # It takes the default branch to work on. # # Usage: # # /var/lib/weblate/scripts/save-suggestions.py repopath import argparse import logging import logging.config import os import pathlib import subprocess import sys import tempfile sys.path.insert(0, "/usr/local/share/weblate") os.environ["DJANGO_SETTINGS_MODULE"] = "weblate.settings" os.environ["DJANGO_IS_MANAGEMENT_COMMAND"] = "1" from django.core.wsgi import get_wsgi_application application = get_wsgi_application() from weblate.trans import models NAME = "saveSuggestions" logging.config.fileConfig('/var/lib/weblate/config/{NAME}.conf'.format(NAME=NAME)) logger = logging.getLogger(NAME) def log_subprocess_output(cmd, **kwargs): """Add subprocess output to logger.""" logger.info("Running command '%s':", " ".join(cmd)) popen = subprocess.Popen(cmd, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs) for stdout_line in iter(popen.stdout.readline, ""): logger.debug(stdout_line.strip()) popen.stdout.close() returncode = popen.wait() if returncode: raise subprocess.CalledProcessError(returncode, cmd) def update_unit(translation, unit, target): # copied from weblate/trans/models/translation.py: update_units src = unit.get_source_plurals()[0] add = False pounit, add = translation.store.find_unit(unit.context, src) # Bail out if we have not found anything if pounit is None: logger.warning('message %s disappeared!', unit) return pounit.set_target(target) # update po file with first suggestion pounit.mark_fuzzy(False) # mark change as non fuzzy def valid_suggestion(s): problems = [] if s.target.startswith('\n') != s.unit.source.startswith('\n'): problems.append('original message and suggestion do not both begin with \\n') if s.target.endswith('\n') != s.unit.source.endswith('\n'): problems.append('original message and suggestion do not both end with \\n') if problems: logger.error("Invalid suggestion for: %s", s.unit.full_slug) logger.error(" URL: https://translate.tails.boum.org%s", s.unit.get_absolute_url()) logger.error(" Problem(s):") for problem in problems: logger.error(" - " + problem) return False return True def commandline(): parser = argparse.ArgumentParser() parser.add_argument( "-v", "--verbose", action='store_true', help="verbose logging.") parser.add_argument( "repopath", type=pathlib.Path, help="path to the repository.") args = parser.parse_args() prog = parser.prog if args.verbose: for handler in logging.getLogger().handlers: if isinstance(handler, logging.StreamHandler): handler.level = logging.DEBUG repopath = args.repopath logger.info("-- Start to run %s for %s.", NAME, repopath) try: log_subprocess_output(["git", "clean", "-fd"], cwd=str(repopath)) log_subprocess_output(["git", "fetch"], cwd=str(repopath)) log_subprocess_output(["git", "reset", 'FETCH_HEAD', "--hard"], cwd=str(repopath)) except: logger.exception("-- Something unexpected happened. Giving up. --") raise logger.info("Start search for suggestions.") subprojects = models.Component.objects.all() for i, subproject in enumerate(subprojects): try: for translation in subproject.translation_set.all(): changed = False for unit in translation.unit_set.all(): if unit.suggestions: # Get newest most voted suggestion valid = filter(valid_suggestion, unit.suggestions) date_sorted = sorted(valid, key=lambda i: i.timestamp, reverse=True) s = sorted(date_sorted, key=lambda i: i.get_num_votes(), reverse=True) if not s: continue s = s[0] logger.debug("Found suggestion for: %s", unit.full_slug) logger.debug(" URL: https://translate.tails.boum.org%s", unit.get_absolute_url()) update_unit(translation, unit, s.target) changed = True elif unit.pending: # Save uncommitted changes logger.debug("Uncommitted changes found for: %s", unit.full_slug) logger.debug(" URL: https://translate.tails.boum.org%s", unit.get_absolute_url()) update_unit(translation, unit, unit.target) changed = True # save with suggestions if changed: filepath = repopath/translation.filename with tempfile.NamedTemporaryFile() as f: translation.store.store.serialize(f) f.flush() try: subprocess.check_output(['diff', "-q", str(filepath), f.name], stderr=subprocess.STDOUT) logger.debug("don't update {}, because no diff was found".format(translation.filename)) # The file is still the same, so no need to touch the original file except subprocess.CalledProcessError: stat = filepath.stat() os.fchmod(f.fileno(), stat.st_mode) os.fchown(f.fileno(), stat.st_uid, stat.st_gid) f.delete = False # There is a bug in stdlib, that the delete flag is not forward to the real object f._closer.delete = False f.close() os.rename(f.name, str(filepath)) except: logger.exception("-- Got an exception for %s(%i) --", subproject.name, i) raise logger.info("-- Successfully run %s for %s.", NAME, repopath) if __name__ == "__main__": commandline()