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
*.pyd
*.so
*.mo
appxmanifest.xml
build/
coverage.xml

View File

@@ -283,8 +283,8 @@ min-similarity-lines=4
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=_, N_, ngettext, gettext_countries,
gettext_attributes, pgettext_attributes
additional-builtins=_, N_, ngettext, gettext_attributes, pgettext_attributes,
gettext_constants, gettext_countries
# Tells whether unused global variables should be treated as a violation.
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:
old_script_text = _s.value(old_script_text_option, TextOption, "")
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)
_s["list_of_scripts"] = list_of_scripts
_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))
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_constants = _load_translation('picard-constants', localedir, language=current_locale)
trans_countries = _load_translation('picard-countries', localedir, language=current_locale)
trans.install(['ngettext'])
builtins.__dict__['gettext_countries'] = trans_countries.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'):
builtins.__dict__['pgettext_attributes'] = trans_attributes.pgettext

View File

@@ -115,7 +115,7 @@ class GeneralOptionsPage(OptionsPage):
for level, description in PROGRAM_UPDATE_LEVELS.items():
# TODO: Remove temporary workaround once https://github.com/python-babel/babel/issues/415 has been resolved.
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)
if idx == -1:
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_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):
return strxfrm(x[2])

View File

@@ -130,7 +130,7 @@ class MetadataOptionsPage(OptionsPage):
def make_locales_text(self):
def translated_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()))
@@ -205,7 +205,7 @@ class MultiLocaleSelector(PicardDialog):
# 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 specific locale.
label = _(ALIAS_LOCALES[locale])
label = gettext_constants(ALIAS_LOCALES[locale])
item = QtWidgets.QListWidgetItem(label)
item.setData(QtCore.Qt.ItemDataRole.UserRole, locale)
self.ui.selected_locales.addItem(item)
@@ -214,7 +214,7 @@ class MultiLocaleSelector(PicardDialog):
def indented_translated_locale(locale, level):
return _("{indent}{locale}").format(
indent=" " * level,
locale=_(ALIAS_LOCALES[locale])
locale=gettext_constants(ALIAS_LOCALES[locale])
)
self.ui.available_locales.clear()
@@ -240,7 +240,7 @@ class MultiLocaleSelector(PicardDialog):
# 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 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.setCurrentRow(self.ui.selected_locales.count() - 1)
@@ -290,7 +290,7 @@ class ScriptExceptionSelector(PicardDialog):
@staticmethod
def make_label(script_id, script_weighting):
return "{script} ({weighting}%)".format(
script=_(SCRIPTS[script_id]),
script=gettext_constants(SCRIPTS[script_id]),
weighting=script_weighting,
)

View File

@@ -424,7 +424,7 @@ class ProfilesOptionsPage(OptionsPage):
id = str(uuid.uuid4())
settings = deepcopy(self.profile_settings[self.current_profile_id])
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)
self.ui.profile_list.add_profile(name=name, profile_id=id)
self.update_config_overrides()

View File

@@ -931,7 +931,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog):
def new_script_name(self, base_title=None):
"""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())
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)
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)
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):
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())]
return unique_numbered_title(base_name, existing_titles)
@@ -95,7 +95,7 @@ class ProfileListWidgetItem(HashableListWidgetItem):
super().__init__(name)
self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable)
if name is None:
name = _(DEFAULT_PROFILE_NAME)
name = gettext_constants(DEFAULT_PROFILE_NAME)
self.setText(name)
self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked)
if not profile_id:

View File

@@ -69,7 +69,7 @@ class ScriptListWidget(QtWidgets.QListWidget):
def unique_script_name(self):
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):
numbered_name = self.unique_script_name()
@@ -114,7 +114,7 @@ class ScriptListWidgetItem(HashableListWidgetItem):
super().__init__(name)
self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable)
if name is None:
name = _(DEFAULT_SCRIPT_NAME)
name = gettext_constants(DEFAULT_SCRIPT_NAME)
self.setText(name)
self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked)
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
"""
if fmt is None:
fmt = _(DEFAULT_NUMBERED_TITLE_FORMAT)
fmt = gettext_constants(DEFAULT_NUMBERED_TITLE_FORMAT)
escaped_title = re.escape(default_title)
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.
"""
if fmt is None:
fmt = _(DEFAULT_NUMBERED_TITLE_FORMAT)
fmt = gettext_constants(DEFAULT_NUMBERED_TITLE_FORMAT)
escaped_suffix = re.escape(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):
"""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)

View File

@@ -159,7 +159,7 @@ class UpdateCheckManager(QtCore.QObject):
_("Picard Update"),
_("There is no update currently available for your subscribed update level: {update_level}\n\n"
"Your version: {picard_old_version}\n").format(
update_level=_(update_level),
update_level=gettext_constants(update_level),
picard_old_version=PICARD_FANCY_VERSION_STR,
),
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
setuptools>=62.4.0

View File

@@ -7,7 +7,7 @@
# E501: line too long (xx > 79 characters)
# W503: line break occurred before a binary operator
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
[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_constants_pot_description = "Regenerate po/constants/constants.pot, parsing source tree for new or updated strings"
try:
from babel import __version__ as babel_version
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):
description = _regen_pot_description
def initialize_options(self):
# cannot use super() with old-style parent class
babel.extract_messages.initialize_options(self)
super().initialize_options()
self.output_file = 'po/picard.pot'
self.input_dirs = 'picard'
if self.input_dirs and input_dirs_workaround:
self._input_dirs = self.input_dirs
self.ignore_dirs = ('const',)
def finalize_options(self):
babel.extract_messages.finalize_options(self)
if input_dirs_workaround and self._input_dirs:
self.input_dirs = re.split(r',\s*', self._input_dirs)
class picard_regen_constants_pot_file(babel.extract_messages):
description = _regen_constants_pot_description
def initialize_options(self):
super().initialize_options()
self.output_file = 'po/constants/constants.pot'
self.input_dirs = 'picard/const'
except ImportError:
class picard_regen_pot_file(Command):
@@ -533,6 +527,9 @@ except ImportError:
def run(self):
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):
"""Returns the name of the option for specified Command object"""
@@ -688,8 +685,9 @@ def _picard_get_locale_files():
locales = []
path_domain = {
'po': 'picard',
os.path.join('po', 'countries'): 'picard-countries',
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 filepath in glob.glob(os.path.join(path, '*.po')):
@@ -756,6 +754,7 @@ args = {
'install_locales': picard_install_locales,
'update_constants': picard_update_constants,
'regen_pot_file': picard_regen_pot_file,
'regen_constants_pot_file': picard_regen_constants_pot_file,
'patch_version': picard_patch_version,
},
'scripts': ['scripts/' + PACKAGE_NAME],

View File

@@ -53,8 +53,9 @@ class TestI18n(PicardTestCase):
self.assertEqual('Country', N_('Country'))
self.assertEqual('%i image', ngettext('%i image', '%i images', 1))
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('French', gettext_constants('French'))
self.assertEqual('France', gettext_countries('France'))
@unittest.skipUnless(os.path.exists(os.path.join(localedir, 'de')),
'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('%i Bild', ngettext('%i image', '%i images', 1))
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('Französisch', gettext_constants('French'))
self.assertEqual('Frankreich', gettext_countries('France'))
@patch('locale.getpreferredencoding', autospec=True)