Skip to content

Change the Map Language

Every Maptoolkit community style ships as a set of stylesheets — one with local place names, plus one per translation, for example summer.json (local names) and summer-en.json (English names). This map lets you switch the language of every place label — cities, water bodies, POIs, peaks — anywhere in the world, by loading the matching stylesheet with map.setStyle(). All 7 styles (summer, light, hiking, winter, dark, street, cycling) follow this same <style>-<language>.json naming.

Arabic (ar) is written right to left, which MapLibre GL JS does not support on its own. The example registers the RTL text plugin before it creates the map; without that line, Arabic labels appear with unconnected letters in the wrong order. The other 12 languages are not affected.
import * as maplibregl from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/maplibre-gl.mjs';
import { MaptoolkitLogoControl } from 'https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-gl-logo/+esm';

const LANGUAGES = [
  { label: 'Local', code: null },
  { label: 'العربية', code: 'ar' },
  { label: 'čeština', code: 'cs' },
  { label: 'Deutsch', code: 'de' },
  { label: 'English', code: 'en' },
  { label: 'Español', code: 'es' },
  { label: 'Français', code: 'fr' },
  { label: 'हिन्दी', code: 'hi' },
  { label: 'Magyar', code: 'hu' },
  { label: 'Italiano', code: 'it' },
  { label: '日本語', code: 'ja' },
  { label: '한국어', code: 'ko' },
  { label: 'Polski', code: 'pl' },
  { label: '中文', code: 'zh' }
];

// Every style is published per language, e.g. summer.json (local names) and
// summer-en.json (English names). The same <style>-<language>.json naming
// applies to all 7 Maptoolkit styles, so swap styleBase to switch this map
// to light, hiking, winter, dark, street, or cycling.
const styleBase = 'https://styles.maptoolkit.org/summer';

function styleUrl(code) {
  return code ? `${styleBase}-${code}.json` : `${styleBase}.json`;
}

const map = new maplibregl.Map({
  container: 'map', attributionControl: { customAttribution: '' },
  style: styleUrl(null),
  center: [17.71561, 47.64616], zoom: 3.6, bearing: 0, pitch: 0
});

// Arabic/Hebrew: MapLibre has no right-to-left support built in, this plugin adds it
maplibregl.setRTLTextPlugin('https://cdn.jsdelivr.net/npm/@mapbox/[email protected]/dist/mapbox-gl-rtl-text.js', false);

function applyLanguage(code) {
  map.setStyle(styleUrl(code));
}

// Dropdown UI: a trigger button showing the current language, and a panel
// listing every language with the stylesheet it loads.
(function initLangDropdown() {
  const root    = document.getElementById('mapLang');
  const trigger = root.querySelector('.map-lang-trigger');
  const panel   = root.querySelector('.map-lang-panel');
  const current = root.querySelector('.map-lang-current');

  LANGUAGES.forEach(function (lang, i) {
    const opt = document.createElement('button');
    opt.type = 'button';
    opt.className = 'map-lang-option' + (i === 0 ? ' active' : '');
    opt.role = 'option';
    opt.innerHTML = '<span></span><span class="map-lang-field"></span>';
    opt.children[0].textContent = lang.label;
    opt.children[1].textContent = styleUrl(lang.code).split('/').pop();
    opt.addEventListener('click', function () {
      current.textContent = lang.label;
      panel.querySelectorAll('.map-lang-option').forEach(function (o) { o.classList.toggle('active', o === opt); });
      root.classList.remove('open');
      trigger.setAttribute('aria-expanded', 'false');
      applyLanguage(lang.code);
    });
    panel.appendChild(opt);
  });

  trigger.addEventListener('click', function (e) {
    e.stopPropagation();
    const open = root.classList.toggle('open');
    trigger.setAttribute('aria-expanded', String(open));
  });
  document.addEventListener('click', function (e) {
    if (!root.contains(e.target)) {
      root.classList.remove('open');
      trigger.setAttribute('aria-expanded', 'false');
    }
  });
})();

