From b612e18d9622fb91195fd13541cae25021afd614 Mon Sep 17 00:00:00 2001 From: Peter Jonas Date: Wed, 27 Jul 2022 02:01:52 +0100 Subject: [PATCH] Generate placeholder translations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add placeholder_translations.py script to generate fake translations. Run the script on GitHub Actions to add the translations to nightly and development builds. Fake translations are displayed in the UI like this: Source text: Choose instruments Translation: ᵗʳ«Choose instruments» This enables developers to see which strings have been correctly marked for translation without having to wait for a proper translation to be made available. Special markup is used to identify plural translations: Source text: %n measure(s) Translation (1st plural form): ᵗʳ¹«%n measure(s)» Translation (2nd plural form): ᵗʳ²«%n measure(s)» Etc. See the second page of the New Score dialog for an example of plurals. Replacement markers %1, %2, etc. for QString arg() are also identified: Source text: Add %1 to chord Translation: ᵗʳ«Add ⌜%1⌝ to chord» Arguments must be translated separately to the main string: Non-translated argument: ᵗʳ«Add ⌜D⌝ to chord» Translated argument: ᵗʳ«Add ⌜ᵗʳ«D»⌝ to chord» The Python script could be modified to display more information in the translated strings such as context, disambiguation, comments or file name and line number of the string in the code or in the .ts file. --- .github/workflows/ci_linux_mu4.yml | 17 ++++ .github/workflows/ci_macos_mu4.yml | 17 ++++ .github/workflows/ci_windows_mu4.yml | 19 ++++ .gitignore | 1 + .../preferences/generalpreferencesmodel.cpp | 7 ++ src/languages/internal/languagesservice.cpp | 8 ++ src/languages/languagestypes.h | 1 + .../translations/placeholder_translations.py | 91 +++++++++++++++++++ 8 files changed, 161 insertions(+) create mode 100755 tools/translations/placeholder_translations.py diff --git a/.github/workflows/ci_linux_mu4.yml b/.github/workflows/ci_linux_mu4.yml index 5898f17ca1..505b966c0d 100644 --- a/.github/workflows/ci_linux_mu4.yml +++ b/.github/workflows/ci_linux_mu4.yml @@ -73,6 +73,13 @@ jobs: fi fi + DO_PLACEHOLDER_TRANSLATIONS='false' + if [[ "$DO_BUILD" == "true" ]]; then + if [[ "$BUILD_MODE" == "nightly_build" || "$BUILD_MODE" == "devel_build" ]]; then + DO_PLACEHOLDER_TRANSLATIONS='true' + fi + fi + DO_UPLOAD_SYMBOLS='false' SENTRY_PROJECT=${{ github.event.inputs.sentry_project }} SENTRY_URL="" @@ -101,6 +108,8 @@ jobs: echo "DO_BUILD: $DO_BUILD" echo "DO_UPDATE_TS=$DO_UPDATE_TS" >> $GITHUB_ENV echo "DO_UPDATE_TS: $DO_UPDATE_TS" + echo "DO_PLACEHOLDER_TRANSLATIONS=$DO_PLACEHOLDER_TRANSLATIONS" >> $GITHUB_ENV + echo "DO_PLACEHOLDER_TRANSLATIONS: $DO_PLACEHOLDER_TRANSLATIONS" echo "DO_PUBLISH=$DO_PUBLISH" >> $GITHUB_ENV echo "DO_PUBLISH: $DO_PUBLISH" echo "DO_UPLOAD_SYMBOLS=$DO_UPLOAD_SYMBOLS" >> $GITHUB_ENV @@ -116,11 +125,19 @@ jobs: if: env.DO_BUILD == 'true' run: | sudo bash ./build/ci/linux/setup.sh + - name: Generate _en.ts files + if: env.DO_BUILD == 'true' + run: | + sudo bash ./build/ci/translation/run_lupdate.sh - name: Update .ts files if: env.DO_UPDATE_TS == 'true' run: | sudo bash ./build/ci/translation/tx_install.sh -t ${{ secrets.TRANSIFEX_API_TOKEN }} -s linux sudo bash ./build/ci/translation/tx_pull.sh + - name: Generate placeholder.ts files + if: env.DO_PLACEHOLDER_TRANSLATIONS == 'true' + run: | + sudo python3 ./tools/translations/placeholder_translations.py - name: Generate .qm files if: env.DO_BUILD == 'true' run: | diff --git a/.github/workflows/ci_macos_mu4.yml b/.github/workflows/ci_macos_mu4.yml index 1f16bad060..926747a107 100644 --- a/.github/workflows/ci_macos_mu4.yml +++ b/.github/workflows/ci_macos_mu4.yml @@ -89,6 +89,13 @@ jobs: fi fi + DO_PLACEHOLDER_TRANSLATIONS='false' + if [[ "$DO_BUILD" == "true" ]]; then + if [[ "$BUILD_MODE" == "nightly_build" || "$BUILD_MODE" == "devel_build" ]]; then + DO_PLACEHOLDER_TRANSLATIONS='true' + fi + fi + DO_UPLOAD_SYMBOLS='false' SENTRY_PROJECT=${{ github.event.inputs.sentry_project }} SENTRY_URL="" @@ -118,6 +125,8 @@ jobs: echo "DO_BUILD: $DO_BUILD" echo "DO_UPDATE_TS=$DO_UPDATE_TS" >> $GITHUB_ENV echo "DO_UPDATE_TS: $DO_UPDATE_TS" + echo "DO_PLACEHOLDER_TRANSLATIONS=$DO_PLACEHOLDER_TRANSLATIONS" >> $GITHUB_ENV + echo "DO_PLACEHOLDER_TRANSLATIONS: $DO_PLACEHOLDER_TRANSLATIONS" echo "DO_NOTARIZE=$DO_NOTARIZE" >> $GITHUB_ENV echo "DO_NOTARIZE: $DO_NOTARIZE" echo "DO_PUBLISH=$DO_PUBLISH" >> $GITHUB_ENV @@ -135,11 +144,19 @@ jobs: if: env.DO_BUILD == 'true' run: | bash ./build/ci/macos/setup.sh + - name: Generate _en.ts files + if: env.DO_BUILD == 'true' + run: | + bash ./build/ci/translation/run_lupdate.sh - name: Update .ts files if: env.DO_UPDATE_TS == 'true' run: | bash ./build/ci/translation/tx_install.sh -t ${{ secrets.TRANSIFEX_API_TOKEN }} -s macos bash ./build/ci/translation/tx_pull.sh + - name: Generate placeholder.ts files + if: env.DO_PLACEHOLDER_TRANSLATIONS == 'true' + run: | + python3 ./tools/translations/placeholder_translations.py - name: Generate .qm files if: env.DO_BUILD == 'true' run: | diff --git a/.github/workflows/ci_windows_mu4.yml b/.github/workflows/ci_windows_mu4.yml index 5a9db57fd5..e2b9a4243a 100644 --- a/.github/workflows/ci_windows_mu4.yml +++ b/.github/workflows/ci_windows_mu4.yml @@ -69,6 +69,13 @@ jobs: fi fi + DO_PLACEHOLDER_TRANSLATIONS='false' + if [[ "$DO_BUILD" == "true" ]]; then + if [[ "$BUILD_MODE" == "nightly_build" || "$BUILD_MODE" == "devel_build" ]]; then + DO_PLACEHOLDER_TRANSLATIONS='true' + fi + fi + DO_UPLOAD_SYMBOLS='false' SENTRY_PROJECT=${{ github.event.inputs.sentry_project }} SENTRY_URL="" @@ -97,6 +104,8 @@ jobs: echo "DO_BUILD: $DO_BUILD" echo "DO_UPDATE_TS=$DO_UPDATE_TS" >> $GITHUB_ENV echo "DO_UPDATE_TS: $DO_UPDATE_TS" + echo "DO_PLACEHOLDER_TRANSLATIONS=$DO_PLACEHOLDER_TRANSLATIONS" >> $GITHUB_ENV + echo "DO_PLACEHOLDER_TRANSLATIONS: $DO_PLACEHOLDER_TRANSLATIONS" echo "DO_PUBLISH=$DO_PUBLISH" >> $GITHUB_ENV echo "DO_PUBLISH: $DO_PUBLISH" echo "DO_UPLOAD_SYMBOLS=$DO_UPLOAD_SYMBOLS" >> $GITHUB_ENV @@ -117,12 +126,22 @@ jobs: shell: bash run: | bash ./build/ci/windows/make_environment.sh + - name: Generate _en.ts files + if: env.DO_BUILD == 'true' + shell: bash + run: | + bash ./build/ci/translation/run_lupdate.sh - name: Update .ts files if: env.DO_UPDATE_TS == 'true' shell: bash run: | bash ./build/ci/translation/tx_install.sh -t ${{ secrets.TRANSIFEX_API_TOKEN }} -s windows bash ./build/ci/translation/tx_pull.sh + - name: Generate placeholder.ts files + if: env.DO_PLACEHOLDER_TRANSLATIONS == 'true' + shell: bash + run: | + python3 -X utf8 ./tools/translations/placeholder_translations.py - name: Generate .qm files if: env.DO_BUILD == 'true' shell: bash diff --git a/.gitignore b/.gitignore index eaecc5abd1..89769feb64 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ MuseScorePortable /*.files /*.includes *.user +/share/locale/*placeholder.ts *.qm share/manual/en share/manual/de diff --git a/src/appshell/view/preferences/generalpreferencesmodel.cpp b/src/appshell/view/preferences/generalpreferencesmodel.cpp index 2be0bbcdfc..b95f36e1dd 100644 --- a/src/appshell/view/preferences/generalpreferencesmodel.cpp +++ b/src/appshell/view/preferences/generalpreferencesmodel.cpp @@ -92,6 +92,13 @@ QVariantList GeneralPreferencesModel::languages() const return l.toMap().value("code").toString() < r.toMap().value("code").toString(); }); + if (!languagesConfiguration()->languageFilePaths(PLACEHOLDER_LANGUAGE_CODE).empty()) { + QVariantMap placeholderLanguageObj; + placeholderLanguageObj["code"] = PLACEHOLDER_LANGUAGE_CODE; + placeholderLanguageObj["name"] = "«Placeholder translations»"; + result.prepend(placeholderLanguageObj); + } + QVariantMap systemLanguageObj; systemLanguageObj["code"] = SYSTEM_LANGUAGE_CODE; systemLanguageObj["name"] = mu::qtrc("appshell/preferences", "System default"); diff --git a/src/languages/internal/languagesservice.cpp b/src/languages/internal/languagesservice.cpp index 3434b11246..1b6876482b 100644 --- a/src/languages/internal/languagesservice.cpp +++ b/src/languages/internal/languagesservice.cpp @@ -201,6 +201,14 @@ void LanguagesService::setCurrentLanguage(const QString& languageCode) return; } + if (languageCode == PLACEHOLDER_LANGUAGE_CODE) { + Ret load = loadLanguage(languageCode); + if (!load) { + LOGE() << load.toString(); + } + return; // no hash for this language + } + LanguagesHash languageHash = languages().val; if (!languageHash.contains(languageCode)) { LOGE() << "Unknown language: " << languageCode; diff --git a/src/languages/languagestypes.h b/src/languages/languagestypes.h index e3084aad3c..1714e7d529 100644 --- a/src/languages/languagestypes.h +++ b/src/languages/languagestypes.h @@ -31,6 +31,7 @@ namespace mu::languages { const QString SYSTEM_LANGUAGE_CODE = "system"; +const QString PLACEHOLDER_LANGUAGE_CODE = "en@placeholder"; class LanguageStatus { diff --git a/tools/translations/placeholder_translations.py b/tools/translations/placeholder_translations.py new file mode 100755 index 0000000000..a750db5674 --- /dev/null +++ b/tools/translations/placeholder_translations.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +# Generates fake translations to be displayed in MuseScore's UI when the +# language is set to «Placeholder translations» in Preferences > General. +# This enables developers to see which strings have been correctly marked for +# translation without having to wait for a proper translation to be made. + +# Steps: +# 1. Add Qt's bin folder to $PATH +# 2. Call run_lupdate.sh +# 3. Run this script +# 4. Call run_lrelease.sh +# 5. Compile & run MuseScore +# 6. In Preferences > General, set language to «Placeholder translations» + +import glob +import io +import os +import re +import sys +import xml.etree.ElementTree as ET + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +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 + # many plural forms as are required by the locale. + +source_lang_ts = source_lang + '.ts' +target_lang_ts = target_lang + '.ts' + +for source_file in glob.glob('share/locale/*_' + source_lang_ts): + target_file = source_file[:-len(source_lang_ts)] + target_lang_ts + + eprint("Reading " + source_file) + tree = ET.parse(source_file) + root = tree.getroot() + + assert(root.tag == 'TS') + root.set('language', target_lang) + + for message in root.findall('.//message'): + source = message.find('source') + translation = message.find('translation') + plurals = translation.findall('numerusform') + + # use the source as basis for the translation + tr_txt = source.text + + # 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) + + # identify start and end of translated string + tr_txt = '«' + tr_txt + '»' + + if plurals: + for idx, plural in enumerate(plurals): + plural.text = 'ᵗʳ' + superscript_numbers[idx] + tr_txt + else: + 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. + for el in root.findall('.//'): + if el.text: + # lrelease 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;') + + # Write XML tree to memory so that we can manipulate it as raw text + memfile = io.StringIO() + tree.write(memfile, encoding='unicode', xml_declaration=True) + + # Manipulate raw XML and write to disk + with open(target_file, 'w', newline='\n', encoding='utf-8') as f: + memfile.seek(0) + f.write(next(iter(memfile))) # write first line (the XML declaration) + f.write('\n') + for line in memfile: + f.write(line + .replace('" />', '"/>') # remove space after final attribute in opening tags + .replace('\r', '&') # use the proper escape character in ' and " + )