Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/localization #480

Merged
merged 15 commits into from
Jun 22, 2021
4 changes: 4 additions & 0 deletions Apps/Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
B5327E9D260124C00095B6BD /* MapboxMaps in Frameworks */ = {isa = PBXBuildFile; productRef = B5327E9C260124C00095B6BD /* MapboxMaps */; };
B5327EBF260277930095B6BD /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5327EBE260277930095B6BD /* Example.swift */; };
B5327EC3260277AC0095B6BD /* ExampleProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5327EC2260277AC0095B6BD /* ExampleProtocol.swift */; };
C608C107267BC5B1003C86C3 /* LocalizationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = C608C106267BC5B1003C86C3 /* LocalizationExample.swift */; };
CA03F10F26268DF700673961 /* OfflineManagerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA03F10E26268DF700673961 /* OfflineManagerExample.swift */; };
CA4F1F7225E815CF00822D2A /* SwiftUIExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07A2E03C25CB64E20082BC31 /* SwiftUIExample.swift */; };
CA628414262DFD5C00651488 /* OfflineManagerExample.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CA628413262DFD5C00651488 /* OfflineManagerExample.storyboard */; };
Expand Down Expand Up @@ -149,6 +150,7 @@
A4AC5DEB2542CB2200995E4C /* CustomStyleURLExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStyleURLExample.swift; sourceTree = "<group>"; };
B5327EBE260277930095B6BD /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = "<group>"; };
B5327EC2260277AC0095B6BD /* ExampleProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleProtocol.swift; sourceTree = "<group>"; };
C608C106267BC5B1003C86C3 /* LocalizationExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationExample.swift; sourceTree = "<group>"; };
C64ED3842540A2BE00ADADFB /* CameraAnimationExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraAnimationExample.swift; sourceTree = "<group>"; };
C64ED3882540BA0700ADADFB /* PointAnnotationExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointAnnotationExample.swift; sourceTree = "<group>"; };
C64ED3C42540DD6E00ADADFB /* CustomPointAnnotationExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPointAnnotationExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -257,6 +259,7 @@
73694BD225D4B2CE0064F636 /* TrackingModeExample.swift */,
077E3B932581987B00564A3E /* UpdatePointAnnotationPositionExample.swift */,
07B071D12547CF50007F2865 /* Sample Data */,
C608C106267BC5B1003C86C3 /* LocalizationExample.swift */,
);
path = "All Examples";
sourceTree = "<group>";
Expand Down Expand Up @@ -527,6 +530,7 @@
CADCF71F2584990E0065C51B /* DataDrivenSymbolsExample.swift in Sources */,
CADCF7302584990E0065C51B /* LayerPositionExample.swift in Sources */,
CA86E81825BE7C2300E5A1D9 /* BuildingExtrusionsExample.swift in Sources */,
C608C107267BC5B1003C86C3 /* LocalizationExample.swift in Sources */,
0706C4A625B1181A008733C0 /* TerrainExample.swift in Sources */,
CADCF7282584990E0065C51B /* LineAnnotationExample.swift in Sources */,
CADCF72F2584990E0065C51B /* SnapshotterCoreGraphicsExample.swift in Sources */,
Expand Down
80 changes: 80 additions & 0 deletions Apps/Examples/Examples/All Examples/LocalizationExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import UIKit
import MapboxMaps

@objc(LocalizationExample)

public class LocalizationExample: UIViewController, ExampleProtocol {

internal var mapView: MapView!

override public func viewDidLoad() {
super.viewDidLoad()

mapView = MapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)

configureLanguageButton()

// Allows the delegate to receive information about map events.
mapView.mapboxMap.onNext(.mapLoaded) { _ in
self.finish() // Needed for internal testing purposes.
}
}

