diff --git a/backend/app/api_views.py b/backend/app/api_views.py index f8f77aac..f9c55b59 100644 --- a/backend/app/api_views.py +++ b/backend/app/api_views.py @@ -1,6 +1,5 @@ import json import random -from math import ceil from rest_framework import status from rest_framework.decorators import api_view @@ -8,12 +7,12 @@ from django.db.models import Q, Prefetch from django.core.paginator import Paginator -from django.views.decorators.csrf import csrf_exempt +from django.utils.translation import gettext_lazy as _ from app.view_helpers import ( get_map_squares_by_arondissement, get_arrondissement_geojson, - tag_confidence + tag_confidence, tag_helper ) from .models import ( @@ -36,6 +35,8 @@ CorpusAnalysisResultsSerializer ) +from .translation_db import TRANSLATIONS, translate_tag + # TODO(ra): See if we can move this elsewhere. PHOTOGRAPHER_SEARCH_ORDER_BY = [ @@ -48,52 +49,6 @@ ] -def tag_helper(tag_name, page=None): - all_yolo_results = PhotoAnalysisResult.objects.filter(name='yolo_model') - - if not all_yolo_results.count(): - return [] - - relevant_results = [] - print('yolo results here: ', len(all_yolo_results)) - for result in all_yolo_results: - data = result.parsed_result() - if tag_name in data['labels']: - relevant_results.append(result) - - print('relevant results: ', len(relevant_results)) - - # TODO(ra) Fix the results per page math... it looks like it's stepping - # through src photo indexes - results_per_page = 20 - result_count = len(relevant_results) - page_count = ceil(result_count / results_per_page) - - if page: - first_result = results_per_page * (page-1) - last_result = first_result + results_per_page - print(first_result, last_result) - relevant_results_this_page = relevant_results[first_result:last_result] - else: - relevant_results_this_page = relevant_results - - print(relevant_results_this_page) - - # sort by confidence - by_confidence = [] - for result in relevant_results_this_page: - data = result.parsed_result() - confidence = 0 - for box in data['boxes']: - # an image may have several tag_name in labels, find greatest confidence - if box['label'] == tag_name: - confidence = max(confidence, box['confidence']) - by_confidence.append((result, confidence)) - - sorted_analysis_obj = sorted(by_confidence, key=lambda obj: obj[1], reverse=True) - return [result[0].photo for result in sorted_analysis_obj], result_count, page_count - - @api_view(['GET']) def photo(request, map_square_number, folder_number, photo_number): """ @@ -105,6 +60,14 @@ def photo(request, map_square_number, folder_number, photo_number): return Response(serializer.data) +@api_view(['GET']) +def translation(request, language_code): + """ + API endpoint to get text translation dictionary + """ + return Response(TRANSLATIONS) + + @api_view(['GET']) def previous_next_photos(request, map_square_number, folder_number, photo_number): """ @@ -159,7 +122,10 @@ def all_map_squares(request): """ map_square_obj = MapSquare.objects.all().prefetch_related("photo_set") serializer = MapSquareSerializerWithoutPhotos(map_square_obj, many=True) - return Response(serializer.data) + return Response({ + map_square["number"]: map_square + for map_square in serializer.data + }) @api_view(['GET']) @@ -346,6 +312,7 @@ def get_photos_by_analysis(request, analysis_name, object_name=None): serializer = PhotoSerializer(sorted_photo_obj, many=True) return Response(serializer.data) + @api_view(['GET']) def get_images_with_text(request): """ @@ -381,7 +348,8 @@ def get_photos_by_tag(request, tag_name): """ API endpoint to get all photos associated with a tag (specified by tag_name) """ - sorted_photo_obj, _, _ = tag_helper(tag_name) + en_tag = translate_tag(tag_name) + sorted_photo_obj, _, _ = tag_helper(en_tag) serializer = PhotoSerializer(sorted_photo_obj, many=True) return Response(serializer.data) @@ -395,6 +363,7 @@ def photo_tag_helper(map_square_number, folder_number, photo_number): else: return None + @api_view(['GET']) def get_random_photos(request): @@ -404,6 +373,7 @@ def get_random_photos(request): serializer = SimplePhotoSerializerForCollage(random_photos, many=True) return Response(serializer.data) + @api_view(['GET']) def get_photo_tags(request, map_square_number, folder_number, photo_number): """ @@ -476,19 +446,19 @@ def explore(request): API endpoint for the explore view, which gives users a filtered view to all of the photos in the collection """ - tag = request.data.get('selectedTag') + ALL = _("ALL") + + tag = translate_tag( + request.data.get('selectedTag'), default=ALL + ) page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 10)) - ALL = 'All' - query = Q() # Filter by tags if tag != ALL: query |= Q(analyses__name='yolo_model', analyses__result__icontains=tag) - - if tag != ALL: prefetch = Prefetch('analyses', queryset=PhotoAnalysisResult.objects.filter(name='yolo_model')) photos = Photo.objects.filter(query).prefetch_related(prefetch).distinct() photos_with_analysis = [ diff --git a/backend/app/management/commands/compile_translations.py b/backend/app/management/commands/compile_translations.py new file mode 100644 index 00000000..d19c3888 --- /dev/null +++ b/backend/app/management/commands/compile_translations.py @@ -0,0 +1,119 @@ +""" +Django management command launch_site +""" + +import io +import os +import shutil + +from tqdm import tqdm +from translate_po.main import ( + recognize_po_file, read_lines, + translate as translate_line +) + +from django.conf import settings +from django.core.management.base import BaseCommand +from django.core.management import call_command + +def save_lines(file: str, lines: list): + """ Save lines from memory into a file. + :parameter file: + :parameter lines: + """ + with io.open(file, 'w', encoding='utf8') as infile: + infile.write(""" +msgid "" +msgstr "" +""") + for keys, values in lines.metadata.items(): + infile.write(f'"{keys}:{values}\\n"\n') + infile.write('\n') + for line in lines: + infile.write(line.__unicode__()) + +def translate(fro, to, src_dir, dest_dir, fuzzy=False): + # Work around using parser-based translate_po.main.run function + # due to conflict with Django BaseCommand parser + class Arguments: + def __init__(self, **kwargs): + [setattr(self, attr, val) for attr, val in kwargs.items()] + + arguments = Arguments(fro=fro, to=to, src=src_dir, dest=dest_dir) + + for file in os.listdir(src_dir): + if not recognize_po_file(file): + continue + old_file = os.path.join(arguments.dest, file) + new_file = os.path.join(arguments.src, file) + + print(f"Translating {old_file}...") + entries = read_lines(old_file) + for entry in tqdm(entries): + if entry.translated() or entry.obsolete or entry.fuzzy: + continue + line_parts = entry.msgid.split('\n') + translated_line_parts = [( + translate_line(line_part, arguments) + if line_part.strip(" ") else line_part + ) for line_part in line_parts] + entry.msgstr = '\n'.join(translated_line_parts) + if fuzzy: + entry.flags.append("fuzzy") + + save_lines(new_file, entries) + + +class Command(BaseCommand): + """ + Custom django-admin command to build project translation + + https://testdriven.io/blog/multiple-languages-in-django/ + """ + + help = "Custom django-admin command to compile translations in translation_db.py" + + def add_arguments(self, parser): + parser.add_argument("--no_auto_trans", action="store_true") + parser.add_argument("--rebuild", action="store_true") + parser.add_argument( + "--main_lang", type=str, action="store", default="en" + ) + parser.add_argument( + "--mark_fuzzy", action="store_true", + help="Mark auto-translations as fuzzy" + ) + + def handle(self, *args, **options): + no_auto_translate: bool = options.get("no_auto_trans") + main_lang: str = options.get("main_lang") + rebuild: bool = options.get("rebuild") + mark_fuzzy: bool = options.get("mark_fuzzy") + + def iter_locale_paths(): + for locale_path in settings.LOCALE_PATHS: + for language_code, _ in settings.LANGUAGES: + yield locale_path, language_code + + # Make locale paths + for locale_path, language_code in iter_locale_paths(): + messages_path = os.path.join(locale_path, language_code) + if rebuild and os.path.exists(messages_path): + shutil.rmtree(messages_path) + os.makedirs( + messages_path, + exist_ok=True + ) + + call_command("makemessages", all=True, ignore=["env"]) + if not no_auto_translate: + for locale_path, language_code in iter_locale_paths(): + if language_code == main_lang: + continue + po_dir = os.path.join(locale_path, language_code, "LC_MESSAGES") + translate( + fro=main_lang, to=language_code, + src_dir=po_dir, dest_dir=po_dir, + fuzzy=mark_fuzzy + ) + call_command("compilemessages", ignore=["env"]) diff --git a/backend/app/translation_db.py b/backend/app/translation_db.py new file mode 100644 index 00000000..2aa943f3 --- /dev/null +++ b/backend/app/translation_db.py @@ -0,0 +1,236 @@ +from textwrap import dedent +from django.utils.translation import gettext_lazy + +# pylint: disable=line-too-long + + +def _(text, dedent_text=True): + if dedent_text: + text = dedent(text) + return gettext_lazy(text.strip(" \n\t\r")) + + +def get_tag_translations(): + return { + val: key for key, val in TRANSLATIONS["global"]["objectTags"].items() + } + + +def translate_tag(tag, default=None): + return get_tag_translations().get( + tag, tag if default is None else default + ) + + +TRANSLATIONS = { + "global": { + "projectTitle": _("This was Paris in 1970"), + "labName": _("MIT Digital Humanities Lab"), + "arrondissement": _("Arrondissement"), + "photosAvailable": _("Photos available"), + "mapSquare": _("Map Square"), + "photos": _("photos"), + "objectTags": { + "person": _("person"), + "bicycle": _("bicycle"), + "car": _("car"), + "motorbike": _("motorbike"), + "motorcycle": _("motorcycle"), + "aeroplane": _("aeroplane"), + "airplane": _("airplane"), + "bus": _("bus"), + "train": _("train"), + "truck": _("truck"), + "boat": _("boat"), + "traffic light": _("traffic light"), + "fire hydrant": _("fire hydrant"), + "stop sign": _("stop sign"), + "parking meter": _("parking meter"), + "bench": _("bench"), + "bird": _("bird"), + "cat": _("cat"), + "dog": _("dog"), + "horse": _("horse"), + "sheep": _("sheep"), + "cow": _("cow"), + "elephant": _("elephant"), + "bear": _("bear"), + "zebra": _("zebra"), + "giraffe": _("giraffe"), + "backpack": _("backpack"), + "umbrella": _("umbrella"), + "handbag": _("handbag"), + "tie": _("tie"), + "suitcase": _("suitcase"), + "frisbee": _("frisbee"), + "skis": _("skis"), + "snowboard": _("snowboard"), + "sports ball": _("sports ball"), + "kite": _("kite"), + "baseball bat": _("baseball bat"), + "baseball glove": _("baseball glove"), + "skateboard": _("skateboard"), + "surfboard": _("surfboard"), + "tennis racket": _("tennis racket"), + "bottle": _("bottle"), + "wine glass": _("wine glass"), + "cup": _("cup"), + "fork": _("fork"), + "knife": _("knife"), + "spoon": _("spoon"), + "bowl": _("bowl"), + "banana": _("banana"), + "apple": _("apple"), + "sandwich": _("sandwich"), + "orange": _("orange"), + "broccoli": _("broccoli"), + "carrot": _("carrot"), + "hot dog": _("hot dog"), + "pizza": _("pizza"), + "donut": _("donut"), + "cake": _("cake"), + "chair": _("chair"), + "sofa": _("sofa"), + "pottedplant": _("pottedplant"), + "potted plant": _("potted plant"), + "bed": _("bed"), + "diningtable": _("diningtable"), + "dining table": _("dining table"), + "toilet": _("toilet"), + "tvmonitor": _("tvmonitor"), + "tv": _("tv"), + "laptop": _("laptop"), + "mouse": _("mouse"), + "remote": _("remote"), + "keyboard": _("keyboard"), + "cell phone": _("cell phone"), + "microwave": _("microwave"), + "oven": _("oven"), + "toaster": _("toaster"), + "sink": _("sink"), + "refrigerator": _("refrigerator"), + "book": _("book"), + "clock": _("clock"), + "vase": _("vase"), + "scissors": _("scissors"), + "teddy bear": _("teddy bear"), + "hair drier": _("hair drier"), + "toothbrush": _("toothbrush") + }, + }, + # "HomePage": { + "HomePage": { + "wipModal": { + "title": _("Pardon Our Dust!"), + "description1": _(""" + This was Paris in 1970 is a project by + the MIT Digital Humanities Lab in collaboration with + Catherine Clark, Associate Professor of History and French Studies + at MIT and Director of MIT Digital Humanities. + """), + "description2": _(""" + This project is still under construction and contains + student work, so there may be features that are + currently incomplete or inaccurate. + """), + "close": _("Close") + }, + "scrollDown": _("Scroll down to enter"), + "explore": _("Explore Photos by Subject"), + "view": _("View Photos by Location"), + "context": _("Context and Research"), + "about": _("About the Project") + }, + "About": { + "aboutHeader": _("About"), + "context1": _("In May 1970, thousands of amateur photographers spread out across Paris to take pictures. They were participants in a photo contest, “This was Paris in 1970,” organized by the cooperative electronics store the Fnac. Each contestant had been assigned to document a 250m square of the city. By the end of the month, this army of photographers had produced an unprecedented collection of 100,000 photographs: 70,000 black-and-white prints and 30,000 colors slides. This website currently hosts 5,000 color slides from the 13th and 19th arrondissements, areas of the city which were undergoing significant change in 1960s and 1970s."), + "context2": _(" The project This was Paris in 1970 provides tools to explore the rich archive: a map to see the photos square by square; an object detector to search for photos of many objects from people to cats, cars to strollers; a similar photo viewer to identify photos by composition rather than subject; and articles providing context and analysis."), + "teamHeader": _("The Team"), + "team1": _("This is Paris in 1970 was created in MIT’s Digital Humanities Lab as a collaboration between DH Fellow Prof. Catherine Clark, four dozen undergraduate research associates, and the instructional staff of the DH Lab. Justice Vidal built out the first version of the site, and Nina Li spearheaded the design work."), + "team2": _("The Bibliothèque historique de la Ville de Paris holds the contest photographs. Its photo department made this project possible."), + }, + "Explore": { + "rangeError": _('Desired page out of range'), + "paginationText1": _("Page {{currentPage}} of {{totalPages}}"), + "paginationText2": _("Showing Photos {{startImage}} - {{endImage}} out of {{totalCount}}"), + "previous": _("Previous Page"), + "goTo": _("Go to Page: "), + "next": _("Next Page"), + "go": _("Go"), + "objectsLabel": _("Objects"), + "objectDropdownDefault": _("All"), + "imagePerPageLabel": _("Images per Page"), + "toolExplanation": _(""" + Use these tools to sort photos according to subject. The object detection tool uses + the YOLO (You Only Look Once) system. Click on any photo to view it and its metadata, + where you will also find a gateway to the similarity algorithm. + """), + }, + "MapPage": { + "return": _("Return"), + "descriptionHeader": _("Map"), + "description": _(""" + In order to document the entire city, and not just its most touristy or photogenic neighborhoods, + the organizers of “This was Paris in 1970” divided up the city in 1755 squares and assigned + participants to document a square. Each square measured 250m by 250m. Because there were + more participants than squares, many contain documentation by multiple people. Squares that + contain no photos here were likely captured in black-and-white prints, which are available at the + BHVP in Paris. + """), + "instructions": _("Click on a square to see photos taken there."), + }, + "MapSquare": { + "notInDB": _("Map Square {{squareNum}} is not in the database."), + "noMetadata": _("No metadata has been transcribed for these photos."), + }, + "PhotographerSearch": {}, + "Photographer": { + "notInDB": _("Photographer number {{photographerNum}} is not in the database."), + "profile": _("Photographer Profile"), + "number": _("Number"), + "sex": _("Recorded Sex"), + "address": _("Address"), + "noRecord": _("No Record"), + "photosTaken": _("{{name}} took a total of {{numPhotos}} photos for the competition."), + "activity": _("MAP OF ACTIVITY"), + }, + "Blog": { + "Sidebar": { + "posts": _("Posts") + }, + "title": _("Articles"), + "readMore": _("Read more"), + "header": _("Here you will find work exploring Paris, the contest photos, and this project's tools. Some of this is student work; some is by more established researchers. If you use the photos and would like your work to be included here, please email Catherine Clark."), + }, + "BlogPost": { + "previewNotice": _(""" + This page is only a preview of the post and is only visible to the author and + site admin. + To make it visible to anyone, the author or a user with blog edit access must + click + "published" in the admin panel. + """) + }, + "PhotoView": { + "PHOTO": _("PHOTO"), + "SLIDE": _("SLIDE"), + "similarPhotos": _("Similar Photos"), + "similarPhotosHelp": _("""This is what similar photos are and how we generate them."""), + "sortBy": _("Sort By..."), + "PHOTOGRAPHER": _("PHOTOGRAPHER"), + "TAGS": _("TAGS"), + "TAGSHelp": _("This is what a tag is and how we generate them."), + "noTags": _("No tags to display for this photo."), + "LOCATION": _("LOCATION"), + "detailsHeader": _("Photograph Details"), + "photoNotFound": _("Photo with id {{photoId}} is not in database.") + }, + "TagView": { + "tagHeader": _("Photographs tagged"), + "numResults": _("{{numResults}} results."), + "pageIndicator": _("Showing page {{currentPage}} of {{totalPages}}"), + "tagNotFound": _("Tag {{tagName}} is not in the database.") + }, + # }, + "description": _("This project is still under construction and contains student work, so there may be features that are currently incomplete or inaccurate.") +} diff --git a/backend/app/views.py b/backend/app/views.py index 75ade1e0..b1d80e7f 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -15,7 +15,7 @@ from app.models import Photo, MapSquare from app.serializers import SimplePhotoSerializer - +from .translation_db import translate_tag # app views def render_view(request, context): @@ -232,8 +232,9 @@ def tag_view(request, tag_name, page=1): """ Tag page, specified by tag_name """ - sorted_photo_obj, result_count, page_count = tag_helper(tag_name, - page=page) + sorted_photo_obj, result_count, page_count = tag_helper( + translate_tag(tag_name), page=page + ) serializer = SimplePhotoSerializer(sorted_photo_obj, many=True) print('we are here') # there's probably a much simpler way... diff --git a/backend/blog/admin.py b/backend/blog/admin.py index 79aacee1..6cc1acb5 100644 --- a/backend/blog/admin.py +++ b/backend/blog/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.conf import settings from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from tinymce.widgets import TinyMCE @@ -24,7 +25,7 @@ def custom_slug(self, obj): return mark_safe('{}'.format( "/" + settings.BLOG_ROOT_URL + "/" + obj.slug, obj.slug)) - custom_slug.short_description = "Slug (preview on site)" + custom_slug.short_description = "Slug " + _("(preview on site)") admin.site.register(BlogPost, BlogPostAdmin) diff --git a/backend/blog/forms.py b/backend/blog/forms.py index c5263ec2..48221801 100644 --- a/backend/blog/forms.py +++ b/backend/blog/forms.py @@ -1,6 +1,7 @@ import datetime from django import forms from django.template.defaultfilters import slugify +from django.utils.translation import gettext_lazy as _ from blog.models import BlogPost @@ -25,16 +26,16 @@ def clean(self): slug = self.slug_generator() if BlogPost.objects.filter(slug=slug).exists(): raise forms.ValidationError( - {'slug': 'Autogenerated slug already exists. Enter unique ' - 'slug'}) + {'slug': _('Autogenerated slug already exists. Enter unique ' + 'slug')}) if BlogPost.objects.filter(slug=slug).exists(): raise forms.ValidationError( - {"slug": "Slug already exists. Enter unique slug or leave empty"}) + {"slug": _("Slug already exists. Enter unique slug or leave empty")}) else: if slug is None: slug = self.slug_generator() if BlogPost.objects.filter(slug=slug).exists() and slug != self.instance.slug: raise forms.ValidationError( - {"slug": "Slug already exists. Enter unique slug"}) + {"slug": _("Slug already exists. Enter unique slug")}) diff --git a/backend/blog/views.py b/backend/blog/views.py index 38a645f4..2e41d156 100644 --- a/backend/blog/views.py +++ b/backend/blog/views.py @@ -5,6 +5,8 @@ from django.utils import dateparse from django.shortcuts import render from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ + from blog.models import ( BlogPost ) @@ -30,7 +32,7 @@ def blog_home_page(request): context = { 'page_metadata': { - 'title': 'Blog Home Page' + 'title': _('Blog Home Page') }, 'component_name': 'Blog', 'component_props': { @@ -73,7 +75,7 @@ def blog_post(request, slug): data['tags'] = list(data['tags'].names()) context = { 'page_metadata': { - 'title': 'Blog Post: ' + data['slug'] + 'title': _('Blog Post') + ': ' + data['slug'] }, 'component_name': 'BlogPost', 'component_props': { diff --git a/backend/config/settings/base.py b/backend/config/settings/base.py index 86bb0fc1..324c176b 100644 --- a/backend/config/settings/base.py +++ b/backend/config/settings/base.py @@ -10,6 +10,7 @@ import os from pathlib import Path +from django.utils.translation import gettext_lazy as _ DEBUG = False # override in dev settings @@ -20,6 +21,9 @@ SETTINGS_DIR = os.path.join(CONFIG_DIR, 'settings') BACKEND_DATA_DIR = os.path.join(BACKEND_DIR, 'data') +LOCALE_PATHS = ( + os.path.join(BACKEND_DIR, "locale"), +) ANALYSIS_DIR = Path(PROJECT_ROOT, 'backend', 'app', 'analysis') ANALYSIS_PICKLE_PATH = Path(BACKEND_DIR, ANALYSIS_DIR, 'analysis_results') @@ -63,6 +67,7 @@ 'django.contrib.staticfiles', 'django.contrib.sites', 'django.contrib.flatpages', + 'rosetta', # 3rd party 'rest_framework', @@ -87,9 +92,9 @@ MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', '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', @@ -181,6 +186,11 @@ USE_TZ = True +LANGUAGES = [ + ('en', 'English'), + ('fr', 'French'), +] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.0/howto/static-files/ diff --git a/backend/config/urls.py b/backend/config/urls.py index f1357e6b..12cd5c8c 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -17,7 +17,8 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth import views as auth_views -from django.urls import path +from django.conf.urls.i18n import i18n_patterns +from django.urls import path, include from app import views, api_views from app.common import render_react_view @@ -36,124 +37,129 @@ def react_view_path(route, component_name): urlpatterns = [ - # Django admin page - path('admin/', admin.site.urls), - - ################################################################################ - # API endpoints - ################################################################################ - # Photos - path('api/photo////', - api_views.photo, - name="photo"), - - path('api/prev_next_photos////', - api_views.previous_next_photos, - name="previous_next_photos"), - - path( - 'api/similar_photos/////', - api_views.get_photo_by_similarity, - name="similar_photos" - ), - - path('api/all_photos/', - api_views.all_photos, - name="all_photos"), - - path('api/random_photos/', - api_views.get_random_photos, - name="random_photos"), - # explore path('api/explore/', api_views.explore, name="explore"), - - # Photographers - path('api/search_photographers/', - api_views.search_photographers), - - path('api/search_photographers/dropdown_options', - api_views.get_search_photographers_dropdown_options), - - path('api/photographer/', - api_views.get_photographer, - name="all_photographers"), - - path('api/photographer//', api_views.get_photographer, - name='photographer'), - - # Map Squares - path('api/map_square//', - api_views.get_map_square, - name="map_square"), - - path('api/all_map_squares/', - api_views.all_map_squares, - name="all_map_squares"), - - # Tags - path('api/tag//', - api_views.get_photos_by_tag, - name="get_photos_by_tag"), - - # Analyses - path('api/all_analyses/', api_views.all_analyses, name='all_analyses'), - path('api/similarity/', api_views.get_all_photos_in_order, name="all_photos_in_order"), - path('api/analysis//', api_views.get_photos_by_analysis, - name="get_photos_by_analysis"), - path('api/analysis///', api_views.get_photos_by_analysis, - name="get_photos_by_analysis"), - path('api/corpus_analysis/', api_views.get_corpus_analysis_results, name="get_corpus"), - path('api/clustering///', - api_views.get_photos_by_cluster, name="clustering"), - path('api/text_ocr/', api_views.get_images_with_text), - - - # Arrondissements - path('api/arrondissements_geojson/', api_views.get_arrondissements_geojson, - name="get_arrondissement"), - path('api/arrondissements_geojson//', - api_views.get_arrondissements_geojson, name="get_one_arrondissement"), - path('api/arrondissements_map_squares/', api_views.get_arrondissements_map_squares), - path('api/arrondissements_map_squares/', api_views.get_arrondissements_map_squares), - - # Distances - path('api/get_photo_distances//', - api_views.get_photo_distances, name="get_photo_distances"), - - - ################################################################################ - # View Pages - ################################################################################ - path('', views.index), - path('map/', views.map_page), - path('about/', views.about), - path('search/', views.search_view), - path('explore/', views.explore_view), - # Photographers - path('photographers/', views.photographer_list_view), - path('photographer//', views.photographer_view), - - # Photos - path('photo////', views.photo_view), - - # blog urls - path(f'{settings.BLOG_ROOT_URL}/', blog_views.blog_home_page, name="blog_home"), - path(f'{settings.BLOG_ROOT_URL}//', blog_views.blog_post, - name='blog-detail'), - - # Map Squares - path('map_square//', views.map_square_view), - path('text_ocr/', views.text_ocr_view), - path('similarity////', views.similar_photos_view), - - # path('clustering///', views.cluster_view), - # Blog urls - # Log in/out urls - path('login/', auth_views.LoginView.as_view(template_name='admin/login.html'), name='login'), - path('logout/', auth_views.LogoutView.as_view(), name='logout'), - # Tags - path('tag//', views.tag_view), - path('tag///', views.tag_view), - -] + path("locales//", api_views.translation, name="translation") + +] + i18n_patterns( + # Django admin page + path('admin/', admin.site.urls), + + # Translation + path('rosetta/', include('rosetta.urls')), + + ################################################################################ + # API endpoints + ################################################################################ + # Photos + path('api/photo////', + api_views.photo, + name="photo"), + + path('api/prev_next_photos////', + api_views.previous_next_photos, + name="previous_next_photos"), + + path( + 'api/similar_photos/////', + api_views.get_photo_by_similarity, + name="similar_photos" + ), + + path('api/all_photos/', + api_views.all_photos, + name="all_photos"), + + path('api/random_photos/', + api_views.get_random_photos, + name="random_photos"), + + # Photographers + path('api/search_photographers/', + api_views.search_photographers), + + path('api/search_photographers/dropdown_options', + api_views.get_search_photographers_dropdown_options), + + path('api/photographer/', + api_views.get_photographer, + name="all_photographers"), + + path('api/photographer//', api_views.get_photographer, + name='photographer'), + + # Map Squares + path('api/map_square//', + api_views.get_map_square, + name="map_square"), + + path('api/all_map_squares/', + api_views.all_map_squares, + name="all_map_squares"), + + # Tags + path('api/tag//', + api_views.get_photos_by_tag, + name="get_photos_by_tag"), + + # Analyses + path('api/all_analyses/', api_views.all_analyses, name='all_analyses'), + path('api/similarity/', api_views.get_all_photos_in_order, name="all_photos_in_order"), + path('api/analysis//', api_views.get_photos_by_analysis, + name="get_photos_by_analysis"), + path('api/analysis///', api_views.get_photos_by_analysis, + name="get_photos_by_analysis"), + path('api/corpus_analysis/', api_views.get_corpus_analysis_results, name="get_corpus"), + path('api/clustering///', + api_views.get_photos_by_cluster, name="clustering"), + path('api/text_ocr/', api_views.get_images_with_text), + + + # Arrondissements + path('api/arrondissements_geojson/', api_views.get_arrondissements_geojson, + name="get_arrondissement"), + path('api/arrondissements_geojson//', + api_views.get_arrondissements_geojson, name="get_one_arrondissement"), + path('api/arrondissements_map_squares/', api_views.get_arrondissements_map_squares), + path('api/arrondissements_map_squares/', api_views.get_arrondissements_map_squares), + + # Distances + path('api/get_photo_distances//', + api_views.get_photo_distances, name="get_photo_distances"), + + + ################################################################################ + # View Pages + ################################################################################ + path('', views.index), + path('map/', views.map_page), + path('about/', views.about), + path('search/', views.search_view), + path('explore/', views.explore_view), + # Photographers + path('photographers/', views.photographer_list_view), + path('photographer//', views.photographer_view), + + # Photos + path('photo////', views.photo_view), + + # blog urls + path(f'{settings.BLOG_ROOT_URL}/', blog_views.blog_home_page, name="blog_home"), + path(f'{settings.BLOG_ROOT_URL}//', blog_views.blog_post, + name='blog-detail'), + + # Map Squares + path('map_square//', views.map_square_view), + path('text_ocr/', views.text_ocr_view), + path('similarity////', views.similar_photos_view), + + # path('clustering///', views.cluster_view), + # Blog urls + # Log in/out urls + path('login/', auth_views.LoginView.as_view(template_name='admin/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + # Tags + path('tag//', views.tag_view), + path('tag///', views.tag_view), + +) diff --git a/backend/templates/base.html b/backend/templates/base.html index fb14c849..7b1f2a79 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -1,25 +1,26 @@ +{% load i18n %} {% load webpack_loader %} {% load render_bundle from webpack_loader %} - - This was Paris in 1970 - - - - - - - - + + + diff --git a/backend/templates/blog/blog_404.html b/backend/templates/blog/blog_404.html index 999041cc..040e31c8 100644 --- a/backend/templates/blog/blog_404.html +++ b/backend/templates/blog/blog_404.html @@ -1,3 +1,4 @@ +{% load i18n %} {% include "base.html" %} {% block content %}
@@ -7,9 +8,9 @@ {{ post.content | safe }}
- Blog page not found. + {% trans "Blog page not found." %} - Return to Blog Home + {% trans "Return to Blog Home" %}
diff --git a/backend/templates/includes/navbar.html b/backend/templates/includes/navbar.html index b2ade27b..45585fb1 100644 --- a/backend/templates/includes/navbar.html +++ b/backend/templates/includes/navbar.html @@ -1,10 +1,11 @@ +{% load i18n %}