Localization (#214)

* General localization:
  - Add gettext to literals
  - Add trans template tag to templates
  - Set localized date and time
  - Add locale option to TEMPLATE.env
  - Add migrations that result from model field changes

* Add german locale

* Add german locale
This commit is contained in:
Christian Wiegand 2022-08-29 00:22:50 +02:00 committed by GitHub
parent 4d7c036acc
commit d134c0049d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1348 additions and 395 deletions

View File

@ -29,6 +29,10 @@ DJANGO_SECRET_KEY=random_string
# For better security, set this to your deployment's domain. Comma separated. # For better security, set this to your deployment's domain. Comma separated.
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
# Localization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE=en-us
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended) # Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
ACCOUNT_SIGNUPS_ENABLED=False ACCOUNT_SIGNUPS_ENABLED=False

View File

@ -0,0 +1,29 @@
# 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: 2022-06-24 13:20+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=2; plural=(n != 1);\n"
#: a17t/templates/a17t/includes/pagination.html:5
#: a17t/templates/a17t/includes/pagination.html:7
msgid "Previous"
msgstr "Zurück"
#: a17t/templates/a17t/includes/pagination.html:10
#: a17t/templates/a17t/includes/pagination.html:12
msgid "Next"
msgstr "Vor"

View File

@ -1 +1,2 @@
<label class="label" for="{{field.auto_id}}">{{ field.label }} {% if not field.field.required %}<span class="badge ~neutral">Optional</span>{% endif %}</label> {% load i18n %}
<label class="label" for="{{field.auto_id}}">{% trans field.label %} {% if not field.field.required %}<span class="badge ~neutral">Optional</span>{% endif %}</label>

View File

@ -1,14 +1,15 @@
{% load i18n %}
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination"> <nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
<div class="w-full md:w-auto mb-2"> <div class="w-full md:w-auto mb-2">
{% if page.has_previous %} {% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto mr-1">Previous</a> <a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto mr-1">{% trans 'Previous' %}</a>
{% else %} {% else %}
<a class="button field !low bg-neutral-000 w-auto mr-1" disabled>Previous</a> <a class="button field !low bg-neutral-000 w-auto mr-1" disabled>{% trans 'Previous' %}</a>
{% endif %} {% endif %}
{% if page.has_next %} {% if page.has_next %}
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto">Next</a> <a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field !low bg-neutral-000 w-auto">{% trans 'Next' %}</a>
{% else %} {% else %}
<a class="button field !low bg-neutral-000 w-auto" disabled>Next</a> <a class="button field !low bg-neutral-000 w-auto" disabled>{% trans 'Next' %}</a>
{% endif %} {% endif %}
</div> </div>

View File

@ -0,0 +1,118 @@
# 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.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-24 13:20+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=2; plural=(n != 1);\n"
#: analytics/models.py:18
msgid "Service"
msgstr "Dienst"
#: analytics/models.py:24
msgid "Identifier"
msgstr "Kennung"
#: analytics/models.py:29
msgid "Start time"
msgstr "Startzeit"
#: analytics/models.py:32
msgid "Last seen"
msgstr "Zuletzt gesehen"
#: analytics/models.py:36
msgid "User agent"
msgstr ""
#: analytics/models.py:37
msgid "Browser"
msgstr ""
#: analytics/models.py:38
msgid "Device"
msgstr "Gerät"
#: analytics/models.py:42
msgid "Phone"
msgstr ""
#: analytics/models.py:43
msgid "Tablet"
msgstr ""
#: analytics/models.py:44
msgid "Desktop"
msgstr ""
#: analytics/models.py:45
msgid "Robot"
msgstr ""
#: analytics/models.py:46
msgid "Other"
msgstr "Andere"
#: analytics/models.py:49
msgid "Device type"
msgstr "Gerätetyp"
#: analytics/models.py:51
msgid "OS"
msgstr "Betriessystem"
#: analytics/models.py:52
msgid "IP"
msgstr ""
#: analytics/models.py:55
msgid "Asn"
msgstr ""
#: analytics/models.py:56
msgid "Country"
msgstr "Land"
#: analytics/models.py:57
msgid "Longitude"
msgstr "Längengrad"
#: analytics/models.py:58
msgid "Latitude"
msgstr "Breitengrad"
#: analytics/models.py:59
msgid "Time zone"
msgstr "Zeitzone"
#: analytics/models.py:61
msgid "Is bounce"
msgstr "Absprung"
#: analytics/models.py:64 analytics/models.py:100
msgid "Session"
msgstr "Sitzung"
#: analytics/models.py:65
msgid "Sessions"
msgstr "Sitzungen"
#: analytics/models.py:122
msgid "Hit"
msgstr "Besuch"
#: analytics/models.py:123
msgid "Hits"
msgstr "Besuche"

View File

@ -0,0 +1,114 @@
# Generated by Django 3.2.12 on 2022-06-24 11:44
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('core', '0009_auto_20220624_0744'),
('analytics', '0009_auto_20210329_1100'),
]
operations = [
migrations.AlterModelOptions(
name='hit',
options={'ordering': ['-start_time'], 'verbose_name': 'Hit', 'verbose_name_plural': 'Hits'},
),
migrations.AlterModelOptions(
name='session',
options={'ordering': ['-start_time'], 'verbose_name': 'Session', 'verbose_name_plural': 'Sessions'},
),
migrations.AlterField(
model_name='hit',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='hit',
name='session',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='analytics.session', verbose_name='Session'),
),
migrations.AlterField(
model_name='session',
name='asn',
field=models.TextField(blank=True, verbose_name='Asn'),
),
migrations.AlterField(
model_name='session',
name='browser',
field=models.TextField(verbose_name='Browser'),
),
migrations.AlterField(
model_name='session',
name='country',
field=models.TextField(blank=True, verbose_name='Country'),
),
migrations.AlterField(
model_name='session',
name='device',
field=models.TextField(verbose_name='Device'),
),
migrations.AlterField(
model_name='session',
name='device_type',
field=models.CharField(choices=[('PHONE', 'Phone'), ('TABLET', 'Tablet'), ('DESKTOP', 'Desktop'), ('ROBOT', 'Robot'), ('OTHER', 'Other')], default='OTHER', max_length=7, verbose_name='Device type'),
),
migrations.AlterField(
model_name='session',
name='identifier',
field=models.TextField(blank=True, db_index=True, verbose_name='Identifier'),
),
migrations.AlterField(
model_name='session',
name='ip',
field=models.GenericIPAddressField(db_index=True, null=True, verbose_name='IP'),
),
migrations.AlterField(
model_name='session',
name='is_bounce',
field=models.BooleanField(db_index=True, default=True, verbose_name='Is bounce'),
),
migrations.AlterField(
model_name='session',
name='last_seen',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Last seen'),
),
migrations.AlterField(
model_name='session',
name='latitude',
field=models.FloatField(null=True, verbose_name='Latitude'),
),
migrations.AlterField(
model_name='session',
name='longitude',
field=models.FloatField(null=True, verbose_name='Longitude'),
),
migrations.AlterField(
model_name='session',
name='os',
field=models.TextField(verbose_name='OS'),
),
migrations.AlterField(
model_name='session',
name='service',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.service', verbose_name='Service'),
),
migrations.AlterField(
model_name='session',
name='start_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Start time'),
),
migrations.AlterField(
model_name='session',
name='time_zone',
field=models.TextField(blank=True, verbose_name='Time zone'),
),
migrations.AlterField(
model_name='session',
name='user_agent',
field=models.TextField(verbose_name='User agent'),
),
]

View File

