diff --git a/.gitignore b/.gitignore
index ac787cc33c76b8381057dcc186f9035e4a1adbd2..92077e16995dd10d83f8a30c671a4068febcd808 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ swh/web/static/js/
 swh/web/static/css/
 swh/web/static/fonts/
 swh/web/static/jssources/
+swh/web/static/img/thirdParty/
 .cache-loader/
 build/
 dist/
diff --git a/package.json b/package.json
index 979dbc46526626c361e8843bc211bb2d7bbaab01..ab4c73132ac1b3dc4fab333c073111a744c66803 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "ansi_up": "^4.0.3",
     "bootstrap": "^4.3.1",
     "bootstrap-year-calendar-bs4": "^1.0.0",
+    "chosen-js": "^1.8.7",
     "clipboard": "^2.0.4",
     "core-js": "^3.1.4",
     "d3": "^5.9.7",
diff --git a/swh/web/assets/config/webpack.config.development.js b/swh/web/assets/config/webpack.config.development.js
index 569a14d77f6d199558a113e0236a0ec88d74cee3..e6c49f54dc255ea01c3d967a8a3d1a58eacc9afd 100644
--- a/swh/web/assets/config/webpack.config.development.js
+++ b/swh/web/assets/config/webpack.config.development.js
@@ -302,6 +302,15 @@ module.exports = {
             outputPath: 'fonts/'
           }
         }]
+      }, {
+        test: /\.png$/,
+        use: [{
+          loader: 'file-loader',
+          options: {
+            name: '[name].[ext]',
+            outputPath: 'img/thirdParty/'
+          }
+        }]
       }
     ],
     // tell webpack to not parse minified pdfjs file to speedup build process
diff --git a/swh/web/assets/src/bundles/browse/content.css b/swh/web/assets/src/bundles/browse/content.css
index adcaf49a0716b05664cd4eecefa1c694e51a881b..58f29e81e9a81a971a59b463087825d93a500163 100644
--- a/swh/web/assets/src/bundles/browse/content.css
+++ b/swh/web/assets/src/bundles/browse/content.css
@@ -21,3 +21,32 @@
 .swh-content svg {
     max-width: 100%;
 }
+
+.chosen-container {
+    color: #444;
+}
+
+.chosen-container .chosen-single div b {
+    margin-top: 2px;
+    filter: brightness(0%);
+}
+
+.chosen-container .chosen-single {
+    height: 31px;
+    line-height: 1.5;
+    border-radius: 0px;
+    background-color: #f4f4f4;
+    border-color: #ddd;
+    background-image:none;
+    -webkit-box-shadow: none;
+	-moz-box-shadow: none;
+    box-shadow: none;
+    font-weight: 400;
+    font-size: 14px;
+    padding-top: 4px;
+    padding-bottom: 4px;
+}
+
+.chosen-container .chosen-single:hover {
+    background-color: #e7e7e7;
+}
diff --git a/swh/web/assets/src/bundles/browse/origin-search.js b/swh/web/assets/src/bundles/browse/origin-search.js
index 78a9bafa3f02c56d4723ef8f34d64f00deae8ff5..c5e68325fcb19aa8e91c6524efe6a703bb14477e 100644
--- a/swh/web/assets/src/bundles/browse/origin-search.js
+++ b/swh/web/assets/src/bundles/browse/origin-search.js
@@ -43,7 +43,7 @@ function populateOriginSearchResultsTable(origins, offset) {
       table.append(tableRow);
       // get async latest visit snapshot and update visit status icon
       let latestSnapshotUrl = Urls.api_1_origin_visit_latest(origin.url);
-      latestSnapshotUrl += "?require_snapshot=true";
+      latestSnapshotUrl += '?require_snapshot=true';
       fetch(latestSnapshotUrl)
         .then(response => response.json())
         .then(data => {
diff --git a/swh/web/assets/src/bundles/vendors/index.js b/swh/web/assets/src/bundles/vendors/index.js
index 11a3e44a1d21f68b94b2b1afca28d0bd2cd6d253..91fb058ce3528f77bdc29449fc3e7a028b39d8d4 100644
--- a/swh/web/assets/src/bundles/vendors/index.js
+++ b/swh/web/assets/src/bundles/vendors/index.js
@@ -31,6 +31,12 @@ import 'datatables.net-bs4/css/dataTables.bootstrap4.css';
 import 'datatables.net-responsive-bs4/css/responsive.bootstrap4.css';
 import './datatables.css';
 
+// chosen-js
+import 'chosen-js';
+import 'chosen-js/chosen.min.css';
+import 'chosen-js/chosen-sprite.png';
+import 'chosen-js/chosen-sprite@2x.png';
+
 // iframe-resizer
 import 'iframe-resizer';
 
diff --git a/swh/web/browse/views/content.py b/swh/web/browse/views/content.py
index d91f9d5843fd7e62f1a39b9349720dd34f0dce75..4fd9361bec162023e9efb0a6aacb5034c055c320 100644
--- a/swh/web/browse/views/content.py
+++ b/swh/web/browse/views/content.py
@@ -14,7 +14,7 @@ from django.template.defaultfilters import filesizeformat
 
 from swh.model.hashutil import hash_to_hex
 
-from swh.web.common import query, service
+from swh.web.common import query, service, highlightjs
 from swh.web.common.utils import (
     reverse, gen_path_info, swh_object_icons
 )
@@ -181,6 +181,8 @@ def content_display(request, query_string):
                                        raise_if_unavailable=False)
         origin_type = request.GET.get('origin_type', None)
         origin_url = request.GET.get('origin_url', None)
+        selected_language = request.GET.get('language', None)
+
         if not origin_url:
             origin_url = request.GET.get('origin', None)
         snapshot_context = None
@@ -218,6 +220,15 @@ def content_display(request, query_string):
         language = content_display_data['language']
         mimetype = content_display_data['mimetype']
 
+    # Override language with user-selected language
+    if selected_language is not None:
+        language = selected_language
+
+    available_languages = None
+
+    if mimetype and 'text/' in mimetype:
+        available_languages = highlightjs._hljs_languages
+
     root_dir = None
     filename = None
     path_info = None
@@ -303,6 +314,7 @@ def content_display(request, query_string):
                    'max_content_size': content_display_max_size,
                    'mimetype': mimetype,
                    'language': language,
+                   'available_languages': available_languages,
                    'breadcrumbs': breadcrumbs,
                    'top_right_link': {
                         'url': content_raw_url,
diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py
index 7a55a06ab1988e0acdff393eeef4f423aa3c2b02..b03cb0e28fdddbeb9e0a8f55f26a64e6c539ebf5 100644
--- a/swh/web/browse/views/origin.py
+++ b/swh/web/browse/views/origin.py
@@ -79,9 +79,10 @@ def origin_content_browse(request, origin_url, origin_type=None, path=None,
         * :http:get:`/browse/origin/[(origin_type)/url/](origin_url)/visit/(timestamp)/content/(path)/`
 
     """ # noqa
+    language = request.GET.get('language', None)
     return browse_snapshot_content(request, origin_type=origin_type,
                                    origin_url=origin_url, timestamp=timestamp,
-                                   path=path)
+                                   path=path, selected_language=language)
 
 
 PER_PAGE = 20
diff --git a/swh/web/browse/views/snapshot.py b/swh/web/browse/views/snapshot.py
index 25be99327bd0eb8e75080296358d3f88049ac347..e40a50963f7e413938c8a57d4a04206613ba6cc2 100644
--- a/swh/web/browse/views/snapshot.py
+++ b/swh/web/browse/views/snapshot.py
@@ -60,7 +60,9 @@ def snapshot_content_browse(request, snapshot_id, path):
     The url that points to it is
         :http:get:`/browse/snapshot/(snapshot_id)/content/(path)/`
     """
-    return browse_snapshot_content(request, snapshot_id=snapshot_id, path=path)
+    language = request.GET.get('language', None)
+    return browse_snapshot_content(request, snapshot_id=snapshot_id, path=path,
+                                   selected_language=language)
 
 
 @browse_route(r'snapshot/(?P<snapshot_id>[0-9a-f]+)/log/',
diff --git a/swh/web/browse/views/utils/snapshot_context.py b/swh/web/browse/views/utils/snapshot_context.py
index 60e8180af0074387e334f56b856917f1ad441363..3a22f80375a14a7a976e222e7e0db020f262930f 100644
--- a/swh/web/browse/views/utils/snapshot_context.py
+++ b/swh/web/browse/views/utils/snapshot_context.py
@@ -23,7 +23,7 @@ from swh.web.browse.utils import (
     gen_snapshot_link, process_snapshot_branches
 )
 
-from swh.web.common import service
+from swh.web.common import service, highlightjs
 from swh.web.common.exc import (
     handle_view_exception, NotFoundExc
 )
@@ -399,7 +399,8 @@ def browse_snapshot_directory(request, snapshot_id=None, origin_type=None,
 
 
 def browse_snapshot_content(request, snapshot_id=None, origin_type=None,
-                            origin_url=None, timestamp=None, path=None):
+                            origin_url=None, timestamp=None, path=None,
+                            selected_language=None):
     """
     Django view implementation for browsing a content in a snapshot context.
     """
@@ -453,6 +454,15 @@ def browse_snapshot_content(request, snapshot_id=None, origin_type=None,
         language = content_display_data['language']
         mimetype = content_display_data['mimetype']
 
+    # Override language with user-selected language
+    if selected_language is not None:
+        language = selected_language
+
+    available_languages = None
+
+    if mimetype and 'text/' in mimetype:
+        available_languages = highlightjs._hljs_languages
+
     browse_view_name = 'browse-' + swh_type + '-directory'
 
     breadcrumbs = []
@@ -562,6 +572,7 @@ def browse_snapshot_content(request, snapshot_id=None, origin_type=None,
                    'max_content_size': content_display_max_size,
                    'mimetype': mimetype,
                    'language': language,
+                   'available_languages': available_languages,
                    'breadcrumbs': breadcrumbs if root_sha1_git else [],
                    'top_right_link': {
                         'url': content_raw_url,
diff --git a/swh/web/templates/includes/content-display.html b/swh/web/templates/includes/content-display.html
index cad9f23b6a3b911ce9f87a797f742fca98d3ac55..4a625521546eccae62132b94f937e1f8f933ef41 100644
--- a/swh/web/templates/includes/content-display.html
+++ b/swh/web/templates/includes/content-display.html
@@ -53,7 +53,25 @@ See top-level LICENSE file for more information
     {% elif swh_object_metadata.filename and swh_object_metadata.filename|default:""|slice:"-5:" == "ipynb" %}
       swh.webapp.renderNotebook({{ top_right_link.url|jsonify }}, '.swh-ipynb');
     {% elif content %}
+      let codeContainer = $('code');
+      let content = codeContainer.text();
+
       swh.webapp.highlightCode();
+
+      function updateLanguage(language) {
+        codeContainer.text(content);
+        codeContainer.removeClass();
+        codeContainer.addClass(language);
+
+        let urlParams = new URLSearchParams(window.location.search);
+        urlParams.set('language', language);
+
+        const newUrl = window.location.pathname + '?' + urlParams.toString() + window.location.hash;
+        window.history.replaceState('', document.title, newUrl);
+
+        swh.webapp.highlightCode();
+      }
+
     {% endif %}
   </script>
 {% endif %}
diff --git a/swh/web/templates/includes/top-navigation.html b/swh/web/templates/includes/top-navigation.html
index 94a2a3bb3e35a74c57de582c79e98cc656b6777c..21c2bf9a046f6c2fb6495ca64e813573ac2084c8 100644
--- a/swh/web/templates/includes/top-navigation.html
+++ b/swh/web/templates/includes/top-navigation.html
@@ -94,6 +94,14 @@ See top-level LICENSE file for more information
         {{ top_right_link.text }}
       </a>
     {% endif %}
+    {% if available_languages %}
+      <select data-placeholder="Select Language" class="language-select chosen-select">
+        <option value=""></option>
+        {% for lang in available_languages %}
+          <option value="{{ lang }}">{{ lang }}</option>
+        {% endfor %}
+      </select>
+    {% endif %}
     {% if show_actions_menu %}
       <button class="btn btn-default btn-sm dropdown-toggle" type="button" data-toggle="dropdown">
         <i class="fa fa-bars fa-fw" aria-hidden="true"></i>Actions
@@ -122,5 +130,11 @@ See top-level LICENSE file for more information
     snapshotContext = true;
     branch = "{{ snapshot_context.branch|escape }}";
   {% endif %}
+  {% if available_languages %}
+    $(".chosen-select").val("{{ language }}");
+    $(".chosen-select").chosen().change(function(event, params) {
+      updateLanguage(params.selected);
+    });
+  {% endif %}
   swh.browse.initSnapshotNavigation(snapshotContext, branch !== "None");
 </script>
diff --git a/yarn.lock b/yarn.lock
index c234062f1d0e3efd7f327deacd207bcf437344ca..78b94a756449235af0cf3a865641a0e7bed97736 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2005,6 +2005,11 @@ chokidar@^2.0.2, chokidar@^2.1.5, chokidar@^2.1.6:
   optionalDependencies:
     fsevents "^1.2.7"
 
+chosen-js@^1.8.7:
+  version "1.8.7"
+  resolved "https://registry.yarnpkg.com/chosen-js/-/chosen-js-1.8.7.tgz#9bfa5597f5081d602ff4ae904af9aef33265bb1d"
+  integrity sha512-eVdrZJ2U5ISdObkgsi0od5vIJdLwq1P1Xa/Vj/mgxkMZf14DlgobfB6nrlFi3kW4kkvKLsKk4NDqZj1MU1DCpw==
+
 chownr@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"