diff --git a/backend/app/alembic/versions/c8e9f3a2d1b4_add_user_locale_preference_column.py b/backend/app/alembic/versions/c8e9f3a2d1b4_add_user_locale_preference_column.py index 1b83358..c9d94e4 100644 --- a/backend/app/alembic/versions/c8e9f3a2d1b4_add_user_locale_preference_column.py +++ b/backend/app/alembic/versions/c8e9f3a2d1b4_add_user_locale_preference_column.py @@ -23,10 +23,7 @@ def upgrade() -> None: # VARCHAR(10) supports BCP 47 format (e.g., "en", "it", "en-US", "it-IT") # Nullable: NULL means "not set yet", will use Accept-Language header fallback # Indexed: For analytics queries and filtering by locale - op.add_column( - "users", - sa.Column("locale", sa.String(length=10), nullable=True) - ) + op.add_column("users", sa.Column("locale", sa.String(length=10), nullable=True)) # Create index on locale column for performance op.create_index( diff --git a/backend/app/api/dependencies/locale.py b/backend/app/api/dependencies/locale.py index 7c3d25e..72b691f 100644 --- a/backend/app/api/dependencies/locale.py +++ b/backend/app/api/dependencies/locale.py @@ -117,8 +117,9 @@ async def get_locale( if current_user and current_user.locale: # Validate that saved locale is still supported # (in case SUPPORTED_LOCALES changed after user set preference) - if current_user.locale in SUPPORTED_LOCALES: - return current_user.locale + locale_value = str(current_user.locale) + if locale_value in SUPPORTED_LOCALES: + return locale_value # Priority 2: Accept-Language header accept_language = request.headers.get("accept-language", "") diff --git a/backend/app/schemas/users.py b/backend/app/schemas/users.py index 729c22d..1229954 100755 --- a/backend/app/schemas/users.py +++ b/backend/app/schemas/users.py @@ -40,9 +40,9 @@ class UserUpdate(BaseModel): locale: str | None = Field( None, max_length=10, - pattern=r'^[a-z]{2}(-[A-Z]{2})?$', + pattern=r"^[a-z]{2}(-[A-Z]{2})?$", description="User's preferred locale (BCP 47 format: en, it, en-US, it-IT)", - examples=["en", "it", "en-US", "it-IT"] + examples=["en", "it", "en-US", "it-IT"], ) is_active: bool | None = ( None # Changed default from True to None to avoid unintended updates @@ -70,12 +70,12 @@ class UserUpdate(BaseModel): return v # Only support English and Italian for template showcase # Note: Locales stored in lowercase for case-insensitive matching - SUPPORTED_LOCALES = {"en", "it", "en-us", "en-gb", "it-it"} + supported_locales = {"en", "it", "en-us", "en-gb", "it-it"} # Normalize to lowercase for comparison and storage v_lower = v.lower() - if v_lower not in SUPPORTED_LOCALES: + if v_lower not in supported_locales: raise ValueError( - f"Unsupported locale '{v}'. Supported locales: {sorted(SUPPORTED_LOCALES)}" + f"Unsupported locale '{v}'. Supported locales: {sorted(supported_locales)}" ) # Return normalized lowercase version for consistency return v_lower diff --git a/backend/tests/api/dependencies/test_locale_dependencies.py b/backend/tests/api/dependencies/test_locale_dependencies.py index fa8da4c..d10704b 100644 --- a/backend/tests/api/dependencies/test_locale_dependencies.py +++ b/backend/tests/api/dependencies/test_locale_dependencies.py @@ -67,9 +67,7 @@ class TestParseAcceptLanguage: def test_parse_complex_header(self): """Test complex Accept-Language header with multiple locales""" - result = parse_accept_language( - "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6" - ) + result = parse_accept_language("it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6") assert result == "it-it" def test_parse_whitespace_handling(self): @@ -199,9 +197,7 @@ class TestGetLocale: assert result == "en" @pytest.mark.asyncio - async def test_locale_from_accept_language_header( - self, async_user_without_locale - ): + async def test_locale_from_accept_language_header(self, async_user_without_locale): """Test locale detection from Accept-Language header when user has no preference""" # Mock request with Italian Accept-Language (it-IT has highest priority) mock_request = MagicMock() diff --git a/backend/tests/schemas/test_user_schemas.py b/backend/tests/schemas/test_user_schemas.py index a04dd33..16c5647 100755 --- a/backend/tests/schemas/test_user_schemas.py +++ b/backend/tests/schemas/test_user_schemas.py @@ -334,11 +334,7 @@ class TestLocaleValidation: def test_locale_in_user_update_with_other_fields(self): """Test locale validation works when combined with other fields""" # Valid locale with other fields - user = UserUpdate( - first_name="Mario", - last_name="Rossi", - locale="it" - ) + user = UserUpdate(first_name="Mario", last_name="Rossi", locale="it") assert user.locale == "it" assert user.first_name == "Mario" @@ -347,7 +343,7 @@ class TestLocaleValidation: UserUpdate( first_name="Pierre", last_name="Dupont", - locale="fr" # Unsupported + locale="fr", # Unsupported ) def test_supported_locales_list(self): @@ -357,7 +353,9 @@ class TestLocaleValidation: # Expected output (normalized to lowercase) expected_outputs = ["en", "it", "en-us", "en-gb", "it-it"] - for input_locale, expected_output in zip(input_locales, expected_outputs): + for input_locale, expected_output in zip( + input_locales, expected_outputs, strict=True + ): user = UserUpdate(locale=input_locale) assert user.locale == expected_output