diff --git a/ui/babel.config.js b/ui/babel.config.js
index 8967ba4..80ce92a 100644
--- a/ui/babel.config.js
+++ b/ui/babel.config.js
@@ -13,7 +13,7 @@ module.exports = function (api) {
       plugins.push([
         'i18next-extract',
         {
-          locales: ['en', 'da'],
+          locales: ['en', 'da', 'fr', 'sv'],
           defaultValue: null,
         },
       ])
diff --git a/ui/extractedTranslations/da/translation.json b/ui/extractedTranslations/da/translation.json
index 5c10625..bfa9682 100644
--- a/ui/extractedTranslations/da/translation.json
+++ b/ui/extractedTranslations/da/translation.json
@@ -75,6 +75,9 @@
   "photos_page": {
     "title": "Billeder"
   },
+  "places_page": {
+    "title": null
+  },
   "routes": {
     "page_not_found": "Side ikke fundet"
   },
diff --git a/ui/extractedTranslations/en/translation.json b/ui/extractedTranslations/en/translation.json
index 09f8ced..820513e 100644
--- a/ui/extractedTranslations/en/translation.json
+++ b/ui/extractedTranslations/en/translation.json
@@ -75,6 +75,9 @@
   "photos_page": {
     "title": "Photos"
   },
+  "places_page": {
+    "title": "Places"
+  },
   "routes": {
     "page_not_found": "Page not found"
   },
diff --git a/ui/extractedTranslations/fr/translation.json b/ui/extractedTranslations/fr/translation.json
index 83ca3fd..6897f30 100644
--- a/ui/extractedTranslations/fr/translation.json
+++ b/ui/extractedTranslations/fr/translation.json
@@ -75,6 +75,9 @@
   "photos_page": {
     "title": "Photos"
   },
+  "places_page": {
+    "title": null
+  },
   "routes": {
     "page_not_found": "Page non trouvée"
   },
@@ -218,7 +221,7 @@
           "exposure": "Exposure",
           "exposure_program": "Programme",
           "flash": "Flash",
-          "de prise": "Distance focale",
+          "focal_length": "Distance focale",
           "iso": "ISO",
           "lens": "Objectif",
           "maker": "Marque"
diff --git a/ui/extractedTranslations/sv/translation.json b/ui/extractedTranslations/sv/translation.json
index 3b55f60..72be0a1 100644
--- a/ui/extractedTranslations/sv/translation.json
+++ b/ui/extractedTranslations/sv/translation.json
@@ -75,6 +75,9 @@
   "photos_page": {
     "title": "Bilder"
   },
+  "places_page": {
+    "title": null
+  },
   "routes": {
     "page_not_found": "Sidan hittades inte"
   },
diff --git a/ui/package-lock.json b/ui/package-lock.json
index c0733f6..221dce9 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -55,7 +55,9 @@
         "@babel/preset-env": "^7.13.15",
         "@testing-library/jest-dom": "^5.11.10",
         "@testing-library/react": "^11.2.6",
+        "@types/geojson": "^7946.0.7",
         "@types/jest": "^26.0.22",
+        "@types/mapbox-gl": "^2.1.1",
         "@types/react": "^17.0.3",
         "@types/react-dom": "^17.0.3",
         "@types/react-helmet": "^6.1.1",
@@ -2735,6 +2737,12 @@
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
       "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.7",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
+      "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
+      "dev": true
+    },
     "node_modules/@types/graceful-fs": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz",
@@ -2795,6 +2803,15 @@
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
       "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
     },
+    "node_modules/@types/mapbox-gl": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.1.1.tgz",
+      "integrity": "sha512-oDgHhkAC9iYIj/H/9iwm6gHHKKPzweTUgOlozcrvWr8T07dyymGty6mON2j4kEeBGyTTI291vI6gP9k2ArxvHw==",
+      "dev": true,
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
     "node_modules/@types/node": {
       "version": "14.11.2",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
@@ -16369,6 +16386,12 @@
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
       "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
     },
+    "@types/geojson": {
+      "version": "7946.0.7",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
+      "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==",
+      "dev": true
+    },
     "@types/graceful-fs": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz",
@@ -16429,6 +16452,15 @@
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
       "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw=="
     },
