PICARD-2690: Move picard/const translations into separate message domain

This most prominently moves translations of dropdown lists for locales
and writing systems into a separate component, making it easier to
translate the main UI texts.
This commit is contained in:
Philipp Wolfer
2023-08-23 18:29:50 +02:00
parent fd9a6ea681
commit 83f6b6fc93
19 changed files with 4487 additions and 4468 deletions

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ Thumbs.db:encryptable
*.pyc *.pyc
*.pyd *.pyd
*.so *.so
*.mo
appxmanifest.xml appxmanifest.xml
build/ build/
coverage.xml coverage.xml

View File

@@ -283,8 +283,8 @@ min-similarity-lines=4
# List of additional names supposed to be defined in builtins. Remember that # List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible. # you should avoid defining new builtins when possible.
additional-builtins=_, N_, ngettext, gettext_countries, additional-builtins=_, N_, ngettext, gettext_attributes, pgettext_attributes,
gettext_attributes, pgettext_attributes gettext_constants, gettext_countries
# Tells whether unused global variables should be treated as a violation. # Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes allow-global-unused-variables=yes

View File

@@ -229,7 +229,12 @@ def upgrade_to_v1_4_0_dev_6(config):
if old_script_text_option in _s: if old_script_text_option in _s:
old_script_text = _s.value(old_script_text_option, TextOption, "") old_script_text = _s.value(old_script_text_option, TextOption, "")
if old_script_text: if old_script_text:
old_script = (0, unique_numbered_title(_(DEFAULT_SCRIPT_NAME), list_of_scripts), _s["enable_tagger_scripts"], old_script_text) old_script = (
0,
unique_numbered_title(gettext_constants(DEFAULT_SCRIPT_NAME), list_of_scripts),
_s["enable_tagger_scripts"],
old_script_text,
)
list_of_scripts.append(old_script) list_of_scripts.append(old_script)
_s["list_of_scripts"] = list_of_scripts _s["list_of_scripts"] = list_of_scripts
_s.remove(old_enabled_option) _s.remove(old_enabled_option)

View File

@@ -172,12 +172,14 @@ def setup_gettext(localedir, ui_language=None, logger=None):
QLocale.setDefault(QLocale(current_locale)) QLocale.setDefault(QLocale(current_locale))
trans = _load_translation('picard', localedir, language=current_locale) trans = _load_translation('picard', localedir, language=current_locale)
trans_countries = _load_translation('picard-countries', localedir, language=current_locale)
trans_attributes = _load_translation('picard-attributes', localedir, language=current_locale) trans_attributes = _load_translation('picard-attributes', localedir, language=current_locale)
trans_constants = _load_translation('picard-constants', localedir, language=current_locale)
trans_countries = _load_translation('picard-countries', localedir, language=current_locale)
trans.install(['ngettext']) trans.install(['ngettext'])
builtins.__dict__['gettext_countries'] = trans_countries.gettext
builtins.__dict__['gettext_attributes'] = trans_attributes.gettext builtins.__dict__['gettext_attributes'] = trans_attributes.gettext
builtins.__dict__['gettext_constants'] = trans_constants.gettext
builtins.__dict__['gettext_countries'] = trans_countries.gettext
if hasattr(trans_attributes, 'pgettext'): if hasattr(trans_attributes, 'pgettext'):
builtins.__dict__['pgettext_attributes'] = trans_attributes.pgettext builtins.__dict__['pgettext_attributes'] = trans_attributes.pgettext

View File

@@ -115,7 +115,7 @@ class GeneralOptionsPage(OptionsPage):
for level, description in PROGRAM_UPDATE_LEVELS.items(): for level, description in PROGRAM_UPDATE_LEVELS.items():
# TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved. # TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved.
babel_415_workaround = description['title'] babel_415_workaround = description['title']
self.ui.update_level.addItem(_(babel_415_workaround), level) self.ui.update_level.addItem(gettext_constants(babel_415_workaround), level)
idx = self.ui.update_level.findData(value) idx = self.ui.update_level.findData(value)
if idx == -1: if idx == -1:
idx = self.ui.update_level.findData(DEFAULT_PROGRAM_UPDATE_LEVEL) idx = self.ui.update_level.findData(DEFAULT_PROGRAM_UPDATE_LEVEL)

