nsNeruno
nsNeruno

Reputation: 81

Xcode Strings Catalog does not work in SwiftUI Framework (.xcframework) Project

I have defined a Strings Catalog for my iOS Framework Project, not an iOS App Project. And it didn't seem to perform any translations nor providing error feedbacks if something went wrong. The project is currently using an app-level localization, not using system's so I have to maintain UserDefaults to persist the last used locale identifier.

First, here's a helper class I defined as ObservableObject to help with locale switching, also including an extenstion to Locale class to provide shortcut to String init with Locale. The examples here are for testing English and Arabic translations.

class LanguageSettings: ObservableObject {
    
    init() {
        var localeId = UserDefaults.standard.value(forKey: "locale") as? String
        if localeId == nil {
            localeId = Locale.current.identifier
        }
        switch localeId?.lowercased() {
        case "en","en-us":
            break
        case "ar":
            locale = LanguageSettings.arabic
            break
        default:
            break
        }
    }
    
    init(localeTest: Locale) {
        locale = localeTest
    }
    
    @Published var locale: Locale = Locale(identifier: "EN-US") {
        didSet {
            switch locale.identifier.lowercased() {
            case "en", "en-us":
                break
            case "ar":
                break
            default:
                locale = oldValue
                break
            }
            updateUserDefaults()
        }
    }
    
    private func updateUserDefaults() {
        UserDefaults.standard.setValue(locale.identifier, forKey: "locale")
    }
    
    static let english = Locale(identifier: "EN-US")
    static let arabic = Locale(identifier: "AR")
    
    static let availableLocales = [
        english, arabic
    ]
}

extension Locale {
    
    func string(localized: String.LocalizationValue, comment: StaticString? = nil) -> String {
        
        let localizedString = String(localized: localized, bundle: Bundle.main, locale: self, comment: comment)
        print("Value for \(localized) for \(identifier) is \(localizedString)")
        return localizedString
    }
}

Then here's a SwiftUI View consuming it.

import Foundation
import SwiftUI

struct ErrorDisplay: View {
    
    @EnvironmentObject private var languageSettings: LanguageSettings
    @Environment(\.locale) private var locale: Locale
    
    var body: some View {
        
        let localeIdToName: [String: String] = [
            "en": "English",
            "en-us": "English",
            "ar": "Arabic"
        ]
        
//        let locale = languageSettings.locale
        
        GeometryReader { _ in
            VStack {
                Image(.exclamationTriangleSolid).foregroundColor(.white)
                    .padding(.top, 10)
                Text(locale.identifier)
                Divider().background(.white)
                HStack {
                    Text(localeIdToName[locale.identifier.lowercased()] ?? "-")
                        .padding(.all, 5)
                        .foregroundColor(.black)
                    Image(systemName: "chevron.down").foregroundColor(.black)
                        .padding(.trailing, 5)
                }
                    .background(.white)
                    .padding(.all, 8)
                    .containerShape(Rectangle())
                    .onTapGesture {
                        withAnimation {
                            showLocalePicker = true
                        }
                    }
                Text(message ?? "An error has occurred")
                    .multilineTextAlignment(.center)
                    .font(.system(size: 16))
                    .foregroundColor(.white)
                Button(
                    action: onReload
                ) {
                    Text(locale.string(localized: "Reload Video")
                }
                    .padding(.vertical, 10)
            }
        }
            .sheet(isPresented: $showLocalePicker) {
                Text(
                    locale.string(localized: "Select Locale")
                ).padding()
                Text("Select Locale")
                ForEach(0..<availableLocales.count) { i in
                    let locale = availableLocales[i]
                    let name = localeIdToName[locale.identifier.lowercased()]
                    
                    Button(
                        action: {
                            withAnimation {
                                showLocalePicker = false
                            }
                            if name != nil {
                                languageSettings.locale = locale
                            }
                        }
                    ) {
                        Text(name ?? "-")
                    }.padding()
                }
                
                Button(
                    role: .destructive,
                    action: {
                        withAnimation {
                            showLocalePicker = false
                        }
                    }
                ) {
                    Text(locale.string(localized: "Cancel"))
                }.padding()
            }
    }
    
    var message: String? = nil
    
    var availableLocales: [Locale] = LanguageSettings.availableLocales
    
    var onReload: () -> Void
    
    @State private var showLocalePicker = false
}

My Strings Catalog is already at 100% Translation Progress as indicated below. Strings Catalog.

I also tried previewing the View like this.

#Preview {
    ErrorDisplay(
        onReload: {}
    )
        .environment(\.locale, LanguageSettings.arabic
        )
        .environmentObject(
            LanguageSettings(localeTest: LanguageSettings.arabic)
        )
}

Then I did confirm that injected @Environment level Locale was indeed correct. I also defined an extension to Locale class to help with tracing.

Every time I changed the locale, it did trigger the @Environment locale to rebuild the View so it kept printing for each time I changed the locale. But on the print result indicated that it always returned the Localization Key String instead.

I have looked at similar thread here and no answers provided there yet.

Also to clarify, this is a Framework Project which I intend to use, build and distribute as .xcframework format. And I do not use Swift Package Manager nor Podfile here.

Upvotes: 0

Views: 458

Answers (1)

nsNeruno
nsNeruno

Reputation: 81

Answering my own question after some trial-and-errors & reading past posts, I replaced the Strings Catalog with the Legacy Strings File and generated the .lproj files, referring to this old answer, after being referred from a specific Apple forum post.

So editing on my existing code from this

// Before
extension Locale {
    func string(localized: String.LocalizationValue, comment: StaticString? = nil) -> String {
        return String(localized: localized, bundle: bundle, locale: self, comment: comment)
    }
}

Into this

// After
extension Locale {
    func string(localized: String, comment: String = "") -> String {
        // Fetch my custom framework's bundle, defaults to Bundle.main
        var bundle = Bundle(identifier: "com.framework.personal") ?? Bundle.main
        // Look for lproj file path inside the bundle then load the localized bundle
        if let bundlePath = bundle.path(forResource: languageCodeFromIdentifier, ofType: "lproj") {
            bundle = Bundle(path: bundlePath) ?? bundle
        }
        return NSLocalizedString(localized, bundle: bundle, value: localized, comment: comment)
    }

    // Helper getter to fetch language code from the Locale as reading from language?.languageCode is not reliable after some tests.
    var languageCodeFromIdentifier: String {
        get {
            let languageCode = identifier.split(separator: "_").first?.lowercased()
            return languageCode ?? "en"
        }
    }
}

This worked for me right now for my use case where the string has to be localized by app defined Locale instead of Locale.current.

This should be a quick workaround, but not clean enough yet, which I will optimize on my own project.

Upvotes: 0

Related Questions