Localizing an app with String Catalog

Alexander Chekushkin
6 min readOct 16, 2023

--

Xcode 15.0 brings a new way of working with localizations in your app. In this article we’ll take a dive into it, learn how it looks like and how we can automate the process.

String Catalog is a tool that hosts translations for different regions/locales, configures pluralization messages.
String Catalog (.xcstrings) detects your main language(the one you select) and compares it against others.

The usage of .xcstrings provides you with some bells and whistles along the way:

  • Ability to check the localization progress — how much of other locales you’ve translated so far.
  • A straightforward way to see if you happen to miss some of the localizations.
  • A way to check if the project has some unused strings, to help you clean things up.

It also bring some pains:

  • It’s quite unreal to work with without any script pulling localizations automatically, instead of you manually creating entries every single time you team blesses you with a new killer-feature.

Let’s create a new String Catalogue file and inspect it in greater details:

  • New project: Your Project -> File -> New -> File -> String Catalogue
  • New project: Your Project -> ⌘ + N -> String Catalogue
  • Existing project: Your Project -> Localizable.strings -> Right Click -> Migrate to String Catalog…

Once created(or migrated), you’ll be presented with an empty document.

As you can see the file has already has some predefined columns:
Key: an ID for your text. It’s up to you how to name it, though, the more descriptive — the better.
English(en): a field for your localization. This is the text a user will see in theirs UI. If you’ve created the key, used it in UI but didn’t add the value — a user will be presented the key instead.
Comment: your comments/notes regarding translation. For example, “short version”, “widget usage only”, etc. A user will never see the comment for a localized string.
State: the current state of a string. It marks whether the string is being used, if it’s new or obsolete. The system manages this column for you automatically.

Now, when we understand how thing are, let’s create new entries!

Click “+” button and add first localizations:

Key: hello, value: Hello!
Key: goodbye, value: Goodbye!
Key: temperature_with_value, value: Temperature: %@

You can now use the text in you app for “en” language.

Here’s some options of using the texts:

// Modern way of initing a string
let hello = String(localized: "hello")

// Legacy way of initing a string
let goodbye = NSLocalizedString("goodbye", comment: "")

// Adding an argument to a localized string. %@ will be switched with "73º"
let temperatureLocalized = NSLocalizedString("temperature_with_value", comment: "")
let temperatureFormatted = String(format: temperatureLocalized, "73º")

Let’s add some additional languages: PL and DE. Open you “Localizable” file and click “+” at the bottom left corner.

Once the languages are added you’ll already have the keys, it’s only a matter of adding texts at this point

At this point you might’ve noticed: adding the texts manually is extremely tedious and time consuming task. Let’s fix it!

In order to automate the process we need to come-up with a structure of how we are to work with localizations. In my case, I’ll:

  • Have a structured document, which is easy to change and work with to multiple people(i.e.: localization department)
  • Have a Python script that will be run during the app build — to parse a document, create and change .xcstrings file.

Let’s start with a document:
– I need to create a document with clearly separated languages for every single key;
– The file must be easy to change, other people should be able to collaborate in it;
– I must be able to download the file in format I’d need. In my case it’ll be .csv

Here’s how such document would look like for me:

Inspect it for yourself: LINK

Let’s download the document as a .csv file and write a script to parse it!
Link -> File -> Download -> Comma Separated Values (.csv)

But how does String Catalog looks like under the hood?
Right-click on file -> Open as -> Source Code

So far, it’s just a plain JSON file, should be easy to write our own!

As for the script:

  • We need to detect what column(of our localization document) is responsible for key;
  • Assign the text per language per key;
  • Save the file in the right directory;

Here are the steps:

  • Open the project, create a new folder named “Scripts”
  • Add an empty file in “Scripts” -> ⌘ + N -> Empty -> Save it as “localizationScript.py”
    Add the following code to the localizationScript.py:
import os
import csv
import json

def parseLocalizationCSV(file_name):
try:
# Get the directory of the current script
current_script_path = os.path.abspath(__file__)
script_directory = os.path.dirname(current_script_path)

# Construct the full path to the CSV file
file_path = os.path.join(script_directory, file_name)

with open(file_path, newline='') as csvfile:
reader = csv.DictReader(csvfile)

output = {
"sourceLanguage": "en",
"version": "1.0",
"strings": {}
}

for row in reader:
key = row.pop("DEV_KEY")
entry = {
key: {
"localizations": {}
}
}

for column, value in row.items():
if value:
entry[key]["localizations"][column] = {
"stringUnit": {
"state": "translated",
"value": value
}
}

output["strings"].update(entry)

saveObjectToJSONFile(output, "Localizable.xcstrings")
except Exception as e:
print(f"Error: {str(e)}")

def saveObjectToJSONFile(data, file_name):
try:
# Get the absolute path of the current working directory
current_directory = os.path.abspath(os.getcwd())

# Define the folder name where you want to save the file
folder_name = "LocalizationsDemo/Localizations"

# Construct the full path to the output file
file_path = os.path.join(current_directory, folder_name, file_name)

# Ensure the folder exists before saving
os.makedirs(os.path.dirname(file_path), exist_ok=True)

with open(file_path, "w") as jsonfile:
json.dump(data, jsonfile, indent=4)

print(f"Object saved to: {file_path}")
except Exception as e:
print(f"Error saving object to file: {str(e)}")

# Apply you localization file name
parseLocalizationCSV('RawLocalizations.csv')
  • Download your localizations in .csv format. In our case, the script will be looking for a file named “RawLocalizations.csv”
  • Add it to “Scripts” file
  • Go to your Project directory, select the target -> Build Phases -> “+” -> New Run Script Phase
    Run script:
/usr/bin/python3 "${PROJECT_DIR}/Scripts/localizationScript.py"
  • Go to Project Directory -> Build Settings -> ENABLE_USER_SCRIPT_SANDBOXING = NO

Finally, Build the project and check your “Localizable” file:

At this point you should be able to step-up your localization practise so much closer to a perfection. Where to go from there? As far as I see it, my approach is still lacking the support of plurals, I challenge you to add the support for the feature and share your progress.

Resources:

Apple Documentation
Project Demo on GitHub

Hit me up on LinkedIn

--

--