View File

@@ -122,7 +122,7 @@ class InterfaceOptionsPage(OptionsPage):
self.ui.ui_theme.setCurrentIndex(self.ui.ui_theme.findData(UiTheme.DEFAULT)) self.ui.ui_theme.setCurrentIndex(self.ui.ui_theme.findData(UiTheme.DEFAULT))
self.ui.ui_language.addItem(_('System default'), '') self.ui.ui_language.addItem(_('System default'), '')
language_list = [(lang[0], lang[1], _(lang[2])) for lang in UI_LANGUAGES] language_list = [(lang[0], lang[1], gettext_constants(lang[2])) for lang in UI_LANGUAGES]
def fcmp(x): def fcmp(x):
return strxfrm(x[2]) return strxfrm(x[2])

View File

@@ -130,7 +130,7 @@ class MetadataOptionsPage(OptionsPage):
def make_locales_text(self): def make_locales_text(self):
def translated_locales(): def translated_locales():
for locale in self.current_locales: for locale in self.current_locales:
yield _(ALIAS_LOCALES[locale]) yield gettext_constants(ALIAS_LOCALES[locale])
self.ui.selected_locales.setText('; '.join(translated_locales())) self.ui.selected_locales.setText('; '.join(translated_locales()))
@@ -205,7 +205,7 @@ class MultiLocaleSelector(PicardDialog):
# Note that items in the selected locales list are not indented because # Note that items in the selected locales list are not indented because
# the root locale may not be in the list, or may not immediately precede # the root locale may not be in the list, or may not immediately precede
# the specific locale. # the specific locale.
label = _(ALIAS_LOCALES[locale]) label = gettext_constants(ALIAS_LOCALES[locale])
item = QtWidgets.QListWidgetItem(label) item = QtWidgets.QListWidgetItem(label)
item.setData(QtCore.Qt.ItemDataRole.UserRole, locale) item.setData(QtCore.Qt.ItemDataRole.UserRole, locale)
self.ui.selected_locales.addItem(item) self.ui.selected_locales.addItem(item)
@@ -214,7 +214,7 @@ class MultiLocaleSelector(PicardDialog):
def indented_translated_locale(locale, level): def indented_translated_locale(locale, level):
return _("{indent}{locale}").format( return _("{indent}{locale}").format(
indent=" " * level, indent=" " * level,
locale=_(ALIAS_LOCALES[locale]) locale=gettext_constants(ALIAS_LOCALES[locale])
) )
self.ui.available_locales.clear() self.ui.available_locales.clear()
@@ -240,7 +240,7 @@ class MultiLocaleSelector(PicardDialog):
# Note that items in the selected locales list are not indented because # Note that items in the selected locales list are not indented because
# the root locale may not be in the list, or may not immediately precede # the root locale may not be in the list, or may not immediately precede
# the specific locale. # the specific locale.
new_item.setText(_(ALIAS_LOCALES[locale])) new_item.setText(gettext_constants(ALIAS_LOCALES[locale]))
self.ui.selected_locales.addItem(new_item) self.ui.selected_locales.addItem(new_item)
self.ui.selected_locales.setCurrentRow(self.ui.selected_locales.count() - 1) self.ui.selected_locales.setCurrentRow(self.ui.selected_locales.count() - 1)
@@ -290,7 +290,7 @@ class ScriptExceptionSelector(PicardDialog):
@staticmethod @staticmethod
def make_label(script_id, script_weighting): def make_label(script_id, script_weighting):
return "{script} ({weighting}%)".format( return "{script} ({weighting}%)".format(
script=_(SCRIPTS[script_id]), script=gettext_constants(SCRIPTS[script_id]),
weighting=script_weighting, weighting=script_weighting,
) )

View File

@@ -424,7 +424,7 @@ class ProfilesOptionsPage(OptionsPage):
id = str(uuid.uuid4()) id = str(uuid.uuid4())
settings = deepcopy(self.profile_settings[self.current_profile_id]) settings = deepcopy(self.profile_settings[self.current_profile_id])
self.profile_settings[id] = settings self.profile_settings[id] = settings
base_title = "%s %s" % (get_base_title(item.name), _(DEFAULT_COPY_TEXT)) base_title = "%s %s" % (get_base_title(item.name), gettext_constants(DEFAULT_COPY_TEXT))
name = self.ui.profile_list.unique_profile_name(base_title) name = self.ui.profile_list.unique_profile_name(base_title)
self.ui.profile_list.add_profile(name=name, profile_id=id) self.ui.profile_list.add_profile(name=name, profile_id=id)
self.update_config_overrides() self.update_config_overrides()