@ -3,6 +3,7 @@ import uuid
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import Service, ACTIVE_USER_TIMEDELTA from core.models import Service, ACTIVE_USER_TIMEDELTA
@ -13,43 +14,55 @@ def _default_uuid():
class Session(models.Model): class Session(models.Model):
uuid = models.UUIDField(default=_default_uuid, primary_key=True) uuid = models.UUIDField(default=_default_uuid, primary_key=True)
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True) service = models.ForeignKey(
Service, verbose_name=_('Service'),
on_delete=models.CASCADE, db_index=True
)
# Cross-session identification; optional, and provided by the service # Cross-session identification; optional, and provided by the service
identifier = models.TextField(blank=True, db_index=True) identifier = models.TextField(
blank=True, db_index=True, verbose_name=_('Identifier')
)
# Time # Time
start_time = models.DateTimeField(default=timezone.now, db_index=True) start_time = models.DateTimeField(
last_seen = models.DateTimeField(default=timezone.now, db_index=True) default=timezone.now, db_index=True, verbose_name=_('Start time')
)
last_seen = models.DateTimeField(
default=timezone.now, db_index=True, verbose_name=_('Last seen')
)
# Core request information # Core request information
user_agent = models.TextField() user_agent = models.TextField(verbose_name=_('User agent'))
browser = models.TextField() browser = models.TextField(verbose_name=_('Browser'))
device = models.TextField() device = models.TextField(verbose_name=_('Device'))
device_type = models.CharField( device_type = models.CharField(
max_length=7, max_length=7,
choices=[ choices=[
("PHONE", "Phone"), ("PHONE", _("Phone")),
("TABLET", "Tablet"), ("TABLET", _("Tablet")),
("DESKTOP", "Desktop"), ("DESKTOP", _("Desktop")),
("ROBOT", "Robot"), ("ROBOT", _("Robot")),
("OTHER", "Other"), ("OTHER", _("Other")),
], ],
default="OTHER", default="OTHER",
verbose_name=_('Device type')
) )
os = models.TextField() os = models.TextField(verbose_name=_('OS'))
ip = models.GenericIPAddressField(db_index=True, null=True) ip = models.GenericIPAddressField(db_index=True, null=True, verbose_name=_('IP'))
# GeoIP data # GeoIP data
asn = models.TextField(blank=True) asn = models.TextField(blank=True, verbose_name=_('Asn'))
country = models.TextField(blank=True) country = models.TextField(blank=True, verbose_name=_('Country'))
longitude = models.FloatField(null=True) longitude = models.FloatField(null=True, verbose_name=_('Longitude'))
latitude = models.FloatField(null=True) latitude = models.FloatField(null=True, verbose_name=_('Latitude'))
time_zone = models.TextField(blank=True) time_zone = models.TextField(blank=True, verbose_name=_('Time zone'))
is_bounce = models.BooleanField(default=True, db_index=True) is_bounce = models.BooleanField(default=True, db_index=True, verbose_name=_('Is bounce'))
class Meta: class Meta:
verbose_name = _('Session')
verbose_name_plural = _('Sessions')
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["service", "-start_time"]), models.Index(fields=["service", "-start_time"]),
@ -82,7 +95,10 @@ class Session(models.Model):
class Hit(models.Model): class Hit(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True) session = models.ForeignKey(
Session, on_delete=models.CASCADE, db_index=True,
verbose_name=_('Session')
)
initial = models.BooleanField(default=True, db_index=True) initial = models.BooleanField(default=True, db_index=True)
# Base request information # Base request information
@ -103,6 +119,8 @@ class Hit(models.Model):
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True) service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
class Meta: class Meta:
verbose_name = _('Hit')
verbose_name_plural = _('Hits')
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["session", "-start_time"]), models.Index(fields=["session", "-start_time"]),
@ -111,6 +129,7 @@ class Hit(models.Model):
models.Index(fields=["session", "referrer"]), models.Index(fields=["session", "referrer"]),
] ]
@property @property
def duration(self): def duration(self):
return self.last_seen - self.start_time return self.last_seen - self.start_time

View File

@ -0,0 +1,87 @@
# 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: 2022-06-24 13:20+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=2; plural=(n != 1);\n"
#: core/models.py:58
msgid "Active"
msgstr "Aktiv"
#: core/models.py:58
msgid "Archived"
msgstr "Archiviert"
#: core/models.py:61
msgid "Name"
msgstr "Name"
#: core/models.py:63
msgid "Owner"
msgstr "Eigentümer"
#: core/models.py:67
msgid "Collaborators"
msgstr "Mitarbeiter"
#: core/models.py:70
msgid "created"
msgstr "Erstellt"
#: core/models.py:71
msgid "link"
msgstr "Verweis"
#: core/models.py:72
msgid "origins"
msgstr ""
#: core/models.py:75
msgid "status"
msgstr "Status"
#: core/models.py:77
msgid "Respect dnt"
msgstr "DNT beachten"
#: core/models.py:78
msgid "Ignore robots"
msgstr "Robots ignorieren"
#: core/models.py:79
msgid "Collect ips"
msgstr "IPs erfassen"
#: core/models.py:82
msgid "Igored ips"
msgstr "IPs ignorieren"
#: core/models.py:86
msgid "Hide referrer regex"
msgstr "Referrer Regex ausblenden"
#: core/models.py:88
msgid "Script inject"
msgstr ""
#: core/models.py:91
msgid "Service"
msgstr "Dienst"
#: core/models.py:92
msgid "Services"
msgstr "Dienste"

View File

@ -1,247 +0,0 @@
#: account/adapter.py:51
msgid "A user is already registered with this e-mail address."
msgstr "A user is already registered with this email address."
#: account/adapter.py:294
#, python-brace-format
msgid "Password must be a minimum of {0} characters."
msgstr ""
#: account/forms.py:92
msgid "Remember Me"
msgstr "Remember me"
#: account/forms.py:101
msgid "The e-mail address and/or password you specified are not correct."
msgstr "The email address and/or password are not correct."
#: account/forms.py:113 account/forms.py:268 account/forms.py:426
#: account/forms.py:495
msgid "E-mail address"
msgstr "Email address"
#: account/forms.py:115 account/forms.py:301 account/forms.py:421
#: account/forms.py:490
msgid "E-mail"
msgstr "Email"
#: account/forms.py:130
msgid "Username or e-mail"
msgstr "Username or email"
#: account/forms.py:292
msgid "E-mail (again)"
msgstr "Email (again)"
#: account/forms.py:296
msgid "E-mail address confirmation"
msgstr "Email address confirmation"
#: account/forms.py:304
msgid "E-mail (optional)"
msgstr "Email (optional)"
#: account/forms.py:432
msgid "This e-mail address is already associated with this account."
msgstr "This email address is already associated with this account."
#: account/forms.py:434
msgid "This e-mail address is already associated with another account."
msgstr "This email address is already associated with another account."
#: account/forms.py:504
msgid "The e-mail address is not assigned to any user account"
msgstr "The email address is not assigned to any user account."
#: account/models.py:25 account/models.py:78
msgid "e-mail address"
msgstr "email address"
#: socialaccount/adapter.py:26
#, python-format
msgid ""
"An account already exists with this e-mail address. Please sign in to that "
"account first, then connect your %s account."
msgstr ""
"An account already exists with this email address. Please sign in to that "
"account first, then connect your %s account."
#: socialaccount/adapter.py:138
msgid "Your account has no verified e-mail address."
msgstr "Your account has no verified email address."
#: templates/account/email.html:8
msgid "E-mail Addresses"
msgstr "Email Addresses"
#: templates/account/email.html:10
msgid "The following e-mail addresses are associated with your account:"
msgstr "The following email addresses are associated with your account:"
#: templates/account/email.html:43
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
"You currently do not have any email address set up. You should really add "
"an email address so you can receive notifications, reset your password, etc."
#: templates/account/email.html:48
msgid "Add E-mail Address"
msgstr "Add Email Address"
#: templates/account/email.html:53
msgid "Add E-mail"
msgstr "Add Email"
#: templates/account/email.html:62
msgid "Do you really want to remove the selected e-mail address?"
msgstr "Do you really want to remove the selected email address?"
#: templates/account/email/email_confirmation_message.txt:1
#, python-format
msgid ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this e-mail because user %(user_display)s has given yours "
"as an e-mail address to connect their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
msgstr ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this email because user %(user_display)s has given yours "
"as an email address to connect their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
#: templates/account/email/email_confirmation_message.txt:7
#, python-format
msgid ""
"Thank you from %(site_name)s!\n"
"%(site_domain)s"
msgstr ""
#: templates/account/email/email_confirmation_subject.txt:3
msgid "Please Confirm Your E-mail Address"
msgstr "Please Confirm Your Email Address"
#: templates/account/email/password_reset_key_message.txt:1
#, python-format
msgid ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this e-mail because you or someone else has requested a "
"password for your user account.\n"
"It can be safely ignored if you did not request a password reset. Click the "
"link below to reset your password."
msgstr ""
"Hello from %(site_name)s!\n"
"\n"
"You're receiving this email because you or someone else has requested a "
"password for your user account.\n"
"It can be safely ignored if you did not request a password reset. Click the "
"link below to reset your password."
#: templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset E-mail"
msgstr "Password Reset Email"
#: templates/account/email_confirm.html:6
#: templates/account/email_confirm.html:10
msgid "Confirm E-mail Address"
msgstr "Confirm Email Address"
#: templates/account/email_confirm.html:16
#, python-format
msgid ""
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an e-mail "
"address for user %(user_display)s."
msgstr ""
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an email "
"address for user %(user_display)s."
#: templates/account/email_confirm.html:27
#, python-format
msgid ""
"This e-mail confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new e-mail confirmation request</a>."
msgstr ""
"This email confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new email confirmation request</a>."
#: templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary e-mail address (%(email)s)."
msgstr "You cannot remove your primary email address (%(email)s)."
#: templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation e-mail sent to %(email)s."
msgstr "Confirmation email sent to %(email)s."
#: templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed e-mail address %(email)s."
msgstr "Removed email address %(email)s."
#: templates/account/messages/primary_email_set.txt:2
msgid "Primary e-mail address set."
msgstr "Primary email address set."
#: templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary e-mail address must be verified."
msgstr "Your primary email address must be verified."
#: templates/account/password_reset.html:15
msgid ""
"Forgotten your password? Enter your e-mail address below, and we'll send you "
"an e-mail allowing you to reset it."
msgstr ""
"Forgotten your password? Enter your email address below, and we'll send you "
"an email allowing you to reset it."
#: templates/account/password_reset_done.html:15
msgid ""
"We have sent you an e-mail. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
"We have sent you an email. Please contact us if you do not receive it "
"within a few minutes."
#: templates/account/verification_sent.html:5
#: templates/account/verification_sent.html:8
#: templates/account/verified_email_required.html:5
#: templates/account/verified_email_required.html:8
msgid "Verify Your E-mail Address"
msgstr "Verify Your Email Address"
#: templates/account/verification_sent.html:10
msgid ""
"We have sent an e-mail to you for verification. Follow the link provided to "
"finalize the signup process. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
"We have sent an email to you for verification. Follow the link provided to "
"finalize the signup process. Please contact us if you do not receive it "
"within a few minutes."
#: templates/account/verified_email_required.html:12
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your e-mail address. "
msgstr ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your email address. "
#: templates/account/verified_email_required.html:16
msgid ""
"We have sent an e-mail to you for\n"
"verification. Please click on the link inside this e-mail. Please\n"
"contact us if you do not receive it within a few minutes."
msgstr ""
"We have sent an email to you for\n"
"verification. Please click on the link inside this email. Please\n"
"contact us if you do not receive it within a few minutes."