+    "@types/mapbox-gl": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.1.1.tgz",
+      "integrity": "sha512-oDgHhkAC9iYIj/H/9iwm6gHHKKPzweTUgOlozcrvWr8T07dyymGty6mON2j4kEeBGyTTI291vI6gP9k2ArxvHw==",
+      "dev": true,
+      "requires": {
+        "@types/geojson": "*"
+      }
+    },
     "@types/node": {
       "version": "14.11.2",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.2.tgz",
diff --git a/ui/package.json b/ui/package.json
index c207876..83aabc9 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -66,7 +66,9 @@
     "@babel/preset-env": "^7.13.15",
     "@testing-library/jest-dom": "^5.11.10",
     "@testing-library/react": "^11.2.6",
+    "@types/geojson": "^7946.0.7",
     "@types/jest": "^26.0.22",
+    "@types/mapbox-gl": "^2.1.1",
     "@types/react": "^17.0.3",
     "@types/react-dom": "^17.0.3",
     "@types/react-helmet": "^6.1.1",
diff --git a/ui/src/Pages/PlacesPage/MapClusterMarker.js b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx
similarity index 64%
rename from ui/src/Pages/PlacesPage/MapClusterMarker.js
rename to ui/src/Pages/PlacesPage/MapClusterMarker.tsx
index 52e10bc..40165d7 100644
--- a/ui/src/Pages/PlacesPage/MapClusterMarker.js
+++ b/ui/src/Pages/PlacesPage/MapClusterMarker.tsx
@@ -1,8 +1,9 @@
 import React from 'react'
-import PropTypes from 'prop-types'
 import styled from 'styled-components'
 
 import imagePopupSrc from './image-popup.svg'
+import { MediaMarker } from './MapPresentMarker'
+import { PresentMarker } from './PlacesPage'
 
 const Wrapper = styled.div`
   width: 56px;
@@ -40,20 +41,21 @@ const PointCountCircle = styled.div`
   padding-top: 2px;
 `
 
+type MapClusterMarkerProps = {
+  setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
+  marker: MediaMarker
+}
+
 const MapClusterMarker = ({
-  thumbnail: thumbJson,
-  point_count_abbreviated,
-  cluster,
-  cluster_id,
-  media_id,
+  marker,
   setPresentMarker,
-}) => {
-  const thumbnail = JSON.parse(thumbJson)
+}: MapClusterMarkerProps) => {
+  const thumbnail = JSON.parse(marker.thumbnail)
 
   const presentMedia = () => {
     setPresentMarker({
-      cluster: !!cluster,
-      id: cluster ? cluster_id : media_id,
+      cluster: !!marker.cluster,
+      id: marker.cluster ? marker.cluster_id : marker.media_id,
     })
   }
 
@@ -61,20 +63,11 @@ const MapClusterMarker = ({
     <Wrapper onClick={presentMedia}>
       <PopupImage src={imagePopupSrc} />
       <ThumbnailImage src={thumbnail.url} />
-      {cluster && (
-        <PointCountCircle>{point_count_abbreviated}</PointCountCircle>
+      {marker.cluster && (
+        <PointCountCircle>{marker.point_count_abbreviated}</PointCountCircle>
       )}
     </Wrapper>
   )
 }
 
-MapClusterMarker.propTypes = {
-  thumbnail: PropTypes.string,
-  cluster: PropTypes.bool,
-  point_count_abbreviated: PropTypes.number,
-  cluster_id: PropTypes.number,
-  media_id: PropTypes.number,
-  setPresentMarker: PropTypes.func,
-}
-
 export default MapClusterMarker
diff --git a/ui/src/Pages/PlacesPage/MapPresentMarker.js b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx
similarity index 55%
rename from ui/src/Pages/PlacesPage/MapPresentMarker.js
rename to ui/src/Pages/PlacesPage/MapPresentMarker.tsx
index 57605cc..defddf1 100644
--- a/ui/src/Pages/PlacesPage/MapPresentMarker.js
+++ b/ui/src/Pages/PlacesPage/MapPresentMarker.tsx
@@ -1,8 +1,9 @@
 import { gql } from '@apollo/client'
-import PropTypes from 'prop-types'
 import React, { useEffect, useState } from 'react'
 import { useLazyQuery } from '@apollo/client'
 import PresentView from '../../components/photoGallery/presentView/PresentView'
+import type mapboxgl from 'mapbox-gl'
+import { PresentMarker } from './PlacesPage'
 
 const QUERY_MEDIA = gql`
   query placePageQueryMedia($mediaIDs: [ID!]!) {
@@ -29,31 +30,57 @@ const QUERY_MEDIA = gql`
   }
 `
 
-const getMediaFromMarker = (map, presentMarker) =>
-  new Promise((resolve, reject) => {
+const getMediaFromMarker = (map: mapboxgl.Map, presentMarker: PresentMarker) =>
+  new Promise<MediaMarker[]>((resolve, reject) => {
     const { cluster, id } = presentMarker
 
     if (cluster) {
-      map
-        .getSource('media')
-        .getClusterLeaves(id, 1000, 0, (error, features) => {
-          if (error) {
-            reject(error)
-            return
-          }
+      const mediaSource = map.getSource('media') as mapboxgl.GeoJSONSource
 
-          const media = features.map(feat => feat.properties)
-          resolve(media)
-        })
+      mediaSource.getClusterLeaves(id, 1000, 0, (error, features) => {
+        if (error) {
+          reject(error)
+          return
+        }
+
+        const media = features.map(feat => feat.properties) as MediaMarker[]
+        resolve(media)
+      })
     } else {
       const features = map.querySourceFeatures('media')
-      const media = features.find(f => f.properties.media_id == id).properties
+      const media = features.find(f => f.properties?.media_id == id)
+        ?.properties as MediaMarker | undefined
+
+      if (media === undefined) {
+        reject('ERROR: media is undefined')
+        return
+      }
+
       resolve([media])
     }
   })
 
-const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => {
-  const [mediaMarkers, setMediaMarkers] = useState(null)
+export interface MediaMarker {
+  id: number
+  thumbnail: string
+  cluster: boolean
+  point_count_abbreviated: number
+  cluster_id: number
+  media_id: number
+}
+
+type MapPresetMarkerProps = {
+  map: mapboxgl.Map | null
+  presentMarker: PresentMarker | null
+  setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
+}
+
+const MapPresentMarker = ({
+  map,
+  presentMarker,
+  setPresentMarker,
+}: MapPresetMarkerProps) => {
+  const [mediaMarkers, setMediaMarkers] = useState<MediaMarker[] | null>(null)
   const [currentIndex, setCurrentIndex] = useState(0)
 
   const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA)
@@ -78,11 +105,12 @@ const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => {
     })
   }, [mediaMarkers])
 
-  if (presentMarker == null || map == null) {
-    return null
-  }
-
-  if (loadedMedia == null) {
+  if (
+    presentMarker == null ||
+    map == null ||
+    mediaMarkers == null ||
+    loadedMedia == null
+  ) {
     return null
   }
 
@@ -105,10 +133,4 @@ const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => {
   )
 }
 
-MapPresentMarker.propTypes = {
-  map: PropTypes.object,
-  presentMarker: PropTypes.object,
-  setPresentMarker: PropTypes.func.isRequired,
-}
-
 export default MapPresentMarker
diff --git a/ui/src/Pages/PlacesPage/PlacesPage.js b/ui/src/Pages/PlacesPage/PlacesPage.tsx
similarity index 82%
rename from ui/src/Pages/PlacesPage/PlacesPage.js
rename to ui/src/Pages/PlacesPage/PlacesPage.tsx
index 610a64a..9e8b2b2 100644
--- a/ui/src/Pages/PlacesPage/PlacesPage.js
+++ b/ui/src/Pages/PlacesPage/PlacesPage.tsx
@@ -1,14 +1,15 @@
+import { gql, useQuery } from '@apollo/client'
+import type mapboxgl from 'mapbox-gl'
 import React, { useEffect, useRef, useState } from 'react'
-import { useQuery, gql } from '@apollo/client'
+import { Helmet } from 'react-helmet'
+import { useTranslation } from 'react-i18next'
 import styled from 'styled-components'
-
-// Will be bundled to dist/src/Pages/PlacesPage/PlacesPage.css
-import 'mapbox-gl/dist/mapbox-gl.css'
-
 import Layout from '../../Layout'
 import { makeUpdateMarkers } from './mapboxHelperFunctions'
 import MapPresentMarker from './MapPresentMarker'
-import { Helmet } from 'react-helmet'
+
+// Will be bundled to dist/src/Pages/PlacesPage/PlacesPage.css
+import 'mapbox-gl/dist/mapbox-gl.css'
 
 const MapWrapper = styled.div`
   width: 100%;
@@ -27,17 +28,25 @@ const MAPBOX_DATA_QUERY = gql`
   }
 `
 
+export type PresentMarker = {
+  id: number
+  cluster: boolean
+}
+
 const MapPage = () => {
-  const [mapboxLibrary, setMapboxLibrary] = useState(null)
-  const [presentMarker, setPresentMarker] = useState(null)
-  const mapContainer = useRef()
-  const map = useRef()
+  const { t } = useTranslation()
+
+  const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl | null>()
+  const [presentMarker, setPresentMarker] = useState<PresentMarker | null>(null)
+  const mapContainer = useRef<HTMLDivElement | null>(null)
+  const map = useRef<mapboxgl.Map | null>(null)
 
   const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY)
 
   useEffect(() => {
     async function loadMapboxLibrary() {
       const mapbox = (await import('mapbox-gl')).default
+
       setMapboxLibrary(mapbox)
     }
     loadMapboxLibrary()
@@ -65,6 +74,11 @@ const MapPage = () => {
     map.current.addControl(new mapboxLibrary.NavigationControl())
 
     map.current.on('load', () => {
+      if (map.current == null) {
+        console.error('ERROR: map is null')
+        return
+      }
+
       map.current.addSource('media', {
         type: 'geojson',
         data: mapboxData.myMediaGeoJson,
@@ -98,7 +112,7 @@ const MapPage = () => {
 
   if (mapboxData && mapboxData.mapboxToken == null) {
     return (
-      <Layout>
+      <Layout title={t('places_page.title', 'Places')}>
         <h1>Mapbox token is not set</h1>
         <p>
           To use map related features a mapbox token is needed.
diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js
deleted file mode 100644
index 32e412c..0000000
--- a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom'
-import MapClusterMarker from './MapClusterMarker'
-
-let markers = {}
-let markersOnScreen = {}
-
-export const makeUpdateMarkers = ({
-  map,
-  mapboxLibrary,
-  setPresentMarker,
-}) => () => {
-  let newMarkers = {}
-  const features = map.querySourceFeatures('media')
-
-  // for every media on the screen, create an HTML marker for it (if we didn't yet),
-  // and add it to the map if it's not there already
-  for (let i = 0; i < features.length; i++) {
-    const coords = features[i].geometry.coordinates
-    const props = features[i].properties
-    const id = props.cluster
-      ? `cluster_${props.cluster_id}`
-      : `media_${props.media_id}`
-
-    let marker = markers[id]
-    if (!marker) {
-      let el = createClusterPopupElement(props, setPresentMarker)
-      marker = markers[id] = new mapboxLibrary.Marker({
-        element: el,
-      }).setLngLat(coords)
-    }
-    newMarkers[id] = marker
-
-    if (!markersOnScreen[id]) marker.addTo(map)
-  }
-  // for every marker we've added previously, remove those that are no longer visible
-  for (const id in markersOnScreen) {
-    if (!newMarkers[id]) markersOnScreen[id].remove()
-  }
-  markersOnScreen = newMarkers
-}
-
-function createClusterPopupElement(geojsonProps, setPresentMarker) {
-  const el = document.createElement('div')
-  ReactDOM.render(
-    <MapClusterMarker {...geojsonProps} setPresentMarker={setPresentMarker} />,
-    el
-  )
-  return el
-}
diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx
new file mode 100644
index 0000000..86dd965
--- /dev/null
+++ b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.tsx
@@ -0,0 +1,72 @@
+import type mapboxgl from 'mapbox-gl'
+import type geojson from 'geojson'
+import React from 'react'
+import ReactDOM from 'react-dom'
+import MapClusterMarker from './MapClusterMarker'
+import { MediaMarker } from './MapPresentMarker'
+import { PresentMarker } from './PlacesPage'
+
+const markers: { [key: string]: mapboxgl.Marker } = {}
+let markersOnScreen: typeof markers = {}
+
+type makeUpdateMarkersArgs = {
+  map: mapboxgl.Map
+  mapboxLibrary: typeof mapboxgl
+  setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
+}
+
+export const makeUpdateMarkers = ({
+  map,
+  mapboxLibrary,
+  setPresentMarker,
+}: makeUpdateMarkersArgs) => () => {
+  const newMarkers: typeof markers = {}
+  const features = map.querySourceFeatures('media')
+
+  // for every media on the screen, create an HTML marker for it (if we didn't yet),
+  // and add it to the map if it's not there already
+  for (const feature of features) {
+    const point = feature.geometry as geojson.Point
+    const coords = point.coordinates as [number, number]
+    const props = feature.properties as MediaMarker
+    if (props == null) {
+      console.warn('WARN: geojson feature had no properties', feature)
+      continue
+    }
+
+    const id = props.cluster
+      ? `cluster_${props.cluster_id}`
+      : `media_${props.media_id}`
+
+    let marker = markers[id]
+    if (!marker) {
+      const el = createClusterPopupElement(props, setPresentMarker)
+      marker = markers[id] = new mapboxLibrary.Marker({
+        element: el,
+      }).setLngLat(coords)
+    }
+    newMarkers[id] = marker
+
+    if (!markersOnScreen[id]) marker.addTo(map)
+  }
+  // for every marker we've added previously, remove those that are no longer visible
+  for (const id in markersOnScreen) {
+    if (!newMarkers[id]) markersOnScreen[id].remove()
+  }
+  markersOnScreen = newMarkers
+}
+
+function createClusterPopupElement(
+  geojsonProps: MediaMarker,
+  setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
+) {
+  const el = document.createElement('div')
+  ReactDOM.render(
+    <MapClusterMarker
+      marker={geojsonProps}
+      setPresentMarker={setPresentMarker}
+    />,
+    el
+  )
+  return el
+}
