Index: /trunk/data/tag2link.sparql
===================================================================
--- /trunk/data/tag2link.sparql	(revision 15677)
+++ /trunk/data/tag2link.sparql	(revision 15677)
@@ -0,0 +1,11 @@
+SELECT ?OSM_key ?formatter_URL WHERE {
+  { ?item wdt:P1282 ?OSM_key. }
+  FILTER(STRSTARTS(?OSM_key, 'Key:')) .
+
+  {
+    { ?item wdt:P1630 ?formatter_URL. }
+    UNION
+    { ?item wdt:P3303 ?formatter_URL. }
+  }
+}
+ORDER BY LCASE(?OSM_tag_or_key)
Index: /trunk/src/org/openstreetmap/josm/gui/MainInitialization.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/MainInitialization.java	(revision 15676)
+++ /trunk/src/org/openstreetmap/josm/gui/MainInitialization.java	(revision 15677)
@@ -26,4 +26,5 @@
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
 import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.gui.util.Tag2Link;
 import org.openstreetmap.josm.io.FileWatcher;
 import org.openstreetmap.josm.io.OsmApi;
@@ -117,4 +118,5 @@
             new InitializationTask(tr("Initializing presets"), TaggingPresets::initialize),
             new InitializationTask(tr("Initializing map styles"), MapPaintPreference::initialize),
+            new InitializationTask(tr("Initializing Tag2Link rules"), Tag2Link::initialize),
             new InitializationTask(tr("Loading imagery preferences"), ImageryPreference::initialize)
         );
Index: /trunk/src/org/openstreetmap/josm/gui/util/Tag2Link.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/util/Tag2Link.java	(revision 15676)
+++ /trunk/src/org/openstreetmap/josm/gui/util/Tag2Link.java	(revision 15677)
@@ -3,9 +3,23 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
+import static org.openstreetmap.josm.tools.I18n.trn;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonValue;
+
+import com.drew.lang.Charsets;
 import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.io.CachedFile;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.MultiMap;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -13,10 +27,15 @@
  * Extracts web links from OSM tags.
  *
- * @since xxx
+ * @since 15673
  */
