Abstracting nested types with a protocol

In my project we use enums to namespace static variables that wrap localized strings. Eg:

enum DisplayStrings {
    enum General {
        static var ok: String { “DisplayStrings.General.ok”.localized }
        static var cancel: String { “ DisplayStrings.General.cancel”.localized }
    }
    enum Alerts {
        enum Error {
            static var title: String {...}
            static var body: String {...}
        }
        enum Success {
            static var title: String {...}
            static var body: String {...}
        }
    }
}

However now we need to inject this into and so have it represented by a protocol so that there can multiple implementations.
I can’t work out how to create a protocol that allows this even having multiple protocols and using associatedtypes doesn’t work (because then you can only use it as a generic constraint)

Ideally something like this would be possible:

protocol DisplayStringsType {
    static General {
        static var ok: String { get }
    }
    // etc 
}

(Ideally it seems like var wouldn’t be needed in protocols because the protocol doesn’t care how it’s implemented, as in swift 5.3 you can use enum cases to conform to a protocol)

Since types are not allowed inside protocol, how about using typealias?

protocol DisplayStringsType {
  typealias General = DisplayStrings.General
}

Could you use a struct instead?

enum DisplayStrings {
  struct General {
    let ok: String
    let cancel: String
  }
    
  // ..
}

extension DisplayStrings.General {
  static let `default` = Self(ok: "DisplayStrings.General.ok".localized,
                              cancel: "DisplayStrings.General.cancel".localized)
}

// Usage
func takesGeneralStrings(strings: DisplayStrings.General) {
    // Stuff
}

takesGeneralStrings(strings: .default) // or you can pass a different one

I don't think that will be useful in your case as (1) you're not using an enum case in your display string enums and (2) even if you were, the property protocol requirements in your display string protocol would need to have Self/FooEnum type and you won't be able to access ".localised" on the string as the raw value would need to be a literal:

protocol DisplayStringsProtocol {
  static var ok: Self { get } 
  // or static var ok: DisplayStringsEnum { get } but that's not helpful in this case
}

enum DisplayStringsEnum: String, DisplayStringsProtocol {
  case ok = "Ok" // can't use "DisplayStrings.ok".localized here
}

although I think you might be able to work around it by creating your own raw value type that calls ".localized" internally:

struct LocalizedStringLiteral: ExpressibleByStringLiteral, RawRepresentable, Equatable {
    let rawValue: String
    typealias StringLiteralType = String
    
    init(stringLiteral: String) {
        self.rawValue = stringLiteral.localized
    }
    
    init?(rawValue: String) {
        self.rawValue = rawValue.localized
    }
}

protocol DisplayStringsProtocol where Self: RawRepresentable, Self.RawValue == LocalizedStringLiteral {
    static var ok: Self { get }
}

enum DisplayStrings: LocalizedStringLiteral, DisplayStringsProtocol {
    case ok = "DisplayStrings.Ok"
}

func takesStrings<T: DisplayStringsProtocol>(value: T.Type) {
    print(value.ok.rawValue.rawValue)
}

takesStrings(value: DisplayStrings.self)

I tried using typealias with the right side being another protocol, but then you can't access static variables on a protocol.

Yeah that wouldn't be useful here. I meant given that now you can use enum cases to conform to a protocol I don't see why you should have to write var and func in general in swift protocols. Even before you could conform to a protocol by using a let if the protocol didn't require a setter. (One of the first things I got confused about with Swift was why you couldn't put let in a protocol, and I've seen others get confused as well, if we weren't writing var at all then that confusion wouldn't occur.) It seems like Swift protocols should be written like:

protocol Foo {
    bar: String { get }
    barFn(_: String) -> String
}

that would mean you could do the following because the protocol doesn't care wether General is a var, enum, case or let.

protocol DisplayStringsType {
    static General: DisplayString_GeneralType
}

protocol DisplayString_GeneralType {
   static ok: String { get }
}

enum DisplayStrings: DisplayStringsType {
    enum General: DisplayString_GeneralType {
        static var ok: String { return "....".localized }
    }
}

The problem with using a struct to wrap it is that a most of the time we want to assign the DisplayString to a component like UILabel. So it would require all our code to be label.text = DisplayStrings.General.ok.rawValue which doesn't seem ideal.

Also the problem with using a struct like the below, is that if the language changes from inside the app then the old language will be stored in the struct.

extension DisplayStrings.General {
  static let `default` = Self(ok: "DisplayStrings.General.ok".localized,
                              cancel: "DisplayStrings.General.cancel".localized)
}

I suppose we could allow let properties in protocols, but I don't know how useful it would be (plus you can already use a let property as a witness for a property requirement with a getter only).

You would only have to use the rawValue when accessing an enum case (or any other RawRepresentable type), though, but I agree it would be less ideal. I suppose you could work around it by adding a convenience method on UILabel (or if you want similar syntax like you'd get with a static String property, then perhaps you can use a protocol that provides an RawValue property and conform UILabel to it).

I think you can avoid that by making default a computed property.

I don’t think you should be able to put let inside protocols as that’s an implementation detail, I just think you shouldn’t be able to put var or func in there either.

I’ll give the struct version a go, but I don’t think it v will quite fit with how we have it set up now, and I’m trying to avoid changing the hundreds of strings we have set up now that would require the whole team to adapt to, when the current setup works really simply/well apart from this.

It wouldn't be completely unreasonable to allow it in some scenarios:

https://twitter.com/dgregor79/status/1269393002524372994?s=12

Sure, I just think that until we have the ability to nest types inside protocol declarations, using concrete types (or protocols+enum w/ SE-0280) might be alternatives that could be worth exploring for your use case.