diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 5e6c28255..f1bb0564f 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -906,6 +906,7 @@ def update( update_header_comment: bool = False, keep_user_comments: bool = True, update_creation_date: bool = True, + no_fuzzy_flags: bool = False, ) -> None: """Update the catalog based on the given template catalog. @@ -961,6 +962,9 @@ def update( :param update_header_comment: whether to copy the header comment from the template :param keep_user_comments: whether to keep user comments from the old catalog :param update_creation_date: whether to copy the creation date from the template + :param no_fuzzy_flags: do not mark messages as fuzzy solely because their + IDs were matched approximately. Existing fuzzy + flags and flags caused by plural changes are kept. """ messages = self._messages remaining = messages.copy() @@ -984,7 +988,7 @@ def _merge( message = message.clone() fuzzy = False if oldkey != newkey: - fuzzy = True + fuzzy = not no_fuzzy_flags fuzzy_matches.add(oldkey) oldmsg = messages.get(oldkey) assert oldmsg is not None diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 89826e834..5ec61d1b1 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -713,6 +713,8 @@ class UpdateCatalog(CommandMixin): 'do not use fuzzy matching'), ('update-header-comment', None, 'update target header comment'), + ('no-fuzzy-flags', None, + 'do not mark fuzzy matches as fuzzy'), ('previous', None, 'keep previous msgids of translated messages'), ('check=', None, @@ -730,6 +732,7 @@ class UpdateCatalog(CommandMixin): 'no-fuzzy-matching', 'previous', 'update-header-comment', + 'no-fuzzy-flags', 'check', 'ignore-pot-creation-date', ] @@ -747,6 +750,7 @@ def initialize_options(self): self.init_missing = False self.no_fuzzy_matching = False self.update_header_comment = False + self.no_fuzzy_flags = False self.previous = False self.check = False self.ignore_pot_creation_date = False @@ -830,6 +834,7 @@ def run(self): template, no_fuzzy_matching=self.no_fuzzy_matching, update_header_comment=self.update_header_comment, + no_fuzzy_flags=self.no_fuzzy_flags, update_creation_date=not self.ignore_pot_creation_date, ) diff --git a/docs/cmdline.rst b/docs/cmdline.rst index 672bbfe70..7ecb2669e 100644 --- a/docs/cmdline.rst +++ b/docs/cmdline.rst @@ -236,6 +236,7 @@ a PO template file:: do not use fuzzy matching --update-header-comment update target header comment + --no-fuzzy-flags do not mark fuzzy matches as fuzzy --previous keep previous msgids of translated messages @@ -248,6 +249,16 @@ If neither the ``output_file`` nor the ``locale`` option is set, this command looks for all catalog files in the base directory that match the given domain, and updates each of them. +When correcting source text without changing its meaning, such as fixing a +spelling error, ``--no-fuzzy-flags`` reuses approximately matched translations +without adding a ``fuzzy`` flag just because the message ID changed. Use this +option only when the existing translations are known to remain valid. Existing +``fuzzy`` flags are preserved, and changes to the singular/plural structure or +the number of plural translations still mark a message as fuzzy. + +This differs from ``--no-fuzzy-matching``, which disables approximate matching +and therefore does not reuse translations for changed message IDs. + concat ====== diff --git a/tests/messages/frontend/test_frontend.py b/tests/messages/frontend/test_frontend.py index c8b8d17d5..5cc1c14d0 100644 --- a/tests/messages/frontend/test_frontend.py +++ b/tests/messages/frontend/test_frontend.py @@ -25,7 +25,7 @@ ExtractMessages, UpdateCatalog, ) -from babel.messages.pofile import write_po +from babel.messages.pofile import read_po, write_po from tests.messages.consts import project_dir from tests.messages.utils import CUSTOM_EXTRACTOR_COOKIE @@ -239,6 +239,35 @@ def test_update_catalog_boolean_args(): assert cmdinst.previous is False # Mutually exclusive with no_fuzzy_matching +@pytest.mark.parametrize('no_fuzzy_flags', [False, True]) +def test_update_catalog_no_fuzzy_flags(tmp_path, no_fuzzy_flags): + template = Catalog() + template.add('Save changes') + template.add('Existing fuzzy') + cat = Catalog(locale='de', fuzzy=False) + cat.add('Save chnages', 'Speichern') + cat.add('Existing fuzzy', 'Ungeprüft', flags=['fuzzy']) + pot = tmp_path / 'messages.pot' + po = tmp_path / 'de.po' + for path, value in [(pot, template), (po, cat)]: + with path.open('wb') as file: + write_po(file, value) + + cmd = configure_cli_command([ + 'update', '-i', str(pot), '-o', str(po), '-l', 'de', '--previous', + *(['--no-fuzzy-flags'] if no_fuzzy_flags else []), + ]) + cmd.run() + + with po.open('rb') as file: + result = read_po(file) + assert result['Save changes'].string == 'Speichern' + assert result['Save changes'].fuzzy is (not no_fuzzy_flags) + assert result['Existing fuzzy'].fuzzy + assert not result.obsolete + assert '#| msgid "Save chnages"' in po.read_text(encoding='utf-8') + + def test_compile_catalog_dir(tmp_path): """ Test that `compile` can compile all locales in a directory. diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 7c730d325..a862e9164 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -15,6 +15,8 @@ import pickle from io import StringIO +import pytest + from babel.dates import UTC, format_datetime from babel.messages import catalog, pofile from babel.util import FixedOffsetTimezone @@ -158,6 +160,56 @@ def test_catalog_update_fuzzy_matching_with_case_change(): assert cat['foo'].fuzzy is True +@pytest.mark.parametrize('no_fuzzy_flags', [False, True]) +@pytest.mark.parametrize('already_fuzzy', [False, True]) +@pytest.mark.parametrize('context', [None, 'button']) +@pytest.mark.parametrize('plural', [False, True]) +def test_catalog_update_no_fuzzy_flags(no_fuzzy_flags, already_fuzzy, context, plural): + cat = catalog.Catalog(locale='de') + old_id = ('Save chnages', 'Save all chnages') if plural else 'Save chnages' + new_id = ('Save changes', 'Save all changes') if plural else 'Save changes' + translation = ('Speichern', 'Alle speichern') if plural else 'Speichern' + flags = {'fuzzy'} if already_fuzzy else set() + cat.add(old_id, translation, context=context, flags=flags, user_comments=['Keep this']) + tmpl = catalog.Catalog() + tmpl.add(new_id, context=context, locations=[('main.py', 42)]) + + cat.update(tmpl, no_fuzzy_flags=no_fuzzy_flags) + + message = cat.get(new_id, context=context) + assert message.string == translation + assert message.fuzzy is (already_fuzzy or not no_fuzzy_flags) + assert message.previous_id == (list(old_id) if plural else [old_id]) + assert message.user_comments == ['Keep this'] + assert message.locations == [('main.py', 42)] + assert not cat.obsolete + assert not tmpl.get(new_id, context=context).fuzzy + + +@pytest.mark.parametrize(('old_id', 'translation', 'new_id'), [ + ('item', 'Ding', ('item', 'items')), + (('item', 'items'), ('Ding', 'Dinge'), 'item'), + (('item', 'items'), ('Ding',), ('item', 'items')), +]) +def test_catalog_update_no_fuzzy_flags_preserves_plural_checks(old_id, translation, new_id): + cat = catalog.Catalog(locale='de') + cat.add(old_id, translation) + tmpl = catalog.Catalog() + tmpl.add(new_id) + cat.update(tmpl, no_fuzzy_flags=True) + assert cat[new_id].fuzzy + + +def test_catalog_update_no_fuzzy_flags_with_no_matching(): + cat = catalog.Catalog(locale='de') + cat.add('Save chnages', 'Speichern') + tmpl = catalog.Catalog() + tmpl.add('Save changes') + cat.update(tmpl, no_fuzzy_matching=True, no_fuzzy_flags=True) + assert not cat['Save changes'].string + assert 'Save chnages' in cat.obsolete + + def test_catalog_update_fuzzy_matching_with_char_change(): cat = catalog.Catalog() cat.add('fo', 'Voh')