Skip to content

osm-americana/diplomat

Diplomat

Diplomat prepares your interactive map for an international audience. With a few lines of code, your MapLibre GL JS–powered map will speak the user’s preferred language while informing them about local languages the world over.

Before After
Navajo Nation;Naabeehó Bináhásdzo Nación Navajo (Navajo Nation • Naabeehó Bináhásdzo)
North Sea / Nordsee / Noordzee / Nordsøen / Nordsjøen / Mer du Nord Mare Germanicum
Ross Avenue;Tennessee Avenue at Rhode Island Avenue;Section Avenue Ross Avenue • Tennessee Avenue at Rhode Island Avenue • Section Avenue

Features

Diplomat gives all parties a win-win:

  • Tailors labels to the user’s preferred language.
  • Respects multilingualism with optional dual language labels: both the user’s preferred language and the local native language.
  • Recognizes any language, dialect, or script out of the box.

Diplomat lets your designer save face:

  • Uses space efficiently with both multiline and inline label layouts.
  • Deduplicates names within a label to avoid clutter.
  • Preserves diacritics in English exonyms where appropriate.
  • Respects right-to-left and vertical text layouts.

Diplomat works quietly behind the scenes:

  • Changes the style on the fly at runtime – no need to publish a new style or tileset for every language.
  • You choose which style layers to localize, or localize them all automatically.
  • Supports multiple popular vector tile schemas without hacky workarounds, plus custom vector tilesets and GeoJSON sources.

All localization is subject to the availability of localized names in the map data source. See the caveats for more details.

Requirements

Diplomat is compatible with applications using MapLibre GL JS v5.13.0 and above.

The stylesheet must use the newer expression syntax; legacy style functions are not supported. The stylesheet’s sources must conform to Diplomat’s schema. Several popular vector tilesets already conform to this schema, including:

With additional configuration, Diplomat supports even more vector tilesets, including:

Installation

This plugin is available as an NPM package. To install it, run the following command:

npm install @americana/diplomat

Alternatively, you can include the plugin as a standalone script from a CDN such as unpkg.

Usage

Import Diplomat as a module:

import * as Diplomat from "@americana/diplomat";

After creating an instance of maplibregl.Map, register an event listener for the styledata event that localizes the map:

map.once("styledata", (event) => {
  Diplomat.localizeStyle(map);
});

If your stylesheet uses a tileset that formats the name keys differently, such as OpenHistoricalMap or Shortbread, set the format when localizing the layers, for example:

map.once("styledata", (event) => {
  let locales = Diplomat.getLocales();
  Diplomat.localizeStyle(map, locales, {
    localizedNamePropertyFormat: "name_$1",
  });
});

If you set the hash option to a string when creating the Map, you can have this code respond to a language parameter in the URL hash. Add a window event listener for whenever the hash changes, in order to update the layers:

addEventListener("hashchange", (event) => {
  let oldLanguage = Diplomat.getLanguageFromURL(new URL(event.oldURL));
  let newLanguage = Diplomat.getLanguageFromURL(new URL(event.newURL));

  if (oldLanguage !== newLanguage) {
    Diplomat.localizeStyle(map);
  }
});

Similarly, you can immediately respond to any change the user makes to their browser language preference in real time:

addEventListener("languagechange", (event) => {
  Diplomat.localizeStyle(map);
});

Note

By default, MapLibre GL JS does not support bidirectional text. Arabic, Hebrew, and other right-to-left languages will be unreadable unless you install the mapbox-gl-rtl-text plugin.

Schema

Diplomat can manipulate any GeoJSON or vector tile source, as long as it includes the following properties on each feature:

  • name (string): The name in the local or official language. You can customize this property by setting the unlocalizedNameProperty option when calling maplibregl.Map.prototype.localizeStyle().
  • name:xyz (string): The name in another language, where xyz is a valid IETF language tag. For example, name:zh for Chinese, name:zh-Hant for Traditional Chinese, name:zh-Hant-TW for Traditional Chinese (Taiwan), and name:zh-Latn-pinyin for Chinese in pinyin. You can customize this format by setting the localizedNamePropertyFormat option when calling maplibregl.Map.prototype.localizeStyle().

For compatibility with the OpenMapTiles schema, name_en and name_de are also recognized as alternatives to name:en and name:de for English and German, respectively, but only in the transportation_name layer. For performance reasons, Diplomat does not look for this format by default for any other language or layer.

Each of the supported properties may be set to a list of values separated by semicolons. For example, if a place speaks both English and French, name should be English Name;French Name. Similarly, if a landmark has three equally common names in Spanish, regardless of dialect, name:es should be Nombre Uno;Nombre Dos;Nombre Tres. In the rare case that a single name contains a semicolon, it should be escaped as a double semicolon (;;).

API

This plugin adds several symbols to a maplibregl.Diplomat namespace and adds a single method to each instance of maplibregl.Map. The following documentation uses the notation maplibregl.Diplomat.* in case you include Diplomat as a script. However, if you import Diplomat as a module, these symbols are directly imported into your code, without any namespacing.

maplibregl.Diplomat.localizedName

An expression that produces the names in the user's preferred language, each on a separate line.

Use this constant if you are building the entire stylesheet programmatically before initializing a maplibregl.Map and want more fine-grained control over which layers have which label layout than maplibregl.Map.prototype.localizeStyle() provides.

This expression is appropriate for labeling a type of feature that almost always has a familiar translation in the user’s preferred language, such as the name of a country. It is also appropriate for minor features like points of interest, for which an extra local-language gloss would clutter the map.

Example:

map.setLayoutProperty(
  "country-labels",
  "text-field",
  maplibregl.Diplomat.localizedName,
);

maplibregl.Diplomat.localizedNameInline

An expression that produces the names in the user's preferred language, all on the same line.

Use this constant if you are building the entire stylesheet programmatically before initializing a maplibregl.Map and want more fine-grained control over which layers have which label layout than maplibregl.Map.prototype.localizeStyle() provides.

This expression is appropriate for labeling a linear feature, such as a road or waterway. The symbol layer’s symbol-placement layout property should be set to either line or line-center.

Example:

map.setLayoutProperty(
  "road-labels",
  "text-field",
  maplibregl.Diplomat.localizedNameInline,
);

maplibre.Diplomat.localizedNameWithLocalGloss

An expression that produces the name in the user's preferred language, followed by the name in the local language in parentheses if it differs.

Use this constant if you are building the entire stylesheet programmatically before initializing a maplibregl.Map and want more fine-grained control over which layers have which label layout than maplibregl.Map.prototype.localizeStyle() provides.

This expression is appropriate for labeling a type of feature that is only sometimes translated into user’s preferred language, such as the name of a city or town. The extra local-language gloss respects local customs and keeps the user informed, but it can also risk information overload and crowd out other useful labels.

Example:

map.setLayoutProperty(
  "city-labels",
  "text-field",
  maplibregl.Diplomat.localizedNameWithGloss,
);

maplibregl.Diplomat.getLocalizedCountryNameExpression()

Returns an expression that converts the given country code to a human-readable name in the user's preferred language.

This method is useful for stylesheets powered by OpenMapTiles, which only provides the ISO 3166-1 alpha-3 code of the country on either side of a boundary, but not the full country name in any language.

Parameters:

  • code (string): An expression that evaluates to an ISO 3166-1 alpha-3 country code.

Example:

map.setLayoutProperty(
  "boundary-edge-labels",
  "text-field",
  maplibregl.Diplomat.getLocalizedCountryNameExpression(["get", "adm0_l"]),
);

Note

Use maplibregl.Diplomat.getGlobalStateForLocalization() to populate the global state required by this expression, then call maplibregl.Map.prototype.localizeStyle(). Otherwise, this expression evaluates to the raw country code.

maplibregl.Diplomat.getGlobalStateForLocalization()

