layers = mapboxMap.getLayers();
+ for (Source source : mapboxMap.getSources()) {
+ if (sourceIsFromMapbox(source)) {
+ for (Layer layer : layers) {
+ if (layerHasAdjustableTextField(layer)) {
+ String textField = ((SymbolLayer) layer).getTextField().getValue();
+ if (textField != null
+ && (textField.contains("{name") || textField.contains("{abbr}"))) {
+ textField = textField.replaceAll("[{]((name).*?)[}]",
+ String.format("{%s}", mapLocale.getMapLanguage()));
+ layer.setProperties(textField(textField));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * Camera bounding box
+ */
+
+ /**
+ * Adjust the map's camera position so that the entire countries boarders are within the viewport.
+ * Specifically, this method gets the devices currently set locale and adjust the map camera to
+ * view that country if a {@link MapLocale]} matches.
+ *
+ * @since 0.1.0
+ */
+ public void setCameraToLocaleCountry() {
+ setCameraToLocaleCountry(Locale.getDefault());
+ }
+
+ /**
+ * If you'd like to manually set the camera position to a specific map region or country, pass in
+ * the locale (which must have a paired }{@link MapLocale}) to work properly
+ *
+ * @param locale a {@link Locale} which has a complementary {@link MapLocale} for it
+ * @throws NullPointerException thrown when the locale passed into the method doesn't have a
+ * matching {@link MapLocale}
+ * @since 0.1.0
+ */
+ public void setCameraToLocaleCountry(Locale locale) {
+ setCameraToLocaleCountry(checkMapLocalNonNull(locale));
+ }
+
+ /**
+ * You can pass in a {@link MapLocale} directly into this method which uses the country bounds
+ * defined in it to represent the language found on the map.
+ *
+ * @param mapLocale he {@link MapLocale} object which contains the desired map bounds
+ * @throws NullPointerException thrown when it was expecting a {@link LatLngBounds} but instead
+ * it was null
+ * @since 0.1.0
+ */
+ public void setCameraToLocaleCountry(MapLocale mapLocale) {
+ LatLngBounds bounds = mapLocale.getCountryBounds();
+ if (bounds == null) {
+ throw new NullPointerException("Expected a LatLngBounds object but received null instead. Mak"
+ + "e sure your MapLocale instance also has a country bounding box defined.");
+ }
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
+ }
+
+ /*
+ * Supporting methods
+ */
+
+ private MapLocale checkMapLocalNonNull(Locale locale) {
+ MapLocale mapLocale = MapLocale.getMapLocale(locale);
+ if (mapLocale == null) {
+ throw new NullPointerException("Locale " + locale.toString() + " has no matching MapLocale ob"
+ + "ject. You need to create an instance of MapLocale and add it to the MapLocale Cache usin"
+ + "g the addMapLocale method.");
+ }
+ return mapLocale;
+ }
+
+ /**
+ * Checks whether the map's source is a source provided by Mapbox, rather than a custom source.
+ *
+ * @param singleSource an individual source object from the map
+ * @return true if the source is from the Mapbox Streets vector source, false if it's not.
+ */
+ private boolean sourceIsFromMapbox(Source singleSource) {
+ return singleSource instanceof VectorSource
+ && ((VectorSource) singleSource).getUrl().substring(0, 9).equals("mapbox://")
+ && (((VectorSource) singleSource).getUrl().contains("mapbox.mapbox-streets-v7")
+ || ((VectorSource) singleSource).getUrl().contains("mapbox.mapbox-streets-v6"));
+ }
+
+ /**
+ * Checks whether a single map layer has a textField that could potentially be localized to the
+ * device's language.
+ *
+ * @param singleLayer an individual layer from the map
+ * @return true if the layer has a textField eligible for translation, false if not.
+ */
+ private boolean layerHasAdjustableTextField(Layer singleLayer) {
+ return singleLayer instanceof SymbolLayer && (((SymbolLayer) singleLayer).getTextField() != null
+ && (((SymbolLayer) singleLayer).getTextField().getValue() != null
+ && !(((SymbolLayer) singleLayer).getTextField().getValue().isEmpty())));
+ }
+}
\ No newline at end of file
diff --git a/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/MapLocale.java b/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/MapLocale.java
new file mode 100644
index 000000000..83906d897
--- /dev/null
+++ b/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/MapLocale.java
@@ -0,0 +1,342 @@
+package com.mapbox.mapboxsdk.plugins.localization;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringDef;
+
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+
+import java.lang.annotation.Retention;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * A {@link MapLocale} object builds off of the {@link Locale} object and provides additional
+ * geographical information particular to the Mapbox Map SDK. Like Locale, MapLocale can be used to
+ * make the map locale sensitive.
+ *
+ * The {@link MapLocale} object can be used to aquire the matching Locales map language; useful for
+ * translating the map language into one of the supported ones found in {@link Languages}.
+ *
+ * You'll also be able to get bounding box information for that same country so the map starting
+ * position target can adjust itself over the devices locale country.
+ *
+ * A handful of {@link MapLocale}'s are already constructed and offered through this class as static
+ * variables. If a country is missing and you'd like to add it, you can use one of the
+ * {@link MapLocale} constructors to build a valid map locale. Once this is done, you need to add it
+ * to the locale cache using {@link MapLocale#addMapLocale(Locale, MapLocale)} were the first
+ * parameter is the {@link Locale} object which matches up with your newly created
+ * {@link MapLocale}.
+ *
+ * @since 0.1.0
+ */
+public final class MapLocale {
+
+ /*
+ * Supported Mapbox map languages.
+ */
+
+ /**
+ * the name (or names) used locally for the place.
+ */
+ public static final String LOCAL_NAME = "name";
+
+ /**
+ * English (if available, otherwise same as name)
+ */
+ public static final String ENGLISH = "name_en";
+
+ /**
+ * French (if available, otherwise same as name_en)
+ */
+ public static final String FRENCH = "name_fr";
+
+ /**
+ * Arabic (if available, otherwise same as name)
+ */
+ public static final String ARABIC = "name_ar";
+
+ /**
+ * Spanish (if available, otherwise same as name_en)
+ */
+ public static final String SPANISH = "name_es";
+
+ /**
+ * German (if available, otherwise same as name_en)
+ */
+ public static final String GERMAN = "name_de";
+
+ /**
+ * Portuguese (if available, otherwise same as name_en)
+ */
+ public static final String PORTUGUESE = "name_pt";
+
+ /**
+ * Russian (if available, otherwise same as name)
+ */
+ public static final String RUSSIAN = "name_ru";
+
+ /**
+ * Chinese (if available, otherwise same as name)
+ */
+ public static final String CHINESE = "name_zh";
+
+ /**
+ * Simplified Chinese (if available, otherwise same as name)
+ */
+ public static final String SIMPLIFIED_CHINESE = "name_zh-Hans";
+
+ @Retention(SOURCE)
+ @StringDef( {LOCAL_NAME, ENGLISH, FRENCH, SIMPLIFIED_CHINESE, ARABIC, SPANISH, GERMAN, PORTUGUESE,
+ RUSSIAN, CHINESE})
+ public @interface Languages {
+ }
+
+ /*
+ * Some Country Bounding Boxes used for the default provided MapLocales.
+ */
+
+ /**
+ * USA Bounding box excluding Hawaii and Alaska extracted from Open Street Map
+ */
+ static final LatLngBounds USA_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(49.388611, -124.733253))
+ .include(new LatLng(24.544245, -66.954811)).build();
+
+ /**
+ * UK Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds UK_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(59.360249, -8.623555))
+ .include(new LatLng(49.906193, 1.759)).build();
+
+ /**
+ * Canada Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds CANADA_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(83.110626, -141.0))
+ .include(new LatLng(41.67598, -52.636291)).build();
+
+ /**
+ * China Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds CHINA_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(53.56086, 73.557693))
+ .include(new LatLng(15.775416, 134.773911)).build();
+
+ /**
+ * Germany Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds GERMANY_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(55.055637, 5.865639))
+ .include(new LatLng(47.275776, 15.039889)).build();
+
+ /**
+ * Korea Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds KOREA_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(38.612446, 125.887108))
+ .include(new LatLng(33.190945, 129.584671)).build();
+
+ /**
+ * Japan Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds JAPAN_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(45.52314, 122.93853))
+ .include(new LatLng(24.249472, 145.820892)).build();
+
+ /**
+ * France Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds FRANCE_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(51.092804, -5.142222))
+ .include(new LatLng(41.371582, 9.561556)).build();
+
+ /**
+ * Italy Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds ITALY_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(47.095196, 6.614889))
+ .include(new LatLng(36.652779, 18.513445)).build();
+
+ /**
+ * Peoples Republic of China Bounding Box extracted from Open Street Map
+ */
+ static final LatLngBounds PRC_BBOX = new LatLngBounds.Builder()
+ .include(new LatLng(53.56086, 73.557693))
+ .include(new LatLng(15.775416, 134.773911)).build();
+
+ /*
+ * Some MapLocales already defined (these match with the predefined ones in the Locale class)
+ */
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale FRANCE = new MapLocale(FRENCH, FRANCE_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale GERMANY = new MapLocale(GERMAN, GERMANY_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale ITALY = new MapLocale(LOCAL_NAME, ITALY_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale JAPAN = new MapLocale(LOCAL_NAME, JAPAN_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale KOREA = new MapLocale(LOCAL_NAME, KOREA_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale CHINA = new MapLocale(SIMPLIFIED_CHINESE, CHINA_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale PRC = new MapLocale(SIMPLIFIED_CHINESE, PRC_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale UK = new MapLocale(ENGLISH, UK_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale US = new MapLocale(ENGLISH, USA_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale CANADA = new MapLocale(ENGLISH, CANADA_BBOX);
+
+ /**
+ * Useful constant for country.
+ */
+ public static final MapLocale CANADA_FRENCH = new MapLocale(FRENCH, CANADA_BBOX);
+
+ /**
+ * Maps out the Matching pair of {@link Locale} and {@link MapLocale}. In other words, if I have a
+ * {@link Locale#CANADA}, this should be matched up with {@link MapLocale#CANADA}.
+ */
+ private static final Map LOCALE_SET;
+
+ static {
+ LOCALE_SET = new HashMap<>();
+ LOCALE_SET.put(Locale.US, MapLocale.US);
+ LOCALE_SET.put(Locale.CANADA_FRENCH, MapLocale.CANADA_FRENCH);
+ LOCALE_SET.put(Locale.CANADA, MapLocale.CANADA);
+ LOCALE_SET.put(Locale.CHINA, MapLocale.CHINA);
+ LOCALE_SET.put(Locale.PRC, MapLocale.PRC);
+ LOCALE_SET.put(Locale.ITALY, MapLocale.ITALY);
+ LOCALE_SET.put(Locale.UK, MapLocale.UK);
+ LOCALE_SET.put(Locale.JAPAN, MapLocale.JAPAN);
+ LOCALE_SET.put(Locale.KOREA, MapLocale.KOREA);
+ LOCALE_SET.put(Locale.GERMANY, MapLocale.GERMANY);
+ LOCALE_SET.put(Locale.FRANCE, MapLocale.FRANCE);
+ }
+
+ private final LatLngBounds countryBounds;
+ private final String mapLanguage;
+
+ /**
+ * Construct a new MapLocale instance using one of the map languages found in {@link Languages}.
+ *
+ * @param mapLanguage a non-null string which is allowed from {@link Languages}
+ * @since 0.1.0
+ */
+ public MapLocale(@NonNull @Languages String mapLanguage) {
+ this(mapLanguage, null);
+ }
+
+ /**
+ * Construct a new MapLocale instance by passing in a LatLngBounds object.
+ *
+ * @param countryBounds non-null {@link LatLngBounds} object which wraps around the country
+ * @since 0.1.0
+ */
+ public MapLocale(@NonNull LatLngBounds countryBounds) {
+ this(LOCAL_NAME, countryBounds);
+ }
+
+ /**
+ * /**
+ * Construct a new MapLocale instance using one of the map languages found in {@link Languages}
+ * and also passing in a LatLngBounds object.
+ *
+ * @param mapLanguage a non-null string which is allowed from {@link Languages}
+ * @param countryBounds {@link LatLngBounds} object which wraps around the country
+ * @since 0.1.0
+ */
+ public MapLocale(@NonNull @Languages String mapLanguage, @Nullable LatLngBounds countryBounds) {
+ this.countryBounds = countryBounds;
+ this.mapLanguage = mapLanguage;
+ }
+
+ /**
+ * Returns the Map Language which can be fed directly into {@code textField} in runtime styling to
+ * change language.
+ *
+ * @return a string representing the map language code.
+ * @since 0.1.0
+ */
+ @NonNull
+ public String getMapLanguage() {
+ return mapLanguage;
+ }
+
+ /**
+ * Returns a {@link LatLngBounds} which represents the viewport bounds which allow for the entire
+ * viewing of a country within the devices viewport.
+ *
+ * @return a {@link LatLngBounds} which can be used when user locations unknown but locale is
+ * @since 0.1.0
+ */
+ @Nullable
+ public LatLngBounds getCountryBounds() {
+ return countryBounds;
+ }
+
+ /**
+ * When creating a new MapLocale, you'll need to associate a {@link Locale} so that
+ * {@link Locale#getDefault()} will find the correct corresponding {@link MapLocale}.
+ *
+ * @param locale a valid {@link Locale} instance shares a 1 to 1 relationship with the
+ * {@link MapLocale}
+ * @param mapLocale the {@link MapLocale} which shares a 1 to 1 relationship with the
+ * {@link Locale}
+ * @since 0.1.0
+ */
+ public static void addMapLocale(@NonNull Locale locale, @NonNull MapLocale mapLocale) {
+ LOCALE_SET.put(locale, mapLocale);
+ }
+
+ /**
+ * Passing in a Locale, you are able to receive the {@link MapLocale} object which it is currently
+ * paired with. If this returns null, there was no matching {@link MapLocale} to go along with the
+ * passed in Locale. If you expected a non-null result, you should make sure you used
+ * {@link #addMapLocale(Locale, MapLocale)} before making this call.
+ *
+ * @param locale the locale which you'd like to recieve it's matching {@link MapLocale} if one exist
+ * @return the matching {@link MapLocale} if one exist, otherwise null
+ * @since 0.1.0
+ */
+ @Nullable
+ public static MapLocale getMapLocale(@NonNull Locale locale) {
+ return LOCALE_SET.get(locale);
+ }
+}
\ No newline at end of file
diff --git a/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/package-info.java b/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/package-info.java
new file mode 100644
index 000000000..6b045cd44
--- /dev/null
+++ b/plugin-localization/src/main/java/com.mapbox.mapboxsdk.plugins.localization/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Contains the classes relevant to the Mapbox Localization Plugin.
+ *
+ * @since 0.1.0
+ */
+package com.mapbox.mapboxsdk.plugins.localization;
\ No newline at end of file
diff --git a/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/LocalizationPluginTest.java b/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/LocalizationPluginTest.java
new file mode 100644
index 000000000..c7f5c1dcd
--- /dev/null
+++ b/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/LocalizationPluginTest.java
@@ -0,0 +1,43 @@
+package com.mapbox.mapboxsdk.plugins.localization;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Locale;
+
+import static junit.framework.Assert.assertNotNull;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.mockito.Mockito.mock;
+
+
+public class LocalizationPluginTest {
+
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void sanity() throws Exception {
+ LocalizationPlugin localizationPlugin
+ = new LocalizationPlugin(mock(MapView.class), mock(MapboxMap.class));
+ assertNotNull(localizationPlugin);
+ }
+
+ @Test
+ @Ignore
+ public void setMapLanguage_localePassedInNotValid() throws Exception {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage(containsString("has no matching MapLocale object. You need to create"));
+ LocalizationPlugin localizationPlugin
+ = new LocalizationPlugin(mock(MapView.class), mock(MapboxMap.class));
+ localizationPlugin.setMapLanguage(new Locale("foo", "bar"));
+ }
+}
\ No newline at end of file
diff --git a/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/MapLocaleTest.java b/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/MapLocaleTest.java
new file mode 100644
index 000000000..93c7d7948
--- /dev/null
+++ b/plugin-localization/src/test/java/com/mapbox/mapboxsdk/plugins/localization/MapLocaleTest.java
@@ -0,0 +1,28 @@
+package com.mapbox.mapboxsdk.plugins.localization;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Locale;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+public class MapLocaleTest {
+
+ @Test
+ public void sanity() throws Exception {
+ MapLocale locale = new MapLocale(MapLocale.FRENCH, MapLocale.FRANCE_BBOX);
+ assertThat(locale.getMapLanguage(), equalTo(MapLocale.FRENCH));
+ assertThat(locale.getCountryBounds(), equalTo(MapLocale.FRANCE_BBOX));
+ }
+
+ @Test
+ public void addMapLocale_doesGetAddedAndReferencedCorrectly() throws Exception {
+ Locale locale = new Locale("foo", "bar");
+ MapLocale mapLocale = new MapLocale("abc");
+ MapLocale.addMapLocale(locale, mapLocale);
+ MapLocale mapLocale1 = MapLocale.getMapLocale(locale);
+ Assert.assertThat(mapLocale1.getMapLanguage(), equalTo("abc"));
+ }
+}
diff --git a/settings.gradle b/settings.gradle
index 7ac3b66d3..35c6282e6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -5,4 +5,5 @@ include ':plugin-building'
include ':plugin-cluster'
include ':plugin-geojson'
include ':plugin-places'
-include 'plugin-offline'
+include ':plugin-offline'
+include ':plugin-localization'