Check for translation errors when generating placeholder translations

Check for common errors like trailing whitespace in a string marked for
translation. The filename and line number are displayed in the terminal
output, along with a description of the problem and possible solutions.

If translation errors are found then the Linux CI build will fail, but
the Windows and macOS builds are allowed to proceed. Other kinds of
error (e.g. Python exceptions) will cause all builds to fail.
This commit is contained in:
Peter Jonas 2023-04-12 16:35:03 +01:00
parent 96addbf260
commit a0a6a662d2
5 changed files with 73 additions and 8 deletions

View file

@ -170,7 +170,7 @@ jobs:
- name: Generate placeholder.ts files
if: env.DO_PLACEHOLDER_TRANSLATIONS == 'true'
run: |
python3 ./tools/translations/placeholder_translations.py
python3 ./tools/translations/placeholder_translations.py --warn-only
- name: Generate .qm files
if: env.DO_BUILD == 'true'
run: |

View file

@ -157,7 +157,7 @@ jobs:
if: env.DO_PLACEHOLDER_TRANSLATIONS == 'true'
shell: bash
run: |
python3 -X utf8 ./tools/translations/placeholder_translations.py
python3 -X utf8 ./tools/translations/placeholder_translations.py --warn-only
- name: Generate .qm files
if: env.DO_BUILD == 'true'
shell: bash

View file

@ -3385,7 +3385,8 @@ String Note::accessibleInfo() const
int on = _playEvents[0].ontime();
int off = _playEvents[0].offtime();
if (on != 0 || off != NoteEvent::NOTE_LENGTH) {
onofftime = mtrc("engraving", " (on %1‰ off %2‰)").arg(on, off);
//: Note-on and note-off times relative to note duration, expressed in thousandths (per mille)
onofftime = u" " + mtrc("engraving", "(on %1‰ off %2‰)").arg(on, off);
}
}

View file

@ -277,7 +277,7 @@ bool SaveProjectScenario::warnBeforeSavingToExistingPubliclyVisibleCloudProject(
IInteractive::Result result = interactive()->warning(
trc("project/save", "Publish changes online?"),
trc("project/save", "Your saved changes will be publicly visible. We will also "
"need to generate a new MP3 for public playback. "),
"need to generate a new MP3 for public playback."),
buttons, int(IInteractive::Button::Ok));
return result.standardButton() == IInteractive::Button::Ok;

View file

@ -13,6 +13,7 @@
# 5. Compile & run MuseScore
# 6. In Preferences > General, set language to «Placeholder translations»
import argparse
import glob
import io
import os
@ -20,21 +21,41 @@ import re
import sys
import xml.etree.ElementTree as ET
parser = argparse.ArgumentParser(description='Generate fake translations for testing purposes.')
parser.add_argument('--warn-only', action='store_true',
help='exit with zero status even when translation errors are detected')
args = parser.parse_args()
exit_status = 0
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def tr_error(message_element, description, resolution, hint=''):
location = message_element.find('location')
filename = location.get('filename')
filename = os.path.relpath(os.path.join('share/locale', filename))
line_num = location.get('line')
eprint(f'Translation error at line {line_num} in file {filename}:')
eprint(f' Problem: {description}')
eprint(f' Solution: {resolution}')
if hint:
eprint(f' Hint: {hint}')
superscript_numbers = '¹²³⁴⁵⁶⁷⁸⁹'
os.chdir(sys.path[0] + '/../..') # make all paths relative to repository root
source_lang = 'en'
target_lang = 'en@placeholder' # Substring before '@' must correspond to a real
# locale for plurals to work. You must provide as
# locale for plurals to work. We must provide as
# many plural forms as are required by the locale.
source_lang_ts = source_lang + '.ts'
target_lang_ts = target_lang + '.ts'
eprint(f'{sys.argv[0]}: Generating fake translations for testing purposes.')
for source_file in glob.glob('share/locale/*_' + source_lang_ts):
target_file = source_file[:-len(source_lang_ts)] + target_lang_ts
@ -50,9 +71,44 @@ for source_file in glob.glob('share/locale/*_' + source_lang_ts):
translation = message.find('translation')
plurals = translation.findall('numerusform')
bytes = source.findall('byte')
if bytes:
values = ', '.join([ f"'{b.get('value')}'" for b in bytes ])
if len(bytes) == 1:
tr_error(message, f'Translated string contains illegal byte: {values}.',
'Remove the illegal byte or provide it untranslated.')
else:
tr_error(message, f'Translated string contains illegal bytes: {values}.',
'Remove the illegal bytes or provide them untranslated.')
exit_status = 1
continue
# use the source as basis for the translation
tr_txt = source.text
if not tr_txt:
# Sadly, this test only works for empty strings in QML files. If a translated
# string is empty in a C++ file then lupdate doesn't include it in the TS file.
tr_error(message, 'Translated string is empty.',
'Provide a non-empty string or use "" untranslated if it really needs to be empty.')
exit_status = 1
continue
tr_stripped = tr_txt.strip()
if not tr_stripped:
tr_error(message, 'Translated string only contains whitespace characters.',
'Include non-whitespace characters or provide the whitepace as untranslated text.')
exit_status = 1
continue
if tr_txt != tr_stripped:
tr_error(message, 'Translated string contains leading and/or trailing whitespace.',
'Remove the whitepace or provide it separately as untranslated text.',
'Use .arg() and %1 tags if you need to insert text or numbers into a translated string.')
exit_status = 1
continue
# identify QString arg() markers '%1', '%2', etc. as their
# arguments will require separate translation
tr_txt = re.sub(r'%([1-9]+)', r'%\1⌝', tr_txt)
@ -67,11 +123,11 @@ for source_file in glob.glob('share/locale/*_' + source_lang_ts):
translation.text = 'ᵗʳ' + tr_txt
eprint("Writing " + target_file)
# Tweak ElementTree's XML formatting to match that of Qt's lrelease. The aim is to
# minimise the diff between source_file and target_file.
# Tweak ElementTree's XML formatting to match that of Qt's lupdate. The aim
# is to minimise the diff between source_file and target_file.
for el in root.findall('.//'):
if el.text:
# lrelease XML-escapes more characters than ElementTree, but ElementTree
# lupdate XML-escapes more characters than ElementTree, but ElementTree
# won't allow us to write the & escape character so use \r instead.
el.text = el.text.replace('\'', '\rapos;').replace('"', '\rquot;')
@ -89,3 +145,11 @@ for source_file in glob.glob('share/locale/*_' + source_lang_ts):
.replace('" />', '"/>') # remove space after final attribute in opening tags
.replace('\r', '&') # use the proper escape character in ' and "
)
if exit_status == 0:
eprint(f'{sys.argv[0]}: Success!')
elif args.warn_only:
eprint(f'{sys.argv[0]}: Success! Some errors were ignored because of the --warn-only option.')
else:
eprint(f'{sys.argv[0]}: Failed! Translation errors were detected.')
raise SystemExit(exit_status)