Returns the global state that Diplomat needs to fully localize the style.

If you are building a stylesheet programmatically, you can use this method to populate a state property at the root of the stylesheet before initializing a maplibregl.Map. You can add additional global state properties besides the ones that come from this object, as long as you avoid making a deep clone of the object.

If your stylesheet is powered by OpenMapTiles, you need to set this global state object in order to localizing boundary edge labels that come from the boundary layer. Otherwise, the user will see only ISO 3166-1 alpha-3 codes, because OpenMapTiles only provides these codes instead of the full country name on either side of a boundary.

Parameters:

  • locales (string): The locales for formatting the country names.
  • options.uppercaseCountryNames (boolean): Whether to write the country names in all uppercase, respecting the locale’s case conventions. Enable this option if you intend to display the boundary edge labels in uppercase, because the upcase expression operator is locale-insensitive.

Example:

map.once("styledata", (event) => {
  let localizationState = maplibregl.Diplomat.getGlobalStateForLocalization(
    getLocales(),
    {
      uppercaseCountryNames: true,
    },
  );
  for (let [key, value] of Object.entries(localizationState)) {
    map.setGlobalStateProperty(key, value);
  }
});

maplibregl.Diplomat.getLocales()

Returns the languages that the user prefers.

Example:

maplibregl.Diplomat.getLocales().includes("en");

maplibregl.Diplomat.localizeStyle()

Updates each style layer's text-field value to match the given locales, upgrading any unlocalizable layer along the way.

This function ugprades unlocalizable layers to localized multiline or inline labels depending on the symbol-placement layout property. To add a dual language label to a layer, set its text-field layout property manually using the maplibregl.Diplomat.localizedNameWithLocalGloss constant.

Parameters:

  • map (maplibregl.Map): The map to localize.
  • locales ([string]): The locales to insert into each layer, as a comma-separated list of IETF language tags. Uses the language URL hash parameter or browser preferences by default.
  • options (object):
    • layers ([string]): If specified, only these style layers will be made localizable. Otherwise, any style layer that uses the unlocalized name property will be made localizable.
    • unlocalizedNameProperty (string): The name of the property holding the unlocalized name. name by default.
    • localizedNamePropertyFormat (string): "The format of properties holding localized names, where $1 is replaced by an IETF language tag. name:$1 by default.
    • options.uppercaseCountryNames (boolean): Whether to write country names in all uppercase, respecting the locale’s case conventions.

Example:

maplibregl.Diplomat.localizeStyle(map, ["eo"], {
  layers: ["place-labels"],
});

maplibregl.Map.prototype.localizeStyle()

A wrapper for maplibregl.Diplomat.localizeStyle() that does not require passing in a maplibregl.Map. Only available when including Diplomat as a script.

Example:

map.localizeStyle(["eo"], {
  localizedNamePropertyFormat: "name_$1",
});

Caveats

Diplomat only switches between languages that are present in the stylesheet’s data sources. It does not fetch translations from other sources or generate its own transliterations. By convention, OpenStreetMap’s coverage in some regions is largely limited to locally spoken languages. If you need more comprehensive coverage in a given language, consider using a tileset that combines names from OpenStreetMap and Wikidata, such as the OpenStreetMap U.S. Tileservice.

By default, MapLibre GL JS does not support bidirectional text. Arabic, Hebrew, and other right-to-left languages will be unreadable unless you install the mapbox-gl-rtl-text plugin.

Diplomat only performs basic language fallbacks according to the ICU locale fallback algorithm. It makes no attempt to fallback to a related but distinct language code, for example from sr-Cyrl to ru or from nb to no. Instead, the user can set their preferred languages in their browser or operating system settings.

For historical reasons, OpenStreetMap’s coverage in many reasons encodes multiple local names separated by human-readable punctuation. Diplomat makes no attempt to guess which punctuation is part of a name and which punctuation delimits two names.

Acknowledgments

This plugin was spun out of the OpenStreetMap Americana project. It was originally inspired by Mapbox GL Language.