Changeset View
Changeset View
Standalone View
Standalone View
contrib/devtools/update-translations.py
Show All 29 Lines | |||||
LOCALE_DIR = 'src/qt/locale' | LOCALE_DIR = 'src/qt/locale' | ||||
# Minimum number of messages for translation to be considered at all | # Minimum number of messages for translation to be considered at all | ||||
MIN_NUM_MESSAGES = 10 | MIN_NUM_MESSAGES = 10 | ||||
def check_at_repository_root(): | def check_at_repository_root(): | ||||
if not os.path.exists('.git'): | if not os.path.exists('.git'): | ||||
print('No .git directory found') | print('No .git directory found') | ||||
print('Execute this script at the root of the repository', file=sys.stderr) | print( | ||||
'Execute this script at the root of the repository', | |||||
file=sys.stderr) | |||||
sys.exit(1) | sys.exit(1) | ||||
def fetch_all_translations(): | def fetch_all_translations(): | ||||
if subprocess.call([TX, 'pull', '-f', '-a']): | if subprocess.call([TX, 'pull', '-f', '-a']): | ||||
print('Error while fetching translations', file=sys.stderr) | print('Error while fetching translations', file=sys.stderr) | ||||
sys.exit(1) | sys.exit(1) | ||||
Show All 24 Lines | def split_format_specifiers(specifiers): | ||||
# If both numeric format specifiers and "others" are used, assume we're dealing | # If both numeric format specifiers and "others" are used, assume we're dealing | ||||
# with a Qt-formatted message. In the case of Qt formatting (see https://doc.qt.io/qt-5/qstring.html#arg) | # with a Qt-formatted message. In the case of Qt formatting (see https://doc.qt.io/qt-5/qstring.html#arg) | ||||
# only numeric formats are replaced at all. This means "(percentage: %1%)" is valid, without needing | # only numeric formats are replaced at all. This means "(percentage: %1%)" is valid, without needing | ||||
# any kind of escaping that would be necessary for strprintf. Without this, this function | # any kind of escaping that would be necessary for strprintf. Without this, this function | ||||
# would wrongly detect '%)' as a printf format specifier. | # would wrongly detect '%)' as a printf format specifier. | ||||
if numeric: | if numeric: | ||||
other = [] | other = [] | ||||
# numeric (Qt) can be present in any order, others (strprintf) must be in specified order | # numeric (Qt) can be present in any order, others (strprintf) must be in | ||||
# specified order | |||||
return set(numeric), other | return set(numeric), other | ||||
def sanitize_string(s): | def sanitize_string(s): | ||||
'''Sanitize string for printing''' | '''Sanitize string for printing''' | ||||
return s.replace('\n', ' ') | return s.replace('\n', ' ') | ||||
def check_format_specifiers(source, translation, errors, numerus): | def check_format_specifiers(source, translation, errors, numerus): | ||||
source_f = split_format_specifiers(find_format_specifiers(source)) | source_f = split_format_specifiers(find_format_specifiers(source)) | ||||
# assert that no source messages contain both Qt and strprintf format specifiers | # assert that no source messages contain both Qt and strprintf format specifiers | ||||
# if this fails, go change the source as this is hacky and confusing! | # if this fails, go change the source as this is hacky and confusing! | ||||
assert(not(source_f[0] and source_f[1])) | assert(not(source_f[0] and source_f[1])) | ||||
try: | try: | ||||
translation_f = split_format_specifiers( | translation_f = split_format_specifiers( | ||||
find_format_specifiers(translation)) | find_format_specifiers(translation)) | ||||
except IndexError: | except IndexError: | ||||
errors.append("Parse error in translation for '{}': '{}'".format( | errors.append("Parse error in translation for '{}': '{}'".format( | ||||
sanitize_string(source), sanitize_string(translation))) | sanitize_string(source), sanitize_string(translation))) | ||||
return False | return False | ||||
else: | else: | ||||
if source_f != translation_f: | if source_f != translation_f: | ||||
if numerus and source_f == (set(), ['n']) and translation_f == (set(), []) and translation.find('%') == -1: | if numerus and source_f == (set(), ['n']) and translation_f == ( | ||||
# Allow numerus translations to omit %n specifier (usually when it only has one possible value) | set(), []) and translation.find('%') == -1: | ||||
# Allow numerus translations to omit %n specifier (usually when | |||||
# it only has one possible value) | |||||
return True | return True | ||||
errors.append("Mismatch between '{}' and '{}'".format( | errors.append("Mismatch between '{}' and '{}'".format( | ||||
sanitize_string(source), sanitize_string(translation))) | sanitize_string(source), sanitize_string(translation))) | ||||
return False | return False | ||||
return True | return True | ||||
def all_ts_files(suffix=''): | def all_ts_files(suffix=''): | ||||
for filename in os.listdir(LOCALE_DIR): | for filename in os.listdir(LOCALE_DIR): | ||||
# process only language files, and do not process source language | # process only language files, and do not process source language | ||||
if not filename.endswith('.ts' + suffix) or filename == SOURCE_LANG + suffix: | if not filename.endswith( | ||||
'.ts' + suffix) or filename == SOURCE_LANG + suffix: | |||||
continue | continue | ||||
if suffix: # remove provided suffix | if suffix: # remove provided suffix | ||||
filename = filename[0:-len(suffix)] | filename = filename[0:-len(suffix)] | ||||
filepath = os.path.join(LOCALE_DIR, filename) | filepath = os.path.join(LOCALE_DIR, filename) | ||||
yield(filename, filepath) | yield(filename, filepath) | ||||
FIX_RE = re.compile(b'[\x00-\x09\x0b\x0c\x0e-\x1f]') | FIX_RE = re.compile(b'[\x00-\x09\x0b\x0c\x0e-\x1f]') | ||||
Show All 25 Lines | if reduce_diff_hacks: | ||||
ET._escape_cdata = escape_cdata | ET._escape_cdata = escape_cdata | ||||
for (filename, filepath) in all_ts_files(): | for (filename, filepath) in all_ts_files(): | ||||
os.rename(filepath, filepath + '.orig') | os.rename(filepath, filepath + '.orig') | ||||
have_errors = False | have_errors = False | ||||
for (filename, filepath) in all_ts_files('.orig'): | for (filename, filepath) in all_ts_files('.orig'): | ||||
# pre-fixups to cope with transifex output | # pre-fixups to cope with transifex output | ||||
# need to override encoding because 'utf8' is not understood only 'utf-8' | # need to override encoding because 'utf8' is not understood only | ||||
# 'utf-8' | |||||
parser = ET.XMLParser(encoding='utf-8') | parser = ET.XMLParser(encoding='utf-8') | ||||
with open(filepath + '.orig', 'rb') as f: | with open(filepath + '.orig', 'rb') as f: | ||||
data = f.read() | data = f.read() | ||||
# remove control characters; this must be done over the entire file otherwise the XML parser will fail | # remove control characters; this must be done over the entire file | ||||
# otherwise the XML parser will fail | |||||
data = remove_invalid_characters(data) | data = remove_invalid_characters(data) | ||||
tree = ET.parse(io.BytesIO(data), parser=parser) | tree = ET.parse(io.BytesIO(data), parser=parser) | ||||
# iterate over all messages in file | # iterate over all messages in file | ||||
root = tree.getroot() | root = tree.getroot() | ||||
for context in root.findall('context'): | for context in root.findall('context'): | ||||
for message in context.findall('message'): | for message in context.findall('message'): | ||||
numerus = message.get('numerus') == 'yes' | numerus = message.get('numerus') == 'yes' | ||||
Show All 35 Lines | for (filename, filepath) in all_ts_files('.orig'): | ||||
for message in context.findall('message'): | for message in context.findall('message'): | ||||
num_messages += 1 | num_messages += 1 | ||||
if num_messages < MIN_NUM_MESSAGES: | if num_messages < MIN_NUM_MESSAGES: | ||||
print('Removing {}, as it contains only {} messages'.format( | print('Removing {}, as it contains only {} messages'.format( | ||||
filepath, num_messages)) | filepath, num_messages)) | ||||
continue | continue | ||||
# write fixed-up tree | # write fixed-up tree | ||||
# if diff reduction requested, replace some XML to 'sanitize' to qt formatting | # if diff reduction requested, replace some XML to 'sanitize' to qt | ||||
# formatting | |||||
if reduce_diff_hacks: | if reduce_diff_hacks: | ||||
out = io.BytesIO() | out = io.BytesIO() | ||||
tree.write(out, encoding='utf-8') | tree.write(out, encoding='utf-8') | ||||
out = out.getvalue() | out = out.getvalue() | ||||
out = out.replace(b' />', b'/>') | out = out.replace(b' />', b'/>') | ||||
with open(filepath, 'wb') as f: | with open(filepath, 'wb') as f: | ||||
f.write(out) | f.write(out) | ||||
else: | else: | ||||
tree.write(filepath, encoding='utf-8') | tree.write(filepath, encoding='utf-8') | ||||
return have_errors | return have_errors | ||||
if __name__ == '__main__': | if __name__ == '__main__': | ||||
check_at_repository_root() | check_at_repository_root() | ||||
fetch_all_translations() | fetch_all_translations() | ||||
postprocess_translations() | postprocess_translations() |