private func configureLanguageButton() {
// Set up layer postion change button
let button = UIButton(type: .system)
button.setTitle("Change Language", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = #colorLiteral(red: 0, green: 0.4784313725, blue: 0.9882352941, alpha: 1)
button.translatesAutoresizingMaskIntoConstraints = false
button.layer.cornerRadius = 20
button.clipsToBounds = true
button.addTarget(self, action: #selector(changeLanguage(sender:)), for: .touchUpInside)
view.addSubview(button)

// Set button location
let horizontalConstraint = button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -24)
let verticalConstraint = button.centerXAnchor.constraint(equalTo: view.centerXAnchor)
let widthConstraint = button.widthAnchor.constraint(equalToConstant: 200)
let heightConstraint = button.heightAnchor.constraint(equalToConstant: 40)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
}

@objc public func changeLanguage(sender: UIButton) {
let alert = UIAlertController(title: "Languages",
message: "Please select a language to localize to.",
preferredStyle: .actionSheet)

alert.addAction(UIAlertAction(title: "Spanish", style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self.mapView.locale = Locale(identifier: SupportedLanguage.spanish.rawValue)
}))

alert.addAction(UIAlertAction(title: "French", style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self.mapView.locale = Locale(identifier: SupportedLanguage.french.rawValue)
}))

alert.addAction(UIAlertAction(title: "Traditional Chinese", style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self.mapView.locale = Locale(identifier: SupportedLanguage.traditionalChinese.rawValue)
}))

alert.addAction(UIAlertAction(title: "Arabic", style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self.mapView.locale = Locale(identifier: SupportedLanguage.arabic.rawValue)
}))

alert.addAction(UIAlertAction(title: "English", style: .default, handler: { [weak self] _ in
guard let self = self else { return }
self.mapView.locale = Locale(identifier: SupportedLanguage.english.rawValue)
}))

alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))

present(alert, animated: true)
}
}
5 changes: 4 additions & 1 deletion Apps/Examples/Examples/Models/Examples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,10 @@ public struct Examples {
type: LineGradientExample.self),
Example(title: "Change the map's style",
description: "Switch between local and default Mapbox styles for the same map view.",
type: SwitchStylesExample.self)
type: SwitchStylesExample.self),
Example(title: "Change the map's language",
description: "Switch between supported languages for Symbol Layers",
type: LocalizationExample.self)
]

