Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix KeyError for translations #7370

Merged
merged 7 commits into from
Apr 19, 2023
Merged

Fix KeyError for translations #7370

merged 7 commits into from
Apr 19, 2023

Conversation

drew2a
Copy link
Contributor

@drew2a drew2a commented Apr 17, 2023

This PR fixes #7363 by fixing the root cause of the issue — incorrect translation files.

Also, it modifies the tr function in a way that it is safe from this type of error:

class TranslatedString(str):
    """ This class is used to wrap translated strings to be able to log untranslated strings in case of errors.
        Thanks to this class no `KeyError` exceptions are raised when a translation is missing.
    """

    def __new__(cls, translation, original_string):  # pylint: disable=unused-argument
        return super().__new__(cls, translation)

    def __init__(self, translation: str, original_string: str):  # pylint: disable=unused-argument
        super().__init__()
        self.original_string = original_string

    def __mod__(self, other):
        try:
            return str.__mod__(self, other)
        except KeyError as e:
            msg = f'No value provided for {e} in translation "{self}", original string: "{self.original_string}"'
            logger.warning(f'{type(e).__name__}: {msg}')
            return self.original_string % other


def tr(key):
    return f"{QCoreApplication.translate('@default', key)}"
    translated_string = QCoreApplication.translate('@default', key)
    return TranslatedString(translated_string, original_string=key)

@drew2a drew2a marked this pull request as ready for review April 17, 2023 10:48
@drew2a drew2a requested review from a team and kozlovsky and removed request for a team April 17, 2023 10:48
Copy link
Contributor

@kozlovsky kozlovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nice update of the tr function!
It is also necessary to convert .ts files to .qm files by executing the update_translations.sh script and commit the updated .qm files.

@drew2a
Copy link
Contributor Author

drew2a commented Apr 18, 2023

@kozlovsky it gives me the following error:

➜ ./update_translations.sh
./update_translations.sh: line 3: lrelease: command not found

ref:

@drew2a drew2a force-pushed the fix/7363 branch 2 times, most recently from bd35516 to 4315a09 Compare April 18, 2023 13:13
@drew2a drew2a requested a review from kozlovsky April 18, 2023 13:28
Copy link
Contributor

@kozlovsky kozlovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested Tribler with ES_ES locale, and it still crashes on

    <message>
        <location filename="../utilities.py" line="166"/>
        <source>%(seconds)is</source>
        <translation>%(segundos)is</translation>
    </message>

@sentry-for-tribler
Copy link

Sentry issue: TMP-5P

@kozlovsky
Copy link
Contributor

It looks like intercepting KeyError inside the tr function doesn't work because the actual error happens outside, for example:

return tr("%(seconds)is") % data

Here, the tr function returns an incorrect translation with a wrong key, and then the % operation is applied to this returned translation string.

The QCoreApplication.translate call inside the tr function doesn't raise any KeyError, so it is better to remove try/except here.

If we want to prevent this type of KeyError exception, we can return a special subclass of a string with overridden % method that catches KeyError and returns the result of % applied to the original string.

@kozlovsky
Copy link
Contributor

kozlovsky commented Apr 18, 2023

For example, we can use the following TranslatedString class to catch KeyError exceptions and properly log warnings:

class TranslatedString(str):
    def __new__(cls, translation, original_string):
        return super().__new__(cls, translation)

    def __init__(self, translation: str, original_string: str):  # pylint: disable=unused-argument
        super().__init__()
        self.original_string = original_string

    def __mod__(self, other):
        try:
            return str.__mod__(self, other)
        except KeyError as e:
            msg = f'No value provided for {e} in translation "{self}", original string: "{self.original_string}"'
            logger.warning(f'{type(e).__name__}: {msg}')
            return self.original_string % other


def tr(key):
    translated_string = QCoreApplication.translate('@default', key)
    return TranslatedString(translated_string, original_string=key)

Tests:

def test_correct_translation():
    original_string = 'original %(key1)s'
    translated_string = 'translated %(key1)s'
    s = TranslatedString(translated_string, original_string)
    assert s % {'key1': '123'} == 'translated 123'


@patch('tribler.gui.utilities.logger.warning')
def test_missed_key_in_translated_string(warning: Mock):
    original_string = 'original %(key1)s'
    translated_string = 'translated %(key2)s'
    s = TranslatedString(translated_string, original_string)

    assert s % {'key1': '123'} == 'original 123'

    warning.assert_called_once_with('KeyError: No value provided for \'key2\' in translation "translated %(key2)s", '
                                    'original string: "original %(key1)s"')


@patch('tribler.gui.utilities.logger.warning')
def test_missed_key_in_both_translated_and_original_strings(warning: Mock):
    original_string = 'original %(key1)s'
    translated_string = 'translated %(key2)s'
    s = TranslatedString(translated_string, original_string)

    with pytest.raises(KeyError, match=r"^'key1'$"):
        s % {'key3': '123'}

    warning.assert_called_once_with('KeyError: No value provided for \'key2\' in translation "translated %(key2)s", '
                                    'original string: "original %(key1)s"')

@drew2a drew2a requested a review from kozlovsky April 19, 2023 09:51
Copy link
Contributor

@kozlovsky kozlovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the .ts file still needs to be fixed (with recompilation of the .qm file):

    <message>
        <location filename="../utilities.py" line="166"/>
        <source>%(seconds)is</source>
        <translation>%(segundos)is</translation>
    </message>

@drew2a drew2a requested a review from kozlovsky April 19, 2023 12:24
</message>
<message>
<location filename="../utilities.py" line="159"/>
<source>%(weeks)iw %(days)id</source>
<translation>%(semanas)iw %(días)id</translation>
<translation>%(weeks)is %(days)id</translation>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Currently, both weeks (semanas) and seconds (segundos) are abbreviated as s. I suspect it can cause some confusion. According to the Wikipedia, in Spain, "the letters "m" and "s" are used to denote minutes and seconds". With this, I'd prefer to leave "weeks" without an abbreviation:

Suggested change
<translation>%(weeks)is %(days)id</translation>
<translation>%(weeks)i semanas %(days)i días</translation>

But we can leave it as is as well.

@drew2a drew2a merged commit 2d5e005 into Tribler:release/7.13 Apr 19, 2023
@drew2a drew2a deleted the fix/7363 branch April 26, 2023 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants