diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 194c7e8fbb..31880846f0 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -10,7 +10,7 @@ from django.utils.safestring import mark_safe from django.utils.timezone import is_aware, utc from django.utils.translation import ( gettext as _, gettext_lazy, ngettext, ngettext_lazy, npgettext_lazy, - pgettext, + pgettext, round_away_from_one, ) register = template.Library() @@ -158,7 +158,8 @@ def intword(value): large_number = 10 ** exponent if value < large_number * 1000: new_value = value / large_number - return _check_for_i18n(new_value, *converters(new_value)) + rounded_value = round_away_from_one(new_value) + return _check_for_i18n(new_value, *converters(rounded_value)) return value diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index d8fb0a5396..174d2a092d 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -812,7 +812,7 @@ def filesizeformat(bytes_): 102 bytes, etc.). """ try: - bytes_ = float(bytes_) + bytes_ = int(bytes_) except (TypeError, ValueError, UnicodeDecodeError): value = ngettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} return avoid_wrapping(value) diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index 8f3e3396a4..e48c7d245d 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -4,6 +4,7 @@ Internationalization support. import re import warnings from contextlib import ContextDecorator +from decimal import ROUND_UP, Decimal from django.utils.autoreload import autoreload_started, file_changed from django.utils.deprecation import RemovedInDjango40Warning @@ -332,3 +333,7 @@ trim_whitespace_re = re.compile(r'\s*\n\s*') def trim_whitespace(s): return trim_whitespace_re.sub(' ', s.strip()) + + +def round_away_from_one(value): + return int(Decimal(value - 1).quantize(Decimal('0'), rounding=ROUND_UP)) + 1 diff --git a/docs/ref/contrib/humanize.txt b/docs/ref/contrib/humanize.txt index 9a30ce1aa4..edbdb136b0 100644 --- a/docs/ref/contrib/humanize.txt +++ b/docs/ref/contrib/humanize.txt @@ -57,7 +57,9 @@ e.g. with the ``'de'`` language: =========== Converts a large integer (or a string representation of an integer) to a -friendly text representation. Works best for numbers over 1 million. +friendly text representation. Translates ``1.0`` as a singular phrase and all +other numeric values as plural, this may be incorrect for some languages. Works +best for numbers over 1 million. Examples: @@ -74,6 +76,11 @@ e.g. with the ``'de'`` language: * ``1200000`` becomes ``'1,2 Millionen'``. * ``1200000000`` becomes ``'1,2 Milliarden'``. +.. versionchanged:: 3.0 + + All numeric values are now translated as plural, except ``1.0`` which is + translated as a singular phrase. This may be incorrect for some languages. + .. templatefilter:: naturalday ``naturalday`` diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index f6a45ebfc9..e58a18f9da 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -416,6 +416,9 @@ Miscellaneous when ``doseq=False``, rather than iterating them, bringing it into line with the standard library :func:`urllib.parse.urlencode` function. +* ``intword`` template filter now translates ``1.0`` as a singular phrase and + all other numeric values as plural. This may be incorrect for some languages. + .. _deprecated-features-3.0: Features deprecated in 3.0 diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 4302e01fda..8bb284e0c3 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -34,9 +34,9 @@ from django.utils.translation import ( LANGUAGE_SESSION_KEY, activate, check_for_language, deactivate, get_language, get_language_bidi, get_language_from_request, get_language_info, gettext, gettext_lazy, ngettext, ngettext_lazy, - npgettext, npgettext_lazy, pgettext, to_language, to_locale, trans_null, - trans_real, ugettext, ugettext_lazy, ugettext_noop, ungettext, - ungettext_lazy, + npgettext, npgettext_lazy, pgettext, round_away_from_one, to_language, + to_locale, trans_null, trans_real, ugettext, ugettext_lazy, ugettext_noop, + ungettext, ungettext_lazy, ) from django.utils.translation.reloader import ( translation_file_changed, watch_for_translation_changes, @@ -1883,3 +1883,31 @@ class TranslationFileChangedTests(SimpleTestCase): self.assertEqual(trans_real._translations, {}) self.assertIsNone(trans_real._default) self.assertIsInstance(trans_real._active, _thread._local) + + +class UtilsTests(SimpleTestCase): + def test_round_away_from_one(self): + tests = [ + (0, 0), + (0., 0), + (0.25, 0), + (0.5, 0), + (0.75, 0), + (1, 1), + (1., 1), + (1.25, 2), + (1.5, 2), + (1.75, 2), + (-0., 0), + (-0.25, -1), + (-0.5, -1), + (-0.75, -1), + (-1, -1), + (-1., -1), + (-1.25, -2), + (-1.5, -2), + (-1.75, -2), + ] + for value, expected in tests: + with self.subTest(value=value): + self.assertEqual(round_away_from_one(value), expected)