Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .sampo/changesets/noble-stormcaller-lempo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
pypi/gt-fastapi: minor
pypi/gt-flask: minor
pypi/gt-i18n: minor
---

feat: add expected functionalities
13 changes: 11 additions & 2 deletions packages/gt-fastapi/src/gt_fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
"""FastAPI integration for General Translation."""

from gt_i18n import declare_static, declare_var, decode_vars, t
from gt_i18n import declare_static, declare_var, decode_vars, get_default_locale, get_locale, get_locales, t

from gt_fastapi._setup import initialize_gt

__all__ = ["initialize_gt", "t", "declare_var", "declare_static", "decode_vars"]
__all__ = [
"initialize_gt",
"t",
"declare_var",
"declare_static",
"decode_vars",
"get_locale",
"get_locales",
"get_default_locale",
]
41 changes: 5 additions & 36 deletions packages/gt-fastapi/src/gt_fastapi/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,23 @@
from contextlib import asynccontextmanager
from typing import Any

from generaltranslation import CustomMapping
from generaltranslation._settings import LIBRARY_DEFAULT_LOCALE
from generaltranslation.locales import determine_locale
from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401


def _detect_from_accept_language(request: Any, manager: I18nManager) -> str:
"""Parse Accept-Language header and resolve against configured locales."""
accept = request.headers.get("accept-language", "")
if not accept:
return manager.default_locale

locales: list[tuple[float, str]] = []
for part in accept.split(","):
part = part.strip()
if not part:
continue
if ";q=" in part:
lang, q = part.split(";q=", 1)
try:
quality = float(q.strip())
except ValueError:
quality = 0.0
locales.append((quality, lang.strip()))
else:
locales.append((1.0, part))

locales.sort(key=lambda x: x[0], reverse=True)
locale_list = [loc for _, loc in locales]

approved = manager.get_locales()
if not approved:
return locale_list[0] if locale_list else manager.default_locale

result = determine_locale(locale_list, approved)
return result or manager.default_locale
from gt_i18n import I18nManager, set_i18n_manager
from gt_i18n.internal import _detect_from_accept_language


def initialize_gt(
app: Any,
*,
default_locale: str = LIBRARY_DEFAULT_LOCALE,
locales: list[str] | None = None,
custom_mapping: CustomMapping | None = None,
project_id: str | None = None,
cache_url: str | None = None,
get_locale: Callable[..., str] | None = None,
load_translations: Callable[[str], dict[str, str]] | None = None,
eager_loading: bool = True,
**kwargs: Any,
) -> I18nManager:
"""Initialize General Translation for a FastAPI app.

Expand All @@ -74,10 +43,10 @@ def initialize_gt(
manager = I18nManager(
default_locale=default_locale,
locales=locales,
custom_mapping=custom_mapping,
project_id=project_id,
cache_url=cache_url,
load_translations=load_translations,
**kwargs,
)
set_i18n_manager(manager)

Expand Down
13 changes: 11 additions & 2 deletions packages/gt-flask/src/gt_flask/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
"""Flask integration for General Translation."""

from gt_i18n import t
from gt_i18n import declare_static, declare_var, decode_vars, get_default_locale, get_locale, get_locales, t

from gt_flask._setup import initialize_gt

__all__ = ["initialize_gt", "t"]
__all__ = [
"initialize_gt",
"t",
"declare_var",
"declare_static",
"decode_vars",
"get_locale",
"get_locales",
"get_default_locale",
]
43 changes: 5 additions & 38 deletions packages/gt-flask/src/gt_flask/_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,23 @@
from collections.abc import Callable
from typing import Any

from generaltranslation import CustomMapping
from generaltranslation._settings import LIBRARY_DEFAULT_LOCALE
from generaltranslation.locales import determine_locale
from gt_i18n import I18nManager, set_i18n_manager, t # noqa: F401


def _detect_from_accept_language(request: Any, manager: I18nManager) -> str:
"""Parse Accept-Language header and resolve against configured locales."""
accept = request.headers.get("Accept-Language", "")
if not accept:
return manager.default_locale

# Parse Accept-Language: e.g. "en-US,en;q=0.9,es;q=0.8"
locales: list[tuple[float, str]] = []
for part in accept.split(","):
part = part.strip()
if not part:
continue
if ";q=" in part:
lang, q = part.split(";q=", 1)
try:
quality = float(q.strip())
except ValueError:
quality = 0.0
locales.append((quality, lang.strip()))
else:
locales.append((1.0, part))

# Sort by quality descending
locales.sort(key=lambda x: x[0], reverse=True)
locale_list = [loc for _, loc in locales]

approved = manager.get_locales()
if not approved:
return locale_list[0] if locale_list else manager.default_locale

result = determine_locale(locale_list, approved)
return result or manager.default_locale
from gt_i18n import I18nManager, set_i18n_manager
from gt_i18n.internal import _detect_from_accept_language


def initialize_gt(
app: Any,
*,
default_locale: str = LIBRARY_DEFAULT_LOCALE,
locales: list[str] | None = None,
custom_mapping: CustomMapping | None = None,
project_id: str | None = None,
cache_url: str | None = None,
get_locale: Callable[..., str] | None = None,
load_translations: Callable[[str], dict[str, str]] | None = None,
eager_loading: bool = True,
**kwargs: Any,
) -> I18nManager:
"""Initialize General Translation for a Flask app.

