From 340899be64721dc2b52b91fdd37baaf4b8e828c9 Mon Sep 17 00:00:00 2001 From: ianegordon Date: Mon, 30 Jul 2018 16:55:45 -0400 Subject: [PATCH] Add Bidi utilities (#42) Initial import of Bidi utility functions. --- .travis.yml | 3 +- .../Flags/Flags.xcodeproj/project.pbxproj | 6 + .../BidiIcon.imageset/Contents.json | 23 +++ .../baseline_compare_arrows_black_36pt_1x.png | Bin 0 -> 178 bytes .../baseline_compare_arrows_black_36pt_2x.png | Bin 0 -> 234 bytes .../baseline_compare_arrows_black_36pt_3x.png | Bin 0 -> 341 bytes .../Base.lproj/Main.storyboard | 119 +++++++++++-- .../BidirectionalViewController.h | 21 +++ .../BidirectionalViewController.m | 77 ++++++++ .../project.pbxproj | 20 +++ Sources/MDFInternationalization.h | 2 + Sources/NSLocale+MaterialRTL.h | 36 ++++ Sources/NSLocale+MaterialRTL.m | 38 ++++ Sources/NSString+MaterialBidi.h | 84 +++++++++ Sources/NSString+MaterialBidi.m | 165 +++++++++++++++++ Sources/UIImage+MaterialRTL.m | 2 +- Tests/MDFBidiTests.m | 167 ++++++++++++++++++ Tests/MDFInternationalizationTests.m | 2 + Tests/MDFRTLTests.m | 2 +- 19 files changed, 753 insertions(+), 14 deletions(-) create mode 100644 Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/Contents.json create mode 100644 Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_1x.png create mode 100644 Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_2x.png create mode 100644 Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_3x.png create mode 100644 Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.h create mode 100644 Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.m create mode 100644 Sources/NSLocale+MaterialRTL.h create mode 100644 Sources/NSLocale+MaterialRTL.m create mode 100644 Sources/NSString+MaterialBidi.h create mode 100644 Sources/NSString+MaterialBidi.m create mode 100644 Tests/MDFBidiTests.m diff --git a/.travis.yml b/.travis.yml index 48cbd20..1765301 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,5 +44,4 @@ script: - env - set -o pipefail - echo xcodebuild -project "$TRAVIS_XCODE_PROJECT" -scheme "$TRAVIS_XCODE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" test - - xcodebuild -project "$TRAVIS_XCODE_PROJECT" -scheme "$TRAVIS_XCODE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" test | xcpretty - + - xcodebuild -project "$TRAVIS_XCODE_PROJECT" -scheme "$TRAVIS_XCODE_SCHEME" -sdk "$SDK" -destination "$DESTINATION" test | xcpretty \ No newline at end of file diff --git a/Examples/Flags/Flags.xcodeproj/project.pbxproj b/Examples/Flags/Flags.xcodeproj/project.pbxproj index 33ec355..6ac5a44 100644 --- a/Examples/Flags/Flags.xcodeproj/project.pbxproj +++ b/Examples/Flags/Flags.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 40CFE6791D82047100745882 /* Countries.strings in Resources */ = {isa = PBXBuildFile; fileRef = 40CFE67B1D82047100745882 /* Countries.strings */; }; 40CFE67E1D820EC800745882 /* Flags.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 40CFE67D1D820EC800745882 /* Flags.xcassets */; }; 40E3986B1D9DAFAB007F01DE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 40E3986D1D9DAFAB007F01DE /* InfoPlist.strings */; }; + 40F9EF0E20DC5FB300F761C2 /* BidirectionalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 40F9EF0D20DC5FB300F761C2 /* BidirectionalViewController.m */; }; 40FE8A9B1D9AC719000A2A57 /* InformationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 40FE8A9A1D9AC719000A2A57 /* InformationViewController.m */; }; /* End PBXBuildFile section */ @@ -83,6 +84,8 @@ 40E3986C1D9DAFAB007F01DE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 40E3986E1D9DAFAD007F01DE /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = ""; }; 40E3A2551DBEC54700C8B4CC /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Main.strings; sourceTree = ""; }; + 40F9EF0C20DC5FB300F761C2 /* BidirectionalViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BidirectionalViewController.h; sourceTree = ""; }; + 40F9EF0D20DC5FB300F761C2 /* BidirectionalViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BidirectionalViewController.m; sourceTree = ""; }; 40FE8A991D9AC719000A2A57 /* InformationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InformationViewController.h; sourceTree = ""; }; 40FE8A9A1D9AC719000A2A57 /* InformationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InformationViewController.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -131,6 +134,8 @@ children = ( 4096E62C1D78ED7600389ECD /* AppDelegate.h */, 4096E62D1D78ED7600389ECD /* AppDelegate.m */, + 40F9EF0C20DC5FB300F761C2 /* BidirectionalViewController.h */, + 40F9EF0D20DC5FB300F761C2 /* BidirectionalViewController.m */, 40CFE6711D82017200745882 /* CountryCell.h */, 40CFE6721D82017200745882 /* CountryCell.m */, 40CFE6741D82018D00745882 /* CountryTableViewController.h */, @@ -288,6 +293,7 @@ 40CFE6731D82017200745882 /* CountryCell.m in Sources */, 4096E62E1D78ED7600389ECD /* AppDelegate.m in Sources */, 40CFE6761D82018D00745882 /* CountryTableViewController.m in Sources */, + 40F9EF0E20DC5FB300F761C2 /* BidirectionalViewController.m in Sources */, 4096E62B1D78ED7600389ECD /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/Contents.json b/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/Contents.json new file mode 100644 index 0000000..a8e43d1 --- /dev/null +++ b/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "baseline_compare_arrows_black_36pt_1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "baseline_compare_arrows_black_36pt_2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "baseline_compare_arrows_black_36pt_3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_1x.png b/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_1x.png new file mode 100644 index 0000000000000000000000000000000000000000..401ebdf9a22d21bac8e3ea8d921c7827b8547181 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBDm`5sLn;{GUS?!FU?AXfai`bq zY!TbAi%T6YIOiuauuQrY_*s%+-ibq&&!25|Si$xn=+F|54vD4^115zeHnXkC0+Z`2 z6#v?9Srr?}kP#cgsPTJ+Q~>|hDGfWM<)<=RFLk)Hj-@If;zR-*-C%n@XZB_8>iD)t Ub2p0L1v-Mk)78&qol`;+00D16{r~^~ literal 0 HcmV?d00001 diff --git a/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_2x.png b/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ed2f451915fc3c6f7dca74e2d0f8a1d402278912 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!8$DedLn;{G-e6=qVj$pf(e@h? z?*#4b3cLarc$ft%=kGkQ*i(J~RnJK(K=jh-L+sn)K#67Hk8@5KiKi?uSS%q&gzB-Pvp)p96V`zYqi1z ekOMrUZgHR8A{AL|T%QPZJcFmJpUXO@geCw)08|eE literal 0 HcmV?d00001 diff --git a/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_3x.png b/Examples/Flags/MDFInternationalizationExample/Assets.xcassets/BidiIcon.imageset/baseline_compare_arrows_black_36pt_3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a31ae153826f7f5abda9e682ee5dd238c741ada4 GIT binary patch literal 341 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz1|<8_!p|}=Fv@tkIEGX(zP-iB#uO;xaB=_C zDEEv&*M!VsdikvGQvdg=dQMV7AgjXZQ$6qhx0z-XA-zFNY=eZ@1|hKx z3}MVZ^JSMav1zklAd3wRbNRU?bP~QZuo>(($T+Z|x51J5*;40wLJJu;^IyJtZ0FU_ z_VMPjx*KvCawT|f^gYuvWHf6s<7dm6-+%iT(+wM`4UQW)O%f(2FlQW4JIHoH@AZU+ zw;5WEj@(Tk^m^^Bg}E0RWKHuE_Es{!KFd4fzfr=58r6A55uYwG@=Uv4b+etZSrmvE wnxE - + + + + + - + @@ -20,104 +23,124 @@ + + + + + + + + + + + @@ -213,7 +236,81 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -228,19 +325,19 @@ - + - + - + @@ -280,7 +377,8 @@ - + + @@ -289,6 +387,7 @@ + diff --git a/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.h b/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.h new file mode 100644 index 0000000..941d281 --- /dev/null +++ b/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.h @@ -0,0 +1,21 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface BidirectionalViewController : UIViewController + +@end diff --git a/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.m b/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.m new file mode 100644 index 0000000..93bc50b --- /dev/null +++ b/Examples/Flags/MDFInternationalizationExample/BidirectionalViewController.m @@ -0,0 +1,77 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "BidirectionalViewController.h" + + +static NSString *kMDFLTREmbedding = @"\u202a"; // left-to-right embedding +static NSString *kMDFRTLEmbedding = @"\u202b"; // right-to-left embedding +static NSString *kMDFBidiPopEmbedding = @"\u202c"; // pop directional embedding + +static NSString *kMDFLTRMark = @"\u200e"; // left-to-right mark +static NSString *kMDFRTLMark = @"\u200f"; // right-to-left mark + +// The following only work on iOS 10+ +static NSString *kMDFLTRIsolate = @"\u2066"; // left-to-right isolate +static NSString *kMDFRTLIsolate = @"\u2067"; // right-to-left isolate +static NSString *kMDFFirstStrongIsolate = @"\u2068"; // first strong isolate +static NSString *kMDFPopIsolate = @"\u2069"; // pop directional isolate + + +@interface BidirectionalViewController () + +@property (weak, nonatomic) IBOutlet UILabel *labelOne; +@property (weak, nonatomic) IBOutlet UILabel *labelTwo; +@property (weak, nonatomic) IBOutlet UILabel *labelThree; +@property (weak, nonatomic) IBOutlet UILabel *labelFour; + +@end + +@implementation BidirectionalViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view. + // The following lines will display a (: if the markers are supported, ): if not. + self.labelOne.text = + [NSString stringWithFormat:@"Bidi Isolate supported %@)%@:", kMDFRTLIsolate, kMDFPopIsolate]; + self.labelTwo.text = + [NSString stringWithFormat:@"Bidi Embed supported %@)%@:", kMDFRTLEmbedding, kMDFBidiPopEmbedding]; +// self.labelThree.text = @"1st a-\u2068.)\u200f\u2069-b"; +// self.labelFour.text = @"1st a-\u2068\u200f.)\u2069-b"; +// self.labelFour.text = @"الو" // Hello + NSString *three = [NSString stringWithFormat:@"Read %@15 books%@ EOL", kMDFFirstStrongIsolate, kMDFPopIsolate]; + self.labelThree.text = three; + NSString *four = [NSString stringWithFormat:@"Read %@15 كتاب%@ EOL", kMDFFirstStrongIsolate, kMDFPopIsolate]; + self.labelFour.text = four; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +/* +#pragma mark - Navigation + +// In a storyboard-based application, you will often want to do a little preparation before navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. +} +*/ + +@end diff --git a/MDFInternationalization.xcodeproj/project.pbxproj b/MDFInternationalization.xcodeproj/project.pbxproj index a3c5ab8..e837aef 100644 --- a/MDFInternationalization.xcodeproj/project.pbxproj +++ b/MDFInternationalization.xcodeproj/project.pbxproj @@ -7,7 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 4008C247201B6A3D00F089DB /* MDFBidiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4008C246201B6A3D00F089DB /* MDFBidiTests.m */; }; + 404B29FF20F5631800FA5DB0 /* NSLocale+MaterialRTL.m in Sources */ = {isa = PBXBuildFile; fileRef = 404B29FD20F5631800FA5DB0 /* NSLocale+MaterialRTL.m */; }; + 404B2A0020F5631800FA5DB0 /* NSLocale+MaterialRTL.h in Headers */ = {isa = PBXBuildFile; fileRef = 404B29FE20F5631800FA5DB0 /* NSLocale+MaterialRTL.h */; settings = {ATTRIBUTES = (Public, ); }; }; 405E9FD41FFC894A005FA14D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 405E9FD31FFC894A005FA14D /* README.md */; }; + 407431D5201A67000031D58A /* NSString+MaterialBidi.h in Headers */ = {isa = PBXBuildFile; fileRef = 407431D3201A67000031D58A /* NSString+MaterialBidi.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 407431D6201A67000031D58A /* NSString+MaterialBidi.m in Sources */ = {isa = PBXBuildFile; fileRef = 407431D4201A67000031D58A /* NSString+MaterialBidi.m */; }; 4096E5BA1D78CB5500389ECD /* MDFInternationalization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4096E5AF1D78CB5500389ECD /* MDFInternationalization.framework */; }; 4096E5D51D78CBD400389ECD /* MDFInternationalization.h in Headers */ = {isa = PBXBuildFile; fileRef = 4096E5CC1D78CBD400389ECD /* MDFInternationalization.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4096E5D71D78CBD400389ECD /* MDFRTL.h in Headers */ = {isa = PBXBuildFile; fileRef = 4096E5CE1D78CBD400389ECD /* MDFRTL.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -31,7 +36,12 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 4008C246201B6A3D00F089DB /* MDFBidiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MDFBidiTests.m; path = Tests/MDFBidiTests.m; sourceTree = ""; }; + 404B29FD20F5631800FA5DB0 /* NSLocale+MaterialRTL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSLocale+MaterialRTL.m"; path = "Sources/NSLocale+MaterialRTL.m"; sourceTree = ""; }; + 404B29FE20F5631800FA5DB0 /* NSLocale+MaterialRTL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSLocale+MaterialRTL.h"; path = "Sources/NSLocale+MaterialRTL.h"; sourceTree = ""; }; 405E9FD31FFC894A005FA14D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 407431D3201A67000031D58A /* NSString+MaterialBidi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSString+MaterialBidi.h"; path = "Sources/NSString+MaterialBidi.h"; sourceTree = ""; }; + 407431D4201A67000031D58A /* NSString+MaterialBidi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSString+MaterialBidi.m"; path = "Sources/NSString+MaterialBidi.m"; sourceTree = ""; }; 4096E5AF1D78CB5500389ECD /* MDFInternationalization.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MDFInternationalization.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4096E5B91D78CB5500389ECD /* MDFInternationalizationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MDFInternationalizationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4096E5CB1D78CBD400389ECD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Sources/Info.plist; sourceTree = ""; }; @@ -93,6 +103,10 @@ 4096E5CC1D78CBD400389ECD /* MDFInternationalization.h */, 4096E5CE1D78CBD400389ECD /* MDFRTL.h */, 4096E5CF1D78CBD400389ECD /* MDFRTL.m */, + 404B29FE20F5631800FA5DB0 /* NSLocale+MaterialRTL.h */, + 404B29FD20F5631800FA5DB0 /* NSLocale+MaterialRTL.m */, + 407431D3201A67000031D58A /* NSString+MaterialBidi.h */, + 407431D4201A67000031D58A /* NSString+MaterialBidi.m */, 4096E5D01D78CBD400389ECD /* UIImage+MaterialRTL.h */, 4096E5D11D78CBD400389ECD /* UIImage+MaterialRTL.m */, 4096E5D21D78CBD400389ECD /* UIView+MaterialRTL.h */, @@ -108,6 +122,7 @@ 4096E5DD1D78CBE700389ECD /* Info.plist */, 4096E5DE1D78CBE700389ECD /* MDFInternationalizationTests.m */, 4096E5E51D78E02600389ECD /* MDFRTLTests.m */, + 4008C246201B6A3D00F089DB /* MDFBidiTests.m */, ); name = Tests; sourceTree = ""; @@ -137,6 +152,8 @@ files = ( 4096E5D71D78CBD400389ECD /* MDFRTL.h in Headers */, 4096E5DB1D78CBD400389ECD /* UIView+MaterialRTL.h in Headers */, + 404B2A0020F5631800FA5DB0 /* NSLocale+MaterialRTL.h in Headers */, + 407431D5201A67000031D58A /* NSString+MaterialBidi.h in Headers */, 4096E5D91D78CBD400389ECD /* UIImage+MaterialRTL.h in Headers */, 4096E5D51D78CBD400389ECD /* MDFInternationalization.h in Headers */, ); @@ -239,6 +256,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 404B29FF20F5631800FA5DB0 /* NSLocale+MaterialRTL.m in Sources */, + 407431D6201A67000031D58A /* NSString+MaterialBidi.m in Sources */, 4096E5D81D78CBD400389ECD /* MDFRTL.m in Sources */, 4096E5DA1D78CBD400389ECD /* UIImage+MaterialRTL.m in Sources */, 4096E5DC1D78CBD400389ECD /* UIView+MaterialRTL.m in Sources */, @@ -251,6 +270,7 @@ files = ( 4096E5E61D78E02600389ECD /* MDFRTLTests.m in Sources */, 4096E5E11D78CC0100389ECD /* MDFInternationalizationTests.m in Sources */, + 4008C247201B6A3D00F089DB /* MDFBidiTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/MDFInternationalization.h b/Sources/MDFInternationalization.h index 2446234..2368edc 100644 --- a/Sources/MDFInternationalization.h +++ b/Sources/MDFInternationalization.h @@ -17,6 +17,8 @@ #import #import +#import +#import #import #import diff --git a/Sources/NSLocale+MaterialRTL.h b/Sources/NSLocale+MaterialRTL.h new file mode 100644 index 0000000..f73c95a --- /dev/null +++ b/Sources/NSLocale+MaterialRTL.h @@ -0,0 +1,36 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface NSLocale (MaterialRTL) + +/** + Is the direction of the current locale's default language Left-To-Right? + + @return YES if the language is LTR, NO if the language is any other direction. + */ ++ (BOOL)mdf_isDefaultLanguageLTR; + +/** + Is the direction of the current locale's default language Right-To-Left? + + @return YES if the language is RTL, NO if the language is any other direction. + */ ++ (BOOL)mdf_isDefaultLanguageRTL; + +@end + diff --git a/Sources/NSLocale+MaterialRTL.m b/Sources/NSLocale+MaterialRTL.m new file mode 100644 index 0000000..21256d2 --- /dev/null +++ b/Sources/NSLocale+MaterialRTL.m @@ -0,0 +1,38 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "NSLocale+MaterialRTL.h" + +@implementation NSLocale (MaterialRTL) + ++ (BOOL)mdf_isDefaultLanguageLTR { + NSString *languageCode = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]; + NSLocaleLanguageDirection characterDirection = + [NSLocale characterDirectionForLanguage:languageCode]; + BOOL localeLanguageDirectionIsLTR = (characterDirection == NSLocaleLanguageDirectionLeftToRight); + return localeLanguageDirectionIsLTR; +} + ++ (BOOL)mdf_isDefaultLanguageRTL { + NSString *languageCode = [[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode]; + NSLocaleLanguageDirection characterDirection = + [NSLocale characterDirectionForLanguage:languageCode]; + BOOL localeLanguageDirectionIsRTL = (characterDirection == NSLocaleLanguageDirectionRightToLeft); + return localeLanguageDirectionIsRTL; +} + +@end + diff --git a/Sources/NSString+MaterialBidi.h b/Sources/NSString+MaterialBidi.h new file mode 100644 index 0000000..5aeec86 --- /dev/null +++ b/Sources/NSString+MaterialBidi.h @@ -0,0 +1,84 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +@interface NSString (MaterialBidi) + +/** + Uses CFStringTokenizerCopyBestStringLanguage to determine string's language direction. + If the language direction is unknown or vertical returns left-to-right. + + As CFStringTokenizerCopyBestStringLanguage is Apple's API, its result may change if + Apple improves or modifies the implementation. + + @return the direction of the string + */ +- (NSLocaleLanguageDirection)mdf_calculatedLanguageDirection; + +/** + Initializes a copy of the string tagged with the given language direction. This + formatting adds the appropriate Unicode embedding characters at the beginning and end of the + string. + + Only NSLocaleLanguageDirectionLeftToRight and NSLocaleLanguageDirectionRightToLeft + language directions are supported. Other values of NSLocalLanguageDirection will + return a copy of self. + + Returns a string wrapped with Unicode bidi formatting characters by inserting these characters + around the string: + RLE+|string|+PDF for RTL text, or LRE+|string|+PDF for LTR text. + + @returns the new string. + */ +- (nonnull NSString *)mdf_stringWithBidiEmbedding:(NSLocaleLanguageDirection)languageDirection; + +/** + Returns a copy of the string explicitly tagged with a language direction. + + Uses mdf_calculatedLanguageDirection to determine string's language direction then invokes + mdf_stringWithBidiEmbedding:. + + @return the new string. + */ +- (nonnull NSString *)mdf_stringWithBidiEmbedding; + +/** + This method will wrap the string in embedding (LRE/RLE and PDF) characters, based on the string + direction and additionally wrapping the string in marks (LRM and RLM) if the string's direction + is different from the context direction. + + |direction| can be NSLocaleLanguageDirectionLeftToRight, NSLocaleLanguageDirectionRightToLeft, or + NSLocaleLanguageDirectionUnknown. If NSLocaleLanguageDirectionUnknown, the direction of the string + will be calculated with mdf_calculatedLanguageDirection. + + |contextDirection| must be specified and cannot be unknown. Only + NSLocaleLanguageDirectionLeftToRight and NSLocaleLanguageDirectionRightToLeft language directions + are supported. + + @returns the new string. + */ +- (nonnull NSString *)mdf_stringWithStereoReset:(NSLocaleLanguageDirection)direction + context:(NSLocaleLanguageDirection)contextDirection; + +/** + Returns a new string in which all occurrences of Unicode bidirectional format markers are removed. + + @returns the new string. + */ +- (nonnull NSString *)mdf_stringWithBidiMarkersStripped; + +@end diff --git a/Sources/NSString+MaterialBidi.m b/Sources/NSString+MaterialBidi.m new file mode 100644 index 0000000..f98d416 --- /dev/null +++ b/Sources/NSString+MaterialBidi.m @@ -0,0 +1,165 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "NSString+MaterialBidi.h" + +#import + +@implementation NSString (MaterialBidi) + +// https://www.w3.org/International/questions/qa-bidi-unicode-controls +//TODO: Reach out to AAA about the utility of the Isolate markers +// ??? Do we want Embedding or Isolate markers? w3 recommends isolate? +// Add reference : Unicode® Standard Annex #9 UNICODE BIDIRECTIONAL ALGORITHM +// go/android-bidiformatter +// http://unicode.org/reports/tr9/ + +// Mark influences the directionality of neutral characters when the context is opposite of the +// neutral chatacter's desired directionality. +static NSString *kMDFLTRMark = @"\u200e"; // left-to-right mark +static NSString *kMDFRTLMark = @"\u200f"; // right-to-left mark + +// Embedding indicates a text segment is embedded in a larger context with the opposite +// directionality. +static NSString *kMDFLTREmbedding = @"\u202a"; // left-to-right embedding +static NSString *kMDFRTLEmbedding = @"\u202b"; // right-to-left embedding + +// Override reverses the directionality of strongly LTR or RTL characters +static NSString *kMDFLTROverride = @"\u202d"; // left-to-right override +static NSString *kMDFRTLOverride = @"\u202e"; // right-to-left override + +// Pop is used to denote the end of an embedding or override text segment +static NSString *kMDFPopFormatting = @"\u202c"; // pop directional formatting + +// Version 6.3.0 Bidi algorithm additions +// The following only work on iOS 10+ + +// Isolate indicates that the text segment has an internal directionality with no effect on +// surrounding characters. +static NSString *kMDFLTRIsolate = @"\u2066"; // left-to-right isolate +static NSString *kMDFRTLIsolate = @"\u2067"; // right-to-left isolate +static NSString *kMDFFirstStrongIsolate = @"\u2068"; // first strong isolate + +// Pop Isolate is used to denote the end of an isolate text segment +static NSString *kMDFPopIsolate = @"\u2069"; // pop directional isolate + + +- (NSLocaleLanguageDirection)mdf_calculatedLanguageDirection { + // Attempt to determine language of string. + NSLocaleLanguageDirection languageDirection = NSLocaleLanguageDirectionUnknown; + + // Pass string into CoreFoundation's language identifier + CFStringRef text = (__bridge CFStringRef)self; + CFRange range = CFRangeMake(0, [self length]); + NSString *languageCode = + (NSString *)CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage(text, range)); + if (languageCode) { + // If we identified a language, explicitly set the string direction based on that + languageDirection = [NSLocale characterDirectionForLanguage:languageCode]; + } + + // If the result is not LTR or RTL, fallback to LTR + // ??? Should I be defaulting to NSLocale.NSLocaleLanguageCode.characterDiretion? + if (languageDirection != NSLocaleLanguageDirectionLeftToRight && + languageDirection != NSLocaleLanguageDirectionRightToLeft) { + languageDirection = NSLocaleLanguageDirectionLeftToRight; + } + + return languageDirection; +} + +- (NSString *)mdf_stringWithBidiEmbedding { + NSLocaleLanguageDirection languageDirection = [self mdf_calculatedLanguageDirection]; + + return [self mdf_stringWithBidiEmbedding:languageDirection]; +} + +- (NSString *)mdf_stringWithBidiEmbedding:(NSLocaleLanguageDirection)languageDirection { + if (languageDirection == NSLocaleLanguageDirectionRightToLeft) { + return [NSString stringWithFormat:@"%@%@%@", kMDFRTLEmbedding, self, kMDFPopFormatting]; + } else if (languageDirection == NSLocaleLanguageDirectionLeftToRight) { + return [NSString stringWithFormat:@"%@%@%@", kMDFLTREmbedding, self, kMDFPopFormatting]; + } else { + // Return a copy original string if an unsupported direction is passed in. + return [self copy]; + } +} + +- (nonnull NSString *)mdf_stringWithStereoReset:(NSLocaleLanguageDirection)direction + context:(NSLocaleLanguageDirection)contextDirection { +#if DEBUG + // Disable in release, as a pre-caution in case not everyone defines NS_BLOCK_ASSERTION. + NSCAssert((contextDirection != NSLocaleLanguageDirectionLeftToRight || + contextDirection != NSLocaleLanguageDirectionRightToLeft), + @"contextStringDirection must be passed in and set to either" + "NSLocaleLanguageDirectionLeftToRight or NSLocaleLanguageDirectionRightToLeft."); + + NSCAssert((direction != NSLocaleLanguageDirectionLeftToRight || + direction != NSLocaleLanguageDirectionRightToLeft || + direction != NSLocaleLanguageDirectionUnknown), + @"stringToBeInsertedDirection must be set to either NSLocaleLanguageDirectionUnknown," + "NSLocaleLanguageDirectionLeftToRight, or NSLocaleLanguageDirectionRightToLeft."); +#endif + + if (self.length == 0) { + return [self copy]; + } + + if (direction != NSLocaleLanguageDirectionLeftToRight && + direction != NSLocaleLanguageDirectionRightToLeft) { + direction = [self mdf_calculatedLanguageDirection]; + } + + NSString *bidiEmbeddedString = [self mdf_stringWithBidiEmbedding:direction]; + + NSString *bidiResetString; + if (direction != contextDirection) { + if (contextDirection == NSLocaleLanguageDirectionRightToLeft) { + bidiResetString = + [NSString stringWithFormat:@"%@%@%@", kMDFRTLMark, bidiEmbeddedString, kMDFRTLMark]; + } else { + bidiResetString = + [NSString stringWithFormat:@"%@%@%@", kMDFLTRMark, bidiEmbeddedString, kMDFLTRMark]; + } + } else { + bidiResetString = bidiEmbeddedString; + } + + return bidiResetString; +} + +- (NSString *)mdf_stringWithBidiMarkersStripped { + NSString *strippedString = self; + NSArray *directionalMarkers = @[ kMDFLTRMark, + kMDFRTLMark, + kMDFRTLEmbedding, + kMDFLTREmbedding, + kMDFRTLOverride, + kMDFLTROverride, + kMDFPopFormatting, + kMDFLTRIsolate, + kMDFRTLIsolate, + kMDFFirstStrongIsolate, + kMDFPopIsolate + ]; + for (NSString *markerString in directionalMarkers) { + strippedString = + [strippedString stringByReplacingOccurrencesOfString:markerString withString:@""]; + } + return strippedString; +} + +@end diff --git a/Sources/UIImage+MaterialRTL.m b/Sources/UIImage+MaterialRTL.m index 2ee208e..b75d734 100644 --- a/Sources/UIImage+MaterialRTL.m +++ b/Sources/UIImage+MaterialRTL.m @@ -130,7 +130,7 @@ - (UIImage *)mdf_imageWithHorizontallyFlippedOrientation { // On iOS 10 and above, UIImage supports the imageWithHorizontallyFlippedOrientation method. // Otherwise, we manually manipulate the image. if ([self respondsToSelector:@selector(imageWithHorizontallyFlippedOrientation)]) { - //TODO(#22): Replace with @availability when we adopt Xcode 9 as our minimum supported version. + //TODO: (#22) Replace with @availability when we adopt Xcode 9 as our minimum supported version. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" return [self imageWithHorizontallyFlippedOrientation]; diff --git a/Tests/MDFBidiTests.m b/Tests/MDFBidiTests.m new file mode 100644 index 0000000..aad61a8 --- /dev/null +++ b/Tests/MDFBidiTests.m @@ -0,0 +1,167 @@ +/* + Copyright 2018-present Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import + +@interface MDFBidiTests : XCTestCase + +@end + +@implementation MDFBidiTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testUniqueLocaleDirectionality { + BOOL isLTR = [NSLocale mdf_isDefaultLanguageLTR]; + BOOL isRTL = [NSLocale mdf_isDefaultLanguageRTL]; + + XCTAssertTrue(isLTR != isRTL, @"Locale cannot be LTR and RTL at the same time"); +} + +- (void)testLTRStringDirection { + NSString *testString = @"The quick brown fox jumps over the lazy dog."; + NSLocaleLanguageDirection MDFResult = [testString mdf_calculatedLanguageDirection]; + XCTAssertEqual(MDFResult, NSLocaleLanguageDirectionLeftToRight); +} + +- (void)testPersoArabicRTLStringDirection { + NSString *testString = @"الثعلب البني السريع يقفز فوق الكلب الكسول"; + NSLocaleLanguageDirection MDFResult = [testString mdf_calculatedLanguageDirection]; + XCTAssertEqual(MDFResult, NSLocaleLanguageDirectionRightToLeft); +} + +- (void)testHebrewRTLStringDirection { + NSString *testString = @"12 ספרים"; + NSLocaleLanguageDirection MDFResult = [testString mdf_calculatedLanguageDirection]; + XCTAssertEqual(MDFResult, NSLocaleLanguageDirectionRightToLeft); +} + +- (void)testAddLTRMarkers { + NSString *testString = @"quick brown fox"; + NSString *wrappedString = + [testString mdf_stringWithBidiEmbedding:NSLocaleLanguageDirectionLeftToRight]; + XCTAssertTrue([wrappedString isEqualToString:@"\u202aquick brown fox\u202c"]); +} + +- (void)testAddRTLMarkers { + NSString *testString = @"quick brown fox"; + NSString *wrappedString = + [testString mdf_stringWithBidiEmbedding:NSLocaleLanguageDirectionRightToLeft]; + XCTAssertTrue([wrappedString isEqualToString:@"\u202bquick brown fox\u202c"]); +} + +- (void)testAddCalculatedMarkers { + NSString *testString = @"The quick brown fox jumps over the lazy dog."; + NSString *wrappedString = + [testString mdf_stringWithBidiEmbedding]; + XCTAssertTrue([wrappedString isEqualToString:@"\u202aThe quick brown fox jumps over the lazy dog.\u202c"]); +} + +- (void)testStripMarkers { + NSString *testString = @"\u202aThe quick brown fox jumps over the lazy dog.\u202c"; + NSString *strippedString = [testString mdf_stringWithBidiMarkersStripped]; + XCTAssertTrue([strippedString isEqualToString:@"The quick brown fox jumps over the lazy dog."]); +} + +- (void)testLTRStereoIsolateLTR { + NSString *testString = @"The quick brown fox jumps over the lazy dog."; + NSLocaleLanguageDirection stringDirection = NSLocaleLanguageDirectionLeftToRight; + NSLocaleLanguageDirection contextDirection = NSLocaleLanguageDirectionLeftToRight; + + NSString *wrappedString = + [testString mdf_stringWithStereoReset:stringDirection + context:contextDirection]; + + // ??? Since the context is the same as the string, do we need the markers + XCTAssertTrue([wrappedString isEqualToString:@"\u202aThe quick brown fox jumps over the lazy dog.\u202c"]); +} + +- (void)testLTRStereoIsolateRTL { + NSString *testString = @"The quick brown fox jumps over the lazy dog."; + NSLocaleLanguageDirection stringDirection = NSLocaleLanguageDirectionLeftToRight; + NSLocaleLanguageDirection contextDirection = NSLocaleLanguageDirectionRightToLeft; + + NSString *wrappedString = + [testString mdf_stringWithStereoReset:stringDirection + context:contextDirection]; + + // !!! wrappedString is "\u202b\u202aThe quick brown fox jumps over the lazy dog.\u202c\u202c + // ??? Since the context is the same as the string, do we need the markers + XCTAssertTrue([wrappedString isEqualToString:@"\u200f\u202aThe quick brown fox jumps over the lazy dog.\u202c\u200f"]); +} + +- (void)testRTLStereoIsolateRTL { + NSString *testString = @"The quick brown fox jumps over the lazy dog."; + NSLocaleLanguageDirection stringDirection = NSLocaleLanguageDirectionRightToLeft; + NSLocaleLanguageDirection contextDirection = NSLocaleLanguageDirectionRightToLeft; + + NSString *wrappedString = + [testString mdf_stringWithStereoReset:stringDirection + context:contextDirection]; + + // !!! wrappedString is "\u202b\u202aThe quick brown fox jumps over the lazy dog.\u202c\u202c + // ??? Since the context is the same as the string, do we need the markers + XCTAssertTrue([wrappedString isEqualToString:@"\u202bThe quick brown fox jumps over the lazy dog.\u202c"]); +} + +// Simple Tests + +- (void)testSimpleLanguageDirectionOfPunctuationString { + NSString *testString = @"@#%+=-^*"; + + // Then + XCTAssertEqual([testString mdf_calculatedLanguageDirection], NSLocaleLanguageDirectionLeftToRight); +} + +- (void)testSimpleLanguageDirectionOfNumericString { + NSString *testString = @"123"; + + // Then + XCTAssertEqual([testString mdf_calculatedLanguageDirection], NSLocaleLanguageDirectionLeftToRight); +} + +- (void)testSimpleLanguageDirectionOfHebrewString { + NSString *testString = @"!שלום"; + + // Then + XCTAssertEqual([testString mdf_calculatedLanguageDirection], NSLocaleLanguageDirectionRightToLeft); +} + +- (void)testSimpleLanguageDirectionOfEnglishString { + NSString *testString = @"Hello!"; + + // Then + XCTAssertEqual([testString mdf_calculatedLanguageDirection], NSLocaleLanguageDirectionLeftToRight); +} + +- (void)testSimpleLanguageDirectionOfEmptyString { + NSString *testString = @""; + + // Then + XCTAssertEqual([testString mdf_calculatedLanguageDirection], NSLocaleLanguageDirectionLeftToRight); +} + +@end diff --git a/Tests/MDFInternationalizationTests.m b/Tests/MDFInternationalizationTests.m index ebd7bc8..a6dfdb3 100644 --- a/Tests/MDFInternationalizationTests.m +++ b/Tests/MDFInternationalizationTests.m @@ -16,6 +16,8 @@ #import +//FIXME: Remove this file if there are no tests + @interface MDFInternationalizationTests : XCTestCase @end diff --git a/Tests/MDFRTLTests.m b/Tests/MDFRTLTests.m index 385ab63..b330592 100644 --- a/Tests/MDFRTLTests.m +++ b/Tests/MDFRTLTests.m @@ -108,7 +108,7 @@ - (void)testMDCRTLFlippedImagePortsRenderingMode { XCTAssertTrue(flippedOriginalImage.renderingMode == UIImageRenderingModeAlwaysOriginal); } -//TODO(#6): Implement per-pixel comparison +//TODO: (#6) Implement per-pixel comparison //- (void)testImageMirror { // UIImage *sourceImage = [RTLTests standardImage]; // UIImage *flippedImage = [sourceImage mdf_imageWithHorizontallyFlippedOrientation];