Skip to Content
Course content

211: Testing Localized Apps with Different Locales

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I’ve seen this play out a dozen times with junior devs: they spend the first few hours of a localization sprint in a state of total frustration. They’ll write a piece of code to format a price or a date, then they’ll manually open the iOS Simulator, navigate to Settings > General > Language > Add Language, move French to the top, and then jump back to the app to see if the comma is in the right place. Then they do it all again for Japanese. Then German. It's a tedious, soul-crushing loop that kills your flow.

The Friction of System-Wide Changes

The "naive" way to test localization is to treat the simulator like a physical device in your hand. While this feels "realistic," it's fundamentally the wrong approach for development. When you change the system language, you're not just testing your app; you're changing the environment for every single process on that virtual device. It's slow, it's prone to human error, and most importantly, it's impossible to automate. You can't write a XCTest that tells the Simulator's system settings to change language mid-run.

If you're relying on the System Settings menu, you're essentially guessing. You might see that a price looks correct in French, but did you check if the currency symbol is placed before or after the amount? Did you check if the decimal separator changed? By the time you've manually toggled through five locales, you've likely forgotten the specific edge case you were looking for in the first one.

Leveraging Scheme Overrides for Fast Iteration

The better way—and the way I want you to start using immediately—is via Xcode Scheme overrides. You don't need to touch the Simulator settings at all. If you click on your target at the top of the Xcode window and select "Edit Scheme," you can head over to the "Run" section and find the "Options" tab. There, you'll see "App Language" and "App Region."

By changing these dropdowns, you're telling Xcode to launch your app with a specific localization environment, regardless of what the simulator is set to. It's a massive quality-of-life improvement. I usually keep a few different schemes configured for my most "problematic" locales—like Arabic for Right-to-Left layout testing or German for those notoriously long compound words that break my UI buttons. It takes three seconds to switch, and it keeps your simulator's global state clean.

Why Locale.current is a Testing Nightmare

Now, schemes are great for manual "smoke testing," but they don't help you with unit tests. This is where many engineers hit a wall because they've hard-coded Locale.current inside their logic. If your price formatter looks like this: let formatter = NumberFormatter(); formatter.locale = Locale.current, your test is now a hostage to whatever the machine running the test happens to be set to.

The professional approach is to inject the locale. I always suggest treating Locale as a dependency. Instead of letting your formatter reach out into the global environment, pass the locale in through the initializer. This allows you to write a test that explicitly creates a Locale(identifier: "fr_FR"), passes it to your formatter, and asserts that the output is "1 234,56 €" instead of "$1,234.56".

It feels like a bit more boilerplate upfront, but the trade-off is a test suite that is deterministic. Your CI server in a data center in Virginia should produce the exact same test results as your MacBook in a coffee shop. That's the only way to truly sleep soundly when shipping a global product.




📋 Practical Task

Exercise: Implementing a Deterministic Currency Validator

You have a PricePresenter class that is currently using Locale.current, making it impossible to test different currencies reliably. Your task is to refactor this class to support locale injection and write two unit tests to verify the formatting.

Requirements:

  • Modify the PricePresenter class to accept a Locale object in its initializer (defaulting to .current for production use).
  • Implement a method formatPrice(amount: Double) -> String that uses a NumberFormatter configured with the injected locale and the style .currency.
  • Write a test case that verifies an amount of 1234.56 returns "1 234,56 €" (or the equivalent French formatting) when initialized with Locale(identifier: "fr_FR").
  • Write a second test case that verifies the same amount returns "$1,234.56" when initialized with Locale(identifier: "en_US").
// Starting point for your refactor:
class PricePresenter {
    func formatPrice(amount: Double) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = Locale.current // This is the line you need to change!
        return formatter.string(from: NSNumber(value: amount)) ?? ""
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.