diff --git a/.gitignore b/.gitignore index 360db7219d..8e5ec99dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ win-version-info.txt *~ appledev.p12 build.cfg +.weblate.ini diff --git a/.tx/config b/.tx/config deleted file mode 100644 index 1db9c65576..0000000000 --- a/.tx/config +++ /dev/null @@ -1,34 +0,0 @@ -; See https://docs.transifex.com/client/client-configuration - -[main] -host = https://www.transifex.com - -[o:musicbrainz:p:musicbrainz:r:picard] -file_filter = po/.po -source_file = po/picard.pot -source_lang = en -type = PO - -[o:musicbrainz:p:musicbrainz:r:picard_appstream] -file_filter = po/appstream/.po -source_file = po/appstream/picard-appstream.pot -source_lang = en -type = PO - -[o:musicbrainz:p:musicbrainz:r:picard_installer] -file_filter = installer/i18n/sources/.json -source_file = installer/i18n/sources/en.json -source_lang = en -type = KEYVALUEJSON - -[o:musicbrainz:p:musicbrainz:r:countries] -file_filter = po/countries/.po -source_file = po/countries/countries.pot -source_lang = en -type = PO - -[o:musicbrainz:p:musicbrainz:r:attributes] -file_filter = po/attributes/.po -source_file = po/attributes/attributes.pot -source_lang = en -type = PO diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 415b812903..00a81a088a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,7 +71,7 @@ We follow the "typical" GitHub workflow when contributing changes: ## Translations -See po/README.md for information about translations. +See [po/README.md](./po/README.md) for information about translations. ## Audio Metadata Specifications @@ -122,4 +122,4 @@ Also relevant: ### Other specs -- [ReplayGain 2.0 specification](http://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification) \ No newline at end of file +- [ReplayGain 2.0 specification](http://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification) diff --git a/RELEASING.md b/RELEASING.md index 496ad17532..50d659c056 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -49,12 +49,6 @@ This shouldn't be needed, but better to check before releasing git ls-tree --full-tree -r HEAD --name-only |while read f; do sed -i '1s/^\xEF\xBB\xBF//' "$f"; done && git diff --quiet || git commit -a -m 'Remove nasty BOM bytes' ``` -## Get latest translations from Transifex - -```bash -python setup.py pull_translations && git diff --quiet || git commit -m 'Update .po files' -- po/ -``` - ## Synchronize generated consts ```bash diff --git a/org.musicbrainz.Picard.appdata.xml.in b/org.musicbrainz.Picard.appdata.xml.in index a3a6f99d53..78caac8883 100644 --- a/org.musicbrainz.Picard.appdata.xml.in +++ b/org.musicbrainz.Picard.appdata.xml.in @@ -103,7 +103,7 @@ https://picard-docs.musicbrainz.org/en/faq/faq.html https://picard-docs.musicbrainz.org/ https://metabrainz.org/donate - https://www.transifex.com/musicbrainz/musicbrainz/picard/ + https://translations.metabrainz.org/projects/picard/ picard MetaBrainz Foundation diff --git a/po/README.md b/po/README.md index d9d7f0db1e..ba44baca18 100644 --- a/po/README.md +++ b/po/README.md @@ -1,57 +1,77 @@ Translations ============ -Picard translations are handled by [Transifex](https://www.transifex.com). +Picard translations are handled by [Weblate](https://translations.metabrainz.org/projects/picard/). For translation instructions please see [Picard, Picard Website and Picard User Guide Internationalization](https://wiki.musicbrainz.org/MusicBrainz_Picard/Internationalization). + +The translation files are automatically synced between the Picard Github repository and Weblate. Translations can be done in Weblate or by updating the translation files directly. + +Below is a technical description for managing the translations as a Picard maintainer or developer. -_Please do not manually edit the PO files._ Required tools -------------- -* [Transifex client 1.x](https://developers.transifex.com/docs/cli) +* [Weblate Client](https://docs.weblate.org/en/latest/wlc.html) * [Babel](https://babel.pocoo.org/) Picard source tree strings -------------------------- -Their translations are handled at +Their translations are handled at One can update `picard.pot` using: + ```bash $ python setup.py regen_pot_file ``` -Transifex will _automatically_ pick `picard.pot` from [Picard git repository master branch](https://github.com/metabrainz/picard/tree/master) once per day. +Weblate will _automatically_ sync the changed `picard.pot` and update the translation files (`*.po`) with msgmerge. -Attributes and countries strings --------------------------------- +AppStream metadata translations +------------------------------- -Their translations are handled at and +Translations for the strings from `org.musicbrainz.Picard.appdata.xml.in` are handled at . -`attributes.pot` and `countries.pot` are updated by [musicbrainz-server project](https://github.com/metabrainz/musicbrainz-server), outside the Picard project. +One can update `appstream/picard-appstream.pot` using: -Picard maintainers can regenerate `picard/const/attributes.py` and `picard/const/countries.py`, which are using `attributes.pot` and `countries.pot` as base, using the command: ```bash -$ python setup.py update_constants +$ python setup.py regen_appdata_pot_file ``` -It will retrieve and parse latest `attributes.pot` and `countries.pot` to rebuild `picard/const/attributes.py` and `picard/const/countries.py`. +Weblate will _automatically_ sync the changed `picard-appstream.pot` and update the translation files (`appstream/*.po`) with msgmerge. + + +Windows installer translations +------------------------------ -To fetch latest translations from Transifex -------------------------------------------- +The translations for the Windows installer are inside the JSON files in `installer/i18n/sources`. +Translation in Weblate is done at + + +Attributes and countries strings +-------------------------------- + +Their translations are handled at and + +`attributes.pot` and `countries.pot` are updated by [musicbrainz-server project](https://github.com/metabrainz/musicbrainz-server), outside the Picard project. + +Picard maintainers can regenerate `picard/const/attributes.py` and `picard/const/countries.py`, which are using `attributes.pot` and `countries.pot` as base. For this an Weblate API key is required, which can be found in your Weblate user settings under [API access](https://translations.metabrainz.org/accounts/profile/#api). The constants can then be updated with the following command: -Use the following command: ```bash -$ python setup.py pull_translations +$ python setup.py update_constants --weblate-key={YOUR_WEBLATE_API_KEY} ``` -It will fetch all po files from Transifex, but the most incomplete ones. +Instead of entering the Weblate API key each time you can also place a file `.weblate.ini` in the root of the repository with the following content: -The minimum acceptable percentage of a translation in order to download it can be seen using: -```bash -$ python setup.py pull_translations --help +```ini +[weblate] +url = https://translations.metabrainz.org/api/ + +[keys] +https://translations.metabrainz.org/api/ = YOUR_WEBLATE_API_KEY ``` -The percentage value is passed to the `tx pull` command. + +It will retrieve and parse latest `attributes.pot` and `countries.pot` to rebuild `picard/const/attributes.py` and `picard/const/countries.py`. diff --git a/requirements-dev.txt b/requirements-dev.txt index f6084448a0..c05b5088af 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,4 @@ flake8 isort>=5.0 pycodestyle pylint>=2.6.0 +wlc diff --git a/scripts/tools/pull-shared-translations.py b/scripts/tools/pull-shared-translations.py new file mode 100755 index 0000000000..b9b768f05b --- /dev/null +++ b/scripts/tools/pull-shared-translations.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2023 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +import argparse +import logging +import os +import os.path +import sys + +from wlc import ( + Component, + Weblate, +) +from wlc.config import WeblateConfig + + +WEBLATE_URL = 'https://translations.metabrainz.org/api/' +PROJECT_NAME = 'musicbrainz' +PROJECT_COMPONENTS = ( + 'attributes', + 'countries', +) +MIN_TRANSLATED_PERCENT = 10 + + +logging.basicConfig( + force=True, + format="%(asctime)s:%(levelname)s: %(message)s", + level=logging.INFO, + stream=sys.stderr, +) + + +def fetch_translations(component_name: str, user_key: str = '', config: WeblateConfig = None): + weblate = Weblate(key=user_key, url=WEBLATE_URL, config=config) + component = Component(weblate, f'components/{PROJECT_NAME}/{component_name}/') + logging.info('Processing component %s...', component['name']) + translations = component.list() + source_language = component['source_language']['code'] + output_dir = get_output_dir(component_name) + logging.info('Output dir: %s', output_dir) + for translation in translations: + # Skip source language and translation templates + language_name = translation['language']['name'] + language_code = translation['language']['code'] + if (language_code == source_language + or translation['translated_percent'] < MIN_TRANSLATED_PERCENT + or translation['is_template']): + logging.info('Skipping translation file for %s.', language_name) + continue + + logging.info('Downloading translation file for %s...', language_name) + data = translation.download() + output_path = os.path.join(output_dir, f'{language_code}.po') + with open(output_path, 'bw') as output_file: + output_file.write(data) + + +def get_output_dir(component_name: str) -> str: + path = os.path.join(os.path.dirname(__file__), '..', '..', 'po', component_name) + os.makedirs(path, exist_ok=True) + return path + + +def load_config() -> WeblateConfig: + config_path = os.path.join(os.path.dirname(__file__), '..', '..', '.weblate.ini') + if os.path.exists: + config = WeblateConfig() + config.load(config_path) + return config + else: + return None + + +def main(): + parser = argparse.ArgumentParser( + prog='pull-shared-translations', + description=( + 'Fetches the translations for attributes and countries from ' + 'the MusicBrainz Server project on Weblate.' + ), + epilog=( + 'Instead of passing the --key parameter the key can also be set in ' + 'a file .weblate.ini in the repositories root directory. See ' + 'po/README.md for details.' + )) + parser.add_argument('-k', '--key', help='Weblate user key') + args = parser.parse_args() + + config = None + if not args.key: + config = load_config() + if not config: + parser.print_usage() + parser.error('No Weblate user key specified. See po/README.md for details.') + url, key = config.get_url_key() + if not key or url != WEBLATE_URL: + parser.print_usage() + parser.error('Invalid .weblate.ini. See po/README.md for details.') + + for component_name in PROJECT_COMPONENTS: + fetch_translations(component_name, user_key=args.key, config=config) + + +if __name__ == '__main__': + logging.debug("Starting...") + main() diff --git a/setup.py b/setup.py index 745cf6d351..78be63b820 100644 --- a/setup.py +++ b/setup.py @@ -86,8 +86,6 @@ Extension('picard.util._astrcmp', sources=['picard/util/_astrcmp.c']), ] -tx_executable = which('tx') - def newer(source, target): """Return true if 'source' exists and is more recently modified than @@ -493,40 +491,6 @@ def run(self): ]) -class picard_pull_translations(Command): - description = "Retrieve po files from transifex" - minimum_perc_default = 5 - user_options = [ - ('minimum-perc=', 'm', - "Specify the minimum acceptable percentage of a translation (default: %d)" % minimum_perc_default) - ] - - def initialize_options(self): - self.minimum_perc = self.minimum_perc_default - - def finalize_options(self): - self.minimum_perc = int(self.minimum_perc) - - def run(self): - if tx_executable is None: - sys.exit('Transifex client executable (tx) not found.') - self.spawn([ - tx_executable, - 'pull', - '--force', - '--all', - '--minimum-perc=%d' % self.minimum_perc - ]) - self.spawn([ - tx_executable, - 'pull', - '--force', - '--languages', - 'en_AU,en_GB,en_CA' - 'musicbrainz.picard', - ]) - - _regen_pot_description = "Regenerate po/picard.pot, parsing source tree for new or updated strings" try: from babel import __version__ as babel_version @@ -581,32 +545,29 @@ def _get_option_name(obj): class picard_update_constants(Command): description = "Regenerate attributes.py and countries.py" user_options = [ - ('skip-pull', None, "skip the tx pull steps"), + ('skip-pull', None, "skip the translation pull step"), + ('weblate-key=', None, "Weblate API key"), ] boolean_options = ['skip-pull'] def initialize_options(self): self.skip_pull = None + self.weblate_key = None def finalize_options(self): self.locales = self.distribution.locales def run(self): - if tx_executable is None: - sys.exit('Transifex client executable (tx) not found.') - from babel.messages import pofile if not self.skip_pull: - txpull_cmd = [ - tx_executable, - 'pull', - '--force', - '--source', - 'musicbrainz.attributes', - 'musicbrainz.countries', + cmd = [ + os.path.join(os.path.dirname(__file__), 'scripts', 'tools', 'pull-shared-translations.py'), ] - self.spawn(txpull_cmd) + if self.weblate_key: + cmd.append('--key') + cmd.append(self.weblate_key) + self.spawn(cmd) countries = dict() countries_potfile = os.path.join('po', 'countries', 'countries.pot') @@ -794,7 +755,6 @@ def _get_requirements(): 'install': picard_install, 'install_locales': picard_install_locales, 'update_constants': picard_update_constants, - 'pull_translations': picard_pull_translations, 'regen_pot_file': picard_regen_pot_file, 'patch_version': picard_patch_version, },