View File

@ -0,0 +1,90 @@
# Generated by Django 3.2.12 on 2022-06-24 11:44
import core.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0008_auto_20200628_1403'),
]
operations = [
migrations.AlterModelOptions(
name='service',
options={'ordering': ['name', 'uuid'], 'verbose_name': 'Service', 'verbose_name_plural': 'Services'},
),
migrations.AlterField(
model_name='service',
name='collaborators',
field=models.ManyToManyField(blank=True, related_name='collaborating_services', to=settings.AUTH_USER_MODEL, verbose_name='Collaborators'),
),
migrations.AlterField(
model_name='service',
name='collect_ips',
field=models.BooleanField(default=True, verbose_name='Collect ips'),
),
migrations.AlterField(
model_name='service',
name='created',
field=models.DateTimeField(auto_now_add=True, verbose_name='created'),
),
migrations.AlterField(
model_name='service',
name='hide_referrer_regex',
field=models.TextField(blank=True, default='', validators=[core.models._validate_regex], verbose_name='Hide referrer regex'),
),
migrations.AlterField(
model_name='service',
name='ignore_robots',
field=models.BooleanField(default=False, verbose_name='Ignore robots'),
),
migrations.AlterField(
model_name='service',
name='ignored_ips',
field=models.TextField(blank=True, default='', validators=[core.models._validate_network_list], verbose_name='Igored ips'),
),
migrations.AlterField(
model_name='service',
name='link',
field=models.URLField(blank=True, verbose_name='link'),
),
migrations.AlterField(
model_name='service',
name='name',
field=models.TextField(max_length=64, verbose_name='Name'),
),
migrations.AlterField(
model_name='service',
name='origins',
field=models.TextField(default='*', verbose_name='origins'),
),
migrations.AlterField(
model_name='service',
name='owner',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owning_services', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='service',
name='respect_dnt',
field=models.BooleanField(default=True, verbose_name='Respect dnt'),
),
migrations.AlterField(
model_name='service',
name='script_inject',
field=models.TextField(blank=True, default='', verbose_name='Script inject'),
),
migrations.AlterField(
model_name='service',
name='status',
field=models.CharField(choices=[('AC', 'Active'), ('AR', 'Archived')], db_index=True, default='AC', max_length=2, verbose_name='status'),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -12,6 +12,7 @@ from django.db.models.functions import TruncDate, TruncHour
from django.db.utils import NotSupportedError from django.db.utils import NotSupportedError
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online) # How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
ACTIVE_USER_TIMEDELTA = timezone.timedelta( ACTIVE_USER_TIMEDELTA = timezone.timedelta(
@ -54,34 +55,41 @@ class User(AbstractUser):
class Service(models.Model): class Service(models.Model):
ACTIVE = "AC" ACTIVE = "AC"
ARCHIVED = "AR" ARCHIVED = "AR"
SERVICE_STATUSES = [(ACTIVE, "Active"), (ARCHIVED, "Archived")] SERVICE_STATUSES = [(ACTIVE, _("Active")), (ARCHIVED, _("Archived"))]
uuid = models.UUIDField(default=_default_uuid, primary_key=True) uuid = models.UUIDField(default=_default_uuid, primary_key=True)
name = models.TextField(max_length=64) name = models.TextField(max_length=64, verbose_name=_('Name'))
owner = models.ForeignKey( owner = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="owning_services" User, verbose_name=_('Owner'),
on_delete=models.CASCADE, related_name="owning_services"
) )
collaborators = models.ManyToManyField( collaborators = models.ManyToManyField(
User, related_name="collaborating_services", blank=True User, verbose_name=_('Collaborators'),
related_name="collaborating_services", blank=True
) )
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
link = models.URLField(blank=True) link = models.URLField(blank=True, verbose_name=_('link'))
origins = models.TextField(default="*") origins = models.TextField(default="*", verbose_name=_('origins'))
status = models.CharField( status = models.CharField(
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True,
verbose_name=_('status')
) )
respect_dnt = models.BooleanField(default=True) respect_dnt = models.BooleanField(default=True, verbose_name=_('Respect dnt'))
ignore_robots = models.BooleanField(default=False) ignore_robots = models.BooleanField(default=False, verbose_name=_('Ignore robots'))
collect_ips = models.BooleanField(default=True) collect_ips = models.BooleanField(default=True, verbose_name=_('Collect ips'))
ignored_ips = models.TextField( ignored_ips = models.TextField(
default="", blank=True, validators=[_validate_network_list] default="", blank=True, validators=[_validate_network_list],
verbose_name=_('Igored ips')
) )
hide_referrer_regex = models.TextField( hide_referrer_regex = models.TextField(
default="", blank=True, validators=[_validate_regex] default="", blank=True, validators=[_validate_regex],
verbose_name=_('Hide referrer regex')
) )
script_inject = models.TextField(default="", blank=True) script_inject = models.TextField(default="", blank=True, verbose_name=_('Script inject'))
class Meta: class Meta:
verbose_name = _('Service')
verbose_name_plural = _('Services')
ordering = ["name", "uuid"] ordering = ["name", "uuid"]
def __str__(self): def __str__(self):

View File

@ -25,19 +25,19 @@ class ServiceForm(forms.ModelForm):
"name": forms.TextInput(), "name": forms.TextInput(),
"origins": forms.TextInput(), "origins": forms.TextInput(),
"ignored_ips": forms.TextInput(), "ignored_ips": forms.TextInput(),
"respect_dnt": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "respect_dnt": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]),
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "collect_ips": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]),
"ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "ignore_robots": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]),
"hide_referrer_regex": forms.TextInput(), "hide_referrer_regex": forms.TextInput(),
"script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}), "script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}),
} }
labels = { labels = {
"origins": "Allowed origins", "origins": _("Allowed origins"),
"respect_dnt": "Respect DNT", "respect_dnt": _("Respect DNT"),
"ignored_ips": "Ignored IP addresses", "ignored_ips": _("Ignored IP addresses"),
"ignore_robots": "Ignore robots", "ignore_robots": _("Ignore robots"),
"hide_referrer_regex": "Hide specific referrers", "hide_referrer_regex": _("Hide specific referrers"),
"script_inject": "Additional injected JS", "script_inject": _("Additional injected JS"),
} }
help_texts = { help_texts = {
"name": _("What should the service be called?"), "name": _("What should the service be called?"),
@ -45,18 +45,18 @@ class ServiceForm(forms.ModelForm):
"origins": _( "origins": _(
"At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)." "At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
), ),
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?", "respect_dnt": _("Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?"),
"ignored_ips": "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32').", "ignored_ips": _("A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."),
"ignore_robots": "Should sessions generated by bots be excluded from tracking?", "ignore_robots": _("Should sessions generated by bots be excluded from tracking?"),
"hide_referrer_regex": "Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank.", "hide_referrer_regex": _("Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank."),
"script_inject": "Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed.", "script_inject": _("Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed."),
} }
collect_ips = forms.BooleanField( collect_ips = forms.BooleanField(
help_text="IP address collection is disabled globally by your administrator." help_text=_("IP address collection is disabled globally by your administrator.")
if settings.BLOCK_ALL_IPS if settings.BLOCK_ALL_IPS
else "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.", else _("Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected."),
widget=forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), widget=forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]),
initial=False if settings.BLOCK_ALL_IPS else True, initial=False if settings.BLOCK_ALL_IPS else True,
required=False, required=False,
disabled=settings.BLOCK_ALL_IPS, disabled=settings.BLOCK_ALL_IPS,
@ -68,7 +68,7 @@ class ServiceForm(forms.ModelForm):
return False if settings.BLOCK_ALL_IPS else collect_ips return False if settings.BLOCK_ALL_IPS else collect_ips
collaborators = forms.CharField( collaborators = forms.CharField(
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)", help_text=_("Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)"),
required=False, required=False,
) )

View File

@ -0,0 +1,698 @@
# 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: 2022-06-24 13:43+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=2; plural=(n != 1);\n"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "Yes"
msgstr "Ja"
#: forms.py:28 forms.py:29 forms.py:30 forms.py:59
msgid "No"
msgstr "Nein"
#: forms.py:35
msgid "Allowed origins"
msgstr "Erlaubte origins"
#: forms.py:36
msgid "Respect DNT"
msgstr "DNT beachten"
#: forms.py:37
msgid "Ignored IP addresses"
msgstr "Ignorierte IP Adressen"
#: forms.py:38
msgid "Ignore robots"
msgstr "Robots ignorieren"
#: forms.py:39
msgid "Hide specific referrers"
msgstr "Bestimmte Referrer nicht zeigen"
#: forms.py:40
msgid "Additional injected JS"
msgstr "Zusätzlich injiziertes JS"
#: forms.py:43
msgid "What should the service be called?"
msgstr "Welchen Namen soll der Dienst haben?"
#: forms.py:44
msgid "What's the service's primary URL?"
msgstr "Was ist die primäre URL des Dienstes?"
#: forms.py:46
msgid ""
"At what origins does the service operate? Use commas to separate multiple "
"values. This sets CORS headers, so use '*' if you're not sure (or don't "
"care)."
msgstr ""
"Mit welchen origins arbeitet der Dienst? Verwenden Sie Kommas, um mehrere "
"Werte zu trennen. Dies setzt den CORS-Header. Verwenden Sie '*', wenn Sie "
"nicht sicher sind (oder es egal ist)."
#: forms.py:48
msgid ""
"Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/"
"Do_Not_Track'>Do Not Track</a> be excluded from all data?"
msgstr ""
"Sollen Besucher, die <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do "
"Not Track</a> aktiviert haben, von allen Daten ausgeschlossen werden?"
#: forms.py:49
msgid ""
"A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to "
"exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."
msgstr ""
"Eine Komma-separierte Liste von IP Adressen oder IP Bereichen (IPv4 and "
"IPv6), die vom Tracking ausgeschlossen werden sollen (z.B. '192.168.0.2, "
"127.0.0.1/32')."
#: forms.py:50
msgid "Should sessions generated by bots be excluded from tracking?"
msgstr ""
"Sollten von Bots generierte Sitzungen vom Tracking ausgeschlossen werden?"
#: forms.py:51
msgid ""
"Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will "
"not be listed in the referrer summary. Sessions will still be tracked "
"normally. No effect if left blank."
msgstr ""
"Alle Referrer, die diesem <a href='https://regexr.com/'>RegEx</a> "
"entsprechen, werden nicht in der Referrer-Zusammenfassung aufgeführt. Die "
"Sitzungen werden weiterhin normal verfolgt."
#: forms.py:52
msgid ""
"Optional additional JavaScript to inject at the end of the Shynet script. "
"This code will be injected on every page where this service is installed."
msgstr ""
"Optionales zusätzliches JavaScript, das am Ende des Shynet-Skripts eingefügt "
"wird. Dieser Code wird auf jeder Seite eingeschleust, auf der dieser Dienst "
"installiert ist."
#: forms.py:56
msgid "IP address collection is disabled globally by your administrator."
msgstr ""
"Die Erfassung von IP-Adressen wurde vom Administrator global deaktiviert."
#: forms.py:58
msgid ""
"Should individual IP addresses be collected? IP metadata (location, host, "
"etc) will still be collected."
msgstr ""
"Sollten einzelne IP-Adressen erfasst werden? IP-Metadaten (Standort, Host, "
"usw.) werden weiterhin erfasst."
#: forms.py:71
msgid ""
"Which users on this Shynet instance should have read-only access to this "
"service? (Comma separated list of emails.)"
msgstr ""
"Welche Benutzer dieser Shynet-Instanz sollen Lesezugriff auf diesen Dienst "
"haben? (Kommaseparierte Liste von E-Mails.)"
#: templates/account/account_inactive.html:5
#: templates/account/account_inactive.html:6
msgid "Account Inactive"
msgstr "Konto inaktiv"
#: templates/account/account_inactive.html:9
msgid "This account is inactive."
msgstr "Dieses Konto ist inaktiv"
#: templates/account/email.html:5 templates/account/email.html:6
msgid "Email Addresses"
msgstr "E-Mail Adressen"
#: templates/account/email.html:12
msgid "These are your known email addresses:"
msgstr "Ihre bekannten E-Mail Adressen:"
#: templates/account/email.html:26
msgid "Verified"
msgstr "Verifiziert"
#: templates/account/email.html:28
msgid "Unverified"
msgstr "Unverifiziert"
#: templates/account/email.html:30
msgid "Primary"
msgstr "Primär"
#: templates/account/email.html:36
msgid "Make Primary"
msgstr "Als primär kennzeichnen"
#: templates/account/email.html:37
msgid "Resend Verification"
msgstr "Verifikation erneut senden"
#: templates/account/email.html:38
msgid "Remove"
msgstr "Entfernen"
#: templates/account/email.html:46
msgid ""
"You currently do not have an email address associated with your account. "
"Without one, you won't be able to reset your password, receive "
"notifications, etc."
msgstr ""
"Sie haben noch keine E-Mail Adresse mit Ihrem Konto verknüpft. Ohne diese "
"können Sie Ihr Kennwort nicht zurücksetzen, keine Benachrichtigungen "
"erhalten etc."
#: templates/account/email.html:57
msgid "Add Address"
msgstr "Adresse hinzufügen"
#: templates/account/email.html:66
msgid "Do you really want to remove the selected email address?"
msgstr "Wollen Sie diese E-Mail Adresse wirkliche löschen?"
#: templates/account/email/email_confirmation_message.txt:1
#, python-format
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because %(user_display)s has listed this email "
"as a valid contact address for their account.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s\n"
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese E-Mail, da der Benutzer %(user_display)s diese Adresse "
"als gültige Kontakt-Adressse für sein Konto angegeben hat.\n"
"\n"
"Um die E-Mail Adresse zu bestätigen, gehen Sie auf %(activate_url)s\n"
#: templates/account/email/email_confirmation_message.txt:7
#: templates/account/email/password_reset_key_message.txt:9
#, python-format
msgid ""
"Thank you,\n"
"%(site_name)s\n"
msgstr ""
#: templates/account/email/email_confirmation_subject.txt:3
#: templates/account/email_confirm.html:6
#: templates/account/email_confirm.html:7
msgid "Confirm Email Address"
msgstr "E-Mail Adresse bestätigen"
#: templates/account/email/password_reset_key_message.txt:1
msgid ""
"Hi there,\n"
"\n"
"You're receiving this email because you or someone else has requested a "
"password for your account.\n"
"\n"
"This message can be safely ignored if you did not request a password reset. "
"Click the link below to reset your password."
msgstr ""
"Hallo,\n"
"\n"
"Sie erhalten diese E-Mail, weil Sie oder jemand anderes ein Kennwort für Ihr "
"Konto angefordert hat.\n"
"\n"
"Sie können diese Nachricht ignorieren, wenn Sie kein Kennwort angefordert "
"haben. Klicken Sie auf den unten stehenden Link, um Ihr Kennwort "
"zurückzusetzen."
#: templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset Email"
msgstr "Kennwort zurücksetzen E-Mail"
#: templates/account/email_confirm.html:15
#, python-format
msgid ""
"Please confirm that <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> is a valid email where we "
"can reach you."
msgstr ""
"Bitte bestätigen Sie, dass <a\n"
" href=\"mailto:%(email)s\">%(email)s</a> eine gültige E-Mail Adress "
"ist, unter der wie Sie erreichen können."
#: templates/account/email_confirm.html:21
msgid "Confirm"
msgstr "Bestätigen"
#: templates/account/email_confirm.html:28
#, python-format
msgid ""
"This email confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new\n"
" email confirmation request</a>."
msgstr ""
"Diese Bestätigungs-E-Mail ist abgelaufen oder ungültig. Bitte <a href="
"\"%(email_url)s\">fordern Sie eine neue\n"
" Bestätigungs-E-Mail</a>. an"
#: templates/account/login.html:6 templates/account/login.html:7
#: templates/account/login.html:19
msgid "Sign In"
msgstr "Anmelden"
#: templates/account/login.html:20 templates/account/password_reset.html:21
msgid "Reset Password"
msgstr "Kennwort zurücksetzen"
#: templates/account/logout.html:5 templates/account/logout.html:6
#: templates/account/logout.html:16
msgid "Sign Out"
msgstr "Abmelden"
#: templates/account/logout.html:9
msgid "Are you sure you want to sign out?"
msgstr "Sind Sie sicher, dass Sie sich abmelden wollen?"
#: templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary email address (%(email)s)."
msgstr "Sie können Ihre primäre E-Mail Adresse nicht entfernen (%(email)s)."
#: templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation email sent to %(email)s."
msgstr "Bestätigungs-E-Mail gesendet an %(email)s."
#: templates/account/messages/email_confirmed.txt:2
#, python-format
msgid "Confirmed %(email)s."
msgstr "%(email)s bestätigt."
#: templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed email address %(email)s."
msgstr "E-Mail-Adresse %(email)s entfernt."
#: templates/account/messages/logged_in.txt:4
#, python-format
msgid "Successfully signed in as %(name)s."
msgstr "Sid sind angemeldet als %(name)s"
#: templates/account/messages/logged_out.txt:2
msgid "You have signed out."
msgstr "Sie haben sich abgemeldet."
#: templates/account/messages/password_changed.txt:2
msgid "Password successfully changed."
msgstr "Ihr Kennwort wurde erfolgreich geändert."
#: templates/account/messages/password_set.txt:2
msgid "Password successfully set."
msgstr "Kennwort erfolgreich gesetzt."
#: templates/account/messages/primary_email_set.txt:2
msgid "New primary email address set."
msgstr "Eine neue primäre E-Mail Adresse wurde gesetzt."
#: templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary email address must be verified."
msgstr "Ihre primäre E-Mail-Adresse muss verifiziert werden."
#: templates/account/password_change.html:5
#: templates/account/password_change.html:6
#: templates/account/password_change.html:12
#: templates/account/password_reset_from_key.html:4
#: templates/account/password_reset_from_key.html:5
#: templates/account/password_reset_from_key.html:16
#: templates/account/password_reset_from_key_done.html:4
#: templates/account/password_reset_from_key_done.html:5
msgid "Change Password"
msgstr "Kennwort ändern"
#: templates/account/password_reset.html:6
#: templates/account/password_reset.html:7
#: templates/account/password_reset_done.html:6
#: templates/account/password_reset_done.html:7
msgid "Password Reset"
msgstr "Kennwort zurücksetzen"
#: templates/account/password_reset.html:15
msgid ""
"Forgotten your password? Enter your email address below, and we'll send you "
"an email to reset it."
msgstr ""
"Haben Sie Ihr Kennwort vergessen? Geben Sie unten Ihre E-Mail-Adresse ein, "
"und wir senden Ihnen eine E-Mail, um es zurückzusetzen."
#: templates/account/password_reset_done.html:14
msgid ""
"We have sent you an email with a password reset link. Please try again if "
"you do not receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts "
"geschickt. Bitte versuchen Sie es erneut, wenn Sie ihn nicht innerhalb "
"weniger Minuten erhalten."
#: templates/account/password_reset_from_key.html:10
#, python-format
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
"a>."
msgstr ""
"Der Link zum Zurücksetzen des Kennworts war ungültig, möglicherweise weil er "
"bereits verwendet wurde. Bitte fordern Sie eine <a href="
"\"%(passwd_reset_url)s\">neue Kennwortrücksetzung</a> an."
#: templates/account/password_reset_from_key.html:19
#: templates/account/password_reset_from_key_done.html:8
msgid "Your password is now changed."
msgstr "Ihr Kennwort wurde geändert."
#: templates/account/password_set.html:5 templates/account/password_set.html:6
#: templates/account/password_set.html:12
msgid "Set Password"
msgstr "Kennwort setzen"
#: templates/account/signup.html:5 templates/account/signup.html:6
#: templates/account/signup.html:17
msgid "Sign Up"
msgstr "Registrieren"
#: templates/account/signup.html:9
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a> "
"instead."
msgstr ""
"Sie haben bereits ein Konto? Dann melden Sie sich bitt<a href=\"%(login_url)s"
"\">sign in</a> "
#: templates/account/signup_closed.html:5
#: templates/account/signup_closed.html:6
msgid "Sign Up Closed"
msgstr "Registrierung geschlossen"
#: templates/account/signup_closed.html:9
msgid "Public sign-ups are not allowed at this time."
msgstr "Öffentliche Registrierungen sind zur Zeit nicht erlaubt."
#: templates/account/snippets/already_logged_in.html:6
msgid "Note"
msgstr "Hinweis"
#: templates/account/snippets/already_logged_in.html:6
#, python-format
msgid "you are already logged in as %(user_display)s."
msgstr "Sie sind bereits als %(user_display)s angemeldet."
#: templates/account/verification_sent.html:5
#: templates/account/verification_sent.html:6
#: templates/account/verified_email_required.html:5
#: templates/account/verified_email_required.html:6
msgid "Verify Email Address"
msgstr "E-Mail Adresse verifizieren"
#: templates/account/verification_sent.html:9
msgid ""
"We have sent an email to you for verification. Follow the link provided to "
"finalize the signup process. Please try to log in again if you do not "
"receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail zur Verifizierung gesendet. Folgen Sie dem "
"enthaltenen Link, um den Registrierungsprozess abzuschließen. Versuchen Sie "
"sich erneut anzumelden, falls Sie die E-Mail nicht innerhalb weniger Minuten "
"erhalten."
#: templates/account/verified_email_required.html:11
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your email address. "
msgstr ""
"Dieser Teil der Website erfordert eine Überprüfung, dass Sie derjenige "
"sind,\n"
"der Sie vorgeben zu sein. Zu diesem Zweck verlangen wir, dass Sie\n"
"den Besitz Ihrer E-Mail-Adresse bestätigen. "
#: templates/account/verified_email_required.html:15
msgid ""
"We have sent an email to you for\n"
"verification. Please click on the link inside this email. Please\n"
"try again if you do not receive it within a few minutes."
msgstr ""
"Wir haben Ihnen eine E-Mail zur Überprüfung gesendet.\n"
"Bitte klicken Sie auf den Link in dieser E-Mail. Bitte\n"
"versuchen Sie es erneut, wenn Sie die E-Mail nicht innerhalb \n"
"weniger Minuten erhalten."
#: templates/account/verified_email_required.html:19
#, python-format
msgid ""
"<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your "
"email address</a>."
msgstr ""
"<strong>Hinweis:</strong> Sie können Ihre E-Mail-Adresse noch <a href="
"\"%(email_url)s\">ändern</a>."
#: templates/base.html:47
msgid "Services"
msgstr "Sitzungen"
#: templates/base.html:60
msgid "+ Create"
msgstr "+ Neu"
#: templates/base.html:69
msgid "Collaborations"
msgstr "Zusammenarbeit"
#: templates/base.html:81
msgid "Account"
msgstr "Konto"
#: templates/dashboard/includes/service_form.html:8
msgid "Advanced settings"
msgstr "Erweiterte Einstellungen"
#: templates/dashboard/includes/service_overview.html:15
#: templates/dashboard/pages/service_session_list.html:5
msgid "Sessions"
msgstr "Sitzungen"
#: templates/dashboard/includes/service_overview.html:22
#: templates/dashboard/includes/session_list.html:9
#: templates/dashboard/pages/service.html:38
#: templates/dashboard/pages/service.html:110
msgid "Hits"
msgstr "Besuche"
#: templates/dashboard/includes/service_overview.html:29
#: templates/dashboard/pages/service.html:60
msgid "Bounce Rate"
msgstr "Absprungrate"
#: templates/dashboard/includes/service_overview.html:40
msgid "Avg. Duration"
msgstr "Durchschn. Dauer"
#: templates/dashboard/includes/session_list.html:5
msgid "Session Start"
msgstr "Sitzungsstart"
#: templates/dashboard/includes/session_list.html:6
msgid "Identity"
msgstr "Kennung"
#: templates/dashboard/includes/session_list.html:7
#: templates/dashboard/pages/service_session.html:43
msgid "Network"
msgstr "Netzwerk"
#: templates/dashboard/includes/session_list.html:8
#: templates/dashboard/pages/service.html:73
#: templates/dashboard/pages/service_session.html:81
msgid "Duration"
msgstr "Dauer"
#: templates/dashboard/includes/session_list.html:36
msgid "No data yet"
msgstr "Noch keine Daten"
#: templates/dashboard/pages/dashboard.html:16
msgid "New Service"
msgstr "Neuer Dienst"
#: templates/dashboard/pages/index.html:6
#: templates/dashboard/pages/service_session_list.html:9
msgid "Analytics"
msgstr "Analytik"
#: templates/dashboard/pages/index.html:8
msgid "Log In"
msgstr "Anmelden"
#: templates/dashboard/pages/service.html:9
msgid "Manage"
msgstr "Verwalten"
#: templates/dashboard/pages/service.html:17
msgid ""
"This service hasn't collected any data yet. To get started, place the "
"following code snippet at the end of the <code>&lt;body&gt;</code> tag on "
"any page you'd like to track."
msgstr ""
#: templates/dashboard/pages/service.html:29
#: templates/dashboard/pages/service.html:158
#: templates/dashboard/pages/service.html:192
#: templates/dashboard/pages/service.html:226
#: templates/dashboard/pages/service.html:260
#: templates/dashboard/pages/service.html:295
msgid "sessions"
msgstr "Sitzungen"
#: templates/dashboard/pages/service.html:47
msgid "Load Time"
msgstr "Ladezeit"
#: templates/dashboard/pages/service.html:86
msgid "Hits/Session"
msgstr "Besuche/Sitzung"
#: templates/dashboard/pages/service.html:109
#: templates/dashboard/pages/service_session.html:51
msgid "Location"
msgstr "Ort"
#: templates/dashboard/pages/service.html:133
msgid "No data yet..."
msgstr "Noch keine Daten..."
#: templates/dashboard/pages/service.html:141
msgid "Sessions by Geography"
msgstr "Sitzungen nach Geograpie"
#: templates/dashboard/pages/service.html:143
msgid "view table"
msgstr "Tabellenansicht"
#: templates/dashboard/pages/service.html:153
#: templates/dashboard/pages/service_session.html:47
msgid "Country"
msgstr "Land"
#: templates/dashboard/pages/service.html:191
msgid "Referrer"
msgstr ""
#: templates/dashboard/pages/service.html:225
msgid "Operating System"
msgstr "Betriebssystem"
#: templates/dashboard/pages/service.html:259
#: templates/dashboard/pages/service_session.html:27
msgid "Browser"
msgstr ""
#: templates/dashboard/pages/service.html:294
#: templates/dashboard/pages/service_session.html:35
msgid "Device Type"
msgstr "Gerätetyp"
#: templates/dashboard/pages/service.html:329
msgid "View more sessions"
msgstr "Weitere Sitzungen"
#: templates/dashboard/pages/service_create.html:5
#: templates/dashboard/pages/service_create.html:8
msgid "Create Service"
msgstr "Neuer Dienst"
#: templates/dashboard/pages/service_create.html:16
msgid "Create"
msgstr "Neu"
#: templates/dashboard/pages/service_create.html:17
#: templates/dashboard/pages/service_update.html:30
msgid "Cancel"
msgstr "Abbrechen"
#: templates/dashboard/pages/service_delete.html:5
#: templates/dashboard/pages/service_update.html:33
msgid "Delete"
msgstr "Löschen"
#: templates/dashboard/pages/service_delete.html:12
msgid ""
"Are you sure you want to delete this service? All of its analytics and "
"associated data will be permanently deleted."
msgstr ""
"Sind Sie sicher, dass Sie diesen Dienst löschen wollen? Alle damit "
"verbundenen Daten werden unwiederruflich gelöscht."
#: templates/dashboard/pages/service_session.html:31
msgid "Device"
msgstr "Geräte"
#: templates/dashboard/pages/service_session.html:39
msgid "OS"
msgstr "Betriebssystem"
#: templates/dashboard/pages/service_session.html:54
msgid "Open in Maps"
msgstr "In Karte öffnen"
#: templates/dashboard/pages/service_session.html:56
msgid "Unknown"
msgstr "Unbekannt"
#: templates/dashboard/pages/service_session.html:85
msgid "Load"
msgstr "Laden"
#: templates/dashboard/pages/service_session.html:89
msgid "Tracker"
msgstr ""
#: templates/dashboard/pages/service_update.html:5
msgid "Management"
msgstr "Verwaltung"
#: templates/dashboard/pages/service_update.html:8
msgid "View"
msgstr "Ansicht"
#: templates/dashboard/pages/service_update.html:13
msgid "Installation"
msgstr ""
#: templates/dashboard/pages/service_update.html:15
msgid ""
"Place the following snippet at the end of the <code>&lt;body&gt;</code> tag "
"on any page you'd like to track."
msgstr ""
"Kopieren Sie den folgenden Code ans Ende des <code>&lt;body&gt;</code> Tags "
"in allen Seiten, die Sie tracken möchten."
#: templates/dashboard/pages/service_update.html:21
msgid "Settings"
msgstr "Einstellungen"
#: templates/dashboard/pages/service_update.html:29
msgid "Save"
msgstr "Speichern"

