diff options
20 files changed, 535 insertions, 102 deletions
diff --git a/platform/darwin/src/MGLBackgroundStyleLayer.mm b/platform/darwin/src/MGLBackgroundStyleLayer.mm index 7c4cf79709..993645d3a8 100644 --- a/platform/darwin/src/MGLBackgroundStyleLayer.mm +++ b/platform/darwin/src/MGLBackgroundStyleLayer.mm @@ -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 { diff --git a/platform/darwin/src/MGLFillExtrusionStyleLayer.mm b/platform/darwin/src/MGLFillExtrusionStyleLayer.mm index 1baa264689..688ce4c1ac 100644 --- a/platform/darwin/src/MGLFillExtrusionStyleLayer.mm +++ b/platform/darwin/src/MGLFillExtrusionStyleLayer.mm @@ -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 { diff --git a/platform/darwin/src/MGLFillStyleLayer.mm b/platform/darwin/src/MGLFillStyleLayer.mm index ab28b414b5..c975e28d6b 100644 --- a/platform/darwin/src/MGLFillStyleLayer.mm +++ b/platform/darwin/src/MGLFillStyleLayer.mm @@ -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 { diff --git a/platform/darwin/src/MGLLineStyleLayer.mm b/platform/darwin/src/MGLLineStyleLayer.mm index 4e6ff27b39..619cc70afe 100644 --- a/platform/darwin/src/MGLLineStyleLayer.mm +++ b/platform/darwin/src/MGLLineStyleLayer.mm @@ -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 { diff --git a/platform/darwin/src/MGLStyle.h b/platform/darwin/src/MGLStyle.h index eb7554534f..cb0cdbac71 100644 --- a/platform/darwin/src/MGLStyle.h +++ b/platform/darwin/src/MGLStyle.h @@ -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. - */ -@property (nonatomic) BOOL localizesLabels; + 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`. + */ +- (void)localizeLabelsIntoLocale:(nullable NSLocale *)locale; @end diff --git a/platform/darwin/src/MGLStyle.mm b/platform/darwin/src/MGLStyle.mm index 0162dbd354..867ac6c451 100644 --- a/platform/darwin/src/MGLStyle.mm +++ b/platform/darwin/src/MGLStyle.mm @@ -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; + } } } diff --git a/platform/darwin/src/MGLStyleLayer.mm.ejs b/platform/darwin/src/MGLStyleLayer.mm.ejs index ac7676a1cc..42940083b5 100644 --- a/platform/darwin/src/MGLStyleLayer.mm.ejs +++ b/platform/darwin/src/MGLStyleLayer.mm.ejs @@ -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"]) { -%> diff --git a/platform/darwin/src/MGLSymbolStyleLayer.mm b/platform/darwin/src/MGLSymbolStyleLayer.mm index 24d6643e6c..0d9fac4808 100644 --- a/platform/darwin/src/MGLSymbolStyleLayer.mm +++ b/platform/darwin/src/MGLSymbolStyleLayer.mm @@ -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 { diff --git a/platform/darwin/src/MGLVectorTileSource.mm b/platform/darwin/src/MGLVectorTileSource.mm index 5678d9c0bb..c1f7267e4a 100644 --- a/platform/darwin/src/MGLVectorTileSource.mm +++ b/platform/darwin/src/MGLVectorTileSource.mm @@ -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 { diff --git a/platform/darwin/src/MGLVectorTileSource_Private.h b/platform/darwin/src/MGLVectorTileSource_Private.h index 41873b4ff6..77521869f1 100644 --- a/platform/darwin/src/MGLVectorTileSource_Private.h +++ b/platform/darwin/src/MGLVectorTileSource_Private.h @@ -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; diff --git a/platform/darwin/src/NSExpression+MGLAdditions.h b/platform/darwin/src/NSExpression+MGLAdditions.h index 66d306ad06..a0081cb428 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.h +++ b/platform/darwin/src/NSExpression+MGLAdditions.h @@ -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 diff --git a/platform/darwin/src/NSExpression+MGLAdditions.mm b/platform/darwin/src/NSExpression+MGLAdditions.mm index c175868dae..be6eef7f47 100644 --- a/platform/darwin/src/NSExpression+MGLAdditions.mm +++ b/platform/darwin/src/NSExpression+MGLAdditions.mm @@ -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 diff --git a/platform/darwin/src/NSExpression+MGLPrivateAdditions.h b/platform/darwin/src/NSExpression+MGLPrivateAdditions.h index d255cfdb0f..a1948f9e45 100644 --- a/platform/darwin/src/NSExpression+MGLPrivateAdditions.h +++ b/platform/darwin/src/NSExpression+MGLPrivateAdditions.h @@ -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 diff --git a/platform/darwin/test/MGLExpressionTests.mm b/platform/darwin/test/MGLExpressionTests.mm index b050c5f652..c91cf36d1a 100644 --- a/platform/darwin/test/MGLExpressionTests.mm +++ b/platform/darwin/test/MGLExpressionTests.mm @@ -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 diff --git a/platform/darwin/test/MGLStyleTests.mm b/platform/darwin/test/MGLStyleTests.mm index 7b1cc56ba4..6048f39ea3 100644 --- a/platform/darwin/test/MGLStyleTests.mm +++ b/platform/darwin/test/MGLStyleTests.mm @@ -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 diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 6eec7c0e48..f9644e8817 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -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)) diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m index 07d684cd64..0a76f8c0c5 100644 --- a/platform/ios/app/MBXViewController.m +++ b/platform/ios/app/MBXViewController.m @@ -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 { diff --git a/platform/macos/CHANGELOG.md b/platform/macos/CHANGELOG.md index 9df33211af..0cfba09455 100644 --- a/platform/macos/CHANGELOG.md +++ b/platform/macos/CHANGELOG.md @@ -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)) diff --git a/platform/macos/app/MapDocument.m b/platform/macos/app/MapDocument.m index 2a65ee144c..9b18dbd761 100644 --- a/platform/macos/app/MapDocument.m +++ b/platform/macos/app/MapDocument.m @@ -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; diff --git a/platform/macos/macos.xcodeproj/xcshareddata/xcschemes/macosapp.xcscheme b/platform/macos/macos.xcodeproj/xcshareddata/xcschemes/macosapp.xcscheme index b0ce01fbf0..aab7486fbe 100644 --- a/platform/macos/macos.xcodeproj/xcshareddata/xcschemes/macosapp.xcscheme +++ b/platform/macos/macos.xcodeproj/xcshareddata/xcschemes/macosapp.xcscheme @@ -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" |