From db34eaad7d44bdce464ef99918f01b30d2b5d35a Mon Sep 17 00:00:00 2001 From: Dmytro Gokun Date: Thu, 28 Mar 2019 18:46:54 +0200 Subject: [PATCH] Attempt to localize mapbox UI --- src/ui/control/fullscreen_control.js | 6 +++++- src/ui/control/geolocate_control.js | 17 +++++++++++------ src/ui/control/logo_control.js | 2 +- src/ui/control/navigation_control.js | 22 +++++++++++++++------- src/ui/control/scale_control.js | 18 +++++++----------- src/ui/default_locale.js | 20 ++++++++++++++++++++ src/ui/map.js | 17 +++++++++++++++-- 7 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 src/ui/default_locale.js diff --git a/src/ui/control/fullscreen_control.js b/src/ui/control/fullscreen_control.js index a47a39c3171..e7006d5d871 100644 --- a/src/ui/control/fullscreen_control.js +++ b/src/ui/control/fullscreen_control.js @@ -93,11 +93,15 @@ class FullscreenControl { } _updateTitle() { - const title = this._isFullscreen() ? "Exit fullscreen" : "Enter fullscreen"; + const title = this._getTitle(); this._fullscreenButton.setAttribute("aria-label", title); this._fullscreenButton.title = title; } + _getTitle() { + return this._map._getUIString(this._isFullscreen() ? 'FullscreenControl.Exit' : 'FullscreenControl.Enter'); + } + _isFullscreen() { return this._fullscreen; } diff --git a/src/ui/control/geolocate_control.js b/src/ui/control/geolocate_control.js index 0291c6641ed..0f9a0e6a894 100644 --- a/src/ui/control/geolocate_control.js +++ b/src/ui/control/geolocate_control.js @@ -263,8 +263,9 @@ class GeolocateControl extends Evented { this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background'); this._geolocateButton.classList.remove('mapboxgl-ctrl-geolocate-background-error'); this._geolocateButton.disabled = true; - this._geolocateButton.title = 'Location not available'; - this._geolocateButton.setAttribute('aria-label', 'Location not available'); + const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); + this._geolocateButton.title = title; + this._geolocateButton.setAttribute('aria-label', title); if (this._geolocationWatchID !== undefined) { this._clearWatch(); @@ -293,13 +294,17 @@ class GeolocateControl extends Evented { this._geolocateButton = DOM.create('button', `mapboxgl-ctrl-geolocate`, this._container); DOM.create('span', `mapboxgl-ctrl-icon`, this._geolocateButton).setAttribute('aria-hidden', true); this._geolocateButton.type = 'button'; - this._geolocateButton.title = 'Find my location'; - this._geolocateButton.setAttribute('aria-label', 'Find my location'); + if (supported === false) { warnOnce('Geolocation support is not available so the GeolocateControl will be disabled.'); + const title = this._map._getUIString('GeolocateControl.LocationNotAvailable'); this._geolocateButton.disabled = true; - this._geolocateButton.title = 'Location not available'; - this._geolocateButton.setAttribute('aria-label', 'Location not available'); + this._geolocateButton.title = title; + this._geolocateButton.setAttribute('aria-label', title); + } else { + const title = this._map._getUIString('GeolocateControl.FindMyLocation'); + this._geolocateButton.title = title; + this._geolocateButton.setAttribute('aria-label', title); } if (this.options.trackUserLocation) { diff --git a/src/ui/control/logo_control.js b/src/ui/control/logo_control.js index c7c2e4cbe5c..a56fdb912a1 100644 --- a/src/ui/control/logo_control.js +++ b/src/ui/control/logo_control.js @@ -31,7 +31,7 @@ class LogoControl { anchor.target = "_blank"; anchor.rel = "noopener nofollow"; anchor.href = "https://www.mapbox.com/"; - anchor.setAttribute("aria-label", "Mapbox logo"); + anchor.setAttribute("aria-label", this._map._getUIString('LogoControl.Title')); anchor.setAttribute("rel", "noopener nofollow"); this._container.appendChild(anchor); this._container.style.display = 'none'; diff --git a/src/ui/control/navigation_control.js b/src/ui/control/navigation_control.js index 8aa3a456b3c..9721d202fee 100644 --- a/src/ui/control/navigation_control.js +++ b/src/ui/control/navigation_control.js @@ -38,7 +38,7 @@ class NavigationControl { _container: HTMLElement; _zoomInButton: HTMLButtonElement; _zoomOutButton: HTMLButtonElement; - _compass: HTMLElement; + _compass: HTMLButtonElement; _compassIcon: HTMLElement; _handler: DragRotateHandler; @@ -50,18 +50,19 @@ class NavigationControl { if (this.options.showZoom) { bindAll([ + '_setButtonTitle', '_updateZoomButtons' ], this); - this._zoomInButton = this._createButton('mapboxgl-ctrl-zoom-in', 'Zoom in', (e) => this._map.zoomIn({}, {originalEvent: e})); + this._zoomInButton = this._createButton('mapboxgl-ctrl-zoom-in', (e) => this._map.zoomIn({}, {originalEvent: e})); DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomInButton).setAttribute('aria-hidden', true); - this._zoomOutButton = this._createButton('mapboxgl-ctrl-zoom-out', 'Zoom out', (e) => this._map.zoomOut({}, {originalEvent: e})); + this._zoomOutButton = this._createButton('mapboxgl-ctrl-zoom-out', (e) => this._map.zoomOut({}, {originalEvent: e})); DOM.create('span', `mapboxgl-ctrl-icon`, this._zoomOutButton).setAttribute('aria-hidden', true); } if (this.options.showCompass) { bindAll([ '_rotateCompassArrow' ], this); - this._compass = this._createButton('mapboxgl-ctrl-compass', 'Reset bearing to north', (e) => { + this._compass = this._createButton('mapboxgl-ctrl-compass', (e) => { if (this.options.visualizePitch) { this._map.resetNorthPitch({}, {originalEvent: e}); } else { @@ -90,10 +91,13 @@ class NavigationControl { onAdd(map: Map) { this._map = map; if (this.options.showZoom) { + this._setButtonTitle(this._zoomInButton, 'ZoomIn'); + this._setButtonTitle(this._zoomOutButton, 'ZoomOut'); this._map.on('zoom', this._updateZoomButtons); this._updateZoomButtons(); } if (this.options.showCompass) { + this._setButtonTitle(this._compass, 'ResetBearing'); if (this.options.visualizePitch) { this._map.on('pitch', this._rotateCompassArrow); } @@ -126,14 +130,18 @@ class NavigationControl { delete this._map; } - _createButton(className: string, ariaLabel: string, fn: () => mixed) { + _createButton(className: string, fn: () => mixed) { const a = DOM.create('button', className, this._container); a.type = 'button'; - a.title = ariaLabel; - a.setAttribute('aria-label', ariaLabel); a.addEventListener('click', fn); return a; } + + _setButtonTitle(button: HTMLButtonElement, title: string) { + const str = this._map._getUIString(`NavigationControl.${title}`); + button.title = str; + button.setAttribute('aria-label', str); + } } export default NavigationControl; diff --git a/src/ui/control/scale_control.js b/src/ui/control/scale_control.js index f642ef911eb..f57c919ccb9 100644 --- a/src/ui/control/scale_control.js +++ b/src/ui/control/scale_control.js @@ -100,27 +100,23 @@ function updateScale(map, container, options) { const maxFeet = 3.2808 * maxMeters; if (maxFeet > 5280) { const maxMiles = maxFeet / 5280; - setScale(container, maxWidth, maxMiles, 'mi'); + setScale(container, maxWidth, maxMiles, map._getUIString('ScaleControl.Miles')); } else { - setScale(container, maxWidth, maxFeet, 'ft'); + setScale(container, maxWidth, maxFeet, map._getUIString('ScaleControl.Feet')); } } else if (options && options.unit === 'nautical') { const maxNauticals = maxMeters / 1852; - setScale(container, maxWidth, maxNauticals, 'nm'); + setScale(container, maxWidth, maxNauticals, map._getUIString('ScaleControl.NauticalMiles')); + } else if (maxMeters >= 1000) { + setScale(container, maxWidth, maxMeters / 1000, map._getUIString('ScaleControl.Kilometers')); } else { - setScale(container, maxWidth, maxMeters, 'm'); + setScale(container, maxWidth, maxMeters, map._getUIString('ScaleControl.Meters')); } } function setScale(container, maxWidth, maxDistance, unit) { - let distance = getRoundNum(maxDistance); + const distance = getRoundNum(maxDistance); const ratio = distance / maxDistance; - - if (unit === 'm' && distance >= 1000) { - distance = distance / 1000; - unit = 'km'; - } - container.style.width = `${maxWidth * ratio}px`; container.innerHTML = distance + unit; } diff --git a/src/ui/default_locale.js b/src/ui/default_locale.js new file mode 100644 index 00000000000..d5696f38695 --- /dev/null +++ b/src/ui/default_locale.js @@ -0,0 +1,20 @@ +// @flow + +const defaultLocale = { + 'FullscreenControl.Enter': 'Enter fullscreen', + 'FullscreenControl.Exit': 'Exit fullscreen', + 'GeolocateControl.FindMyLocation': 'Find my location', + 'GeolocateControl.LocationNotAvailable': 'Location not available', + 'LogoControl.Title': 'Mapbox logo', + 'NavigationControl.ResetBearing': 'Reset bearing to north', + 'NavigationControl.ZoomIn': 'Zoom in', + 'NavigationControl.ZoomOut': 'Zoom out', + 'ScaleControl.Feet': 'ft', + 'ScaleControl.Meters': 'm', + 'ScaleControl.Kilometers': 'km', + 'ScaleControl.Miles': 'mi', + 'ScaleControl.NauticalMiles': 'nm' + +}; + +export default defaultLocale; diff --git a/src/ui/map.js b/src/ui/map.js index 001ccc76e86..8bd87bb11da 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -44,6 +44,7 @@ import type DragPanHandler, {DragPanOptions} from './handler/drag_pan'; import type KeyboardHandler from './handler/keyboard'; import type DoubleClickZoomHandler from './handler/dblclick_zoom'; import type TouchZoomRotateHandler from './handler/touch_zoom_rotate'; +import defaultLocale from './default_locale'; import type {TaskID} from '../util/task_queue'; import type {Cancelable} from '../types/cancelable'; import type { @@ -96,7 +97,8 @@ type MapOptions = { renderWorldCopies?: boolean, maxTileCacheSize?: number, transformRequest?: RequestTransformFunction, - accessToken: string + accessToken: string, + locale?: Object }; const defaultMinZoom = 0; @@ -235,7 +237,7 @@ const defaultOptions = { * @param {number} [options.fadeDuration=300] Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. * @param {boolean} [options.crossSourceCollisions=true] If `true`, symbols from multiple sources can collide with each other during collision detection. If `false`, collision detection is run separately for the symbols in each source. * @param {string} [options.accessToken=null] If specified, map will use this token instead of the one defined in mapboxgl.accessToken. - + * @param {string} [options.locale=null] A patch to apply to the default localization table for UI strings, e.g. control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table). * @example * var map = new mapboxgl.Map({ * container: 'map', @@ -293,6 +295,7 @@ class Map extends Camera { _mapId: number; _localIdeographFontFamily: string; _requestManager: RequestManager; + _locale: Object; /** * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad. @@ -374,6 +377,7 @@ class Map extends Camera { this._renderTaskQueue = new TaskQueue(); this._controls = []; this._mapId = uniqueId(); + this._locale = extend({}, defaultLocale, options.locale); this._requestManager = new RequestManager(options.transformRequest, options.accessToken); @@ -1168,6 +1172,15 @@ class Map extends Camera { } } + _getUIString(key: string) { + const str = this._locale[key]; + if (str == null) { + throw new Error(`Missing UI string '${key}'`); + } + + return str; + } + _updateStyle(style: StyleSpecification | string | null, options?: {diff?: boolean} & StyleOptions) { if (this.style) { this.style.setEventedParent(null);