Remote Localizable Strings

by Junda Ong
samwize.com

Intermittent Working

  • Work
  • Indie
  • Work
  • Indie
  • Work
  • Indie 👈🏻

1. Fetch remotely

let (data, _) = try await urlSession.data(from: url)

try data.write(to: fileUrl) // eg. Base/Localizable.strings

2. How to read .strings format?

"some-key" = "Hello World";
...

 

let dict = NSDictionary(contentsOf: fileUrl) as! [String: String]

3. NSLocalizedString Wrapper

func LS(_ key: String, tableName: String? = nil, bundle: Bundle = Bundle.main) -> String {
    let value = NSLocalizedString(key, tableName: tableName, bundle: bundle, comment: "")
    if value != key { return value }

    // Fall back to en
    guard
        let path = bundle.path(forResource: "en", ofType: "lproj"),
        let enBundle = Bundle(path: path)
        else { return value }
    return NSLocalizedString(key, tableName: tableName, bundle: enBundle, comment: "")
}

 

func LS(_ key: String, tableName: String? = nil, bundle: Bundle = Bundle.main) -> String {
    if let remoteString = dict[key] {
        return remoteString
    }
    
    ...
}

4. Handle arguments

func LS(_ key: String, _ args: CVarArg...) -> String {
    ...

    return String(format: remoteString, arguments: args)
}

 

// Wrong type will CRASH!
String(format: "Carelessly translated %@", arguments: [123])

Let's Write Unit Tests

class SafeStringFormatTests: XCTestCase {

    func testArgumentIntButWronglyTranslatedToStringFormat() {
        let translated = "Carelessly translated %@" // From "Originally %d"
        let t = String(safeFormat: translated, arguments: [123])
        XCTAssertEqual("Carelessly translated 123", t)
    }

}

Safe String Format

extension String {
    init(safeFormat format: String, arguments: [CVarArg]) {
        // Use Regex capture %@, %d, %1$@ etc
        let regex = try! NSRegularExpression(pattern: "%\\d?\\$?[@d]{1}", options: [])
        let matches = regex.matches(in: format, options: [], range: NSRange(location: 0, length: format.count))
        
        // Fix format based on the argument type
        ...
    }
}

Bonus: Strings in a separate repo

  • 1 source of truth
  • No more git conflicts
  • Use git submodule
  • Hooks to & from translation platform