[Proposal] (NS)Locale identifiers, etc. should have custom types

In a look over NSLocale.swift, I noticed that there are many apis that take or return a String that is used with parameter or property names that (to me) imply they should actually take or provide custom String wrappers.

These apis as they exist are simply not "swifty", and should be deprecated and replaced.
Additionally, this permits foundation to pre-validate these identifiers, and to potentially specialize their string storage.

I have here split these apis into the types they imply should exist.
Is it acceptable to implement this?

Locale.Identifier

Current apis on NSLocale:

public init(localeIdentifier string: String)
public var localeIdentifier: String
open class var availableLocaleIdentifiers: [String]

open class func components(fromLocaleIdentifier string: String) -> [String : String]
open class func canonicalLocaleIdentifier(from string: String) -> String
open class func windowsLocaleCode(fromLocaleIdentifier localeIdentifier: String) -> UInt32

open class func localeIdentifier(fromComponents dict: [String : String]) -> String
open class func localeIdentifier(fromWindowsLocaleCode lcid: UInt32) -> String?

How I think it should look:

extension (NS)Locale {
  public init(_ : Identifier)
  public var identifier: Identifier
  open class var availableLocaleIdentifiers: [Identifier]

  struct Identifier : RawRepresentable {
    var rawValue: String

    public var components : [String : String]
    public var canonicalized : Identifier
    public var windowsLocaleCode : UInt32

    init(components dict: [String : String])
    init?(windowsLocaleCode lcid: UInt32)
  }
}

Locale.ISO codes:

Current apis on NSLocale:

// Locale.ISOLanguageCode
open class var isoLanguageCodes: [String]
open class func characterDirection(forLanguage isoLangCode: String) -> NSLocale.LanguageDirection
open class func lineDirection(forLanguage isoLangCode: String) -> NSLocale.LanguageDirection

// Locale.ISOCountryCode
open class var isoCountryCodes: [String]

// Locale.ISOCurrencyCode
open class var isoCurrencyCodes: [String]
open class var commonISOCurrencyCodes: [String]

How I think it should look:

extension (NS)Locale {
  open class var isoLanguageCodes: [ISOLanguageCode]

  struct ISOLanguageCode : RawRepresentable {
    var rawValue: String

    public var characterDirection : NSLocale.LanguageDirection
    public var lineDirection : NSLocale.LanguageDirection
  }

  open class var isoCountryCodes: [ISOCountryCode]

  struct ISOCountryCode : RawRepresentable {
    var rawValue: String
  }

  open class var isoCurrencyCodes: [ISOCurrencyCode]
  open class var commonISOCurrencyCodes: [ISOCurrencyCode]

  struct ISOCurrencyCode : RawRepresentable {
    var rawValue: String
  }
}

Locale.LanguageIdentifier:

Current apis on NSLocale:

open class var preferredLanguages: [String]
open class func canonicalLanguageIdentifier(from string: String) -> String

How I think it should look:

extension (NS)Locale {
  open class var preferredLanguages: [LanguageIdentifier]

  struct LanguageIdentifier : RawRepresentable {
    var rawValue: String

    // Because the parameter in `canonicalLanguageIdentifier(from string: String)` is simply "from"
    init(canonicalizing string: String)
  }
}

I think the value in having the stronger type comes from its use in APIs outside of Locale itself. In order for this to be useful, we really would have to find all other API in the SDK that takes locale identifiers, ISO codes, currency codes, etc, and make them take this new type.

ISOCountryCode, for example, doesn't really seem to add much value beyond being a string. It has no API itself, and no other functions in the SDK accept it as an argument. So what value does the type add beyond not being a string?

Also: it is now more important than ever for us to consider source stability when making API changes. See here for some info:

As a change to existing API, this would have to meet that high bar.

Yes, most of the value of this change would be in changing outside APIs to use the new types. However I don't know where these would be; I was just glancing through Foundation and noticed these APIs were not swifty.

Not really any, I was listing out the rest of the APIs, and wasn't thinking how useful these types would be, merely how in-swifty the existing APIs are.

I don't think the existing APIs should be removed, especially not immediately, merely depreciated and given replacements.

The value specific types add, compared to what they really are underneath, is that they make the API more descriptive and they avoid misuse of them: you can’t use a country code as a name (without explicitly doing so, e.g. using a .value property). It’s the same concept of newtype in other languages :)

I think this can be a motivating example for strong type-aliases.

1 Like

@Tony_Parker NSLocale.h contains an NSLocaleKey which isn't used in two APIs:

typedef NSString * NSLocaleKey NS_STRING_ENUM;

@interface NSLocale (NSLocaleGeneralInfo)
+ (NSDictionary<NSString *, NSString *> *)componentsFromLocaleIdentifier:(NSString *)string;
+ (NSString *)localeIdentifierFromComponents:(NSDictionary<NSString *, NSString *> *)dict;
@end

The corresponding value type has two similar APIs:

public struct Locale {
    public static func components(fromIdentifier string: String) -> [String : String]
    public static func identifier(fromComponents components: [String : String]) -> String
}

Is it too late to change those Objective-C and Swift APIs?

e.g. I'm using NSLocale.Key.languageCode.rawValue but a shorthand .languageCode key would be much better.

The NSLocaleKey type is used for the argument to objectForKey and displayNameForKey methods. It's not supposed to be the type of the identifier (which is a string).

Locale identifiers are basically a little mini-language themselves. They can have a lot of components. Check out the example in this documentation:

Examples of locale identifiers include "en_GB", "es_ES_PREEURO", and "zh-Hant_HK_POSIX@collation=pinyin;currency=CNY".

The purpose of Locale is to be the strong type which is created with one of these strings, providing type safe access to the attributes of the locale via properties. So I don't think we need another Identifier type, as that type is Locale itself.

I can see the argument that language, country, currency codes may make more sense as a nested strong type. However, in order to have a complete story here, there would have to be other APIs that take only those kinds of things (and currently takes Strings). Otherwise, the result just becomes annoying to use because you have to convert it to something more useful first (like a string).

1 Like

I don't think Foundation can offer much on this, but in (almost) every third party project I've seen there's plenty of them. Often kept as Strings and them misused...

@Tony_Parker The identifier would still be (NSString *), but the components dictionary would change:

--- (NSDictionary<NSString *,  NSString *> *)
+++ (NSDictionary<NSLocaleKey, NSString *> *)

Ah, that does look like an oversight when adopting the NSLocaleKey type. Filing a radar would be the best way to get that issue looked at. I've done that for you. rdar://problem/37241163

1 Like