diff --git a/website/backend/FAQ/admin.py b/website/backend/FAQ/admin.py index fe4cdd3d96..87a475db57 100644 --- a/website/backend/FAQ/admin.py +++ b/website/backend/FAQ/admin.py @@ -1,10 +1,12 @@ from django.contrib import admin from .models import FAQ +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. @admin.register(FAQ) -class FAQAdmin(admin.ModelAdmin): +class FAQAdmin(TranslationAdmin): list_display = ('question', 'answer', 'created_at') readonly_fields = ('created_at', 'updated_at') list_per_page = 10 diff --git a/website/backend/FAQ/migrations/0002_faq_answer_en_faq_answer_fr_faq_question_en_and_more.py b/website/backend/FAQ/migrations/0002_faq_answer_en_faq_answer_fr_faq_question_en_and_more.py new file mode 100644 index 0000000000..e216695c95 --- /dev/null +++ b/website/backend/FAQ/migrations/0002_faq_answer_en_faq_answer_fr_faq_question_en_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.5 on 2024-02-07 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('FAQ', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='faq', + name='answer_en', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='faq', + name='answer_fr', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='faq', + name='question_en', + field=models.CharField(max_length=150, null=True), + ), + migrations.AddField( + model_name='faq', + name='question_fr', + field=models.CharField(max_length=150, null=True), + ), + ] diff --git a/website/backend/FAQ/translation.py b/website/backend/FAQ/translation.py new file mode 100644 index 0000000000..12e823b0e4 --- /dev/null +++ b/website/backend/FAQ/translation.py @@ -0,0 +1,7 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(FAQ) +class FAQTranslationOptions(TranslationOptions): + fields = ('question', 'answer',) diff --git a/website/backend/FAQ/views.py b/website/backend/FAQ/views.py index 5be044810e..13fbfdb60a 100644 --- a/website/backend/FAQ/views.py +++ b/website/backend/FAQ/views.py @@ -1,3 +1,4 @@ +from django.utils import translation from rest_framework import viewsets from .models import FAQ from .serializers import FAQSerializer @@ -6,3 +7,12 @@ class FAQViewSet(viewsets.ReadOnlyModelViewSet): queryset = FAQ.objects.all() serializer_class = FAQSerializer + permission_classes = [] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/africancities/admin.py b/website/backend/africancities/admin.py index b706d5d9cd..f74ff5baa0 100644 --- a/website/backend/africancities/admin.py +++ b/website/backend/africancities/admin.py @@ -1,29 +1,36 @@ from django.contrib import admin import nested_admin from .models import AfricanCountry, City, Content, Image, Description +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. + + class ImageInline(nested_admin.NestedTabularInline): fields = ('image', 'order') readonly_fields = ('author', 'updated_by') model = Image extra = 0 + class DescriptionInline(nested_admin.NestedTabularInline): - fields = ('paragraph','order') + fields = ('paragraph_en', 'paragraph_fr', 'order') readonly_fields = ('author', 'updated_by') model = Description extra = 0 + class ContentInline(nested_admin.NestedStackedInline): - fields = ('title','order') + fields = ('title_en', 'title_fr', 'order') readonly_fields = ('author', 'updated_by') model = Content extra = 0 - inlines = (DescriptionInline,ImageInline,) + inlines = (DescriptionInline, ImageInline,) + class CityInline(nested_admin.NestedTabularInline): - fields = ('city_name','order') + fields = ('city_name_en', 'city_name_fr', 'order') readonly_fields = ('author', 'updated_by') model = City extra = 0 @@ -31,11 +38,11 @@ class CityInline(nested_admin.NestedTabularInline): @admin.register(AfricanCountry) -class AfricanCitiesAdmin(nested_admin.NestedModelAdmin): - fields = ('country_name', 'country_flag','order','author', 'updated_by') +class AfricanCitiesAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): + fields = ('country_name', 'country_flag', 'order', 'author', 'updated_by') readonly_fields = ('id', 'author', 'created', 'updated_by', 'modified') - list_display=('country_name','flag_preview','order','author') - search_fields =('country_name','author') + list_display = ('country_name', 'flag_preview', 'order', 'author') + search_fields = ('country_name', 'author') list_filter = ('created',) inlines = (CityInline,) list_per_page = 10 @@ -46,4 +53,4 @@ def flag_preview(self, obj): return format_html(f'') - flag_preview.allow_tags = True \ No newline at end of file + flag_preview.allow_tags = True diff --git a/website/backend/africancities/migrations/0002_africancountry_country_name_en_and_more.py b/website/backend/africancities/migrations/0002_africancountry_country_name_en_and_more.py new file mode 100644 index 0000000000..a126438ec4 --- /dev/null +++ b/website/backend/africancities/migrations/0002_africancountry_country_name_en_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.5 on 2024-02-07 17:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('africancities', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='africancountry', + name='country_name_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='africancountry', + name='country_name_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='city', + name='city_name_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='city', + name='city_name_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='content', + name='title_en', + field=models.CharField(max_length=150, null=True), + ), + migrations.AddField( + model_name='content', + name='title_fr', + field=models.CharField(max_length=150, null=True), + ), + migrations.AddField( + model_name='description', + name='paragraph_en', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='description', + name='paragraph_fr', + field=models.TextField(null=True), + ), + ] diff --git a/website/backend/africancities/translation.py b/website/backend/africancities/translation.py new file mode 100644 index 0000000000..8f1fe4d336 --- /dev/null +++ b/website/backend/africancities/translation.py @@ -0,0 +1,22 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(AfricanCountry) +class AfricanCountryTranslationOptions(TranslationOptions): + fields = ('country_name',) + + +@register(City) +class CityTranslationOptions(TranslationOptions): + fields = ('city_name',) + + +@register(Content) +class ContentTranslationOptions(TranslationOptions): + fields = ('title',) + + +@register(Description) +class DescriptionTranslationOptions(TranslationOptions): + fields = ('paragraph',) diff --git a/website/backend/africancities/views.py b/website/backend/africancities/views.py index f932896288..7368bc7511 100644 --- a/website/backend/africancities/views.py +++ b/website/backend/africancities/views.py @@ -1,3 +1,4 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import AfricanCountry @@ -5,7 +6,16 @@ # Create your views here. + class AfricanCityViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) queryset = AfricanCountry.objects.all() - serializer_class = AfricanCitySerializer \ No newline at end of file + serializer_class = AfricanCitySerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/assets/cleanair/resources/Screenshot_2024-02-06_121827.png b/website/backend/assets/cleanair/resources/Screenshot_2024-02-06_121827.png new file mode 100644 index 0000000000..d8eceb56e2 Binary files /dev/null and b/website/backend/assets/cleanair/resources/Screenshot_2024-02-06_121827.png differ diff --git a/website/backend/assets/events/Screenshot_2024-02-02_220244.png b/website/backend/assets/events/Screenshot_2024-02-02_220244.png new file mode 100644 index 0000000000..c759a51279 Binary files /dev/null and b/website/backend/assets/events/Screenshot_2024-02-02_220244.png differ diff --git a/website/backend/assets/publications/Screenshot_2024-02-06_121611.png b/website/backend/assets/publications/Screenshot_2024-02-06_121611.png new file mode 100644 index 0000000000..3b9e165a23 Binary files /dev/null and b/website/backend/assets/publications/Screenshot_2024-02-06_121611.png differ diff --git a/website/backend/board/admin.py b/website/backend/board/admin.py index 8df0f80745..c67ea543c0 100644 --- a/website/backend/board/admin.py +++ b/website/backend/board/admin.py @@ -1,16 +1,21 @@ from django.contrib import admin from .models import BoardMember, BoardMemberBiography import nested_admin +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. + + class BoardMemberBiographyInline(nested_admin.NestedTabularInline): - fields = ('description', 'author', 'order') + fields = ('description_en', 'description_fr', 'author', 'order') readonly_fields = ('author', ) model = BoardMemberBiography extra = 0 + @admin.register(BoardMember) -class BoardMemberAdmin(nested_admin.NestedModelAdmin): +class BoardMemberAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): list_display = ("name", "title", "image_tag") readonly_fields = ( "id", diff --git a/website/backend/board/migrations/0002_boardmember_name_en_boardmember_name_fr_and_more.py b/website/backend/board/migrations/0002_boardmember_name_en_boardmember_name_fr_and_more.py new file mode 100644 index 0000000000..7c8f2d4770 --- /dev/null +++ b/website/backend/board/migrations/0002_boardmember_name_en_boardmember_name_fr_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.5 on 2024-02-07 14:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('board', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='boardmember', + name='name_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='boardmember', + name='name_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='boardmember', + name='title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='boardmember', + name='title_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='boardmemberbiography', + name='description_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='boardmemberbiography', + name='description_fr', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/website/backend/board/translation.py b/website/backend/board/translation.py new file mode 100644 index 0000000000..8a0fa9dbd9 --- /dev/null +++ b/website/backend/board/translation.py @@ -0,0 +1,12 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(BoardMember) +class BoardMemberTranslationOptions(TranslationOptions): + fields = ('name', 'title') + + +@register(BoardMemberBiography) +class BoardMemberBiographyTranslationOptions(TranslationOptions): + fields = ('description',) diff --git a/website/backend/board/views.py b/website/backend/board/views.py index 59a35ac861..b196cf268a 100644 --- a/website/backend/board/views.py +++ b/website/backend/board/views.py @@ -1,3 +1,4 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import BoardMember @@ -5,7 +6,14 @@ class BoardViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) - ordering_fields = ('order', 'name') queryset = BoardMember.objects.all() serializer_class = BoardMemberSerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/career/admin.py b/website/backend/career/admin.py index 24645ffb40..a1ba2596c2 100644 --- a/website/backend/career/admin.py +++ b/website/backend/career/admin.py @@ -1,30 +1,34 @@ from django.contrib import admin import nested_admin from .models import Department, Career, JobDescription, BulletDescription, BulletPoint +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. @admin.register(Department) -class DepartmentAdmin(admin.ModelAdmin): +class DepartmentAdmin(TranslationAdmin): list_display = ('id', 'name') readonly_fields = ('id', 'author', 'updated_by') list_per_page = 10 search_fields = ('id', 'name') + class JobDescriptionInline(nested_admin.NestedTabularInline): - fields = ('description', 'order', 'author') + fields = ('description_en', 'description_fr', 'order', 'author') readonly_fields = ('author', ) model = JobDescription extra = 0 + class BulletPointInline(nested_admin.NestedTabularInline): - fields = ('point', 'order') + fields = ('point_en', 'point_fr', 'order') model = BulletPoint extra = 0 class BulletDescriptionInline(nested_admin.NestedTabularInline): - fields = ('name', 'order', 'author') + fields = ('name_en', 'name_fr', 'order', 'author') readonly_fields = ('author',) model = BulletDescription inlines = (BulletPointInline, ) @@ -32,13 +36,14 @@ class BulletDescriptionInline(nested_admin.NestedTabularInline): @admin.register(Career) -class CareerAdmin(nested_admin.NestedModelAdmin): - list_display = ('title','department', 'closing_date', 'author') +class CareerAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): + list_display = ('title', 'department', 'closing_date', 'author') readonly_fields = ('id', 'author', 'created', 'updated_by', 'modified') - fields = ('id', "title", "department", "type", "apply_url", "closing_date", 'author', 'created', 'updated_by', 'modified') + fields = ('id', "title", "department", "type", "apply_url", + "closing_date", 'author', 'created', 'updated_by', 'modified') list_per_page = 10 - search_fields = ('title','department') - list_filter=('department','closing_date') + search_fields = ('title', 'department') + list_filter = ('department', 'closing_date') inlines = (JobDescriptionInline, BulletDescriptionInline) diff --git a/website/backend/career/migrations/0007_bulletdescription_name_en_bulletdescription_name_fr_and_more.py b/website/backend/career/migrations/0007_bulletdescription_name_en_bulletdescription_name_fr_and_more.py new file mode 100644 index 0000000000..06091d2094 --- /dev/null +++ b/website/backend/career/migrations/0007_bulletdescription_name_en_bulletdescription_name_fr_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 4.2.5 on 2024-02-07 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('career', '0006_auto_20220718_0617'), + ] + + operations = [ + migrations.AddField( + model_name='bulletdescription', + name='name_en', + field=models.CharField(max_length=30, null=True), + ), + migrations.AddField( + model_name='bulletdescription', + name='name_fr', + field=models.CharField(max_length=30, null=True), + ), + migrations.AddField( + model_name='bulletpoint', + name='point_en', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='bulletpoint', + name='point_fr', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='career', + name='title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='career', + name='title_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='department', + name='name_en', + field=models.CharField(max_length=30, null=True), + ), + migrations.AddField( + model_name='department', + name='name_fr', + field=models.CharField(max_length=30, null=True), + ), + migrations.AddField( + model_name='jobdescription', + name='description_en', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='jobdescription', + name='description_fr', + field=models.TextField(null=True), + ), + ] diff --git a/website/backend/career/translation.py b/website/backend/career/translation.py new file mode 100644 index 0000000000..b4016e8b0f --- /dev/null +++ b/website/backend/career/translation.py @@ -0,0 +1,27 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(Department) +class DepartmentTranslationOptions(TranslationOptions): + fields = ('name',) + + +@register(Career) +class CareerTranslationOptions(TranslationOptions): + fields = ('title',) + + +@register(JobDescription) +class JobDescriptionTranslationOptions(TranslationOptions): + fields = ('description',) + + +@register(BulletDescription) +class BulletDescriptionTranslationOptions(TranslationOptions): + fields = ('name',) + + +@register(BulletPoint) +class BulletPointTranslationOptions(TranslationOptions): + fields = ('point',) diff --git a/website/backend/career/views.py b/website/backend/career/views.py index 42c5f72853..4a15a5e039 100644 --- a/website/backend/career/views.py +++ b/website/backend/career/views.py @@ -1,16 +1,27 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Career, Department from .serializers import CareerSerializer, DepartmentSerializer -class DepartmentViewSet(viewsets.ReadOnlyModelViewSet): +class BaseViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (AllowAny,) + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) + + +class DepartmentViewSet(BaseViewSet): queryset = Department.objects.all() serializer_class = DepartmentSerializer -class CareerViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) +class CareerViewSet(BaseViewSet): queryset = Career.objects.all() serializer_class = CareerSerializer diff --git a/website/backend/cleanair/admin.py b/website/backend/cleanair/admin.py index ecf8cbd752..812272c3ca 100644 --- a/website/backend/cleanair/admin.py +++ b/website/backend/cleanair/admin.py @@ -1,11 +1,16 @@ from django.contrib import admin from .models import CleanAirResource +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. + + @admin.register(CleanAirResource) -class CleanAirResourceAdmin(admin.ModelAdmin): - fields = ('resource_category','resource_title', 'resource_link', 'resource_file', 'author_title','resource_authors','order',) - list_filter = ("resource_category",'created') - list_display=('resource_title','resource_category','order','author') - search_fields = ("resource_title",'author') +class CleanAirResourceAdmin(TranslationAdmin): + fields = ('resource_category', 'resource_title', 'resource_link', + 'resource_file', 'author_title', 'resource_authors', 'order',) + list_filter = ("resource_category", 'created') + list_display = ('resource_title', 'resource_category', 'order', 'author') + search_fields = ("resource_title", 'author') list_per_page = 12 diff --git a/website/backend/cleanair/migrations/0005_cleanairresource_author_title_en_and_more.py b/website/backend/cleanair/migrations/0005_cleanairresource_author_title_en_and_more.py new file mode 100644 index 0000000000..34eda1589d --- /dev/null +++ b/website/backend/cleanair/migrations/0005_cleanairresource_author_title_en_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.5 on 2024-02-07 17:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cleanair', '0004_cleanairresource_resource_authors'), + ] + + operations = [ + migrations.AddField( + model_name='cleanairresource', + name='author_title_en', + field=models.CharField(blank=True, default='Created By', max_length=40, null=True), + ), + migrations.AddField( + model_name='cleanairresource', + name='author_title_fr', + field=models.CharField(blank=True, default='Created By', max_length=40, null=True), + ), + migrations.AddField( + model_name='cleanairresource', + name='resource_authors_en', + field=models.CharField(default='AirQo', max_length=200, null=True), + ), + migrations.AddField( + model_name='cleanairresource', + name='resource_authors_fr', + field=models.CharField(default='AirQo', max_length=200, null=True), + ), + migrations.AddField( + model_name='cleanairresource', + name='resource_title_en', + field=models.CharField(max_length=120, null=True), + ), + migrations.AddField( + model_name='cleanairresource', + name='resource_title_fr', + field=models.CharField(max_length=120, null=True), + ), + ] diff --git a/website/backend/cleanair/translation.py b/website/backend/cleanair/translation.py new file mode 100644 index 0000000000..36fd5d59dd --- /dev/null +++ b/website/backend/cleanair/translation.py @@ -0,0 +1,7 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(CleanAirResource) +class CleanAirResourceTranslationOptions(TranslationOptions): + fields = ('resource_title', 'author_title', 'resource_authors',) diff --git a/website/backend/cleanair/views.py b/website/backend/cleanair/views.py index cdbeb9d104..7fc2d4cf39 100644 --- a/website/backend/cleanair/views.py +++ b/website/backend/cleanair/views.py @@ -1,9 +1,19 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import CleanAirResource from .serializers import CleanAirResourceSerializer + class CleanAirResourceViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) queryset = CleanAirResource.objects.all() serializer_class = CleanAirResourceSerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/event/admin.py b/website/backend/event/admin.py index 8c31decefd..bb136a95ec 100644 --- a/website/backend/event/admin.py +++ b/website/backend/event/admin.py @@ -1,54 +1,74 @@ +from .translation import * from django.contrib import admin import nested_admin from .models import Event, Program, Session, PartnerLogo, Inquiry, Resource +from modeltranslation.admin import TranslationAdmin, TranslationStackedInline, TranslationTabularInline + +# Create a new class that inherits from both NestedTabularInline and TranslationTabularInline + + +class TranslationNestedTabularInline(TranslationTabularInline, nested_admin.NestedTabularInline): + pass # Register your models here. -class InquiryInline(nested_admin.NestedStackedInline): - fields = ('inquiry','role','email', 'order') - readonly_fields = ('author', 'updated_by') + + +class InquiryInline(nested_admin.NestedTabularInline): model = Inquiry extra = 0 - -class SessionInline(nested_admin.NestedStackedInline): - fields = ('session_title','session_details','venue','start_time','end_time', 'order') + fields = ('inquiry', 'role', 'email', 'order') readonly_fields = ('author', 'updated_by') + + +class SessionInline(TranslationStackedInline): model = Session extra = 0 - -class ProgramInline(nested_admin.NestedTabularInline): - fields = ('date','program_details','order') + fields = ('session_title', 'session_details', + 'venue', 'start_time', 'end_time', 'order') readonly_fields = ('author', 'updated_by') + + +class ProgramInline(TranslationNestedTabularInline): # Use the new class here model = Program - inlines = (SessionInline,) extra = 0 - -class PartnerLogoInline(nested_admin.NestedTabularInline): - fields=('name','partner_logo', 'order') + fields = ('date', 'program_details', 'order') readonly_fields = ('author', 'updated_by') + inlines = [SessionInline] + + +class PartnerLogoInline(TranslationNestedTabularInline): # And here model = PartnerLogo extra = 0 - -class ResourceInline(nested_admin.NestedTabularInline): - fields=('title','link', 'resource', 'order') + fields = ('name', 'partner_logo', 'order') readonly_fields = ('author', 'updated_by') + + +class ResourceInline(TranslationNestedTabularInline): # And here model = Resource extra = 0 + fields = ('title', 'link', 'resource', 'order') + readonly_fields = ('author', 'updated_by') + @admin.register(Event) -class EventAdmin(nested_admin.NestedModelAdmin): - fields= ('title', 'title_subtext', 'start_date','end_date','start_time','end_time','website_category','registration_link','event_tag','event_image','background_image','location_name','location_link','event_details','order','author', 'updated_by') +class EventAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): + model = Event + fields = ('title', 'title_subtext', 'start_date', 'end_date', 'start_time', 'end_time', 'website_category', 'registration_link', + 'event_tag', 'event_category', 'event_image', 'background_image', 'location_name', 'location_link', 'event_details', 'order', 'author', 'updated_by') readonly_fields = ('id', 'author', 'created', 'updated_by', 'modified') - list_display=('title','start_date', 'event_tag','website_category','author') - search_fields =('title','event_tag','location_name') - list_filter = ('website_category','event_tag','start_date',) - inlines = (ProgramInline, PartnerLogoInline, InquiryInline, ResourceInline,) + list_display = ('title', 'start_date', 'event_tag', + 'website_category', 'author') + search_fields = ('title', 'event_tag', 'location_name') + list_filter = ('website_category', 'event_tag', 'start_date',) + inlines = [ProgramInline, PartnerLogoInline, InquiryInline, ResourceInline] list_per_page = 10 + @admin.register(Resource) -class ResourceAdmin(admin.ModelAdmin): - fields=('event','title','link', 'resource', 'order') - list_display=('title','event','author',) - search_fields =('event','title',) - list_filter = ('author','created') +class ResourceAdmin(TranslationAdmin): + model = Resource + fields = ('event', 'title', 'link', 'resource', 'order') + list_display = ('title', 'event', 'author',) + search_fields = ('event', 'title',) + list_filter = ('author', 'created') list_per_page = 10 - diff --git a/website/backend/event/migrations/0007_event_event_details_en_event_event_details_fr_and_more.py b/website/backend/event/migrations/0007_event_event_details_en_event_event_details_fr_and_more.py new file mode 100644 index 0000000000..7e0d9f83dc --- /dev/null +++ b/website/backend/event/migrations/0007_event_event_details_en_event_event_details_fr_and_more.py @@ -0,0 +1,114 @@ +# Generated by Django 5.0.2 on 2024-02-13 00:44 + +import django_quill.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0006_event_website_category'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='event_details_en', + field=django_quill.fields.QuillField(null=True), + ), + migrations.AddField( + model_name='event', + name='event_details_fr', + field=django_quill.fields.QuillField(null=True), + ), + migrations.AddField( + model_name='event', + name='location_name_en', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='event', + name='location_name_fr', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='event', + name='title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='event', + name='title_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='event', + name='title_subtext_en', + field=models.CharField(max_length=90, null=True), + ), + migrations.AddField( + model_name='event', + name='title_subtext_fr', + field=models.CharField(max_length=90, null=True), + ), + migrations.AddField( + model_name='partnerlogo', + name='name_en', + field=models.CharField(max_length=70, null=True), + ), + migrations.AddField( + model_name='partnerlogo', + name='name_fr', + field=models.CharField(max_length=70, null=True), + ), + migrations.AddField( + model_name='program', + name='program_details_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='program', + name='program_details_fr', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='resource', + name='title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='resource', + name='title_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='session', + name='session_details_en', + field=django_quill.fields.QuillField(null=True), + ), + migrations.AddField( + model_name='session', + name='session_details_fr', + field=django_quill.fields.QuillField(null=True), + ), + migrations.AddField( + model_name='session', + name='session_title_en', + field=models.CharField(max_length=150, null=True), + ), + migrations.AddField( + model_name='session', + name='session_title_fr', + field=models.CharField(max_length=150, null=True), + ), + migrations.AddField( + model_name='session', + name='venue_en', + field=models.CharField(blank=True, max_length=80, null=True), + ), + migrations.AddField( + model_name='session', + name='venue_fr', + field=models.CharField(blank=True, max_length=80, null=True), + ), + ] diff --git a/website/backend/event/migrations/0008_event_event_category.py b/website/backend/event/migrations/0008_event_event_category.py new file mode 100644 index 0000000000..ecb62380cd --- /dev/null +++ b/website/backend/event/migrations/0008_event_event_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-17 16:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0007_event_event_details_en_event_event_details_fr_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='event_category', + field=models.CharField(blank=True, choices=[('all', 'All'), ('webinar', 'Webinar'), ('workshop', 'Workshop'), ('marathon', 'Marathon'), ('conference', 'Conference'), ('summit', 'Summit'), ('commemoration', 'Commemoration'), ('others', 'Others')], default='all', max_length=40, null=True), + ), + ] diff --git a/website/backend/event/migrations/0009_alter_event_event_category.py b/website/backend/event/migrations/0009_alter_event_event_category.py new file mode 100644 index 0000000000..e09e7ca0f8 --- /dev/null +++ b/website/backend/event/migrations/0009_alter_event_event_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-17 16:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0008_event_event_category'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='event_category', + field=models.CharField(blank=True, choices=[('none', 'None'), ('webinar', 'Webinar'), ('workshop', 'Workshop'), ('marathon', 'Marathon'), ('conference', 'Conference'), ('summit', 'Summit'), ('commemoration', 'Commemoration'), ('others', 'Others')], default='none', max_length=40, null=True), + ), + ] diff --git a/website/backend/event/migrations/0010_alter_event_event_category.py b/website/backend/event/migrations/0010_alter_event_event_category.py new file mode 100644 index 0000000000..1f96a5577c --- /dev/null +++ b/website/backend/event/migrations/0010_alter_event_event_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-17 16:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0009_alter_event_event_category'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='event_category', + field=models.CharField(blank=True, choices=[('none', 'None'), ('webinar', 'Webinar'), ('workshop', 'Workshop'), ('marathon', 'Marathon'), ('conference', 'Conference'), ('summit', 'Summit'), ('commemoration', 'Commemoration')], default='none', max_length=40, null=True), + ), + ] diff --git a/website/backend/event/migrations/0011_alter_event_event_category.py b/website/backend/event/migrations/0011_alter_event_event_category.py new file mode 100644 index 0000000000..434b1921ac --- /dev/null +++ b/website/backend/event/migrations/0011_alter_event_event_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2024-02-17 16:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('event', '0010_alter_event_event_category'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='event_category', + field=models.CharField(blank=True, choices=[('none', 'None'), ('webinar', 'Webinar'), ('workshop', 'Workshop'), ('marathon', 'Marathon'), ('conference', 'Conference'), ('summit', 'Summit'), ('commemoration', 'Commemoration'), ('in-person', 'In-person'), ('hybrid', 'Hybrid')], default='none', max_length=40, null=True), + ), + ] diff --git a/website/backend/event/models.py b/website/backend/event/models.py index 7fdf0aa7c7..dc5d9f234b 100644 --- a/website/backend/event/models.py +++ b/website/backend/event/models.py @@ -1,3 +1,4 @@ + from django.db import models from backend.utils.models import BaseModel from author.decorators import with_author @@ -7,6 +8,8 @@ from django.dispatch import receiver # Create your models here. + + @with_author class Event(BaseModel): title = models.CharField(max_length=100) @@ -48,8 +51,26 @@ class EventTag(models.TextChoices): event_tag = models.CharField( max_length=40, default=EventTag.Untagged, choices=EventTag.choices, null=True, blank=True ) - event_image = CloudinaryField("EventImage", overwrite=True, resource_type="image") - background_image = CloudinaryField("BackgroundImage", overwrite=True, resource_type="image") + + class EventCategory(models.TextChoices): + NoneCategory = "none", "None" + Webinar = "webinar", "Webinar" + Workshop = "workshop", "Workshop" + Marathon = "marathon", "Marathon" + Conference = "conference", "Conference" + Summit = "summit", "Summit" + Commemoration = "commemoration", "Commemoration" + InPerson = "in-person", "In-person" + Hybrid = "hybrid", "Hybrid" + + event_category = models.CharField( + max_length=40, default=EventCategory.NoneCategory, choices=EventCategory.choices, null=True, blank=True + ) + + event_image = CloudinaryField( + "EventImage", overwrite=True, resource_type="image") + background_image = CloudinaryField( + "BackgroundImage", overwrite=True, resource_type="image") location_name = models.CharField(max_length=100, null=True, blank=True) location_link = models.URLField(null=True, blank=True) event_details = QuillField() @@ -61,11 +82,13 @@ class Meta: def __str__(self): return self.title + @receiver(pre_save, dispatch_uid="append_short_name", sender=Event) def append_short_name(sender, instance, *args, **kwargs): if not instance.unique_title: instance.unique_title = instance.generate_unique_title() + @with_author class Inquiry(BaseModel): inquiry = models.CharField(max_length=80) @@ -79,12 +102,14 @@ class Inquiry(BaseModel): related_name="inquiry", on_delete=models.deletion.SET_NULL, ) + class Meta: ordering = ['order'] def __str__(self): return f"Inquiry - {self.inquiry}" + @with_author class Program(BaseModel): date = models.DateField() @@ -97,12 +122,14 @@ class Program(BaseModel): related_name="program", on_delete=models.deletion.SET_NULL, ) + class Meta: ordering = ['order'] def __str__(self): return f"Program - {self.session}" + @with_author class Session(BaseModel): start_time = models.TimeField() @@ -113,17 +140,20 @@ class Session(BaseModel): order = models.IntegerField(default=1) program = models.ForeignKey( Program, - null = True, + null=True, blank=True, related_name="session", on_delete=models.deletion.SET_NULL, ) + class Meta: ordering = ['order'] + @with_author class PartnerLogo(BaseModel): - partner_logo = CloudinaryField('PartnerImage', overwrite=True, resource_type="image") + partner_logo = CloudinaryField( + 'PartnerImage', overwrite=True, resource_type="image") name = models.CharField(max_length=70) order = models.IntegerField(default=1) event = models.ForeignKey( @@ -133,12 +163,14 @@ class PartnerLogo(BaseModel): related_name="partner", on_delete=models.deletion.SET_NULL, ) + class Meta: ordering = ['order'] def __str__(self): return f"Partner - {self.name}" + @with_author class Resource(BaseModel): title = models.CharField(max_length=100) @@ -152,6 +184,7 @@ class Resource(BaseModel): related_name="resource", on_delete=models.deletion.SET_NULL, ) + class Meta: ordering = ['order'] diff --git a/website/backend/event/translation.py b/website/backend/event/translation.py new file mode 100644 index 0000000000..4b8e9161e8 --- /dev/null +++ b/website/backend/event/translation.py @@ -0,0 +1,26 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + +# @register(Inquiry) +# class InquiryTranslationOptions(TranslationOptions): +# fields = ('inquiry', 'role',) + +@register(Program) +class ProgramTranslationOptions(TranslationOptions): + fields = ('program_details',) + +@register(Session) +class SessionTranslationOptions(TranslationOptions): + fields = ('venue', 'session_title', 'session_details',) + +@register(PartnerLogo) +class PartnerLogoTranslationOptions(TranslationOptions): + fields = ('name',) + +@register(Resource) +class ResourceTranslationOptions(TranslationOptions): + fields = ('title',) + +@register(Event) +class EventTranslationOptions(TranslationOptions): + fields = ('title', 'title_subtext', 'location_name', 'event_details',) diff --git a/website/backend/event/views.py b/website/backend/event/views.py index a57bf7a76b..e73e671a71 100644 --- a/website/backend/event/views.py +++ b/website/backend/event/views.py @@ -1,3 +1,4 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Event @@ -5,8 +6,16 @@ # Create your views here. + class EventViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) queryset = Event.objects.all() serializer_class = EventSerializer - + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) \ No newline at end of file diff --git a/website/backend/highlights/admin.py b/website/backend/highlights/admin.py index 30bd64942a..969421fdc4 100644 --- a/website/backend/highlights/admin.py +++ b/website/backend/highlights/admin.py @@ -1,9 +1,11 @@ from django.contrib import admin - +from modeltranslation.admin import TranslationAdmin +from .translation import * from .models import Highlight, Tag + @admin.register(Highlight) -class HighlightAdmin(admin.ModelAdmin): +class HighlightAdmin(TranslationAdmin): list_display = ("title", "highlight_tags", "image_preview", "created") list_filter = ("tags", "created") list_per_page = 8 @@ -40,9 +42,10 @@ def image_preview(self, obj): image_preview.allow_tags = True + @admin.register(Tag) -class TagAdmin(admin.ModelAdmin): - list_display = ("id","name", "created" ) +class TagAdmin(TranslationAdmin): + list_display = ("id", "name", "created") list_filter = ("name", ) list_per_page = 10 search_fields = ("name", "id") diff --git a/website/backend/highlights/migrations/0008_highlight_link_title_en_highlight_link_title_fr_and_more.py b/website/backend/highlights/migrations/0008_highlight_link_title_en_highlight_link_title_fr_and_more.py new file mode 100644 index 0000000000..e164a4bfc4 --- /dev/null +++ b/website/backend/highlights/migrations/0008_highlight_link_title_en_highlight_link_title_fr_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.5 on 2024-02-07 16:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('highlights', '0007_alter_highlight_link_title'), + ] + + operations = [ + migrations.AddField( + model_name='highlight', + name='link_title_en', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name='highlight', + name='link_title_fr', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name='highlight', + name='title_en', + field=models.CharField(max_length=110, null=True), + ), + migrations.AddField( + model_name='highlight', + name='title_fr', + field=models.CharField(max_length=110, null=True), + ), + migrations.AddField( + model_name='tag', + name='name_en', + field=models.CharField(max_length=20, null=True), + ), + migrations.AddField( + model_name='tag', + name='name_fr', + field=models.CharField(max_length=20, null=True), + ), + ] diff --git a/website/backend/highlights/translation.py b/website/backend/highlights/translation.py new file mode 100644 index 0000000000..b2199c862a --- /dev/null +++ b/website/backend/highlights/translation.py @@ -0,0 +1,12 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(Tag) +class TagTranslationOptions(TranslationOptions): + fields = ('name',) + + +@register(Highlight) +class HighlightTranslationOptions(TranslationOptions): + fields = ('title', 'link_title',) diff --git a/website/backend/highlights/views.py b/website/backend/highlights/views.py index 08a1fe300d..18d29a4a4c 100644 --- a/website/backend/highlights/views.py +++ b/website/backend/highlights/views.py @@ -1,14 +1,27 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Highlight, Tag from .serializers import HighlightSerializer, TagSerializer -class HighlightViewSet(viewsets.ReadOnlyModelViewSet): + +class BaseViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (AllowAny,) + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) + + +class HighlightViewSet(BaseViewSet): queryset = Highlight.objects.all() serializer_class = HighlightSerializer -class TagViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) + +class TagViewSet(BaseViewSet): queryset = Tag.objects.all() - serializer_class = TagSerializer \ No newline at end of file + serializer_class = TagSerializer diff --git a/website/backend/impact/admin.py b/website/backend/impact/admin.py index 4ff5806067..3b411ff428 100644 --- a/website/backend/impact/admin.py +++ b/website/backend/impact/admin.py @@ -2,8 +2,10 @@ from .models import ImpactNumber # Register your models here. + + @admin.register(ImpactNumber) class ImpactAdmin(admin.ModelAdmin): - fields = ('african_cities', 'champions', 'deployed_monitors', 'data_records', 'research_papers', 'partners',) - list_display = ('modified','updated_by') - + fields = ('african_cities', 'champions', 'deployed_monitors', + 'data_records', 'research_papers', 'partners',) + list_display = ('modified', 'updated_by') diff --git a/website/backend/impact/views.py b/website/backend/impact/views.py index 40531ede0c..d23448b4ce 100644 --- a/website/backend/impact/views.py +++ b/website/backend/impact/views.py @@ -3,6 +3,7 @@ from .models import ImpactNumber from .serializers import ImpactSerializer + class ImpactViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (AllowAny,) queryset = ImpactNumber.objects.all() diff --git a/website/backend/partners/admin.py b/website/backend/partners/admin.py index 09db7166dc..0fb1b1b223 100644 --- a/website/backend/partners/admin.py +++ b/website/backend/partners/admin.py @@ -1,23 +1,28 @@ from django.contrib import admin import nested_admin - from .models import Partner, PartnerDescription +from modeltranslation.admin import TranslationAdmin +from .translation import * + class PartnerDescriptionInline(nested_admin.NestedTabularInline): - fields = ('description', 'author', 'order') + fields = ('description_en', 'description_fr', 'author', 'order') readonly_fields = ('author', ) model = PartnerDescription extra = 0 + @admin.register(Partner) -class PartnerAdmin(nested_admin.NestedModelAdmin): - list_display = ('partner_name','website_category','type', 'logo_preview', 'image_preview') +class PartnerAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): + list_display = ('partner_name', 'website_category', + 'type', 'logo_preview', 'image_preview') readonly_fields = ('author', 'created', 'updated_by', 'modified') - list_filter = ('website_category','type',) + list_filter = ('website_category', 'type',) - fields = ('partner_name','website_category','type','partner_logo','partner_image','partner_link','order','author', 'created', 'updated_by', 'modified') + fields = ('partner_name', 'website_category', 'type', 'partner_logo', 'partner_image', + 'partner_link', 'order', 'author', 'created', 'updated_by', 'modified') list_per_page = 10 - search_fields = ('partner_name','type') + search_fields = ('partner_name', 'type') inlines = (PartnerDescriptionInline,) diff --git a/website/backend/partners/migrations/0010_partner_partner_name_en_partner_partner_name_fr_and_more.py b/website/backend/partners/migrations/0010_partner_partner_name_en_partner_partner_name_fr_and_more.py new file mode 100644 index 0000000000..05686b197b --- /dev/null +++ b/website/backend/partners/migrations/0010_partner_partner_name_en_partner_partner_name_fr_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.5 on 2024-02-07 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0009_alter_partner_type'), + ] + + operations = [ + migrations.AddField( + model_name='partner', + name='partner_name_en', + field=models.CharField(max_length=200, null=True), + ), + migrations.AddField( + model_name='partner', + name='partner_name_fr', + field=models.CharField(max_length=200, null=True), + ), + migrations.AddField( + model_name='partnerdescription', + name='description_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='partnerdescription', + name='description_fr', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/website/backend/partners/translation.py b/website/backend/partners/translation.py new file mode 100644 index 0000000000..feeaf136a3 --- /dev/null +++ b/website/backend/partners/translation.py @@ -0,0 +1,12 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(Partner) +class PartnerTranslationOptions(TranslationOptions): + fields = ('partner_name',) + + +@register(PartnerDescription) +class PartnerDescriptionTranslationOptions(TranslationOptions): + fields = ('description',) diff --git a/website/backend/partners/views.py b/website/backend/partners/views.py index d7dcd9a3b8..c29fcab850 100644 --- a/website/backend/partners/views.py +++ b/website/backend/partners/views.py @@ -1,10 +1,21 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Partner from .serializers import PartnerSerializer # Create your views here. + + class PartnerViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) + permission_classes = [AllowAny] queryset = Partner.objects.all() serializer_class = PartnerSerializer + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/press/admin.py b/website/backend/press/admin.py index 75ce5c7f56..01f4f7c680 100644 --- a/website/backend/press/admin.py +++ b/website/backend/press/admin.py @@ -1,10 +1,14 @@ +from modeltranslation.admin import TranslationAdmin from django.contrib import admin from .models import Press +from .translation import * + @admin.register(Press) -class PressAdmin(admin.ModelAdmin): - list_display = ("article_title", "date_published", "logo_preview", "website_category","created") - list_filter = ("website_category","date_published",) +class PressAdmin(TranslationAdmin): + list_display = ("article_title", "date_published", + "logo_preview", "website_category", "created") + list_filter = ("website_category", "date_published",) list_per_page = 12 search_fields = ("article_title", "date_published") readonly_fields = ( diff --git a/website/backend/press/migrations/0005_press_article_intro_en_press_article_intro_fr_and_more.py b/website/backend/press/migrations/0005_press_article_intro_en_press_article_intro_fr_and_more.py new file mode 100644 index 0000000000..85cd4dc739 --- /dev/null +++ b/website/backend/press/migrations/0005_press_article_intro_en_press_article_intro_fr_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.5 on 2024-02-07 11:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('press', '0004_press_article_tag'), + ] + + operations = [ + migrations.AddField( + model_name='press', + name='article_intro_en', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AddField( + model_name='press', + name='article_intro_fr', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AddField( + model_name='press', + name='article_title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='press', + name='article_title_fr', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/website/backend/press/translation.py b/website/backend/press/translation.py new file mode 100644 index 0000000000..7142d42694 --- /dev/null +++ b/website/backend/press/translation.py @@ -0,0 +1,7 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import Press + + +@register(Press) +class PressTranslationOptions(TranslationOptions): + fields = ('article_title', 'article_intro',) diff --git a/website/backend/press/views.py b/website/backend/press/views.py index 16738f57a2..5ad94b4954 100644 --- a/website/backend/press/views.py +++ b/website/backend/press/views.py @@ -1,9 +1,19 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Press from .serializers import PressSerializer + class PressViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) queryset = Press.objects.all() serializer_class = PressSerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/publications/admin.py b/website/backend/publications/admin.py index 4c1cf55ab9..7fa28f183b 100644 --- a/website/backend/publications/admin.py +++ b/website/backend/publications/admin.py @@ -1,9 +1,12 @@ +from modeltranslation.admin import TranslationAdmin from django.contrib import admin from .models import Publication +from .translation import * + @admin.register(Publication) -class PublicationAdmin(admin.ModelAdmin): - list_display = ("title", "category","authors") +class PublicationAdmin(TranslationAdmin): + list_display = ("title", "category", "authors") list_filter = ("category", "created") list_per_page = 10 search_fields = ("title", "category", "authors") diff --git a/website/backend/publications/migrations/0004_publication_authors_en_publication_authors_fr_and_more.py b/website/backend/publications/migrations/0004_publication_authors_en_publication_authors_fr_and_more.py new file mode 100644 index 0000000000..ab7ccaeccc --- /dev/null +++ b/website/backend/publications/migrations/0004_publication_authors_en_publication_authors_fr_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.5 on 2024-02-07 17:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('publications', '0003_publication_resource_file'), + ] + + operations = [ + migrations.AddField( + model_name='publication', + name='authors_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='publication', + name='authors_fr', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='publication', + name='link_title_en', + field=models.CharField(blank=True, default='Read More', max_length=100, null=True), + ), + migrations.AddField( + model_name='publication', + name='link_title_fr', + field=models.CharField(blank=True, default='Read More', max_length=100, null=True), + ), + migrations.AddField( + model_name='publication', + name='title_en', + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name='publication', + name='title_fr', + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/website/backend/publications/translation.py b/website/backend/publications/translation.py new file mode 100644 index 0000000000..7ec63b6ebd --- /dev/null +++ b/website/backend/publications/translation.py @@ -0,0 +1,7 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(Publication) +class PublicationTranslationOptions(TranslationOptions): + fields = ('title', 'authors', 'link_title',) diff --git a/website/backend/publications/views.py b/website/backend/publications/views.py index aded8f2c46..d1a3d7895f 100644 --- a/website/backend/publications/views.py +++ b/website/backend/publications/views.py @@ -1,10 +1,21 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Publication from .serializers import PublicationSerializer # Create your views here. + + class PublicationViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) queryset = Publication.objects.all() serializer_class = PublicationSerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/backend/settings.py b/website/backend/settings.py index df0eb8dbb7..c2c34a26fd 100644 --- a/website/backend/settings.py +++ b/website/backend/settings.py @@ -14,6 +14,7 @@ from pathlib import Path import cloudinary import dj_database_url +from django.utils.translation import gettext_lazy as _ # from dotenv import load_dotenv @@ -89,6 +90,7 @@ # Third-party apps "cloudinary", "rest_framework", + 'modeltranslation', "drf_yasg", 'django_quill', # My apps @@ -112,6 +114,7 @@ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -168,6 +171,12 @@ # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ +LANGUAGES = [ + ('en', _('English')), + ('fr', _('French')), +] + + LANGUAGE_CODE = "en-us" TIME_ZONE = "Africa/Kampala" @@ -178,6 +187,10 @@ USE_TZ = True +LOCALE_PATHS = [ + os.path.join(BASE_DIR, "locale"), +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ diff --git a/website/backend/team/admin.py b/website/backend/team/admin.py index 569dd5e4e2..d8eb977eaf 100644 --- a/website/backend/team/admin.py +++ b/website/backend/team/admin.py @@ -1,16 +1,21 @@ from django.contrib import admin from .models import Member, MemberBiography import nested_admin +from modeltranslation.admin import TranslationAdmin +from .translation import * # Register your models here. + + class MemberBiographyInline(nested_admin.NestedTabularInline): - fields = ('description', 'author', 'order') + fields = ('description_en', 'description_fr', 'author', 'order') readonly_fields = ('author', ) model = MemberBiography extra = 0 + @admin.register(Member) -class MemberAdmin(nested_admin.NestedModelAdmin): +class MemberAdmin(TranslationAdmin, nested_admin.NestedModelAdmin): list_display = ("name", "title", "image_tag") readonly_fields = ( "id", diff --git a/website/backend/team/migrations/0010_member_about_en_member_about_fr_member_name_en_and_more.py b/website/backend/team/migrations/0010_member_about_en_member_about_fr_member_name_en_and_more.py new file mode 100644 index 0000000000..6302218455 --- /dev/null +++ b/website/backend/team/migrations/0010_member_about_en_member_about_fr_member_name_en_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.5 on 2024-02-07 15:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('team', '0009_memberbiography'), + ] + + operations = [ + migrations.AddField( + model_name='member', + name='about_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='member', + name='about_fr', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='member', + name='name_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='member', + name='name_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='member', + name='title_en', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='member', + name='title_fr', + field=models.CharField(max_length=100, null=True), + ), + migrations.AddField( + model_name='memberbiography', + name='description_en', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='memberbiography', + name='description_fr', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/website/backend/team/translation.py b/website/backend/team/translation.py new file mode 100644 index 0000000000..7c802c5cc9 --- /dev/null +++ b/website/backend/team/translation.py @@ -0,0 +1,12 @@ +from modeltranslation.translator import register, TranslationOptions +from .models import * + + +@register(Member) +class MemberTranslationOptions(TranslationOptions): + fields = ('name', 'title', 'about') + + +@register(MemberBiography) +class MemberBiographyTranslationOptions(TranslationOptions): + fields = ('description',) diff --git a/website/backend/team/views.py b/website/backend/team/views.py index fa001a8af3..f47a7c1b44 100644 --- a/website/backend/team/views.py +++ b/website/backend/team/views.py @@ -1,3 +1,4 @@ +from django.utils import translation from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Member @@ -5,7 +6,14 @@ class TeamViewSet(viewsets.ReadOnlyModelViewSet): - permission_classes = (AllowAny,) - ordering_fields = ('order', 'name') queryset = Member.objects.all() serializer_class = TeamMemberSerializer + permission_classes = [AllowAny] + + def list(self, request, *args, **kwargs): + language = request.session.get('django_language') + if language is None: + language = request.COOKIES.get('django_language') + if language is not None: + translation.activate(language) + return super().list(request, *args, **kwargs) diff --git a/website/frontend/apis/index.js b/website/frontend/apis/index.js index ca696a6adc..0d846056a1 100644 --- a/website/frontend/apis/index.js +++ b/website/frontend/apis/index.js @@ -30,10 +30,20 @@ const apiCall = async (url, method, data = null) => { const response = await axios(config); return response.data; } catch (error) { - console.error(error); + return; } }; +const fetchData = async (url, lang) => { + return await axios + .get(url, { + headers: { + 'Accept-Language': lang + } + }) + .then((response) => response.data); +}; + export const getAirQloudSummaryApi = () => apiCall(AIRQLOUD_SUMMARY, 'get'); export const newsletterSubscriptionApi = (data) => apiCall(NEWSLETTER_SUBSCRIPTION, 'post', data); @@ -44,51 +54,41 @@ export const sendInquiryApi = (data) => apiCall(INQUIRY_URL, 'post', data); export const requestDataAccessApi = (data) => apiCall(EXPLORE_DATA_URL, 'post', data); -// Careers endpoints -export const getAllCareersApi = async () => - await axios.get(CAREERS_URL).then((response) => response.data); +// Careers endpoint +export const getAllCareersApi = async (lang) => fetchData(CAREERS_URL, lang); -export const getAllDepartmentsApi = async () => - await axios.get(DEPARTMENTS_URL).then((response) => response.data); +// Departments endpoint +export const getAllDepartmentsApi = async (lang) => fetchData(DEPARTMENTS_URL, lang); -// Teams endpoints -export const getAllTeamMembersApi = async () => - await axios.get(TEAMS_URL).then((response) => response.data); +// Teams endpoint +export const getAllTeamMembersApi = async (lang) => fetchData(TEAMS_URL, lang); -// Highlights endpoints -export const getAllHighlightsApi = async () => - await axios.get(HIGHLIGHTS_URL).then((response) => response.data); -export const getAllTagsApi = async () => - await axios.get(TAGS_URL).then((response) => response.data); +// Highlights endpoint +export const getAllHighlightsApi = async (lang) => fetchData(HIGHLIGHTS_URL, lang); -// Partners endpoints -export const getAllPartnersApi = async () => - await axios.get(PARTNERS_URL).then((response) => response.data); +// Tags endpoint +export const getAllTagsApi = async (lang) => fetchData(TAGS_URL, lang); -// Board Members endpoints -export const getBoardMembersApi = async () => - await axios.get(BOARD_MEMBERS_URL).then((response) => response.data); +// Partners endpoint +export const getAllPartnersApi = async (lang) => fetchData(PARTNERS_URL, lang); -// Publications endpoints -export const getAllPublicationsApi = async () => - await axios.get(PUBLICATIONS_URL).then((response) => response.data); +// Board Members endpoint +export const getBoardMembersApi = async (lang) => fetchData(BOARD_MEMBERS_URL, lang); -// Press endpoints -export const getAllPressApi = async () => - await axios.get(PRESS_URL).then((response) => response.data); +// Publications endpoint +export const getAllPublicationsApi = async (lang) => fetchData(PUBLICATIONS_URL, lang); + +// Events endpoint +export const getAllPressApi = async (lang) => fetchData(PRESS_URL, lang); // Events endpoint -export const getAllEventsApi = async () => - await axios.get(EVENTS_URL).then((response) => response.data); +export const getAllEventsApi = async (lang) => fetchData(EVENTS_URL, lang); -// African Cities endpoint -export const getAllCitiesApi = async () => - await axios.get(CITIES_URL).then((response) => response.data); +// Cities endpoint +export const getAllCitiesApi = async (lang) => fetchData(CITIES_URL, lang); // Impact Numbers endpoint -export const getAllImpactNumbersApi = async () => - await axios.get(IMPACT_URL).then((response) => response.data); +export const getAllImpactNumbersApi = async () => fetchData(IMPACT_URL, null); -// Clean Air endpoints -export const getAllCleanAirApi = async () => - await axios.get(CLEAN_AIR_URL).then((response) => response.data); +// Clean Air endpoint +export const getAllCleanAirApi = async (lang) => fetchData(CLEAN_AIR_URL, lang); diff --git a/website/frontend/locales/en/translation.json b/website/frontend/locales/en/translation.json index 6d8e570c3d..7087184802 100644 --- a/website/frontend/locales/en/translation.json +++ b/website/frontend/locales/en/translation.json @@ -140,13 +140,13 @@ }, "about": { "section1": { - "title": "The CLEAN-Air
Network", + "title": "The CLEAN-Air
Network", "subText": "<0 className='fact'>An African-led, multi-regional network
bringing together a community of practice for air quality solutions and air quality management across Africa.", "cta": "Join the Network" }, "section2": { "title": "“Championing Liveable urban Environments through African Networks for Air”", - "acronym": "<0 style={{color: '#135DFF'}}>CLEAN-Air, is an acronym coined from", + "acronym": "<0 className='highlight'>CLEAN-Air, is an acronym coined from", "subText": "The network brings together stakeholders and researchers in air quality management to share best practices and knowledge on developing and implementing air quality management solutions in African cities.", "cta": "Are you an organization or individual interested in air quality in Africa? <1 href=\"https://docs.google.com/forms/d/e/1FAIpQLScIPz7VrhfO2ifMI0dPWIQRiGQ9y30LoKUCT-DDyorS7sAKUA/viewform\" target=\"_blank\" rel=\"noopener noreferrer\"><0 style={{color: '#135DFF'}}> Join the network" }, @@ -177,6 +177,11 @@ "subText": "CLEAN-Air network is a nexus for developing tangible and contextual clean air solutions and frameworks for African cities." } } + }, + "highlightSection": { + "tag": "Featured Event", + "tag2": "All", + "cta": "Read More" } }, "membership": { @@ -234,7 +239,11 @@ "1": "All", "2": "Webinar", "3": "Workshop", - "4": "Conference" + "4": "Marathon", + "5": "Conference", + "6": "Summit", + "7": "Commemoration", + "8": "Others" }, "label2": "Location", "options2": { @@ -503,7 +512,8 @@ }, "fourth": { "title": "<0>Simple user interface", - "subText": "Our calibration tool features a user-friendly interface that simplifies the calibration process. Even without technical expertise, you can easily navigate the tool and calibrate the data from air quality monitors." + "subText": "Our calibration tool features a user-friendly interface that simplifies the calibration process. Even without technical expertise, you can easily navigate the tool and calibrate the data from air quality monitors.", + "cta": "Calibration guide" } }, "Analytics": { diff --git a/website/frontend/locales/fr/translation.json b/website/frontend/locales/fr/translation.json index 81788100e3..fe95f410da 100644 --- a/website/frontend/locales/fr/translation.json +++ b/website/frontend/locales/fr/translation.json @@ -140,13 +140,13 @@ }, "about": { "section1": { - "title": "Le réseau
CLEAN-Air", + "title": "Le réseau
CLEAN-Air", "subText": "<0 className='fact'>Un réseau multirégional dirigé par l’Afrique
rassemblant une communauté de pratique pour les solutions de qualité de l’air et la gestion de la qualité de l’air à travers l’Afrique.", "cta": "Rejoignez le réseau" }, "section2": { "title": "“Promouvoir des environnements urbains vivables grâce aux réseaux africains pour l'air”", - "acronym": "<0 style={{color: '#135DFF'}}>CLEAN-Air, est un acronyme forgé à partir de", + "acronym": "<0 className='highlight'>CLEAN-Air, est un acronyme inventé à partir de", "subText": "Le réseau rassemble les parties prenantes et les chercheurs en gestion de la qualité de l'air afin de partager les meilleures pratiques et les connaissances sur le développement et la mise en œuvre de solutions de gestion de la qualité de l'air dans les villes africaines.", "cta": "Êtes-vous une organisation ou un individu intéressé par la qualité de l'air en Afrique? <1 href=\"https://docs.google.com/forms/d/e/1FAIpQLScIPz7VrhfO2ifMI0dPWIQRiGQ9y30LoKUCT-DDyorS7sAKUA/viewform\" target=\"_blank\" rel=\"noopener noreferrer\"><0 style={{color: '#135DFF'}}> Rejoignez le réseau" }, @@ -177,10 +177,18 @@ "subText": "Le réseau CLEAN-Air est un centre de développement de solutions et de cadres concrets et contextuels pour l'air pur pour les villes africaines." } } + }, + "highlightSection": { + "tag": "Événement en vedette", + "tag2": "Tous", + "cta": "Lire la suite" } }, "membership": { - "section1": "Le réseau CLEAN-Air est un réseau multirégional qui renforce les collaborations et les partenariats interrégionaux pour permettre l'apprentissage collectif et le partage des connaissances.

Nous avons une liste croissante de partenaires issus de diverses disciplines à travers le monde, reflétant la multidisciplinarité de la lutte contre la pollution atmosphérique urbaine.", + "section1": { + "subText": "Nous disposons d'une liste croissante de partenaires issus de disciplines diverses et originaires de différentes régions du monde, ce qui reflète la multidisciplinarité de la lutte contre la pollution de l'air en milieu urbain.", + "intro": "

Tirer parti de l'expertise et des ressources uniques des partenaires de mise en œuvre pour renforcer les capacités de gestion de la qualité de l'air en Afrique.

" + }, "implementingPartners": { "intro": "Profitant de l'expertise et des ressources uniques des partenaires d'exécution pour renforcer les capacités de gestion de la qualité de l'air en Afrique.", "subText": "

Les partenaires d'exécution ont un intérêt actif pour le travail sur la qualité de l'air en Afrique, disposent de personnel jouant un rôle clé dans la qualité de l'air, organisent et accueillent des activités d'engagement, participent aux réunions annuelles du réseau CLEAN-Air et peuvent apporter un soutien logistique/ou financier aux partenaires.

" @@ -198,20 +206,63 @@ "subText": "

Le réseau CLEAN-Air est soutenu par des partenaires de développement et des organisations philanthropiques, dont Google.org, WEHUBIT et le Département d'État américain, avec une longue tradition de mise en œuvre d'une surveillance continue de la qualité de l'air dans les villes gourmandes en données à travers les ambassades américaines du monde entier.

Les partenaires de soutien fournissent un soutien logistique et/ou financier aux membres du réseau et peuvent participer à des activités telles que les réunions annuelles du réseau CLEAN-Air.

" }, "individualSection": { - "subText": "

Les personnes activement impliquées dans le travail sur la qualité de l’air en Afrique sont invitées à rejoindre le réseau CLEAN-Air Africa.

", + "subText": "Les personnes activement impliquées dans le travail sur la qualité de l’air en Afrique sont invitées à rejoindre le réseau CLEAN-Air Africa.", "cta": "Enregistrez votre intérêt" } }, "events": { - "section1": "Le réseau CLEAN-Air fournit une plateforme pour faciliter les activités d'engagement, notamment des conférences, des webinaires, des ateliers, des formations et des campagnes communautaires.

Les partenaires auront accès à des ressources partagées sous forme de kits d'outils pour les médias sociaux, de modèles de communiqués de presse, de bannières numériques, etc. qui peuvent être personnalisés pour s'adapter à chaque activité. Les membres auront également accès à un groupe diversifié d'experts qui peuvent être invités à participer à différentes activités d'engagement, en tant qu'intervenants ou co-organisateurs, etc.", - "section2": { + "section1": { + "text": "

Le réseau CLEAN-Air fournit une plateforme facilitant les activités d'engagement, notamment les conférences, les webinaires, les ateliers, les formations et les campagnes communautaires.

Les partenaires auront accès à des ressources partagées sous forme de kits d'outils pour les médias sociaux, de modèles de communiqués de presse, de bannières numériques, etc., qui peuvent être personnalisés pour chaque activité. Les membres auront également accès à un large éventail d'experts qui peuvent être invités à participer à différentes activités d'engagement, en tant que conférenciers ou co-organisateurs, etc.

" + }, + "dropdowns": { + "date": { + "btnLabel": "Date", + "options": { + "1": "Janvier", + "2": "Février", + "3": "Mars", + "4": "Avril", + "5": "Mai", + "6": "Juin", + "7": "Juillet", + "8": "Août", + "9": "Septembre", + "10": "Octobre", + "11": "Novembre", + "12": "Décembre" + } + }, + "filter": { + "btnLabel": "Filtrer", + "label1": "Format", + "options1": { + "1": "Tous", + "2": "Webinaire", + "3": "Atelier", + "4": "Marathon", + "5": "Conférence", + "6": "Sommet", + "7": "Commémoration", + "8": "Autres" + }, + "label2": "Lieu", + "options2": { + "1": "Tous", + "2": "Présentiel", + "3": "Hybride", + "4": "Autres" + } + } + }, + "card": { "subText": "

Augmentez la visibilité de votre événement. Enregistrez votre activité d'engagement et profitez de ressources précieuses et de perspectives de réseautage.

", - "cta": "Enregistrer l'événement" + "btnText": "En savoir plus" }, - "subNavs": { + "sectionTitles": { "upcoming": "Événements à venir", "past": "Événements passés" - } + }, + "noEvents": "Aucun événement disponible" }, "eventsDetails": { "header": { @@ -234,14 +285,15 @@ } }, "publications": { - "title": "RESSOURCE
<2>CENTER", + "title": "Centre de ressources", "navs": { - "toolkits": "boîtes à outils", - "reports": "rapports techniques", - "workshops": "rapports d'ateliers", - "research": "publications de recherche" + "toolkits": "Guides pratiques", + "reports": "Rapports techniques", + "workshops": "Rapports d'ateliers", + "research": "Publications de recherche" }, - "noResources": "Aucune ressource disponible" + "noResources": "Aucune ressource disponible", + "cardBtnText": "En savoir plus" }, "bottomCTA": { "left": { @@ -459,8 +511,9 @@ "cta": "Calibrez vos données" }, "fourth": { - "title": "<0>Interface utilisateur simple .", - "subText": "Notre outil d’étalonnage dispose d’une interface conviviale qui simplifie le processus d’étalonnage. Même sans expertise technique, vous pouvez facilement naviguer dans l’outil et calibrer les données des moniteurs de qualité de l’air." + "title": "<0>Interface utilisateur simple", + "subText": "Notre outil d'étalonnage dispose d'une interface conviviale qui simplifie le processus d'étalonnage. Même sans expertise technique, vous pouvez facilement naviguer dans l'outil et étalonner les données des capteurs de qualité de l'air.", + "cta": "Guide d'étalonnage" } }, "Analytics": { diff --git a/website/frontend/reduxStore/AfricanCities/CitiesSlice.js b/website/frontend/reduxStore/AfricanCities/CitiesSlice.js index 335f550b4c..fe144b54de 100644 --- a/website/frontend/reduxStore/AfricanCities/CitiesSlice.js +++ b/website/frontend/reduxStore/AfricanCities/CitiesSlice.js @@ -1,8 +1,9 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { getAllCitiesApi } from '../../apis'; -export const getAllCities = createAsyncThunk('/getCities', async () => { - const response = await getAllCitiesApi(); +export const getAllCities = createAsyncThunk('/getCities', async (_, thunkAPI) => { + const lang = thunkAPI.getState().eventsNavTab.languageTab; + const response = await getAllCitiesApi(lang); return response; }); diff --git a/website/frontend/reduxStore/Board/operations.js b/website/frontend/reduxStore/Board/operations.js index 0ba049bed5..e305735196 100644 --- a/website/frontend/reduxStore/Board/operations.js +++ b/website/frontend/reduxStore/Board/operations.js @@ -7,9 +7,10 @@ import { UPDATE_BOARD_LOADER_FAILURE } from './actions'; -export const loadBoardData = () => async (dispatch) => { +export const loadBoardData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; dispatch({ type: UPDATE_BOARD_LOADER_SUCCESS, payload: { loading: true } }); - await getBoardMembersApi() + await getBoardMembersApi(lang) .then((resData) => { if (isEmpty(resData || [])) return; dispatch({ diff --git a/website/frontend/reduxStore/Careers/operations.js b/website/frontend/reduxStore/Careers/operations.js index c04caed388..0d039b5bbb 100644 --- a/website/frontend/reduxStore/Careers/operations.js +++ b/website/frontend/reduxStore/Careers/operations.js @@ -6,41 +6,44 @@ import { UPDATE_CAREERS_LOADER_SUCCESS, UPDATE_CAREERS_LOADER_FAILURE, LOAD_DEPARTMENTS_SUCCESS, - LOAD_DEPARTMENTS_FAILURE, + LOAD_DEPARTMENTS_FAILURE } from './actions'; import { transformArray } from '../utils'; -export const loadCareersListingData = () => async (dispatch) => { +export const loadCareersListingData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; dispatch({ type: UPDATE_CAREERS_LOADER_SUCCESS, payload: { loading: true } }); - await getAllCareersApi() + await getAllCareersApi(lang) .then((resData) => { if (isEmpty(resData || [])) return; dispatch({ type: LOAD_CAREERS_SUCCESS, - payload: transformArray(resData, 'unique_title'), + payload: transformArray(resData, 'unique_title') }); }) .catch((err) => { dispatch({ type: LOAD_CAREERS_FAILURE, - payload: err && err.message, + payload: err && err.message }); }); dispatch({ type: UPDATE_CAREERS_LOADER_SUCCESS, payload: { loading: false } }); }; -export const loadCareersDepartmentsData = () => async (dispatch) => { - await getAllDepartmentsApi().then((resData) => { - if (isEmpty(resData || [])) return; - dispatch({ - type: LOAD_DEPARTMENTS_SUCCESS, - payload: resData, - }); - }) +export const loadCareersDepartmentsData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; + await getAllDepartmentsApi(lang) + .then((resData) => { + if (isEmpty(resData || [])) return; + dispatch({ + type: LOAD_DEPARTMENTS_SUCCESS, + payload: resData + }); + }) .catch((err) => { dispatch({ type: LOAD_DEPARTMENTS_FAILURE, - payload: err && err.message, + payload: err && err.message }); }); }; diff --git a/website/frontend/reduxStore/CleanAirNetwork/CleanAir.js b/website/frontend/reduxStore/CleanAirNetwork/CleanAir.js index 3ad29a2f0e..be584a56f5 100644 --- a/website/frontend/reduxStore/CleanAirNetwork/CleanAir.js +++ b/website/frontend/reduxStore/CleanAirNetwork/CleanAir.js @@ -1,12 +1,16 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { getAllCleanAirApi } from '../../apis'; +import i18n from 'i18next'; -export const fetchCleanAirData = createAsyncThunk('tabs/fetchCleanAirData', async () => { - return null; +export const fetchCleanAirData = createAsyncThunk('tabs/fetchCleanAirData', async (_, thunkAPI) => { + const lang = thunkAPI.getState().eventsNavTab.languageTab; + const response = await getAllCleanAirApi(lang); + return response; }); const initialState = { activeTab: 0, - activeResource: 'toolkits', + activeResource: i18n.t('cleanAirSite.publications.navs.toolkits'), airData: [], loading: false, error: null diff --git a/website/frontend/reduxStore/Events/EventSlice.js b/website/frontend/reduxStore/Events/EventSlice.js index 5cabf1a4c0..138d852a17 100644 --- a/website/frontend/reduxStore/Events/EventSlice.js +++ b/website/frontend/reduxStore/Events/EventSlice.js @@ -2,9 +2,10 @@ import { createSlice } from '@reduxjs/toolkit'; import { getAllEventsApi } from '../../apis'; import { isEmpty } from 'underscore'; -export const getAllEvents = () => async (dispatch) => { +export const getAllEvents = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; dispatch(isLoading(true)); - await getAllEventsApi() + await getAllEventsApi(lang) .then((res) => { if (isEmpty(res || [])) return; dispatch(getEventsReducer(res)); @@ -30,7 +31,7 @@ export const eventSlice = createSlice({ isLoading: (state, action) => { state.loading = action.payload; } - }, + } }); export const { getEventsReducer, getEventsFailure, isLoading } = eventSlice.actions; diff --git a/website/frontend/reduxStore/EventsNav/NavigationSlice.js b/website/frontend/reduxStore/EventsNav/NavigationSlice.js index 9bdc86a14f..31457459ea 100644 --- a/website/frontend/reduxStore/EventsNav/NavigationSlice.js +++ b/website/frontend/reduxStore/EventsNav/NavigationSlice.js @@ -1,17 +1,18 @@ import { createSlice } from '@reduxjs/toolkit'; +import i18n from 'i18next'; export const navigationSlice = createSlice({ name: 'navBarTab', initialState: { - tab: 'upcoming events', - languageTab: 'English' + tab: i18n.t('about.events.navTabs.upcoming'), + languageTab: 'en' }, reducers: { setNavTab: (state, action) => { state.tab = action.payload; }, - setLanguageTab: (state, action)=>{ - state.languageTab = action.payload + setLanguageTab: (state, action) => { + state.languageTab = action.payload; } } }); diff --git a/website/frontend/reduxStore/Highlights/operations.js b/website/frontend/reduxStore/Highlights/operations.js index 4756cc83ef..440dbdb606 100644 --- a/website/frontend/reduxStore/Highlights/operations.js +++ b/website/frontend/reduxStore/Highlights/operations.js @@ -1,37 +1,44 @@ import { isEmpty } from 'underscore'; import { getAllHighlightsApi, getAllTagsApi } from '../../apis'; -import { LOAD_HIGHLIGHTS_FAILURE, LOAD_HIGHLIGHTS_SUCCESS, LOAD_TAGS_FAILURE, LOAD_TAGS_SUCCESS } from './actions'; +import { + LOAD_HIGHLIGHTS_FAILURE, + LOAD_HIGHLIGHTS_SUCCESS, + LOAD_TAGS_FAILURE, + LOAD_TAGS_SUCCESS +} from './actions'; -export const loadHighlightsData = () => async (dispatch) => { - await getAllHighlightsApi() - .then((resData) => { - if (isEmpty(resData || [])) return; - dispatch({ - type: LOAD_HIGHLIGHTS_SUCCESS, - payload: resData, - }); - }) - .catch((err) => { - dispatch({ - type: LOAD_HIGHLIGHTS_FAILURE, - payload: err && err.message, - }); - }); +export const loadHighlightsData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; + await getAllHighlightsApi(lang) + .then((resData) => { + if (isEmpty(resData || [])) return; + dispatch({ + type: LOAD_HIGHLIGHTS_SUCCESS, + payload: resData + }); + }) + .catch((err) => { + dispatch({ + type: LOAD_HIGHLIGHTS_FAILURE, + payload: err && err.message + }); + }); }; -export const loadTagsData = () => async (dispatch) => { - await getAllTagsApi() - .then((resData) => { - if (isEmpty(resData || [])) return; - dispatch({ - type: LOAD_TAGS_SUCCESS, - payload: resData, - }); - }) - .catch((err) => { - dispatch({ - type: LOAD_TAGS_FAILURE, - payload: err && err.message, - }); - }); -}; \ No newline at end of file +export const loadTagsData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; + await getAllTagsApi(lang) + .then((resData) => { + if (isEmpty(resData || [])) return; + dispatch({ + type: LOAD_TAGS_SUCCESS, + payload: resData + }); + }) + .catch((err) => { + dispatch({ + type: LOAD_TAGS_FAILURE, + payload: err && err.message + }); + }); +}; diff --git a/website/frontend/reduxStore/Partners/operations.js b/website/frontend/reduxStore/Partners/operations.js index da05df27eb..ef252891c1 100644 --- a/website/frontend/reduxStore/Partners/operations.js +++ b/website/frontend/reduxStore/Partners/operations.js @@ -2,19 +2,20 @@ import { isEmpty } from 'underscore'; import { getAllPartnersApi } from '../../apis'; import { LOAD_PARTNERS_FAILURE, LOAD_PARTNERS_SUCCESS } from './actions'; -export const loadPartnersData = () => async (dispatch) => { - await getAllPartnersApi() - .then((resData) => { - if (isEmpty(resData || [])) return; - dispatch({ - type: LOAD_PARTNERS_SUCCESS, - payload: resData, - }); - }) - .catch((err) => { - dispatch({ - type: LOAD_PARTNERS_FAILURE, - payload: err && err.message, - }); - }); +export const loadPartnersData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; + await getAllPartnersApi(lang) + .then((resData) => { + if (isEmpty(resData || [])) return; + dispatch({ + type: LOAD_PARTNERS_SUCCESS, + payload: resData + }); + }) + .catch((err) => { + dispatch({ + type: LOAD_PARTNERS_FAILURE, + payload: err && err.message + }); + }); }; diff --git a/website/frontend/reduxStore/Press/PressSlice.js b/website/frontend/reduxStore/Press/PressSlice.js index 73131e5445..1f8668020a 100644 --- a/website/frontend/reduxStore/Press/PressSlice.js +++ b/website/frontend/reduxStore/Press/PressSlice.js @@ -7,8 +7,9 @@ const initialState = { error: null }; -export const loadPressData = createAsyncThunk('get/press', async () => { - const response = await getAllPressApi(); +export const loadPressData = createAsyncThunk('get/press', async (_, thunkAPI) => { + const lang = thunkAPI.getState().eventsNavTab.languageTab; + const response = await getAllPressApi(lang); return response; }); @@ -33,7 +34,7 @@ const pressSlice = createSlice({ state.error = action.error.message; state.loading = false; }); - }, + } }); export const { getPressData } = pressSlice.actions; diff --git a/website/frontend/reduxStore/Publications/operations.js b/website/frontend/reduxStore/Publications/operations.js index bf32b90e5f..728f595aba 100644 --- a/website/frontend/reduxStore/Publications/operations.js +++ b/website/frontend/reduxStore/Publications/operations.js @@ -1,9 +1,10 @@ import { isEmpty } from 'underscore'; import { getAllPublicationsApi } from '../../apis'; import { LOAD_PUBLICATIONS_FAILURE, LOAD_PUBLICATIONS_SUCCESS } from './actions'; - -export const loadPublicationsData = () => async (dispatch) => { - await getAllPublicationsApi() + +export const loadPublicationsData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; + await getAllPublicationsApi(lang) .then((resData) => { if (isEmpty(resData || [])) return; dispatch({ diff --git a/website/frontend/reduxStore/Team/operations.js b/website/frontend/reduxStore/Team/operations.js index 62aab04cb9..184c4e0f8a 100644 --- a/website/frontend/reduxStore/Team/operations.js +++ b/website/frontend/reduxStore/Team/operations.js @@ -4,23 +4,24 @@ import { LOAD_TEAM_SUCCESS, LOAD_TEAM_FAILURE, UPDATE_TEAM_LOADER_SUCCESS, - UPDATE_TEAM_LOADER_FAILURE, + UPDATE_TEAM_LOADER_FAILURE } from './actions'; -export const loadTeamData = () => async (dispatch) => { +export const loadTeamData = () => async (dispatch, getState) => { + const lang = getState().eventsNavTab.languageTab; dispatch({ type: UPDATE_TEAM_LOADER_SUCCESS, payload: { loading: true } }); - await getAllTeamMembersApi() + await getAllTeamMembersApi(lang) .then((resData) => { if (isEmpty(resData || [])) return; dispatch({ type: LOAD_TEAM_SUCCESS, - payload: resData, + payload: resData }); }) .catch((err) => { dispatch({ type: LOAD_TEAM_FAILURE, - payload: err && err.message, + payload: err && err.message }); }); diff --git a/website/frontend/src/components/CleanAir/Hightlights/MainHighlight.js b/website/frontend/src/components/CleanAir/Hightlights/MainHighlight.js index a933a5b435..1a03eb4b46 100644 --- a/website/frontend/src/components/CleanAir/Hightlights/MainHighlight.js +++ b/website/frontend/src/components/CleanAir/Hightlights/MainHighlight.js @@ -6,8 +6,10 @@ import { useNavigate } from 'react-router-dom'; import { AccessTimeOutlined, CalendarMonth } from '@mui/icons-material'; import { format } from 'date-fns'; import Spinner from '../loaders/Spinner'; +import { useTranslation, Trans } from 'react-i18next'; const Highlight = () => { + const { t } = useTranslation(); const navigate = useNavigate(); const dispatch = useDispatch(); const eventsData = useSelector((state) => state.eventsData.events); @@ -51,7 +53,7 @@ const Highlight = () => {
-

Featured Event

+

{t('cleanAirSite.about.highlightSection.tag')}

@@ -84,7 +86,7 @@ const Highlight = () => { {featuredEvent.end_time.slice(0, -3)} ) : ( - All Day + {t('cleanAirSite.about.highlightSection.tag2')} )}
@@ -97,7 +99,7 @@ const Highlight = () => { onClick={() => navigate(`/clean-air/event-details/${featuredEvent.unique_title}/`) }> - Read More {' -->'} + {t('cleanAirSite.about.highlightSection.cta')} {' -->'}
diff --git a/website/frontend/src/components/CleanAir/Sections/RegisterSection.js b/website/frontend/src/components/CleanAir/Sections/RegisterSection.js index a3e74af7a0..27aaf9ee84 100644 --- a/website/frontend/src/components/CleanAir/Sections/RegisterSection.js +++ b/website/frontend/src/components/CleanAir/Sections/RegisterSection.js @@ -14,9 +14,7 @@ const RegisterSection = ({ link }) => {
Team
-

- -

+

{t('cleanAirSite.membership.individualSection.subText')}

diff --git a/website/frontend/src/components/CleanAir/Sections/SingleSection.js b/website/frontend/src/components/CleanAir/Sections/SingleSection.js index 168667b69b..fbea2e9656 100644 --- a/website/frontend/src/components/CleanAir/Sections/SingleSection.js +++ b/website/frontend/src/components/CleanAir/Sections/SingleSection.js @@ -15,7 +15,7 @@ const SingleSection = ({ className={`single-section ${removeTopMargin ? 'no-top' : ''}`} style={{ backgroundColor: bgColor, padding }}>
- {content} +
{content}
{btnText && link && (
@@ -29,7 +29,7 @@ SingleSection.propTypes = { bgColor: PropTypes.string, btnText: PropTypes.string, padding: PropTypes.string, - content: PropTypes.string.isRequired, + content: PropTypes.any.isRequired, link: PropTypes.string, btnStyle: PropTypes.object, removeTopMargin: PropTypes.bool diff --git a/website/frontend/src/components/CleanAir/Sections/Split_Text_section.js b/website/frontend/src/components/CleanAir/Sections/Split_Text_section.js index 56d1675bb1..9054c24272 100644 --- a/website/frontend/src/components/CleanAir/Sections/Split_Text_section.js +++ b/website/frontend/src/components/CleanAir/Sections/Split_Text_section.js @@ -12,6 +12,7 @@ const Split_Text_section = ({ bgColor, content, title, lists, loading }) => { const navigate = useNavigate(); const onLogoClick = (data) => (event) => { + console.log(data); event.preventDefault(); if (data.descriptions.length > 0) { navigate(`/partners/${data.unique_title}`); diff --git a/website/frontend/src/components/CleanAir/cards/CardComponent.js b/website/frontend/src/components/CleanAir/cards/CardComponent.js index 8558c0f812..4490377010 100644 --- a/website/frontend/src/components/CleanAir/cards/CardComponent.js +++ b/website/frontend/src/components/CleanAir/cards/CardComponent.js @@ -1,7 +1,9 @@ import React from 'react'; import { FileDownloadOutlined } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; const CardComponent = ({ title, authors, link, linkTitle, downloadLink }) => { + const { t } = useTranslation(); return (
@@ -11,7 +13,7 @@ const CardComponent = ({ title, authors, link, linkTitle, downloadLink }) => {
{link !== null ? ( - {linkTitle || 'Read More'} + {linkTitle || t('cleanAirSite.publications.card.readmore')} ) : ( @@ -19,7 +21,8 @@ const CardComponent = ({ title, authors, link, linkTitle, downloadLink }) => { {downloadLink !== null ? ( - Download {' '} + {t('cleanAirSite.publications.card.download')} + {' '} ) : ( diff --git a/website/frontend/src/components/GetInvolvedModal.js b/website/frontend/src/components/GetInvolvedModal.js index df227bc259..bcd66d03ba 100644 --- a/website/frontend/src/components/GetInvolvedModal.js +++ b/website/frontend/src/components/GetInvolvedModal.js @@ -24,7 +24,11 @@ const categoryMapper = { developer: 'developers' }; -const BoxWrapper = ({ children }) =>
{children}
; +const BoxWrapper = React.forwardRef(({ children }, ref) => ( +
+ {children} +
+)); const GetInvolvedTab = ({ icon, category, infoText }) => { const dispatch = useDispatch(); @@ -244,7 +248,7 @@ const GetInvolvedModal = () => { return ( - + {!getInvolvedData.complete && } {getInvolvedData.complete && } diff --git a/website/frontend/src/components/HighlightsSection/index.js b/website/frontend/src/components/HighlightsSection/index.js index f9eda80f9b..c19669c306 100644 --- a/website/frontend/src/components/HighlightsSection/index.js +++ b/website/frontend/src/components/HighlightsSection/index.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import Pagination from './pagination'; import Post from './post'; import { useHighlightsData } from '../../../reduxStore/Highlights/selectors'; @@ -9,11 +9,12 @@ import ImageLoader from '../LoadSpinner/ImageLoader'; const HighlightsSection = () => { const dispatch = useDispatch(); const highlightsData = useHighlightsData(); + const language = useSelector((state) => state.eventsNavTab.languageTab); useEffect(() => { dispatch(loadTagsData()); dispatch(loadHighlightsData()); - }, [highlightsData.length]); + }, [highlightsData.length, language]); const highlights = highlightsData.slice(0, 3); @@ -76,7 +77,7 @@ const HighlightsSection = () => {
) : ( - + )} ); diff --git a/website/frontend/src/components/LanguageSwitcher.js b/website/frontend/src/components/LanguageSwitcher.js index 453b6545ff..e4996f8c06 100644 --- a/website/frontend/src/components/LanguageSwitcher.js +++ b/website/frontend/src/components/LanguageSwitcher.js @@ -10,13 +10,13 @@ const LanguageSwitcher = () => { const dispatch = useDispatch(); const { t, i18n } = useTranslation(); const languageTab = useSelector((state) => state.eventsNavTab.languageTab); - const [language, setLanguage] = useState(localStorage.getItem('language') || languageTab); + const [language, setLanguage] = useState(languageTab || localStorage.getItem('language')); const [open, setOpen] = useState(false); const ref = useRef(null); const lngs = { - en: { nativeName: 'English' } - // fr: { nativeName: 'French' } + en: { nativeName: 'English' }, + fr: { nativeName: 'French' } }; // Ensure language is a key in lngs @@ -52,7 +52,7 @@ const LanguageSwitcher = () => { i18n.changeLanguage(localLanguage); dispatch(setLanguageTab(localLanguage)); } - }, []); + }, [languageTab, i18n, dispatch]); return (
diff --git a/website/frontend/src/components/nav/TopBar.js b/website/frontend/src/components/nav/TopBar.js index 81380c75e4..e4e95db1a8 100644 --- a/website/frontend/src/components/nav/TopBar.js +++ b/website/frontend/src/components/nav/TopBar.js @@ -158,9 +158,6 @@ const TopBar = () => {

{t('navbar.about.subnav.careers')}

-
-

{t('navbar.getInvolved')}

-

{t('navbar.about.subnav.contact')}

diff --git a/website/frontend/src/pages/AboutUsPage.js b/website/frontend/src/pages/AboutUsPage.js index 4260c4088c..d828fd28cb 100644 --- a/website/frontend/src/pages/AboutUsPage.js +++ b/website/frontend/src/pages/AboutUsPage.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Link } from 'react-scroll'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { isEmpty } from 'underscore'; import { useInitScrollTop } from 'utilities/customHooks'; import { useTeamData } from 'reduxStore/Team/selectors'; @@ -31,6 +31,7 @@ const AboutUsPage = () => { const navigate = useNavigate(); const showModal = () => dispatch(showGetInvolvedModal(true)); const partnersData = allPartnersData.filter((partner) => partner.website_category === 'airqo'); + const language = useSelector((state) => state.eventsNavTab.languageTab); const [togglePartnersDisplay, setTogglePartnersDisplay] = useState(false); @@ -55,10 +56,11 @@ const AboutUsPage = () => { }; useEffect(() => { - if (isEmpty(teamData)) dispatch(loadTeamData()); - if (isEmpty(partnersData)) dispatch(loadPartnersData()); - if (isEmpty(boardData)) dispatch(loadBoardData()); - }, [partnersData, teamData]); + dispatch(loadTeamData()); + dispatch(loadPartnersData()); + dispatch(loadBoardData()); + }, [language]); + return (
diff --git a/website/frontend/src/pages/CareerPage.js b/website/frontend/src/pages/CareerPage.js index 43ed9cfd9b..859586a0ef 100644 --- a/website/frontend/src/pages/CareerPage.js +++ b/website/frontend/src/pages/CareerPage.js @@ -1,11 +1,10 @@ import React, { useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { useInitScrollTop } from 'utilities/customHooks'; import Page from './Page'; import { loadCareersListingData, loadCareersDepartmentsData } from 'reduxStore/Careers/operations'; import { useCareerListingData, useCareerDepartmentsData } from 'reduxStore/Careers/selectors'; -import { isEmpty } from 'underscore'; import { groupBy } from 'underscore'; import SectionLoader from '../components/LoadSpinner/SectionLoader'; import SEO from 'utilities/seo'; @@ -55,7 +54,7 @@ const CareerPage = () => { const careerListing = useCareerListingData(); const departments = useCareerDepartmentsData(); const [loading, setLoading] = useState(false); - + const language = useSelector((state) => state.eventsNavTab.languageTab); const groupedListing = groupBy(Object.values(careerListing), (v) => v['department']['name']); const [groupedKeys, setGroupedKeys] = useState(Object.keys(groupedListing)); @@ -63,9 +62,11 @@ const CareerPage = () => { const filterGroups = (value) => { setSelectedTag(value); - const allKeys = Object.keys(groupedListing); - if (value === 'all') return setGroupedKeys(allKeys); - return setGroupedKeys(allKeys.filter((v) => v === value)); + if (groupedListing) { + const allKeys = Object.keys(groupedListing); + if (value === 'all') return setGroupedKeys(allKeys); + return setGroupedKeys(allKeys.filter((v) => v === value)); + } }; const onTagClick = (value) => (event) => { @@ -79,16 +80,17 @@ const CareerPage = () => { }; useEffect(() => { - setLoading(true); - if (isEmpty(careerListing)) dispatch(loadCareersListingData()); - if (isEmpty(departments)) dispatch(loadCareersDepartmentsData()); - setLoading(false); - }, []); + const fetchData = async () => { + setLoading(true); + await dispatch(loadCareersListingData()); + await dispatch(loadCareersDepartmentsData()); + setLoading(false); + }; + fetchData(); + }, [language]); useEffect(() => { - setLoading(true); setGroupedKeys(Object.keys(groupedListing)); - setLoading(false); }, [careerListing]); return ( @@ -108,7 +110,15 @@ const CareerPage = () => {
{loading ? ( - +
+ +
) : (
diff --git a/website/frontend/src/pages/CleanAir/CleanAirAbout.js b/website/frontend/src/pages/CleanAir/CleanAirAbout.js index 450d3a9272..23cce7178e 100644 --- a/website/frontend/src/pages/CleanAir/CleanAirAbout.js +++ b/website/frontend/src/pages/CleanAir/CleanAirAbout.js @@ -2,7 +2,6 @@ import React from 'react'; import { useInitScrollTop } from 'utilities/customHooks'; import { SplitSection, SingleSection, MainHighlight, ButtonCTA } from 'components/CleanAir'; import Section1 from 'assets/img/cleanAir/section1.png'; -import Section2 from 'assets/img/cleanAir/section2.png'; import Section3 from 'assets/img/cleanAir/section3.png'; import Section4 from 'assets/img/cleanAir/section4.png'; import Placeholder1 from 'assets/img/cleanAir/goal1.png'; @@ -50,7 +49,7 @@ const CleanAirAbout = () => {

- The CLEAN-Air
Network + The CLEAN-Air
Network

@@ -79,13 +78,7 @@ const CleanAirAbout = () => {

- - CLEAN-Air - - , is an acronym coined from + CLEAN-Air, is an acronym coined from

{t('cleanAirSite.about.section2.title')}

@@ -104,9 +97,6 @@ const CleanAirAbout = () => {
-
- Descriptive text -
@@ -160,9 +150,9 @@ const CleanAirAbout = () => { alignItems: 'flex-start' }}> -

Goals

+

{t('cleanAirSite.about.section5.pillTitle')}

-

CLEAN Air Goals

+

{t('cleanAirSite.about.section5.title')}

diff --git a/website/frontend/src/pages/CleanAir/CleanAirEvents.js b/website/frontend/src/pages/CleanAir/CleanAirEvents.js index 161c952b97..73e943da70 100644 --- a/website/frontend/src/pages/CleanAir/CleanAirEvents.js +++ b/website/frontend/src/pages/CleanAir/CleanAirEvents.js @@ -1,6 +1,5 @@ import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import SEO from 'utilities/seo'; -import { isEmpty } from 'underscore'; import { useInitScrollTop } from 'utilities/customHooks'; import { getAllEvents } from '../../../reduxStore/Events/EventSlice'; import { useDispatch, useSelector } from 'react-redux'; @@ -29,6 +28,7 @@ const CleanAirEvents = () => { const { t } = useTranslation(); const dispatch = useDispatch(); const allEventsData = useSelector((state) => state.eventsData.events); + const language = useSelector((state) => state.eventsNavTab.languageTab); const navigate = useNavigate(); // State @@ -46,43 +46,44 @@ const CleanAirEvents = () => { // Derived data const eventsApiData = useMemo(() => { - const filteredEvents = allEventsData.filter((event) => event.website_category === 'cleanair'); + return allEventsData.filter((event) => { + if (event.website_category !== 'cleanair') { + return false; + } - // If a month is selected, filter the events based on the selected month - if (selectedMonth) { - return filteredEvents.filter((event) => { + if (selectedMonth) { const eventDate = new Date(event.start_date); - console.log(eventDate.getMonth(), selectedMonth); - return eventDate.getMonth() === selectedMonth; - }); - } + if (eventDate.getMonth() !== selectedMonth) { + return false; + } + } - // TODO: Add filter for format to model - // If a filter is selected, filter the events based on the selected filter - // if (filter) { - // return filteredEvents.filter((event) => event.format === filter); - // } + if (filter && filter !== 'all' && filter !== 'others' && event.event_category !== filter) { + return false; + } - return filteredEvents; - }, [allEventsData, selectedMonth]); + return true; + }); + }, [allEventsData, selectedMonth, filter]); const upcomingEvents = useMemo(() => getUpcomingEvents(eventsApiData), [eventsApiData]); const pastEvents = useMemo(() => getPastEvents(eventsApiData), [eventsApiData]); // Effects useEffect(() => { - if (isEmpty(eventsApiData)) { + const fetchAllEvents = async () => { setLoading(true); - dispatch(getAllEvents()) - .then(() => { - setLoading(false); - }) - .catch((error) => { - console.error(error); - setLoading(false); - }); - } - }, []); + try { + await dispatch(getAllEvents()); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + fetchAllEvents(); + }, [language, dispatch]); useEffect(() => { const handleClickOutside = (event) => { @@ -162,14 +163,18 @@ const CleanAirEvents = () => { ]; const filterOption1 = [ - { label: t('cleanAirSite.events.dropdowns.filter.options1.1'), value: 'webinar' }, - { label: t('cleanAirSite.events.dropdowns.filter.options1.2'), value: 'workshop' }, - { label: t('cleanAirSite.events.dropdowns.filter.options1.3'), value: 'conference' }, - { label: t('cleanAirSite.events.dropdowns.filter.options1.4'), value: 'others' } + { label: t('cleanAirSite.events.dropdowns.filter.options1.1'), value: 'all' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.2'), value: 'webinar' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.3'), value: 'workshop' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.4'), value: 'marathon' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.5'), value: 'conference' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.6'), value: 'summit' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.7'), value: 'commemoration' }, + { label: t('cleanAirSite.events.dropdowns.filter.options1.8'), value: 'others' } ]; const filterOption2 = [ - { label: t('cleanAirSite.events.dropdowns.filter.options2.1'), value: 'online' }, + { label: t('cleanAirSite.events.dropdowns.filter.options2.1'), value: 'all' }, { label: t('cleanAirSite.events.dropdowns.filter.options2.2'), value: 'in-person' }, { label: t('cleanAirSite.events.dropdowns.filter.options2.3'), value: 'hybrid' }, { label: t('cleanAirSite.events.dropdowns.filter.options2.4'), value: 'others' } diff --git a/website/frontend/src/pages/CleanAir/CleanAirPartners.js b/website/frontend/src/pages/CleanAir/CleanAirPartners.js index 5067f35c22..576532c1c1 100644 --- a/website/frontend/src/pages/CleanAir/CleanAirPartners.js +++ b/website/frontend/src/pages/CleanAir/CleanAirPartners.js @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { isEmpty } from 'underscore'; import SEO from 'utilities/seo'; import { useInitScrollTop } from 'utilities/customHooks'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { SplitTextSection, RegisterSection, IntroSection } from 'components/CleanAir'; import { usePartnersData } from '../../../reduxStore/Partners/selectors'; import { loadPartnersData } from '../../../reduxStore/Partners/operations'; @@ -17,12 +17,11 @@ const CleanAirPartners = () => { const partnersData = usePartnersData(); const { width } = useWindowSize(); const isLoading = isEmpty(partnersData); + const language = useSelector((state) => state.eventsNavTab.languageTab); useEffect(() => { - if (isEmpty(partnersData)) { - dispatch(loadPartnersData()); - } - }, []); + dispatch(loadPartnersData()); + }, [language]); const cleanAirPartners = partnersData.filter( (partner) => partner.website_category === 'cleanair' diff --git a/website/frontend/src/pages/CleanAir/CleanAirPublications.js b/website/frontend/src/pages/CleanAir/CleanAirPublications.js index a408a5f601..18f5eaa572 100644 --- a/website/frontend/src/pages/CleanAir/CleanAirPublications.js +++ b/website/frontend/src/pages/CleanAir/CleanAirPublications.js @@ -1,11 +1,10 @@ import React, { useEffect, useState, useRef } from 'react'; import SEO from 'utilities/seo'; import { useInitScrollTop } from 'utilities/customHooks'; -import { isEmpty } from 'underscore'; import { useDispatch, useSelector } from 'react-redux'; import { setActiveResource } from 'reduxStore/CleanAirNetwork/CleanAir'; import { ReportComponent } from 'components/CleanAir'; -import { getAllCleanAirApi } from 'apis/index.js'; +import { getAllCleanAirApi } from 'apis/index'; import { useTranslation } from 'react-i18next'; import { RegisterSection, IntroSection, RotatingLoopIcon } from 'components/CleanAir'; import ResourceImage from 'assets/img/cleanAir/resource.png'; @@ -15,6 +14,8 @@ import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArro import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +const ITEMS_PER_PAGE = 3; + const CleanAirPublications = () => { useInitScrollTop(); const { t } = useTranslation(); @@ -23,32 +24,33 @@ const CleanAirPublications = () => { const [openfilter, setFilter] = useState(false); const [cleanAirResources, setCleanAirResources] = useState([]); const activeResource = useSelector((state) => state.cleanAirData.activeResource); + const language = useSelector((state) => state.eventsNavTab.languageTab); + const [currentPage, setCurrentPage] = useState(1); + const [loading, setLoading] = useState(false); + const resources = [ t('cleanAirSite.publications.navs.toolkits'), t('cleanAirSite.publications.navs.reports'), t('cleanAirSite.publications.navs.workshops'), t('cleanAirSite.publications.navs.research') ]; - const [loading, setLoading] = useState(false); - - useEffect(() => { - if (isEmpty(activeResource)) { - dispatch(setActiveResource(t('cleanAirSite.publications.navs.toolkits'))); - } - }, [activeResource]); useEffect(() => { - setLoading(true); - getAllCleanAirApi() - .then((response) => { + const fetchCleanAirApi = async () => { + try { + setLoading(true); + const response = await getAllCleanAirApi(language); setCleanAirResources(response); + dispatch(setActiveResource(t('cleanAirSite.publications.navs.toolkits'))); + } catch (error) { + console.error('Failed to fetch clean air API:', error); + } finally { setLoading(false); - }) - .catch((error) => { - console.log(error); - setLoading(false); - }); - }, []); + } + }; + + fetchCleanAirApi(); + }, [language, dispatch]); const toolkitData = cleanAirResources.filter( (resource) => resource.resource_category === 'toolkit' @@ -63,10 +65,25 @@ const CleanAirPublications = () => { (resource) => resource.resource_category === 'research_publication' ); - const ITEMS_PER_PAGE = 3; + useEffect(() => { + const handleClickOutside = (event) => { + if (filterRef.current && !filterRef.current.contains(event.target)) { + setFilter(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [filterRef]); + + const handleFilterSelect = (filter) => { + dispatch(setActiveResource(filter)); + setFilter(false); + }; const renderData = (data, showSecondAuthor) => { - const [currentPage, setCurrentPage] = useState(1); const totalPages = Math.ceil(data.length / ITEMS_PER_PAGE); const handleClickNext = () => { @@ -137,24 +154,6 @@ const CleanAirPublications = () => { ); }; - useEffect(() => { - const handleClickOutside = (event) => { - if (filterRef.current && !filterRef.current.contains(event.target)) { - setFilter(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [filterRef]); - - const handleFilterSelect = (filter) => { - dispatch(setActiveResource(filter)); - setFilter(false); - }; - return (
{ - const [selectedTab, setSelectedTab] = useState(navTabs[0]); - const selectedNavTab = useSelector((state) => state.eventsNavTab.tab); - const onClickTabItem = (tab) => setSelectedTab(tab); + const activeTab = useSelector((state) => state.eventsNavTab.tab); const dispatch = useDispatch(); useEffect(() => { - setSelectedTab(selectedNavTab); + dispatch(setNavTab(navTabs[0])); }, []); + const onClickTabItem = (tab) => { + dispatch(setNavTab(tab)); + }; + return ( - <> -
- {navTabs.map((tab) => ( - - - - ))} -
- +
+ {navTabs.map((tab) => ( + + + + ))} +
); }; diff --git a/website/frontend/src/pages/Events/index.js b/website/frontend/src/pages/Events/index.js index afadc30db1..16b8d6e5c5 100644 --- a/website/frontend/src/pages/Events/index.js +++ b/website/frontend/src/pages/Events/index.js @@ -7,7 +7,6 @@ import { useInitScrollTop } from 'utilities/customHooks'; import EventCard from './EventCard'; import { useDispatch, useSelector } from 'react-redux'; import { getAllEvents } from '../../../reduxStore/Events/EventSlice'; -import { isEmpty } from 'underscore'; import Loadspinner from '../../components/LoadSpinner'; import { useTranslation } from 'react-i18next'; @@ -25,6 +24,7 @@ const EventsPage = () => { const navTabs = [`${t('about.events.navTabs.upcoming')}`, `${t('about.events.navTabs.past')}`]; const selectedNavTab = useSelector((state) => state.eventsNavTab.tab); const allEventsData = useSelector((state) => state.eventsData.events); + const language = useSelector((state) => state.eventsNavTab.languageTab); const eventsApiData = allEventsData.filter((event) => event.website_category === 'airqo'); @@ -41,10 +41,8 @@ const EventsPage = () => { const loading = useSelector((state) => state.eventsData.loading); useEffect(() => { - if (isEmpty(eventsApiData)) { - dispatch(getAllEvents()); - } - }, [selectedNavTab]); + dispatch(getAllEvents()); + }, [language]); // hook to handle see more/less button const [numEventsToShow, setNumEventsToShow] = useState(9); diff --git a/website/frontend/src/pages/OurProducts/CalibrationPage.js b/website/frontend/src/pages/OurProducts/CalibrationPage.js index 644982b724..3943cf5c17 100644 --- a/website/frontend/src/pages/OurProducts/CalibrationPage.js +++ b/website/frontend/src/pages/OurProducts/CalibrationPage.js @@ -107,7 +107,7 @@ const CalibrationPage = () => {

{t('products.calibrate.fourth.subText')}

{
{icon === null ? ( -
+
{}
) : ( -
+
icon { const { t } = useTranslation(); const dispatch = useDispatch(); const allPressData = useSelector((state) => state.pressData.pressData); + const language = useSelector((state) => state.eventsNavTab.languageTab); const pressData = allPressData.filter((event) => event.website_category === 'airqo'); const loading = useSelector((state) => state.pressData.loading); const [numArticlesToShow, setNumArticlesToShow] = useState(5); useEffect(() => { - if (isEmpty(pressData)) { - dispatch(loadPressData()); - } - }, []); + dispatch(loadPressData()); + }, [language]); const sortedArticles = [...pressData].sort((a, b) => new Date(b.date) - new Date(a.date)); @@ -62,7 +61,7 @@ const Press = () => { {sortedArticles.slice(0, numArticlesToShow).map((article, index) => { if (index % 5 === 4) { return ( -
+
{ ); } return ( -
+
{ const { t } = useTranslation(); const [selectedTab, setSelectedTab] = useState('Research'); const onClickTabItem = (tab) => setSelectedTab(tab); + const language = useSelector((state) => state.eventsNavTab.languageTab); const dispatch = useDispatch(); const publicationsData = usePublicationsData(); @@ -48,7 +49,7 @@ const PublicationsPage = () => { useEffect(() => { dispatch(loadPublicationsData()); - }, [publicationsData.length]); + }, [publicationsData.length, language]); return ( diff --git a/website/frontend/styles/CleanAirPage.scss b/website/frontend/styles/CleanAirPage.scss index 5579f95867..c8c054242a 100644 --- a/website/frontend/styles/CleanAirPage.scss +++ b/website/frontend/styles/CleanAirPage.scss @@ -70,6 +70,11 @@ } } + .highlight { + color: #135dff; + font-weight: bold; + } + .page-container { z-index: 1; position: relative; @@ -670,7 +675,6 @@ } .acronym-section-container { - height: 1000px; display: flex; flex-direction: column; justify-content: center; @@ -680,18 +684,19 @@ margin: 0 auto; padding: 0; width: 100%; + background-color: $aq-blue-1; } .acronym-section { + position: relative; display: flex; flex-direction: column; - justify-content: center; - align-items: center; + justify-content: flex-start; + align-items: inherit; margin: 0 auto; padding: 0 16px; width: 100%; - height: auto; - background-color: $aq-blue-1; + height: 100%; .content { width: auto; @@ -806,16 +811,16 @@ } } - @media screen and (min-width: 768px) { - .acronym-section-container { - height: 700px; - } - } - @media screen and (min-width: 1024px) { .acronym-section-container { height: 1000px; } + .acronym-section { + background-image: url('assets/img/cleanAir/section2.png'); + background-size: cover; + background-position: 50% 50%; + background-repeat: no-repeat; + } } .single-section { diff --git a/website/locale/fr/LC_MESSAGES/django.po b/website/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..e7c980719f --- /dev/null +++ b/website/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,1324 @@ +# 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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-17 19:04+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \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" + +#: .\backend\settings.py:175 +msgid "English" +msgstr "" + +#: .\backend\settings.py:176 +msgid "French" +msgstr "" + +#: .\env\Lib\site-packages\cloudinary\forms.py:55 +#: .\env\Lib\site-packages\cloudinary\forms.py:124 +msgid "No file selected!" +msgstr "" + +#: .\env\Lib\site-packages\django\contrib\messages\apps.py:16 +msgid "Messages" +msgstr "" + +#: .\env\Lib\site-packages\django\contrib\sitemaps\apps.py:8 +msgid "Site Maps" +msgstr "" + +#: .\env\Lib\site-packages\django\contrib\staticfiles\apps.py:9 +msgid "Static Files" +msgstr "" + +#: .\env\Lib\site-packages\django\contrib\syndication\apps.py:7 +msgid "Syndication" +msgstr "" + +#. Translators: String used to replace omitted page numbers in elided page +#. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. +#: .\env\Lib\site-packages\django\core\paginator.py:30 +msgid "…" +msgstr "" + +#: .\env\Lib\site-packages\django\core\paginator.py:32 +msgid "That page number is not an integer" +msgstr "" + +#: .\env\Lib\site-packages\django\core\paginator.py:33 +msgid "That page number is less than 1" +msgstr "" + +#: .\env\Lib\site-packages\django\core\paginator.py:34 +msgid "That page contains no results" +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:22 +msgid "Enter a valid value." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:104 +#: .\env\Lib\site-packages\django\forms\fields.py:760 +msgid "Enter a valid URL." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:165 +msgid "Enter a valid integer." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:176 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: .\env\Lib\site-packages\django\core\validators.py:259 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:267 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:279 +#: .\env\Lib\site-packages\django\core\validators.py:306 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:286 +#: .\env\Lib\site-packages\django\core\validators.py:307 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:298 +#: .\env\Lib\site-packages\django\core\validators.py:305 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:341 +msgid "Enter only digits separated by commas." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:347 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:382 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:391 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:400 +#, python-format +msgid "Ensure this value is a multiple of step size %(limit_value)s." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:407 +#, python-format +msgid "" +"Ensure this value is a multiple of step size %(limit_value)s, starting from " +"%(offset)s, e.g. %(offset)s, %(valid_value1)s, %(valid_value2)s, and so on." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:439 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\core\validators.py:457 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\core\validators.py:480 +#: .\env\Lib\site-packages\django\forms\fields.py:355 +#: .\env\Lib\site-packages\django\forms\fields.py:394 +msgid "Enter a number." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:482 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\core\validators.py:487 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\core\validators.py:492 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\core\validators.py:563 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: .\env\Lib\site-packages\django\core\validators.py:624 +msgid "Null characters are not allowed." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\base.py:1473 +#: .\env\Lib\site-packages\django\forms\models.py:906 +msgid "and" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\base.py:1475 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\constraints.py:20 +#, python-format +msgid "Constraint “%(name)s” is violated." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:133 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:134 +msgid "This field cannot be null." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:135 +msgid "This field cannot be blank." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:136 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or +#. 'month'. Eg: "Title must be unique for pub_date year" +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:140 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:179 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1155 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1156 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1158 +msgid "Boolean (Either True or False)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1208 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1210 +msgid "String (unlimited)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1314 +msgid "Comma-separated integers" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1415 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1419 +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1554 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1423 +msgid "Date (without time)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1550 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1558 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1563 +msgid "Date (with time)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1690 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1692 +msgid "Decimal number" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1853 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." +"uuuuuu] format." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1857 +msgid "Duration" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1909 +msgid "Email address" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:1934 +msgid "File path" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2012 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2014 +msgid "Floating point number" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2054 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2056 +msgid "Integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2152 +msgid "Big (8 byte) integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2169 +msgid "Small integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2177 +msgid "IPv4 address" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2208 +msgid "IP address" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2301 +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2302 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2304 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2355 +msgid "Positive big integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2370 +msgid "Positive integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2385 +msgid "Positive small integer" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2401 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2437 +msgid "Text" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2512 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2516 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2520 +msgid "Time" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2628 +msgid "URL" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2652 +msgid "Raw binary data" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2717 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\__init__.py:2719 +msgid "Universally unique identifier" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\files.py:232 +msgid "File" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\files.py:393 +msgid "Image" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\json.py:26 +msgid "A JSON object" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\json.py:28 +msgid "Value must be valid JSON." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:939 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:941 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:1235 +msgid "One-to-one relationship" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:1292 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:1294 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: .\env\Lib\site-packages\django\db\models\fields\related.py:1342 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: .\env\Lib\site-packages\django\forms\boundfield.py:185 +msgid ":?.!" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:95 +msgid "This field is required." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:304 +msgid "Enter a whole number." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:475 +#: .\env\Lib\site-packages\django\forms\fields.py:1252 +msgid "Enter a valid date." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:498 +#: .\env\Lib\site-packages\django\forms\fields.py:1253 +msgid "Enter a valid time." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:525 +msgid "Enter a valid date/time." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:559 +msgid "Enter a valid duration." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:560 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:629 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:630 +msgid "No file was submitted." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:631 +msgid "The submitted file is empty." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:633 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\forms\fields.py:638 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:702 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:874 +#: .\env\Lib\site-packages\django\forms\fields.py:960 +#: .\env\Lib\site-packages\django\forms\models.py:1585 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:962 +#: .\env\Lib\site-packages\django\forms\fields.py:1081 +#: .\env\Lib\site-packages\django\forms\models.py:1583 +msgid "Enter a list of values." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:1082 +msgid "Enter a complete value." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:1321 +msgid "Enter a valid UUID." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\fields.py:1351 +msgid "Enter a valid JSON." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: .\env\Lib\site-packages\django\forms\forms.py:94 +msgid ":" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\forms.py:231 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\formsets.py:61 +#, python-format +msgid "" +"ManagementForm data is missing or has been tampered with. Missing fields: " +"%(field_names)s. You may need to file a bug report if the issue persists." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\formsets.py:65 +#, python-format +msgid "Please submit at most %(num)d form." +msgid_plural "Please submit at most %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\forms\formsets.py:70 +#, python-format +msgid "Please submit at least %(num)d form." +msgid_plural "Please submit at least %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\forms\formsets.py:484 +#: .\env\Lib\site-packages\django\forms\formsets.py:491 +msgid "Order" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\formsets.py:499 +msgid "Delete" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:899 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:904 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:911 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:920 +msgid "Please correct the duplicate values below." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:1357 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:1448 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\models.py:1587 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\utils.py:227 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:461 +msgid "Clear" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:462 +msgid "Currently" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:463 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\includes\grappelli_inline_tabular.html:32 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:42 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_stacked.html:34 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\stacked.html:34 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\tabular.html:59 +msgid "Change" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:800 +msgid "Unknown" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:801 +msgid "Yes" +msgstr "" + +#: .\env\Lib\site-packages\django\forms\widgets.py:802 +msgid "No" +msgstr "" + +#. Translators: Please do not add spaces around commas. +#: .\env\Lib\site-packages\django\template\defaultfilters.py:876 +msgid "yes,no,maybe" +msgstr "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:906 +#: .\env\Lib\site-packages\django\template\defaultfilters.py:923 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:925 +#, python-format +msgid "%s KB" +msgstr "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:927 +#, python-format +msgid "%s MB" +msgstr "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:929 +#, python-format +msgid "%s GB" +msgstr "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:931 +#, python-format +msgid "%s TB" +msgstr "" + +#: .\env\Lib\site-packages\django\template\defaultfilters.py:933 +#, python-format +msgid "%s PB" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:74 +msgid "p.m." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:75 +msgid "a.m." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:80 +msgid "PM" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:81 +msgid "AM" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:153 +msgid "midnight" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dateformat.py:155 +msgid "noon" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:7 +msgid "Monday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:8 +msgid "Tuesday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:9 +msgid "Wednesday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:10 +msgid "Thursday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:11 +msgid "Friday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:12 +msgid "Saturday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:13 +msgid "Sunday" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:16 +msgid "Mon" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:17 +msgid "Tue" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:18 +msgid "Wed" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:19 +msgid "Thu" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:20 +msgid "Fri" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:21 +msgid "Sat" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:22 +msgid "Sun" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:25 +msgid "January" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:26 +msgid "February" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:27 +msgid "March" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:28 +msgid "April" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:29 +msgid "May" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:30 +msgid "June" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:31 +msgid "July" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:32 +msgid "August" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:33 +msgid "September" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:34 +msgid "October" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:35 +msgid "November" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:36 +msgid "December" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:39 +msgid "jan" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:40 +msgid "feb" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:41 +msgid "mar" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:42 +msgid "apr" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:43 +msgid "may" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:44 +msgid "jun" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:45 +msgid "jul" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:46 +msgid "aug" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:47 +msgid "sep" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:48 +msgid "oct" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:49 +msgid "nov" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:50 +msgid "dec" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:53 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:54 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:55 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:56 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:57 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:58 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:59 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:60 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:61 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:62 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:63 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:64 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:67 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:68 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:69 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:70 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:71 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:72 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:73 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:74 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:75 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:76 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:77 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\dates.py:78 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\ipv6.py:8 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: .\env\Lib\site-packages\django\utils\text.py:70 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: .\env\Lib\site-packages\django\utils\text.py:270 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: .\env\Lib\site-packages\django\utils\text.py:289 +#: .\env\Lib\site-packages\django\utils\timesince.py:135 +msgid ", " +msgstr "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:8 +#, python-format +msgid "%(num)d year" +msgid_plural "%(num)d years" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:9 +#, python-format +msgid "%(num)d month" +msgid_plural "%(num)d months" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:10 +#, python-format +msgid "%(num)d week" +msgid_plural "%(num)d weeks" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:11 +#, python-format +msgid "%(num)d day" +msgid_plural "%(num)d days" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:12 +#, python-format +msgid "%(num)d hour" +msgid_plural "%(num)d hours" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\utils\timesince.py:13 +#, python-format +msgid "%(num)d minute" +msgid_plural "%(num)d minutes" +msgstr[0] "" +msgstr[1] "" + +#: .\env\Lib\site-packages\django\views\csrf.py:29 +msgid "Forbidden" +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:30 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:34 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:40 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:45 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:54 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:60 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: .\env\Lib\site-packages\django\views\csrf.py:66 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:44 +msgid "No year specified" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:64 +#: .\env\Lib\site-packages\django\views\generic\dates.py:115 +#: .\env\Lib\site-packages\django\views\generic\dates.py:214 +msgid "Date out of range" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:94 +msgid "No month specified" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:147 +msgid "No day specified" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:194 +msgid "No week specified" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:349 +#: .\env\Lib\site-packages\django\views\generic\dates.py:380 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:652 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\dates.py:692 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\detail.py:56 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\list.py:70 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\list.py:77 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: .\env\Lib\site-packages\django\views\generic\list.py:169 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: .\env\Lib\site-packages\django\views\static.py:49 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: .\env\Lib\site-packages\django\views\static.py:51 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: .\env\Lib\site-packages\django\views\static.py:68 +#: .\env\Lib\site-packages\django\views\templates\directory_index.html:8 +#: .\env\Lib\site-packages\django\views\templates\directory_index.html:11 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:7 +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:220 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:206 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:221 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:229 +msgid "Django Documentation" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:230 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:238 +msgid "Tutorial: A Polling App" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:239 +msgid "Get started with Django" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:247 +msgid "Django Community" +msgstr "" + +#: .\env\Lib\site-packages\django\views\templates\default_urlconf.html:248 +msgid "Connect, get help, or contribute" +msgstr "" + +#: .\env\Lib\site-packages\modeltranslation\widgets.py:24 +msgid "None" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\nested.py:171 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:93 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_tabular.html:81 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:90 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_stacked.html:74 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\stacked.html:71 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\tabular.html:122 +#, python-format +msgid "Add another %(verbose_name)s" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\nested.py:175 +msgid "Remove" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\includes\grappelli_inline_tabular.html:38 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:50 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:44 +msgid "Move Item" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\includes\grappelli_inline_tabular.html:45 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\includes\grappelli_inline_tabular.html:52 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:57 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:61 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:51 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:55 +msgid "Delete Item" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:15 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:14 +msgid "Open All Items" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:16 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:15 +msgid "Close All Items" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:17 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:97 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_tabular.html:83 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:16 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:94 +msgid "Add Item" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_stacked.html:46 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_grappelli_stacked.html:40 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\polymorphic_stacked.html:36 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\stacked.html:36 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\tabular.html:61 +msgid "View on site" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\grappelli_tabular.html:17 +msgid "Add Another" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\stacked.html:34 +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\tabular.html:59 +msgid "View" +msgstr "" + +#: .\env\Lib\site-packages\nested_admin\templates\nesting\admin\inlines\tabular.html:33 +msgid "Delete?" +msgstr "" diff --git a/website/requirements.txt b/website/requirements.txt index bc78459c38..ebe6465e3d 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -13,4 +13,6 @@ drf-yasg invoke psycopg2-binary django-storages[google] -gunicorn \ No newline at end of file +gunicorn +django-modeltranslation +python-dotenv \ No newline at end of file