Merge pull request #192

i18n support and Chinese translation
This commit is contained in:
Klaas van Schelven
2025-08-28 20:23:54 +02:00
committed by GitHub
54 changed files with 1360 additions and 285 deletions

View File

@@ -0,0 +1,20 @@
from django.db import models
IGNORED_ATTRS = ['verbose_name', 'help_text']
original_deconstruct = models.Field.deconstruct
def new_deconstruct(self):
# works around the non-fix of https://code.djangoproject.com/ticket/21498 (I don't agree with the reasoning that
# "in principle any field could influence the database schema"; you must be _insane_ if verbose_name or help_text
# actually do, and the cost of the migrations is real)
# solution from https://stackoverflow.com/a/39801321/339144
name, path, args, kwargs = original_deconstruct(self)
for attr in IGNORED_ATTRS:
kwargs.pop(attr, None)
return name, path, args, kwargs
def monkey_patch_deconstruct():
models.Field.deconstruct = new_deconstruct

View File

@@ -0,0 +1,8 @@
from django.core.management.commands.makemigrations import Command as OriginalCommand
from . import monkey_patch_deconstruct
monkey_patch_deconstruct()
class Command(OriginalCommand):
pass # no changes, except the monkey patch above

View File

@@ -1,6 +1,9 @@
import time
from django.core.management.commands.migrate import Command as DjangoMigrateCommand
from . import monkey_patch_deconstruct
monkey_patch_deconstruct() # needed for migrate.py to avoid the warning about non-reflected changes
class Command(DjangoMigrateCommand):
# We override the default Django migrate command to add the elapsed time for each migration. (This could in theory
@@ -10,8 +13,7 @@ class Command(DjangoMigrateCommand):
# We care more about the elapsed time for each migration than the average Django user because sqlite takes such a
# prominent role in our architecture, and because migrations are run out of our direct control ("self hosted").
#
# AFAIU, "just dropping a file called migrate.py in one of our apps" is good enough to be the override (and if it
# isn't, it's not critical, since all we do is add a bit more info to the output).
# AFAIU, "just dropping a file called migrate.py in one of our apps" is good enough to be the override.
def migration_progress_callback(self, action, migration=None, fake=False):
# Django 4.2's method, with a single change

View File

@@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Auth Tokens · {{ site_title }}{% endblock %}
{% block title %}{% translate "Auth Tokens" %} · {{ site_title }}{% endblock %}
{% block content %}
@@ -19,12 +20,12 @@
{% endif %}
<div class="flex">
<h1 class="text-4xl mt-4 font-bold">Auth Tokens</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Auth Tokens" %}</h1>
<div class="ml-auto mt-6">
<form action="{% url "auth_token_create" %}" method="post">
{% csrf_token %} {# margins display slightly different from the <a href version that I have for e.g. project memembers, but I don't care _that_ much #}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Add Token</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Add Token" %}</button>
</form>
</div>
</div>
@@ -37,7 +38,7 @@
<tbody>
<thead>
<tr class="bg-slate-200 dark:bg-slate-800">
<th class="w-full p-4 text-left text-xl" colspan="2">Auth Tokens</th>
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Auth Tokens" %}</th>
</tr>
{% for auth_token in auth_tokens %}
@@ -50,7 +51,7 @@
<td class="p-4">
<div class="flex justify-end">
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Delete</button>
<button name="action" value="delete:{{ auth_token.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Delete" %}</button>
</div>
</td>
@@ -59,7 +60,7 @@
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
<td class="w-full p-4">
<div>
No Auth Tokens.
{% translate "No Auth Tokens." %}
</div>
</td>

View File

