Skip to content

Commit

Permalink
Add settings for locale mapping (#6)
Browse files Browse the repository at this point in the history
* Add callback setting for mapping locales
* Handle plain dict for locale mapping
  • Loading branch information
zerolab authored Jul 2, 2024
1 parent 7c1c94b commit 668bb56
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 7 deletions.
21 changes: 17 additions & 4 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
[run]
branch = True
include = wagtail_localize_smartling/*
include = src/wagtail_localize_smartling/*
omit = */migrations/*,*/tests/*

[paths]
source =
src/wagtail_localize_smartling
.tox/py*/**/site-packages

[report]
show_missing = True
ignore_errors = True
skip_covered = True

# Regexes for lines to exclude from consideration
exclude_lines =
exclude_also =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain about missing debug-only code:
def __repr__
if self\.debug
if self.debug
if settings.DEBUG

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
Expand All @@ -21,4 +31,7 @@ exclude_lines =
if 0:
if __name__ == .__main__.:

ignore_errors = True
# Nor complain about type checking
"if TYPE_CHECKING:",
class .*\bProtocol\):
@(abc\.)?abstractmethod
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,35 @@ integrates with the Smartling translation platform.
}
```

If your project locales do not match those in Smartling (e.g. `ro` in your project, `ro-RO` in Smartling),
then you can provide a Wagtail locale id to Smartling locale id mapping via the `LOCALE_TO_SMARTLING_LOCALE` setting:

```python
WAGTAIL_LOCALIZE_SMARTLING = {
"LOCALE_TO_SMARTLING_LOCALE": {
"ro": "ro-RO"
}
}
```

or you can specify a callable or a dotted path to a callable in the `LOCALE_MAPPING_CALLBACK` setting

```python
def map_project_locale_to_smartling(locale: str) -> str:
if locale == "ro":
return "ro-RO"
return locale
WAGTAIL_LOCALIZE_SMARTLING = {
# ...
"LOCALE_MAPPING_CALLBACK": "settings.map_project_locale_to_smartling"
}
```
The callback receives a `WAGTAIL_CONTENT_LANGUAGES` local code string and is expected to return
a valid mapped locale id (or the original locale id).
4. Run migrations:
```sh
Expand Down
54 changes: 53 additions & 1 deletion src/wagtail_localize_smartling/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.conf import settings as django_settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import SimpleLazyObject
from django.utils.module_loading import import_string


logger = logging.getLogger(__name__)
Expand All @@ -19,6 +20,12 @@ class SmartlingSettings:
REQUIRED: bool = False
ENVIRONMENT: Literal["production", "staging"] = "production"
API_TIMEOUT_SECONDS: float = 5.0
LOCALE_TO_SMARTLING_LOCALE: "dict[str, str]" = dataclasses.field(
default_factory=dict
)
SMARTLING_LOCALE_TO_LOCALE: "dict[str, str]" = dataclasses.field(
default_factory=dict
)


def _init_settings() -> SmartlingSettings:
Expand Down Expand Up @@ -68,12 +75,57 @@ def _init_settings() -> SmartlingSettings:
f"{setting_name}['API_TIMEOUT_SECONDS'] must be a number"
) from e

if (api_timeout_seconds := settings_dict["API_TIMEOUT_SECONDS"]) <= 0:
if api_timeout_seconds <= 0:
raise ImproperlyConfigured(
f"{setting_name}['API_TIMEOUT_SECONDS'] must be a positive number"
)
settings_kwargs["API_TIMEOUT_SECONDS"] = api_timeout_seconds

if (
"LOCALE_MAPPING_CALLBACK" in settings_dict
and "LOCALE_TO_SMARTLING_LOCALE" in settings_dict
):
raise ImproperlyConfigured(
f"{setting_name} cannot have both LOCALE_MAPPING_CALLBACK "
f"and LOCALE_TO_SMARTLING_LOCALE"
)

if "LOCALE_MAPPING_CALLBACK" in settings_dict:
func_or_path = settings_dict["LOCALE_MAPPING_CALLBACK"]
if isinstance(func_or_path, str):
func_or_path = import_string(func_or_path)

LOCALE_TO_SMARTLING_LOCALE: dict[str, str] = {}
SMARTLING_LOCALE_TO_LOCALE: dict[str, str] = {}

for locale_id, _locale in getattr(
django_settings, "WAGTAIL_CONTENT_LANGUAGES", []
):
if mapped_locale_id := func_or_path(locale_id):
LOCALE_TO_SMARTLING_LOCALE[locale_id] = mapped_locale_id
SMARTLING_LOCALE_TO_LOCALE[mapped_locale_id] = locale_id

settings_kwargs["LOCALE_TO_SMARTLING_LOCALE"] = LOCALE_TO_SMARTLING_LOCALE
settings_kwargs["SMARTLING_LOCALE_TO_LOCALE"] = SMARTLING_LOCALE_TO_LOCALE

elif "LOCALE_TO_SMARTLING_LOCALE" in settings_dict:
if not isinstance(settings_dict["LOCALE_TO_SMARTLING_LOCALE"], dict):
raise ImproperlyConfigured(
f"{setting_name}['LOCALE_TO_SMARTLING_LOCALE'] must be a dictionary "
f"with the Wagtail locale id as key and the Smartling locale as value"
)
LOCALE_TO_SMARTLING_LOCALE = settings_dict["LOCALE_TO_SMARTLING_LOCALE"].copy()
for locale_id, _locale in getattr(
django_settings, "WAGTAIL_CONTENT_LANGUAGES", []
):
if locale_id not in LOCALE_TO_SMARTLING_LOCALE:
LOCALE_TO_SMARTLING_LOCALE[locale_id] = locale_id

