Sitemap

All You Need to Know for iOS App Localization

Tips, Tricks, and Best Practices

--

Press enter or click to view image in full size

Hi, my name is Siarhei, and I’m a Software Engineer with over 10 years of experience in iOS app development. During my career, I have worked with many teams to create multilingual apps for different audiences and industries. This experience has taught me a lot about the tools and services used for localization and the common challenges developers face. Over the years, localization has become much easier and cheaper, making it more accessible than ever.

However, even when using professional translators or AI tools, developers play an important role in the localization process. We need to provide clear and well-structured text for translation and ensure that all content is displayed correctly in the app. I’ve seen skilled developers make mistakes because they didn’t know the differences between languages. These mistakes often go unnoticed because everything looks correct in their own language.

In this article, I’ll share simple tips and best practices to help developers create apps that feel natural for users, no matter their language. This guide will be helpful for both new and experienced developers, not just for iOS but for all Apple platforms. Let’s explore how we can make localization better and easier for everyone.

String vs. LocalizedStringKey

One of the first things to understand is how strings are handled. Let’s start with two examples:

let string = "Hello World!"
Text(string)
Text("Hello World!")

At first glance, these examples might seem identical, but they behave differently because of the types of data passed to Text. In the first example, the constant string is implicitly inferred to be of type String because it’s initialized with a string literal. When passed to Text, the String object is used directly, and the text “Hello World!” is displayed as plain text.

In the second example, the string literal “Hello World!” is directly passed to Text. Here, the literal is treated as an instance of LocalizedStringKey, because Text by default expects an object of type LocalizedStringKey. In this case, “Hello World!” is not treated as a value but as a key. The result is determined as follows:

  • If the key “Hello World!” exists in the default localization table, the corresponding value will be displayed.
  • If no entry is found for the key, Xcode may automatically add this string to the default String Catalog file (if this option is enabled in Build Settings). In this case, the key itself will be displayed.

If you do not want to use the default localization table, you can specify a custom table with the tableName parameter. You can also define the source of localization files by using the bundle parameter.

Text("Hello World!", tableName: "CustomLocalizations", bundle: .main)

If you want to make sure that your string is not added to the String Catalog file and is displayed as-is, you can use

Text(verbatim: "Hello World!")

or explicitly pass a String object.

The same logic with LocalizedStringKey applies to other UI elements in SwiftUI. Always be careful about the type of object that a view expects to display.

If your strings are initialized outside of views, such as in business logic, or if you work with UIKit or AppKit, you can still use translations and automatically add strings to String Catalog files. To do this, use the following approach:

let string = String(localized: "Hello World!")

If you want to use a non-default localization file, you can specify it like this:

let string = String(
localized: "Hello World!",
table: "CustomLocalizations",
bundle: .main)

Understanding this behavior is critical for ensuring that strings are localized correctly. It also highlights the importance of carefully managing your localization files to avoid unexpected results.

Providing Context

Providing translators with context is essential for ensuring accurate translations. Whether your app is translated by a human or AI, a lack of context can lead to incorrect results. This is especially important for short strings, where it might be impossible to determine the part of speech or the intended meaning without additional information. To avoid such issues, you can add an optional comment to explain the string’s purpose:

Text("Hello World!", comment: "Comment for translator")
let string = String(
localized: "Hello World!",
comment: "Comment for translator")

The comment you provide will automatically be included with the string in the String Catalog file.

Using a string as both the key and value for the main language is convenient, but it can sometimes lead to problems in other languages. For example, imagine a screen with a title “Archive” and a button labeled “Archive” that moves items into the archive. In code, this might look like:

Text("Archive")

Button("Archive")

Both the button and the title would reference the same string in the localization table. While this might work in some languages, in many others, the noun and verb forms of “Archive” would require different translations.

If you notice that identical strings have different meanings depending on their context, it’s better to create separate keys for them. This can be done as follows:

let archiveTitle = String(
localized: "archive.label",
defaultValue: "Archive",
comment: "Name of the Archive folder in the sidebar")
// EN: Archive
// ES: Archivado
// RU: Архив

let archiveAction = String(
localized: "archive.menuItem",
defaultValue: "Archive",
comment: "Menu item title for moving the email into the Archive folder")
// EN: Archive
// ES: Archivar
// RU: Архивировать

By creating unique keys for strings with different contexts, you ensure that translations are accurate and meaningful for all languages.

Pluralization

Localizing strings with numerical variables is a common task, but it can also be a source of issues if not done correctly. Often, you might see code like this:

Text(count == 1 ? "\(count) apple" : "\(count) apples")

While this may work for English, it poses significant challenges for other languages. Words that depend on numbers often change based on complex grammatical rules. For example, in Russian:

  • 1 яблоко
  • 2 яблока
  • 5 яблок
  • 20 яблок
  • 21 яблоко

These variations follow specific pluralization rules, which can differ greatly across languages. The complete set of these rules is available on the CLDR Unicode website.

To handle pluralization correctly, you can start by writing:

Text("\(count) apples")

Then, in your String Catalog file, right-click on the string and select “Vary by Plural” from the context menu. After enabling pluralization, update the fields for your main language. For example, in English:

  • In the “One” field, enter %lld apple.
  • In the “Other” field, enter %lld apples.
Press enter or click to view image in full size

For each supported language, additional fields will appear, allowing you to provide translations that follow the CLDR rules.

Another way to handle pluralized strings is by using automatic inflection:

Text("^[\(count) apple](inflect: true)")

The word will automatically change based on the value of “count”. This works for both countable and uncountable nouns:

let count = 3
Text("^[\(count) person](inflect: true)") // 3 people
Text("^[\(count) company](inflect: true)") // 3 companies
Text("^[\(count) deer](inflect: true)") // 3 deer

While this method works well, it has a significant limitation: it does not support all languages. You can check if a language is supported using the following code:

let canInflect = InflectionRule.canInflect(language: "<language_code>")
// or
let canInflect = InflectionRule.canInflectPreferredLocalization

Currently supported languages are:

  • iOS 16: English, Italian, Spanish, Portuguese, French
  • iOS 17: English, Italian, Spanish, Portuguese, French, German
  • iOS 18: English, Italian, Spanish, Portuguese, French, German, Korean, Hindi

On iOS 15, the canInflect function returns true for English, Italian, Spanish, Russian, and Swedish, but in reality, only English and Spanish are supported.

Get Siarhei Razanau’s stories in your inbox

Join Medium for free to get updates from this writer.

Due to the limited support for this feature, I would still recommend using the manual approach with String Catalog to ensure broader language compatibility.

Formatting Data

It’s common to see code like this:

Text("\(price)$")

While this might work in specific cases, it fails to account for how data is displayed in different regions. Even countries that share the same language can use different formats for various types of data. Thankfully, Apple has already implemented logic to handle these variations.

The formatted function is a powerful tool that adapts values based on the current locale. It supports translations for unit names, including plural forms, and can be used for a wide range of data. For example, prices, percentages, and dates can be formatted automatically as follows:

let currency = 1390.5.formatted(.currency(code: "USD"))
// en_US: $1,390.50
// es_ES: 1390,50 US$
// ru_RU: 1 390,50 $

let percent = 25.formatted(.percent)
// en_US: 25%
// es_ES: 25 %
// tr_TR: %25

let date = Date().formatted(.dateTime)
// en_US: 1/25/2025, 8:10 PM
// es_ES: 25/1/2025, 20:10
// ru_RU: 25.01.2025, 20:10

Another example is the formatting of lists, which can vary depending on the language. Apple’s tools make it easy to handle these differences:

let list = ["iPhone", "iPad", "Apple Watch"].formatted(.list(type: .and))
// en_US: iPhone, iPad, or Apple Watch
// es_ES: iPhone, iPad y Apple Watch
// ru_RU: iPhone, iPad и Apple Watch

Apple also provides a way to display most physical units of measurement in the correct format for any region using the Measurement structure. For example, to display temperature:

let measurement = Measurement(
value: 24,
unit: UnitTemperature.celsius)
let temperature = measurement.formatted(.measurement())
// en_US: 75.2°F
// es_ES: 24°C
// ru_RU: 24 °C