View File

@ -1,4 +1,4 @@
{% load static rules helpers %} {% load static i18n rules helpers %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -43,7 +43,9 @@
<div id="navMenuExpanded" <div id="navMenuExpanded"
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full"> class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
{% if user.owning_services.all %} {% if user.owning_services.all %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Services' %}
</p>
{% for service in user.owning_services.all %} {% for service in user.owning_services.all %}
{% contextual_url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
@ -55,14 +57,17 @@
{% has_perm 'core.create_service' user as can_create %} {% has_perm 'core.create_service' user as can_create %}
{% if can_create %} {% if can_create %}
{% url 'dashboard:service_create' as url %} {% url 'dashboard:service_create' as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label="+ Create" url=url %} {% trans '+ Create' as create %}
{% include 'dashboard/includes/sidebar_portal.html' with label=create url=url %}
<hr class="sep h-8"> <hr class="sep h-8">
{% endif %} {% endif %}
{% if user.collaborating_services.all %} {% if user.collaborating_services.all %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Collaborations' %}
</p>
{% for service in user.collaborating_services.all %} {% for service in user.collaborating_services.all %}
{% contextual_url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
@ -72,7 +77,9 @@
<hr class="sep h-8"> <hr class="sep h-8">
{% endif %} {% endif %}
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Account</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">
{% trans 'Account' %}
</p>
{% if user.is_authenticated %} {% if user.is_authenticated %}

View File

@ -1,3 +1,5 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
<form method="GET" id="datePicker"> <form method="GET" id="datePicker">
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate"> <input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate"> <input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
@ -20,11 +22,20 @@
} }
</style> </style>
<script> <script>
var picker = new Litepicker({ function getLocaleDateString(locale) {
const formats = {
'de': "DD. MMM YY"
}
return formats[locale] || "MMM DD 'YY";
}
let locale = "{{ LANGUAGE_CODE }}";
let picker = new Litepicker({
element: document.getElementById('rangePicker'), element: document.getElementById('rangePicker'),
plugins: ['ranges'], plugins: ['ranges'],
singleMode: false, singleMode: false,
format: "MMM DD 'YY", lang: locale,
format: getLocaleDateString(locale),
maxDate: new Date(), maxDate: new Date(),
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")), startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")), endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),

View File

@ -1,11 +1,11 @@
{% load a17t_tags %} {% load i18n a17t_tags %}
{{form.name|a17t}} {{form.name|a17t}}
{{form.link|a17t}} {{form.link|a17t}}
{{form.collaborators|a17t}} {{form.collaborators|a17t}}
<details {% if form.errors %}open{% endif %}> <details {% if form.errors %}open{% endif %}>
<summary class="cursor-pointer text-sm">Advanced settings</summary> <summary class="cursor-pointer text-sm">{% trans 'Advanced settings' %}</summary>
<hr class="sep h-4"> <hr class="sep h-4">
{{form.respect_dnt|a17t}} {{form.respect_dnt|a17t}}
{{form.collect_ips|a17t}} {{form.collect_ips|a17t}}

View File

@ -1,4 +1,4 @@
{% load humanize helpers %} {% load i18n humanize helpers %}
<a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}"> <a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
{% with stats=object.stats %} {% with stats=object.stats %}
@ -12,21 +12,21 @@
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none"> <div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none">
<div> <div>
<p>Sessions</p> <p>{% trans 'Sessions' %}</p>
<p class="label"> <p class="label">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
{% compare stats.compare.session_count stats.session_count "UP" %} {% compare stats.compare.session_count stats.session_count "UP" %}
</p> </p>
</div> </div>
<div> <div>
<p>Hits</p> <p>{% trans 'Hits' %}</p>
<p class="label"> <p class="label">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
{% compare stats.compare.hit_count stats.hit_count "UP" %} {% compare stats.compare.hit_count stats.hit_count "UP" %}
</p> </p>
</div> </div>
<div> <div>
<p>Bounce Rate</p> <p>{% trans 'Bounce Rate' %}</p>
<p class="label"> <p class="label">
{% if stats.bounce_rate_pct != None %} {% if stats.bounce_rate_pct != None %}
{{stats.bounce_rate_pct|floatformat:"-1"}}% {{stats.bounce_rate_pct|floatformat:"-1"}}%
@ -37,7 +37,7 @@
</p> </p>
</div> </div>
<div> <div>
<p>Avg. Duration</p> <p>{% trans 'Avg. Duration' %}</p>
<p class="label"> <p class="label">
{% if stats.avg_session_duration != None %} {% if stats.avg_session_duration != None %}
{{stats.avg_session_duration|naturaldelta}} {{stats.avg_session_duration|naturaldelta}}

View File

@ -1,12 +1,12 @@
{% load humanize helpers %} {% load i18n humanize helpers %}
<table class="table"> <table class="table">
<thead> <thead>
<th>Session Start</th> <th>{% trans 'Session Start' %}</th>
<th>Identity</th> <th>{% trans 'Identity' %}</th>
<th>Network</th> <th>{% trans 'Network' %}</th>
<th class="rf">Duration</th> <th class="rf">{% trans 'Duration' %}</th>
<th class="rf">Hits</th> <th class="rf">{% trans 'Hits' %}</th>
</thead> </thead>
<tbody> <tbody>
{% for session in object_list %} {% for session in object_list %}
@ -14,7 +14,7 @@
<td> <td>
<a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}" <a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}"
class="font-medium text-urge-700"> class="font-medium text-urge-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}} {{ session.start_time|date:"DATETIME_FORMAT"|capfirst }}
{% if session.is_currently_active %} {% if session.is_currently_active %}
<span class="badge ~positive">Online</span> <span class="badge ~positive">Online</span>
{% endif %} {% endif %}
@ -33,7 +33,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td><span class="text-gray-600">No data yet...</span></td> <td><span class="text-gray-600">{% trans 'No data yet' %}...</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -1,6 +1,6 @@
{% load helpers %} {% load i18n helpers %}
<div> <div>
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{{label}}" <a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{% trans label %}"
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a> {% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{% trans label %}</span></a>
</div> </div>

View File

@ -1,6 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load rules pagination %} {% load i18n rules pagination %}
{% block content %} {% block content %}
<div class="md:flex justify-between items-center"> <div class="md:flex justify-between items-center">
@ -13,7 +13,7 @@
</div> </div>
{% has_perm "core.create_service" user as can_create %} {% has_perm "core.create_service" user as can_create %}
{% if can_create %} {% if can_create %}
<a href="{% url 'dashboard:service_create' %}" class="button field !low bg-neutral-000 w-auto">+ New Service</a> <a href="{% url 'dashboard:service_create' %}" class="button field !low bg-neutral-000 w-auto">+ {% trans 'New Service' %}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -1,9 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %}
{% block content %} {% block content %}
<section class="content"> <section class="content">
<h2>{{request.site.name}} Analytics</h2> <h2>{{request.site.name}} {% trans 'Analytics' %}</h2>
<p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p> <p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a> <a href="{% url 'account_login' %}" class="button ~urge !high">{% trans 'Log In' %}</a>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,12 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load humanize helpers rules %} {% load i18n humanize helpers rules %}
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
{% has_perm 'core.change_service' user object as can_update %} {% has_perm 'core.change_service' user object as can_update %}
{% if can_update %} {% if can_update %}
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field !low bg-neutral-000 w-auto">Manage &rarr;</a> <a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field !low bg-neutral-000 w-auto">{% trans 'Manage' %} &rarr;</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@ -14,7 +14,11 @@
{% if not stats.has_hits %} {% if not stats.has_hits %}
<div class="content mb-6"> <div class="content mb-6">
<p> <p>
This service hasn't collected any data yet. To get started, place the following code snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track. {% blocktrans trimmed %}
This service hasn't collected any data yet. To get started, place the
following code snippet at the end of the <code>&lt;body&gt;</code> tag on any
page you'd like to track.
{% endblocktrans %}
</p> </p>
{% include 'dashboard/includes/service_snippet.html' %} {% include 'dashboard/includes/service_snippet.html' %}
</div> </div>
@ -22,7 +26,7 @@
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats"> <div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
{% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %} {% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
<article class=""> <article class="">
<p class="label text-gray-400">Sessions</p> <p class="label text-gray-400">{% trans 'sessions' %}</p>
<p class="heading"> <p class="heading">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
<div> <div>
@ -31,7 +35,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Hits</p> <p class="label text-gray-400">{% trans 'Hits' %}</p>
<p class="heading"> <p class="heading">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
<div> <div>
@ -40,7 +44,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Load Time</p> <p class="label text-gray-400">{% trans 'Load Time' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_load_time %} {% if stats.avg_load_time %}
{{stats.avg_load_time|floatformat:"0"}}ms {{stats.avg_load_time|floatformat:"0"}}ms
@ -53,7 +57,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Bounce Rate</p> <p class="label text-gray-400">{% trans 'Bounce Rate' %}</p>
<p class="heading"> <p class="heading">
{% if stats.bounce_rate_pct %} {% if stats.bounce_rate_pct %}
{{stats.bounce_rate_pct|floatformat:"-1"}}% {{stats.bounce_rate_pct|floatformat:"-1"}}%
@ -66,7 +70,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Duration</p> <p class="label text-gray-400">{% trans 'Duration' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_session_duration %} {% if stats.avg_session_duration %}
{{stats.avg_session_duration|naturaldelta}} {{stats.avg_session_duration|naturaldelta}}
@ -79,7 +83,7 @@
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Hits/Session</p> <p class="label text-gray-400">{% trans 'Hits/Session' %}</p>
<p class="heading"> <p class="heading">
{% if stats.avg_hits_per_session %} {% if stats.avg_hits_per_session %}
{{stats.avg_hits_per_session|floatformat:"-1"}} {{stats.avg_hits_per_session|floatformat:"-1"}}
@ -102,8 +106,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Location</th> <th>{% trans 'Location' %}</th>
<th class="rf">Hits</th> <th class="rf">{% trans 'Hits' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -126,7 +130,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td><span class="text-gray-600">No data yet...</span></td> <td><span class="text-gray-600">{% trans 'No data yet...' %}</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -134,9 +138,9 @@
</div> </div>
<div class="geo-map card ~neutral !low py-2 overflow-y-hidden"> <div class="geo-map card ~neutral !low py-2 overflow-y-hidden">
<p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)"> <p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)">
Sessions by Geography &nbsp {% trans 'Sessions by Geography' %} &nbsp
<button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 button ~urge !low"> <button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 button ~urge !low">
(view table) ({% trans 'view table' %})
</button> </button>
</p> </p>
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %} {% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
@ -146,12 +150,12 @@
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th> <th>
Country &nbsp {% trans 'Country' %} &nbsp
<button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 button ~urge !low"> <button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 button ~urge !low">
(view map) (view map)
</button> </button>
</th> </th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -184,8 +188,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Referrer</th> <th>{% trans 'Referrer' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -218,8 +222,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Operating System</th> <th>{% trans 'Operating System' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -252,8 +256,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Browser</th> <th>{% trans 'Browser' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -287,8 +291,8 @@
<table class="table"> <table class="table">
<thead class="text-sm"> <thead class="text-sm">
<tr> <tr>
<th>Device Type</th> <th>{% trans 'Device Type' %}</th>
<th class="rf">Sessions</th> <th class="rf">{% trans 'sessions' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -321,8 +325,8 @@
<div class="card ~neutral !low py-2 overflow-auto"> <div class="card ~neutral !low py-2 overflow-auto">
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8 md:h-12"> <hr class="sep h-8 md:h-12">
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more <a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">
sessions {% trans 'View more sessions' %} &rarr;
&rarr;</a> </a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}Create Service{% endblock %} {% block head_title %}{% trans 'Create Service' %}{% endblock %}
{% block content %} {% block content %}
<h4 class="heading leading-none">Create Service</h4> <h4 class="heading leading-none">{% trans 'Create Service' %}</h4>
<hr class="sep"> <hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}
@ -13,8 +13,8 @@
{% include 'dashboard/includes/service_form.html' %} {% include 'dashboard/includes/service_form.html' %}
</div> </div>
<div class="section ~urge !normal p-4"> <div class="section ~urge !normal p-4">
<button type="submit" class="button ~urge !high">Create</button> <button type="submit" class="button ~urge !high">{% trans 'Create' %}</button>
<a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">Cancel</a> <a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">{% trans 'Cancel' %}</a>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -2,14 +2,18 @@
{% load a17t_tags %} {% load a17t_tags %}
{% block head_title %}Delete {{object.name}}{% endblock %} {% block head_title %}{% trans 'Delete' %} {{object.name}}{% endblock %}
{% block service_content %} {% block service_content %}
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
<p>Are you sure you want to delete this service? All of its <p>
analytics and associated data will be permanently deleted.</p> {% blocktrans trimmed %}
Are you sure you want to delete this service? All of its
analytics and associated data will be permanently deleted.
{% endblocktrans %}
</p>
{{form|a17t}} {{form|a17t}}
</div> </div>
<div class="section ~critical !normal p-4"> <div class="section ~critical !normal p-4">

View File

@ -24,37 +24,36 @@
<hr class="sep h-8 md:h-12"> <hr class="sep h-8 md:h-12">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
<div> <div>
<p>Browser</p> <p>{% trans 'Browser' %}</p>
<p class="label">{{session.browser|default:"Unknown"}}</p> <p class="label">{{session.browser|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Device</p> <p>{% trans 'Device' %}</p>
<p class="label">{{session.device|default:"Unknown"}}</p> <p class="label">{{session.device|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Device Type</p> <p{% trans 'Device Type' %}</p>
<p class="label">{{session.device_type|title}}</p> <p class="label">{{session.device_type|title}}</p>
</div> </div>
<div> <div>
<p>OS</p> <p>{% trans 'OS' %}</p>
<p class="label">{{session.os|default:"Unknown"}}</p> <p class="label">{{session.os|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Network</p> <p>{% trans 'Network' %}</p>
<p class="label">{{session.asn|default:"Unknown"}}</p> <p class="label">{{session.asn|default:"Unknown"}}</p>
</div> </div>
<div> <div>
<p>Country</p> <p>{% trans 'Country' %}</p>
<p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p> <p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p>
</div> </div>
<div> <div>
<p>Location</p> <p>{% trans 'Location' %}</p>
<p class="label"> <p class="label">
{% if session.latitude %} {% if session.latitude %}
<a href="{{session|location_url}}" target="_blank">Open <a href="{{session|location_url}}" target="_blank">{% trans 'Open in Maps' %} &nearr;</a>
in Maps &nearr;</a>
{% else %} {% else %}
Unknown {% trans 'Unknown' %}
{% endif %} {% endif %}
</p> </p>
</div> </div>
@ -79,15 +78,15 @@
</div> </div>
<div class="grid grid-cols-3 gap-3 md:pl-8 md:w-1/2"> <div class="grid grid-cols-3 gap-3 md:pl-8 md:w-1/2">
<div> <div>
<p>Duration</p> <p>{% trans 'Duration' %}</p>
<p class="label">{{hit.duration|naturaldelta}}</p> <p class="label">{{hit.duration|naturaldelta}}</p>
</div> </div>
<div> <div>
<p>Load</p> <p>{% trans 'Load' %}</p>
<p class="label">{{hit.load_time|floatformat:"0"}}ms</p> <p class="label">{{hit.load_time|floatformat:"0"}}ms</p>
</div> </div>
<div> <div>
<p>Tracker</p> <p>{% trans 'Tracker' %}</p>
<p class="label">{{hit.tracker}}</p> <p class="label">{{hit.tracker}}</p>
</div> </div>
</div> </div>

View File

@ -1,12 +1,12 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags pagination humanize helpers %} {% load i18n a17t_tags pagination humanize helpers %}
{% block head_title %}{{object.name}} Sessions{% endblock %} {% block head_title %}{{object.name}} {% trans 'Sessions' %}{% endblock %}
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low bg-neutral-000 w-auto">Analytics &rarr;</a> <a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral !low bg-neutral-000 w-auto">{% trans 'Analytics' %} &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}

View File

@ -1,20 +1,24 @@
{% extends "dashboard/service_base.html" %} {% extends "dashboard/service_base.html" %}
{% load a17t_tags %} {% load i18n a17t_tags %}
{% block head_title %}{{object.name}} Management{% endblock %} {% block head_title %}{{object.name}} {% trans 'Management' %}{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">View &rarr;</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button field !low bg-neutral-000 w-auto">{% trans 'View' %} &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
<div class="max-w-xl content"> <div class="max-w-xl content">
<h5>Installation</h5> <h5>{% trans 'Installation' %}</h5>
<p>Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.</p> <p>
{% blocktrans trimmed %}
Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.
{% endblocktrans %}
</p>
{% include 'dashboard/includes/service_snippet.html' %} {% include 'dashboard/includes/service_snippet.html' %}
<hr class="sep h-4"> <hr class="sep h-4">
<h5>Settings</h5> <h5>{% trans 'Settings' %}</h5>
<form class="card ~neutral !low p-0" method="POST"> <form class="card ~neutral !low p-0" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
@ -22,11 +26,11 @@
</div> </div>
<div class="section ~neutral !normal p-4 flex justify-between"> <div class="section ~neutral !normal p-4 flex justify-between">
<div> <div>
<button type="submit" class="button ~neutral !high">Save</button> <button type="submit" class="button ~neutral !high">{% trans 'Save' %}</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">Cancel</a> <a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">{% trans 'Cancel' %}</a>
</div> </div>
<div> <div>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">Delete</a> <a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">{% trans 'Delete' %}</a>
</div> </div>
</div> </div>
</form> </form>

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
""" """
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
# import module sys to get the type of exception # import module sys to get the type of exception
@ -201,7 +202,7 @@ LOGGING = {
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/ # https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = os.getenv("LANGUAGE_CODE", "en-us")
TIME_ZONE = os.getenv("TIME_ZONE", "America/New_York") TIME_ZONE = os.getenv("TIME_ZONE", "America/New_York")