Skip to content

Set Map Labels from the Browser Language in MapLibre GL JS

A map that labels Vienna as “Wien” inside an English interface looks unfinished. The Maptoolkit vector tiles carry a name field per language, so matching the map to the visitor means reading the browser’s preference once and pointing every symbol layer at the right field. This example does that in MapLibre GL JS and keeps a manual switch, because the guess is sometimes wrong.

const API_KEY = "YOUR_API_KEY";

    // The languages the tiles carry names in. Anything else falls back to local names.
    const LANGUAGES = { de: "Deutsch", en: "English", es: "Español", fr: "Français", it: "Italiano", ru: "Русский",
      zh: "中文", ja: "日本語", ko: "한국어", pl: "Polski", cs: "Čeština", hu: "Magyar", ar: "العربية", hi: "हिन्दी" };

    // The first preferred language the tiles support. "de-AT" and "de" both give "de".
    function detectLanguage() {
      for (const tag of navigator.languages || [navigator.language]) {
        const base = tag?.toLowerCase().split("-")[0];
        if (base in LANGUAGES) return base;
      }
      return null;
    }

    const map = new maplibregl.Map({
      container: "map",
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [10, 50],
      zoom: 4,
      attributionControl: { compact: false },
    });
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    function setLanguage(code) {
      // The translated name where the feature has one, else the local name. null means local names.
      const field = code ? ["coalesce", ["get", `name_${code}`], ["get", "name"]] : ["get", "name"];
      for (const layer of map.getStyle().layers) {
        // Only symbol layers that already write text; icon-only layers stay as they are.
        if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
          map.setLayoutProperty(layer.id, "text-field", field);
        }
      }
    }

    map.once("style.load", () => {
      const detected = detectLanguage();
      setLanguage(detected);

      const options = [["", "Local names"], ...Object.entries(LANGUAGES)]
        .map(([code, name]) => `<option value="${code}"${code === (detected || "") ? " selected" : ""}>${name}</option>`)
        .join("");
      const panel = document.getElementById("panel");
      panel.innerHTML = '<div class="detected"></div><select id="language"></select>';
      panel.querySelector(".detected").textContent = `Browser: ${(navigator.languages || []).join(", ") || "unknown"}`;
      panel.querySelector("select").innerHTML = options;
      panel.querySelector("select").addEventListener("change", (event) => setLanguage(event.target.value || null));
    });
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.css" />
  <script src="https://cdn.jsdelivr.net/npm/maplibre-gl@5.6.1/dist/maplibre-gl.js"></script>
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }
    #panel {
      position: absolute; top: 12px; left: 12px; z-index: 1; width: 210px; padding: 10px 12px;
      background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(20, 30, 60, 0.18);
      font: 13px/1.4 system-ui, sans-serif; color: #1f2430;
    }
    #panel .detected { margin-bottom: 6px; color: #6b7185; font-size: 12px; word-break: break-word; }
    #panel select { width: 100%; padding: 5px; border: 1px solid #d0d4e0; border-radius: 6px; font: inherit; }
  </style>
</head>
<body>
  <div id="map"></div>
  <div id="panel">Loading...</div>
  <script>
    const API_KEY = "YOUR_API_KEY";

    // The languages the tiles carry names in. Anything else falls back to local names.
    const LANGUAGES = { de: "Deutsch", en: "English", es: "Español", fr: "Français", it: "Italiano", ru: "Русский",
      zh: "中文", ja: "日本語", ko: "한국어", pl: "Polski", cs: "Čeština", hu: "Magyar", ar: "العربية", hi: "हिन्दी" };

    // The first preferred language the tiles support. "de-AT" and "de" both give "de".
    function detectLanguage() {
      for (const tag of navigator.languages || [navigator.language]) {
        const base = tag?.toLowerCase().split("-")[0];
        if (base in LANGUAGES) return base;
      }
      return null;
    }

    const map = new maplibregl.Map({
      container: "map",
      style: `https://styles.maptoolkit.net/maptoolkit/maptoolkit.summer.json?api_key=${API_KEY}`,
      center: [10, 50],
      zoom: 4,
      attributionControl: { compact: false },
    });
    map.addControl(new maplibregl.NavigationControl(), "top-right");

    function setLanguage(code) {
      // The translated name where the feature has one, else the local name. null means local names.
      const field = code ? ["coalesce", ["get", `name_${code}`], ["get", "name"]] : ["get", "name"];
      for (const layer of map.getStyle().layers) {
        // Only symbol layers that already write text; icon-only layers stay as they are.
        if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
          map.setLayoutProperty(layer.id, "text-field", field);
        }
      }
    }

    map.once("style.load", () => {
      const detected = detectLanguage();
      setLanguage(detected);

      const options = [["", "Local names"], ...Object.entries(LANGUAGES)]
        .map(([code, name]) => `<option value="${code}"${code === (detected || "") ? " selected" : ""}>${name}</option>`)
        .join("");
      const panel = document.getElementById("panel");
      panel.innerHTML = '<div class="detected"></div><select id="language"></select>';
      panel.querySelector(".detected").textContent = `Browser: ${(navigator.languages || []).join(", ") || "unknown"}`;
      panel.querySelector("select").innerHTML = options;
      panel.querySelector("select").addEventListener("change", (event) => setLanguage(event.target.value || null));
    });
  </script>
</body>
</html>

Use the prompt below with any LLM to get the same result. Make sure the Maptoolkit MCP server is connected first — check out AI Integration & MCP to get started.

Use the Maptoolkit Connector. Create a MapLibre GL JS map of Europe with the Maptoolkit summer style. Detect the visitor’s language from navigator.languages, set the text-field of every labeled symbol layer to the matching name field with a fallback to the local name, and add a select to change the language by hand.

How it works

navigator.languages is a list. It holds the visitor’s preferences in order, so the loop takes the first one the tiles can serve. navigator.language alone would drop the fallbacks the visitor set up.

Tags are not codes. Browsers report de-AT, en-GB or pt-BR, and the tiles are keyed on the base language, so the region is cut off. name_de-AT does not exist, and the map would fall back to local names as if detection had failed.

Check against what the tiles have. A text-field pointing at name_sv, when the tiles carry no Swedish, gives empty labels rather than an error. LANGUAGES lists the name fields the tiles have, and anything else keeps the local names.

coalesce per feature. A capital has names in many languages, a village often only its own. ["coalesce", ["get", "name_en"], ["get", "name"]] uses the translation where there is one and the local name where there isn’t. Without it, whole zoom levels go blank in a less translated language.

Only labeled layers. A style has symbol layers that only draw icons. The loop skips layers without a text-field, so they don’t start drawing names.

The change is applied on style.load. If your page switches styles with setStyle(), apply the language again after each switch, because the new style comes with its own text-field values.

Labels are the easy part of localization: units, date formats and number separators have to follow too.

Next steps

To remember the choice, store it in localStorage, or in the URL so a shared link keeps the language. For the styles themselves, see Vector Tiles in MapLibre GL JS.

The same example in Maptoolkit Maps JS is Set Map Labels from the Browser Language.