Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NSLocalizedString and Add Localization Tests #2824

Merged
merged 6 commits into from
Dec 16, 2017

Conversation

MSFTFox
Copy link
Member

@MSFTFox MSFTFox commented Dec 14, 2017

Add automated tests using NSLocalizedStringFromTableInBundle and a fabricated bundle. Also add a page in WoCCatalog for testing various system languages and NSLocalizedString from the main bundle.

Fixes #2791


This change is Reviewable

@@ -531,6 +536,10 @@ CF_PRIVATE CFArrayRef _CFBundleCopyUserLanguages() {
static CFArrayRef _CFBundleUserLanguages = NULL;
static dispatch_once_t once = 0;
dispatch_once(&once, ^{
// WINOBJC: __CFAppleLanguages does not exist on Windows
#if DEPLOYMENT_TARGET_WINDOWS
_CFBundleUserLanguages = EnumerateUserPreferredLanguages();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per offline suggestion, I'd prefer we use CFLocaleGetPreferredUserLanguages or whatever it's called. That way we don't need to use our WinRT Enumerate directly everywhere.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: should we be transforming this list from - to _ on creation? it could save us the step of converting the - to a _ for every lookup.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could do it inside Enumerate if CF prefers _ locale names

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows locales are set up to use -. If somebody is setting up localization, they may think to use the natural windows syntax in their bundle names. I think it makes sense to support both - and _ in that case.

@@ -663,8 +673,66 @@ static CFStringRef _CFBundleCopyLanguageFoundInLocalizations(CFArrayRef localiza
return NULL;
}

// WINOBJC: Helper functions for workaround in _CFBundleCreateMutableArrayOfFallbackLanguages
static CFStringRef _copyStringTruncated(CFStringRef localization, CFRange cutoff) {
CFMutableStringRef truncatedString = CFStringCreateMutableCopy(NULL, 0, localization);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CFStringCreateWithSubstring

// Given a list of localizations (e.g., provided as argument to API, or present as .lproj directories), return a mutable array of localizations in preferred order. Returns NULL if nothing is found.
static CFMutableArrayRef _CFBundleCreateMutableArrayOfFallbackLanguages(CFArrayRef availableLocalizations, CFArrayRef preferredLocalizations) {
// WINOBJC: The API that performs the work described below does not exist in our 3rd party libraries

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add note that ualoc_ is an apple-specific ICU API suite

// We know the user languages are in preferred order, add to this list in this order
// Prefer the full language locale, attempt to convert any hyphens to underscores as
// Windows language settings are retrieved with hyphens while underscores are commonly used for localization.
// Finally, attempt to truncate any underscores from the language to find a base localization.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: give examples of these. "The user's preferred language list comes in as en-US and de-DE, ..."


for (CFIndex i = 0, count = CFArrayGetCount(preferredLocalizations); i < count; i++) {
CFStringRef preferredLocalization = (CFStringRef)CFArrayGetValueAtIndex(preferredLocalizations, i);
for (CFIndex j = 0, count = CFArrayGetCount(availableLocalizations); j < count; j++) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these multiple scope-nested counts scare me

NSString* helloWorldInGerman = NSLocalizedStringFromTableInBundle(helloWorld, nil, bundle, nil);
NSString* codingInChinese = NSLocalizedStringFromTableInBundle(coding, nil, bundle, nil);
ASSERT_OBJCEQ(@"Hallo Welt", helloWorldInGerman);
// Cannot use OBJCEQ as these unicode characters will cause a crash. isEqualToString is a code point literal comparison.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm sorry? what? a crash?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, unicode characters in ASSERT_OBJCEQ was just like... ¯_(ツ)_/¯ crash.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compile time? Runtime? Either way, please file it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run time. Will do!

NSString* _subDirectory = @"en.lproj";
NSString* localizationResourceName = @"Localizable.strings";
// Create a unique test directory
NSString* tempDir = [[@"./tmp_TestFoundation" stringByAppendingString:[NSUUID UUID].UUIDString] stringByAppendingString:@"/"];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agh stringByAppendingPathComponent:

ASSERT_EQ(nil, error);

// Make a flat bundle in the playground
NSString* bundlePath = [tempDir stringByAppendingString:_bundleName];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

[[NSFileManager defaultManager] createDirectoryAtPath:localizationPath
withIntermediateDirectories:false
attributes:nil
error:&error];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just flatten these all into one path, but set withIntermediateDirectories:true.


protected:
virtual void SetUp() {
NSString* _subDirectory = @"en.lproj";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? you're only testing looking strings up in the single lproj that exists, which usually matches the system locale.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point was to always find a localization we have. Since we know we can use English as a fallback the test doesn't care about what locale or language settings the machine has. This way we can ensure we're testing the localization changes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by testing the base localization, we aren't testing anything related to 1. localization or 2. localization changes though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, we're certainly testing the NSLocalizedString path. Grabbing a particular locale is not the goal here, nor is that something I think we can rely on with a unit test. The system can only have one locale setting, so it doesn't seem to matter to me which locale that is. Since English is a known fallback is that not acceptable?

Copy link

@DHowett-MSFT DHowett-MSFT left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine now!

CFStringDelete(truncatedString, CFRangeMake(cutoff.location, CFStringGetLength(truncatedString) - cutoff.location));
return truncatedString;
return CFStringCreateWithSubstring(NULL, localization, CFRangeMake(0, cutoff.location));
//CFMutableStringRef truncatedString = CFStringCreateMutableCopy(NULL, 0, localization);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh woops. I wanted to keep that around in case I goofed anything in the substring call.

@@ -28,10 +28,10 @@ @implementation LocalizationViewController {
UILabel* _pasteTextLabel;
}
- (void)viewDidLoad {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you may need [super viewDidLoad] here for completeness' sake

@@ -30,20 +30,13 @@ virtual void SetUp() {
NSString* _subDirectory = @"en.lproj";
NSString* localizationResourceName = @"Localizable.strings";
// Create a unique test directory
NSString* tempDir = [[@"./tmp_TestFoundation" stringByAppendingString:[NSUUID UUID].UUIDString] stringByAppendingString:@"/"];

NSString* tempDir = [[@"./tmp_TestFoundation" stringByAppendingPathComponent:[NSUUID UUID].UUIDString] stringByAppendingPathComponent:@"/"];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ is not a path component, so you can leave this call out.

NSString* tempDir = [[@"./tmp_TestFoundation" stringByAppendingString:[NSUUID UUID].UUIDString] stringByAppendingString:@"/"];

NSString* tempDir = [[@"./tmp_TestFoundation" stringByAppendingPathComponent:[NSUUID UUID].UUIDString] stringByAppendingPathComponent:@"/"];
NSString* bundlePath = [tempDir stringByAppendingPathComponent:_bundleName];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funny question: why not just expose bundlePath in the fixture and use that instead of re-combining _playground and _bundleName later?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not funny at all. That seems like a good idea to avoid extra string work. 👍

@@ -61,14 +57,15 @@ virtual void TearDown() {
}

StrongId<NSString> _playground;
StrongId<NSString> bundlePath;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_

@@ -25,6 +25,11 @@
#endif
#include <ctype.h>

// WINOBJC: Helper for retrieving list of preferred languages
#if DEPLOYMENT_TARGET_WINDOWS
#include "_CFLocaleInternal.h"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this still necessary?

@DHowett-MSFT
Copy link

you don't need to take all of my feedback; you are free to reject some.

@MSFTFox MSFTFox merged commit 22fb297 into microsoft:develop Dec 16, 2017
@triplef
Copy link
Contributor

triplef commented Dec 16, 2017

Thank you for this fix!

Just to clarify, does this only support getting localized strings or does it enable getting any kind of localized bundle resource?

@MSFTFox
Copy link
Member Author

MSFTFox commented Dec 18, 2017

It should have fixed a fundamental problem in lproj lookup. The problem was that an API for looking up localized resources did not have support for retrieving user preferred languages. This has been solved.

If you have some other particular APIs you would like us to take a look at, please let us know. This fix was really focused on ensuring localized strings were functional.

@triplef
Copy link
Contributor

triplef commented Dec 18, 2017

Sounds great, thanks!

MSFTFox added a commit that referenced this pull request Dec 18, 2017
* Fix NSLocalizedString and add new tests

* Address CR Feedback.

* Forgot to change to StringByAppendingPath

* Nevermind. ASSERT_OBJCEQ does indeed work.

* Address feedback.

* Address feedback.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants