Localize expressions more thoroughly (#11651)

* [ios] Removed changelog entry for Terrarium

The broader feature is new to v4.0.0 as well.

* [ios, macos] Localize expressions more thoroughly

Replaced the MGLStyle.localizesLabels property with a -localizeLabelsIntoLocale: method that allows the caller to specify the locale to localize into. Also exposed a per-expression localization method for developers who want to vary behavior from layer to layer.

* [macos] Offer English labels if preferred language is unsupported

* [ios, macos] Removed dead code

* [ios] Use new localization method in iosapp

* [ios, macos] Fixed local name labels

* [ios, macos] Convert tokens to key path expressions in stop dictionaries

* [ios, macos] Streamlined token upgrading

Separated token upgrading into a separate process that only happens as part of the MGLSymbolStyleLayer.text and MGLSymbolStyleLayer.iconImageName properties’ getters, so that it’s easy to remove later when mbgl changes obviate this workaround. Removed the replacesTokens parameter to the expression localization methods.

* [ios, macos] Preserve whitespace between tokens

* [ios, macos] Moved token replacement to a consistent category

Fixed a build warning.

* [ios, macos] Replace tokens in all string-typed getters

For consistency, replace tokens with key paths in all string-typed style paint and layout properties.

* [ios, macos] Test token replacement

Added tests for replacement of tokens with key paths in expressions. Fixed token replacement for raw strings in stop dictionaries. Avoid sticking a single string inside an mgl_join: call.

* [ios, macos] Test token replacement, localization

Added unit tests of token replacement and localization of expressions. Only NSExpression is responsible for resolving the preferred language now, since NSLocale tends to tack a region code onto the locale identifier and the NSExpression method can be called independently anyways. Added a private variation of +[MGLVectorTileSource preferredMapboxStreetsLanguage] that takes an array of preferred languages. Fixed localization of non-expressions in stop dictionaries.

* [ios, macos] Dictionary keys aren’t necessarily zoom levels
This commit is contained in:
Minh Nguyễn
2018-04-16 15:24:33 -07:00
committed by GitHub
parent 62dd097328
commit e16ce7d879
20 changed files with 534 additions and 101 deletions

View File

@@ -116,7 +116,8 @@
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultBackgroundPattern();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setBackgroundPatternTransition:(MGLTransition )transition {

View File

@@ -231,7 +231,8 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultFillExtrusionPattern();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setFillExtrusionPatternTransition:(MGLTransition )transition {

View File

@@ -220,7 +220,8 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultFillPattern();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setFillPatternTransition:(MGLTransition )transition {

View File

@@ -390,7 +390,8 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultLinePattern();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setLinePatternTransition:(MGLTransition )transition {

View File

@@ -493,17 +493,20 @@ MGL_EXPORT
#pragma mark Localizing Map Content
/**
A Boolean value that determines whether the style attempts to localize labels in
the style into the system’s preferred language.
Attempts to localize labels in the style into the given locale.
When this property is enabled, the style automatically modifies the text property
of any symbol style layer whose source is the
<a href="https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview">Mapbox
Streets source</a>. On iOS, the user can set the system’s preferred language in
Settings, General Settings, Language & Region. On macOS, the user can set the
system’s preferred language in the Language & Region pane of System Preferences.
This method automatically modifies the text property of any symbol style layer
in the style whose source is the
<a href="https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview">Mapbox Streets source</a>.
On iOS, the user can set the system’s preferred language in Settings, General
Settings, Language & Region. On macOS, the user can set the system’s preferred
language in the Language & Region pane of System Preferences.
@param locale The locale into which labels should be localized. To use the
system’s preferred language, if supported, specify `nil`. To use the local
language, specify a locale with the identifier `mul`.
*/
@property (nonatomic) BOOL localizesLabels;
- (void)localizeLabelsIntoLocale:(nullable NSLocale *)locale;
@end

View File

@@ -570,73 +570,27 @@ static_assert(6 == mbgl::util::default_styles::numOrderedStyles,
#pragma mark Mapbox Streets source introspection
- (void)setLocalizesLabels:(BOOL)localizesLabels
{
if (_localizesLabels != localizesLabels) {
_localizesLabels = localizesLabels;
} else {
return;
}
- (void)localizeLabelsIntoLocale:(nullable NSLocale *)locale {
NSSet<MGLVectorTileSource *> *streetsSources =
[self.sources filteredSetUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(MGLVectorTileSource * _Nullable source, NSDictionary<NSString *, id> * _Nullable bindings) {
return [source isKindOfClass:[MGLVectorTileSource class]] && [source isMapboxStreets];
}]];
NSSet<NSString *> *streetsSourceIdentifiers = [streetsSources valueForKey:@"identifier"];
if (_localizesLabels) {
NSString *preferredLanguage = [MGLVectorTileSource preferredMapboxStreetsLanguage];
NSMutableDictionary *localizedKeysByKeyBySourceIdentifier = [NSMutableDictionary dictionary];
for (MGLSymbolStyleLayer *layer in self.layers) {
if (![layer isKindOfClass:[MGLSymbolStyleLayer class]]) {
continue;
}
MGLVectorTileSource *source = (MGLVectorTileSource *)[self sourceWithIdentifier:layer.sourceIdentifier];
if (![source isKindOfClass:[MGLVectorTileSource class]] || !source.mapboxStreets) {
continue;
}
NSDictionary *localizedKeysByKey = localizedKeysByKeyBySourceIdentifier[layer.sourceIdentifier];
if (!localizedKeysByKey) {
localizedKeysByKey = localizedKeysByKeyBySourceIdentifier[layer.sourceIdentifier] = [source localizedKeysByKeyForPreferredLanguage:preferredLanguage];
}
NSString *(^stringByLocalizingString)(NSString *) = ^ NSString * (NSString *string) {
NSMutableString *localizedString = string.mutableCopy;
[localizedKeysByKey enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull localizedKey, BOOL * _Nonnull stop) {
NSAssert([key isKindOfClass:[NSString class]], @"key is not a string");
NSAssert([localizedKey isKindOfClass:[NSString class]], @"localizedKey is not a string");
[localizedString replaceOccurrencesOfString:[NSString stringWithFormat:@"{%@}", key]
withString:[NSString stringWithFormat:@"{%@}", localizedKey]
options:0
range:NSMakeRange(0, localizedString.length)];
}];
return localizedString;
};
if (layer.text.expressionType == NSConstantValueExpressionType) {
NSString *textField = layer.text.constantValue;
NSString *localizingString = stringByLocalizingString(textField);
if (![textField isEqualToString:localizingString]) {
MGLTextLanguage *textLanguage = [[MGLTextLanguage alloc] initWithTextLanguage:textField
updatedTextField:localizingString];
[self.localizedLayersByIdentifier setObject:@{ textField : textLanguage } forKey:layer.identifier];
layer.text = [NSExpression expressionForConstantValue:localizingString];
}
}
for (MGLSymbolStyleLayer *layer in self.layers) {
if (![layer isKindOfClass:[MGLSymbolStyleLayer class]]) {
continue;
}
if (![streetsSourceIdentifiers containsObject:layer.sourceIdentifier]) {
continue;
}
} else {
[self.localizedLayersByIdentifier enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, NSDictionary<NSObject *, MGLTextLanguage *> *textFields, BOOL *done) {
MGLSymbolStyleLayer *layer = (MGLSymbolStyleLayer *)[self.mapView.style layerWithIdentifier:identifier];
if (layer.text.expressionType == NSConstantValueExpressionType) {
NSString *textField = layer.text.constantValue;
[textFields enumerateKeysAndObjectsUsingBlock:^(NSObject *originalLanguage, MGLTextLanguage *textLanguage, BOOL *done) {
if ([textLanguage.updatedTextField isEqualToString:textField]) {
layer.text = [NSExpression expressionForConstantValue:textLanguage.originalTextField];
}
}];
}
}];
self.localizedLayersByIdentifier = [NSMutableDictionary dictionary];
NSExpression *text = layer.text;
NSExpression *localizedText = [text mgl_expressionLocalizedIntoLocale:locale];
if (![localizedText isEqual:text]) {
layer.text = localizedText;
}
}
}

View File

@@ -136,7 +136,12 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefault<%- camelize(originalPropertyName(property)) %>();
}
<% if (property.type === 'string') { -%>
NSExpression *expression = MGLStyleValueTransformer<<%- valueTransformerArguments(property).join(', ') %>>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
<% } else { -%>
return MGLStyleValueTransformer<<%- valueTransformerArguments(property).join(', ') %>>().toExpression(propertyValue);
<% } -%>
}
<% if (property.original) { -%>
@@ -174,7 +179,12 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefault<%- camelize(originalPropertyName(property)) %>();
}
<% if (property.type === 'string') { -%>
NSExpression *expression = MGLStyleValueTransformer<<%- valueTransformerArguments(property).join(', ') %>>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
<% } else { -%>
return MGLStyleValueTransformer<<%- valueTransformerArguments(property).join(', ') %>>().toExpression(propertyValue);
<% } -%>
}
<% if (property["transition"]) { -%>

View File

@@ -233,7 +233,8 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultIconImage();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setIconImage:(NSExpression *)iconImage {
@@ -578,7 +579,8 @@ namespace mbgl {
if (propertyValue.isUndefined()) {
propertyValue = self.rawLayer->getDefaultTextField();
}
return MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
NSExpression *expression = MGLStyleValueTransformer<std::string, NSString *>().toExpression(propertyValue);
return expression.mgl_expressionByReplacingTokensWithKeyPaths;
}
- (void)setTextField:(NSExpression *)textField {

View File

@@ -75,28 +75,55 @@
@implementation MGLVectorTileSource (Private)
/**
An array of locale codes with dedicated name fields in the Mapbox Streets
source.
https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview
*/
static NSArray * const MGLMapboxStreetsLanguages = @[
@"ar", @"de", @"en", @"es", @"fr", @"pt", @"ru", @"zh", @"zh-Hans",
];
/**
Like `MGLMapboxStreetsLanguages`, but deanglicized for use with
`+[NSBundle preferredLocalizationsFromArray:forPreferences:]`.
*/
static NSArray * const MGLMapboxStreetsAlternativeLanguages = @[
@"mul", @"ar", @"de", @"es", @"fr", @"pt", @"ru", @"zh", @"zh-Hans",
];
+ (NS_SET_OF(NSString *) *)mapboxStreetsLanguages {
// https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview
static dispatch_once_t onceToken;
static NS_SET_OF(NSString *) *mapboxStreetsLanguages;
dispatch_once(&onceToken, ^{
// https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview
mapboxStreetsLanguages = [NSSet setWithObjects:@"ar", @"de", @"en", @"es", @"fr", @"pt", @"ru", @"zh", @"zh-Hans", nil];
mapboxStreetsLanguages = [NSSet setWithArray:MGLMapboxStreetsLanguages];
});
return mapboxStreetsLanguages;
}
+ (NSString *)preferredMapboxStreetsLanguage {
NSArray<NSString *> *supportedLanguages = [MGLVectorTileSource mapboxStreetsLanguages].allObjects;
NSArray<NSString *> *preferredLanguages = [NSBundle preferredLocalizationsFromArray:supportedLanguages
forPreferences:[NSLocale preferredLanguages]];
return [self preferredMapboxStreetsLanguageForPreferences:[NSLocale preferredLanguages]];
}
+ (NSString *)preferredMapboxStreetsLanguageForPreferences:(NSArray<NSString *> *)preferencesArray {
BOOL acceptsEnglish = [preferencesArray filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(NSString * _Nullable language, NSDictionary<NSString *,id> * _Nullable bindings) {
return [[NSLocale localeWithLocaleIdentifier:language].languageCode isEqualToString:@"en"];
}]].count;
NSArray<NSString *> *preferredLanguages = [NSBundle preferredLocalizationsFromArray:MGLMapboxStreetsAlternativeLanguages
forPreferences:preferencesArray];
NSString *mostSpecificLanguage;
for (NSString *language in preferredLanguages) {
if (language.length > mostSpecificLanguage.length) {
mostSpecificLanguage = language;
}
}
return mostSpecificLanguage ?: @"en";
if ([mostSpecificLanguage isEqualToString:@"mul"]) {
return acceptsEnglish ? @"en" : nil;
}
return mostSpecificLanguage;
}
- (BOOL)isMapboxStreets {

View File

@@ -8,7 +8,8 @@ NS_ASSUME_NONNULL_BEGIN
+ (NS_SET_OF(NSString *) *)mapboxStreetsLanguages;
+ (NSString *)preferredMapboxStreetsLanguage;
+ (nullable NSString *)preferredMapboxStreetsLanguage;
+ (nullable NSString *)preferredMapboxStreetsLanguageForPreferences:(NSArray<NSString *> *)preferencesArray;
- (NS_DICTIONARY_OF(NSString *, NSString *) *)localizedKeysByKeyForPreferredLanguage:(nullable NSString *)preferredLanguage;

View File

@@ -44,6 +44,22 @@ NS_ASSUME_NONNULL_BEGIN
*/
@property (nonatomic, readonly) id mgl_jsonExpressionObject;
/**
Returns a copy of the receiver localized into the given locale.
This method assumes the receiver refers to the feature attributes that are
available in vector tiles supplied by the
<a href="https://www.mapbox.com/vector-tiles/mapbox-streets-v7/#overview">Mapbox Streets source</a>.
On iOS, the user can set the system’s preferred language in Settings, General
Settings, Language & Region. On macOS, the user can set the system’s preferred
language in the Language & Region pane of System Preferences.
@param locale The locale into which labels should be localized. To use the
system’s preferred language, if supported, specify `nil`. To use the local
language, specify a locale with the identifier `mul`.
*/
- (NSExpression *)mgl_expressionLocalizedIntoLocale:(nullable NSLocale *)locale;
@end
NS_ASSUME_NONNULL_END

View File

@@ -10,6 +10,7 @@
#endif
#import "NSPredicate+MGLAdditions.h"
#import "NSValue+MGLStyleAttributeAdditions.h"
#import "MGLVectorTileSource_Private.h"
#import <objc/runtime.h>
@@ -328,6 +329,112 @@
return {};
}
// Selectors of functions that can contain tokens in arguments.
static NSArray * const MGLTokenizedFunctions = @[
@"mgl_interpolateWithCurveType:parameters:stops:",
@"mgl_interpolate:withCurveType:parameters:stops:",
@"mgl_stepWithMinimum:stops:",
@"mgl_step:from:stops:",
];
/**
Returns a copy of the given collection with tokens replaced by key path
expressions.
If no replacements take place, this method returns the original collection.
*/
NS_ARRAY_OF(NSExpression *) *MGLCollectionByReplacingTokensWithKeyPaths(NS_ARRAY_OF(NSExpression *) *collection) {
__block NSMutableArray *upgradedCollection;
[collection enumerateObjectsUsingBlock:^(NSExpression * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
NSExpression *upgradedItem = item.mgl_expressionByReplacingTokensWithKeyPaths;
if (upgradedItem != item) {
if (!upgradedCollection) {
upgradedCollection = [collection mutableCopy];
}
upgradedCollection[idx] = upgradedItem;
}
}];
return upgradedCollection ?: collection;
};
/**
Returns a copy of the given stop dictionary with tokens replaced by key path
expressions.
If no replacements take place, this method returns the original stop
dictionary.
*/
NS_DICTIONARY_OF(NSNumber *, NSExpression *) *MGLStopDictionaryByReplacingTokensWithKeyPaths(NS_DICTIONARY_OF(NSNumber *, NSExpression *) *stops) {
__block NSMutableDictionary *upgradedStops;
[stops enumerateKeysAndObjectsUsingBlock:^(id _Nonnull zoomLevel, NSExpression * _Nonnull value, BOOL * _Nonnull stop) {
if (![value isKindOfClass:[NSExpression class]]) {
value = [NSExpression expressionForConstantValue:value];
}
NSExpression *upgradedValue = value.mgl_expressionByReplacingTokensWithKeyPaths;
if (upgradedValue != value) {
if (!upgradedStops) {
upgradedStops = [stops mutableCopy];
}
upgradedStops[zoomLevel] = upgradedValue;
}
}];
return upgradedStops ?: stops;
};
- (NSExpression *)mgl_expressionByReplacingTokensWithKeyPaths {
switch (self.expressionType) {
case NSConstantValueExpressionType: {
NSString *constantValue = self.constantValue;
if ([constantValue isKindOfClass:[NSString class]] &&
[constantValue containsString:@"{"] && [constantValue containsString:@"}"]) {
NSMutableArray *components = [NSMutableArray array];
NSScanner *scanner = [NSScanner scannerWithString:constantValue];
scanner.charactersToBeSkipped = nil;
while (!scanner.isAtEnd) {
NSString *string;
if ([scanner scanUpToString:@"{" intoString:&string]) {
[components addObject:[NSExpression expressionForConstantValue:string]];
}
NSString *token;
if ([scanner scanString:@"{" intoString:NULL]
&& [scanner scanUpToString:@"}" intoString:&token]
&& [scanner scanString:@"}" intoString:NULL]) {
[components addObject:[NSExpression expressionForKeyPath:token]];
}
}
if (components.count == 1) {
return components.firstObject;
}
return [NSExpression expressionForFunction:@"mgl_join:"
arguments:@[[NSExpression expressionForAggregate:components]]];
}
NSDictionary *stops = self.constantValue;
if ([stops isKindOfClass:[NSDictionary class]]) {
NSDictionary *localizedStops = MGLStopDictionaryByReplacingTokensWithKeyPaths(stops);
if (localizedStops != stops) {
return [NSExpression expressionForConstantValue:localizedStops];
}
}
return self;
}
case NSFunctionExpressionType: {
if ([MGLTokenizedFunctions containsObject:self.function]) {
NSArray *arguments = self.arguments;
NSArray *localizedArguments = MGLCollectionByReplacingTokensWithKeyPaths(arguments);
if (localizedArguments != arguments) {
return [NSExpression expressionForFunction:self.operand selectorName:self.function arguments:localizedArguments];
}
}
return self;
}
default:
return self;
}
}
@end
@implementation NSObject (MGLExpressionAdditions)
@@ -1108,4 +1215,126 @@ NSArray *MGLSubexpressionsWithJSONObjects(NSArray *objects) {
return expressionObject;
}
#pragma mark Localization
/**
Returns a localized copy of the given collection.
If no localization takes place, this method returns the original collection.
*/
NS_ARRAY_OF(NSExpression *) *MGLLocalizedCollection(NS_ARRAY_OF(NSExpression *) *collection, NSLocale * _Nullable locale) {
__block NSMutableArray *localizedCollection;
[collection enumerateObjectsUsingBlock:^(NSExpression * _Nonnull item, NSUInteger idx, BOOL * _Nonnull stop) {
NSExpression *localizedItem = [item mgl_expressionLocalizedIntoLocale:locale];
if (localizedItem != item) {
if (!localizedCollection) {
localizedCollection = [collection mutableCopy];
}
localizedCollection[idx] = localizedItem;
}
}];
return localizedCollection ?: collection;
};
/**
Returns a localized copy of the given stop dictionary.
If no localization takes place, this method returns the original stop
dictionary.
*/
NS_DICTIONARY_OF(NSNumber *, NSExpression *) *MGLLocalizedStopDictionary(NS_DICTIONARY_OF(NSNumber *, NSExpression *) *stops, NSLocale * _Nullable locale) {
__block NSMutableDictionary *localizedStops;
[stops enumerateKeysAndObjectsUsingBlock:^(id _Nonnull zoomLevel, NSExpression * _Nonnull value, BOOL * _Nonnull stop) {
if (![value isKindOfClass:[NSExpression class]]) {
value = [NSExpression expressionForConstantValue:value];
}
NSExpression *localizedValue = [value mgl_expressionLocalizedIntoLocale:locale];
if (localizedValue != value) {
if (!localizedStops) {
localizedStops = [stops mutableCopy];
}
localizedStops[zoomLevel] = localizedValue;
}
}];
return localizedStops ?: stops;
};
- (NSExpression *)mgl_expressionLocalizedIntoLocale:(nullable NSLocale *)locale {
switch (self.expressionType) {
case NSConstantValueExpressionType: {
NSDictionary *stops = self.constantValue;
if ([stops isKindOfClass:[NSDictionary class]]) {
NSDictionary *localizedStops = MGLLocalizedStopDictionary(stops, locale);
if (localizedStops != stops) {
return [NSExpression expressionForConstantValue:localizedStops];
}
}
return self;
}
case NSKeyPathExpressionType: {
if ([self.keyPath isEqualToString:@"name"] || [self.keyPath hasPrefix:@"name_"]) {
NSString *localizedKeyPath = @"name";
if (![locale.localeIdentifier isEqualToString:@"mul"]) {
NSArray *preferences = locale ? @[locale.localeIdentifier] : [NSLocale preferredLanguages];
NSString *preferredLanguage = [MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences];
if (preferredLanguage) {
localizedKeyPath = [NSString stringWithFormat:@"name_%@", preferredLanguage];
}
}
return [NSExpression expressionForKeyPath:localizedKeyPath];
}
return self;
}
case NSFunctionExpressionType: {
NSExpression *operand = self.operand;
NSExpression *localizedOperand = [operand mgl_expressionLocalizedIntoLocale:locale];
NSArray *arguments = self.arguments;
NSArray *localizedArguments = MGLLocalizedCollection(arguments, locale);
if (localizedArguments != arguments) {
return [NSExpression expressionForFunction:localizedOperand
selectorName:self.function
arguments:localizedArguments];
}
if (localizedOperand != operand) {
return [NSExpression expressionForFunction:localizedOperand
selectorName:self.function
arguments:self.arguments];
}
return self;
}
case NSConditionalExpressionType: {
if (@available(iOS 9.0, *)) {
NSExpression *trueExpression = self.trueExpression;
NSExpression *localizedTrueExpression = [trueExpression mgl_expressionLocalizedIntoLocale:locale];
NSExpression *falseExpression = self.falseExpression;
NSExpression *localizedFalseExpression = [falseExpression mgl_expressionLocalizedIntoLocale:locale];
if (localizedTrueExpression != trueExpression || localizedFalseExpression != falseExpression) {
return [NSExpression expressionForConditional:self.predicate
trueExpression:localizedTrueExpression
falseExpression:localizedFalseExpression];
}
}
return self;
}
case NSAggregateExpressionType: {
NSArray *collection = self.collection;
if ([collection isKindOfClass:[NSArray class]]) {
NSArray *localizedCollection = MGLLocalizedCollection(collection, locale);
if (localizedCollection != collection) {
return [NSExpression expressionForAggregate:localizedCollection];
}
}
return self;
}
default:
return self;
}
}
@end

View File

@@ -27,6 +27,11 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) mbgl::FeatureIdentifier mgl_featureIdentifier;
@property (nonatomic, readonly) std::vector<mbgl::FeatureIdentifier> mgl_aggregateFeatureIdentifier;
/**
Returns a copy of the receiver with tokens replaced by key path expressions.
*/
- (NSExpression *)mgl_expressionByReplacingTokensWithKeyPaths;
@end
@interface NSNull (MGLExpressionAdditions)
@@ -74,6 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
- (NSExpression *)mgl_expressionWithContext:(NSDictionary<NSString *, NSExpression *> *)context;
- (id)mgl_has:(id)element;
@end

View File

@@ -866,4 +866,144 @@ using namespace std::string_literals;
}
}
#pragma mark - Localization tests
- (void)testTokenReplacement {
{
NSExpression *original = MGLConstantExpression(@"");
NSExpression *expected = original;
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
{
NSExpression *original = MGLConstantExpression(@"{");
NSExpression *expected = original;
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
{
NSExpression *original = MGLConstantExpression(@"{token");
NSExpression *expected = original;
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
{
NSExpression *original = MGLConstantExpression(@"{token}");
NSExpression *expected = [NSExpression expressionForKeyPath:@"token"];
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
{
NSExpression *original = MGLConstantExpression(@"{token} {token}");
NSExpression *expected = [NSExpression expressionWithFormat:@"mgl_join({token, ' ', token})"];
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
{
NSExpression *original = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, '{short}', %@)", @{
@1: MGLConstantExpression(@"{short}"),
@2: @"…",
@3: @"{long}",
}];
NSExpression *expected = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, short, %@)", @{
@1: [NSExpression expressionForKeyPath:@"short"],
@2: @"…",
@3: [NSExpression expressionForKeyPath:@"long"],
}];
XCTAssertEqualObjects(original.mgl_expressionByReplacingTokensWithKeyPaths, expected);
}
}
- (void)testLocalization {
{
NSExpression *original = MGLConstantExpression(@"");
NSExpression *expected = original;
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:nil], expected);
}
{
NSExpression *original = MGLConstantExpression(@"Old MacDonald");
NSExpression *expected = original;
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:nil], expected);
}
{
NSExpression *original = MGLConstantExpression(@"{name_en}");
NSExpression *expected = original;
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:nil], expected);
}
{
NSExpression *original = [NSExpression expressionForKeyPath:@"name_en"];
NSExpression *expected = original;
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:nil], expected);
}
{
NSExpression *original = [NSExpression expressionForKeyPath:@"name_en"];
NSExpression *expected = [NSExpression expressionForKeyPath:@"name"];
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"mul"]], expected);
}
{
NSExpression *original = [NSExpression expressionForKeyPath:@"name_en"];
NSExpression *expected = [NSExpression expressionForKeyPath:@"name_fr"];
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"fr-CA"]], expected);
}
{
NSExpression *original = [NSExpression expressionForKeyPath:@"name_en"];
NSExpression *expected = [NSExpression expressionForKeyPath:@"name_zh-Hans"];
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"zh-Hans"]], expected);
}
{
NSExpression *original = [NSExpression expressionForKeyPath:@"name_en"];
NSExpression *expected = [NSExpression expressionForKeyPath:@"name"];
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"tlh"]], expected);
}
{
NSExpression *original = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, short, %@)", @{
@1: [NSExpression expressionForKeyPath:@"abbr"],
@2: @"…",
@3: [NSExpression expressionForKeyPath:@"name_fr"],
}];
NSExpression *expected = [NSExpression expressionWithFormat:@"mgl_step:from:stops:($zoomLevel, short, %@)", @{
@1: [NSExpression expressionForKeyPath:@"abbr"],
@2: @"…",
@3: [NSExpression expressionForKeyPath:@"name_es"],
}];
XCTAssertEqualObjects([original mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"es-PR"]], expected);
}
{
NSArray *jsonExpression = @[
@"step",
@[@"zoom"],
@[
@"case",
@[
@"<",
@[
@"to-number",
@[@"get", @"area"]
],
@80000
],
@[@"get", @"abbr"],
@[@"get", @"name_en"]
],
@5, @[@"get", @"name_en"]
];
NSArray *localizedJSONExpression = @[
@"step",
@[@"zoom"],
@[
@"case",
@[
@"<",
@[
@"to-number",
@[@"get", @"area"]
],
@80000
],
@[@"get", @"abbr"],
@[@"get", @"name"]
],
@5, @[@"get", @"name"]
];
NSExpression *expression = [NSExpression expressionWithMGLJSONObject:jsonExpression];
NSExpression *localizedExpression = [expression mgl_expressionLocalizedIntoLocale:[NSLocale localeWithLocaleIdentifier:@"mul"]];
XCTAssertEqualObjects(localizedExpression.mgl_jsonExpressionObject, localizedJSONExpression);
}
}
@end

