-
Notifications
You must be signed in to change notification settings - Fork 156
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
Feature/localization #480
Changes from all commits
3d16037
140fa7b
ddc0ec9
3fbb7ef
318817d
8be4ca5
73ab57b
6e2a40e
d8873f3
87a6ac2
3573782
6a8a7c1
a04a530
3510b14
ca1dfc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
@_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) throws { | ||
|
||
/// 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not every symbol layer should be forced to have a |
||
|
||
/// Expression to be applied to the `SymbolLayer.textField`to localize the language | ||
/// Sample Expression JSON: `["format",["coalesce",["get","name_en"],["get","name"]],{}]` | ||
let replacement = "[\"get\",\"name_\(getLocaleValue(locale: locale))\"]" | ||
|
||
let expressionCoalesce = try NSRegularExpression(pattern: "\\[\"get\",\\s*\"(name_.{2,7})\"\\]", | ||
options: .caseInsensitive) | ||
let expressionAbbr = try NSRegularExpression(pattern: "\\[\"get\",\\s*\"abbr\"\\]", | ||
options: .caseInsensitive) | ||
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It’s unnecessary to convert the entire expression to JSON, then to a string, then monkeypatch the string. That defeats the purpose of having a structured, type-safe representation of an expression. Consider adapting the implementation in mapbox/mapbox-navigation-ios#2933 that recurses into the expression, surgically replacing occurrences of |
||
|
||
for layerInfo in symbolLayers { | ||
let tempLayer = try layer(withId: layerInfo.id) as SymbolLayer | ||
|
||
if var stringExpression = String(data: try JSONEncoder().encode(tempLayer.textField), encoding: .utf8), | ||
stringExpression != "null" { | ||
stringExpression.updateExpression(replacement: replacement, regex: expressionCoalesce) | ||
stringExpression.updateExpression(replacement: replacement, regex: expressionAbbr) | ||
|
||
// Turn the new json string back into an Expression | ||
let data = stringExpression.data(using: .utf8) | ||
let convertedExpression = try JSONSerialization.jsonObject(with: data!, options: []) | ||
|
||
try setLayerProperty(for: tempLayer.id, property: "text-field", value: convertedExpression) | ||
} | ||
Comment on lines
+25
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code should be extracted into a public method, so that a map optimized for turn-by-turn navigation can localize place and POI labels but not road labels. |
||
} | ||
} | ||
|
||
/// Filters through source to determine supported locale styles. | ||
/// This is needed for v7 support | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tileset ID always occurs in the hostname part of the URL, so compare against |
||
if locale.identifier.contains("zh") { | ||
// v7 styles does not support value of "name_zh-Hant" | ||
if locale.identifier == SupportedLanguage.traditionalChinese.rawValue { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace SupportedLanguage with an array of |
||
return SupportedLanguage.chinese.rawValue | ||
} | ||
} | ||
} | ||
} catch { | ||
Log.warning(forMessage: "Error localizing textField for Symbol Layer", category: "Style") | ||
} | ||
} | ||
|
||
return locale.identifier | ||
} | ||
} | ||
|
||
extension String { | ||
|
||
/// Updates string using a regex | ||
/// - Parameters: | ||
/// - replacement: New string to replace the matched pattern | ||
/// - regex: The regex pattern that will be matched for replacement | ||
internal mutating func updateExpression(replacement: String, regex: NSRegularExpression) { | ||
let range = NSRange(location: 0, length: self.count) | ||
|
||
self = regex.stringByReplacingMatches(in: self, | ||
options: [], | ||
range: range, | ||
withTemplate: replacement) | ||
} | ||
} |
There was a problem hiding this comment.
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.