Skip to content

Instantly share code, notes, and snippets.

@davedelong
Last active October 15, 2022 08:02
Show Gist options
  • Save davedelong/2a2ae3c4682586278b14d0d5131fe050 to your computer and use it in GitHub Desktop.
Save davedelong/2a2ae3c4682586278b14d0d5131fe050 to your computer and use it in GitHub Desktop.
Locales
/*
======================================================
THIS CODE IS FOR EDUCATIONAL PURPOSES ONLY.
I'M NOT RESPONSIBLE IF YOU SHIP THIS AND IT BLOWS UP IN YOUR FACE.
IF IT DOES AND YOU COMPLAIN TO ME I WILL LAUGH AT YOU.
======================================================
This code is based on the following premise:
• You want to show things in a particular locale
• You want to honor any overrides the user has set
iOS will only format dates using the +[NSLocale currentLocale] if the app is localized to support that locale.
That means that if your app is running on:
• a device set to a language your app does not support AND
• the user has custom overrides for 12/24 hour, date/time formats, etc
THEN:
• the user will see dates formatted in another language, and WITHOUT their overrides.
This is because you end up getting a locale that's just the "default" locale for a particular language,
with no overrides applied.
Enter this code.
This code attempts to work around this (admittedly rather edge case) scenario.
The overrides on a locale are stored inside the "_prefs" field. For the current locale,
this is basically copied out of NSUserDefaults and is how the current locale knows about the
12/24 hour override, among other things.
For custom locales, this is NULL. And there is no way to change it.
Also working against us is the fact that _prefs IS NOT AN OBJC IVAR FIELD. NSLocale is toll-free
bridged to CFLocaleRef, which means we're really dealing with pointers to C structs, and the fields
on those structs are not described to the Objective-C runtime. So the Runtime doesn't even help us.
So instead, we get to drop down to the raw memory layout and fudge things around.
Please don't use this in shipping code.
*/
@interface NSLocale (Voodoo)
// hide all the nastiness behind a nice, pleasant method
+ (NSLocale *)localeIncludingOverridesWithLocaleIdentifier:(NSString *)localeIdentifier;
@end
// this is the (rough) layout of a CFLocaleRef
// I figured this out based on:
// - https://github.com/apple/swift-corelibs-foundation/blob/155f1ce1965effe55289477507a6f9fbdc8fe333/CoreFoundation/Locale.subproj/CFLocale.c#L144-L151
// - too much time in the debugger
struct __DDLocale {
void *_base; // The CF version of an "isa" pointer
void *_field1; // maybe the locale identifier?
void *_field2; // maybe a cache
void *_field3; // no idea
CFDictionaryRef _prefs; // The Secret Sauce
void *_lock; // maybe some thread safety thing
Boolean _nullLocale; // who knows
};
@implementation NSLocale (Voodoo)
+ (NSLocale *)localeIncludingOverridesWithLocaleIdentifier:(NSString *)localeIdentifier {
// First, rip apart the locale identifier in to its components
// Why? because if you create a locale w/ an identifier, and another locale instance
// already exists that has the same identifier, you'll just get a pointer back
// to the pre-existing object. For this class, that's probably what you want
// most of the time. For this scenario, it's not.
NSMutableDictionary *components = [[NSLocale componentsFromLocaleIdentifier:localeIdentifier] mutableCopy];
// So instead we'll "force" a new instance of a locale by creating a never-before-seen
// locale identifier, thanks to the improbable magic of NSUUID
[components setObject:[[NSUUID UUID] UUIDString] forKey:@"custom"];
// Turn the components back into a locale identifier. It's now "guaranteed"
// to be unique
NSString *newID = [NSLocale localeIdentifierFromComponents:components];
// Construct a brand-spanking-new NSLocale
NSLocale *copy = [[NSLocale alloc] initWithLocaleIdentifier:newID];
// If, for some reason, it failed, early return
// This is because we're about to do pointer magic, and doing pointer
// magic with a NULL pointer is a great way to crash. Let's not crash.
if (copy == nil) { return nil; }
// Cast (lie to the compiler) that these pointers are actually pointers
// to our struct definition that we worked out before
struct __DDLocale* this = (struct __DDLocale *)[NSLocale currentLocale];
struct __DDLocale* that = (struct __DDLocale *)copy;
// Copy over the dictionary of overrides from the +currentLocale
// to our new copy. This is how our copy will get to know about the
// overrides as well
that->_prefs = CFDictionaryCreateMutableCopy(NULL, 0, this->_prefs);
// Share And Enjoy.
return copy;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment