From 41a733b777440cb460a6bbadf71056e0f279e00b Mon Sep 17 00:00:00 2001 From: Mathijs Miermans Date: Tue, 17 Dec 2024 07:55:59 -0800 Subject: [PATCH 1/2] [HNT-295] fix section date locations --- .../curated_recommendations/localization.py | 29 +++++++++++++++++++ merino/curated_recommendations/provider.py | 14 ++++++--- poetry.lock | 18 ++++++++++-- pyproject.toml | 1 + .../test_curated_recommendations.py | 9 ++++-- .../test_localization.py | 23 ++++++++++++++- 6 files changed, 84 insertions(+), 10 deletions(-) diff --git a/merino/curated_recommendations/localization.py b/merino/curated_recommendations/localization.py index 986c4b3a..968520e5 100644 --- a/merino/curated_recommendations/localization.py +++ b/merino/curated_recommendations/localization.py @@ -1,5 +1,9 @@ """Hardcoded localized strings.""" +from datetime import datetime + +from babel.dates import format_date + from merino.curated_recommendations.corpus_backends.protocol import ScheduledSurfaceId import logging @@ -72,3 +76,28 @@ def get_translation(surface_id: ScheduledSurfaceId, topic: str, default_topic: s return default_topic return translations[topic] + + +# Localization for Babel date formats based on surface_id. +SURFACE_ID_TO_LOCALE = { + ScheduledSurfaceId.NEW_TAB_EN_US: "en_US", + ScheduledSurfaceId.NEW_TAB_EN_GB: "en_GB", + ScheduledSurfaceId.NEW_TAB_DE_DE: "de_DE", + ScheduledSurfaceId.NEW_TAB_FR_FR: "fr_FR", + ScheduledSurfaceId.NEW_TAB_ES_ES: "es_ES", + ScheduledSurfaceId.NEW_TAB_IT_IT: "it_IT", + ScheduledSurfaceId.NEW_TAB_EN_INTL: "en_IN", # En-Intl is primarily used in India. +} + + +def get_localized_date(surface_id: ScheduledSurfaceId, date: datetime) -> str: + """Return a localized date string for the given ScheduledSurfaceId. + + Args: + surface_id (ScheduledSurfaceId): The New Tab surface ID. + date (datetime): The date to be localized. + + Returns: + str: Localized date string, for example "December 17, 2024". + """ + return format_date(date, format="long", locale=SURFACE_ID_TO_LOCALE.get(surface_id, "en_US")) diff --git a/merino/curated_recommendations/provider.py b/merino/curated_recommendations/provider.py index 4639554c..c47e6a46 100644 --- a/merino/curated_recommendations/provider.py +++ b/merino/curated_recommendations/provider.py @@ -3,10 +3,9 @@ import logging import time import re -from datetime import datetime from typing import cast -from merino.curated_recommendations import ExtendedExpirationCorpusBackend +from merino.curated_recommendations import ExtendedExpirationCorpusBackend, CorpusApiBackend from merino.curated_recommendations.corpus_backends.protocol import ( CorpusBackend, ScheduledSurfaceId, @@ -21,7 +20,11 @@ layout_4_large, layout_6_tiles, ) -from merino.curated_recommendations.localization import get_translation, LOCALIZED_SECTION_TITLES +from merino.curated_recommendations.localization import ( + get_translation, + LOCALIZED_SECTION_TITLES, + get_localized_date, +) from merino.curated_recommendations.prior_backends.protocol import PriorBackend from merino.curated_recommendations.protocol import ( Locale, @@ -358,12 +361,15 @@ async def get_sections( remaining_recs = general_recs[top_stories_count:] # Create "Today's top stories" section with the first 6 recommendations + surface_date = CorpusApiBackend.get_scheduled_surface_date( + CorpusApiBackend.get_surface_timezone(surface_id) + ) feeds = CuratedRecommendationsFeed( news_section=Section( receivedFeedRank=0, recommendations=news_recs, title=get_translation(surface_id, "news-section", "In the News"), - subtitle=datetime.now().strftime("%B %d, %Y"), # e.g., "October 24, 2024" + subtitle=get_localized_date(surface_id, surface_date), # e.g., "October 24, 2024" layout=layout_4_large, ), top_stories_section=Section( diff --git a/poetry.lock b/poetry.lock index e8bf72dd..7790faa2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiodogstatsd" @@ -219,6 +219,20 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "backoff" version = "2.2.1" @@ -4325,4 +4339,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f2c1b934c19dc93b60327f5be32db8e158d71d3c26e1f6029be5cfe3be4bbeae" +content-hash = "a3fe3f7c88bd885a10e89ef77b918384d2a14cc6513b807638e0d6fe0132ddf0" diff --git a/pyproject.toml b/pyproject.toml index 0be5c0a1..dbd4261a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ pydantic = "^2.1.0" scipy = "^1.14.1" orjson = "^3.10.7" tenacity = "^9.0.0" +babel = "^2.16.0" [tool.poetry.group.jobs.dependencies] # Jobs specific dependecies required on top of some of the main dependencies above diff --git a/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py b/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py index 1c425db9..f3b79eeb 100644 --- a/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py +++ b/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py @@ -1446,7 +1446,7 @@ async def test_curated_recommendations_with_sections_feed_boost_followed_section @pytest.mark.asyncio @pytest.mark.parametrize( - "locale, expected_titles", + "locale, expected_titles, expected_news_subtitle", [ ( "en-US", @@ -1457,6 +1457,7 @@ async def test_curated_recommendations_with_sections_feed_boost_followed_section "education": "Education", "sports": "Sports", }, + "December 17, 2024", # Frozen time 2024-12-18 6am UTC = 2024-12-17 10pm PST ), ( "de-DE", @@ -1467,10 +1468,12 @@ async def test_curated_recommendations_with_sections_feed_boost_followed_section "education": "Bildung", "sports": "Sport", }, + "18. Dezember 2024", # Frozen time 2024-12-18 6am UTC = 2024-12-17 7am CET ), ], ) - async def test_sections_feed_titles(self, locale, expected_titles): + @freezegun.freeze_time("2024-12-18 06:00:00", tz_offset=0) + async def test_sections_feed_titles(self, locale, expected_titles, expected_news_subtitle): """Test the curated recommendations endpoint 'sections' have the expected (sub)titles.""" async with AsyncClient(app=app, base_url="http://test") as ac: # Mock the endpoint to request the sections feed @@ -1495,7 +1498,7 @@ async def test_sections_feed_titles(self, locale, expected_titles): # Ensure "Today's top stories" is present with a valid date subtitle news_section = data["feeds"].get("news_section") assert news_section is not None - assert news_section["subtitle"] is not None + assert news_section["subtitle"] == expected_news_subtitle class TestExtendedExpiration: diff --git a/tests/unit/curated_recommendations/test_localization.py b/tests/unit/curated_recommendations/test_localization.py index 3c93cb3a..76577221 100644 --- a/tests/unit/curated_recommendations/test_localization.py +++ b/tests/unit/curated_recommendations/test_localization.py @@ -1,7 +1,11 @@ """Tests covering merino/curated_recommendations/localization.py""" -from merino.curated_recommendations.localization import get_translation +from datetime import datetime + +import pytest + from merino.curated_recommendations.corpus_backends.protocol import ScheduledSurfaceId +from merino.curated_recommendations.localization import get_translation, get_localized_date def test_get_translation_existing_translation(): @@ -28,3 +32,20 @@ def test_get_translation_non_existing_slug(caplog): errors = [r for r in caplog.records if r.levelname == "ERROR"] assert len(errors) == 1 assert "Missing or empty translation for topic" in errors[0].message + + +@pytest.mark.parametrize( + "surface_id, expected_output", + [ + (ScheduledSurfaceId.NEW_TAB_EN_US, "December 17, 2024"), + (ScheduledSurfaceId.NEW_TAB_EN_GB, "17 December 2024"), + (ScheduledSurfaceId.NEW_TAB_DE_DE, "17. Dezember 2024"), + (ScheduledSurfaceId.NEW_TAB_FR_FR, "17 décembre 2024"), + (ScheduledSurfaceId.NEW_TAB_ES_ES, "17 de diciembre de 2024"), + (ScheduledSurfaceId.NEW_TAB_IT_IT, "17 dicembre 2024"), + (ScheduledSurfaceId.NEW_TAB_EN_INTL, "17 December 2024"), + ], +) +def test_get_localized_date_output(surface_id: ScheduledSurfaceId, expected_output: str): + """Test that get_localized_date returns correctly formatted date strings.""" + assert get_localized_date(surface_id, date=datetime(2024, 12, 17)) == expected_output From 0bf156641c8badc34bf2d8ffa0f1f086618fa730 Mon Sep 17 00:00:00 2001 From: Mathijs Miermans Date: Tue, 7 Jan 2025 11:15:38 +0100 Subject: [PATCH 2/2] fix comment --- .../v1/curated_recommendations/test_curated_recommendations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py b/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py index f3b79eeb..bfb9477f 100644 --- a/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py +++ b/tests/integration/api/v1/curated_recommendations/test_curated_recommendations.py @@ -1468,7 +1468,7 @@ async def test_curated_recommendations_with_sections_feed_boost_followed_section "education": "Bildung", "sports": "Sport", }, - "18. Dezember 2024", # Frozen time 2024-12-18 6am UTC = 2024-12-17 7am CET + "18. Dezember 2024", # Frozen time 2024-12-18 6am UTC = 2024-12-18 7am CET ), ], )