-final class Tag2Link {
+public final class Tag2Link {
 
     // Related implementations:
     // - https://github.com/openstreetmap/openstreetmap-website/blob/master/app/helpers/browse_tags_helper.rb
+
+    /**
+     * Maps OSM keys to formatter URLs from Wikidata where {@code "$1"} has to be replaced by a value.
+     */
+    protected static MultiMap<String, String> wikidataRules = new MultiMap<>();
 
     private Tag2Link() {
@@ -27,4 +46,48 @@
     interface LinkConsumer {
         void acceptLink(String name, String url);
+    }
+
+    /**
+     * Initializes the tag2link rules
+     */
+    public static void initialize() {
+        try {
+            fetchRulesFromWikidata();
+        } catch (Exception e) {
+            Logging.error("Failed to initialize tag2link rules");
+            Logging.error(e);
+        }
+    }
+
+    /**
+     * Fetches rules from Wikidata using a SPARQL query.
+     *
+     * @throws IOException in case of I/O error
+     */
+    private static void fetchRulesFromWikidata() throws IOException {
+        final String sparql = new String(new CachedFile("resource://data/tag2link.sparql").getByteContent(), Charsets.UTF_8);
+        final CachedFile sparqlFile = new CachedFile("https://query.wikidata.org/sparql?query=" + Utils.encodeUrl(sparql))
+                .setHttpAccept("application/json");
+
+        final JsonArray rules;
+        try (BufferedReader reader = sparqlFile.getContentReader()) {
+            rules = Json.createReader(reader).read().asJsonObject().getJsonObject("results").getJsonArray("bindings");
+        }
+
+        for (JsonValue rule : rules) {
+            final String key = rule.asJsonObject().getJsonObject("OSM_key").getString("value");
+            final String url = rule.asJsonObject().getJsonObject("formatter_URL").getString("value");
+            if (key.startsWith("Key:")) {
+                wikidataRules.put(key.substring("Key:".length()), url);
+            }
+        }
+        // We handle those keys ourselves
+        Stream.of("image", "url", "website", "wikidata", "wikimedia_commons")
+                .forEach(wikidataRules::remove);
+
+        Logging.info(trn(
+                "Obtained {0} Tag2Link rule from {1}",
+                "Obtained {0} Tag2Link rules from {1}",
+                wikidataRules.size(), wikidataRules.size(), "Wikidata"));
     }
 
@@ -41,11 +104,11 @@
         final boolean valueIsURL = value.matches("^(http:|https:|www\\.).*");
         if (key.matches("^(.+[:_])?website([:_].+)?$") && valueIsURL) {
-            linkConsumer.acceptLink(tr("View website"), value);
+            linkConsumer.acceptLink(getLinkName(value, key), value);
         }
         if (key.matches("^(.+[:_])?source([:_].+)?$") && valueIsURL) {
-            linkConsumer.acceptLink(tr("View website"), value);
+            linkConsumer.acceptLink(getLinkName(value, key), value);
         }
         if (key.matches("^(.+[:_])?url([:_].+)?$") && valueIsURL) {
-            linkConsumer.acceptLink(tr("View URL"), value);
+            linkConsumer.acceptLink(getLinkName(value, key), value);
         }
         if (key.matches("image") && valueIsURL) {
@@ -61,8 +124,9 @@
         if (key.matches("(.*:)?wikidata")) {
             OsmUtils.splitMultipleValues(value)
-                    .forEach(q -> linkConsumer.acceptLink(tr("View Wikidata item"), "https://www.wikidata.org/wiki/" + q));
+                    .forEach(q -> linkConsumer.acceptLink(tr("View Wikidata item {0}", q), "https://www.wikidata.org/wiki/" + q));
         }
-        if (key.matches("species")) {
-            linkConsumer.acceptLink(tr("View Wikispecies page"), "https://species.wikimedia.org/wiki/" + value);
+        if (key.matches("(.*:)?species")) {
+            final String url = "https://species.wikimedia.org/wiki/" + value;
+            linkConsumer.acceptLink(getLinkName(url, key), url);
         }
         if (key.matches("wikimedia_commons|image") && value.matches("(?i:File):.*")) {
@@ -73,19 +137,15 @@
         }
 
-        // WHC
-        if (key.matches("ref:whc") && (valueMatcher = Pattern.compile("(?<id>[0-9]+)(-.*)?").matcher(value)).matches()) {
-            linkConsumer.acceptLink(tr("View UNESCO sheet"), "http://whc.unesco.org/en/list/" + valueMatcher.group("id"));
-        }
+        wikidataRules.getValues(key).forEach(urlFormatter -> {
+            final String url = urlFormatter.replace("$1", value);
+            linkConsumer.acceptLink(getLinkName(url, key), url);
+        });
+    }
 
-        // Mapillary
-        if (key.matches("((ref|source):)?mapillary") && value.matches("[0-9a-zA-Z-_]+")) {
-            linkConsumer.acceptLink(tr("View {0} image", "Mapillary"), "https://www.mapillary.com/map/im/" + value);
-        }
-
-        // MMSI
-        if (key.matches("seamark:(virtual_aton|radio_station):mmsi") && value.matches("[0-9]+")) {
-            // https://en.wikipedia.org/wiki/Maritime_Mobile_Service_Identity
-            linkConsumer.acceptLink(tr("View MMSI on MarineTraffic"),
-                    "https://www.marinetraffic.com/en/ais/details/ships/shipid:/mmsi:" + value);
+    private static String getLinkName(String url, String fallback) {
+        try {
+            return tr("Open {0}", new URL(url).getHost());
+        } catch (MalformedURLException e) {
+            return tr("Open {0}", fallback);
         }
     }
Index: /trunk/test/unit/org/openstreetmap/josm/gui/util/Tag2LinkTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/gui/util/Tag2LinkTest.java	(revision 15676)
+++ /trunk/test/unit/org/openstreetmap/josm/gui/util/Tag2LinkTest.java	(revision 15677)
@@ -48,5 +48,5 @@
     public void testBrandWikidata() {
         Tag2Link.getLinksForTag("brand:wikidata", "Q259340", this::addLink);
-        checkLinks("View Wikidata item // https://www.wikidata.org/wiki/Q259340");
+        checkLinks("View Wikidata item Q259340 // https://www.wikidata.org/wiki/Q259340");
     }
 
@@ -57,6 +57,6 @@
     public void testArchipelagoWikidata() {
         Tag2Link.getLinksForTag("archipelago:wikidata", "Q756987;Q756988", this::addLink);
-        checkLinks("View Wikidata item // https://www.wikidata.org/wiki/Q756987",
-                "View Wikidata item // https://www.wikidata.org/wiki/Q756988");
+        checkLinks("View Wikidata item Q756987 // https://www.wikidata.org/wiki/Q756987",
+                "View Wikidata item Q756988 // https://www.wikidata.org/wiki/Q756988");
     }
 
