Skip to content
Open
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
5 changes: 4 additions & 1 deletion pontoon/administration/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ class ProjectForm(forms.ModelForm):

def clean(self):
cleaned_data = super().clean()
set_locales_from_repo = cleaned_data.get("set_locales_from_repo")
locales_readonly = cleaned_data.get("locales_readonly")
locales = cleaned_data.get("locales")
if not (locales or locales_readonly):
if not (set_locales_from_repo or locales or locales_readonly):
raise ValidationError("At least one locale must be selected.")

class Meta:
Expand All @@ -61,6 +62,8 @@ class Meta:
"sync_disabled",
"tags_enabled",
"pretranslation_enabled",
"set_locales_from_repo",
"set_translated_resources_from_repo",
"visibility",
)

Expand Down
8 changes: 8 additions & 0 deletions pontoon/administration/static/css/admin_project.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,20 @@ form a:visited {
margin: 5px 9px;
}

.controls .save {
width: 150px !important;
}

.controls .checkbox {
float: left;
margin: -1px 20px 0 0;
text-transform: uppercase;
}

.locales {
margin-top: 40px;
}

.double-list-selector .locale.select .menu {
background: transparent;
border-bottom: 1px solid var(--main-border-1);
Expand Down
51 changes: 49 additions & 2 deletions pontoon/administration/static/js/admin_project.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ $(function () {

button.addClass('in-progress').html('Syncing...');

const slug = $('#id_slug').val();
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/sync/',
url: `/admin/projects/${slug}/sync/`,
success: function () {
button.html('Started');
},
Expand Down Expand Up @@ -105,8 +106,9 @@ $(function () {

button.addClass('in-progress').html('Pretranslating...');

const slug = $('#id_slug').val();
$.ajax({
url: '/admin/projects/' + $('#id_slug').val() + '/pretranslate/',
url: `/admin/projects/${slug}/pretranslate/`,
success: function () {
button.html('Started');
},
Expand Down Expand Up @@ -166,6 +168,51 @@ $(function () {
});
});

const setLocalesCheckbox = $('#id_set_locales_from_repo');
const configFileInput = $('#id_configuration_file');
function setLocalesUpdate() {
const configFile = configFileInput.val();
let reqSync = configFile !== configFileInput.data('init');

const controls = $('.repo-locale-control');
if ($('#id_data_source').val() === 'repository') {
controls.show();
} else {
controls.hide();
setLocalesCheckbox.prop('checked', false);
reqSync = false;
}

const label = $('#id_set_locales_from_repo + span');
const hint = $('#hint_set_locales_from_repo');
if (configFile) {
label.text('Read locales from configuration file');
hint.hide();
} else {
label.text('Set locales from repository');
hint.show();
}

const setLocalesFromRepo = setLocalesCheckbox.prop('checked');
const trDiv = $('#translated_resources_from_repo');
const locales = $('.locales, .locales-toolbar');
if (setLocalesFromRepo) {
trDiv.toggle(!configFile);
locales.hide();
reqSync ||= setLocalesFromRepo !== setLocalesCheckbox.data('init');
} else {
trDiv.hide();
locales.show();
}

$('button.save').text(reqSync ? 'Save & sync project' : 'Save project');
}
setLocalesCheckbox.data('init', setLocalesCheckbox.prop('checked'));
setLocalesCheckbox.on('change', setLocalesUpdate);
configFileInput.data('init', configFileInput.val());
configFileInput.on('change', setLocalesUpdate);
setLocalesUpdate();

self.NProgressUnbind();

// Set locales to existing projects to be copied to the current project
Expand Down
24 changes: 23 additions & 1 deletion pontoon/administration/templates/admin_project.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,29 @@ <h1>

<h3>Locales</h3>

<div class="locales clearfix">
<div class="repo-locale-control checkbox clearfix">
<label for="id_set_locales_from_repo">
{{ form.set_locales_from_repo }}
<span></span>
</label>
<span id="hint_set_locales_from_repo"
>Locale is available if target directory is present.</span
>
</div>

<div
class="repo-locale-control checkbox clearfix"
id="translated_resources_from_repo"
style="display: none"
>
<label for="id_set_translated_resources_from_repo">
{{ form.set_translated_resources_from_repo }} Set per-resource
localizability from repository
</label>
Resource is available if target file is present in locale directory.
</div>

<div class="locales clearfix" style="display: none">
{{ admin_team_selector.render(locales_available, locales_selected, locales_readonly) }}
{{ form.locales }}
{{ form.locales.errors }}
Expand Down
90 changes: 57 additions & 33 deletions pontoon/administration/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import csv
import logging

from typing import cast

from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
Expand Down Expand Up @@ -33,6 +35,8 @@
from pontoon.pretranslation.tasks import pretranslate_task
from pontoon.sync.tasks import sync_project_task

from ..base.models.project import ProjectQuerySet


log = logging.getLogger(__name__)

Expand Down Expand Up @@ -127,6 +131,8 @@ def manage_project(request, slug=None, template="admin_project.html"):
subtitle = "Add project"
pk = None
project = None
prev_config_file = None
prev_set_locales_from_repo = False

# Save project
if request.method == "POST":
Expand All @@ -143,7 +149,11 @@ def manage_project(request, slug=None, template="admin_project.html"):
# Update existing project
try:
pk = request.POST["pk"]
project = Project.objects.visible_for(request.user).get(pk=pk)
project = (
cast(ProjectQuerySet, Project.objects)
.visible_for(request.user)
.get(pk=pk)
)
form = ProjectForm(request.POST, instance=project)
# Needed if form invalid
repo_formset = RepositoryInlineFormSet(request.POST, instance=project)
Expand All @@ -156,6 +166,8 @@ def manage_project(request, slug=None, template="admin_project.html"):
request.POST, instance=project
)
subtitle = "Edit project"
prev_config_file = project.configuration_file
prev_set_locales_from_repo = project.set_locales_from_repo

# Add a new project
except MultiValueDictKeyError:
Expand All @@ -166,7 +178,7 @@ def manage_project(request, slug=None, template="admin_project.html"):
tag_formset = None

if form.is_valid():
project = form.save(commit=False)
project = cast(Project, form.save(commit=False))
repo_formset = RepositoryInlineFormSet(request.POST, instance=project)
external_resource_formset = ExternalResourceInlineFormSet(
request.POST, instance=project
Expand All @@ -180,38 +192,43 @@ def manage_project(request, slug=None, template="admin_project.html"):
)
if formsets_valid:
project.save()

# Manually save ProjectLocales due to intermediary model
locales_form = form.cleaned_data.get("locales", [])
locales_readonly_form = form.cleaned_data.get("locales_readonly", [])
locales = locales_form | locales_readonly_form

(
ProjectLocale.objects.filter(project=project)
.exclude(locale__pk__in=[loc.pk for loc in locales])
.delete()
)

for locale in locales:
# The implicit pre_save and post_save signals sent here are required
# to maintain django-guardian permissions.
ProjectLocale.objects.get_or_create(project=project, locale=locale)

project_locales = ProjectLocale.objects.filter(project=project)

# Update readonly flags
locales_readonly_pks = [loc.pk for loc in locales_readonly_form]
project_locales.filter(readonly=True).exclude(
locale__pk__in=locales_readonly_pks
).update(readonly=False)
project_locales.filter(
locale__pk__in=locales_readonly_pks, readonly=False
).update(readonly=True)
pk = project.pk
data = form.cleaned_data

set_locales_from_repo = data.get("set_locales_from_repo", False)

if set_locales_from_repo:
project_locales = ProjectLocale.objects.filter(project=project)
else:
# Manually save ProjectLocales due to intermediary model
locales_form = data.get("locales", set())
locales_readonly_form = data.get("locales_readonly", set())
locales = locales_form | locales_readonly_form

ProjectLocale.objects.filter(project=project).exclude(
locale__pk__in=[loc.pk for loc in locales]
).delete()

for locale in locales:
# The implicit pre_save and post_save signals sent here are required
# to maintain django-guardian permissions.
ProjectLocale.objects.get_or_create(
project=project, locale=locale
)

project_locales = ProjectLocale.objects.filter(project=project)

# Update readonly flags
locales_readonly_pks = [loc.pk for loc in locales_readonly_form]
project_locales.filter(readonly=True).exclude(
locale__pk__in=locales_readonly_pks
).update(readonly=False)
project_locales.filter(
locale__pk__in=locales_readonly_pks, readonly=False
).update(readonly=True)

# Update pretranslate flags
locales_pretranslate_form = form.cleaned_data.get(
"locales_pretranslate", []
)
locales_pretranslate_form = data.get("locales_pretranslate", [])
locales_pretranslate_pks = [loc.pk for loc in locales_pretranslate_form]
project_locales.filter(pretranslation_enabled=True).exclude(
locale__pk__in=locales_pretranslate_pks,
Expand Down Expand Up @@ -239,7 +256,14 @@ def manage_project(request, slug=None, template="admin_project.html"):
if project.tags_enabled:
tag_formset = TagInlineFormSet(instance=project)
subtitle += ". Saved."
pk = project.pk

if project.data_source == Project.DataSource.REPOSITORY and (
set_locales_from_repo
and not prev_set_locales_from_repo
or data.get("configuration_file") != prev_config_file
):
sync_project_task.delay(project.pk)
subtitle += " Sync started."
else:
subtitle += ". Error."
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.14 on 2026-06-24 09:50

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("base", "0118_fix_terminology_entity_value"),
]

operations = [
migrations.AddField(
model_name="project",
name="set_locales_from_repo",
field=models.BooleanField(
default=False,
help_text="\n Control target locales by their directories' presence in the target repository,\n or in the root locales config if `configuration_file` is not None.\n ",
),
),
migrations.AddField(
model_name="project",
name="set_translated_resources_from_repo",
field=models.BooleanField(
default=False,
help_text="\n If `set_locales_from_repo` is True and `configuration_file` is None,\n setting `set_translated_resources_from_repo` to True will\n control the availability of each resource for translation in each locale\n according to the presence of a file (even if empty) at its expected target path.\n\n If `set_locales_from_repo` is False or `configuration_file` is not None,\n this control has no effect.\n ",
),
),
]
21 changes: 21 additions & 0 deletions pontoon/base/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,27 @@ class Visibility(models.TextChoices):
""",
)

set_locales_from_repo = models.BooleanField(
default=False,
help_text="""
Control target locales by their directories' presence in the target repository,
or in the root locales config if `configuration_file` is not None.
""",
)

set_translated_resources_from_repo = models.BooleanField(
default=False,
help_text="""
If `set_locales_from_repo` is True and `configuration_file` is None,
setting `set_translated_resources_from_repo` to True will
control the availability of each resource for translation in each locale
according to the presence of a file (even if empty) at its expected target path.

If `set_locales_from_repo` is False or `configuration_file` is not None,
this control has no effect.
""",
)

objects = ProjectQuerySet.as_manager()

project_locale: "ProjectLocaleQuerySet"
Expand Down
27 changes: 24 additions & 3 deletions pontoon/sync/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Entity,
Locale,
Project,
ProjectLocale,
TranslatedResource,
Translation,
User,
Expand Down Expand Up @@ -50,10 +51,30 @@ def sync_project(
log.error(f"{log_prefix} {e}")
raise e

locale_map: dict[str, Locale] = {
lc.code: lc for lc in project.locales.order_by("code")
}
locale_map: dict[str, Locale]
if project.set_locales_from_repo:
if not paths.locales:
errmsg = "No locales found in repo"
log.error(f"{log_prefix} {errmsg}")
raise Exception(errmsg)
locales = Locale.objects.filter(code__in=paths.locales).order_by("code")
locale_map = {lc.code: lc for lc in locales}

# Update project locales
ProjectLocale.objects.filter(project=project).exclude(
locale__in=locales
).delete()
for locale in locales:
# The implicit pre_save and post_save signals sent here are required
# to maintain django-guardian permissions.
ProjectLocale.objects.get_or_create(project=project, locale=locale)
ProjectLocale.objects.filter(project=project, readonly=True).update(
readonly=False
)
else:
locale_map = {lc.code: lc for lc in project.locales.order_by("code")}
paths.locales = list(locale_map.keys())

added_entities_count, changed_paths, removed_paths = sync_resources_from_repo(
project, locale_map, checkouts.source, paths, now
)
Expand Down
Loading
Loading