String(localized:) has no separate key and value?
Asked Answered
F

4

8

New in iOS 15, we are invited to use this String initializer method to make localizable strings in our Swift code:

init(localized keyAndValue: String.LocalizationValue, 
    table: String? = nil, bundle: Bundle? = nil, 
    locale: Locale = .current, comment: StaticString? = nil)

The trouble is that the first parameter is, as the internal name suggests, used for both the key and the value. You can see that from this localized French strings file:

/* Alert message: Report a tap */
"You tapped me!" = "Vous m'avez tapé!";

That resulted from my saying

String(localized:"You tapped me!", comment: "Alert message: Report a tap")

and localizing for French.

That's totally wrong! This is supposed to be a list of key–value pairs; we shouldn't be using the English user-facing text as a key.

For one thing, if we now change the English text in our String(localized:comment:) call, our French translation will break. Also, we would be unable to have different French translations for the same English text used in different contexts.

What are we supposed to do about this?

Foreman answered 17/9, 2021 at 17:39 Comment(11)
Isn't using the English value as a key is merely a convention? If a localised string isn't found then the key is shown. If you use English then localisation "gaps" at least show something slightly meaningful but you can use "codes" like "TAPMSG001" and put the appropriate message in your English localisation file "TAP001" = "You tapped me!" And the same for french and so on. This is what you describe in the 3rd paragraph of your answer. The problem is if you miss a localisation the user now sees "TAPMSG001". I have certainly experienced this sort of thingDunghill
I don't see how this is different to i8n previously. I always had an English localizable strings file with "Warning" = "Warning" etc. this is what I provided to my translators as the base file for their language and they send back with "Warning" = "Avertissement" or whateverDunghill
Well that was always wrong; you shouldn't have been using the English as the key either. Indeed, this is one of the reasons Apple switched from strings files alone to xliff, which contains the key, the English, the value, and the comment, when communicating with your translator.Foreman
Yes, I simplified, there was xliff involved, but even Apple's documentation for NSLocalizedString shows using English as the key with the same behaviour. I don't see this as a "bug". Unless I am missing something there is nothing to stop you using codes instead of English and then treating the base locale the same as any other strings file. It is just a convention and how disciplined you are with your design and i8n workflows.Dunghill
I totally agree. What I object to is that I can't do that directly from the String initializer. The compiler used to come along and build the xliff from NSLocalizedString using the key and value I gave there. I could have my cake and eat it too with no explicit English localization. But when it does that for this new String initializer I can't draw that distinction.Foreman
Also note that the wwdc videos on this topic all act as if the parameter here should be the English. That's why I was looking for another parameter for the key.Foreman
@Dunghill So for example if you say NSLocalizedString("key", value: "value", comment: "comment") and then export and import a localization, the resulting Localizable.strings file has "key" = "<translation>", but the string shows up in the English interface as "value". That's how I've always done it. It's easy and convenient. So what I'm saying is, that's how String(localized:) should work.Foreman
Is there any documentation for String(localized:...? The official documentation still treats it as beta, with No overview available, 4 weeks after iOS 15 has been released.Toehold
@Toehold It depends what you mean by "documentation", obviously. My chief source of information, apart from experimentation, is the WWDC videos.Foreman
@Foreman Right, Streamline your localized strings from WWDC21 includes a code comment saying Supports user's preferred numbers, pluralization, RTL variables isolation... Previously: .localizedStringWithFormat(). The method it is supposed to replace has a very different signature. Are you referring to any more specific source?Toehold
@Toehold The replacement of NSLocalizedString by String(localized) is the entire plot of that video, starting at developer.apple.com/videos/play/wwdc2021-10221/?time=239 And in doing that, he gets exactly the sort of bad results I'm asking about. That's what I find so mystifying.Foreman
F
5

I regard this as a major bug in String(localizable:). If we were using NSLocalizedString, we would have individual key: and value: parameters. String(localizable:) needs that.

I can think of two workarounds. One is: don't use String(localizable:). Just keep on using NSLocalizedString.

The other is to localize explicitly for English. Instead of entering the English user-facing text as the localized: parameter, enter a key string. Then, to prevent the keys from appearing in the user interface, export the English localization and "translate" the keys into the desired English user-facing text. Now import the localization to generate the correct English .strings files.

(If your development language isn't English, substitute the development language into those instructions.)

Now when you export a different localization, such as French, the <trans-unit> element's id value is the key, to which the translator pays no attention, and the <source> is the English, which the translator duly translates.

To change the English user-facing text later on, edit the English Localizable.strings file — not the code. Nothing will break because the key remains constant.

Foreman answered 17/9, 2021 at 17:39 Comment(1)
Thanks for sharing this. It's hard to believe String(localizable:) shipped with such a major flaw. I'm currently using NSLocalizedString but would be nice to avoid putting English "values" in code and rather isolate all translations to strings files. Do you have any other gothas/recommendations for the second approach since suggesting it?Gombosi
D
10

If you want to separate the key and value you can call String.init(localized:defaultValue:table:bundle:locale:comment:). This allows you to specify a default value to use if the key does not exist in your strings file, and is used as the default translation when using Xcode's Export Localisations feature.

For example:

let alertMessage = String(localized: "alert.message.report-a-tap", defaultValue: "You tapped me!")

// Xcode's Export Localisations generates the following:
"alert.message.report-a-tap" = "You tapped me!";
Demonstrative answered 3/1, 2023 at 23:31 Comment(2)
How did it miss the point? It allows you to separate the key and the value. You no longer have to use the English facing text as the key and you can change the English text without breaking your other translations. So for example if you say String(localized: "key", defaultValue: "value", comment: "comment") and then export and import a localization, the resulting Localizable.strings file has "key" = "<translation>", but the string shows up in the English interface as "value". Is that not what you want?Demonstrative
Okay, you're right, I'm sort of being silly here, sorry about that.Foreman
F
5

I regard this as a major bug in String(localizable:). If we were using NSLocalizedString, we would have individual key: and value: parameters. String(localizable:) needs that.

I can think of two workarounds. One is: don't use String(localizable:). Just keep on using NSLocalizedString.

The other is to localize explicitly for English. Instead of entering the English user-facing text as the localized: parameter, enter a key string. Then, to prevent the keys from appearing in the user interface, export the English localization and "translate" the keys into the desired English user-facing text. Now import the localization to generate the correct English .strings files.

(If your development language isn't English, substitute the development language into those instructions.)

Now when you export a different localization, such as French, the <trans-unit> element's id value is the key, to which the translator pays no attention, and the <source> is the English, which the translator duly translates.

To change the English user-facing text later on, edit the English Localizable.strings file — not the code. Nothing will break because the key remains constant.

Foreman answered 17/9, 2021 at 17:39 Comment(1)
Thanks for sharing this. It's hard to believe String(localizable:) shipped with such a major flaw. I'm currently using NSLocalizedString but would be nice to avoid putting English "values" in code and rather isolate all translations to strings files. Do you have any other gothas/recommendations for the second approach since suggesting it?Gombosi
H
0

I was experiencing the same problem using SwiftGen to produce my localized strings and the solution for me was to ensure that my generated localized strings file is inside of the appropriate language folder rather than the separate Generated folder I would otherwise use.

Folder hierarchy

Harrisharrisburg answered 3/1, 2022 at 23:46 Comment(1)
That's cool but it has nothing to do with the question. :) You might want to take this out and instead do your own self question-and-answer on the issue with SwiftGen, which is certainly interesting and worth documenting.Foreman
L
0

The explicit localization approach using keys is imo the correct way here, I think the parameter name keyAndValue is just misleading.

See another initializer using String.LocalizationValue, for AttributedString:

https://developer.apple.com/documentation/foundation/attributedstring/3867590-init

On that one the parameter is just named key:

init(localized key: String.LocalizationValue, options: AttributedString.FormattingOptions = [], table: String? = nil, bundle: Bundle? = nil, locale: Locale? = nil, comment: StaticString? = nil)

After all that seems to align well with SwiftUI's usage of LocalizedStringKey, where no value is given next to the key too.

Beside the current lack of documentation, I don't understand the need to introduce String.LocalizationValue having the mentioned LocalizedStringKey but at least it seems to be aligned from the way it's done in SwiftUI, but not from the previous localizedString(forKey:value:table) / NSLocalizedString way.

In my recent projects we always used key-based translations even for the development language, so this new initializer would work pretty well. But Apple should change the parameter name and update the documentation accordingly, as they did for AttributedString.

Laboy answered 5/3, 2022 at 11:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.