Expand All @@ -76,10 +43,10 @@ def initialize_gt(
manager = I18nManager(
default_locale=default_locale,
locales=locales,
custom_mapping=custom_mapping,
project_id=project_id,
cache_url=cache_url,
load_translations=load_translations,
**kwargs,
)
set_i18n_manager(manager)

Expand Down
9 changes: 9 additions & 0 deletions packages/gt-i18n/src/gt_i18n/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

from generaltranslation.static import declare_static, declare_var, decode_vars

from gt_i18n.helpers._locales import (
get_default_locale,
get_locale,
get_locales,
)
from gt_i18n.i18n_manager import (
ContextVarStorageAdapter,
I18nManager,
Expand Down Expand Up @@ -40,6 +45,10 @@
"msg",
"t",
"t_fallback",
# Locale helpers
"get_locale",
"get_locales",
"get_default_locale",
# Static variable helpers
"declare_var",
"declare_static",
Expand Down
19 changes: 19 additions & 0 deletions packages/gt-i18n/src/gt_i18n/helpers/_locales.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from gt_i18n.i18n_manager._singleton import get_i18n_manager


def get_locale() -> str:
"""Get the current locale from the I18nManager."""
manager = get_i18n_manager()
return manager.get_locale()


def get_locales() -> list[str]:
"""Get the locales from the I18nManager."""
manager = get_i18n_manager()
return manager.get_locales()


def get_default_locale() -> str:
"""Get the default locale from the I18nManager."""
manager = get_i18n_manager()
return manager.default_locale
15 changes: 15 additions & 0 deletions packages/gt-i18n/src/gt_i18n/i18n_manager/_i18n_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from typing import TYPE_CHECKING

from generaltranslation import CustomMapping
from generaltranslation._gt import GT
from generaltranslation._settings import LIBRARY_DEFAULT_LOCALE
from generaltranslation.locales import requires_translation

Expand Down Expand Up @@ -37,13 +39,16 @@ def __init__(
locales: list[str] | None = None,
project_id: str | None = None,
cache_url: str | None = None,
custom_mapping: CustomMapping | None = None,
store_adapter: StorageAdapter | None = None,
load_translations: TranslationsLoader | None = None,
cache_expiry_time: int = 60_000,
) -> None:
self._default_locale = default_locale
self._locales = locales or []
self._project_id = project_id
self._cache_url = cache_url
self._custom_mapping = custom_mapping

# Storage
self._store: StorageAdapter = store_adapter or ContextVarStorageAdapter()
Expand All @@ -66,6 +71,16 @@ def __init__(
def default_locale(self) -> str:
return self._default_locale

def get_gt_instance(self) -> GT:
"""Get a new GT instance for the current request."""
return GT(
project_id=self._project_id,
source_locale=self._default_locale,
target_locale=self.get_locale(),
locales=self._locales,
custom_mapping=self._custom_mapping,
)

def get_locales(self) -> list[str]:
return list(self._locales)

Expand Down
5 changes: 5 additions & 0 deletions packages/gt-i18n/src/gt_i18n/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gt_i18n.internal._detect_from_accept_language import _detect_from_accept_language

__all__ = [
"_detect_from_accept_language",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any

from gt_i18n.i18n_manager._i18n_manager import I18nManager


def _detect_from_accept_language(request: Any, manager: I18nManager) -> str:
"""Default, Parse Accept-Language header and resolve against configured locales."""
accept = request.headers.get("Accept-Language", "")
if not accept:
return manager.default_locale

# Parse Accept-Language: e.g. "en-US,en;q=0.9,es;q=0.8"
locales: list[tuple[float, str]] = []
for part in accept.split(","):
part = part.strip()
if not part:
continue
if ";q=" in part:
lang, q = part.split(";q=", 1)
try:
quality = float(q.strip())
except ValueError:
quality = 0.0
locales.append((quality, lang.strip()))
else:
locales.append((1.0, part))

# Sort by quality descending
locales.sort(key=lambda x: x[0], reverse=True)
locale_list = [loc for _, loc in locales]

# Determine the best matching locale
gt = manager.get_gt_instance()
result = gt.determine_locale(locale_list)
return result or manager.default_locale
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ select = ["E", "F", "I", "N", "W", "UP"]
[tool.ruff.lint.per-file-ignores]
"**/tests/**" = ["E501"]

[tool.basedpyright]
typeCheckingMode = "standard"
pythonVersion = "3.10"
exclude = ["examples/"]

[tool.mypy]
python_version = "3.10"
warn_return_any = false
Expand Down