Understanding Localization in iOS

Localization is the process of adapting your app to different languages and regional conventions. In iOS development, this involves providing localized strings, images, and other resources so that the app feels native to users in every supported locale. The foundation of iOS localization rests on Localizable.strings files and the NSLocalizedString macro, but modern tools like string catalogs (introduced in Xcode 15) and .stringsdict files for plurals have significantly simplified the workflow.

When you combine the native iOS localization infrastructure with a headless content management system like Directus, you gain the ability to manage translations dynamically—deploying updates without requiring a new App Store release. This approach is especially valuable for apps that frequently update content or support a growing number of languages.

Setting Up Your Project for Multiple Languages

To enable multiple language support, follow these steps:

  1. Open your Xcode project and select the target you want to localize.
  2. Go to the Info tab under the target settings.
  3. Click the Localizations section and add the languages you intend to support (e.g., French, German, Japanese).
  4. Xcode automatically creates Localizable.strings files and optionally .xib localization variants for storyboards if they are present.

After adding localizations, you will see language‑specific folders (e.g., en.lproj, fr.lproj) inside your project. These folders contain the translated resource files and ensure that the system loads the correct version based on the user’s device language.

Understanding Localization Files Today

While the classic Localizable.strings file remains popular, Xcode 15 introduced string catalogs (.xcstrings) which replace multiple .strings files with a single, more maintainable format. String catalogs support comments, state tracking (new, stale, reviewed), and vary by language directly in the Xcode editor. For example:

// Greeting string with state "new"
"Hello" = "Hello";

For pluralization, you still need .stringsdict files. These let you define rules for zero, one, two, few, many, and other categories that differ across languages.

Managing Translations with Directus

Directus provides a robust headless CMS that can act as a single source of truth for all your translated texts. Instead of hard‑coding every string in Localizable.strings, you can store translatable content in Directus collections and fetch them via REST or GraphQL at runtime. This enables dynamic updates, collaborative translation workflows, and easy fallback strategies.

Structuring Your Data Model in Directus

Within Directus, create a collection called, for example, app_translations. Each item should contain:

  • key (string): The translation identifier, e.g., home.greeting.
  • locale (string): The language code, e.g., en-US, de, ja.
  • value (string): The actual translated text.
  • context (string, optional): A description for translators.

You can also create a separate collection for supported locales with metadata (display name, directionality flag). Using Directus’s built‑in versioning and role‑based access, you can safely collaborate with professional translators or community contributors.

Fetching Translations via API

In your iOS app, use the Directus SDK or plain URLSession to fetch translations on launch. A typical request might look like:

GET /items/app_translations?filter[locale][_eq]=fr

Cache the response in memory or persist it locally using Core Data or a JSON file. For maximum performance, consider requesting only the translations for the user’s current locale, but fallback to the default language if a key is missing.

Implementing a Translation Cache and Fallbacks

To avoid blocking the UI, use an asynchronous loading pattern. Store retrieved translations in a dictionary ([String: String]) and provide a helper method similar to NSLocalizedString:

func localizedString(for key: String) -> String {
    return translations[key] ?? NSLocalizedString(key, comment: "")
}

This ensures that even if the network request fails, the user sees something meaningful (the base localized string from the bundle). For production apps, implement a retry mechanism and background refresh so translations remain up‑to‑date.

Implementing Localization in Code

When using static strings from your app bundle, the NSLocalizedString macro (or the newer String(localized:) initializer in SwiftUI) is still the primary API. For example:

let greeting = NSLocalizedString("Hello", comment: "Greeting message")

In SwiftUI, you can use the Text view directly with a localized string key:

Text("hello_welcome") // automatically localizes if defined in .strings or .xcstrings

If you integrate Directus, you can replace these static keys with a custom manager that resolves keys from the fetched dictionary first, then falls back to the bundle’s NSLocalizedString. This hybrid approach gives you the best of both worlds: dynamic updates from the CMS with static fallback for critical UI elements.

Handling Plurals with .stringsdict

Plural rules vary widely across languages. English uses one form for singular and another for plural (1 item vs. 5 items), but many languages have more complex rules. iOS supports these via .stringsdict files. Create a file named Localizable.stringsdict in each language folder with this structure:

<plist version="1.0">
<dict>
  <key>%d item(s)</key>
  <dict>
    <key>NSStringLocalizedFormatKey</key>
    <string>%#@items@</string>
    <key>items</key>
    <dict>
      <key>NSStringFormatSpecTypeKey</key>
      <string>NSStringPluralRuleType</string>
      <key>NSStringFormatValueTypeKey</key>
      <string>d</string>
      <key>one</key>
      <string>%d item</string>
      <key>other</key>
      <string>%d items</string>
    </dict>
  </dict>
