Localiazble properties in Firestore. Swift example

Created
Updated
TagsFirebaseSwift

Have you ever had some content stored in Firebase Firestore that you'd want to be localized? The most straightforward solution would be to have localization string on client and send localization keys from Firebase making all the localization happen on client but in this short article I'd like to show you an example of how you could store localizations actually on Firestore.

Let's say you want your app to support english and ukrainian. Every text you send from Firestore should therefore have these 2 translations. Now how can we do that? Easy: every text would be an object (or map in Firestore terms) instead of a String and the object will hold different translatons. Here's how it might look like:

▼ text:
		en: Test
		uk: Тест

And here's an equivalent in JSON terms:

{
		"text": {
			"en": "Test",
			"uk": "Тест"
		}
}

So now let's move on and see how would you parse such a thing in Swift in your iOS/macOS app. Let's start straightforward:

struct FBLocalizable: Decodable {
		let value: String

    enum CodingKeys: String, CaseIterable, CodingKey {
        case en, uk
    }

    init(value: String) {
        self.value = value
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let key = CodingKeys.allCases.first(where: { Locale.current.identifier.hasPrefix($0.rawValue) })

        self.value = try container.decode(String.self, forKey: key ?? .en)
    }
}

Here's what's going on here: since on the client we care about only one value – the one that matches user's locale – we introduce a new struct with only one property, called value. Now because Firestore objec will contain all the translations we support we have to parse it and fetch the one needed. Because we need to first parse all supported translations we create an embedded enum which conforms to CodingKey (we use it later for decoding) and add supported language cases there. In our case it's en and uk but if you plan to support more, go ahead and add more cases. If you add a case now then your app will support content in this language out of the box without updating the app, just send objects with more supported translations. So now let's move onto the interesting part – decoding. The key here is to get current locale via Locale.current.identifier and compare it's prefix against supported languages, we'll use .hasPrefix for that. So essentially what we do is we go through all our supported languages – en and uk in this case – and check if current user's locale starts with one of this language codes. If yes, we decode the value of the corresponding key as String and assign it to value property. If no supported language was found then English (en) is used by default.

That's basically it, now every time we want to use it we just make sure we use FBLocalizable instead of a String in the parent struct/class:

struct Parent: Decodable {
		let id: Int
		let title: FBLocalizable
}

Basically that's it. All the content you want can be easily delivered localized. Everything is good here except for one little thing – upon using this structure we're forced to always call it's value property to access String type. For example: Text(parent.title.value). While this isn't horrible, it's quite suboprimall. Let's use Swift feature to make it better. We're going to use some help of property wrappers. This thing is a wrapper around some value that has some additional logic. Without further ado here's our property wrapper:

@propertyWrapper
struct FBLocalizable: Decodable {
    var wrappedValue: String

    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }

    enum CodingKeys: String, CaseIterable, CodingKey {
        case en, uk
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let key = CodingKeys.allCases.first(where: { Locale.current.identifier.hasPrefix($0.rawValue) })

        self.wrappedValue = try container.decode(String.self, forKey: key ?? .en)
    }
}

Here you can see that there's no difference to the structure we created except for the @propertyWrapper annotation in the very beginning and here our value property becomes wrappedValue – this is the requirement of the property wrapper. Now we can mark our property inside any parent structure as @FBLocalizable and because its wrappedValue has a type of String we can safely access it directrly without any .value calls:

struct Parent: Decodable {
		let id: Int
		@FBLocalizable var title: String
}

The call will be as straightforward as: Text(parent.title). That's it, that's how we can seemles integrate translations from Firebase into our project without exposing any APIs and technical details we use. The last thing I'd like to point out is that we have to use variables var instead of constants let when using property wrappers, which means we loose some immutability. This is something we might want to avoid so one thing you could do is to mark the property as a private(set) and you're good to go!

struct Parent: Decodable {
		let id: Int
		@FBLocalizable private(set) var title: String
}

Thanks for reading, I hope you enjoyed it! If you have any questions or suggestions please feel free to contact me: ☎️Contact