View File

@@ -1,6 +1,7 @@
#import <Mapbox/Mapbox.h>
#import "NSBundle+MGLAdditions.h"
#import "MGLVectorTileSource_Private.h"
#import <mbgl/util/default_styles.hpp>
@@ -419,4 +420,45 @@
XCTAssertEqualObjects(layers[startIndex++].identifier, layer4.identifier);
}
#pragma mark Localization tests
- (void)testLanguageMatching {
{
NSArray *preferences = @[@"en"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"en");
}
{
NSArray *preferences = @[@"en-US"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"en");
}
{
NSArray *preferences = @[@"fr"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"fr");
}
{
NSArray *preferences = @[@"zh-Hans"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"zh-Hans");
}
{
NSArray *preferences = @[@"zh-Hans", @"en"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"zh-Hans");
}
{
NSArray *preferences = @[@"zh-Hant"];
XCTAssertNil([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences]);
}
{
NSArray *preferences = @[@"tlh"];
XCTAssertNil([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences]);
}
{
NSArray *preferences = @[@"tlh", @"en"];
XCTAssertEqualObjects([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences], @"en");
}
{
NSArray *preferences = @[@"mul"];
XCTAssertNil([MGLVectorTileSource preferredMapboxStreetsLanguageForPreferences:preferences]);
}
}
@end