settings_kwargs["LOCALE_TO_SMARTLING_LOCALE"] = LOCALE_TO_SMARTLING_LOCALE
settings_kwargs["SMARTLING_LOCALE_TO_LOCALE"] = {
v: k for k, v in LOCALE_TO_SMARTLING_LOCALE.items()
}

return SmartlingSettings(**settings_kwargs)


Expand Down
25 changes: 23 additions & 2 deletions src/wagtail_localize_smartling/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import utils
from .api.client import client
from .api.types import JobStatus
from .settings import settings as smartling_settings
from .signals import translation_imported


Expand Down Expand Up @@ -91,6 +92,15 @@ def _initial_sync(job: "Job") -> None:
)
]

# Apply any custom mapping defined in settings
if smartling_settings.LOCALE_TO_SMARTLING_LOCALE:
target_locale_ids = [
smartling_settings.LOCALE_TO_SMARTLING_LOCALE.get(
target_locale_id, target_locale_id
)
for target_locale_id in target_locale_ids
]

# TODO validate target_locale_ids against the Project's target locales

job_data = client.create_job(
Expand Down Expand Up @@ -184,15 +194,26 @@ def _download_and_apply_translations(job: "Job") -> None:
f"File URI mismatch: expected {job.file_uri}, got {file_uri}"
)

if (
mapped_locale_id := smartling_settings.SMARTLING_LOCALE_TO_LOCALE.get(
smartling_locale_id
)
) is None:
logger.error(
"Cannot match Smartling locale %s to configured locales, skipping",
smartling_locale_id,
)
continue

try:
translation: Translation = job.translations.get(
target_locale__language_code=utils.format_wagtail_locale_id(
smartling_locale_id
mapped_locale_id
)
)
except job.translations.model.DoesNotExist:
logger.error(
"Translation not found for locale %s, skipping", smartling_locale_id
"Translation not found for locale %s, skipping", mapped_locale_id
)
continue

Expand Down
6 changes: 6 additions & 0 deletions testapp/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,9 @@
"USER_SECRET": os.getenv("SMARTLING_USER_SECRET", "test-user-secret"),
"REQUIRED": os.getenv("SMARTLING_REQUIRED", "false").lower() == "true",
}


def map_project_locale_to_smartling(locale: str) -> str:
if locale == "fr":
return "fr-FR"
return locale
119 changes: 119 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import pytest

from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings
from wagtail_localize_smartling.settings import _init_settings


pytestmark = pytest.mark.django_db

REQUIRED_SETTINGS = {
"PROJECT_ID": "test_project_id",
"USER_IDENTIFIER": "test_user_identifier",
"USER_SECRET": "test_user_secret",
}


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"REQUIRED": True,
"ENVIRONMENT": "staging",
"API_TIMEOUT_SECONDS": 10.0,
}
)
def test_settings():
smartling_settings = _init_settings()
assert smartling_settings.REQUIRED is True
assert smartling_settings.ENVIRONMENT == "staging"
assert smartling_settings.API_TIMEOUT_SECONDS == 10.0


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
"PROJECT_ID": "",
"USER_IDENTIFIER": "",
"USER_SECRET": "",
}
)
def test_missing_required_fields(settings):
with pytest.raises(ImproperlyConfigured):
_init_settings()


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"ENVIRONMENT": "invalid_env",
}
)
def test_invalid_environment_value(settings):
with pytest.raises(ImproperlyConfigured):
_init_settings()


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"API_TIMEOUT_SECONDS": "non_numeric_value",
}
)
def test_non_numeric_api_timeout_seconds(settings):
from django.core.exceptions import ImproperlyConfigured

with pytest.raises(ImproperlyConfigured):
_init_settings()


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"LOCALE_MAPPING_CALLBACK": lambda x: x,
"LOCALE_TO_SMARTLING_LOCALE": {"ro": "ro-RO"},
}
)
def test_cannot_have_callback_and_mapping_dict(settings):
with pytest.raises(ImproperlyConfigured):
_init_settings()


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"LOCALE_MAPPING_CALLBACK": "testapp.settings.map_project_locale_to_smartling",
}
)
def test_locale_mapping_callback():
smartling_settings = _init_settings()
assert smartling_settings.LOCALE_TO_SMARTLING_LOCALE == {
"de": "de",
"en": "en",
"fr": "fr-FR",
}
assert smartling_settings.SMARTLING_LOCALE_TO_LOCALE == {
"de": "de",
"en": "en",
"fr-FR": "fr",
}


@override_settings(
WAGTAIL_LOCALIZE_SMARTLING={
**REQUIRED_SETTINGS,
"LOCALE_TO_SMARTLING_LOCALE": {"ro": "ro-RO"},
}
)
def test_locale_map_dict():
smartling_settings = _init_settings()
assert smartling_settings.LOCALE_TO_SMARTLING_LOCALE == {
"de": "de",
"en": "en",
"fr": "fr",
"ro": "ro-RO",
}
assert smartling_settings.SMARTLING_LOCALE_TO_LOCALE == {
"de": "de",
"en": "en",
"fr": "fr",
"ro-RO": "ro",
}

0 comments on commit 668bb56

Please sign in to comment.