Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only generating constants/definitions and not localization helpers #104

Open
liamnichols opened this issue Jul 9, 2024 · 2 comments
Open
Labels
enhancement New feature or request

Comments

@liamnichols
Copy link
Owner

liamnichols commented Jul 9, 2024

The code generated by xcstrings-tool can be broken down into two categories:

  1. Definitions
  2. Helpers

Definitions

internal struct Localizable: Sendable {
enum BundleDescription: Sendable {
case main
case atURL(URL)
case forClass(AnyClass)
#if !SWIFT_PACKAGE
private class BundleLocator {
}
#endif
static var current: BundleDescription {
#if SWIFT_PACKAGE
.atURL(Bundle.module.bundleURL)
#else
.forClass(BundleLocator.self)
#endif
}
}
enum Argument: Sendable {
case int(Int)
case uint(UInt)
case float(Float)
case double(Double)
case object(String)
var value: any CVarArg {
switch self {
case .int(let value):
value
case .uint(let value):
value
case .float(let value):
value
case .double(let value):
value
case .object(let value):
value
}
}
}
let key: StaticString
let arguments: [Argument]
let table: String?
let bundle: BundleDescription
fileprivate init(
key: StaticString,
arguments: [Argument],
table: String?,
bundle: BundleDescription
) {
self.key = key
self.arguments = arguments
self.table = table
self.bundle = bundle
}
/// A key that conflicts with a keyword in swift that isn't suitable for a variable/method and should be backticked.
///
/// ### Source Localization
///
/// ```
/// Continue
/// ```
internal static var `continue`: Localizable {
Localizable(
key: "continue",
arguments: [],
table: "Localizable",
bundle: .current
)
}

Helpers

internal init(localizable: Localizable, locale: Locale? = nil) {
let bundle: Bundle = .from(description: localizable.bundle) ?? .main
let key = String(describing: localizable.key)
self.init(
format: bundle.localizedString(forKey: key, value: nil, table: localizable.table),
locale: locale,
arguments: localizable.arguments.map(\.value)
)
}

extension Bundle {
static func from(description: String.Localizable.BundleDescription) -> Bundle? {
switch description {
case .main:
Bundle.main
case .atURL(let url):
Bundle(url: url)
case .forClass(let anyClass):
Bundle(for: anyClass)
}
}
}

#if canImport(SwiftUI)
import SwiftUI
@available(macOS 10.5, iOS 13, tvOS 13, watchOS 6, *)
extension Text {
/// Creates a text view that displays a localized string defined in the ‘Localizable‘ strings table.
internal 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 .float(let value):
stringInterpolation.appendInterpolation(value)
case .double(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))
}
}
@available(macOS 10.5, iOS 13, tvOS 13, watchOS 6, *)
extension LocalizedStringKey {
/// Creates a localized string key that represents a localized value in the ‘Localizable‘ strings table.
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
internal init(localizable: String.Localizable) {
var stringInterpolation = LocalizedStringKey.StringInterpolation(literalCapacity: 0, interpolationCount: 1)
if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) {
stringInterpolation.appendInterpolation(LocalizedStringResource(localizable: localizable))
} else {
stringInterpolation.appendInterpolation(Text(localizable: localizable))
}
let makeKey = LocalizedStringKey.init(stringInterpolation:)
self = makeKey(stringInterpolation)
}
/// Creates a `LocalizedStringKey` that represents a localized value in the ‘Localizable‘ strings table.
@available(macOS 11, iOS 14, tvOS 14, watchOS 7, *)
internal static func localizable(_ localizable: String.Localizable) -> LocalizedStringKey {
LocalizedStringKey(localizable: localizable)
}
/// Updates the underlying `key` used when performing localization lookups.
///
/// By default, an instance of `LocalizedStringKey` can only be created
/// using string interpolation, so if arguments are included, the format
/// specifiers make up part of the key.
///
/// This method allows you to change the key after initialization in order
/// to match the value that might be defined in the strings table.
fileprivate 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


While the helpers are helpful in getting you up and running in 95% of use cases, they are not always exactly what everybody wants. In my use case, I still have a UIKit application that does custom localizations, so the generated String.init(localizable:locale:) method is not actually useful to me because I need to resolve the language in a different way.

It would be good in my project if I could just generate the definitions, and then write my own String.init(localizable:) method that works exactly how I need it instead of using the generated one.


While I'm aware that I could still do this by just creating a method with a different name, the concern that I have with leaving the default String.init(localizable:) method is that it makes things confusing and risks potentially using the wrong method. It would be better to eliminate it entirely.

@liamnichols liamnichols added the enhancement New feature or request label Jul 9, 2024
@liamnichols
Copy link
Owner Author

Similar to modifying the Access Level, this can probably be achieved using a --exclude-helpers flag, which can also be read from build settings.

@wzio
Copy link

wzio commented Sep 27, 2024

The idea is great, as it provides more possibilities for customizing the loading of strings, such as switching languages within the app. Switching languages within an app commonly requires specifying a bundle.

let fileName: String = isEnglish ? "en" : "fr"
let path = Bundle.main.path(forResource: fileName, ofType: "lproj")
let bundle = Bundle(path: path!)
let string = bundle?.localizedString(forKey: key, value: nil, table: "Localizable")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants