liamnichols / xcstrings-tool

A plugin to generate Swift constants for your Strings Catalogs.
https://swiftpackageindex.com/liamnichols/xcstrings-tool/documentation/documentation
MIT License
117 stars 22 forks source link

Improve SwiftUI support on iOS 15 #60

Open Brett-Best opened 2 weeks ago

Brett-Best commented 2 weeks ago

I was wondering what it would take to get iOS 15 SwiftUI support.

I think it would involve needing to use LocalizedStringKey but not sure how that would manifest with needing to specify a bundle in a Text init for example.

EDIT: Looks like https://github.com/iKenndac/localized-strings-symbols does something similar to my idea above.

Any thoughts around this?

liamnichols commented 2 weeks ago

Hey! So LocalizedStringKey has some annoying limitations, which I think that LocalizedStringResource was supposed to resolve, but in SwiftUI, many views still lack initialisers for the newer type, which is somewhat annoying 😞 (plus the iOS 16+ constraint).

Like you spotted, tableName and bundle exist on the Text initialiser directly and are not part of LocalizedStringKey, but also localized string arguments can only be passed in via string interpolation via LocalizedStringKey, which means that your key must contain the interpolation placeholders as well (i.e "MyKey \(arg)" == MyKey %@).

It's because of these annoying limitations with SwiftUI apis prior to iOS 16 that I didn't bother adding support initially. I couldn't find a worthwhile path forward that worked around all of this πŸ˜•

I guess that #50 is a step forward. You can add a convenience Text initialiser that resolves the string inside the initialiser, or write a small wrapper view that pull the Locale out of the environment and use that when resolving the string, but none of these will be quite as convenient as using Text + LocalizedStringResource unfortunately πŸ˜₯

liamnichols commented 2 weeks ago

I'd need to think about it some more, but another possibility might be to ship a small supplementary library that contains a LocalizedText type that could be used as a backwards compatible drop-in replacement to Text for those who need to support iOS 15 πŸ€”

I've not used SwiftUI too extensively myself, so I just want to confirm that the reason why you are looking for more advanced SwiftUI support on iOS 15 is because of the way that the language should be resolved from within the SwiftUI environment rather than using the String initialiser that would use Locale.current? Or is there something else? Or maybe I just need to push a release that contains #50 πŸ˜„

byMohamedali commented 2 weeks ago

Hi hope you're doing well, is there any reason for not releasing the iOS 15 support yet ? Thank you

liamnichols commented 2 weeks ago

I had been on holiday and had only returned today. I’ll do it over the weekend πŸ™‚

byMohamedali commented 2 weeks ago

Thank you so much, hope you had good holiday πŸ™‚

liamnichols commented 2 weeks ago

0.2.0 has now been released with initial support for iOS 15/macOS 12 πŸ™

Please try it out, and let me know how we might be able to improve usage in SwiftUI πŸ™‡

byMohamedali commented 1 week ago

I tried, it's not working i think you forgot to change the mimimum requirement version platform in the package, i tried to change it locally, but i'm getting error on the parser

image
liamnichols commented 1 week ago

The minimum platform versions in Package.swift are correct. The CLI executable doesn't support macOS 12/iOS 15, only the generated code.

If you need to use the plugin in your iOS 15 project, you should use xcstrings-tool-plugin repository, which uses a precompiled binary artefact, not the xcstrings-tool repository as that requires compiling the CLI from source.

liamnichols commented 1 week ago

I'd need to think about it some more, but another possibility might be to ship a small supplementary library that contains a LocalizedText type that could be used as a backwards compatible drop-in replacement to Text for those who need to support iOS 15 πŸ€”

I've not used SwiftUI too extensively myself, so I just want to confirm that the reason why you are looking for more advanced SwiftUI support on iOS 15 is because of the way that the language should be resolved from within the SwiftUI environment rather than using the String initialiser that would use Locale.current? Or is there something else? Or maybe I just need to push a release that contains #50 πŸ˜„

I realised that this wont quite work, because the locale passed into the String.init(localized:defaultValue:table:bundle:locale:) is not used liek the locale on LocalizedStringResource.. Changing this locale only impacts the number formatting of int/float arguments, just like the old String.init(format:locale:arugments:) initializer, which kind of sucks 😞

I think that basically means that there is no clean way to be able to take the locale from the SwiftUI environment and then to resolve strings in that language prior to iOS 16, unless you poke into the bundle yourself, which isn't ideal.

Hopefully I missed something though.

mylogon341 commented 3 days ago

Hi. I'm also trying to retrofit this into an iOS15 project. I tried integrating xcstrings-tool but when trying to pass in a string into a struct constructor, the error im getting is

Member 'regulationsLink1' in 'String' produces result of type 'String.Onboarding', but context expects 'String'

Am I missing something here?

liamnichols commented 2 days ago

@mylogon341 please share the code that you’re trying to use. For iOS 15, you’d need to resolve the String directly rather than passing the generated type into something like Text.

Something like this is correct usage:

String(onboarding: .regulationsLink1)
mylogon341 commented 2 days ago

@mylogon341 please share the code that you’re trying to use. For iOS 15, you’d need to resolve the String directly rather than passing the generated type into something like Text.

Something like this is correct usage:

String(onboarding: .regulationsLink1)

Of course - yes that works as intended, thanks.

liamnichols commented 2 days ago

I tried thinking how to improve SwiftUI usage on iOS 15, but I am stuck. You are right that whatever we need to do will need to be done through LocalizedStringKey (with Text as well), but the main problem that I can't find a solution for is that with LocalizedStringKey, the format specifiers need to make up the key through interpolation.

If you dump a LocalizedStringKey instance, you can see that there are separate key and arguments properties, but I can't find a way to init a custom instance πŸ˜• ... Maybe there is some private API trickery that can be done, but i'm not sure.

liamnichols commented 1 day ago

After some digging with @Brett-Best today, I think that we are going to try with the following:

#if canImport(SwiftUI)
extension Text {
    init(localizable: String.Localizable) {
        if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
            self.init(LocalizedStringResource(localizable: localizable))
            return
        }

        var stringInterpolation = LocalizedStringKey.StringInterpolation(
            literalCapacity: 0,
            interpolationCount: localizable.arguments.count
        )

        for argument in localizable.arguments {
            switch argument {
            case .int(let value):
                stringInterpolation.appendInterpolation(value)
            case .uint(let value):
                stringInterpolation.appendInterpolation(value)
            case .double(let value):
                stringInterpolation.appendInterpolation(value)
            case .float(let value):
                stringInterpolation.appendInterpolation(value)
            case .object(let value):
                stringInterpolation.appendInterpolation(value)
            }
        }

        let makeKey = LocalizedStringKey.init(stringInterpolation:)
        var key = makeKey(stringInterpolation)
        key.overrideKeyForLookup(using: localizable.key)

        self.init(key, tableName: localizable.table, bundle: .from(description: localizable.bundle))
    }
}

extension LocalizedStringKey {
    mutating func overrideKeyForLookup(using key: StaticString) {
        withUnsafeMutablePointer(to: &self) { pointer in
            let raw = UnsafeMutableRawPointer(pointer)
            let bound = raw.assumingMemoryBound(to: String.self)
            bound.pointee = String(describing: key)
        }
    }
}
#endif

It's somewhat creative, but it will bring a Text initialiser that will work on iOS 13+ πŸ™Œ