I could decipher the key part out of it (in this case "Cancel...")
then I could compare that key to the result of String(localized: localizationValue, ...) and if they are equal – the localisation is missing. Or... it just so happened that localised string matches the key, so the check won't be quite robust.
With the older machinery there was a way to pass a default value to be used when localisation is absent, but the older machinery doesn't work with string catalogues which I want to use.
There isn’t a concrete API for knowing this explicitly (i.e. there is no API that returns some Bool that indicates if lookup succeeded).
This output format is not stable and should not be used as parsing input, any code that does this will definitely break in the future.
String(localized:) also supports a default value - String(localized: “MY_KEY”, defaultValue: “Hello world”) looks up the key “MY_KEY” but will use the default value “Hello world” when a localized version of the string cannot be found in the specified bundle. I’m not sure if that specifically helps with your problem, but there is feature parity here.
Is there something in particular you’d like to do with the information about whether the key was found or not? Perhaps there’s another way to go about what you’re trying to do.
Correct - the key doesn’t have any interpolated values, it’s just a literal value found in the strings file so it takes a StaticString. The default value is the value that can contain interpolations and is expected to be text for display so it is a String.LocalizationValue
Any reason there's no key: String.LocalizationValue, defaultValue: .... initialiser version?
Totally understandable. The plan was to see if it could make that workaround happen (which is to be seen) then use it on my own risk, and when it breaks in the future – reassess the situation right there and then: maybe there'll be a proper way by then, or another workaround, or might well be a whole new way of doing localisation altogether, or I will retire by then, etc.
It’s because it doesn’t really have utility. If both were a String.LocalizationValue, then:
If key and defaultValue are the same, then there’s no need to specify both, this is the existing behavior of String(localized:) (no default value). The key is derived from the content of the provided default value
If key and defaultValue are not the same, then it does not make sense for key to be a String.LocalizationValue with interpolations. When performing lookups in a bundle, the keys we match against are constant strings with no interpolations, so it doesn’t make sense to interpolate values lazily into the key like String.LocalizationValue does. For example, String(localized: “Hello \(name)”, defaultValue: “Goodbye \(name2)”) doesn’t really make sense because name will never be used and it makes it appear as if it’s an entirely separate localized string
In general, we’re trying to move away from a world where you write format specifiers in code that get evaluated at runtime. They bring inherent security risks that are difficult to mitigate. That’s why String.LocalizationValue uses string interpolations, they reduce the risk of format specifiers. So for those reasons, using %d in the defaultValue is a non-goal. Technically we could enable something like:
However, that is ambiguous to me as to whether the key looked up in the bundle should be “key 42” or “key %lld”. If it’s the latter (like I believe you’re looking for) it also requires duplicating the number 42 and it’s ambiguous in the caller what happens when the numbers don’t match (which is used?).
I don’t know if I fully follow how that would resolve what you’re trying to do.
Something like this is more feasible on the technical side since it’s more explicit about what’s happening. However, I’d like to understand a bit more about what you’re hoping to do with the information. What behavior are you hoping to provide when the key isn’t found? Presumably something still would need to be displayed in the UI to the user, and using the default value (in the development language) seems like a good fallback, but if you have a use case where you’d want to display something else instead that requires custom logic at the calling side I’d love to learn more about that.
But this unsafety is still there, just moved to another place, no? Now I could have my app crashed specifying a wrong formatter (%d instead of %@ or vice versa) in the string catalogue. In a certain peculiar way it was even safer in C, that had -Werror=format to check mismatches like "%s", 42 with the likes of print family of operations.
What behavior are you hoping to provide when the key isn’t found
Nothing extraordinary, any or all of these:
show missing localisations in logs
perform precondition checks ("crash fast")
paint missing strings in red (or ornament them differently), e.g. based on a toggle in the app
find the string with fallback (not found in one table/bundle -> look into another)
As a workaround I could:
prefix all my keys with an odd looking symbol (that won't ever happen in localised versions)
check if the localised versions start with that odd symbol - then I know the localisation is missing.
– but that obviously not ideal as my otherwise nice looking code will be polluted with those odd symbols.
To an extent yes, but in practice I’ve found this type of mistake much less common since it’s much less common for a localizer to change a %lld to a %s accidentally in the localized version than it is for a developer to write %d for an Int thinking that it works when it actually doesn’t (because Int is %ld). It also didn’t move but rather there were always mismatches possible in the strings file, but we’ve now eliminated a set of possible mismatches in code so the problem space is smaller rather than just relocated. But you’re correct the problem isn’t eliminated entirely.
Regarding what you’re hoping to do, some of that is actually already possible today:
If you run your application with the NSShowNonLocalizedStrings environment variable set, Foundation will produce error logs for any key/table/bundle pair that was not found. Xcode has an option for this in the Options section of the Run scheme editor. However, it does appear that Foundation does not exhibit this behavior on non-Darwin platforms, so that’s likely a bug that we should address in swift-corelibs-foundation / would address if string localization is brought to swift-foundation.
When running with NSShowNonLocalizedStrings enabled, all strings not found in a strings table will be displayed in all caps (for example, "Hello \(name)” would display as HELLO Jeremy in the UI when unlocalized). So looking for unnatural, all-caps text in the UI can help find unlocalized content.
I’d actually advise against this pattern. Bundle today does exhibit this type of behavior in some specific scenarios and we’ve found it to be a significant performance problem and are actively trying to move away from some of these patterns in Foundation.
The fast crash path and perhaps other behavior is not something Foundation itself supports today, but if there are use cases when that is desirable, perhaps that’s something that Foundation should support and standardize for all apps.