Skip to content

Support Explicitly Excluding Locales From Smartling Sync#65

Merged
stevejalim merged 1 commit intomozilla:mainfrom
dchukhin:support-excluded-locales
Mar 12, 2026
Merged

Support Explicitly Excluding Locales From Smartling Sync#65
stevejalim merged 1 commit intomozilla:mainfrom
dchukhin:support-excluded-locales

Conversation

@dchukhin
Copy link
Contributor

@dchukhin dchukhin commented Mar 5, 2026

This pull request makes it possible to explicitly exclude locales from syncing to Smartling by setting EXCLUDE_LOCALES in the WAGTAIL_LOCALIZE_SMARTLING setting.
Any locales defined in Job.get_or_create_from_source_and_translation_data() are excluded prior to looking for existing jobs or creating a new job.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new WAGTAIL_LOCALIZE_SMARTLING["EXCLUDE_LOCALES"] setting to prevent specific Wagtail locales from entering the Smartling job pipeline, and validates/uses it during job creation.

Changes:

  • Introduces EXCLUDE_LOCALES on SmartlingSettings with validation against WAGTAIL_CONTENT_LANGUAGES.
  • Filters excluded-locale translations inside Job.get_or_create_from_source_and_translation_data() (including a no-op return when all translations are excluded).
  • Adds unit tests and README documentation for the new setting.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/wagtail_localize_smartling/settings.py Adds EXCLUDE_LOCALES setting + validation during settings initialization.
src/wagtail_localize_smartling/models.py Filters out excluded locales before job lookup/creation.
tests/test_settings.py Adds settings validation tests for EXCLUDE_LOCALES.
tests/test_models.py Adds job-creation behavior tests for excluded locales.
README.md Documents how to configure EXCLUDE_LOCALES and what it does.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +199 to +200
f"{setting_name}['EXCLUDE_LOCALES'] must be a list, tuple, or set "
f"of locale code strings"
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message here says EXCLUDE_LOCALES must be a "list, tuple, or set", but the code also accepts a frozenset. Update the message to match the accepted types (and consider saying it must be an iterable of locale code strings).

Suggested change
f"{setting_name}['EXCLUDE_LOCALES'] must be a list, tuple, or set "
f"of locale code strings"
f"{setting_name}['EXCLUDE_LOCALES'] must be an iterable "
f"(list, tuple, set, or frozenset) of locale code strings"

Copilot uses AI. Check for mistakes.
Comment on lines +202 to +214
valid_locale_codes = {
code
for code, _ in getattr(
django_settings, "WAGTAIL_CONTENT_LANGUAGES", []
)
}
invalid_codes = set(exclude) - valid_locale_codes
if invalid_codes:
raise ImproperlyConfigured(
f"{setting_name}['EXCLUDE_LOCALES'] contains locale codes not in "
f"WAGTAIL_CONTENT_LANGUAGES: {sorted(invalid_codes)}"
)
settings_kwargs["EXCLUDE_LOCALES"] = frozenset(exclude)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalid_codes = set(exclude) - valid_locale_codes will raise a TypeError if EXCLUDE_LOCALES contains any unhashable items (e.g. a nested list) and currently doesn't validate that each entry is a string. Consider explicitly validating each element is a str (and rejecting non-hashable values) so misconfiguration consistently raises ImproperlyConfigured with a clear message instead of crashing during settings init.

Suggested change
valid_locale_codes = {
code
for code, _ in getattr(
django_settings, "WAGTAIL_CONTENT_LANGUAGES", []
)
}
invalid_codes = set(exclude) - valid_locale_codes
if invalid_codes:
raise ImproperlyConfigured(
f"{setting_name}['EXCLUDE_LOCALES'] contains locale codes not in "
f"WAGTAIL_CONTENT_LANGUAGES: {sorted(invalid_codes)}"
)
settings_kwargs["EXCLUDE_LOCALES"] = frozenset(exclude)
# Validate that each entry is a string so we can safely use it in a set
exclude_codes: list[str] = []
for value in exclude:
if not isinstance(value, str):
raise ImproperlyConfigured(
f"{setting_name}['EXCLUDE_LOCALES'] must contain only strings; "
f"got {value!r} (type {type(value).__name__})"
)
exclude_codes.append(value)
valid_locale_codes = {
code
for code, _ in getattr(
django_settings, "WAGTAIL_CONTENT_LANGUAGES", []
)
}
invalid_codes = set(exclude_codes) - valid_locale_codes
if invalid_codes:
raise ImproperlyConfigured(
f"{setting_name}['EXCLUDE_LOCALES'] contains locale codes not in "
f"WAGTAIL_CONTENT_LANGUAGES: {sorted(invalid_codes)}"
)
settings_kwargs["EXCLUDE_LOCALES"] = frozenset(exclude_codes)

Copilot uses AI. Check for mistakes.
Comment on lines +373 to +380
excluded_translations = [t for t in translations_list if t.target_locale.language_code in excluded]
if excluded_translations:
logger.info(
"Excluding %d translation(s) for locales: %s",
len(excluded_translations),
", ".join(t.target_locale.language_code for t in excluded_translations),
)
translations_list = [t for t in translations_list if t.target_locale.language_code not in excluded]
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filtering/logging uses t.target_locale.language_code, which will trigger an extra DB query per Translation if translations is a queryset without select_related('target_locale') (e.g. job.translations.all() in the resubmit view). Consider ensuring locales are fetched in bulk (e.g. select_related when possible) or restructuring to use target_locale_id plus a single Locale lookup, to avoid N+1 queries when excluding locales.

Suggested change
excluded_translations = [t for t in translations_list if t.target_locale.language_code in excluded]
if excluded_translations:
logger.info(
"Excluding %d translation(s) for locales: %s",
len(excluded_translations),
", ".join(t.target_locale.language_code for t in excluded_translations),
)
translations_list = [t for t in translations_list if t.target_locale.language_code not in excluded]
# Bulk-load locales to avoid N+1 queries on t.target_locale
target_locale_ids = {
t.target_locale_id for t in translations_list if getattr(t, "target_locale_id", None)
}
locales_by_id = Locale.objects.in_bulk(target_locale_ids) if target_locale_ids else {}
def _is_excluded(translation: Translation) -> bool:
locale = locales_by_id.get(getattr(translation, "target_locale_id", None))
return bool(locale and locale.language_code in excluded)
excluded_translations = [t for t in translations_list if _is_excluded(t)]
if excluded_translations:
excluded_language_codes = sorted(
{
locales_by_id[t.target_locale_id].language_code
for t in excluded_translations
if getattr(t, "target_locale_id", None) in locales_by_id
}
)
logger.info(
"Excluding %d translation(s) for locales: %s",
len(excluded_translations),
", ".join(excluded_language_codes),
)
translations_list = [t for t in translations_list if not _is_excluded(t)]

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@stevejalim stevejalim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with comments (r+wc)

@dchukhin I'm going to merge this now, to get it out sooner rather than later, but take a look at the optimisation comments from Copilot and if you agree they're worth it, feel free to follow up with another PR.

Thanks for sorting this!

len(excluded_translations),
", ".join(t.target_locale.language_code for t in excluded_translations),
)
translations_list = [t for t in translations_list if t.target_locale.language_code not in excluded]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimisation nit, nonblocking: could this be done more efficiently with a set.difference, I'm wondering

@stevejalim stevejalim merged commit a75aae2 into mozilla:main Mar 12, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants