Translating your iOS app with Localazy
A couple of people have asked me to translate No Meat Today in their language, but the last time I tried to tackle that, none of the solutions I found were satisfying or in my price range.
I just found Localazy and I'm sold. And not just because thereâs /lazy/ in the name. Itâs built by devs and from the CLI to the way it works, it felt like we understood each other and it was behaving âas it shouldâ.
So hereâs a little recount of my experience so far, hopefully thatâll help you get started.
If you canât wait to get started, you can register using this link and Iâll earn extra phrases (thanks đ).
What needs translation?
In order to get No Meat Today ready for a new country, I have to translate these file formats:
- strings: until SwiftUI arrived, I would split Localizable.strings into multiple files and use [table names](Apple Developer Documentation) but Iâve started to regroup files and use a simple prefix in the key instead
- stringsdict: this is used to handle plural
- plist: I use this for the daily messages, so itâs a list of sentences, packaged by theme (Default, Premium, Star Wars, XmasâŠ)
- some assets: I only have some screenshots for the paywall that are translated (en/fr), I can default to English
- txt: marketing texts & screenshots published on the App Store. This is downloaded into my app folder by fastlane.tools
Localazy supports the first all 4 file formats đ„ł (it wasn't the case when I wrote this article, but I've updated it and even wrote a follow-up article about Localazy+Fastlane combo).
Pricing
Fair warning: you will probably have to pay. But something that feels fair.
You get 200 phrases for free and can buy more. Adding 500 (so 700 total) will cost you a one-time fee of $50, and 1000 (1200 total) $90.
I found the project launch on Product Hunt and they advertised 1000 keys for free and a 75% discount. I contacted them about it and VĂĄclav was transparent about how this was too generous to build a viable business and that they had to decrease it. I wasnât really surprised to be honest, and the current pricing still seems pretty reasonable to me.
They also offer a starter pack subscription at $19/mo or $199/year, and it wouldnât be surprising if they decided to drop the one time fee at some point. Hopefully that will include more affordable tiers, but in any case, VĂĄclav promised me that any phrases bought now will be owned forever, which is enough for me.
You can get free phrases with a referral link (here is mine, again, I have no shame). Know that this referral bonus is added on a daily basis, and you only get it if the user integrates their app (upload texts).
I was told they would offer more ways how to earn free phrases.
If you're curious: I purchased a set of a 1000 phrases for $90.
Setup
The main pages youâll want to look at are
- Installation â Localazy
- Quick Start - iOS & macOS â Localazy
- File Format - iOS / macOS â Localazy
It can be worth having a look at these two as well since they describe the configuration of the two core commands of Localazy: upload
and download
.
Install the CLI
Installation is done via Homebrew.
xcode-select --install # Only if you haven't done so before
brew tap localazy/tools
brew install localazy
Create your configuration file
I followed the Quick Start - iOS & macOS â Localazy instructions at first, but this didnât work for me.
I have a Watch Extension, so for instance I have a Base.lproj in both the âNo Meat Todayâ subfolder and âNo Meat Today Watch App Extensionâ.
After playing with upload/download a bit, I ended up with this configuration.
{
"writeKey": "my-write-key",
"readKey": "my-read-key",
"upload": {
"files": [
{
"type": "ios-strings",
"pattern": "No Meat Today/Base.lproj/Localizable.strings",
"path": "No Meat Today"
},
{
"type": "ios-strings",
"pattern": "No Meat Today/fr.lproj/Localizable.strings",
"path": "No Meat Today",
"lang": "fr"
},
{
"type": "ios-stringsdict",
"pattern": "No Meat Today/Base.lproj/Localizable.stringsdict",
"path": "No Meat Today"
},
{
"type": "ios-stringsdict",
"pattern": "No Meat Today/fr.lproj/Localizable.stringsdict",
"path": "No Meat Today",
"lang": "fr"
},
{
"type": "ios-plist",
"pattern": "No Meat Today/Base.lproj/Silliness.plist",
"path": "No Meat Today"
},
{
"type": "ios-plist",
"pattern": "No Meat Today/fr.lproj/Silliness.plist",
"path": "No Meat Today",
"lang": "fr"
},
{
"type": "ios-strings",
"pattern": "No Meat Today Watch App Extension/en.lproj/Localizable.strings",
"path": "No Meat Today Watch App Extension"
},
{
"type": "ios-strings",
"pattern": "No Meat Today Watch App Extension/fr.lproj/Localizable.strings",
"path": "No Meat Today Watch App Extension",
"lang": "fr"
}
]
},
"download": {
"files": "${path}/${iosLprojFolder}/${file}"
}
}
As you can see, unlike the proposed configuration I use a path variable so that I can support the same Localizable.strings
file in two different subfolders, which means I can translate all extensions (Watch App, WidgetâŠ).
The appâs default language is English, and my app is localised in French, which means I already had translated texts. The above configuration allowed me to upload my translations in one go.
Does it mean Iâll have to add new rows for each new language? I donât think so.
If you edit a translation file and call localazy upload
it will push the edit translation for review, which is quite nice. While I will probably want to do that for French (since itâs my native language and I may spot typos or find better translations while coding), itâs unlikely that I will try to edit strings in a language that I donât speak. So for these other languages, I will only download and not upload, since I wonât do any changes to these files.
Lingo: before you read the rest
First, a reminder of what a localisation in a strings file looks like and some terms used thereafter.
/* This is a comment */
"a.key" = "This is a key";
- translation notes: the comment / first line
- key: the left hand side of the 2nd line, a uniq string to identify your text across all languages
- phrase: the right hand side of the 2nd line
- source language: the language used in your Base.lproj files
- source phrases: the phrases in your source/base language
Translating
This is what the UI looks like. I think itâs pretty straightforward. But more importantly, I invited a translator who is not a dev and they found it easy to use as well.
Some things to notice:
- everything that can help is there: the key, the source phrase, the translation notes, even the file path
- at the bottom you can see 2 types of suggestions: ShareTM is the database of translations from other apps/devs who decided to share their translations, and the other one is an automatic translation. This is really helpful to get a first quick & dirty translation of each phrase.
- I love bad puns
Good to know & other tips
Things below should help you get started, I figured some of it by trial and error, and the rest by talking with someone in the team (đ Vaclav!)
Your default language is kinda hidden
When you first upload your strings, you can be surprised that you donât find your base/source language. It took me a while to see it, but your source phrases are hidden behind the little sandwich menu next to the mention of your source language.
When I first uploaded my strings, I only found the French language and couldnât figure out where the English phrases were. I even tried to add en_US as a language before I realised the source language wasnât treated as the other languages.
I mentioned this to the team and apparently theyâll try to improve this.
Know that another way to access your source phrases is by heading to the âFile managementâ section.
You will lose control over the format of your files (but not the Base one đ, and itâs OK anyway)
Up until now, I would make sure my files were organised in the same way, having the same number of lines, spaces, comments. I would then open both language files simultaneously and modify them at the same time.
The identical number of lines especially made it easier to spot discrepancies, which meant untranslated keys.
So naturally, when I started searching for a service to handle translations, I imagined there might be one that would scan my files, keep the keys exactly where they were (append the missing ones) and only change the phrases/translations.
But, this is not how things work.
Instead, each time you download the translations, the language files are overwritten and keys are sorted alphabetically.
I can live with that, I guess. I mean, as I explained above, Iâm unlikely to modify phrases in languages other than English and French.
Deleting a key doesnât delete it in the base language
By default, base files are not overwritten, which means that if you delete a key, it will be deleted in all your language files except the base one, where youâll have to delete it manually.
This is a good thing, because it means that you can format your base file as you want.
In particular, you can make use of // MARK : - Section
to make it easier to navigate your Localizable.strings.
If you do use these, make sure you add a different comment (even an empty one such as /**/
) above the first key so that the MARK doesnât show up as translation notes.
Finally, if you do want to overwrite the files in your base language, you can for it by setting the includeSourceLang in your config file.
What if I change a translation in the source language?
This is something I was worried about and it works just as you would expect: when you edit a translation, all languages see a notice that the source phrase was changed, so that they can be changed if necessary.
If you do it from the web, you get a bit more flexibility because you can decide whether existing translations need to be updated or not.
But, I wouldnât do it from there, because, remember, the source phrases are not downloaded, so youâd have to change it manually in Xcode.
What if I change a translation in a language that is not the source language?
If you change a translation in Xcode, say in the fr.lproj/Localizable.strings, and then run localazy upload
, it will upload the translation for review but it wonât be taken into account until your review it
This means that if you run localazy download
before you review the change on the web, your change will be overwritten.
But what if you run localazy upload
again before you review it, will you lose your change? No, because there is a versioning system and youâll still be able to find your unreviewed change. đȘ
There is a Glossary
You can add terms that require extra context or attention in a Glossary.
For instance, I invented the term âCowlidayâ and added it to the Glossary to explain it and give some instructions that will show up each time the term is used.
When translating a phrase, the term is highlighted and hovering it will show the comments.
How can I tell how many keys I have?
The quickest place to find it is probably the Marketplace, where you can see your phrases limit.
Another place where you can find the info is in your source language (see "Your default language is kinda hidden" above), at the bottom right.
How can I tell how many words I have?
That one is less easily accessible but you can find the information by adding a Virtual Translator. Just pick any language, and you will find the number of words.
How can I quickly go from one phrase to the next?
When you begin translating, you'll probably want to go to the next untranslated sentence after you save a translation. There are two ways to make this easier that you'll probably want to communicate to your translators.
The first official one is what I'd call the "translation mode". When you go to your languages list, hover over the three dots and select "Start translating".
The trouble with this is that you can't just pick a key in the list and go from there. So here's my hackish way of doing it.
From your list of phrases, filter the Untranslated ones like I did here in the table header.
Then select the phrase that you want to translate, save it, and you will be right back to the same page, with the same filter, which means that the phrase you just translated will be gone and you can carry on to the next phrase.
Can I not download languages that I added into Localazy but not in Xcode yet?
Until the language is ready to be tested in your app, it's probably not worth downloading it every time with the other files.
For that, just set the "excludedLangs" variable in your "downloads" as such:
"download": {
"files": "${path}/${iosLprojFolder}/${file}",
"excludedLangs": ["de", "nl", "zh#Hans"]
}
How can I translate the App Store marketing content?
Edit: I wrote a follow-up article on this topic alone with a configuration supporting the fastlane txt files since this feature was introduced about 2 days after I wrote this article :D
If you're using fastlane.tools, you can download/upload your App Store metadata to txt files. While you can't simply upload them with Localazy, you can still create an AppStore.strings
file with no Target Membership, and add it to your configuration.
Mine has two keys for the text, but also keys for texts I use in the screenshots (not created by fastlane automatically):
/* Max 30 characters */
"AppStore.subtitle" = "Eat less meat: track, decide!";
/* Max 4000 characters */
"AppStore.description" = "# Exclusive to Apple #
(The rest of my 70 lines of descriptions)
";
"AppStore.screenshots.1.headline" = "Your companion to eat less meat";
"AppStore.screenshots.1.subheadline" = "Size your efforts by attracting cows to your meatless planet";
âŠ
Parting notes
- Localazy supports all the file formats I expected it to, and hopefully txt support will arrive some day to simplify App Store marketing localisation
- Itâs free for apps with less than 200 phrases, and affordable for small apps plus there are ways to get free phrases
- CLI is a charm
- Setup is pretty easy if you follow the configuration above
- The UI to translate is reactive and works well, and is easy to use for non-devs (well, I tested with a single person for now)
Before you leave đ
Consider doing or or more of these:
- Create a Localazy account using my referral link
- Follow me on twitter @sowenjub
- Download No Meat Today, a companion app for people who want to eat less meat, whatever you put behind "less" and "meat" (and ping me if you want to help translate it âșïž)