mac-cain13 / R.swift

Strong typed, autocompleted resources like images, fonts and segues in Swift projects
MIT License
9.44k stars 752 forks source link

R.swift not work well with automatic grammar agreement (`^[..](inflect: true)`) #858

Open susi021 opened 9 months ago

susi021 commented 9 months ago

For example, when I define the string "%lld person" = "^[%lld person](inflect: true)"; in localizable.string, and call it with the R.string.localizable.lldPerson(2), it will return ^[2 person](inflect: true), not the 2 people as I expected.

To leverage the automatic grammar agreement, I need to define the following:

"person" = "person";
"%lld %@" = "^[%lld %@](inflect: true)";

Use the R.swift to get the translation:

let person = R.string.localizable.person()
let attributedString = AttributedString(localized: "\(num) \(person)")
return String(attributedString.characters)

So that it will return "2 people", which is translated and pluralized.

Is there any better way to do that?

tomlokhorst commented 9 months ago

Rswift works with .stringsdict files from Xcode, see: https://developer.apple.com/documentation/xcode/localizing-strings-that-contain-plurals

Alternatively, as of Xcode 15, you can use a strings catalog: https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog

I suggest you first try to make your required behaviour work without Rswift, using the features from the platform, before adding Rswift code generation.

susi021 commented 8 months ago

Thanks for the advice @tomlokhorst!

To make use of automatic grammar agreement, I will retrieve the localizable string without relying on R.swift for handling plurals.

Additionally, I'm curious to know if R.swift has any plans to support String Catalog, or even implement automatic grammar agreement?

fthdgn commented 3 months ago
"%lld car" = "^[%lld car](inflect: true)";

To generate a R.swift method for this key, we should be able to 1- access it by the key 2- pass arguments 3- generate an AttributedString from it. (inflect system is a a feature of AttributedString)

The localization system of Swift, use ExpressibleByStringInterpolation everywhere.

LocalizedStringResource String.LocalizationValue LocalizedStringKey all of these use ExpressibleByStringInterpolation to create necessary information to access and format localized string.

The problem is none of them have any other init to pass key and argument types.

I achieved this by utilizing LocalizedStringResource.init with defaultValue which requires iOS 16.

let carKey: LocalizedStringResource = .init("%lld car", defaultValue: "\(2)")
print(String(AttributedString(localized: carKey).characters))
// prints: 2 cars

I tried other examples.

On this case there are multiple plural and the order of objects are different on different localizations.

// English
"pen_and_book" = "^[%1$lld pen](inflect: true) and ^[%2$lld book](inflect: true) ";
// French
"pen_and_book" = "^[%2$lld livre](inflect: true) and ^[%1$lld stylo](inflect: true) ";
let enKey: LocalizedStringResource = .init("pen_and_book", defaultValue: "\(1)\(2)", locale: .init(identifier: "en"))
print(String(AttributedString(localized: enKey).characters))
// prints: 1 pen and 2 books

let frKey: LocalizedStringResource = .init("pen_and_book", defaultValue: "\(1)\(2)", locale: .init(identifier: "fr"))
print(String(AttributedString(localized: frKey).characters))
// prints: 2 livres et 1 stylo

This method can also replace NSLocalizedString completly (after iOS 16)

"hello_with_name" = "Hello %@!";
let key: LocalizedStringResource = .init("hello_with_name", defaultValue: "\("World")")
print(String(localized: key))
// prints: Hello World!

This is a prelimenery extensions to convert Rswift String resources to String and Attributed String.

@available(iOS 16, *)
extension RswiftResources.StringResource1 {
    // defaultValue does not support CVarArg protocol. I could not find a suitable protocol.

    func localizedStringResource(_ arg1: Arg1) -> LocalizedStringResource where Arg1 == String {
        switch source {
        // Cannot pass bundle inside of source. Its type is different.
        case let .selected(_, locale):
            return .init(self.key, defaultValue: "\(arg1)", table: self.tableName, locale: locale, comment: self.comment)
        default:
            return .init(self.key, defaultValue: "\(arg1)", table: self.tableName, comment: self.comment)
        }
    }

    func localizedStringResource(_ arg1: Arg1) -> LocalizedStringResource where Arg1 == Int {
        switch source {
        case let .selected(_, locale):
            return .init(self.key, defaultValue: "\(arg1)", table: self.tableName, locale: locale, comment: self.comment)
        default:
            return .init(self.key, defaultValue: "\(arg1)", table: self.tableName, comment: self.comment)
        }
    }
}

@available(iOS 16, *)
extension LocalizedStringResource {
    var asString: String {
        .init(localized: self)
    }

    var asAttributedString: AttributedString {
        .init(localized: self)
    }
}

extension AttributedString {
    var asString: String {
        .init(self.characters)
    }
}

How to use:

print(R.string.localizable.lldCar.localizedStringResource(5).asAttributedString.asString)
// prints: 5 cars

print(R.string.localizable(preferredLanguages: ["fr"]).lldCar.localizedStringResource(5).asAttributedString.asString)
// prints: 5 voitures

Currently there is SwiftUI extension of Rswift, however, they lose inflect information.

This implementation fixes that problem.

@available(iOS 16, *)
extension Text {
    public init<Arg1: CVarArg>(_ resource: StringResource1<Arg1>, _ arg1: Arg1) where Arg1 == String {
        self.init(resource.localizedStringResource(arg1))
    }

    public init<Arg1: CVarArg>(_ resource: StringResource1<Arg1>, _ arg1: Arg1) where Arg1 == Int {
        self.init(resource.localizedStringResource(arg1))
    }
}
Text(R.string.localizable.lldCar, 5)
// displays: 5 cars