View File

@@ -12,16 +12,16 @@ The 4.0._x_ series of releases will be the last to support iOS 8. The minimum iO
* Added Danish and Hebrew localizations. ([#10967](https://github.com/mapbox/mapbox-gl-native/pull/10967), [#11136](https://github.com/mapbox/mapbox-gl-native/pull/11134))
* Removed methods, properties, and constants that had been deprecated as of v3.7.4. ([#11205](https://github.com/mapbox/mapbox-gl-native/pull/11205))
### Styles and rendering
### Styles
* Added support for a new layer type: `MGLHeatmapStyleLayer`, a powerful way to visualize point data distributions using heatmaps, fully customizable through runtime styling. [#11046](https://github.com/mapbox/mapbox-gl-native/pull/11046)
* The layout and paint properties on subclasses of `MGLStyleLayer` are now of type `NSExpression` instead of `MGLStyleValue`. A new “Predicates and Expressions” guide provides an overview of the supported operators. ([#10726](https://github.com/mapbox/mapbox-gl-native/pull/10726))
* Renamed `MGLRasterSource` to `MGLRasterTileSource` and `MGLVectorSource` to `MGLVectorTileSource`. ([#11568](https://github.com/mapbox/mapbox-gl-native/pull/11568))
* Added an `MGLComputedShapeSource` class that allows applications to supply vector data to a style layer on a per-tile basis. ([#9983](https://github.com/mapbox/mapbox-gl-native/pull/9983))
* A style can now display smooth hillshading and customize its appearance at runtime using the `MGLHillshadeStyleLayer` class. Hillshading is based on a rasterized digital elevation model supplied by the `MGLRasterDEMSource` class. ([#10642](https://github.com/mapbox/mapbox-gl-native/pull/10642))
* Replaced the `MGLStyle.localizesLabels` property with an `-[MGLStyle localizeLabelsIntoLocale:]` method that allows you to specify the language to localize into. Also added an `-[NSExpression(MGLAdditions) mgl_expressionLocalizedIntoLocale:]` method for localizing an individual value used with `MGLSymbolStyleLayer.text`. ([#11651](https://github.com/mapbox/mapbox-gl-native/pull/11651))
* The `MGLSymbolStyleLayer.textFontNames` property can now depend on a feature’s attributes. ([#10850](https://github.com/mapbox/mapbox-gl-native/pull/10850))
* Added `MGLShapeSourceOptionWrapsCoordinates`, to specify whether the shape of an `MGLComputedShapeSource` should be wrapped to accommodate coordinates with longitudes beyond −180 and 180; and `MGLShapeSourceOptionClipsCoordinates`, to specify whether the shape of an `MGLComputedShapeSource` should be clipped at the edge of each tile. ([#11041](https://github.com/mapbox/mapbox-gl-native/pull/11041))
* Added support for Mapzen Terrarium DEM encoding. ([#11274](https://github.com/mapbox/mapbox-gl-native/pull/11274))
* The layer filtering predicate's format strings now can contain arithmetic and calls to built-in `NSExpression` functions. ([#11587](https://github.com/mapbox/mapbox-gl-native/pull/11587))
* The layer filtering predicate's key paths now may need to be cast to `NSString` or `NSNumber`. ([#11587](https://github.com/mapbox/mapbox-gl-native/pull/11587))

View File

@@ -92,7 +92,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
MBXSettingsMiscellaneousShowZoomLevel,
MBXSettingsMiscellaneousScrollView,
MBXSettingsMiscellaneousToggleTwoMaps,
MBXSettingsMiscellaneousCountryLabels,
MBXSettingsMiscellaneousLocalizeLabels,
MBXSettingsMiscellaneousShowSnapshots,
MBXSettingsMiscellaneousShouldLimitCameraChanges,
MBXSettingsMiscellaneousPrintLogFile,
@@ -130,7 +130,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
@property (nonatomic) NSInteger styleIndex;
@property (nonatomic) BOOL debugLoggingEnabled;
@property (nonatomic) BOOL customUserLocationAnnnotationEnabled;
@property (nonatomic) BOOL usingLocaleBasedCountryLabels;
@property (nonatomic, getter=isLocalizingLabels) BOOL localizingLabels;
@property (nonatomic) BOOL reuseQueueStatsEnabled;
@property (nonatomic) BOOL showZoomLevelEnabled;
@property (nonatomic) BOOL shouldLimitCameraChanges;
@@ -139,7 +139,6 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
@interface MGLMapView (MBXViewController)
@property (nonatomic) BOOL usingLocaleBasedCountryLabels;
@property (nonatomic) NSDictionary *annotationViewReuseQueueByIdentifier;
@end
@@ -383,7 +382,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
[NSString stringWithFormat:@"%@ Zoom/Pitch/Direction Label", (_showZoomLevelEnabled ? @"Hide" :@"Show")],
@"Embedded Map View",
[NSString stringWithFormat:@"%@ Second Map", ([self.view viewWithTag:2] == nil ? @"Show" : @"Hide")],
[NSString stringWithFormat:@"Show Labels in %@", (_usingLocaleBasedCountryLabels ? @"Default Language" : [[NSLocale currentLocale] displayNameForKey:NSLocaleIdentifier value:[self bestLanguageForUser]])],
[NSString stringWithFormat:@"Show Labels in %@", (_localizingLabels ? @"Default Language" : [[NSLocale currentLocale] displayNameForKey:NSLocaleIdentifier value:[self bestLanguageForUser]])],
@"Show Snapshots",
[NSString stringWithFormat:@"%@ Camera Changes", (_shouldLimitCameraChanges ? @"Unlimit" : @"Limit")],
]];
@@ -574,7 +573,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
case MBXSettingsMiscellaneous:
switch (indexPath.row)
{
case MBXSettingsMiscellaneousCountryLabels:
case MBXSettingsMiscellaneousLocalizeLabels:
[self styleCountryLabelsLanguage];
break;
case MBXSettingsMiscellaneousWorldTour:
@@ -1400,8 +1399,8 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
-(void)styleCountryLabelsLanguage
{
_usingLocaleBasedCountryLabels = !_usingLocaleBasedCountryLabels;
self.mapView.style.localizesLabels = _usingLocaleBasedCountryLabels;
_localizingLabels = !_localizingLabels;
[self.mapView.style localizeLabelsIntoLocale:_localizingLabels ? [NSLocale localeWithLocaleIdentifier:@"mul"] : nil];
}
- (void)styleRouteLine
@@ -1984,7 +1983,7 @@ typedef NS_ENUM(NSInteger, MBXSettingsMiscellaneousRows) {
// Default Mapbox styles use {name_en} as their label language, which means
// that a device with an English-language locale is already effectively
// using locale-based country labels.
_usingLocaleBasedCountryLabels = [[self bestLanguageForUser] isEqualToString:@"en"];
_localizingLabels = [[self bestLanguageForUser] isEqualToString:@"en"];
}
- (BOOL)mapView:(MGLMapView *)mapView shouldChangeFromCamera:(MGLMapCamera *)oldCamera toCamera:(MGLMapCamera *)newCamera {

View File

@@ -9,6 +9,7 @@
* Renamed `MGLRasterSource` to `MGLRasterTileSource` and `MGLVectorSource` to `MGLVectorTileSource`. ([#11568](https://github.com/mapbox/mapbox-gl-native/pull/11568))
* Added an `MGLComputedShapeSource` class that allows applications to supply vector data to a style layer on a per-tile basis. ([#9983](https://github.com/mapbox/mapbox-gl-native/pull/9983))
* A style can now display smooth hillshading and customize its appearance at runtime using the `MGLHillshadeStyleLayer` class. Hillshading is based on a rasterized digital elevation model supplied by the `MGLRasterDEMSource` class. ([#10642](https://github.com/mapbox/mapbox-gl-native/pull/10642))
* Replaced the `MGLStyle.localizesLabels` property with an `-[MGLStyle localizeLabelsIntoLocale:]` method that allows you to specify the language to localize into. Note that this method does not automatically update the style when the system’s preferred language changes. Also added an `-[NSExpression(MGLAdditions) mgl_expressionLocalizedIntoLocale:]` method for localizing an individual value used with `MGLSymbolStyleLayer.text`. ([#11651](https://github.com/mapbox/mapbox-gl-native/pull/11651))
* Fixed incorrect color calibration on macOS 10.13 High Sierra when using color-related methods of `MGLStyleLayer` subclasses, as well as when displaying an `MGLAttributionInfo`. It is no longer necessary to explicitly convert an `NSColor` to the sRGB color space before using these classes on High Sierra. ([#11391](https://github.com/mapbox/mapbox-gl-native/pull/11391))
* The `MGLSymbolStyleLayer.textFontNames` property can now depend on a feature’s attributes. ([#10850](https://github.com/mapbox/mapbox-gl-native/pull/10850))
* Properties such as `MGLSymbolStyleLayer.iconAllowsOverlap` and `MGLSymbolStyleLayer.iconIgnoresPlacement` now account for symbols in other sources. ([#10436](https://github.com/mapbox/mapbox-gl-native/pull/10436))

View File

@@ -421,7 +421,7 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio
}
- (void)updateLabels {
self.mapView.style.localizesLabels = _isLocalizingLabels;
[self.mapView.style localizeLabelsIntoLocale:_isLocalizingLabels ? nil : [NSLocale localeWithLocaleIdentifier:@"mul"]];
}
- (void)applyPendingState {
@@ -1067,7 +1067,7 @@ NS_ARRAY_OF(id <MGLAnnotation>) *MBXFlattenedShapes(NS_ARRAY_OF(id <MGLAnnotatio
menuItem.state = menuItem.tag == _isLocalizingLabels ? NSOnState: NSOffState;
if (menuItem.tag) {
NSLocale *locale = [NSLocale localeWithLocaleIdentifier:[NSBundle mainBundle].developmentLocalization];
NSString *preferredLanguage = [MGLVectorTileSource preferredMapboxStreetsLanguage];
NSString *preferredLanguage = [MGLVectorTileSource preferredMapboxStreetsLanguage] ?: @"en";
menuItem.title = [locale displayNameForKey:NSLocaleIdentifier value:preferredLanguage];
}
return YES;

View File

@@ -26,7 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
language = "en"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
@@ -56,7 +56,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"