// Examples that show use cases related to user interaction with the map.
Expand Down
4 changes: 4 additions & 0 deletions Mapbox/MapboxMaps.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@
B5F294BB2635EEAD00199426 /* FloatingPointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F294BA2635EEAD00199426 /* FloatingPointTests.swift */; };
B5F295552635EF4E00199426 /* CompassDirectionLong.strings in Resources */ = {isa = PBXBuildFile; fileRef = B5F295512635EF4E00199426 /* CompassDirectionLong.strings */; };
B5F295562635EF4E00199426 /* CompassDirectionShort.strings in Resources */ = {isa = PBXBuildFile; fileRef = B5F295532635EF4E00199426 /* CompassDirectionShort.strings */; };
C608C133267BD04A003C86C3 /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C608C132267BD04A003C86C3 /* SupportedLanguage.swift */; };
C6334944262E28C300D17701 /* StyleTransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6334943262E28C300D17701 /* StyleTransitionTests.swift */; };
C64994A9258D5ADE0052C21C /* LocationPuckManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64994A5258D5ADD0052C21C /* LocationPuckManager.swift */; };
C64994AB258D5ADE0052C21C /* Puck.swift in Sources */ = {isa = PBXBuildFile; fileRef = C64994A6258D5ADE0052C21C /* Puck.swift */; };
Expand Down Expand Up @@ -691,6 +692,7 @@
B5F2957D2635EF9F00199426 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/CompassDirectionLong.strings; sourceTree = "<group>"; };
B5F2957E2635EF9F00199426 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/CompassDirectionLong.strings; sourceTree = "<group>"; };
B5F2957F2635EF9F00199426 /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/CompassDirectionLong.strings; sourceTree = "<group>"; };
C608C132267BD04A003C86C3 /* SupportedLanguage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = "<group>"; };
C6334943262E28C300D17701 /* StyleTransitionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyleTransitionTests.swift; sourceTree = "<group>"; };
C64994A5258D5ADD0052C21C /* LocationPuckManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationPuckManager.swift; sourceTree = "<group>"; };
C64994A6258D5ADE0052C21C /* Puck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Puck.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1466,6 +1468,7 @@
A4519DCE2432FF03007CF39A /* MapboxMapsStyle */ = {
isa = PBXGroup;
children = (
C608C132267BD04A003C86C3 /* SupportedLanguage.swift */,
CA9F8CE22641F95C00A8BCB6 /* StyleManagerProtocol.swift */,
B5DDE95F2613ACB400998840 /* StyleEncodable.swift */,
0CBCF2D7254229630025F7B3 /* STYLE_README.md */,
Expand Down Expand Up @@ -2039,6 +2042,7 @@
0C708F2924EB1EE2003CE791 /* LocationIndicatorLayer.swift in Sources */,
0C708F6024EC70DD003CE791 /* Color.swift in Sources */,
0C708F2D24EB1EE2003CE791 /* HeatmapLayer.swift in Sources */,
C608C133267BD04A003C86C3 /* SupportedLanguage.swift in Sources */,
0C3B1E9024DDADD000CC29E8 /* EventsManager.swift in Sources */,
0C708F4B24EB23C7003CE791 /* ImageSource.swift in Sources */,
C64994AF258D5ADE0052C21C /* Puck2D.swift in Sources */,
Expand Down
8 changes: 8 additions & 0 deletions Sources/MapboxMaps/Foundation/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@_exported import MapboxCommon
@_implementationOnly import MapboxCoreMaps_Private
@_implementationOnly import MapboxCommon_Private
import Foundation
neelmistry94 marked this conversation as resolved.
Show resolved Hide resolved
import UIKit
import Turf

Expand Down Expand Up @@ -34,6 +35,13 @@ open class MapView: UIView {
/// Controls the addition/removal of annotations to the map.
public internal(set) var annotations: AnnotationOrchestrator!

/// Property that describes the current language for `SymbolLayer.textField`
public var locale: Locale = Locale.current {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locale.current only matches the language that the iOS UI is currently displayed in, which can differ from both the surrounding application and the user’s preference. Bundle.preferredLocalizations has the advantage of matching the language that the rest of the application is currently displayed in. Locale.preferredLanguages has the advantage of matching the user’s preferred content language settings, even if the application is unavailable in the preferred language.

didSet {
mapboxMap.style.localizeLabels(into: locale)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be an unsupported Locale?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, but in that case, we will default to the local case if there is no data that exists

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to make that behavior more explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could add a check when passed in to see if it is under one of our supported languages. If so, then continue, otherwise don't do anything


/// A reference to the `EventsManager` used for dispatching telemetry.
internal var eventsListener: EventsListener!

Expand Down
79 changes: 79 additions & 0 deletions Sources/MapboxMaps/Style/Style+Localization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@_implementationOnly import MapboxCommon_Private

/// Support for localization of labels
extension Style {

/// This function creates an expression that will localize the `textField` property of a `SymbolLayer`
/// - Parameter locale: A `SupportedLanguage` based `Locale`
internal func localizeLabels(into locale: Locale) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nav SDK has a requirement that not all labels are localized I think.. They want to exempt road labels for example from any localization.

To help them do this, it's probably best to break this function up into some functions like this:

public func localizeAllLabels(into locale: Locale) { .. }

public func localizeLabel(withId id: String, into locale: Locale) { .. } 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julianrex @tobrun This feels like something we need to align on. This functionality does not exist on Android


/// Get all symbol layers that are currently on the map
let symbolLayers = allLayerIdentifiers.filter { layer in
return layer.type == .symbol
}
Comment on lines +11 to +13
Copy link
Contributor

@1ec5 1ec5 Jun 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not every symbol layer should be forced to have a name-based text-field, which is specific to the Mapbox Streets source. A symbol layer may be backed by a vector tileset other than Mapbox Streets or a GeoJSON source; that source’s features may have name_en properties but not name properties. There’s code elsewhere that checks the source URL; it would be useful to conditionalize all localization on it, not just detection of Streets source v7.


/// Expression to be applied to the `SymbolLayer.textField`to localize the language
/// Sample Expression JSON: `["format",["coalesce",["get","name_en"],["get","name"]],{}]`
let newVal = "[\"get\",\"name_\(getLocaleValue(locale: locale))\"]"

let EXPRESSION_REGEX = try! NSRegularExpression(pattern: "\\[\"get\",\\s*\"(name_.{2,7})\"\\]",
options: .caseInsensitive)
let EXPRESSION_ABBR_REGEX = try! NSRegularExpression(pattern: "\\[\"get\",\\s*\"abbr\"\\]",
options: .caseInsensitive)

for layerInfo in symbolLayers {
do {
let tempLayer = try layer(withId: layerInfo.id) as SymbolLayer

let encoder = JSONEncoder()
let jsonData = try encoder.encode(tempLayer.textField)

if var stringExpression = String(data: jsonData, encoding: .utf8) {
stringExpression = EXPRESSION_REGEX.stringByReplacingMatches(in: stringExpression,
options: [],
range: NSMakeRange(0, stringExpression.count),
withTemplate: newVal)

stringExpression = EXPRESSION_ABBR_REGEX.stringByReplacingMatches(in: stringExpression,
options: [],
range: NSMakeRange(0, stringExpression.count),
withTemplate: newVal)

let data = stringExpression.data(using: .utf8)
let decodedExpression = try JSONDecoder().decode(Expression.self, from: data!)
try updateLayer(withId: tempLayer.id) { (layer: inout SymbolLayer) throws in
layer.textField = .expression(decodedExpression)
}
}
} catch {
Log.warning(forMessage: "Error localizing textField for Symbol Layer with ID: \(layerInfo.id)", category: "Style")
}
}
}

/// Filters through source to determine supported locale styles.
internal func getLocaleValue(locale: Locale) -> String {
let vectorSources = allSourceIdentifiers.filter { source in
return source.type == .vector
}

for sourceInfo in vectorSources {
do {
let tempSource = try source(withId: sourceInfo.id) as VectorSource

if tempSource.url?.contains("mapbox.mapbox-streets-v7") == true{
if locale.identifier.contains("zh") {
// v7 styles does not support value of "name_zh-Hant"
if locale.identifier == SupportedLanguage.traditionalChinese.rawValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace SupportedLanguage with an array of Locales to avoid unchecked usage of raw values.

return SupportedLanguage.chinese.rawValue
}
}
}
} catch {
Log.warning(forMessage: "Error localizing textField for Symbol Layer", category: "Style")
}
}

return locale.identifier
}
}
45 changes: 45 additions & 0 deletions Sources/MapboxMaps/Style/SupportedLanguage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/// A list of languages that our maps support for localization
public enum SupportedLanguage: String, CaseIterable {

/// Support for Arabic (if available)
case arabic = "ar"

/// Support for Chinese (if available)
case chinese = "zh"

/// Support for English (if available)
case english = "en"

/// Support for French (if available)
case french = "fr"

/// Support for German (if available)
case german = "de"

/// Support for Italian (if available)
case italian = "it"

/// Support for Japanese (if available)
case japanese = "ja"

/// Support for Korean (if available)
case korean = "ko"

/// Support for Portuguese (if available)
case portuguese = "pt"

/// Support for Russian (if available)
case russian = "ru"

/// Support for Simplified Chinese (if available)
case simplifiedChinese = "zh-Hans"

/// Support for Spanish (if available)
case spanish = "es"

/// Support for Traditional Chinese (if available)
neelmistry94 marked this conversation as resolved.
Show resolved Hide resolved
case traditionalChinese = "zh-Hant"

/// Support for Vietnamese (if available)
case vietnamese = "vi"
}