Add user locale preference support and locale detection logic

- Introduced `locale` field in user model and schemas with BCP 47 format validation.
- Created Alembic migration to add `locale` column to the `users` table with indexing for better query performance.
- Implemented `get_locale` dependency to detect locale using user preference, `Accept-Language` header, or default to English.
- Added extensive tests for locale validation, dependency logic, and fallback handling.
- Enhanced documentation and comments detailing the locale detection workflow and SUPPORTED_LOCALES configuration.
This commit is contained in:
Felipe Cardoso
2025-11-17 19:47:50 +01:00
parent 3001484948
commit 68e04a911a
6 changed files with 665 additions and 1 deletions

View File

@@ -37,6 +37,13 @@ class UserUpdate(BaseModel):
phone_number: str | None = None
password: str | None = None
preferences: dict[str, Any] | None = None
locale: str | None = Field(
None,
max_length=10,
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"]
)
is_active: bool | None = (
None # Changed default from True to None to avoid unintended updates
)
@@ -55,6 +62,24 @@ class UserUpdate(BaseModel):
return v
return validate_password_strength(v)
@field_validator("locale")
@classmethod
def validate_locale(cls, v: str | None) -> str | None:
"""Validate locale against supported locales."""
if v is None:
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"}
# Normalize to lowercase for comparison and storage
v_lower = v.lower()
if v_lower not in SUPPORTED_LOCALES:
raise ValueError(
f"Unsupported locale '{v}'. Supported locales: {sorted(SUPPORTED_LOCALES)}"
)
# Return normalized lowercase version for consistency
return v_lower
@field_validator("is_superuser")
@classmethod
def prevent_superuser_modification(cls, v: bool | None) -> bool | None:
@@ -70,6 +95,7 @@ class UserInDB(UserBase):
is_superuser: bool
created_at: datetime
updated_at: datetime | None = None
locale: str | None = None
model_config = ConfigDict(from_attributes=True)
@@ -80,6 +106,7 @@ class UserResponse(UserBase):
is_superuser: bool
created_at: datetime
updated_at: datetime | None = None
locale: str | None = None
model_config = ConfigDict(from_attributes=True)