From 816f33696df41bdb8c19667ba770a4bccd32f657 Mon Sep 17 00:00:00 2001 From: John Wuller <847785bd-d466-47cd-a536-eae4096d241d@anonaddy.me> Date: Sat, 14 Dec 2024 07:24:40 -0500 Subject: [PATCH 01/12] Add en_GB and en_US Fixes #210 --- src/humanize/i18n.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/humanize/i18n.py b/src/humanize/i18n.py index 42447b0..e5c9e52 100644 --- a/src/humanize/i18n.py +++ b/src/humanize/i18n.py @@ -71,6 +71,9 @@ def activate( Raises: Exception: If humanize cannot find the locale folder. """ + if locale == "en_GB" or locale == "en_US": + locale = None + if path is None: path = _get_default_locale_path() From 07a0eab5b605c4627874770e3dca0b6ea8c08a82 Mon Sep 17 00:00:00 2001 From: John Wuller <847785bd-d466-47cd-a536-eae4096d241d@anonaddy.me> Date: Sat, 14 Dec 2024 08:11:16 -0500 Subject: [PATCH 02/12] Add tests for en languages --- tests/test_i18n.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index f3721c9..f57c0fa 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -38,6 +38,19 @@ def test_i18n() -> None: assert humanize.ordinal(5) == "5th" assert humanize.precisedelta(one_min_three_seconds) == "1 minute and 7 seconds" +def test_en_locale() -> None: + three_seconds = NOW - dt.timedelta(seconds=3) + + humanize.i18n.activate(None) + test_str = humanize.naturaltime(three_seconds) + + humanize.i18n.activate("en_US") + assert test_str = humanize.naturaltime(three_seconds) + + humanize.i18n.activate("en_GB") + assert test_str = humanize.naturaltime(three_seconds) + + humanize.i18n.deactivate() def test_intcomma() -> None: number = 10_000_000 From d38f68595c3e2262fc61a57bf73e38612b5a4279 Mon Sep 17 00:00:00 2001 From: John Wuller <847785bd-d466-47cd-a536-eae4096d241d@anonaddy.me> Date: Sat, 14 Dec 2024 08:13:04 -0500 Subject: [PATCH 03/12] Add == instead of = --- tests/test_i18n.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index f57c0fa..0295012 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -45,10 +45,10 @@ def test_en_locale() -> None: test_str = humanize.naturaltime(three_seconds) humanize.i18n.activate("en_US") - assert test_str = humanize.naturaltime(three_seconds) + assert test_str == humanize.naturaltime(three_seconds) humanize.i18n.activate("en_GB") - assert test_str = humanize.naturaltime(three_seconds) + assert test_str == humanize.naturaltime(three_seconds) humanize.i18n.deactivate() From 711df91a282aef168db36a8d3a3e185c5f673a92 Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Sun, 2 Feb 2025 15:07:25 +0100 Subject: [PATCH 04/12] Add handling for en_GB and en_US locale Locales starting with en_* will default to no transaltion. --- src/humanize/i18n.py | 7 ++++--- tests/test_i18n.py | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/humanize/i18n.py b/src/humanize/i18n.py index e5c9e52..3a1dac6 100644 --- a/src/humanize/i18n.py +++ b/src/humanize/i18n.py @@ -71,9 +71,10 @@ def activate( Raises: Exception: If humanize cannot find the locale folder. """ - if locale == "en_GB" or locale == "en_US": - locale = None - + if locale.startswith("en_"): + _CURRENT.locale = None + return _TRANSLATIONS[None] + if path is None: path = _get_default_locale_path() diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 0295012..a3a750e 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -40,8 +40,6 @@ def test_i18n() -> None: def test_en_locale() -> None: three_seconds = NOW - dt.timedelta(seconds=3) - - humanize.i18n.activate(None) test_str = humanize.naturaltime(three_seconds) humanize.i18n.activate("en_US") From 772dc48c2ca3785674681db2e476d3b0939fd720 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Feb 2025 14:38:18 +0000 Subject: [PATCH 05/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_i18n.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index a3a750e..59b573a 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -38,6 +38,7 @@ def test_i18n() -> None: assert humanize.ordinal(5) == "5th" assert humanize.precisedelta(one_min_three_seconds) == "1 minute and 7 seconds" + def test_en_locale() -> None: three_seconds = NOW - dt.timedelta(seconds=3) test_str = humanize.naturaltime(three_seconds) @@ -50,6 +51,7 @@ def test_en_locale() -> None: humanize.i18n.deactivate() + def test_intcomma() -> None: number = 10_000_000 From 6fdd7ac85b7e594541faaaa88a2c809c16537bea Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Sat, 8 Feb 2025 10:01:40 +0100 Subject: [PATCH 06/12] Allow i18n.activate(None) This is similar to calling i18n.deactivate. --- src/humanize/i18n.py | 7 ++++--- tests/test_i18n.py | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/humanize/i18n.py b/src/humanize/i18n.py index 3a1dac6..dd94c7a 100644 --- a/src/humanize/i18n.py +++ b/src/humanize/i18n.py @@ -55,14 +55,15 @@ def get_translation() -> gettext_module.NullTranslations: def activate( - locale: str, path: str | os.PathLike[str] | None = None + locale: str | None, path: str | os.PathLike[str] | None = None ) -> gettext_module.NullTranslations: """Activate internationalisation. Set `locale` as current locale. Search for locale in directory `path`. Args: - locale (str): Language name, e.g. `en_GB`. + locale (str | None): Language name, e.g. `en_GB`. If `None`, defaults to no + transaltion. Similar to calling ``deactivate()``. path (str | pathlib.Path): Path to search for locales. Returns: @@ -71,7 +72,7 @@ def activate( Raises: Exception: If humanize cannot find the locale folder. """ - if locale.startswith("en_"): + if locale is None or locale.startswith("en"): _CURRENT.locale = None return _TRANSLATIONS[None] diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 59b573a..b7f0e12 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -39,19 +39,6 @@ def test_i18n() -> None: assert humanize.precisedelta(one_min_three_seconds) == "1 minute and 7 seconds" -def test_en_locale() -> None: - three_seconds = NOW - dt.timedelta(seconds=3) - test_str = humanize.naturaltime(three_seconds) - - humanize.i18n.activate("en_US") - assert test_str == humanize.naturaltime(three_seconds) - - humanize.i18n.activate("en_GB") - assert test_str == humanize.naturaltime(three_seconds) - - humanize.i18n.deactivate() - - def test_intcomma() -> None: number = 10_000_000 @@ -211,3 +198,31 @@ def test_default_locale_path_undefined__spec__( with pytest.raises(Exception) as excinfo: i18n.activate("ru_RU") assert str(excinfo.value) == self.expected_msg + + @freeze_time("2020-02-02") + def test_en_locale(self) -> None: + three_seconds = NOW - dt.timedelta(seconds=3) + test_str = humanize.naturaltime(three_seconds) + + humanize.i18n.activate("en_US") + assert test_str == humanize.naturaltime(three_seconds) + + humanize.i18n.activate("en_GB") + assert test_str == humanize.naturaltime(three_seconds) + + humanize.i18n.deactivate() + + @freeze_time("2020-02-02") + def test_None_locale(self) -> None: + three_seconds = NOW - dt.timedelta(seconds=3) + test_str = humanize.naturaltime(three_seconds) + + humanize.i18n.activate("fr") + assert humanize.naturaltime(three_seconds) == "il y a 3 secondes" + + humanize.i18n.activate(None) + test_str = humanize.naturaltime(three_seconds) + assert test_str == "3 seconds ago" + + humanize.i18n.deactivate() + assert test_str == humanize.naturaltime(three_seconds) From 5306d9aeafe18deb97deef0ff02839073c310920 Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Sat, 8 Feb 2025 10:35:15 +0100 Subject: [PATCH 07/12] Refactor test_i18n to use NOW as a fixture The global variable NOW came with a gotcha. When using it, it was also needed to decorate the test with freeze_time. NOW is now a fixture which keeps the time frozen for the duration of the test using it. --- tests/test_i18n.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index b7f0e12..431082f 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -4,18 +4,32 @@ import datetime as dt import importlib +from collections.abc import Generator import pytest from freezegun import freeze_time import humanize -with freeze_time("2020-02-02"): - NOW = dt.datetime.now() +@pytest.fixture +def NOW() -> Generator[dt.datetime, None, None]: + """ + Using this fixture will give you a datetime frozen in 2020-02-02 00:00:00+00:00 -@freeze_time("2020-02-02") -def test_i18n() -> None: + In a test using this fixture, further calls to datetime.now() or date.today() would + return also a frozen date / datetime in 2020-02-02. + + Yields: + Generator[datetime, None, None]: UTC aware datetime 2020-02-02 00:00:00+00:00 + """ + with freeze_time("2020-02-02"): + # TODO: Python 3.11+: replace dt.timezone.utc with dt.UTC + NOW = dt.datetime.now(tz=dt.timezone.utc) + yield NOW + + +def test_i18n(NOW: dt.datetime) -> None: three_seconds = NOW - dt.timedelta(seconds=3) one_min_three_seconds = dt.timedelta(milliseconds=67_000) @@ -199,8 +213,7 @@ def test_default_locale_path_undefined__spec__( i18n.activate("ru_RU") assert str(excinfo.value) == self.expected_msg - @freeze_time("2020-02-02") - def test_en_locale(self) -> None: + def test_en_locale(self, NOW: dt.datetime) -> None: three_seconds = NOW - dt.timedelta(seconds=3) test_str = humanize.naturaltime(three_seconds) @@ -212,10 +225,8 @@ def test_en_locale(self) -> None: humanize.i18n.deactivate() - @freeze_time("2020-02-02") - def test_None_locale(self) -> None: + def test_None_locale(self, NOW: dt.datetime) -> None: three_seconds = NOW - dt.timedelta(seconds=3) - test_str = humanize.naturaltime(three_seconds) humanize.i18n.activate("fr") assert humanize.naturaltime(three_seconds) == "il y a 3 secondes" From 443b382e20c5998eeddc1fee89ba2487526ffa37 Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Sat, 8 Feb 2025 11:29:46 +0100 Subject: [PATCH 08/12] Add translation compilation for Windows in CI --- .github/workflows/test.yml | 16 +++++++++++++++- scripts/generate-translation-binaries.ps1 | 6 ++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-translation-binaries.ps1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d44a6e8..0e43826 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,13 @@ jobs: matrix: python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [windows-latest, macos-latest, ubuntu-latest] + include: + - os: windows-latest + translation-script: generate-translation-binaries.ps1 + - os: macos-latest + translation-script: generate-translation-binaries.sh + - os: ubuntu-latest + translation-script: generate-translation-binaries.sh steps: - uses: actions/checkout@v4 @@ -35,12 +42,19 @@ jobs: run: | brew install gettext + - name: Install Windows dependencies + if: startsWith(matrix.os, 'windows') + run: | + nuget install Gettext.Tools -OutputDirectory c:\nuget; + $gettextPath = (Get-ChildItem -Path "C:\nuget" -Directory -Filter "Gettext.Tools.*" | Select-Object -First 1).FullName; + Add-Content $env:GITHUB_PATH "$gettextPath\tools\bin" + - name: Install uv uses: hynek/setup-cached-uv@v2 - name: Generate translation binaries run: | - scripts/generate-translation-binaries.sh + scripts/${{ matrix.translation-script }} - name: Tox tests run: | diff --git a/scripts/generate-translation-binaries.ps1 b/scripts/generate-translation-binaries.ps1 new file mode 100644 index 0000000..0cf3cac --- /dev/null +++ b/scripts/generate-translation-binaries.ps1 @@ -0,0 +1,6 @@ +Get-ChildItem -Path "src\humanize\locale" -Directory | ForEach-Object { + $locale = $_.Name + Write-Host "$locale" + # compile to binary .mo + msgfmt --check -o "src\humanize\locale\$locale\LC_MESSAGES\humanize.mo" "src\humanize\locale\$locale\LC_MESSAGES\humanize.po" +} From a72bc552c41e0ea6756df0a13c53b567a664a919 Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Sat, 8 Feb 2025 12:18:48 +0100 Subject: [PATCH 09/12] Move gettext installation to separate PS1 file The linter does not understand windows powershell syntax. So we move it to the scripts folder. --- .github/workflows/test.yml | 4 +--- scripts/install-gettext-windows.ps1 | 10 ++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 scripts/install-gettext-windows.ps1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e43826..ecc0c68 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,9 +45,7 @@ jobs: - name: Install Windows dependencies if: startsWith(matrix.os, 'windows') run: | - nuget install Gettext.Tools -OutputDirectory c:\nuget; - $gettextPath = (Get-ChildItem -Path "C:\nuget" -Directory -Filter "Gettext.Tools.*" | Select-Object -First 1).FullName; - Add-Content $env:GITHUB_PATH "$gettextPath\tools\bin" + scripts/install-gettext-windows.ps1 - name: Install uv uses: hynek/setup-cached-uv@v2 diff --git a/scripts/install-gettext-windows.ps1 b/scripts/install-gettext-windows.ps1 new file mode 100644 index 0000000..6cb43e8 --- /dev/null +++ b/scripts/install-gettext-windows.ps1 @@ -0,0 +1,10 @@ +# This script is used by the Github Action to install gettext on the CI + +nuget install Gettext.Tools -OutputDirectory c:\nuget + +$gettextPath = ( + Get-ChildItem -Path "C:\nuget" -Directory -Filter "Gettext.Tools.*" | + Select-Object -First 1 +).FullName + +Add-Content $env:GITHUB_PATH "$gettextPath\tools\bin" From f7acee4df84e25875c71418bd68f7f5dd85f5b7f Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Wed, 12 Feb 2025 13:45:43 +0100 Subject: [PATCH 10/12] Remove Windows gettext binary installation Update the unittest to be skipped in case the translation binaries were missing. --- .github/workflows/test.yml | 14 +------------- scripts/generate-translation-binaries.ps1 | 6 ------ scripts/install-gettext-windows.ps1 | 10 ---------- tests/test_i18n.py | 17 +++++++++++------ 4 files changed, 12 insertions(+), 35 deletions(-) delete mode 100644 scripts/generate-translation-binaries.ps1 delete mode 100644 scripts/install-gettext-windows.ps1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ecc0c68..d44a6e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,13 +13,6 @@ jobs: matrix: python-version: ["pypy3.10", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [windows-latest, macos-latest, ubuntu-latest] - include: - - os: windows-latest - translation-script: generate-translation-binaries.ps1 - - os: macos-latest - translation-script: generate-translation-binaries.sh - - os: ubuntu-latest - translation-script: generate-translation-binaries.sh steps: - uses: actions/checkout@v4 @@ -42,17 +35,12 @@ jobs: run: | brew install gettext - - name: Install Windows dependencies - if: startsWith(matrix.os, 'windows') - run: | - scripts/install-gettext-windows.ps1 - - name: Install uv uses: hynek/setup-cached-uv@v2 - name: Generate translation binaries run: | - scripts/${{ matrix.translation-script }} + scripts/generate-translation-binaries.sh - name: Tox tests run: | diff --git a/scripts/generate-translation-binaries.ps1 b/scripts/generate-translation-binaries.ps1 deleted file mode 100644 index 0cf3cac..0000000 --- a/scripts/generate-translation-binaries.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -Get-ChildItem -Path "src\humanize\locale" -Directory | ForEach-Object { - $locale = $_.Name - Write-Host "$locale" - # compile to binary .mo - msgfmt --check -o "src\humanize\locale\$locale\LC_MESSAGES\humanize.mo" "src\humanize\locale\$locale\LC_MESSAGES\humanize.po" -} diff --git a/scripts/install-gettext-windows.ps1 b/scripts/install-gettext-windows.ps1 deleted file mode 100644 index 6cb43e8..0000000 --- a/scripts/install-gettext-windows.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -# This script is used by the Github Action to install gettext on the CI - -nuget install Gettext.Tools -OutputDirectory c:\nuget - -$gettextPath = ( - Get-ChildItem -Path "C:\nuget" -Directory -Filter "Gettext.Tools.*" | - Select-Object -First 1 -).FullName - -Add-Content $env:GITHUB_PATH "$gettextPath\tools\bin" diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 431082f..368b9a2 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -228,12 +228,17 @@ def test_en_locale(self, NOW: dt.datetime) -> None: def test_None_locale(self, NOW: dt.datetime) -> None: three_seconds = NOW - dt.timedelta(seconds=3) - humanize.i18n.activate("fr") - assert humanize.naturaltime(three_seconds) == "il y a 3 secondes" + try: + humanize.i18n.activate("fr") + assert humanize.naturaltime(three_seconds) == "il y a 3 secondes" - humanize.i18n.activate(None) - test_str = humanize.naturaltime(three_seconds) - assert test_str == "3 seconds ago" + humanize.i18n.activate(None) + test_str = humanize.naturaltime(three_seconds) + assert test_str == "3 seconds ago" + except FileNotFoundError: + pytest.skip("Generate .mo with scripts/generate-translation-binaries.sh") + + finally: + humanize.i18n.deactivate() - humanize.i18n.deactivate() assert test_str == humanize.naturaltime(three_seconds) From afc2a733ccfa2f63f075f50460d573c88597e1fc Mon Sep 17 00:00:00 2001 From: Daniel Gillet Date: Wed, 12 Feb 2025 21:27:43 +0100 Subject: [PATCH 11/12] Revert NOW fixture and use global NOW --- tests/test_i18n.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 368b9a2..b0a235a 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -4,32 +4,18 @@ import datetime as dt import importlib -from collections.abc import Generator import pytest from freezegun import freeze_time import humanize +with freeze_time("2020-02-02"): + NOW = dt.datetime.now(tz=dt.timezone.utc) -@pytest.fixture -def NOW() -> Generator[dt.datetime, None, None]: - """ - Using this fixture will give you a datetime frozen in 2020-02-02 00:00:00+00:00 - In a test using this fixture, further calls to datetime.now() or date.today() would - return also a frozen date / datetime in 2020-02-02. - - Yields: - Generator[datetime, None, None]: UTC aware datetime 2020-02-02 00:00:00+00:00 - """ - with freeze_time("2020-02-02"): - # TODO: Python 3.11+: replace dt.timezone.utc with dt.UTC - NOW = dt.datetime.now(tz=dt.timezone.utc) - yield NOW - - -def test_i18n(NOW: dt.datetime) -> None: +@freeze_time("2020-02-02") +def test_i18n() -> None: three_seconds = NOW - dt.timedelta(seconds=3) one_min_three_seconds = dt.timedelta(milliseconds=67_000) @@ -213,7 +199,8 @@ def test_default_locale_path_undefined__spec__( i18n.activate("ru_RU") assert str(excinfo.value) == self.expected_msg - def test_en_locale(self, NOW: dt.datetime) -> None: + @freeze_time("2020-02-02") + def test_en_locale(self) -> None: three_seconds = NOW - dt.timedelta(seconds=3) test_str = humanize.naturaltime(three_seconds) @@ -225,7 +212,8 @@ def test_en_locale(self, NOW: dt.datetime) -> None: humanize.i18n.deactivate() - def test_None_locale(self, NOW: dt.datetime) -> None: + @freeze_time("2020-02-02") + def test_None_locale(self) -> None: three_seconds = NOW - dt.timedelta(seconds=3) try: From 35562f82e08468a8aca990ac5c96e9eda7ef1976 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:22:06 +0200 Subject: [PATCH 12/12] Lowercase function name --- tests/test_i18n.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index b0a235a..83eecc8 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -213,7 +213,7 @@ def test_en_locale(self) -> None: humanize.i18n.deactivate() @freeze_time("2020-02-02") - def test_None_locale(self) -> None: + def test_none_locale(self) -> None: three_seconds = NOW - dt.timedelta(seconds=3) try: