Skip to content

Commit 2051a89

Browse files
frjobmispelon
andauthored
Add missing translated strings (#4789)
(This contribution was sponsored by [DigitalHub.sh](https://digitalhub.sh/)) Fixes #4753 I've split this PR over several commits which are mostly independent of each other. I'm happy to open separate PRs if you think that'd be easier to review. ## Explanations of the separate commits ### Mark missing strings for translation After a few unsuccessful attempts at scripting the identification of untranslated strings, I ended up reviewing all Python files manually. The command I used to identify the relevant files was: ``` git ls-files -z '**.py' ':!**/migrations/*.py' ':!**/urls.py' ':!hypha/settings/*.py' ':!docs/*' ':!**/test*.py' | find -files0-from - -not -empty ``` (That's all Python files, but excluding stuff like migrations, settings, ... (and empty files)) This change should have no impact for a site running the English version of Hypha. ### Add missing strings for translations (templates) Similar to the previous commit, I ended up reviewing all templates manually (`git ls-files '*.html'`), adding `{% trans %}`, `{% blocktrans %}`, or `_(...)` where needed. Again, this change should have no impact. ### Add trimmed option to blocktranslate This removes irrelevant whitespace in source strings, making it a bit nicer. I used `git grep blocktrans | grep -vw trimmed | grep -v endblocktrans` to identify uses of `{% blocktrans %}` without the `trimmed` argument, excluding lines that also included `{% endblocktrans %}` on the same line. This is technically a breaking change, but it only introduces whitespace changes to HTML templates, which is irrelevant. ### Use `{% blocktranslate trimmed %}` to break down long lines This one just makes templates a little bit more readable as it breaks very long lines into more manageable pieces. Again, it can introduce whitespace changes to the HTML, but should not have any effect to the source strings. ### Added `verbose_name` and `verbose_name_plural` to all models Some models already has a translated `verbose_name` or `verbose_name_plural`, but it was inconsistent (and quite rare). This change adds these attributes to all models. I used `git ls-files '**/models/*.py'` and ` git ls-files '**/models.py'` to find all models, then went through the files manually to add the missing `verbose_name` and `verbose_name_plural`. I decided to omit the plural form for model classes inheriting from `BaseSiteSettings` since it didn't seem to make sense. I also opted to use lower case (like `_("foo bar")` for model `FooBar`) to match Django's own default behavior, but I used `Foo Bar` in a few instances where the model already had a `verbose_name` that used titlecase. ## Next steps I ran out of time to complete all my objectives and I've still got some translation-related tasks I wanted to get to. I'm happy to open issues for these if you think they're relevant: * #4752 Fix missing translations in javascript files: I found about 10 instances but fixing them would require either using the [Django javascript translation functions,](https://docs.djangoproject.com/en/6.0/topics/i18n/translation/#internationalization-in-javascript-code) or moving the strings to the HTML (data attributes for example). * #4753 Add explicit `verbose_name` to all declared model fields (and maybe form fields as well). This can probably be scripted somewhat, ideally with a linter that would catch the introducing of future untranslated strings. * Review cases where `gettext` is used at the module level instead of `gettext_lazy` as this could indicate some possible bugs (visible in a multi-language setup where the user is not using the default language). * Fix translations that build up a sentence word by word, as this doesn't really work in practice. Something like `{% trans "Application" %} {{ application_id }} {% trans "updated on" %} {{ date }} {% trans "by" %} {{ author.name }}` should be rewritten to have the whole sentence in a single `blocktrans`, or when not possible small words like `by` should be given a context because they're likely to be translated differently for different sentences. --------- Co-authored-by: Baptiste Mispelon <baptiste.mispelon@torchbox.com>
1 parent 0af41e7 commit 2051a89

179 files changed

Lines changed: 2830 additions & 883 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

hypha/addressfield/fields.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django import forms
55
from django.core.exceptions import ValidationError
6+
from django.utils.translation import gettext_lazy as _
67

78
from .widgets import AddressWidget
89

@@ -49,7 +50,7 @@ def clean(self, value, **kwargs):
4950
try:
5051
country_data = self.data[country]
5152
except KeyError:
52-
raise ValidationError("Invalid country selected") from None
53+
raise ValidationError(_("Invalid country selected")) from None
5354

5455
fields = flatten_data(country_data["fields"])
5556

@@ -59,7 +60,9 @@ def clean(self, value, **kwargs):
5960
if missing_fields:
6061
missing_field_name = [fields[field]["label"] for field in missing_fields]
6162
raise ValidationError(
62-
"Please provide data for: {}".format(", ".join(missing_field_name))
63+
_("Please provide data for: {fields}").format(
64+
fields=", ".join(missing_field_name)
65+
)
6366
)
6467

6568
return super().clean(value, **kwargs)

hypha/apply/activity/adapters/activity_feed.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,9 @@ def handle_report_frequency(self, config, **kwargs):
293293

294294
def handle_skipped_report(self, report, **kwargs):
295295
if report.skipped:
296-
return "Skipped a Report"
296+
return _("Skipped a Report")
297297
else:
298-
return "Marked a Report as required"
298+
return _("Marked a Report as required")
299299

300300
def handle_update_invoice_status(self, invoice, **kwargs):
301301
base_message = _("Updated Invoice status to: {invoice_status}.")

hypha/apply/activity/adapters/base.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.conf import settings
22
from django.contrib import messages
3+
from django.utils.translation import gettext as _
34

45
from hypha.apply.activity.options import MESSAGES
56

@@ -192,11 +193,13 @@ def process_send(
192193

193194
if not settings.SEND_MESSAGES:
194195
if recipient:
195-
debug_message = "{} [to: {}]: {}".format(
196-
self.adapter_type, recipient, message
196+
debug_message = _("{adapter} [to: {recipient}]: {message}").format(
197+
adapter=self.adapter_type, recipient=recipient, message=message
197198
)
198199
else:
199-
debug_message = "{}: {}".format(self.adapter_type, message)
200+
debug_message = _("{adapter}: {message}").format(
201+
adapter=self.adapter_type, message=message
202+
)
200203
messages.add_message(request, messages.DEBUG, debug_message)
201204

202205
def create_logs(self, message, recipient, *events):

hypha/apply/activity/forms.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ class Meta:
3030
"assign_to",
3131
)
3232
labels = {
33-
"visibility": "Visible to",
34-
"message": "Message",
33+
"visibility": _("Visible to"),
34+
"message": _("Message"),
3535
}
3636
help_texts = {
37-
"visibility": "Select a relevant user role. Staff can view every comment."
37+
"visibility": _(
38+
"Select a relevant user role. Staff can view every comment."
39+
)
3840
}
3941
widgets = {
4042
"visibility": forms.RadioSelect(),
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Generated by Django 5.2.13 on 2026-04-13 08:22
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("activity", "0090_alter_activity_visibility_alter_event_type"),
9+
]
10+
11+
operations = [
12+
migrations.AlterModelOptions(
13+
name="activity",
14+
options={
15+
"base_manager_name": "objects",
16+
"ordering": ["-timestamp"],
17+
"verbose_name": "activity",
18+
"verbose_name_plural": "activities",
19+
},
20+
),
21+
migrations.AlterModelOptions(
22+
name="activityattachment",
23+
options={
24+
"verbose_name": "activity attachment",
25+
"verbose_name_plural": "activity attachments",
26+
},
27+
),
28+
migrations.AlterModelOptions(
29+
name="event",
30+
options={"verbose_name": "event", "verbose_name_plural": "events"},
31+
),
32+
migrations.AlterModelOptions(
33+
name="message",
34+
options={"verbose_name": "message", "verbose_name_plural": "messages"},
35+
),
36+
migrations.AlterField(
37+
model_name="activity",
38+
name="message",
39+
field=models.TextField(verbose_name="message"),
40+
),
41+
migrations.AlterField(
42+
model_name="activity",
43+
name="visibility",
44+
field=models.CharField(
45+
choices=[
46+
("applicant", "Applicants"),
47+
("team", "Staff only"),
48+
("reviewers", "Reviewers"),
49+
("all", "All"),
50+
],
51+
default="applicant",
52+
max_length=30,
53+
verbose_name="visibility",
54+
),
55+
),
56+
]

hypha/apply/activity/models.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from django.urls import reverse
1313
from django.utils import timezone
1414
from django.utils.text import get_valid_filename
15-
from django.utils.translation import gettext as _
15+
from django.utils.translation import gettext_lazy as _
1616

1717
from hypha.apply.utils.storage import PrivateStorage
1818

@@ -26,13 +26,10 @@
2626
ACTION: _("Action"),
2727
}
2828

29-
# Visibility strings. Used to determine visibility states but are also
30-
# sometimes shown to users.
31-
# (ie. hypha.apply.activity.templatetags.activity_tags.py)
32-
APPLICANT = _("applicant")
33-
TEAM = _("team")
34-
REVIEWER = _("reviewers")
35-
ALL = _("all")
29+
APPLICANT = "applicant"
30+
TEAM = "team"
31+
REVIEWER = "reviewers"
32+
ALL = "all"
3633

3734
# Visibility choice strings
3835
VISIBILITY = {
@@ -175,6 +172,10 @@ class ActivityAttachment(models.Model):
175172
upload_to=get_attachment_upload_path, storage=PrivateStorage()
176173
)
177174

175+
class Meta:
176+
verbose_name = _("activity attachment")
177+
verbose_name_plural = _("activity attachments")
178+
178179
@property
179180
def filename(self):
180181
return os.path.basename(self.file.name)
@@ -201,9 +202,12 @@ class Activity(models.Model):
201202
source_object_id = models.PositiveIntegerField(blank=True, null=True, db_index=True)
202203
source = GenericForeignKey("source_content_type", "source_object_id")
203204

204-
message = models.TextField()
205+
message = models.TextField(_("message"))
205206
visibility = models.CharField(
206-
choices=list(VISIBILITY.items()), default=APPLICANT, max_length=30
207+
_("visibility"),
208+
choices=list(VISIBILITY.items()),
209+
default=APPLICANT,
210+
max_length=30,
207211
)
208212

209213
# Fields for handling versioning of the comment activity models
@@ -232,6 +236,8 @@ class Activity(models.Model):
232236
class Meta:
233237
ordering = ["-timestamp"]
234238
base_manager_name = "objects"
239+
verbose_name = _("activity")
240+
verbose_name_plural = _("activities")
235241

236242
def get_absolute_url(self):
237243
# coverup for both submission and project as source.
@@ -330,6 +336,10 @@ class Event(models.Model):
330336
object_id = models.PositiveIntegerField(blank=True, null=True)
331337
source = GenericForeignKey("content_type", "object_id")
332338

339+
class Meta:
340+
verbose_name = _("event")
341+
verbose_name_plural = _("events")
342+
333343
def __str__(self):
334344
if self.source and hasattr(self.source, "title"):
335345
return f"{self.by} {self.get_type_display()} - {self.source.title}"
@@ -365,6 +375,10 @@ class Message(models.Model):
365375
sent_in_email_digest = models.BooleanField(default=False)
366376
objects = MessagesQueryset.as_manager()
367377

378+
class Meta:
379+
verbose_name = _("message")
380+
verbose_name_plural = _("messages")
381+
368382
def __str__(self):
369383
return f"[{self.type}][{self.status}] {self.content}"
370384

hypha/apply/activity/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.db.models import TextChoices
2-
from django.utils.translation import gettext as _
2+
from django.utils.translation import gettext_lazy as _
33

44

55
class MESSAGES(TextChoices):

hypha/apply/activity/templates/activity/include/activity_list.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
hx-target="this"
2222
hx-swap="outerHTML transition:true"
2323
hx-select=".h-timeline"
24-
>Show more...</a>
24+
>{% trans "Show more..." %}</a>
2525
{% endif %}
2626
</div>
2727

hypha/apply/activity/templates/activity/ui/activity-comment-item.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
{% for role in activity.user.get_role_names %}
2626
<span
2727
class="inline-block py-0.5 px-2 text-sm font-semibold rounded-xl border border-gray-300 text-fg-muted"
28-
data-tippy-content="This user is a {{ role }}"
28+
data-tippy-content="{% blocktrans %}This user is a {{ role }}{% endblocktrans %}"
2929
>
3030
{{ role }}
3131
</span>
@@ -34,7 +34,7 @@
3434
{% endif %}
3535

3636
<a href="#communications--{{activity.id}}" class="hover:underline">
37-
<span class="text-fg-muted">commented</span>
37+
<span class="text-fg-muted">{% trans "commented" %}</span>
3838
<relative-time
3939
class="text-fg-muted"
4040
datetime="{{ activity.timestamp|date:"c" }}"
@@ -58,7 +58,7 @@
5858

5959
{% with activity.visibility|visibility_display:request.user as visibility_text %}
6060
<span class="flex gap-1 items-center py-0.5 px-1.5 text-xs uppercase rounded-xl border border-gray-300 text-fg-muted"
61-
data-tippy-content="This is visible to {{ visibility_text }}">
61+
data-tippy-content="{% blocktrans %}This is visible to {{ visibility_text }}{% endblocktrans %}">
6262
{% heroicon_outline "eye" size=14 class="inline" aria_hidden=true %}
6363
{{ visibility_text }}
6464
</span>
@@ -69,7 +69,7 @@
6969
<a
7070
hx-get="{% url 'activity:edit-comment' activity.id %}"
7171
hx-target="#text-comment-{{activity.id}}"
72-
title="{% trans "Edit comment" %}"
72+
title="{% trans 'Edit comment' %}"
7373
class="btn btn-sm btn-square btn-ghost"
7474
>
7575
{% heroicon_micro "pencil-square" aria_hidden=true %}
@@ -81,8 +81,8 @@
8181
<a
8282
hx-delete="{% url 'activity:delete-comment' activity.id %}"
8383
hx-target="#communications-wrapper--{{activity.id}}"
84-
hx-confirm="{% trans "Are you sure you want to delete this comment? This action cannot be undone." %}"
85-
title="{% trans "Delete comment" %}"
84+
hx-confirm="{% trans 'Are you sure you want to delete this comment? This action cannot be undone.' %}"
85+
title="{% trans 'Delete comment' %}"
8686
class="btn btn-error btn-sm btn-square btn-ghost"
8787
>
8888
{% heroicon_micro "trash" class="opacity-80 size-4" aria_hidden=true %}

hypha/apply/activity/views.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.core.paginator import Paginator
44
from django.shortcuts import get_object_or_404, render
55
from django.utils.decorators import method_decorator
6+
from django.utils.translation import gettext as _
67
from django.views.decorators.http import require_http_methods
78
from django.views.generic import ListView
89
from rolepermissions.checkers import has_object_permission
@@ -57,10 +58,10 @@ def edit_comment(request, pk):
5758
activity = get_object_or_404(Activity, id=pk)
5859

5960
if activity.type != COMMENT or activity.user != request.user:
60-
raise PermissionError("You can only edit your own comments")
61+
raise PermissionDenied(_("You can only edit your own comments"))
6162

6263
if activity.deleted:
63-
raise PermissionError("You can not edit a deleted comment")
64+
raise PermissionDenied(_("You can not edit a deleted comment"))
6465

6566
if request.GET.get("action") == "cancel":
6667
return render(
@@ -88,10 +89,10 @@ def delete_comment(request, pk):
8889
activity = get_object_or_404(Activity, id=pk)
8990

9091
if activity.type != COMMENT or activity.user != request.user:
91-
raise PermissionError("You can only delete your own comments")
92+
raise PermissionDenied(_("You can only delete your own comments"))
9293

9394
if activity.deleted:
94-
raise PermissionError("You can not delete a deleted comment")
95+
raise PermissionDenied(_("You can not delete a deleted comment"))
9596

9697
if request.method == "DELETE":
9798
activity = services.delete_comment(activity)

0 commit comments

Comments
 (0)