</dict>
</plist>

When using String(format: NSLocalizedString("%d item(s)", comment: ""), count), iOS automatically selects the correct plural form. If you are managing these translations via Directus, you can store the entire .stringsdict content as a JSON field and build the plist dynamically in the app—or simply keep the plural definitions in the bundle and only fetch simple strings from the CMS.

Right‑to‑Left (RTL) and Language Direction

Languages such as Arabic and Hebrew require the entire UI to mirror. iOS supports RTL layout automatically when you use Auto Layout constraints that are language‑aware (e.g., leading/trailing instead of left/right). In your code, avoid hard‑coding left and right alignments; use semantic attributes like .layoutDirectionLeftToRight or .layoutDirectionRightToLeft and respect the user interface layout direction.

When fetching localized content from Directus, include a field for direction in your locales collection. You can then read it on app startup and adjust the UIApplication.shared.userInterfaceLayoutDirection or the SwiftUI environment value layoutDirection. Pay special attention to images with text, quoted elements, and navigation flows—they all must be mirrored or adapted for RTL users.

Testing Your Localization

Testing is critical. Use Xcode’s scheme editor to set a launch argument for a specific language:

-AppleLanguages (de)

Or change the language in the simulator’s Settings app. Additionally, Xcode previews now support localization previews in SwiftUI:

ContentView()
    .environment(\.locale, .init(identifier: "ja"))

For Directus‑powered translations, write unit tests that verify the fallback chain and check that every key expected from the bundle also exists in the fetched dictionary. Use UI tests with different language settings to catch layout breaks or missing translations.

Automated Screenshot Generation

Tools like Fastlane (using snapshot) can generate screenshots in every supported language, which helps you visually inspect the app and also populate App Store Connect with localized screenshots. Ensure your test accounts have cached translations from Directus to mirror real‑world scenarios.

Best Practices for Multi‑language Support

  • Always provide a default base language. Typically English (US) should be complete in both the app bundle and Directus. Use that as the fallback when a key is missing in a target language.
  • Use professional or verified translations. Even with Directus’s collaborative workflow, rely on native speakers or translation services for accuracy.
  • Consider dynamic text sizes and accessibility. Localized strings can be longer or shorter than the English originals. Use Auto Layout constraints that adapt and test with accessibility larger text sizes (Dynamic Type) in every language.
  • Keep strings contextual. When requesting translations in Directus, add a comment field so translators understand the context (button, title, alert message). Use placeholder comments in NSLocalizedString as well.
  • Version your translations. In Directus, use the built‑in revision history to avoid losing changes. When you release a new app version, you can publish a new translation snapshot rather than updating live endpoints.
  • Monitor for over‑fetching. Only download translations for the current locale unless you need offline support for multiple languages. Cache them for the session and refresh periodically in the background.
  • Respect cultural differences. Beyond language, consider date formats, currency symbols, units of measurement, and color symbolism. Use NSDateFormatter and NSNumberFormatter with the user’s locale.

Putting It All Together: A Hybrid Approach

Many successful apps combine static bundle localizations for the main user interface (navigation, alerts, fixed labels) with a headless CMS like Directus for dynamic content (blog posts, product descriptions, help articles). This hybrid model gives you the performance and reliability of built‑in iOS localization for critical UI, while leveraging the flexibility of Directus for frequently updated content.

To implement this, create a service class that wraps both sources. On app launch, load the bundle strings first (they are instant), then asynchronously fetch the Directus translations. When both are ready, merge them, with Directus values taking precedence for keys that exist in both. This way, you can override a string remotely if necessary (e.g., for a special promotion) without altering the bundle.

Conclusion

Implementing multi‑language support in iOS applications is no longer just about adding .strings files. With the power of modern Xcode features like string catalogs, .stringsdict for plurals, and a headless CMS such as Directus for dynamic translations, you can build a scalable, maintainable internationalization system. By following the best practices outlined here—testing across languages, respecting RTL layout, and using a hybrid bundle‑plus‑CMS approach—you can deliver a seamless experience to users around the world.

For further reading, consult the Apple Localization Documentation and the Directus Localization Guide. If you need a tool to manage plural rules, POEditor offers a collaborative translation platform that integrates well with both iOS strings and Directus workflows.