Working with .strings
files based localization is always a bit tricky (at least for me):
- It is not type safe and error prone approach where you should always double check strings you type in
- There's no way for changing translations on the fly in production, assume you made a typo and want to fix it ASAP, all you can do is fix it in bundled
.strings
and wait until new release - Also it's always a big mess regarding namespacing, you have to deal with long names or separate tables to achieve unique keys
I was always working dealing with .yml
files wherever it's possible, so here's a small tool that solves all the problems described above
Assume this is your Localizable.strings
file:
/* Home */
"home.tabs.feed" = "Feed";
"home.tabs.projects" = "My projects";
/* Feed */
"feed.title" = "See what's new";
/* Projects */
"projects.title" = "My projects";
/* Common */
"common.close" = "Close";
"common.cancel" = "Cancel";
"common.delete" = "Delete";
A lot of unnecessary code duplication and symbols (well, not big deal here, but in big project it will be much worse)?
This structure always look like a tree, so why not to describe it in a more appropriate way?
This is what wording.yml
can be look like:
home:
tabs:
feed: Feed
projects: My projects
feed:
title: See what's new
projects:
title: My projects
common:
close: Close
cancel: Cancel
delete: Delete
This file will be generated automatically during the build process every time you edit your wording.yml
file(s)
import Wording
public enum Wording: Wordingable {
fileprivate static var wording = [String: String]()
public static func complement(using wording: [String: Any]) {
complement(using: wording, path: nil)
}
private static func complement(using wording: [String: Any], path: String?) {
for (key, value) in wording {
let path = [path, key]
.compactMap { $0 }
.joined(separator: ".")
if let leaf = value as? String {
Self.wording[path] = leaf
} else if let node = value as? [String: Any] {
complement(using: node, path: path)
}
}
}
}
extension Wording {
public enum Home {
public enum Tabs {
public static var feed: String { leaf("home.tabs.feed") }
public static var projects: String { leaf("home.tabs.projects") }
}
}
public enum Feed {
public static var title: String { leaf("feed.title") }
}
public enum Projects {
public static var title: String { leaf("projects.title") }
}
public enum Common {
public static var close: String { leaf("common.close") }
public static var cancel: String { leaf("common.cancel") }
public static var deelete: String { leaf("common.deelete") }
}
}
private func leaf(_ path: String) -> String {
guard let leafValue = Wording.wording[path] else {
assertionFailure("No wording value for \(path)")
return ""
}
return leafValue
}
After that you can simply type Wording.Home.Tabs.feed
in your code with full IDE code autocompletion support
Much better, right? 🙂
In order to replace .strings
based localization to .yml
based follow these few steps:
- Add package dependency to your package with your
yml
localization resources
.package(url: "https://github.com/kutchie-pelaez-packages/Wording.git", branch: "master")
- Add target dependency to your target with localization resources
.product(name: "Wording", package: "Wording")
- Add plugin dependency to your target with localization resources
.plugin(name: "WordingGenerationPlugin", package: "Wording")
- Hit
cmd+B
to check everything works properly, plugin should generateWording.swift
file in build directory with structure based on yourwording_en.yml
file (or any firstwording....yml
file it found)
At some point you'll need a WordingManager
instance to configure your wording:
- Create an instance of
WordingManager
through public interface ofWordingManagerFactory
somewhere at the start of your app and callstart()
method - You'll also need to create a
WordingManagerProvider
instance in order to provide wording urls to work with