First impressions: Translating my iOS app with Localazy

A couple of people have asked me to translate my iOS app into 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 source keys (thanks 😁).

🎌 What needs translation? 🔗

In order to get my iOS app 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 first wrote this article, but I've updated it and even wrote a follow-up article about Localazy + Fastlane combo).

Note from Localazy: txt format support is available thanks to Arnaud, who requested this feature❤️

💵 Pricing 🔗

Fair warning: you will probably have to pay. But something that feels fair.

You get 200 source keys for free and can buy more. Adding 500 (so 700 total) will cost you a one-time fee of $160 or $4 / month, and 1000 (1200 total) $240 or $6 / month.

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.

Note from Localazy: See the current pricing here.

They also offered 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.

Note from Localazy: We have since updated the pricing and introduced three paid tiers replacing the old model. Read the release article here.

You can get free keys 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 prior to the pricing changes.

⚙️ Setup 🔗

The main pages you’ll want to look at are

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.stringsfile 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 localized 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 localization 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 unique 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 realized 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 organized 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 source keys 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 clicking on Order Translations. 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 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 😄.

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
  • 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 one 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 ☺️)

This article was originally published on 🏝 Living in Paraside, a blog by Arnaud Joubay.

💖 Why developers love Localazy?

Whether you are a single developer or an agency, you can rely on Localazy when it comes to i18n & l10n!

See Testimonials