@@ -2,6 +2,7 @@ from django.shortcuts import render, redirect
from django.http import Http404
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.utils.translation import gettext_lazy as _
from bugsink.decorators import atomic_for_request_method
@@ -20,7 +21,7 @@ def auth_token_list(request):
if action == "delete":
AuthToken.objects.get(pk=pk).delete()
messages.success(request, 'Token deleted')
messages.success(request, _('Token deleted'))
return redirect('auth_token_list')
return render(request, 'bsmain/auth_token_list.html', {

View File

@@ -5,6 +5,10 @@ from django.contrib.auth.decorators import login_required
from django.db import connection
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.utils.translation import get_supported_language_variant
from django.utils.translation.trans_real import parse_accept_lang_header
from django.utils import translation
performance_logger = logging.getLogger("bugsink.performance.views")
@@ -128,3 +132,35 @@ class SetRemoteAddrMiddleware:
request.META["REMOTE_ADDR"] = self.parse_x_forwarded_for(request.META.get("HTTP_X_FORWARDED_FOR", None))
return self.get_response(request)
def language_from_accept_language(request):
"""
Pick a language using ONLY the Accept-Language header. Ignores URL prefixes, session, and cookies. I prefer to have
as little "magic" in the language selection as possible, and I _know_ we don't do anything with paths, so I'd rather
not have such code invoked at all (at the cost of reimplementing some of Django's logic here).
"""
header = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
for lang_code, _q in parse_accept_lang_header(header):
try:
# strict=False lets country variants match (e.g. 'es-CO' for 'es')
return get_supported_language_variant(lang_code, strict=False)
except LookupError:
continue
return settings.LANGUAGE_CODE
def get_chosen_language(request_user, request):
if request_user.is_authenticated and request_user.language != "auto":
return get_supported_language_variant(request_user.language, strict=False)
return language_from_accept_language(request)
class UserLanguageMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
translation.activate(get_chosen_language(request.user, request))
response = self.get_response(request)
return response

View File

@@ -103,6 +103,10 @@ MIDDLEWARE = [
'bugsink.middleware.LoginRequiredMiddleware',
# note on ordering: we need request.user, so after AuthenticationMiddleware; and we're not tied to "before
# CommonMiddleware" as django.middleware.locale.LocaleMiddleware is, because we don't do path-related stuff.
'bugsink.middleware.UserLanguageMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
@@ -241,6 +245,14 @@ TIME_ZONE = 'Europe/Amsterdam'
USE_I18N = True
USE_L10N = True
LOCALE_PATHS = [BASE_DIR / "locale"]
LANGUAGES = (
("en", "English"),
("zh-hans", "简体中文"),
)
USE_TZ = True

View File

@@ -8,6 +8,7 @@ from django.db.models.functions import Concat
from django.template.defaultfilters import date as default_date_filter
from django.conf import settings
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from bugsink.utils import assert_
from bugsink.volume_based_condition import VolumeBasedCondition
@@ -485,16 +486,16 @@ class IssueQuerysetStateManager(object):
class TurningPointKind(models.IntegerChoices):
# The language of the kinds reflects a historic view of the system, e.g. "first seen" as opposed to "new issue"; an
# alternative take (which is more consistent with the language used elsewhere" is a more "active" language.
FIRST_SEEN = 1, "First seen"
RESOLVED = 2, "Resolved"
MUTED = 3, "Muted"
REGRESSED = 4, "Marked as regressed"
UNMUTED = 5, "Unmuted"
FIRST_SEEN = 1, _("First seen")
RESOLVED = 2, _("Resolved")
MUTED = 3, _("Muted")
REGRESSED = 4, _("Marked as regressed")
UNMUTED = 5, _("Unmuted")
NEXT_MATERIALIZED = 10, "Release info added"
NEXT_MATERIALIZED = 10, _("Release info added")
# ASSGINED = 10, "Assigned to user" # perhaps later
MANUAL_ANNOTATION = 100, "Manual annotation"
MANUAL_ANNOTATION = 100, _("Manual annotation")
class TurningPoint(models.Model):

View File

@@ -1,7 +1,8 @@
{% load add_to_qs %}
{% load i18n %}
<form action="{% url this_view issue_pk=issue.pk nav="last" %}" method="get">{# nav="last": when doing a new search on an event-page, you want the most recent matching event to show up #}
<input type="text" name="q" value="{{ q }}" placeholder="search..." class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'search...' %}" class="focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md mr-2"/>
</form>
{% if has_prev %} {# no need for 'is_first': if you can go to the left, you can go all the way to the left too #}

View File

@@ -4,6 +4,8 @@
{% load humanize %}
{% load stricter_templates %}
{% load add_to_qs %}
{% load i18n %}
{% block title %}{{ issue.title }} · {{ block.super }}{% endblock %}
{% block content %}
@@ -18,12 +20,12 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if issue.project.has_releases %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 stroke-slate-300 dark:stroke-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown #}
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">Resolve</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -32,7 +34,7 @@
{% if issue.project.has_releases %}
{# 'by next' is shown even if 'by current' is also shown: just because you haven't seen 'by current' doesn't mean it's actually already solved; and in fact we show this option first precisely because we can always show it #}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
<div class="dropdown">
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-800 dark:text-slate-100 fill-slate-800 dark:fill-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
@@ -51,7 +53,7 @@
{% else %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">Resolve</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -59,14 +61,14 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if not issue.is_muted and not issue.is_resolved %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md" name="action" value="mute">Mute</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="mute">Mute</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="mute">{% translate "Mute" %}</button>
{% endif %}
<div class="dropdown">
{% if not issue.is_muted and not issue.is_resolved %}
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Mute for/until&nbsp;&nbsp;<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<div class="dropdown-content-right flex-col">
{% for mute_option in mute_options %}
@@ -74,7 +76,7 @@
{% endfor %}
</div>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">Mute for/until&nbsp;&nbsp;<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# note that when the issue is muted, no further muting is allowed. this is a design decision, I figured this is the easiest-to-understand UI, #}
{# both at the point-of-clicking and when displaying the when-will-this-be-unmuted in some place #}
{# (the alternative would be to allow multiple simulteneous reasons for unmuting to exist next to each other #}
@@ -83,9 +85,9 @@
</div>
{% if issue.is_muted and not issue.is_resolved %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">Unmute</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">Unmute</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% endif %}
{% endspaceless %}
@@ -107,13 +109,13 @@
{# overflow-x-auto is needed at the level of the flex item such that it works at the level where we need it (the code listings)#}
<div class="ml-4 mb-4 mr-4 border-2 overflow-x-auto flex-[2_1_96rem]"><!-- the whole of the big tabbed view--> {# 96rem is 1536px, which matches the 2xl class; this is no "must" but eyeballing revealed: good result #}
<div class="flex bg-slate-50 dark:bg-slate-800 border-b-2"><!-- container for the actual tab buttons -->
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Stacktrace</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;Details</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Breadcrumbs</div></a>
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Event&nbsp;List</div></a>
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Tags</div></a>
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">Grouping</div></a>
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">History</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "stacktrace" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Stacktrace" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/details/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-details" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;Details" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/event/{% if event %}{{ event.id }}{% else %}last{% endif %}/breadcrumbs/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "breadcrumbs" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Breadcrumbs" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/events/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "event-list" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Event&nbsp;List" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/tags/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "tags" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Tags" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/grouping/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "grouping" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "Grouping" %}</div></a>
<a href="/issues/issue/{{ issue.id }}/history/{% current_qs %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if tab == "history" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 border-slate-400 hover:border-b-4{% endif %}">{% translate "History" %}</div></a>
</div>
<div class="m-4"><!-- div for tab_content -->
@@ -122,19 +124,19 @@
</div>
<div class="flex p-4 bg-slate-200 dark:bg-slate-800 border-b-2"><!-- bottom nav bar -->
{% if is_event_page %}<div>Event {{ event.digest_order|intcomma }} of {{ issue.digested_event_count|intcomma }} which occured at <span class="font-bold">{{ event.ingested_at|date:"j M G:i T" }}</span></div>{% endif %}
{% if is_event_page %}<div>{% blocktranslate with digest_order=event.digest_order|intcomma total_events=issue.digested_event_count|intcomma ingested_at=event.ingested_at|date:"j M G:i T" %}Event {{ digest_order }} of {{ total_events }} which occured at <span class="font-bold">{{ ingested_at }}</span>{% endblocktranslate %}</div>{% endif %}
<div class="ml-auto pr-4 font-bold text-slate-500 dark:text-slate-300">
{% if is_event_page %}
<a href="/events/event/{{ event.id }}/download/">Download</a>
| <a href="/events/event/{{ event.id }}/raw/" >JSON</a>
| <a href="/events/event/{{ event.id }}/plain/" >Plain</a>
<a href="/events/event/{{ event.id }}/download/">{% translate "Download" %}</a>
| <a href="/events/event/{{ event.id }}/raw/" >{% translate "JSON" %}</a>
| <a href="/events/event/{{ event.id }}/plain/" >{% translate "Plain" %}</a>
{% endif %}
{% if app_settings.USE_ADMIN and user.is_staff %}
{% if is_event_page %}
| <a href="/admin/events/event/{{ event.id }}/change/">Event Admin</a> |
| <a href="/admin/events/event/{{ event.id }}/change/">{% translate "Event Admin" %}</a> |
{% endif %}
<a href="/admin/issues/issue/{{ issue.id }}/change/">Issue Admin</a>
<a href="/admin/issues/issue/{{ issue.id }}/change/">{% translate "Issue Admin" %}</a>
{% endif %}
</div>
@@ -152,22 +154,22 @@
<div class="p-4">
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Issue #</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Issue" %} #</div>
<div>{{ issue.friendly_id }} </div>
</div>
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">State</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "State" %}</div>
<div>
{% if issue.is_resolved %}
Resolved
{% translate "Resolved" %}
{% for version in issue.get_fixed_at %}
{% if forloop.first %}in{% endif %}
{% if forloop.first %}{% translate "in" %}{% endif %}
<span {% if version|issha %}class="font-mono"{% endif %}>{{ version|shortsha }}{% if not forloop.last %}</span>,{% endif %}
{% endfor %}
{% else %}
{% if issue.is_muted %}
Muted
{% translate "Muted" %}
{% if issue.unmute_after %}
until {{ issue.unmute_after|date:"j M G:i T" }}.
{% elif issue.get_unmute_on_volume_based_conditions %}
@@ -178,14 +180,14 @@
(unconditionally).
{% endif %}
{% else %}
Open
{% translate "Open" %}
{% endif %}
{% endif %}
</div>
</div>
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Nr. of events:</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Nr. of events" %}:</div>
<div>{{ issue.digested_event_count|intcomma }}
{% if issue.digested_event_count != issue.stored_event_count %}
total seen</div><div>{{ issue.stored_event_count|intcomma }} available</div>
@@ -196,17 +198,17 @@
{% if issue.digested_event_count > 1 %}
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">First seen:</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "First seen" %}:</div>
<div>{{ issue.first_seen|date:"j M G:i T" }}</div>
</div>
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Last seen:</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Last seen" %}:</div>
<div>{{ issue.last_seen|date:"j M G:i T" }}</div>
</div>
{% else %}
<div class="mb-4">
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">Seen at:</div>
<div class="text-sm font-bold text-slate-500 dark:text-slate-300">{% translate "Seen at" %}:</div>
<div>{{ issue.first_seen|date:"j M G:i T" }}</div>
</div>
{% endif %}
@@ -229,7 +231,7 @@
<div class="border-2 mb-4 mr-4"><!-- "issue: tags" box -->
<div class="font-bold border-b-2">
<div class="p-4 border-slate-50 dark:border-slate-900 text-slate-500 dark:text-slate-300">
Issue Tags
{% translate "Issue Tags" %}
</div>
</div>
<div class="p-4">

View File

@@ -3,6 +3,7 @@
{% load stricter_templates %}
{% load issues %}
{% load humanize %}
{% load i18n %}
{% block tab_content %}
@@ -21,7 +22,7 @@
{% if not breadcrumbs %}
<div class="mt-6 mb-6 italic">
No breadcrumbs available for this event.
{% translate "No breadcrumbs available for this event." %}
</div>
{% else %}

View File

@@ -1,11 +1,12 @@
{% extends "issues/base.html" %}
{% load static %}
{% load user %}
{% load i18n %}
{% block tab_content %}
<h1 class="text-2xl font-bold text-ellipsis whitespace-nowrap overflow-hidden">History</h1>
<div class="italic">Most recent first</div>
<h1 class="text-2xl font-bold text-ellipsis whitespace-nowrap overflow-hidden">{% translate "History" %}</h1>
<div class="italic">{% translate "Most recent first" %}</div>
<div class="flex"><!-- single turningpoint (for 'your comments')-->
@@ -17,12 +18,12 @@
<div class="border-slate-300 dark:border-slate-600 border-2 rounded-md mt-6 flex-auto"><!-- the "your comments balloon" -->
<div class="pl-4 flex triangle-left"><!-- 'header' row -->
<div class="mt-4 mb-4">
<span class="font-bold text-slate-800 dark:text-slate-100 italic">Add comment as manual annotation</span>
<span class="font-bold text-slate-800 dark:text-slate-100 italic">{% translate "Add comment as manual annotation" %}</span>
</div>
<div class="ml-auto flex"> <!-- 'header' row right side -->
<div class="p-4">
Now
{% translate "Now" %}
</div>
</div>
</div>
@@ -31,8 +32,8 @@
<div class="mt-4">
<form action="{% url "history_comment_new" issue_pk=issue.id %}" method="post">
{% csrf_token %}
<textarea name="comment" placeholder="comments..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Post comment</button>
<textarea name="comment" placeholder="{% translate 'comments...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md w-full h-32" onkeypress="submitOnCtrlEnter(event)"></textarea>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 mt-2 border-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Post comment" %}</button>
</form>
</div>
</div>{# 'body' part of the balloon #}
@@ -55,7 +56,7 @@
<div class="border-slate-300 dark:border-slate-600 border-2 rounded-md mt-6 flex-auto js-balloon"><!-- the "balloon" -->
<div class="pl-4 flex triangle-left"><!-- 'header' row -->
<div class="mt-4 mb-4">
<span class="font-bold text-slate-800 dark:text-slate-100">{{ turningpoint.get_kind_display }}</span> by
<span class="font-bold text-slate-800 dark:text-slate-100">{{ turningpoint.get_kind_display }}</span> {% translate "by" context "History" %}
<span class="font-bold text-slate-800 dark:text-slate-100">{% if turningpoint.user_id %}{{ turningpoint.user|best_displayname }}{% else %}Bugsink{% endif %}</span>
{% if turningpoint.user_id == request.user.id %}

View File

@@ -2,8 +2,9 @@
{% load static add_to_qs %}
{% load humanize %}
{% load add_to_qs %}
{% load i18n %}
{% block title %}Issues · {{ project.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Issues" %} · {{ project.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -11,15 +12,15 @@
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
<div class="text-center m-4">
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete Issues</h3>
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Issues" %}</h3>
<div class="mt-4 mb-6">
<p class="text-slate-700 dark:text-slate-300">
Deleting an Issue is a permanent action and cannot be undone. It's typically better to resolve or mute an issue instead of deleting it, as this allows you to keep track of past issues and their resolutions.
{% translate "Deleting an Issue is a permanent action and cannot be undone. It's typically better to resolve or mute an issue instead of deleting it, as this allows you to keep track of past issues and their resolutions." %}
</p>
</div>
<div class="flex items-center justify-center space-x-4 mb-4">
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">Cancel</button>
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">Delete</button>
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Cancel" %}</button>
<button id="confirmDelete" type="submit" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring">{% translate "Delete" %}</button>
</div>
</div>
</div>
@@ -27,25 +28,25 @@
<div class="m-4">
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} - Issues</h1>
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} - {% translate "Issues" %}</h1>
{% if unapplied_issue_ids %}
<div class="bg-red-100 w-full mt-2 mb-2 p-4 border-red-800 border-2">
The chosen action is not applicable to all selected issues. Issues for which it has not been applied have been left with checkboxes checked so that you can try again with another action.
{% translate "The chosen action is not applicable to all selected issues. Issues for which it has not been applied have been left with checkboxes checked so that you can try again with another action." %}
</div>
{% endif %}
<div class="flex bg-slate-50 dark:bg-slate-800 border-b-2 mt-4 items-end">
<div class="flex">
<a href="{% url "issue_list_open" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "open" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Open</div></a>
<a href="{% url "issue_list_unresolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "unresolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Unresolved</div></a>
<a href="{% url "issue_list_muted" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "muted" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">Muted</div></a>
<a href="{% url "issue_list_resolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "resolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">Resolved</div></a>
<a href="{% url "issue_list_all" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "all" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">All</div></a>
<a href="{% url "issue_list_open" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "open" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Open" %}</div></a>
<a href="{% url "issue_list_unresolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "unresolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Unresolved" %}</div></a>
<a href="{% url "issue_list_muted" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "muted" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "Muted" %}</div></a>
<a href="{% url "issue_list_resolved" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "resolved" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "Resolved" %}</div></a>
<a href="{% url "issue_list_all" project_pk=project.id %}"><div class="p-4 font-bold hover:bg-slate-200 dark:hover:bg-slate-800 {% if state_filter == "all" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4{% else %}text-slate-500 dark:text-slate-300 hover:border-slate-400 hover:border-b-4{% endif %}">{% translate "All" %}</div></a>
</div>
<div class="ml-auto p-2">
<form action="." method="get">
<input type="text" name="q" value="{{ q }}" placeholder="search issues..." class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
<input type="text" name="q" value="{{ q }}" placeholder="{% translate 'Search issues...' %}" class="bg-slate-50 dark:bg-slate-800 focus:border-cyan-500 dark:focus:border-cyan-400 focus:ring-cyan-200 dark:focus:ring-cyan-700 rounded-md"/>
</form>
</div>
</div>
@@ -75,12 +76,12 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if project.has_releases %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">Resolved in next release</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md" name="action" value="resolved_next">{% translate "Resolved in next release" %}</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-t-2 border-b-2 rounded-e-md"><svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown #}
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">Resolve</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-md" name="action" value="resolved">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -105,7 +106,7 @@
{% else %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">Resolve</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" name="action" value="resolve">{% translate "Resolve" %}</button>
{% endif %}
{% endspaceless %}
@@ -113,14 +114,14 @@
{% spaceless %}{# needed to avoid whitespace between the looks-like-one-buttons #}
{% if not disable_mute_buttons %}
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md">Mute</button>
<button name="action" value="mute" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-s-md">{% translate "Mute" %}</button>
{% else %}
<button disabled name="action" value="mute" class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md">Mute</button>
<button disabled name="action" value="mute" class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 rounded-s-md">{% translate "Mute" %}</button>
{% endif %}
<div class="dropdown">
{% if not disable_mute_buttons %}
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">Mute for/until&nbsp;&nbsp;<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 dark:text-slate-300 fill-slate-500 dark:fill-slate-500 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<div class="dropdown-content-right flex-col">
{% for mute_option in mute_options %}
@@ -128,22 +129,22 @@
{% endfor %}
</div>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">Mute for/until&nbsp;&nbsp;<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 fill-slate-300 dark:fill-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2">{% translate "Mute for/until&nbsp;&nbsp;" %}<svg xmlns="http://www.w3.org/2000/svg" fill="full" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline"><path d="M6.5,8.5l6,7l6-7H6.5z"/></svg></button>
{# we just hide the whole dropdown; this is the easiest implementation of not-showing the dropdown when the issue is already muted #}
{% endif %}
</div>
{% if not disable_unmute_buttons %}
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">Unmute</button>
<button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% else %}
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">Unmute</button>
<button disabled class="font-bold text-slate-300 dark:text-slate-600 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 border-r-2 border-b-2 border-t-2 rounded-e-md" name="action" value="unmute">{% translate "Unmute" %}</button>
{% endif %}
<div class="dropdown">
<button disabled {# disabled b/c clicking the dropdown does nothing - we have hover for that #} class="font-bold text-slate-500 fill-slate-500 border-slate-300 ml-2 pl-4 pr-4 pb-2 pt-2 border-2 hover:bg-slate-200 active:ring rounded-md">...</button>
<div class="dropdown-content-right flex-col">
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring text-left whitespace-nowrap">Delete</button>
<button type="button" onclick="showDeleteConfirmation()" class="block self-stretch font-bold text-red-500 dark:text-slate-300 border-slate-300 pl-4 pr-4 pb-2 pt-2 border-l-2 border-r-2 border-b-2 bg-white dark:bg-slate-900 hover:bg-red-50 dark:hover:bg-red-800 active:ring text-left whitespace-nowrap">{% translate "Delete" %}</button>
</div>
</div>
@@ -152,7 +153,7 @@
{# NOTE: "reopen" is not available in the UI as per the notes in issue_detail #}
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reopen</button> #}
{# only for resolved/muted items <button class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Reopen" %}</button> #}
</div>
</td>
@@ -174,7 +175,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
</svg>&nbsp;&nbsp;{% endif %}{{ issue.title|truncatechars:100 }}</a>
</div>
<div class="text-sm">from <b>{{ issue.first_seen|date:"j M G:i T" }}</b> | last <b>{{ issue.last_seen|date:"j M G:i T" }}</b> | with <b>{{ issue.digested_event_count|intcomma }}</b> events
<div class="text-sm">from <b>{{ issue.first_seen|date:"j M G:i T" }}</b> | last <b>{{ issue.last_seen|date:"j M G:i T" }}</b> | {% blocktranslate with event_count=issue.digested_event_count|intcomma %}with <b>{{ event_count }}</b> events{% endblocktranslate %}
{% if issue.digested_event_count != issue.stored_event_count %}
<span class="text-xs">({{ issue.stored_event_count|intcomma }}&thinsp;av{#ilable#})<span>
{% endif %}
@@ -195,12 +196,12 @@
No {{ state_filter }} issues found for "{{ q }}"
{% else %}
{% if state_filter == "open" %}
Congratulations! You have no open issues.
{% translate "Congratulations! You have no open issues." %}
{% if project.digested_event_count == 0 %}
This might mean you have not yet <a class="text-cyan-500 dark:text-cyan-300 font-bold" href="{% url "project_sdk_setup" project_pk=project.id %}">set up your SDK</a>.
{% endif %}
{% else %}
No {{ state_filter }} issues found.
{% blocktranslate %}No {{ state_filter }} issues found.{% endblocktranslate %}
{% endif %}
{% endif %}
</div>
@@ -230,12 +231,12 @@
{% endif %}
{% if page_obj.object_list|length > 0 %}{# sounds expensive, but this list is cached #}
Issues {{ page_obj.start_index|intcomma }} {{ page_obj.end_index|intcomma }}
{% translate "Issues" %} {{ page_obj.start_index|intcomma }} {{ page_obj.end_index|intcomma }}
{% else %}
{% if page_obj.number > 1 %}
Less than {{ page_obj.start_index }} Issues {# corresponds to the 1/250 case of having an exactly full page and navigating to an empty page after that #}
{% else %}
0 Issues
0 {% translate "Issues" %}
{% endif %}
{% endif %}

View File

@@ -3,6 +3,7 @@
{% load stricter_templates %}
{% load issues %}
{% load humanize %}
{% load i18n %}
{% block tab_content %}
@@ -21,7 +22,7 @@
</div>
<div class="mt-6 mb-6 italic">
No stacktrace available for this event.
{% translate "No stacktrace available for this event." %}
</div>
{% endif %}

View File

@@ -1,5 +1,6 @@
{% extends "issues/base.html" %}
{% load static %}
{% load i18n %}
{% block tab_content %}
@@ -20,7 +21,7 @@
<h1 class="text-2xl font-bold mt-4">No tags</h1>
<div class="mb-6">
No tags found for this issue.
{% trans "No tags found for this issue." %}
</div>
{% endfor %}

Binary file not shown.

View File

@@ -0,0 +1,891 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-28 14:10+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: bsmain/templates/bsmain/auth_token_list.html:5
#: bsmain/templates/bsmain/auth_token_list.html:23
#: bsmain/templates/bsmain/auth_token_list.html:41
msgid "Auth Tokens"
msgstr "授权令牌"
#: bsmain/templates/bsmain/auth_token_list.html:28
msgid "Add Token"
msgstr "添加令牌"
#: bsmain/templates/bsmain/auth_token_list.html:54
#: issues/templates/issues/issue_list.html:23
#: issues/templates/issues/issue_list.html:147
msgid "Delete"
msgstr "删除"
#: bsmain/templates/bsmain/auth_token_list.html:63
msgid "No Auth Tokens."
msgstr "没有可用令牌。"
#: bsmain/views.py:23
msgid "Token deleted"
msgstr "令牌已删除"
#: issues/models.py:489 issues/templates/issues/base.html:201
msgid "First seen"
msgstr "首次出现"
#: issues/models.py:490 issues/templates/issues/base.html:165
#: issues/templates/issues/issue_list.html:44
msgid "Resolved"
msgstr "已解决"
#: issues/models.py:491 issues/templates/issues/base.html:172
#: issues/templates/issues/issue_list.html:43
msgid "Muted"
msgstr "已静默"
#: issues/models.py:492
msgid "Marked as regressed"
msgstr "标记为"
#: issues/models.py:493
msgid "Unmuted"
msgstr "已取消静默"
#: issues/models.py:495
msgid "Release info added"
msgstr ""
#: issues/models.py:498
msgid "Manual annotation"
msgstr "手动注释"
#: issues/templates/issues/_event_nav.html:5
msgid "search..."
msgstr "搜索..."
#: issues/templates/issues/base.html:23 issues/templates/issues/base.html:37
#: issues/templates/issues/issue_list.html:79
msgid "Resolved in next release"
msgstr ""
#: issues/templates/issues/base.html:28 issues/templates/issues/base.html:56
#: issues/templates/issues/issue_list.html:84
#: issues/templates/issues/issue_list.html:109
msgid "Resolve"
msgstr "解决"
#: issues/templates/issues/base.html:64 issues/templates/issues/base.html:66
#: issues/templates/issues/issue_list.html:117
#: issues/templates/issues/issue_list.html:119
msgid "Mute"
msgstr "静默"
#: issues/templates/issues/base.html:71 issues/templates/issues/base.html:79
#: issues/templates/issues/issue_list.html:124
#: issues/templates/issues/issue_list.html:132
msgid "Mute for/until&nbsp;&nbsp;"
msgstr "静默至/&nbsp;&nbsp;"
#: issues/templates/issues/base.html:88 issues/templates/issues/base.html:90
#: issues/templates/issues/issue_list.html:138
#: issues/templates/issues/issue_list.html:140
msgid "Unmute"
msgstr "取消静默"
#: issues/templates/issues/base.html:112
msgid "Stacktrace"
msgstr "栈追踪"
#: issues/templates/issues/base.html:113
msgid "Event&nbsp;Details"
msgstr "事件详情"
#: issues/templates/issues/base.html:114
msgid "Breadcrumbs"
msgstr "面包屑"
#: issues/templates/issues/base.html:115
msgid "Event&nbsp;List"
msgstr "事件列表"
#: issues/templates/issues/base.html:116
msgid "Tags"
msgstr "标签"
#: issues/templates/issues/base.html:117
msgid "Grouping"
msgstr "分组"
#: issues/templates/issues/base.html:118 issues/templates/issues/history.html:8
#: templates/admin/change_form_object_tools.html:5
msgid "History"
msgstr "历史"
#: issues/templates/issues/base.html:127
#, python-format
msgid ""
"Event %(digest_order)s of %(total_events)s which occured at <span "
"class=\"font-bold\">%(ingested_at)s</span>"
msgstr ""
"事件 %(digest_order)s 共 %(total_events)s 个 发生于 <span class=\"font-"
"bold\">%(ingested_at)s</span>"
#: issues/templates/issues/base.html:130
#: templates/admin/change_form_object_tools.html:8
msgid "Download"
msgstr "下载"
#: issues/templates/issues/base.html:131
msgid "JSON"
msgstr "JSON"
#: issues/templates/issues/base.html:132
msgid "Plain"
msgstr "原始文本"
#: issues/templates/issues/base.html:137
msgid "Event Admin"
msgstr "事件管理"
#: issues/templates/issues/base.html:139
msgid "Issue Admin"
msgstr "问题管理"
#: issues/templates/issues/base.html:157
msgid "Issue"
msgstr "问题"
#: issues/templates/issues/base.html:162
msgid "State"
msgstr "状态"
#: issues/templates/issues/base.html:167
msgid "in"
msgstr "于"
#: issues/templates/issues/base.html:183
#: issues/templates/issues/issue_list.html:41
msgid "Open"
msgstr "打开"
#: issues/templates/issues/base.html:190
msgid "Nr. of events"
msgstr "事件数量"
#: issues/templates/issues/base.html:206
msgid "Last seen"
msgstr "最后出现"
#: issues/templates/issues/base.html:211
msgid "Seen at"
msgstr "出现时间"
#: issues/templates/issues/base.html:234
msgid "Issue Tags"
msgstr "问题标签"
#: issues/templates/issues/breadcrumbs.html:25
msgid "No breadcrumbs available for this event."
msgstr "这个事件没有可用的面包屑。"
#: issues/templates/issues/history.html:9
msgid "Most recent first"
msgstr "新内容优先"
#: issues/templates/issues/history.html:21
msgid "Add comment as manual annotation"
msgstr "添加评论作为手动注释"
#: issues/templates/issues/history.html:26
msgid "Now"
msgstr "现在"
#: issues/templates/issues/history.html:35
msgid "comments..."
msgstr "评论..."
#: issues/templates/issues/history.html:36
msgid "Post comment"
msgstr "发表评论"
#: issues/templates/issues/history.html:59
msgctxt "History"
msgid "by"
msgstr "发表自"
#: issues/templates/issues/issue_list.html:7
#: issues/templates/issues/issue_list.html:31
#: issues/templates/issues/issue_list.html:234
#: issues/templates/issues/issue_list.html:239 theme/templates/base.html:40
msgid "Issues"
msgstr "问题"
#: issues/templates/issues/issue_list.html:15
msgid "Delete Issues"
msgstr "删除问题"
#: issues/templates/issues/issue_list.html:18
msgid ""
"Deleting an Issue is a permanent action and cannot be undone. It's typically "
"better to resolve or mute an issue instead of deleting it, as this allows "
"you to keep track of past issues and their resolutions."
msgstr ""
"删除问题是一个无法被撤销的永久行为。通常更建议标记为解决或者静默,而不是直接"
"删除问题,以便于追踪您过去的问题和决议。"
#: issues/templates/issues/issue_list.html:22
#: projects/templates/projects/project_edit.html:21
#: projects/templates/projects/project_edit.html:52
#: projects/templates/projects/project_member_settings.html:43
#: projects/templates/projects/project_member_settings.html:45
#: projects/templates/projects/project_members_invite.html:52
#: projects/templates/projects/project_new.html:27
#: teams/templates/teams/team_edit.html:30
#: teams/templates/teams/team_member_settings.html:43
#: teams/templates/teams/team_member_settings.html:45
#: teams/templates/teams/team_members_invite.html:54
#: teams/templates/teams/team_new.html:25
#: users/templates/users/user_edit.html:28
msgid "Cancel"
msgstr "取消"
#: issues/templates/issues/issue_list.html:35
msgid ""
"The chosen action is not applicable to all selected issues. Issues for which "
"it has not been applied have been left with checkboxes checked so that you "
"can try again with another action."
msgstr ""
#: issues/templates/issues/issue_list.html:42
msgid "Unresolved"
msgstr "未解决"
#: issues/templates/issues/issue_list.html:45
msgid "All"
msgstr "全部"
#: issues/templates/issues/issue_list.html:49
msgid "Search issues..."
msgstr "搜索问题..."
#: issues/templates/issues/issue_list.html:178
#, python-format
msgid "with <b>%(event_count)s</b> events"
msgstr "共 <b>%(event_count)s</b> 个事件"
#: issues/templates/issues/issue_list.html:199
msgid "Congratulations! You have no open issues."
msgstr "恭喜你!现在没有打开的问题。"
#: issues/templates/issues/issue_list.html:204
#, python-format
msgid "No %(state_filter)s issues found."
msgstr "没有找到 %(state_filter)s 的问题。"
#: issues/templates/issues/stacktrace.html:25
msgid "No stacktrace available for this event."
msgstr "这个事件没有栈追踪。"
#: issues/templates/issues/tags.html:24
msgid "No tags found for this issue."
msgstr "这个问题没有标签。"
#: projects/forms.py:17 teams/forms.py:14 users/forms.py:87
msgid "Email"
msgstr "电子邮件"
#: projects/forms.py:19 projects/forms.py:48 teams/forms.py:16
#: teams/forms.py:46
msgid "Role"
msgstr "角色"
#: projects/forms.py:67
#, python-format
msgid "Default (%s, as per %s settings)"
msgstr ""
#: projects/forms.py:68 teams/forms.py:55 users/forms.py:151
msgid "Send email alerts"
msgstr "发送邮件通知"
#: projects/forms.py:89
msgctxt "Project"
msgid "Name"
msgstr "项目名称"
#: projects/forms.py:90 teams/forms.py:76
msgid "Visibility"
msgstr "可见性"
#: projects/forms.py:92
msgid "Retention max event count"
msgstr "事件保留上限"
#: projects/forms.py:93
msgid "The maximum number of events to store before evicting."
msgstr "超过此数量的事件将会被丢弃。"
#: projects/forms.py:101
msgid "DSN (read-only)"
msgstr "DSN (只读)"
#. Translators: {link} will be replaced with an HTML link; place it where it reads naturally.
#, python-brace-format
msgid "Use the DSN to {link}."
msgstr "使用 DSN 来{link}。"
#: projects/forms.py:103
msgid "set up the SDK"
msgstr "设置 SDK"
#: projects/forms.py:115
#. Translators: This text is followed by a clickable link. Adjust punctuation/spacing naturally.
msgid "You don't have any teams yet; "
msgstr "你还没有任何团队;"
#: projects/forms.py:115
msgid "Create a team first."
msgstr "先创建一个团队。"
#: projects/models.py:50 teams/models.py:9
msgid "Member"
msgstr "成员"
#: projects/models.py:51 projects/templates/projects/project_list.html:87
#: projects/templates/projects/project_members.html:51 teams/models.py:10
#: teams/templates/teams/team_list.html:70
#: teams/templates/teams/team_members.html:51 theme/templates/base.html:45
msgid "Admin"
msgstr "管理员"
#: projects/models.py:56 teams/models.py:15
msgid "Joinable"
msgstr "可加入"
#: projects/models.py:60 teams/models.py:20
msgid "Discoverable"
msgstr "可发现"
#: projects/models.py:64 teams/templates/teams/team_members.html:25
msgid "Team Members"
msgstr "团队成员"
#: projects/models.py:113
msgid "Which users can see this project and its issues?"
msgstr "哪些用户可以看到这个项目以及它们的问题?"
#: projects/templates/projects/project_alerts_setup.html:5
#: projects/templates/projects/project_alerts_setup.html:25
msgid "Alerts"
msgstr "通知"
#: projects/templates/projects/project_alerts_setup.html:28
msgid "Add"
msgstr "添加"
#: projects/templates/projects/project_alerts_setup.html:40
msgid "Messaging Services"
msgstr "消息服务"
#: projects/templates/projects/project_alerts_setup.html:68
msgctxt "Alerts"
msgid "Test"
msgstr "测试"
#: projects/templates/projects/project_alerts_setup.html:69
msgctxt "Alerts"
msgid "Remove"
msgstr "移除"
#: projects/templates/projects/project_alerts_setup.html:78
msgid "No Messaging Services Configured."
msgstr "没有已配置的消息通知服务。"
#: projects/templates/projects/project_alerts_setup.html:78
msgid "Add Messaging Service"
msgstr "添加消息服务"
#: projects/templates/projects/project_alerts_setup.html:92
msgctxt "Alerts"
msgid "Settings"
msgstr "项目设置"
#: projects/templates/projects/project_alerts_setup.html:93
#: projects/templates/projects/project_members.html:90
msgid "Back to Projects"
msgstr "返回项目列表"
#: projects/templates/projects/project_edit.html:6
#: teams/templates/teams/team_edit.html:6
#: users/templates/users/user_edit.html:6
msgid "Edit"
msgstr "编辑"
#: projects/templates/projects/project_edit.html:14
#: projects/templates/projects/project_edit.html:53
msgid "Delete Project"
msgstr "删除项目"
#: projects/templates/projects/project_edit.html:17
msgid ""
"Are you sure you want to delete this project? This action cannot be undone "
"and will delete all associated data."
msgstr "确定删除该项目?该操作不可撤销。"
#: projects/templates/projects/project_edit.html:38
#, python-format
msgid "Settings (%(project_name)s)"
msgstr "项目设置 (%(project_name)s)"
#: projects/templates/projects/project_edit.html:42
#, python-format
msgid "Project settings for \"%(project_name)s\"."
msgstr "项目 \"%(project_name)s\" 的设置。"
#: projects/templates/projects/project_edit.html:51
#: projects/templates/projects/project_member_settings.html:41
#: projects/templates/projects/project_new.html:26
#: teams/templates/teams/team_edit.html:29
#: teams/templates/teams/team_member_settings.html:41
#: teams/templates/teams/team_new.html:24
#: users/templates/users/preferences.html:34
#: users/templates/users/user_edit.html:27
msgid "Save"
msgstr "保存"
#: projects/templates/projects/project_list.html:5
#: projects/templates/projects/project_list.html:14
#: theme/templates/base.html:37
msgid "Projects"
msgstr "项目"
#: projects/templates/projects/project_list.html:21
#: projects/templates/projects/project_new.html:6
#: projects/templates/projects/project_new.html:18
msgid "New Project"
msgstr "创建新项目"
#: projects/templates/projects/project_list.html:42
msgid "My Projects"
msgstr "我的项目"
#: projects/templates/projects/project_list.html:44
msgid "Team Projects"
msgstr "团队项目"
#: projects/templates/projects/project_list.html:45
msgid "Other Projects"
msgstr "其他项目"
#: projects/templates/projects/project_list.html:75
#: teams/templates/teams/team_list.html:58
msgid "members"
msgstr "个成员"
#: projects/templates/projects/project_list.html:78
#: teams/templates/teams/team_list.html:60
msgid "my settings"
msgstr "我的设置"
#: projects/templates/projects/project_list.html:85
msgid "You're&nbsp;invited!"
msgstr "您收到了邀请!"
#: projects/templates/projects/project_list.html:155
#: teams/templates/teams/team_list.html:105
msgid "Invitation"
msgstr "接受邀请"
#: projects/templates/projects/project_list.html:159
#: projects/templates/projects/project_members.html:62
#: teams/templates/teams/team_list.html:109
#: teams/templates/teams/team_members.html:62
msgid "Leave"
msgstr "离开"
#: projects/templates/projects/project_list.html:165
#: teams/templates/teams/team_list.html:115
msgid "Join"
msgstr "加入"
#: projects/templates/projects/project_list.html:175
msgid "No projects found."
msgstr "没有找到项目。"
#: projects/templates/projects/project_member_settings.html:6
msgid "Member settings"
msgstr "成员设置"
#: projects/templates/projects/project_member_settings.html:27
#: teams/templates/teams/team_member_settings.html:27
msgid "Membership settings"
msgstr "成员设置"
#: projects/templates/projects/project_member_settings.html:32
#, python-format
msgid "Your membership settings for project \"%(project_name)s\"."
msgstr "你作为项目 \"%(project_name)s\" 的成员设置。"
#: projects/templates/projects/project_member_settings.html:34
#, python-format
msgid "Settings for project \"%(project_name)s\" and user %(username)s."
msgstr "用户 %(username)s 作为项目 \"%(project_name)s\" 的成员设置。"
#: projects/templates/projects/project_members.html:25
msgid "Project Members"
msgstr "项目成员"
#: projects/templates/projects/project_members.html:28
#: projects/templates/projects/project_members_invite.html:50
#: teams/templates/teams/team_members.html:28
#: teams/templates/teams/team_members_invite.html:52
msgid "Invite Member"
msgstr "邀请成员"
#: projects/templates/projects/project_members.html:49
#: teams/templates/teams/team_members.html:49
msgid "Invitation pending"
msgstr "等待同意邀请"
#: projects/templates/projects/project_members.html:59
#: teams/templates/teams/team_members.html:59
msgid "Reinvite"
msgstr "重新邀请"
#: projects/templates/projects/project_members.html:64
#: teams/templates/teams/team_members.html:64
msgid "Remove"
msgstr "移除团队"
#: projects/templates/projects/project_members.html:75
#: teams/templates/teams/team_members.html:75
msgid "No members yet."
msgstr "当前还没有成员。"
#: projects/templates/projects/project_members.html:75
#: teams/templates/teams/team_members.html:75
msgid "Invite someone</a>."
msgstr "邀请成员</a>。"
#: projects/templates/projects/project_members.html:89
#, python-format
msgid " %(team_name)s Members"
msgstr "%(team_name)s 团队的成员"
#: projects/templates/projects/project_members_accept.html:18
msgid "Invitation to"
msgstr "受邀加入"
#: projects/templates/projects/project_members_accept.html:22
#, python-format
msgid ""
"You have been invited to join the project \"%(project_name)s\" in the role "
"of \"%(role)s\". Please confirm by clicking the button below."
msgstr ""
"您正在作为 \"%(role)s\" 被邀请加入项目 \"%(project_name)s\" 项目的角色。请单"
"击下面的按钮确认。"
#: projects/templates/projects/project_members_accept.html:25
msgid "Accept"
msgstr "接受"
#: projects/templates/projects/project_members_accept.html:26
msgid "Decline"
msgstr "拒绝"
#: projects/templates/projects/project_members_invite.html:26
#, python-format
msgid "Invite members (%(project_name)s)"
msgstr "邀请成员 (%(project_name)s)"
#: projects/templates/projects/project_members_invite.html:30
#, python-format
msgid ""
"Invite a member to join the project \"%(project_name)s\". They will receive "
"an email with a link to join."
msgstr ""
"邀请一名新的成员加入项目 \"%(project_name)s\"。对方将收到一封含有加入团队链接"
"的电子邮件。"
#: projects/templates/projects/project_members_invite.html:51
#: teams/templates/teams/team_members_invite.html:53
msgid "Invite and add another"
msgstr "邀请并继续添加"
#: projects/views.py:82
#, python-format
msgid "You have joined the project \"%s\""
msgstr "你已经加入了项目 \"%s\""
#: teams/forms.py:54
#, python-format
msgid "User-default (%s)"
msgstr "用户缺省 (%s)"
#: teams/forms.py:75
msgctxt "Team"
msgid "Name"
msgstr "团队名称"
#: teams/models.py:23
msgid "Hidden"
msgstr ""
#: teams/models.py:33
msgid "Which users can see this team and its issues?"
msgstr "哪些用户可以看到这个团队以及它们的问题?"
#: teams/templates/teams/team_edit.html:18
#, python-format
msgid "Settings (%(team_name)s)"
msgstr "团队设置 (%(team_name)s)"
#: teams/templates/teams/team_edit.html:22
#, python-format
msgid "Team settings for \"%(team_name)s\"."
msgstr "团队 \"%(team_name)s\" 的设置。"
#: teams/templates/teams/team_list.html:5
#: teams/templates/teams/team_list.html:14 theme/templates/base.html:34
msgid "Teams"
msgstr "团队"
#: teams/templates/teams/team_list.html:21
#: teams/templates/teams/team_new.html:18
msgid "New Team"
msgstr "创建新团队"
#: teams/templates/teams/team_list.html:32
msgid "My Teams"
msgstr "我的团队"
#: teams/templates/teams/team_list.html:33
msgid "Other Teams"
msgstr "其他团队"
#: teams/templates/teams/team_list.html:57
msgid "projects"
msgstr "个项目"
#: teams/templates/teams/team_list.html:125
msgid "No teams found."
msgstr "没有找到团队。"
#: teams/templates/teams/team_member_settings.html:32
#, python-format
msgid "Your membership settings for team \"%(team_name)s\"."
msgstr "你作为团队 \"%(team_name)s\" 的成员设置。"
#: teams/templates/teams/team_member_settings.html:34
#, python-format
msgid "Settings for team \"%(team_name)s\" and user %(username)s."
msgstr "用户 %(username)s 作为团队 \"%(team_name)s\" 的成员设置。"
#: teams/templates/teams/team_members.html:5
msgid "Members"
msgstr "成员"
#: teams/templates/teams/team_members.html:89
msgid "Back to Teams"
msgstr "返回团队列表"
#: teams/templates/teams/team_members_invite.html:28
#, python-format
msgid "Invite members (%(team_name)s)"
msgstr "邀请成员 (%(team_name)s)"
#: teams/templates/teams/team_members_invite.html:32
#, python-format
msgid ""
"Invite a member to join the team \"%(team_name)s\". They will receive an "
"email with a link to join."
msgstr ""
"邀请一名新的成员加入团队 \"%(team_name)s\"。对方将收到一封含有加入团队链接的"
"电子邮件。"
#: teams/views.py:58
#, python-format
msgid "You have joined the team \"%s\""
msgstr "你已经加入了团队 \"%s\""
#: templates/admin/change_form_object_tools.html:7
msgid "Raw"
msgstr ""
#: templates/admin/change_form_object_tools.html:9
msgid "View on site"
msgstr ""
#: templates/bugsink/login.html:18
msgid "Your username and password didn't match. Please try again."
msgstr "用户名和密码不匹配,请重试"
#: templates/bugsink/login.html:22
msgid ""
"Your account doesn't have access to this page. To proceed, please login with "
"an account that has access."
msgstr "您的帐户没有权限访问此页面,请登录具有权限的账户后重试"
#: templates/bugsink/login.html:24
msgid "Please login to see this page."
msgstr "请登录后查看此页面"
#: templates/bugsink/login.html:34
msgid "Username"
msgstr "用户名"
#: templates/bugsink/login.html:40
msgid "Password"
msgstr "密码"
#: templates/bugsink/login.html:43 theme/templates/base.html:54
msgid "Login"
msgstr "登录"
#: templates/bugsink/login.html:48
msgid "Forgot password?"
msgstr "忘记密码?"
#: templates/bugsink/login.html:49
msgid "Create an account"
msgstr "创建一个新账户"
#: templates/bugsink/settings.html:7
msgid "Settings"
msgstr "设置"
#: templates/bugsink/settings.html:14
msgid "Site Settings"
msgstr "站点设置"
#: theme/templates/base.html:49 users/templates/users/user_list.html:45
#: users/templates/users/user_list.html:63
msgid "Users"
msgstr "用户"
#: theme/templates/base.html:50
msgid "Tokens"
msgstr "令牌"
#: theme/templates/base.html:56
msgid "Preferences"
msgstr "偏好"
#: theme/templates/base.html:57
msgid "Log out"
msgstr "登出"
#: users/forms.py:17
msgid "Yes"
msgstr "是"
#: users/forms.py:18
msgid "No"
msgstr "否"
#: users/forms.py:137
msgid "Auto (browser preference)"
msgstr "自动(浏览器偏好)"
#: users/forms.py:153
msgid "Theme preference"
msgstr "主题偏好"
#: users/forms.py:159
msgid "Language"
msgstr "语言"
#: users/models.py:21
msgid "System Default"
msgstr "系统缺省"
#: users/models.py:22
msgid "Light"
msgstr "明亮"
#: users/models.py:23
msgid "Dark"
msgstr "黑暗"
#: users/templates/users/confirm_email.html:18
msgid "Confirm your email address by clicking the button below."
msgstr ""
#: users/templates/users/confirm_email.html:23
msgid "Confirm"
msgstr "确认"
#: users/templates/users/confirm_email_sent.html:17
msgid ""
"A verification email has been sent to your email address. Please verify your "
"email address to complete the registration process."
msgstr ""
#: users/templates/users/logged_out.html:16
msgid "You have been logged out."
msgstr "你已经从系统中登出。"
#: users/templates/users/logged_out.html:16
msgid "Log in again</a>."
msgstr "重新登录</a>。"
#: users/templates/users/preferences.html:27
msgid "User Preferences"
msgstr "用户偏好"
#: users/templates/users/request_reset_password.html:23
#: users/templates/users/reset_password.html:27
msgid "Reset password"
msgstr "重置密码"
#: users/templates/users/request_reset_password.html:27
#: users/templates/users/reset_password.html:32
msgid "Log in instead"
msgstr ""
#: users/templates/users/resend_confirmation.html:23
msgid "Resend verification email"
msgstr ""
#: users/templates/users/reset_password_email_sent.html:16
msgid ""
"A password reset link has been sent to your email address. Please check your "
"inbox and follow the instructions to reset your password."
msgstr ""
"一封含有密码重置链接的电子邮件已经发送给您。请您检查您的收件箱并按说明重置密"
"码。"
#: users/templates/users/user_edit.html:22
#, python-format
msgid "Settings for %(username)s."
msgstr "%(username)s 的设置。"
#: users/templates/users/user_list.html:16
msgid ""
"Are you sure you want to delete this user? This action cannot be undone."
msgstr "确定删除该用户?该操作不可撤销。"
#: users/templates/users/user_list.html:72
msgid "Superuser"
msgstr "超级管理员"
#: users/views.py:237
msgid "Updated preferences"
msgstr "偏好已更新"
# Fix on https://code.djangoproject.com/ticket/36579
msgid "yes,no,maybe"
msgstr "是,否,也许"

View File

@@ -2,6 +2,8 @@ from django import forms
from django.contrib.auth import get_user_model
from django.template.defaultfilters import yesno
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html
from bugsink.utils import assert_
from teams.models import TeamMembership
@@ -12,9 +14,10 @@ User = get_user_model()
class ProjectMemberInviteForm(forms.Form):
email = forms.EmailField(label='Email', required=True)
email = forms.EmailField(label=_('Email'), required=True)
role = forms.ChoiceField(
label='Role', choices=ProjectRole.choices, required=True, initial=ProjectRole.MEMBER, widget=forms.RadioSelect)
label=_('Role'), choices=ProjectRole.choices, required=True, initial=ProjectRole.MEMBER,
widget=forms.RadioSelect)
def __init__(self, user_must_exist, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -59,7 +62,7 @@ class MyProjectMembershipForm(forms.ModelForm):
sea_defined_at = "user"
sea_default = self.instance.user.send_email_alerts
empty_label = 'Default (%s, as per %s settings)' % (yesno(sea_default).capitalize(), sea_defined_at)
empty_label = _('Default (%s, as per %s settings)') % (yesno(sea_default).capitalize(), sea_defined_at)
self.fields['send_email_alerts'].empty_label = empty_label
self.fields['send_email_alerts'].widget.choices[0] = ("unknown", empty_label)
@@ -80,7 +83,7 @@ class ProjectForm(forms.ModelForm):
team_qs = kwargs.pop("team_qs", None)
super().__init__(*args, **kwargs)
self.fields["retention_max_event_count"].help_text = "The maximum number of events to store before evicting."
self.fields["retention_max_event_count"].help_text = _("The maximum number of events to store before evicting.")
if self.instance is not None and self.instance.pk is not None:
# for editing, we disallow changing the team. consideration: it's somewhat hard to see what the consequences
# for authorization are (from the user's perspective).
@@ -88,10 +91,13 @@ class ProjectForm(forms.ModelForm):
# for editing, the DSN is availabe, but read-only
self.fields["dsn"].initial = self.instance.dsn
self.fields["dsn"].label = "DSN (read-only)"
self.fields["dsn"].help_text = 'Use the DSN to <a href="' +\
reverse('project_sdk_setup', kwargs={'project_pk': self.instance.pk}) +\
'" class="text-cyan-800 font-bold">set up the SDK</a>.'
self.fields["dsn"].label = _("DSN (read-only)")
href = reverse('project_sdk_setup', kwargs={'project_pk': self.instance.pk})
self.fields["dsn"].help_text = format_html(
_("Use the DSN to {link}."),
link=format_html('<a href="{}" class="text-cyan-800 font-bold">{}</a>', href, _("set up the SDK")),
)
# if we ever push slug to the form, editing it should probably be disallowed as well (but mainly because it
# has consequences on the issue's short identifier)
@@ -102,9 +108,11 @@ class ProjectForm(forms.ModelForm):
# it suggests at least somewhere that teams are a thing)
self.fields["team"].queryset = team_qs
if team_qs.count() == 0:
self.fields["team"].help_text = 'You don\'t have any teams yet; <a href="' +\
reverse("team_new") +\
'" class="text-cyan-800 font-bold">Create a team first.</a>'
href = reverse("team_new")
self.fields["team"].help_text = format_html(
"{}{}", _("You don't have any teams yet; "),
format_html('<a href="{}" class="text-cyan-800 font-bold">{}</a>', href, _("Create a team first.")))
elif team_qs.count() == 1:
self.fields["team"].initial = team_qs.first()

View File

@@ -3,6 +3,7 @@ import uuid
from django.db import models
from django.conf import settings
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from bugsink.app_settings import get_settings
from bugsink.transaction import delay_on_commit
@@ -46,21 +47,21 @@ from .tasks import delete_project_deps
class ProjectRole(models.IntegerChoices):
MEMBER = 0
ADMIN = 1
MEMBER = 0, _("Member")
ADMIN = 1, _("Admin")
class ProjectVisibility(models.IntegerChoices):
# PUBLIC = 0 # anyone can see the project and its members; not sure if I want this or always require click-in
JOINABLE = 1 # anyone can join
JOINABLE = 1, _("Joinable") # anyone can join
# the project's existance is visible, but the project itself is not. the idea would be that you can "request to
# join" (which is currently not implemented as a button, but you could do it 'out of bands' i.e. via email or chat).
DISCOVERABLE = 10
DISCOVERABLE = 10, _("Discoverable")
# the project's exsitance is only visible to team-members; you still need to explicitly click "join" which will
# immediately make you a member
TEAM_MEMBERS = 99
TEAM_MEMBERS = 99, _("Team Members")
# having projects that are part of a certain team, but not visible to the team members, was considered, but
# rejected. The basic thinking on the rejection is: it would hollow out the concept of Team to the point of
@@ -75,7 +76,7 @@ class Project(models.Model):
team = models.ForeignKey("teams.Team", blank=False, null=True, on_delete=models.SET_NULL)
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
name = models.CharField(pgettext_lazy("Project", "Name"), max_length=255, blank=False, null=False, unique=True)
slug = models.SlugField(max_length=50, blank=False, null=False, unique=True)
is_deleted = models.BooleanField(default=False)
@@ -108,15 +109,15 @@ class Project(models.Model):
# visibility
visibility = models.IntegerField(
choices=ProjectVisibility.choices, default=ProjectVisibility.TEAM_MEMBERS,
help_text="Which users can see this project and its issues?")
_("Visibility"), choices=ProjectVisibility.choices, default=ProjectVisibility.TEAM_MEMBERS,
help_text=_("Which users can see this project and its issues?"))
# ingestion/digestion quota
quota_exceeded_until = models.DateTimeField(null=True, blank=True)
next_quota_check = models.PositiveIntegerField(null=False, default=0)
# retention
retention_max_event_count = models.PositiveIntegerField(default=10_000)
retention_max_event_count = models.PositiveIntegerField(_("Retention max event count"), default=10_000)
def __str__(self):
return self.name
@@ -178,9 +179,9 @@ class ProjectMembership(models.Model):
project = models.ForeignKey(Project, on_delete=models.DO_NOTHING)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
send_email_alerts = models.BooleanField(default=None, null=True)
send_email_alerts = models.BooleanField(_("Send email alerts"), default=None, null=True)
role = models.IntegerField(choices=ProjectRole.choices, default=ProjectRole.MEMBER)
role = models.IntegerField(_("Role"), choices=ProjectRole.choices, default=ProjectRole.MEMBER)
accepted = models.BooleanField(default=False)
def __str__(self):

View File

@@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Alerts · {{ project.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Alerts" %} · {{ project.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -21,10 +22,10 @@
{% endif %}
<div class="flex">
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} · Alerts</h1>
<h1 class="text-4xl mt-4 font-bold">{{ project.name }} · {% translate "Alerts" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">Add</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_messaging_service_add" project_pk=project.pk %}">{% translate "Add" %}</a>
</div>
</div>
@@ -36,7 +37,7 @@
<tbody>
<thead>
<tr class="bg-slate-200 dark:bg-slate-800">
<th class="w-full p-4 text-left text-xl" colspan="2">Messaging Services</th>
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Messaging Services" %}</th>
</tr>
{% for service_config in service_configs %}
@@ -64,8 +65,8 @@
<td class="p-4">
<div class="flex justify-end">
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Test</button>
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
<button name="action" value="test:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Test" context "Alerts" %}</button>
<button name="action" value="remove:{{ service_config.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" context "Alerts" %}</button>
</div>
</td>
@@ -74,7 +75,7 @@
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
<td class="w-full p-4">
<div>
No Messaging Services Configured. <a href="{% url "project_messaging_service_add" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Add Messaging Service</a>.
{% translate "No Messaging Services Configured." %} <a href="{% url "project_messaging_service_add" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Add Messaging Service" %}</a>.
</div>
</td>
</tr>
@@ -88,8 +89,8 @@
<div class="flex flex-direction-row">
<div class="ml-auto py-8 pr-4">
<a href="{% url "project_edit" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Settings</a>
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Projects</a>
<a href="{% url "project_edit" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Settings" context "Alerts" %}</a>
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Projects" %}</a>
</div>
</div>
</div>

View File

@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Edit {{ project.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Edit" %} {{ project.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -10,14 +11,14 @@
<div id="deleteModal" class="hidden fixed inset-0 bg-slate-600 dark:bg-slate-900 bg-opacity-50 dark:bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="relative p-6 border border-slate-300 dark:border-slate-600 w-96 shadow-lg rounded-md bg-white dark:bg-slate-900">
<div class="text-center m-4">
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete Project</h3>
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">{% translate "Delete Project" %}</h3>
<div class="mt-4 mb-6">
<p class="text-slate-700 dark:text-slate-300">
Are you sure you want to delete this project? This action cannot be undone and will delete all associated data.
{% translate "Are you sure you want to delete this project? This action cannot be undone and will delete all associated data." %}
</p>
</div>
<div class="flex items-center justify-center space-x-4 mb-4">
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">Cancel</button>
<button id="cancelDelete" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Cancel" %}</button>
<form method="post" action="." id="deleteForm">
{% csrf_token %}
<input type="hidden" name="action" value="delete">
@@ -34,11 +35,11 @@
{% csrf_token %}
<div>
<h1 class="text-4xl my-4 font-bold">Settings ({{ project.name }})</h1>
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with project_name=project.name %}Settings ({{ project_name }}){% endblocktranslate %}</h1>
</div>
<div class="mt-4 mb-4">
Project settings for "{{ project.name }}".
{% blocktranslate with project_name=project.name %}Project settings for "{{ project_name }}".{% endblocktranslate %}
</div>
{% tailwind_formfield form.name %}
@@ -47,9 +48,9 @@
{% tailwind_formfield form.dsn %}
<div class="flex items-center mt-4">
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4 ml-auto">Delete Project</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
<button type="button" id="deleteButton" class="font-bold py-2 px-4 rounded bg-red-500 dark:bg-red-700 text-white border-2 border-red-600 dark:border-red-400 hover:bg-red-600 dark:hover:bg-red-800 active:ring ml-4 ml-auto">{% translate "Delete Project" %}</button>
</div>
</form>
</div>

View File

@@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Projects · {{ site_title }}{% endblock %}
{% block title %}{% translate "Projects" %} · {{ site_title }}{% endblock %}
{% block content %}
@@ -10,14 +11,14 @@
<div class="m-4 flex flex-row items-end">
<div><!-- top, LHS (h1) -->
<h1 class="text-4xl mt-4 font-bold">Projects</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Projects" %}</h1>
</div>
{# align to bottom #}
<div class="ml-auto"><!-- top, RHS (buttons) -->
{% if can_create %}
<div>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'project_new' %}">New Project</a>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'project_new' %}">{% translate "New Project" %}</a>
</div>
{% endif %}
</div> {# top, RHS (buttons) #}
@@ -38,10 +39,10 @@
<div class="flex bg-slate-50 dark:bg-slate-800 mt-4 items-end">
<div class="flex">
<a href="{% url "project_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">My Projects</div></a>
<a href="{% url "project_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "My Projects" %}</div></a>
{% if not app_settings.SINGLE_USER %}
<a href="{% url "project_list_teams" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "teams" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Team Projects</div></a>
<a href="{% url "project_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">Other Projects</div></a>
<a href="{% url "project_list_teams" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "teams" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Team Projects" %}</div></a>
<a href="{% url "project_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-300 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400{% endif %}">{% translate "Other Projects" %}</div></a>
{% endif %}
</div>
{% comment %}
@@ -70,19 +71,19 @@
</div>
<div>
{{ project.team.name }}
| {{ project.member_count }} members
| {{ project.member_count }} {% translate "members" %}
{# | {{ project.open_issue_count }} open issues #}
{% if project.member %}
| <a href="{% url 'project_member_settings' project_pk=project.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">my settings</a>
| <a href="{% url 'project_member_settings' project_pk=project.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">{% translate "my settings" %}</a>
{% endif %}
</div>
</td>
<td class="pr-2 text-center">
{% if project.member %}
{% if not project.member.accepted %}
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">You're&nbsp;invited!</span>
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "You're&nbsp;invited!" %}</span>
{% elif project.member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users; #}
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "Admin" %}</span>
{% endif %}
{% endif %}
</td>
@@ -150,17 +151,17 @@
{% if project.member %}
{% if not project.member.accepted %}
<div>
<a href="{% url 'project_members_accept' project_pk=project.id %}" class="font-bold text-cyan-500 dark:text-cyan-300">Invitation</a>
<a href="{% url 'project_members_accept' project_pk=project.id %}" class="font-bold text-cyan-500 dark:text-cyan-300 whitespace-nowrap">{% translate "Invitation" %}</a>
</div>
{% else %}
<div>
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
<button name="action" value="leave:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
</div>
{% endif %}
{% else %}
{% if ownership_filter == "teams" or project.is_joinable or request.user.is_superuser %}{# ownership_filter check: you can always join your own team's projects, so if you're looking at a list of them... #}
<div>
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring rounded-md">Join</button>
<button name="action" value="join:{{ project.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 dark:hover:bg-slate-700 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
</div>
{% endif %}
{% endif %}
@@ -170,7 +171,7 @@
{% empty %}
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
<td class="w-full p-4">
No projects found.
{% translate "No projects found." %}
</td>
</tr>

View File

@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Member settings · {{ project.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Member settings" %} · {{ project.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -23,25 +24,25 @@
{% endif %}
<div>
<h1 class="text-4xl my-4 font-bold">Membership settings</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "Membership settings" %}</h1>
</div>
<div class="mt-4 mb-4">
{% if this_is_you %}
Your membership settings for project "{{ project.name }}".
{% blocktranslate with project_name=project.name %}Your membership settings for project "{{ project_name }}".{% endblocktranslate %}
{% else %}
Settings for project "{{ project.name }}" and user {{ user.username }}.
{% blocktranslate with project_name=project.name username=user.username %}Settings for project "{{ project_name }}" and user {{ username }}.{% endblocktranslate %}
{% endif %}
</div>
{% tailwind_formfield form.role %}
{% tailwind_formfield form.send_email_alerts %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
{% if this_is_you %}
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
{% else %}
<a href="{% url "project_members" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<a href="{% url "project_members" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
{% endif %}
</form>

View File

@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Members · {{ project.name }} · {{ site_title }}{% endblock %}
@@ -21,10 +22,10 @@
{% endif %}
<div class="flex">
<h1 class="text-4xl mt-4 font-bold">Project Members</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Project Members" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">Invite Member</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "project_members_invite" project_pk=project.pk %}">{% translate "Invite Member" %}</a>
</div>
</div>
@@ -45,9 +46,9 @@
<div>
<a href="{% url "project_member_settings" project_pk=project.pk user_pk=member.user_id %}" class="text-xl text-cyan-500 font-bold">{{ member.user.email }}</a> {# "best name" perhaps later? #}
{% if not member.accepted %}
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation pending</span>
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Invitation pending" %}</span>
{% elif member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Admin" %}</span>
{% endif %}
</div>
</td>
@@ -55,12 +56,12 @@
<td class="p-4">
<div class="flex justify-end">
{% if not member.accepted %}
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reinvite</button>
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
{% endif %}
{% if request.user == member.user %}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
{% endif %}
</div>
</td>
@@ -71,7 +72,7 @@
<td class="w-full p-4">
<div>
{# Note: this is already somewhat exceptional, because the usually you'll at least see yourself here (unless you're a superuser and a project has become memberless) #}
No members yet. <a href="{% url "project_members_invite" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Invite someone</a>.
{% translate "No members yet." %} <a href="{% url "project_members_invite" project_pk=project.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Invite someone</a>." %}
</div>
</td>
</tr>
@@ -85,8 +86,8 @@
<div class="flex flex-direction-row">
<div class="ml-auto py-8 pr-4">
<a href="{% url "team_members" team_pk=project.team_id %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{{ project.team.name }} Members</a>
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Projects</a>
<a href="{% url "team_members" team_pk=project.team_id %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% blocktranslate with team_name=project.team.name %} {{ team_name }} Members{% endblocktranslate %}</a>
<span class="font-bold text-slate-500 dark:text-slate-300">|</span> <a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Projects" %}</a>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Invitation · {{ project.name }} · {{ site_title }}{% endblock %}
@@ -14,15 +15,15 @@
{% csrf_token %}
<div>
<h1 class="text-4xl my-4 font-bold">Invitation to "{{ project.name }}"</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "Invitation to" %} "{{ project.name }}"</h1>
</div>
<div class="mt-4 mb-4">
You have been invited to join the project "{{ project.name }}" in the role of "{{ membership.get_role_display }}". Please confirm by clicking the button below.
{% blocktranslate with project_name=project.name role=membership.get_role_display %}You have been invited to join the project "{{ project_name }}" in the role of "{{ role }}". Please confirm by clicking the button below.{% endblocktranslate %}
</div>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Accept</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Decline</button>
<button name="action" value="accept" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Accept" %}</button>
<button name="action" value="decline" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Decline" %}</button>
</form>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Invite Members · {{ project.name }} · {{ site_title }}{% endblock %}
@@ -22,11 +23,11 @@
{% endif %}
<div>
<h1 class="text-4xl my-4 font-bold">Invite members ({{ project.name }})</h1>
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with project_name=project.name %}Invite members ({{ project_name }}){% endblocktranslate %}</h1>
</div>
<div class="mt-4 mb-4">
Invite a member to join the project "{{ project.name }}". They will receive an email with a link to join.
{% blocktranslate with project_name=project.name %}Invite a member to join the project "{{ project_name }}". They will receive an email with a link to join.{% endblocktranslate %}
</div>
{% tailwind_formfield_implicit form.email %}
@@ -46,9 +47,9 @@
{% endif %}
</div>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Invite Member</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Invite and add another</button>
<a href="{% url "project_members" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">Cancel</a>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
<a href="{% url "project_members" project_pk=project.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
</form>

View File

@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}New project · {{ site_title }}{% endblock %}
{% block title %}{% translate "New Project" %} · {{ site_title }}{% endblock %}
{% block content %}
@@ -14,7 +15,7 @@
{% csrf_token %}
<div>
<h1 class="text-4xl my-4 font-bold">New Project</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "New Project" %}</h1>
</div>
{% tailwind_formfield form.team %}
@@ -22,8 +23,8 @@
{% tailwind_formfield form.visibility %}
{% tailwind_formfield form.retention_max_event_count %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<a href="{% url "project_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>

View File

@@ -11,6 +11,7 @@ from django.contrib import messages
from django.contrib.auth import logout
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from users.models import EmailVerification
from teams.models import TeamMembership, Team, TeamRole
@@ -78,7 +79,7 @@ def project_list(request, ownership_filter=None):
if not project.is_joinable(user=request.user) and not request.user.is_superuser:
raise PermissionDenied("This project is not joinable")
messages.success(request, 'You have joined the project "%s"' % project.name)
messages.success(request, _('You have joined the project "%s"') % project.name)
ProjectMembership.objects.create(
project_id=project_pk, user_id=request.user.id, role=ProjectRole.MEMBER, accepted=True)
return redirect('project_member_settings', project_pk=project_pk, user_pk=request.user.id)

View File

@@ -1,6 +1,7 @@
from django import forms
from django.contrib.auth import get_user_model
from django.template.defaultfilters import yesno
from django.utils.translation import gettext_lazy as _
from bugsink.utils import assert_
from .models import TeamRole, TeamMembership, Team
@@ -9,9 +10,9 @@ User = get_user_model()
class TeamMemberInviteForm(forms.Form):
email = forms.EmailField(label='Email', required=True)
email = forms.EmailField(label=_('Email'), required=True)
role = forms.ChoiceField(
label='Role', choices=TeamRole.choices, required=True, initial=TeamRole.MEMBER, widget=forms.RadioSelect)
label=_('Role'), choices=TeamRole.choices, required=True, initial=TeamRole.MEMBER, widget=forms.RadioSelect)
def __init__(self, user_must_exist, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -37,6 +38,7 @@ class MyTeamMembershipForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
edit_role = kwargs.pop("edit_role")
super().__init__(*args, **kwargs)
assert_(self.instance is not None, "This form is only implemented for editing")
@@ -44,7 +46,9 @@ class MyTeamMembershipForm(forms.ModelForm):
del self.fields['role']
global_send_email_alerts = self.instance.user.send_email_alerts
empty_label = "User-default (%s)" % yesno(global_send_email_alerts).capitalize()
global_send_email_alerts_text = yesno(global_send_email_alerts).capitalize()
empty_label = _("User-default (%s)") % global_send_email_alerts_text
self.fields['send_email_alerts'].empty_label = empty_label
self.fields['send_email_alerts'].widget.choices[0] = ("unknown", empty_label)

View File

@@ -3,34 +3,36 @@ import uuid
from django.db import models
from django.conf import settings
from django.utils.translation import gettext_lazy as _, pgettext_lazy
class TeamRole(models.IntegerChoices):
MEMBER = 0
ADMIN = 1
MEMBER = 0, _("Member")
ADMIN = 1, _("Admin")
class TeamVisibility(models.IntegerChoices):
# PUBLIC = 0 # anyone can see the team and its members not sure if I want this or always want to require click-in
JOINABLE = 1 # anyone can join
JOINABLE = 1, _("Joinable") # anyone can join
# the team's existance is visible in lists, but there is no "Join" button. the idea would be that you can "request
# to join" (which is currently not implemented as a button, but you could do it 'out of bands' i.e. via email or
# chat).
DISCOVERABLE = 10
DISCOVERABLE = 10, _("Discoverable")
# the team is not visible to non-members; you need to be invited
HIDDEN = 99
HIDDEN = 99, _("Hidden")
class Team(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
name = models.CharField(pgettext_lazy("Team", "Name"), max_length=255, blank=False, null=False, unique=True)
visibility = models.IntegerField(
_("Visibility"),
choices=TeamVisibility.choices, default=TeamVisibility.DISCOVERABLE,
help_text="Which users can see this team and its issues?")
help_text=_("Which users can see this team and its issues?"))
def __str__(self):
return self.name
@@ -48,8 +50,8 @@ class TeamMembership(models.Model):
team = models.ForeignKey(Team, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
send_email_alerts = models.BooleanField(default=None, null=True, blank=True)
role = models.IntegerField(choices=TeamRole.choices, default=TeamRole.MEMBER)
send_email_alerts = models.BooleanField(_("Send email alerts"), default=None, null=True, blank=True)
role = models.IntegerField(_("Role"), choices=TeamRole.choices, default=TeamRole.MEMBER)
accepted = models.BooleanField(default=False)
def __str__(self):

View File

@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Edit {{ team.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Edit" %} {{ team.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -14,18 +15,20 @@
{% csrf_token %}
<div>
<h1 class="text-4xl my-4 font-bold">Settings ({{ team.name }})</h1>
<h1 class="text-4xl my-4 font-bold">{% blocktranslate with team_name=team.name %}Settings ({{ team_name }}){% endblocktranslate %}</h1>
</div>
<div class="mt-4 mb-4">
Team settings for "{{ team.name }}".
{% blocktranslate with team_name=team.name %}Team settings for "{{ team_name }}".{% endblocktranslate %}
</div>
{% tailwind_formfield form.name %}
{% tailwind_formfield form.visibility %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<div class="flex items-center mt-4">
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</div>
</form>
</div>

View File

@@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Teams · {{ site_title }}{% endblock %}
{% block title %}{% translate "Teams" %} · {{ site_title }}{% endblock %}
{% block content %}
@@ -10,14 +11,14 @@
<div class="m-4 flex flex-row items-end">
<div><!-- top, LHS (h1) -->
<h1 class="text-4xl mt-4 font-bold">Teams</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Teams" %}</h1>
</div>
{# align to bottom #}
<div class="ml-auto"><!-- top, RHS (buttons) -->
{% if can_create %}
<div>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'team_new' %}">New Team</a>
<a class="block font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url 'team_new' %}">{% translate "New Team" %}</a>
</div>
{% endif %}
</div> {# top, RHS (buttons) #}
@@ -28,8 +29,8 @@
<div class="flex bg-slate-50 dark:bg-slate-800 mt-4 items-end">
<div class="flex">
<a href="{% url "team_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">My Teams</div></a>
<a href="{% url "team_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">Other Teams</div></a>
<a href="{% url "team_list_mine" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "mine" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">{% translate "My Teams" %}</div></a>
<a href="{% url "team_list_other" %}"><div class="p-4 font-bold text-xl hover:bg-slate-200 dark:hover:bg-slate-800 {% if ownership_filter == "other" %}text-cyan-500 dark:text-cyan-400 border-cyan-500 border-b-4 {% else %}text-slate-500 dark:text-slate-300 hover:border-b-4 hover:border-slate-400 dark:hover:border-slate-500{% endif %}">{% translate "Other Teams" %}</div></a>
</div>
{% comment %}
<div class="ml-auto p-2">
@@ -52,10 +53,10 @@
{{ team.name }}
</div>
<div>
{{ team.project_count }} projects
| {{ team.member_count }} members
{{ team.project_count }} {% translate "projects" %}
| {{ team.member_count }} {% translate "members" %}
{% if team.member %}
| <a href="{% url 'team_member_settings' team_pk=team.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">my settings</a>
| <a href="{% url 'team_member_settings' team_pk=team.id user_pk=request.user.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">{% translate "my settings" %}</a>
{% endif %}
</div>
</td>
@@ -65,7 +66,7 @@
{% if not team.member.accepted %}
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">You're&nbsp;invited!</span>
{% elif team.member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm whitespace-nowrap">{% translate "Admin" %}</span>
{% endif %}
{% endif %}
</td>
@@ -100,17 +101,17 @@
{% if team.member %}
{% if not team.member.accepted %}
<div>
<a href="{% url 'team_members_accept' team_pk=team.id %}" class="font-bold text-cyan-500 dark:text-cyan-400">Invitation</a>
<a href="{% url 'team_members_accept' team_pk=team.id %}" class="font-bold text-cyan-500 dark:text-cyan-400 whitespace-nowrap">{% translate "Invitation" %}</a>
</div>
{% else %}
<div>
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
<button name="action" value="leave:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
</div>
{% endif %}
{% else %}
{% if team.is_joinable or request.user.is_superuser %}
<div>
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Join</button>
<button name="action" value="join:{{ team.id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Join" %}</button>
</div>
{% endif %}
{% endif %}
@@ -120,7 +121,7 @@
{% empty %}
<tr class="bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-700 border-b-2">
<td class="w-full p-4">
No teams found.
{% translate "No teams found." %}
</td>
</tr>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Member settings · {{ team.name }} · {{ site_title }}{% endblock %}
@@ -23,25 +24,25 @@
{% endif %}
<div>
<h1 class="text-4xl my-4 font-bold">Membership settings</h1>
<h1 class="text-4xl my-4 font-bold">{% trans "Membership settings" %}</h1>
</div>
<div class="mt-4 mb-4">
{% if this_is_you %}
Your membership settings for team "{{ team.name }}".
{% blocktrans with team_name=team.name %}Your membership settings for team "{{ team_name }}".{% endblocktrans %}
{% else %}
Settings for team "{{ team.name }}" and user {{ user.username }}.
{% blocktrans with team_name=team.name username=user.username %}Settings for team "{{ team_name }}" and user {{ username }}.{% endblocktrans %}
{% endif %}
</div>
{% tailwind_formfield form.role %}
{% tailwind_formfield form.send_email_alerts %}
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<button class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
{% if this_is_you %}
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a> {# not quite perfect, because "you" can also click on yourself in the member list #}
{% else %}
<a href="{% url "team_members" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<a href="{% url "team_members" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
{% endif %}
</form>

View File

@@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Members · {{ team.name }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Members" %} · {{ team.name }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -21,10 +22,10 @@
{% endif %}
<div class="flex">
<h1 class="text-4xl mt-4 font-bold">Team Members</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Team Members" %}</h1>
<div class="ml-auto mt-6">
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">Invite Member</a>
<a class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 ml-1 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md" href="{% url "team_members_invite" team_pk=team.pk %}">{% translate "Invite Member" %}</a>
</div>
</div>
@@ -45,9 +46,9 @@
<div>
<a href="{% url "team_member_settings" team_pk=team.pk user_pk=member.user_id %}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ member.user.email }}</a> {# "best name" perhaps later? #}
{% if not member.accepted %}
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">Invitation pending</span>
<span class="bg-slate-100 dark:bg-slate-700 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Invitation pending" %}</span>
{% elif member.is_admin %} {# NOTE: we intentionally hide admin-ness for non-accepted users #}
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Admin</span>
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Admin" %}</span>
{% endif %}
</div>
</td>
@@ -55,12 +56,12 @@
<td class="p-4">
<div class="flex justify-end">
{% if not member.accepted %}
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Reinvite</button>
<button name="action" value="reinvite:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Reinvite" %}</button>
{% endif %}
{% if request.user == member.user %}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Leave</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Leave" %}</button>
{% else %} {# NOTE: in our setup request_user_is_admin is implied because only admins may view the membership page #}
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Remove</button>
<button name="action" value="remove:{{ member.user_id }}" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md whitespace-nowrap">{% translate "Remove" %}</button>
{% endif %}
</div>
</td>
@@ -71,7 +72,7 @@
<td class="w-full p-4">
<div>
{# Note: this is already somewhat exceptional, because the usually you'll at least see yourself here (unless you're a superuser and a team has become memberless) #}
No members yet. <a href="{% url "team_members_invite" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Invite someone</a>.
{% translate "No members yet." %} <a href="{% url "team_members_invite" team_pk=team.pk %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Invite someone</a>." %}
</div>
</td>
</tr>
@@ -85,7 +86,7 @@
<div class="flex flex-direction-row">
<div class="ml-auto py-8 pr-4">
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">Back to Teams</a>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold">{% translate "Back to Teams" %}</a>
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Invite Members · {{ team.name }} · {{ site_title }}{% endblock %}
@@ -24,11 +25,11 @@
{% endif %}
<div>
<h1 class="text-4xl mt-4 font-bold">Invite members ({{ team.name }})</h1>
<h1 class="text-4xl mt-4 font-bold">{% blocktranslate with team_name=team.name %}Invite members ({{ team_name }}){% endblocktranslate %}</h1>
</div>
<div class="mt-4 mb-4">
Invite a member to join the team "{{ team.name }}". They will receive an email with a link to join.
{% blocktranslate with team_name=team.name %}Invite a member to join the team "{{ team_name }}". They will receive an email with a link to join.{% endblocktranslate %}
</div>
{% tailwind_formfield_implicit form.email %}
@@ -48,9 +49,9 @@
{% endif %}
</div>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Invite Member</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">Invite and add another</button>
<a href="{% url "team_members" team_pk=team.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">Cancel</a>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Invite Member" %}</button>
<button name="action" value="invite_and_add_another" class="font-bold text-slate-500 dark:text-slate-300 border-slate-300 dark:border-slate-600 pl-4 pr-4 pb-2 pt-2 ml-2 border-2 hover:bg-slate-200 dark:hover:bg-slate-800 active:ring rounded-md">{% translate "Invite and add another" %}</button>
<a href="{% url "team_members" team_pk=team.pk %}" class="font-bold text-slate-500 dark:text-slate-300 ml-4">{% translate "Cancel" %}</a>
</form>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}New team · {{ site_title }}{% endblock %}
@@ -14,14 +15,14 @@
{% csrf_token %}
<div>
<h1 class="text-4xl my-4 font-bold">New Team</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "New Team" %}</h1>
</div>
{% tailwind_formfield form.name %}
{% tailwind_formfield form.visibility %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<a href="{% url "team_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>

View File

@@ -9,6 +9,7 @@ from django.utils import timezone
from django.urls import reverse
from django.contrib import messages
from django.contrib.auth import logout
from django.utils.translation import gettext_lazy as _
from users.models import EmailVerification
from bugsink.app_settings import get_settings, CB_ANYBODY, CB_ADMINS, CB_MEMBERS
@@ -54,7 +55,7 @@ def team_list(request, ownership_filter=None):
if not team.is_joinable() and not request.user.is_superuser:
raise PermissionDenied("This team is not joinable")
messages.success(request, 'You have joined the team "%s"' % team.name)
messages.success(request, _('You have joined the team "%s"') % team.name)
TeamMembership.objects.create(team_id=team_pk, user_id=request.user.id, role=TeamRole.MEMBER, accepted=True)
return redirect('team_member_settings', team_pk=team_pk, user_pk=request.user.id)
@@ -126,7 +127,8 @@ def team_edit(request, team_pk):
if action == 'delete':
# Double-check that the user is an admin or superuser
if not (TeamMembership.objects.filter(team=team, user=request.user, role=TeamRole.ADMIN, accepted=True).exists() or
if not (TeamMembership.objects.filter(
team=team, user=request.user, role=TeamRole.ADMIN, accepted=True).exists() or
request.user.is_superuser):
raise PermissionDenied("Only team admins can delete teams")

View File

@@ -1,5 +1,6 @@
{% extends "barest_base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Log in · {{ site_title }}{% endblock %}
@@ -14,13 +15,13 @@
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
{% if form.errors %}
<div class="mb-8 text-red-500 dark:text-red-400">Your username and password didn't match. Please try again.</div>
<div class="mb-8 text-red-500 dark:text-red-400">{% translate "Your username and password didn't match. Please try again." %}</div>
{% elif next %}
{% if user.is_authenticated %}
<div class="mb-8">Your account doesn't have access to this page. To proceed, please login with an account that has access.</div>
<div class="mb-8">{% translate "Your account doesn't have access to this page. To proceed, please login with an account that has access." %}</div>
{% else %}
<div class="mb-8">Please login to see this page.</div>
<div class="mb-8">{% translate "Please login to see this page." %}</div>
{% endif %}
{% endif %}
@@ -30,22 +31,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="absolute ml-3" width="24">
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" />
</svg>
<input name="username" type="text" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.username.value %}value="{{ form.username.value }}"{% endif %} placeholder="Username" />
<input name="username" type="text" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.username.value %}value="{{ form.username.value }}"{% endif %} placeholder="{% translate 'Username' %}" />
</div>
<div class="flex items-center text-lg mb-6 md:mb-8">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="absolute ml-3" width="24">
<path fill-rule="evenodd" d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z" clip-rule="evenodd" />
</svg>
<input name="password" type="password" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.password.value %}value="{{ form.password.value }}"{% endif %} placeholder="Password" />
<input name="password" type="password" class="bg-slate-200 dark:bg-slate-800 pl-12 py-2 md:py-4 focus:outline-none w-full" {% if form.password.value %}value="{{ form.password.value }}"{% endif %} placeholder="{% translate 'Password' %}" />
</div>
<input type="hidden" name="next" value="{{ next }}">
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Login</button>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Login" %}</button>
</form>
<div class="mt-4">
<a href="{% url 'request_reset_password' %}" class="text-slate-800 dark:text-slate-100">Forgot password?</a>
{% if registration_enabled %}<a href="{% url 'signup' %}" class="float-right text-slate-800 dark:text-slate-100">Create an account</a>{% endif %}
<a href="{% url 'request_reset_password' %}" class="text-slate-800 dark:text-slate-100">{% translate "Forgot password?" %}</a>
{% if registration_enabled %}<a href="{% url 'signup' %}" class="float-right text-slate-800 dark:text-slate-100">{% translate "Create an account" %}</a>{% endif %}
</div>
</div>

View File

@@ -2,15 +2,16 @@
{% load static %}
{% load tailwind_forms %}
{% load stricter_templates %}
{% load i18n %}
{% block title %}Settings · {{ site_title }}{% endblock %}
{% block title %}{% translate "Settings" %} · {{ site_title }}{% endblock %}
{% block content %}
<div class="m-4">
<div>
<h1 class="text-4xl my-4 font-bold">Site Settings</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "Site Settings" %}</h1>
</div>
<h1 class="text-2xl font-bold mt-4">Bugsink</h1>

View File

@@ -1,4 +1,4 @@
{% load static tailwind_tags version add_to_qs %}<!DOCTYPE html>
{% load static tailwind_tags version add_to_qs %}{% load i18n %}<!DOCTYPE html>
<html lang="en" data-theme="{% if user.is_authenticated %}{{ user.theme_preference }}{% else %}dark{% endif %}">
<!-- version: {% version %} -->
<head>
@@ -31,30 +31,30 @@
<a href="/"><div class="px-2 py-2 my-2 font-bold hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{{ site_title }}</div></a>
{% if not app_settings.SINGLE_TEAM %}
<a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Teams</div></a>
<a href="{% url "team_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Teams" %}</div></a>
{% endif %}
<a href="{% url "project_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Projects</div></a>
<a href="{% url "project_list" %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Projects" %}</div></a>
{% if project %}
<a href="{% url "issue_list_open" project_pk=project.pk %}{% current_qs %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Issues ({{ project.name }})</div></a>
<a href="{% url "issue_list_open" project_pk=project.pk %}{% current_qs %}"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Issues" %} ({{ project.name }})</div></a>
{% endif %}
<div class="ml-auto flex">
{% if app_settings.USE_ADMIN and user.is_staff %}
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Admin</div></a>
<a href="/admin/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Admin" %}</div></a>
{% endif %}
{% if user.is_superuser %}
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Users</div></a>
<a href="/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Tokens</div></a>
<a href="/users/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Users" %}</div></a>
<a href="/bsmain/auth_tokens/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Tokens" %}</div></a>
{% endif %}
{% if logged_in_user.is_anonymous %}
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Login</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
<a href="/accounts/login/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Login" %}</div></a> {# I don't think this is actually ever shown in practice, because you must always be logged in #}
{% else %}
<a href="/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">Preferences</div></a>
<div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">Log out</button></form></div>
<a href="/accounts/preferences/"><div class="px-4 py-2 my-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl">{% translate "Preferences" %}</div></a>
<div class="px-4 py-2 my-2 mr-2 hover:bg-slate-300 dark:hover:bg-slate-700 rounded-xl"><form id="logout-form" method="post" action="{% url 'logout' %}">{% csrf_token %}<button type="submit">{% translate "Log out" %}</button></form></div>
{% endif %}
</div>
</div>

View File

@@ -9,19 +9,16 @@ from django.core.exceptions import ValidationError
from django.contrib.auth import password_validation
from django.forms import ModelForm
from django.utils.html import escape, mark_safe
from django.utils.translation import gettext_lazy as _, get_language_info
from django.conf import settings
TRUE_FALSE_CHOICES = (
(True, 'Yes'),
(False, 'No')
(True, _("Yes")),
(False, _("No"))
)
def _(x):
# dummy gettext
return x
User = get_user_model()
@@ -87,7 +84,7 @@ class UserEditForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['username'].validators = [EmailValidator()]
self.fields['username'].label = "Email"
self.fields['username'].label = _("Email")
self.fields['username'].help_text = None # "Email" is descriptive enough
@@ -136,6 +133,18 @@ class SetPasswordForm(BaseSetPasswordForm):
self.fields['new_password2'].help_text = None # "Confirm password" is descriptive enough
def language_choices():
items = [("auto", _("Auto (browser preference)"))]
for code, _label in settings.LANGUAGES:
info = get_language_info(code)
label = info["name_local"] \
if info["name_local"] == info["name_translated"] \
else f"{info['name_local']} ({info['name_translated']})"
items.append((code, label))
return items
class PreferencesForm(ModelForm):
# I haven't gotten a decent display for checkboxes in forms yet; the quickest hack around this is a ChoiceField
send_email_alerts = forms.ChoiceField(
@@ -146,7 +155,13 @@ class PreferencesForm(ModelForm):
required=True,
widget=forms.Select(),
)
language = forms.ChoiceField(
label=_("Language"),
choices=language_choices,
required=True,
widget=forms.Select(),
)
class Meta:
model = User
fields = ("send_email_alerts", "theme_preference",)
fields = ("send_email_alerts", "theme_preference", "language",)

View File

@@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0002_user_theme_preference"),
]
operations = [
migrations.AddField(
model_name="user",
name="language",
field=models.CharField(default="auto", max_length=10),
),
]

View File

@@ -3,6 +3,7 @@ import secrets
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class User(AbstractUser):
@@ -18,9 +19,9 @@ class User(AbstractUser):
send_email_alerts = models.BooleanField(default=True, blank=True)
THEME_CHOICES = [
("system", "System Default"),
("light", "Light"),
("dark", "Dark"),
("system", _("System Default")),
("light", _("Light")),
("dark", _("Dark")),
]
theme_preference = models.CharField(
max_length=10,
@@ -28,6 +29,14 @@ class User(AbstractUser):
default="system",
blank=False,
)
language = models.CharField(
max_length=10,
# choices intentionally not set, we don't want changes to trigger migrations; the actual choices are set in
# forms.py; in Django 5.0 and up we can instead used a callable here
# choices=...
default="auto",
blank=False,
)
class Meta:
db_table = 'auth_user'

View File

@@ -1,5 +1,6 @@
{% extends "barest_base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Confirm email · {{ site_title }}{% endblock %}
@@ -14,12 +15,12 @@
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
<div class="mb-8">
Confirm your email address by clicking the button below.
{% translate "Confirm your email address by clicking the button below." %}
</div>
<form method="post" action=".">
{% csrf_token %}
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Confirm</button>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Confirm" %}</button>
</form>
</div>

View File

@@ -1,5 +1,6 @@
{% extends "barest_base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Verification email sent · {{ site_title }}{% endblock %}
@@ -13,7 +14,7 @@
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
A verification email has been sent to your email address. Please verify your email address to complete the registration process.
{% translate "A verification email has been sent to your email address. Please verify your email address to complete the registration process." %}
</div>

View File

@@ -1,5 +1,6 @@
{% extends "barest_base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Logged out · {{ site_title }}{% endblock %}
@@ -12,7 +13,7 @@
</div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
You have been logged out. <a href="{% url 'login' %}" class="text-cyan-500 dark:text-cyan-300">Log in again</a>.
{% translate "You have been logged out." %} <a href="{% url 'login' %}" class="text-cyan-500 dark:text-cyan-300">{% translate "Log in again</a>." %}
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}User Preferences · {{ site_title }}{% endblock %}
@@ -23,13 +24,14 @@
{% endif %}
<div>
<h1 class="text-4xl my-4 font-bold">User Preferences</h1>
<h1 class="text-4xl my-4 font-bold">{% translate "User Preferences" %}</h1>
</div>
{% tailwind_formfield form.send_email_alerts %}
{% tailwind_formfield form.theme_preference %}
{% tailwind_formfield form.language %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
</form>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "barest_base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Resend confirmation · {{ site_title }}{% endblock %}
@@ -19,11 +20,11 @@
{% tailwind_formfield_implicit form.email %}
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Reset password" %}</button>
</form>
<div class="mt-4">
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">Log in instead</a>
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">{% translate "Log in instead" %}</a>
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "barest_base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Resend confirmation · {{ site_title }}{% endblock %}
@@ -19,7 +20,7 @@
{% tailwind_formfield_implicit form.email %}
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Resend verification email</button>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Resend verification email" %}</button>
</form>
<div class="mt-4">

View File

@@ -1,6 +1,7 @@
{% extends "barest_base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Reset password · {{ site_title }}{% endblock %}
@@ -23,12 +24,12 @@
<input type="hidden" name="next" value="{{ next }}" />
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">Reset password</button>
<button class="bg-slate-800 font-medium p-2 md:p-4 text-white uppercase w-full">{% translate "Reset password" %}</button>
</form>
<div class="mt-4">
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">Log in instead</a>
<a href="{% url 'login' %}" class="text-slate-800 dark:text-slate-100">{% translate "Log in instead" %}</a>
</div>
</div>

View File

@@ -1,5 +1,6 @@
{% extends "barest_base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Password reset sent · {{ site_title }}{% endblock %}
@@ -12,7 +13,7 @@
</div>
<div class="p-12 md:pt-24 md:pl-24 md:pr-24 md:pb-16">
A password reset link has been sent to your email address. Please check your inbox and follow the instructions to reset your password.
{% translate "A password reset link has been sent to your email address. Please check your inbox and follow the instructions to reset your password." %}
</div>
</div>

View File

@@ -1,8 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load tailwind_forms %}
{% load i18n %}
{% block title %}Edit {{ form.instance.username }} · {{ site_title }}{% endblock %}
{% block title %}{% translate "Edit" %} {{ form.instance.username }} · {{ site_title }}{% endblock %}
{% block content %}
@@ -18,13 +19,13 @@
</div>
<div class="mt-4 mb-4">
Settings for "{{ form.instance.username }}".
{% blocktranslate with username=form.instance.username %}Settings for {{ username }}.{% endblocktranslate %}
</div>
{% tailwind_formfield form.username %}
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">Save</button>
<a href="{% url "user_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">Cancel</a>
<button name="action" value="invite" class="font-bold text-slate-800 dark:text-slate-100 border-slate-500 dark:border-slate-400 pl-4 pr-4 pb-2 pt-2 border-2 bg-cyan-200 dark:bg-cyan-700 hover:bg-cyan-400 dark:hover:bg-cyan-600 active:ring rounded-md">{% translate "Save" %}</button>
<a href="{% url "user_list" %}" class="text-cyan-500 dark:text-cyan-300 font-bold ml-2">{% translate "Cancel" %}</a>
</form>
</div>

View File

@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% block title %}Users · {{ site_title }}{% endblock %}
@@ -12,7 +13,7 @@
<h3 class="text-2xl font-semibold text-slate-800 dark:text-slate-100 mt-3 mb-4">Delete User</h3>
<div class="mt-4 mb-6">
<p class="text-slate-700 dark:text-slate-300">
Are you sure you want to delete this user? This action cannot be undone.
{% translate "Are you sure you want to delete this user? This action cannot be undone." %}
</p>
</div>
<div class="flex items-center justify-center space-x-4 mb-4">
@@ -41,7 +42,7 @@
{% endif %}
<div class="flex">
<h1 class="text-4xl mt-4 font-bold">Users</h1>
<h1 class="text-4xl mt-4 font-bold">{% translate "Users" %}</h1>
{% comment %}
Our current invite-system is tied to either a team or a project; no "global" invites (yet).
<div class="ml-auto mt-6">
@@ -58,7 +59,7 @@
<tbody>
<thead>
<tr class="bg-slate-200 dark:bg-slate-800">
<th class="w-full p-4 text-left text-xl" colspan="2">Users</th>
<th class="w-full p-4 text-left text-xl" colspan="2">{% translate "Users" %}</th>
</tr>
{% for user in users %}
@@ -67,7 +68,7 @@
<div>
<a href="{% url "user_edit" user_pk=user.pk %}" class="text-xl text-cyan-500 dark:text-cyan-300 font-bold">{{ user.username }}</a>
{% if member.is_superuser %}
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">Superuser</span>
<span class="bg-cyan-100 dark:bg-cyan-900 rounded-2xl px-4 py-2 ml-2 text-sm">{% translate "Superuser" %}</span>
{% endif %}
</div>
</td>

View File

@@ -7,9 +7,12 @@ from django.http import Http404
from django.utils import timezone
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.utils.translation import gettext as _
from django.utils import translation
from bugsink.app_settings import get_settings, CB_ANYBODY
from bugsink.decorators import atomic_for_request_method
from bugsink.middleware import get_chosen_language
from .forms import (
UserCreationForm, ResendConfirmationForm, RequestPasswordResetForm, SetPasswordForm, PreferencesForm, UserEditForm)
@@ -225,8 +228,13 @@ def preferences(request):
form = PreferencesForm(request.POST, instance=user)
if form.is_valid():
form.save()
messages.success(request, "Updated preferences")
user = form.save()
# activate the selected language immediately for the Success message; we've already passed the middleware
# stage (which looked at the pre-change language), so we need to do this ourselves with the fresh value.
translation.activate(get_chosen_language(user, request))
messages.success(request, _("Updated preferences"))
return redirect('preferences')
else: