liamnichols/xcstrings-tool

Suggestion/Question init LocalizedStringKey with .localizable.key

IgorMuzyka opened this issue ยท 8 comments

Hi, great tool, i anticipated the need for localization and used LocalizedStringKey wherever it made sense in my project.
Right now i'm utilizing your tool generated code as follows "\(.localizable.localizedStringKey)", it makes perfect sense to be able to escape the String interpolation completely and just have your library extend the LocalizedStringKey.
Or am i missing something and there's already a way?

Hi @IgorMuzyka, good question! In the initial release, I was keeping things focused entirely on LocalizedStringResource since this was kind of advertised as the future in the WWDC session video.

As you pointed out though, there is lots of SwiftUI that doesn't provide initialisers/api for this which leads to you having to work around it like the example above.

I think that it's easy enough to add the extension that you mentioned, so I'll leave that to you because it'll only iOS 16+ and I hope that Apple will improve things later by adding more APIs that work with LocalizedStringResource.

That said, we're working on some more backwards compatible improvements for SwiftUI in #60 which might help you. They are designed to be backwards compatible though, which requires them to work through Text at the core but we should be able to provide extensions on all types that can go alongside LocalizedStringKey based initialisers.

Have a look at that ticket and see if you think that the approach will help you or not ๐Ÿ™‡

@liamnichols thanks for speedy reply, that pretty much answers my question.

Not supporting iOS<16 and macOS<13.
Sure i'll manage to extend this to fit my needs.

Can i ask you to validate that the following examples are a correct use of generated code?
Notice: they all work as expected; at the moment i only have English localization(unsure if this won't break when more added).

// Example 1
let panel = NSSavePanel()
panel.title = String(localizable: .exportSavePanelTitle)
panel.nameFieldLabel = String(localizable: .exportSavePanelNameFieldLabel)
// Example 2
let adjustPhotoAccessMessage: LocalizedStringKey = .init(
    String(localizable: .libraryAccessRestrictedAdjustMessage(settingsLink))
)

And one more followup question.
If i would make a fork, which files should i adjust to change the generated Localizable.swift?

Ah I see, I think that I misunderstood your initial question. At first, I thought that you just wanted something like this:

extension LocalizedStringKey {
    init(localized: LocalizedStringResource) {
        self = "\(localized)"
    }
}

// Usage
Button(.init(localized: .localizable.myKey)) {
    flag.toggle()
}

But it sounds like you are actually suggesting a series of constants on LocalizedStringKey just like we generate for LocalizedStringResource, which would give you constants such as LocalizedStringKey.localizable.myKey?

I think that the problem with โ˜๏ธ is that it would break the type inference that allows a syntax like this:

Text(.localizable.myKey)` 

Because the above example would be ambiguous since the compiler wouldn't know if you meant Text(LocalizedStringResource.localizable.myKey) or Text(LocalizedStringKey.localizable.myKey).


Example 1 is correct usage, but example 2 is now. For example 2, by using String(localizable: .libraryAccessRestrictedAdjustMessage(settingsLink)) you are first resolving the localization using the system/app language (not from the SwiftUI environment) and you are missing out on other built in features, such as markdown rendering for bold/italic etc.

Using "\(.libraryAccessRestrictedAdjustMessage(settingsLink))" instead would cause the LocalizedStringKey to keep hold of the LocalizedStringResource (not the resolved localization) until the point of rendering, which would allow Text to use the Locale from the SwiftUI environment and then to apply markdown formatting etc.


If i would make a fork, which files should i adjust to change the generated Localizable.swift?

All the magic happens in StringGenerator.swift, but I would avoid forking the current code on main right this moment because I'm shortly going to push a massive refactor to it.

With #60, we plan to add the following extension to Text:

extension Text {
    init(localizable: String.Localizable) 
}

This will be compatible right back to iOS 13 and would allow the following usage:

Text(localizable: .myKey)

Which is great, but it's constrained to Text only so in the many places where SwiftUI provides the LocalizedStringKey convenience initialisers, it would be useless. For example:

public func navigationTitle(_ titleKey: SwiftUI.LocalizedStringKey) -> some SwiftUI.View

Instead, you'd have to use the Text-based modifier:

public func navigationTitle(_ title: SwiftUI.Text) -> some SwiftUI.View

In the Xcode 15.2 .swiftinterface file, I see 160 occurrences of _ titleKey: SwiftUI.LocalizedStringKey, so there are a lot of these convenience initialisers/modifiers ๐Ÿ™ˆ

In theory, I guess that we could also generate our own versions, such as the following:

public func navigationTitle(localizable: String.Localizable) -> some View {
    navigationTitle(Text(localizable: localizable))
}

// Usage:
navigationTitle(localizable: .myKey)

It would be nice, but might bloat the generated code a bit. Perhaps it could be something hidden behind a toggle though.

@liamnichols you might be correct about me missing out on markdown formatting, but the thing is when i need formatting it's either already in Localizable.xcstrings or done via string interpolation as suggested. This seems to work correctly as long as i wrap everything at the end with LocalizedStringKey.

I see that your approach is to extend Text and go from there. However my only interest in this library is with producing constants and utilizing them with LocalizedStringKey.
Besides Text i also use Label, so as you might have pointed out extending initializers for SwiftUI are work intensive and pointless , when we can just hand over localizable to LocalizedStringKey or Text.

Perhaps this ends up being the best way to achieve it:

func localizable(_ localizable: String.Localizable) -> LocalizedStringResource {
    LocalizedStringResource(localizable: localizable) // < This initializer would need to be exposed
}

func localizable(_ localizable: String.Localizable) -> LocalizedStringKey {
    "\(LocalizedStringResource(localizable: localizable))"
}

Text(localizable(.key))
    .navigationTitle(localizable(.myDeviceVariant))

Label(localizable(.myPlural(1)), systemImage: "test")

I'm not a huge fan of the global functions, but it seems like it might make it easier than having to generate multiple sets of constants for different types.

@liamnichols i think i'll live with interpolation for now, nothing inherently wrong with using it.
You can close this issue, questions exhausted.
And many thanks for implementing this tool and answering questions swiftly.

Ok i'll close this for now.

Just a heads up, #82 introduces a LocalizedStringKey.init(localizable:) initalizer alongside the Text one. I'm also taking the conversation/idea about the localizable(_:) global functions to #81 ๐Ÿ‘