View File

@@ -931,7 +931,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
def new_script_name(self, base_title=None): def new_script_name(self, base_title=None):
"""Get new unique script name. """Get new unique script name.
""" """
default_title = base_title if base_title is not None else _(DEFAULT_SCRIPT_NAME) default_title = base_title if base_title is not None else gettext_constants(DEFAULT_SCRIPT_NAME)
existing_titles = set(script['title'] for script in self.naming_scripts.values()) existing_titles = set(script['title'] for script in self.naming_scripts.values())
return unique_numbered_title(default_title, existing_titles) return unique_numbered_title(default_title, existing_titles)
@@ -949,7 +949,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
script_item = self.ui.preset_naming_scripts.itemData(selected) script_item = self.ui.preset_naming_scripts.itemData(selected)
new_item = FileNamingScript.create_from_dict(script_dict=script_item).copy() new_item = FileNamingScript.create_from_dict(script_dict=script_item).copy()
base_title = "%s %s" % (get_base_title(script_item['title']), _(DEFAULT_COPY_TEXT)) base_title = "%s %s" % (get_base_title(script_item['title']), gettext_constants(DEFAULT_COPY_TEXT))
new_item.title = self.new_script_name(base_title) new_item.title = self.new_script_name(base_title)
self._insert_item(new_item.to_dict()) self._insert_item(new_item.to_dict())

View File

@@ -60,7 +60,7 @@ class ProfileListWidget(QtWidgets.QListWidget):
def unique_profile_name(self, base_name=None): def unique_profile_name(self, base_name=None):
if base_name is None: if base_name is None:
base_name = _(DEFAULT_PROFILE_NAME) base_name = gettext_constants(DEFAULT_PROFILE_NAME)
existing_titles = [self.item(i).name for i in range(self.count())] existing_titles = [self.item(i).name for i in range(self.count())]
return unique_numbered_title(base_name, existing_titles) return unique_numbered_title(base_name, existing_titles)
@@ -95,7 +95,7 @@ class ProfileListWidgetItem(HashableListWidgetItem):
super().__init__(name) super().__init__(name)
self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable) self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable)
if name is None: if name is None:
name = _(DEFAULT_PROFILE_NAME) name = gettext_constants(DEFAULT_PROFILE_NAME)
self.setText(name) self.setText(name)
self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked) self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked)
if not profile_id: if not profile_id:

View File

@@ -69,7 +69,7 @@ class ScriptListWidget(QtWidgets.QListWidget):
def unique_script_name(self): def unique_script_name(self):
existing_titles = [self.item(i).name for i in range(self.count())] existing_titles = [self.item(i).name for i in range(self.count())]
return unique_numbered_title(_(DEFAULT_SCRIPT_NAME), existing_titles) return unique_numbered_title(gettext_constants(DEFAULT_SCRIPT_NAME), existing_titles)
def add_script(self): def add_script(self):
numbered_name = self.unique_script_name() numbered_name = self.unique_script_name()
@@ -114,7 +114,7 @@ class ScriptListWidgetItem(HashableListWidgetItem):
super().__init__(name) super().__init__(name)
self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable) self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable)
if name is None: if name is None:
name = _(DEFAULT_SCRIPT_NAME) name = gettext_constants(DEFAULT_SCRIPT_NAME)
self.setText(name) self.setText(name)
self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked) self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked)
self.script = script self.script = script

View File

@@ -1093,7 +1093,7 @@ def unique_numbered_title(default_title, existing_titles, fmt=None):
based on given default title and existing titles based on given default title and existing titles
""" """
if fmt is None: if fmt is None:
fmt = _(DEFAULT_NUMBERED_TITLE_FORMAT) fmt = gettext_constants(DEFAULT_NUMBERED_TITLE_FORMAT)
escaped_title = re.escape(default_title) escaped_title = re.escape(default_title)
reg_count = r'(\d+)' reg_count = r'(\d+)'
@@ -1116,7 +1116,7 @@ def get_base_title_with_suffix(title, suffix, fmt=None):
removing the suffix and number portion from the end. removing the suffix and number portion from the end.
""" """
if fmt is None: if fmt is None:
fmt = _(DEFAULT_NUMBERED_TITLE_FORMAT) fmt = gettext_constants(DEFAULT_NUMBERED_TITLE_FORMAT)
escaped_suffix = re.escape(suffix) escaped_suffix = re.escape(suffix)
reg_title = r'(?P<title>.*?)(?:\s*' + escaped_suffix + ')?' reg_title = r'(?P<title>.*?)(?:\s*' + escaped_suffix + ')?'
@@ -1131,7 +1131,7 @@ def get_base_title_with_suffix(title, suffix, fmt=None):
def get_base_title(title): def get_base_title(title):
"""Extract the base portion of a title, using the standard suffix. """Extract the base portion of a title, using the standard suffix.
""" """
suffix = _(DEFAULT_COPY_TEXT) suffix = gettext_constants(DEFAULT_COPY_TEXT)
return get_base_title_with_suffix(title, suffix) return get_base_title_with_suffix(title, suffix)

View File

@@ -159,7 +159,7 @@ class UpdateCheckManager(QtCore.QObject):
_("Picard Update"), _("Picard Update"),
_("There is no update currently available for your subscribed update level: {update_level}\n\n" _("There is no update currently available for your subscribed update level: {update_level}\n\n"
"Your version: {picard_old_version}\n").format( "Your version: {picard_old_version}\n").format(
update_level=_(update_level), update_level=gettext_constants(update_level),
picard_old_version=PICARD_FANCY_VERSION_STR, picard_old_version=PICARD_FANCY_VERSION_STR,
), ),
QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Ok QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Ok

4186
po/constants/constants.pot Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
Babel==2.9.1 Babel>=2.10.0
PyInstaller==5.12.0 PyInstaller==5.12.0
setuptools>=62.4.0 setuptools>=62.4.0

View File

@@ -7,7 +7,7 @@
# E501: line too long (xx > 79 characters) # E501: line too long (xx > 79 characters)
# W503: line break occurred before a binary operator # W503: line break occurred before a binary operator
ignore = E127,E128,E129,E226,E241,E501,W503 ignore = E127,E128,E129,E226,E241,E501,W503
builtins = _,N_,ngettext,gettext_attributes,pgettext_attributes,gettext_countries builtins = _,N_,ngettext,gettext_attributes,pgettext_attributes,gettext_constants,gettext_countries
exclude = ui_*.py,picard/resources.py exclude = ui_*.py,picard/resources.py
[coverage:run] [coverage:run]

View File