map.addControl(new MaptoolkitLogoControl());
<!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/[email protected]/dist/maplibre-gl.css" />
  <style>
    html, body { width: 100%; height: 100%; margin: 0; padding: 0; }
    #map { width: 100%; height: 100%; }

    .map-lang { position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 1; font: 13px/1.2 system-ui, sans-serif; }
    .map-lang-trigger {
      display: inline-flex; align-items: center; gap: 6px;
      padding: 6px 10px; background: #fff; border: none; border-radius: 6px;
      font-weight: 700; font-size: 13px; color: #222; cursor: pointer;
      box-shadow: 0 1px 4px rgba(0,0,0,.3);
    }
    .map-lang-caret { width: 12px; height: 12px; color: #888; transition: transform .15s; }
    .map-lang.open .map-lang-caret { transform: rotate(180deg); }
    .map-lang-panel {
      position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%); min-width: 170px;
      background: #fff; border-radius: 8px; padding: 4px;
      box-shadow: 0 4px 14px rgba(0,0,0,.3);
      display: none; max-height: 280px; overflow-y: auto;
    }
    .map-lang.open .map-lang-panel { display: block; }
    .map-lang-option {
      display: flex; justify-content: space-between; align-items: center; width: 100%;
      background: transparent; border: none; padding: 6px 10px; border-radius: 6px;
      font-size: 12.5px; color: #222; cursor: pointer; text-align: left; gap: 16px;
    }
    .map-lang-option:hover { background: #f1f1f1; }
    .map-lang-option.active { background: #1a73e8; color: #fff; }
    .map-lang-option .map-lang-field { font-family: ui-monospace, monospace; font-size: 11px; color: #888; }
    .map-lang-option.active .map-lang-field { color: rgba(255,255,255,.7); }
  </style>
</head>
<body>
  <div class="map-lang" id="mapLang">
    <button class="map-lang-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
      <span class="map-lang-current">Local</span>
      <svg class="map-lang-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
    </button>
    <div class="map-lang-panel" role="listbox"></div>
  </div>
  <div id="map"></div>
  <script type="module">
import * as maplibregl from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/maplibre-gl.mjs';
import { MaptoolkitLogoControl } from 'https://cdn.jsdelivr.net/npm/@maptoolkit/maplibre-gl-logo/+esm';

const LANGUAGES = [
  { label: 'Local', code: null },
  { label: 'العربية', code: 'ar' },
  { label: 'čeština', code: 'cs' },
  { label: 'Deutsch', code: 'de' },
  { label: 'English', code: 'en' },
  { label: 'Español', code: 'es' },
  { label: 'Français', code: 'fr' },
  { label: 'हिन्दी', code: 'hi' },
  { label: 'Magyar', code: 'hu' },
  { label: 'Italiano', code: 'it' },
  { label: '日本語', code: 'ja' },
  { label: '한국어', code: 'ko' },
  { label: 'Polski', code: 'pl' },
  { label: '中文', code: 'zh' }
];

// Every style is published per language, e.g. summer.json (local names) and
// summer-en.json (English names). The same <style>-<language>.json naming
// applies to all 7 Maptoolkit styles, so swap styleBase to switch this map
// to light, hiking, winter, dark, street, or cycling.
const styleBase = 'https://styles.maptoolkit.org/summer';

function styleUrl(code) {
  return code ? `${styleBase}-${code}.json` : `${styleBase}.json`;
}

const map = new maplibregl.Map({
  container: 'map', attributionControl: { customAttribution: '' },
  style: styleUrl(null),
  center: [17.71561, 47.64616], zoom: 3.6, bearing: 0, pitch: 0
});

// Arabic/Hebrew: MapLibre has no right-to-left support built in, this plugin adds it
maplibregl.setRTLTextPlugin('https://cdn.jsdelivr.net/npm/@mapbox/[email protected]/dist/mapbox-gl-rtl-text.js', false);

function applyLanguage(code) {
  map.setStyle(styleUrl(code));
}

// Dropdown UI: a trigger button showing the current language, and a panel
// listing every language with the stylesheet it loads.
(function initLangDropdown() {
  const root    = document.getElementById('mapLang');
  const trigger = root.querySelector('.map-lang-trigger');
  const panel   = root.querySelector('.map-lang-panel');
  const current = root.querySelector('.map-lang-current');

  LANGUAGES.forEach(function (lang, i) {
    const opt = document.createElement('button');
    opt.type = 'button';
    opt.className = 'map-lang-option' + (i === 0 ? ' active' : '');
    opt.role = 'option';
    opt.innerHTML = '<span></span><span class="map-lang-field"></span>';
    opt.children[0].textContent = lang.label;
    opt.children[1].textContent = styleUrl(lang.code).split('/').pop();
    opt.addEventListener('click', function () {
      current.textContent = lang.label;
      panel.querySelectorAll('.map-lang-option').forEach(function (o) { o.classList.toggle('active', o === opt); });
      root.classList.remove('open');
      trigger.setAttribute('aria-expanded', 'false');
      applyLanguage(lang.code);
    });
    panel.appendChild(opt);
  });

  trigger.addEventListener('click', function (e) {
    e.stopPropagation();
    const open = root.classList.toggle('open');
    trigger.setAttribute('aria-expanded', String(open));
  });
  document.addEventListener('click', function (e) {
    if (!root.contains(e.target)) {
      root.classList.remove('open');
      trigger.setAttribute('aria-expanded', 'false');
    }
  });
})();

map.addControl(new MaptoolkitLogoControl());
  </script>
</body>
</html>