From 0806e16eb390d9eb7db3157e23798f4010614aca Mon Sep 17 00:00:00 2001 From: Jason Wray Date: Tue, 9 Jan 2018 16:09:41 -0500 Subject: [ios] Fix and expand accessibility feature transliteration - Fixed pre-iOS 11 compatibility. - Expanded transliteration to all supported languages. --- platform/darwin/src/MGLVectorSource_Private.h | 2 ++ platform/darwin/src/NSString+MGLAdditions.h | 14 +++++++++ platform/darwin/src/NSString+MGLAdditions.m | 19 ++++++++++++ platform/darwin/test/MGLNSStringAdditionsTests.m | 34 ++++++++++++++++++++++ platform/ios/ios.xcodeproj/project.pbxproj | 16 ++++++++++ platform/ios/src/MGLMapAccessibilityElement.mm | 24 ++++++--------- platform/ios/src/NSOrthography+MGLAdditions.h | 18 ++++++++++++ platform/ios/src/NSOrthography+MGLAdditions.m | 31 ++++++++++++++++++++ .../ios/test/MGLMapAccessibilityElementTests.m | 8 +++-- platform/ios/test/MGLNSOrthographyAdditionsTests.m | 19 ++++++++++++ 10 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 platform/ios/src/NSOrthography+MGLAdditions.h create mode 100644 platform/ios/src/NSOrthography+MGLAdditions.m create mode 100644 platform/ios/test/MGLNSOrthographyAdditionsTests.m diff --git a/platform/darwin/src/MGLVectorSource_Private.h b/platform/darwin/src/MGLVectorSource_Private.h index 233809aea2..7d19e03a99 100644 --- a/platform/darwin/src/MGLVectorSource_Private.h +++ b/platform/darwin/src/MGLVectorSource_Private.h @@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, getter=isMapboxStreets) BOOL mapboxStreets; ++ (NS_SET_OF(NSString *) *)mapboxStreetsLanguages; + + (NSString *)preferredMapboxStreetsLanguage; - (NS_DICTIONARY_OF(NSString *, NSString *) *)localizedKeysByKeyForPreferredLanguage:(nullable NSString *)preferredLanguage; diff --git a/platform/darwin/src/NSString+MGLAdditions.h b/platform/darwin/src/NSString+MGLAdditions.h index d82ecaa671..75c593c10b 100644 --- a/platform/darwin/src/NSString+MGLAdditions.h +++ b/platform/darwin/src/NSString+MGLAdditions.h @@ -20,6 +20,20 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSString *)mgl_titleCasedStringWithLocale:(NSLocale *)locale; +/** + Returns a transliterated representation of the receiver using the specified + script. If transliteration fails, the receiver will be returned. + + Only supports scripts for languages used by Mapbox Streets. + + On iOS 8 or older, this will method will always return the untransliterated + receiver. + + @param script The four-letter code representing the name of the script, as + specified by ISO 15924. + */ +- (NSString *)mgl_stringByTransliteratingIntoScript:(NSString *)script; + @end @interface NSAttributedString (MGLAdditions) diff --git a/platform/darwin/src/NSString+MGLAdditions.m b/platform/darwin/src/NSString+MGLAdditions.m index cde4bddcc3..8c9bbe3e21 100644 --- a/platform/darwin/src/NSString+MGLAdditions.m +++ b/platform/darwin/src/NSString+MGLAdditions.m @@ -41,6 +41,25 @@ return string; } +- (NSString *)mgl_stringByTransliteratingIntoScript:(NSString *)script { + if (@available(iOS 9.0, *)) { + NSMutableString *string = self.mutableCopy; + NSStringTransform transform; + if ([script isEqualToString:@"Latn"]) { + transform = NSStringTransformToLatin; + } else if ([script isEqualToString:@"Hans"]) { + // No transform available. + } else if ([script isEqualToString:@"Cyrl"]) { + transform = @"Any-Latin; Latin-Cyrillic"; + } else if ([script isEqualToString:@"Arab"]) { + transform = @"Any-Latin; Latin-Arabic"; + } + return transform ? [string stringByApplyingTransform:transform reverse:NO] : string; + } else { + return self; + } +} + @end @implementation NSAttributedString (MGLAdditions) diff --git a/platform/darwin/test/MGLNSStringAdditionsTests.m b/platform/darwin/test/MGLNSStringAdditionsTests.m index 03503b7f8a..2a8715d991 100644 --- a/platform/darwin/test/MGLNSStringAdditionsTests.m +++ b/platform/darwin/test/MGLNSStringAdditionsTests.m @@ -39,4 +39,38 @@ XCTAssertEqualObjects([@"Improve This iPhone" mgl_titleCasedStringWithLocale:locale], @"Improve This iPhone"); } +- (void)testTransliteratedString { + if (@available(iOS 9.0, *)) { + XCTAssertEqualObjects([@"Portland" mgl_stringByTransliteratingIntoScript:@"Latn"], @"Portland"); + XCTAssertEqualObjects([@"Portland" mgl_stringByTransliteratingIntoScript:@"Hans"], @"Portland"); + XCTAssertEqualObjects([@"Portland" mgl_stringByTransliteratingIntoScript:@"Cyrl"], @"Портланд"); + XCTAssertEqualObjects([@"Portland" mgl_stringByTransliteratingIntoScript:@"Arab"], @"پُرتلَند"); + XCTAssertEqualObjects([@"Portland" mgl_stringByTransliteratingIntoScript:@"Fake"], @"Portland"); + + XCTAssertEqualObjects([@"北京" mgl_stringByTransliteratingIntoScript:@"Latn"], @"běi jīng"); + XCTAssertEqualObjects([@"北京" mgl_stringByTransliteratingIntoScript:@"Hans"], @"北京"); + XCTAssertEqualObjects([@"北京" mgl_stringByTransliteratingIntoScript:@"Cyrl"], @"бе̌и йӣнг"); + XCTAssertEqualObjects([@"北京" mgl_stringByTransliteratingIntoScript:@"Arab"], @"بِِ̌ جِينگ"); + XCTAssertEqualObjects([@"北京" mgl_stringByTransliteratingIntoScript:@"Fake"], @"北京"); + + XCTAssertEqualObjects([@"Mосква" mgl_stringByTransliteratingIntoScript:@"Latn"], @"Moskva"); + XCTAssertEqualObjects([@"Mосква" mgl_stringByTransliteratingIntoScript:@"Hans"], @"Mосква"); + XCTAssertEqualObjects([@"Mосква" mgl_stringByTransliteratingIntoScript:@"Cyrl"], @"Москва"); + XCTAssertEqualObjects([@"Mосква" mgl_stringByTransliteratingIntoScript:@"Arab"], @"مُسكڤَ"); + XCTAssertEqualObjects([@"Mосква" mgl_stringByTransliteratingIntoScript:@"Fake"], @"Mосква"); + + XCTAssertEqualObjects([@"ロンドン" mgl_stringByTransliteratingIntoScript:@"Latn"], @"rondon"); + XCTAssertEqualObjects([@"ロンドン" mgl_stringByTransliteratingIntoScript:@"Hans"], @"ロンドン"); + XCTAssertEqualObjects([@"ロンドン" mgl_stringByTransliteratingIntoScript:@"Cyrl"], @"рондон"); + XCTAssertEqualObjects([@"ロンドン" mgl_stringByTransliteratingIntoScript:@"Arab"], @"رُندُن"); + XCTAssertEqualObjects([@"ロンドン" mgl_stringByTransliteratingIntoScript:@"Fake"], @"ロンドン"); + } else { + XCTAssertEqualObjects([@"Made-up Place" mgl_stringByTransliteratingIntoScript:@"Latn"], @"Made-up Place"); + XCTAssertEqualObjects([@"Made-up Place" mgl_stringByTransliteratingIntoScript:@"Hans"], @"Made-up Place"); + XCTAssertEqualObjects([@"Made-up Place" mgl_stringByTransliteratingIntoScript:@"Cyrl"], @"Made-up Place"); + XCTAssertEqualObjects([@"Made-up Place" mgl_stringByTransliteratingIntoScript:@"Arab"], @"Made-up Place"); + XCTAssertEqualObjects([@"Made-up Place" mgl_stringByTransliteratingIntoScript:@"Fake"], @"Made-up Place"); + } +} + @end diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 324ab1fc8f..fb268fab1d 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -227,6 +227,11 @@ 927FBD021F4DB05500F8BF1F /* MGLMapSnapshotter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */; }; 929EFFAB1F56DCD4003A77D5 /* MGLAnnotationView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4018B1C41CDC277F00F666AF /* MGLAnnotationView.mm */; }; 92F2C3ED1F0E3C3A00268EC0 /* MGLRendererFrontend.h in Headers */ = {isa = PBXBuildFile; fileRef = 92F2C3EC1F0E3C3A00268EC0 /* MGLRendererFrontend.h */; }; + 96036A01200565C700510F3D /* NSOrthography+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 960369FF200565C700510F3D /* NSOrthography+MGLAdditions.h */; }; + 96036A02200565C700510F3D /* NSOrthography+MGLAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 960369FF200565C700510F3D /* NSOrthography+MGLAdditions.h */; }; + 96036A03200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96036A00200565C700510F3D /* NSOrthography+MGLAdditions.m */; }; + 96036A04200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96036A00200565C700510F3D /* NSOrthography+MGLAdditions.m */; }; + 96036A0620059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 96036A0520059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m */; }; 960D0C361ECF5AAF008E151F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 960D0C351ECF5AAF008E151F /* Images.xcassets */; }; 960D0C371ECF5AAF008E151F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 960D0C351ECF5AAF008E151F /* Images.xcassets */; }; 9620BB381E69FE1700705A1D /* MGLSDKUpdateChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = 9620BB361E69FE1700705A1D /* MGLSDKUpdateChecker.h */; }; @@ -764,6 +769,9 @@ 927FBCFD1F4DB05500F8BF1F /* MGLMapSnapshotter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMapSnapshotter.h; sourceTree = ""; }; 927FBCFE1F4DB05500F8BF1F /* MGLMapSnapshotter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapSnapshotter.mm; sourceTree = ""; }; 92F2C3EC1F0E3C3A00268EC0 /* MGLRendererFrontend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLRendererFrontend.h; sourceTree = ""; }; + 960369FF200565C700510F3D /* NSOrthography+MGLAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSOrthography+MGLAdditions.h"; sourceTree = ""; }; + 96036A00200565C700510F3D /* NSOrthography+MGLAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSOrthography+MGLAdditions.m"; sourceTree = ""; }; + 96036A0520059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLNSOrthographyAdditionsTests.m; sourceTree = ""; }; 960D0C351ECF5AAF008E151F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 9620BB361E69FE1700705A1D /* MGLSDKUpdateChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLSDKUpdateChecker.h; sourceTree = ""; }; 9620BB371E69FE1700705A1D /* MGLSDKUpdateChecker.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; fileEncoding = 4; path = MGLSDKUpdateChecker.mm; sourceTree = ""; }; @@ -1231,6 +1239,8 @@ children = ( 357FE2DB1E02D2B20068B753 /* NSCoder+MGLAdditions.h */, 357FE2DC1E02D2B20068B753 /* NSCoder+MGLAdditions.mm */, + 960369FF200565C700510F3D /* NSOrthography+MGLAdditions.h */, + 96036A00200565C700510F3D /* NSOrthography+MGLAdditions.m */, 35CE61801D4165D9004F2359 /* UIColor+MGLAdditions.h */, 35CE61811D4165D9004F2359 /* UIColor+MGLAdditions.mm */, 30E578111DAA7D690050F07E /* UIImage+MGLAdditions.h */, @@ -1413,6 +1423,7 @@ 35E208A61D24210F00EC9A46 /* MGLNSDataAdditionsTests.m */, 1F95931C1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm */, DAE7DEC11E245455007505A6 /* MGLNSStringAdditionsTests.m */, + 96036A0520059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m */, DA2E885D1CC0382C00F24E7B /* MGLOfflinePackTests.m */, DA2E885E1CC0382C00F24E7B /* MGLOfflineRegionTests.m */, 55E2AD121E5B125400E8C587 /* MGLOfflineStorageTests.mm */, @@ -1871,6 +1882,7 @@ 353933F21D3FB753003F57D7 /* MGLCircleStyleLayer.h in Headers */, DA8847F31CBAFA5100AB86E3 /* MGLMultiPoint.h in Headers */, 30E578171DAA85520050F07E /* UIImage+MGLAdditions.h in Headers */, + 96036A01200565C700510F3D /* NSOrthography+MGLAdditions.h in Headers */, 1F7454961ECD450D00021D39 /* MGLLight_Private.h in Headers */, DAD1656C1CF41981001FF4B9 /* MGLFeature.h in Headers */, 40EDA1C01CFE0E0200D9EA68 /* MGLAnnotationContainerView.h in Headers */, @@ -1925,6 +1937,7 @@ DABFB8641CBE99E500D62B32 /* MGLOfflineStorage.h in Headers */, 96E516E32000552A00A02306 /* MGLAccountManager_Private.h in Headers */, 96E5170420005A6B00A02306 /* SMCalloutView.h in Headers */, + 96036A02200565C700510F3D /* NSOrthography+MGLAdditions.h in Headers */, DAD165791CF4CDFF001FF4B9 /* MGLShapeCollection.h in Headers */, 4049C29E1DB6CD6C00B3F799 /* MGLPointCollection.h in Headers */, 3566C7671D4A77BA008152BC /* MGLShapeSource.h in Headers */, @@ -2378,6 +2391,7 @@ 40CFA6511D7875BB008103BD /* MGLShapeSourceTests.mm in Sources */, DA35A2C51CCA9F8300E826B2 /* MGLClockDirectionFormatterTests.m in Sources */, 35B8E08C1D6C8B5100E768D2 /* MGLPredicateTests.mm in Sources */, + 96036A0620059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m in Sources */, 1F95931D1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm in Sources */, DD58A4C61D822BD000E1F038 /* MGLExpressionTests.mm in Sources */, 3575798B1D502B0C000B822E /* MGLBackgroundStyleLayerTests.mm in Sources */, @@ -2420,6 +2434,7 @@ DA6408DD1DA4E7D300908C90 /* MGLVectorStyleLayer.m in Sources */, 3566C7681D4A77BA008152BC /* MGLShapeSource.mm in Sources */, 400533021DB0862B0069F638 /* NSArray+MGLAdditions.mm in Sources */, + 96036A03200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */, 35136D421D42274500C20EFD /* MGLRasterStyleLayer.mm in Sources */, 3538AA1F1D542239008EC33D /* MGLForegroundStyleLayer.mm in Sources */, DA00FC901D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, @@ -2508,6 +2523,7 @@ 3566C7691D4A77BA008152BC /* MGLShapeSource.mm in Sources */, 400533031DB086490069F638 /* NSArray+MGLAdditions.mm in Sources */, 35136D431D42274500C20EFD /* MGLRasterStyleLayer.mm in Sources */, + 96036A04200565C700510F3D /* NSOrthography+MGLAdditions.m in Sources */, 3538AA201D542239008EC33D /* MGLForegroundStyleLayer.mm in Sources */, DA00FC911D5EEB0D009AABC8 /* MGLAttributionInfo.mm in Sources */, DAA4E4201CBB730400178DFB /* MGLOfflinePack.mm in Sources */, diff --git a/platform/ios/src/MGLMapAccessibilityElement.mm b/platform/ios/src/MGLMapAccessibilityElement.mm index 1a2953b0bb..79dcda4054 100644 --- a/platform/ios/src/MGLMapAccessibilityElement.mm +++ b/platform/ios/src/MGLMapAccessibilityElement.mm @@ -7,6 +7,8 @@ #import "MGLVectorSource_Private.h" #import "NSBundle+MGLAdditions.h" +#import "NSOrthography+MGLAdditions.h" +#import "NSString+MGLAdditions.h" @implementation MGLMapAccessibilityElement @@ -49,24 +51,16 @@ NSString *languageCode = [MGLVectorSource preferredMapboxStreetsLanguage]; NSString *nameAttribute = [NSString stringWithFormat:@"name_%@", languageCode]; NSString *name = [feature attributeForKey:nameAttribute]; - + // If a feature hasn’t been translated into the preferred language, it // may be in the local language, which may be written in another script. - // Romanize it. - NSLocale *locale = [NSLocale localeWithLocaleIdentifier:languageCode]; - NSOrthography *orthography; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunguarded-availability-new" - if ([NSOrthography respondsToSelector:@selector(defaultOrthographyForLanguage:)]) { - orthography = [NSOrthography defaultOrthographyForLanguage:locale.localeIdentifier]; - } -#pragma clang diagnostic pop -#endif - if ([orthography.dominantScript isEqualToString:@"Latn"]) { - name = [name stringByApplyingTransform:NSStringTransformToLatin reverse:NO]; + // Attempt to transform to the script of the preferred language, keeping + // the original string if no transform exists or if transformation fails. + if (@available(iOS 9.0, *)) { + NSString *dominantScript = [NSOrthography mgl_dominantScriptForMapboxStreetsLanguage:languageCode]; + name = [name mgl_stringByTransliteratingIntoScript:dominantScript]; } - + self.accessibilityLabel = name; } return self; diff --git a/platform/ios/src/NSOrthography+MGLAdditions.h b/platform/ios/src/NSOrthography+MGLAdditions.h new file mode 100644 index 0000000000..a552fc7774 --- /dev/null +++ b/platform/ios/src/NSOrthography+MGLAdditions.h @@ -0,0 +1,18 @@ +#import + +@interface NSOrthography (NSOrthography_MGLAdditions) + +/** + Returns a four-letter ISO 15924 code representing the name of the dominant + script for a given language. + + On iOS 11 or newer, this method wraps + `+[NSOrthography defaultOrthographyForLanguage:]` and supports any language. + On iOS 10 and older, this method only returns values for Mapbox + Streets-supported languages. + + @param language The ISO-639 code representing a language. + */ ++ (NSString *)mgl_dominantScriptForMapboxStreetsLanguage:(NSString *)language; + +@end diff --git a/platform/ios/src/NSOrthography+MGLAdditions.m b/platform/ios/src/NSOrthography+MGLAdditions.m new file mode 100644 index 0000000000..99258862cc --- /dev/null +++ b/platform/ios/src/NSOrthography+MGLAdditions.m @@ -0,0 +1,31 @@ +#import "NSOrthography+MGLAdditions.h" + +@implementation NSOrthography (MGLAdditions) + ++ (NSString *)mgl_dominantScriptForMapboxStreetsLanguage:(NSString *)language { + if (@available(iOS 11.0, *)) { + NSLocale *locale = [NSLocale localeWithLocaleIdentifier:language]; + NSOrthography *orthography = [NSOrthography defaultOrthographyForLanguage:locale.localeIdentifier]; + + return orthography.dominantScript; + } + + // Manually map Mapbox Streets languages to ISO 15924 script codes. + NSSet *latinLanguages = [NSSet setWithObjects:@"de", @"en", @"es", @"fr", @"pt", nil]; + NSSet *hansLanguages = [NSSet setWithObjects:@"zh", @"zh-Hans", nil]; + + if ([latinLanguages containsObject:language]) { + return @"Latn"; + } else if ([hansLanguages containsObject:language]) { + return @"Hans"; + } else if ([language isEqualToString:@"ru"]) { + return @"Cyrl"; + } else if ([language isEqualToString:@"ar"]) { + return @"Arab"; + } else { + // Code for undetermined script + return @"Zyyy"; + } +} + +@end diff --git a/platform/ios/test/MGLMapAccessibilityElementTests.m b/platform/ios/test/MGLMapAccessibilityElementTests.m index 5c79d85de1..89a595421e 100644 --- a/platform/ios/test/MGLMapAccessibilityElementTests.m +++ b/platform/ios/test/MGLMapAccessibilityElementTests.m @@ -19,13 +19,17 @@ }; MGLFeatureAccessibilityElement *element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; XCTAssertEqualObjects(element.accessibilityLabel, @"English", @"Accessibility label should be localized."); - + feature.attributes = @{ @"name": @"Цинциннати", @"name_en": @"Цинциннати", }; element = [[MGLFeatureAccessibilityElement alloc] initWithAccessibilityContainer:self feature:feature]; - XCTAssertEqualObjects(element.accessibilityLabel, @"Cincinnati", @"Accessibility label should be romanized."); + if (@available(iOS 9.0, *)) { + XCTAssertEqualObjects(element.accessibilityLabel, @"Cincinnati", @"Accessibility label should be romanized."); + } else { + XCTAssertEqualObjects(element.accessibilityLabel, @"Цинциннати", @"Accessibility label should not be romanized."); + } } - (void)testPlaceFeatureValues { diff --git a/platform/ios/test/MGLNSOrthographyAdditionsTests.m b/platform/ios/test/MGLNSOrthographyAdditionsTests.m new file mode 100644 index 0000000000..351fe4227e --- /dev/null +++ b/platform/ios/test/MGLNSOrthographyAdditionsTests.m @@ -0,0 +1,19 @@ +#import + +#import "NSOrthography+MGLAdditions.h" +#import "MGLVectorSource_Private.h" + +@interface MGLNSOrthographyAdditionsTests : XCTestCase + +@end + +@implementation MGLNSOrthographyAdditionsTests + +- (void)testStreetsLanguages { + for (NSString *language in [MGLVectorSource mapboxStreetsLanguages]) { + NSString *dominantScript = [NSOrthography mgl_dominantScriptForMapboxStreetsLanguage:language]; + XCTAssertNotEqualObjects(dominantScript, @"Zyyy", @"Mapbox Streets languages should have dominant script"); + } +} + +@end -- cgit v1.2.1