@@ -492,32 +492,26 @@ class picard_regen_appdata_pot_file(Command):
_regen_pot_description = "Regenerate po/picard.pot, parsing source tree for new or updated strings" _regen_pot_description = "Regenerate po/picard.pot, parsing source tree for new or updated strings"
_regen_constants_pot_description = "Regenerate po/constants/constants.pot, parsing source tree for new or updated strings"
try: try:
from babel import __version__ as babel_version
from babel.messages import frontend as babel from babel.messages import frontend as babel
def versiontuple(v):
return tuple(map(int, (v.split("."))))
# input_dirs are incorrectly handled in babel versions < 1.0
# https://web.archive.org/web/20150910064954/babel.edgewall.org/ticket/232
input_dirs_workaround = versiontuple(babel_version) < (1, 0, 0)
class picard_regen_pot_file(babel.extract_messages): class picard_regen_pot_file(babel.extract_messages):
description = _regen_pot_description description = _regen_pot_description
def initialize_options(self): def initialize_options(self):
# cannot use super() with old-style parent class super().initialize_options()
babel.extract_messages.initialize_options(self)
self.output_file = 'po/picard.pot' self.output_file = 'po/picard.pot'
self.input_dirs = 'picard' self.input_dirs = 'picard'
if self.input_dirs and input_dirs_workaround: self.ignore_dirs = ('const',)
self._input_dirs = self.input_dirs
def finalize_options(self): class picard_regen_constants_pot_file(babel.extract_messages):
babel.extract_messages.finalize_options(self) description = _regen_constants_pot_description
if input_dirs_workaround and self._input_dirs:
self.input_dirs = re.split(r',\s*', self._input_dirs) def initialize_options(self):
super().initialize_options()
self.output_file = 'po/constants/constants.pot'
self.input_dirs = 'picard/const'
except ImportError: except ImportError:
class picard_regen_pot_file(Command): class picard_regen_pot_file(Command):
@@ -533,6 +527,9 @@ except ImportError:
def run(self): def run(self):
sys.exit("Babel is required to use this command (see po/README.md)") sys.exit("Babel is required to use this command (see po/README.md)")
class picard_regen_constants_pot_file(picard_regen_pot_file):
description = _regen_constants_pot_description
def _get_option_name(obj): def _get_option_name(obj):
"""Returns the name of the option for specified Command object""" """Returns the name of the option for specified Command object"""
@@ -688,8 +685,9 @@ def _picard_get_locale_files():
locales = [] locales = []
path_domain = { path_domain = {
'po': 'picard', 'po': 'picard',
os.path.join('po', 'countries'): 'picard-countries',
os.path.join('po', 'attributes'): 'picard-attributes', os.path.join('po', 'attributes'): 'picard-attributes',
os.path.join('po', 'constants'): 'picard-constants',
os.path.join('po', 'countries'): 'picard-countries',
} }
for path, domain in path_domain.items(): for path, domain in path_domain.items():
for filepath in glob.glob(os.path.join(path, '*.po')): for filepath in glob.glob(os.path.join(path, '*.po')):
@@ -756,6 +754,7 @@ args = {
'install_locales': picard_install_locales, 'install_locales': picard_install_locales,
'update_constants': picard_update_constants, 'update_constants': picard_update_constants,
'regen_pot_file': picard_regen_pot_file, 'regen_pot_file': picard_regen_pot_file,
'regen_constants_pot_file': picard_regen_constants_pot_file,
'patch_version': picard_patch_version, 'patch_version': picard_patch_version,
}, },
'scripts': ['scripts/' + PACKAGE_NAME], 'scripts': ['scripts/' + PACKAGE_NAME],

View File

@@ -53,8 +53,9 @@ class TestI18n(PicardTestCase):
self.assertEqual('Country', N_('Country')) self.assertEqual('Country', N_('Country'))
self.assertEqual('%i image', ngettext('%i image', '%i images', 1)) self.assertEqual('%i image', ngettext('%i image', '%i images', 1))
self.assertEqual('%i images', ngettext('%i image', '%i images', 2)) self.assertEqual('%i images', ngettext('%i image', '%i images', 2))
self.assertEqual('France', gettext_countries('France'))
self.assertEqual('Cassette', pgettext_attributes('medium_format', 'Cassette')) self.assertEqual('Cassette', pgettext_attributes('medium_format', 'Cassette'))
self.assertEqual('French', gettext_constants('French'))
self.assertEqual('France', gettext_countries('France'))
@unittest.skipUnless(os.path.exists(os.path.join(localedir, 'de')), @unittest.skipUnless(os.path.exists(os.path.join(localedir, 'de')),
'Test requires locales to be built with "python setup.py build_locales -i"') 'Test requires locales to be built with "python setup.py build_locales -i"')
@@ -73,8 +74,9 @@ class TestI18n(PicardTestCase):
self.assertEqual('Country', N_('Country')) self.assertEqual('Country', N_('Country'))
self.assertEqual('%i Bild', ngettext('%i image', '%i images', 1)) self.assertEqual('%i Bild', ngettext('%i image', '%i images', 1))
self.assertEqual('%i Bilder', ngettext('%i image', '%i images', 2)) self.assertEqual('%i Bilder', ngettext('%i image', '%i images', 2))
self.assertEqual('Frankreich', gettext_countries('France'))
self.assertEqual('Kassette', pgettext_attributes('medium_format', 'Cassette')) self.assertEqual('Kassette', pgettext_attributes('medium_format', 'Cassette'))
# self.assertEqual('Französisch', gettext_constants('French'))
self.assertEqual('Frankreich', gettext_countries('France'))
@patch('locale.getpreferredencoding', autospec=True) @patch('locale.getpreferredencoding', autospec=True)