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 network0>
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-Air0>, is an acronym coined from",
+ "acronym": "<0 className='highlight'>CLEAN-Air0>, 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 network0>1>"
},
@@ -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 0> 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’Afrique0>
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-Air0>, 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": "
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.
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.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.
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": "RESSOURCEFeatured Event
+{t('cleanAirSite.about.highlightSection.tag')}
-
{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 }}>
Network
+ The CLEAN-Air
Network
@@ -79,13 +78,7 @@ const CleanAirAbout = () => {
Goals
+{t('cleanAirSite.about.section5.pillTitle')}
-{t('products.calibrate.fourth.subText')}