Skip to content

Instantly share code, notes, and snippets.

@liamnichols
Last active January 18, 2024 10:22
Show Gist options
  • Save liamnichols/81d2e420067f431c59c97aeeb47ee19d to your computer and use it in GitHub Desktop.
Save liamnichols/81d2e420067f431c59c97aeeb47ee19d to your computer and use it in GitHub Desktop.
import Foundation
public struct Localizer {
/// Returns the best match localized string for the given locale based on the languages available as part of the bundle.
///
/// This method is similar to the system `NSLocalizedString(_:tableName:bundle:value:comment:)` function, but its resolution is much more granular.
///
/// With `NSLocalizedString`, if the current language is missing a phrase for the specified `key`, it would return the `value`, or the `key` even if the bundle contained the phrase in a different (but relevant) language.
///
/// Using this method, a lookup list will be made using `Bundle.preferredLocalizations(from:forPreferences:)` instead, and each language in the order of most preferable will be searched instead.
/// If the phrase isn't found in any of the preferred languages, the value will instead be obtained from the `developmentLocalization` instead.
/// Failure to find a phrase using any of the described approaches will result in the `key` being returned.
///
/// This behaviour is much more preferred, especially when the `preferredLocale` has regional information such as `es_AR`. With a development language of `en`, the following languages will be checked in order of top to bottom:
///
/// - `es_AR`
/// - `es_419` (Latin America)
/// - `es`
/// - `en`
///
/// - Parameters:
/// - key: The key for a string in the table identified by `table`.
/// - table: The bundled’s string table to search. If `table` is `nil` or is an empty string, the method attempts to use the table in **Localizable.strings**.
/// - bundle: The bundle containing the table’s strings file. The main bundle is used if one isn’t specified.
/// - preferredLocale: The `Locale` containing the language, region and script information to indicate a preferred localisation.
/// - Returns: A localized string using the first language in the preferred order where the `key` exists, or the localisation from the development language otherwise the `key` if no localization could be found.
public static func string(
forKey key: String,
table: String? = nil,
bundle: Bundle = .main,
preferredLocale: Locale
) -> String {
// Figure out the preferred localizations based on what is available and what is requested
let preferredLocalizations = Bundle.preferredLocalizations(
from: bundle.localizations,
forPreferences: [preferredLocale.localizationIdentifier]
)
// Loop through the preferred localizations and try to find a match
for localization in preferredLocalizations {
if let value = bundle.localizedString(forKey: key, table: table, localization: localization) {
return value
}
}
// Otherwise, try the development language
if let localization = bundle.developmentLocalization,
let value = bundle.localizedString(forKey: key, table: table, localization: localization) {
return value
}
// Finally, just return the `key` if the value couldn't be resolved
return key
}
}
private extension Bundle {
private static let notLocalizedMarker = "__NOT_LOCALIZED__"
/// Returns a localised string for the specified localisation, or `nil` if either the given `localization` was not supported or the phrase with the given `key` was not part of it.
func localizedString(forKey key: String, table: String?, localization: String) -> String? {
// Load a Bundle representing the specified localization, exit early if it doesn't exist
guard let path = path(forResource: localization, ofType: "lproj"), let bundle = Bundle(path: path) else { return nil }
// Query the localized string from within
let value = bundle.localizedString(forKey: key, value: Bundle.notLocalizedMarker, table: table)
// Return the value as long as it was localized as expected, otherwise return nil
if value != Bundle.notLocalizedMarker {
return value
} else {
return nil
}
}
}
private extension Locale {
/// Returns an identifier used in an **.lproj** bundle to represent the given local.
var localizationIdentifier: String {
[languageCode, scriptCode, regionCode]
.compactMap { $0 }
.joined(separator: "-")
}
}
import Foundation
/// A closure used to provide the preferred locale for the `NSLocalizedString(_:tableName:bundle:value:comment:)` override used by GlobalResources.
public var NSLocalizedStringPreferredLocaleProvider: () -> Locale = {
Locale(identifier: "en_US_POSIX")
}
/// An overload of Foundation's `NSLocalizedString(_:tableName:bundle:value:comment:)` method that redirects to `Localizer` using the locale provided by `NSLocalizedStringPreferredLocaleProvider`.
///
/// This exists in order to trick definitions in R.generated.swift to use `Localizer` without modifying the generated code directly. While a more stable solution would be preferred, this approach remains until we can gain greater control over `StringResource` generation.
func NSLocalizedString(
_ key: String,
tableName: String? = nil,
bundle: Bundle = Bundle.main,
value: String = "",
comment: String
) -> String {
Localizer.string(
forKey: key,
table: tableName,
bundle: bundle,
preferredLocale: NSLocalizedStringPreferredLocaleProvider()
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment