Supporting Multiple Languages

My native language is not English. I wanted the game to support multiple languages cleanly. Localization is easy to postpone in the early stages of development, but it becomes much harder if the codebase is built around hard-coded English strings.

This note summarizes the localization rules and tooling I am using in the project. The main goals are:


Use NSLocalizedString for User-Facing Text

The first rule is simple:

Every user-facing string should go through localization.

For example:

label.text = NSLocalizedString("New Game", comment: "")

This is much better than:

label.text = "New Game"

Even if the game initially ships in only one language, wrapping strings early avoids painful cleanup later.

Typical places that need localization:

A good rule is: if the player can see it, localize it.


Do Not Compose Sentences

One of the most important localization rules is:

Do not build sentences by joining smaller pieces.

For example, this is bad:

let text = NSLocalizedString("You found", comment: "") + " " + itemName

It may look harmless in English, but many languages do not use the same word order. Some languages also change grammar depending on gender, case, or plurality.

Instead, use a full localized format string:

let format = NSLocalizedString("You found %@.", comment: "")
let text = String(format: format, itemName)

Another bad example:

titleLabel.text = NSLocalizedString("Level", comment: "") + " \(depth)"

Prefer:

let format = NSLocalizedString("Level %@", comment: "")
titleLabel.text = String(format: format, "\(depth)")

This keeps the word order flexible for translation.


Formatting Strings Safely

When formatting localized strings, it is important to avoid assumptions that may cause crashes or restrict translators.

Prefer %@ Instead of %d or %f

In many cases I prefer using %@ instead of numeric format specifiers like %d or %f.

For example, instead of writing:

let format = NSLocalizedString("Level %d", comment: "")
let text = String(format: format, depth)

I usually write:

let format = NSLocalizedString("Level %@", comment: "")
let text = String(format: format, "\(depth)")

This avoids potential crashes if the type passed to String(format:) does not match the expected format specifier.

Using %@ makes the formatting more tolerant because it simply inserts the string representation of the value.

While %d and %f are technically correct, they are easier to misuse during refactoring or when code changes.

For safety and maintainability, %@ is often the safer choice.


Use Explicit Parameter Ordering

Another important technique is using explicit parameter ordering.

For example:

let format = NSLocalizedString("%1$@ defeated %2$@", comment: "")
let text = String(format: format, heroName, monsterName)

This allows translators to reorder parameters if their language requires a different sentence structure.

For example, a language might translate the sentence as:

%2$@ was defeated by %1$@

Without parameter indexing, translators would not be able to change the order safely.

Using:

%1$@
%2$@
%3$@

makes the localization system much more flexible and prevents awkward translations.

This is especially useful in games where sentences often include multiple dynamic elements such as:


Keep Strings Semantically Complete

Try to localize complete thoughts instead of fragments.

Bad:

NSLocalizedString("You are", comment: "")
NSLocalizedString("hungry", comment: "")

Good:

NSLocalizedString("You are hungry.", comment: "")

Fragments are difficult to translate correctly because translators cannot see the full context.


Separate Game Data from Display Text

Game logic should not rely on visible strings.

Bad:

enum ItemType: String {
    case potion = "Potion"
}

Better:

enum ItemType: String {
    case potion

    var localizedName: String {
        return NSLocalizedString("Potion", comment: "Item name")
    }
}

Internal identifiers should remain stable regardless of translation changes.


Use a Python Pipeline to Automate Localization

Manual localization becomes difficult as the project grows. A Python pipeline can automate much of the process.

Typical workflow:

Swift Source Files
        ↓
Python Extractor
        ↓
Master String List
        ↓
Translation
        ↓
Updated .strings Files

The pipeline can:

Automation keeps the localization process consistent and repeatable.


Conclusion

Adding localization support early makes the game easier to maintain and expand.

For Rocco Rogue, the key rules are:

Localization is not just a translation step at the end of development. It influences UI design, code structure, and build tooling.

Building localization support early creates a strong foundation for supporting multiple languages in the future.


⬅️ Previous | 🏠 Index | Next ➡️