The unit parameter supports various types of measurements. Here’s the full list:

  • UnitAcceleration: Unit of measure for acceleration.
  • UnitAngle: Unit of measure for planar angle and rotation.
  • UnitArea: Unit of measure for area.
  • UnitConcentrationMass: Unit of measure for concentration of mass.
  • UnitDispersion: Unit of measure for dispersion.
  • UnitDuration: Unit of measure for duration of time.
  • UnitElectricCharge: Unit of measure for electric charge.
  • UnitElectricCurrent: Unit of measure for electric current.
  • UnitElectricPotentialDifference: Unit of measure for electric potential difference.
  • UnitElectricResistance: Unit of measure for electric resistance.
  • UnitEnergy: Unit of measure for energy.
  • UnitFrequency: Unit of measure for frequency.
  • UnitFuelEfficiency: Unit of measure for fuel efficiency.
  • UnitIlluminance: Unit of measure for illuminance.
  • UnitInformationStorage: Unit of measure for quantities of information.
  • UnitLength: Unit of measure for length.
  • UnitMass: Unit of measure for mass.
  • UnitPower: Unit of measure for power.
  • UnitPressure: Unit of measure for pressure.
  • UnitSpeed: Unit of measure for speed.
  • UnitTemperature: Unit of measure for temperature.
  • UnitVolume: Unit of measure for volume.

The formatted function is versatile and can be applied to various types of data, such as numbers, dates, times, intervals, and even people’s names. I recommend exploring its parameters and capabilities. It not only simplifies localization but also saves time by automating data formatting and conversion logic.

Localizing User Interface

Localization goes beyond adapting text — it’s about ensuring that your app feels natural and intuitive for users in different regions. People may interact with your app differently depending on their language and cultural norms.

For example, in languages like Arabic or Hebrew, the interface is read from right to left (RTL). Fortunately, standard interface components in Apple’s frameworks already support RTL layouts. To ensure your app handles this seamlessly, use leading and trailing constants instead of left and right when designing your interface. Only use left and right constants for components that should remain in fixed positions regardless of the language.

Another important factor to consider is font compatibility. While custom fonts can be used, I recommend the standard San Francisco font family because it is highly optimized for various languages, provides excellent readability, and ensures proper support for a wide range of characters and symbols. Custom fonts, on the other hand, may not support certain symbols or scripts required for some languages. If your design requires a custom font, ensure that all texts are thoroughly tested across all supported languages to avoid display issues.

Images can also be localized through the asset catalog. While it’s best to avoid placing text directly on images, there are cases where you may need to show different images or icons for specific regions. Apple makes this easy with localized asset variants.

For icons, I highly recommend using SF Symbols. These symbols automatically adapt to different languages and provide consistent visuals across your app.

By considering these aspects of localization, you can create an app that not only displays the right text but also feels tailored to users from any part of the world.

Styling Text Without Breaking Localization

When styling text, it’s essential to keep the entire string intact. A common mistake is splitting a sentence into multiple AttributedString segments and then concatenating them:

var hello = AttributedString(localized: "Hello")
hello.font = .system(size: 12)
hello.foregroundColor = .black

var world = AttributedString(localized: " World")
world.font = .system(size: 17)
world.foregroundColor = .white

let result = hello + world

At first glance, this approach may seem reasonable, but in reality, it creates two separate strings in the String Catalog. This can lead to translation issues because word forms depend on each other in many languages. The number and order of words may also change depending on the language. Additionally, in some cases, you may need to style individual letters rather than entire words, making proper localization even more challenging.

Instead of splitting strings, you can use built-in Markdown formatting or external libraries like swift-markdown-ui or BonMot. These tools allow you to apply different styles to parts of a string while keeping it as a single translatable unit.

If you prefer not to add an extra dependency or need more fine-grained control over text formatting, you can use custom attributes in AttributedString. While this requires additional code, it allows you to apply styles dynamically without breaking localization.

For example, a localized string might look like this:

Press enter or click to view image in full size
^[Fast](rainbow: ‘fun’) & ^[Delicious](rainbow: ‘extreme’) Food

How to implement custom attributes is beyond the scope of this article, but you can check out Apple’s example, which also demonstrates formatting options for other types of data.

Conclusion

I currently live in a country where I don’t speak the national language. Seeing even partial localization in local apps and websites makes a big difference for me. Even machine translation helps a lot because it allows me to guess what will happen when I click a button and feel more confident that I won’t accidentally do something wrong.

I highly recommend translating your apps into as many languages as possible. It can be a competitive advantage, helping you reach more users and stand out in the market. I understand that hiring professional translators can be expensive, but for secondary languages, using AI is a cost-effective solution. For just a few dollars, you can add support for a new language.

The key is to provide enough context for translations, which greatly improves its quality. You can do this manually or use existing services designed for localization. I’ve tried many of these services and eventually built my own macOS app that translates strings using context from comments. If you find it helpful, I’d be very glad. You can check it out here.

I’d also love to hear your thoughts in the comments. It would be great if you shared insights about the unique aspects of your native language and any additional localization tips you might have.

Thank you for reading to the end!

--

--