tva
← Insights

Banderas en Widgets: Por Qué los Códigos ISO de País Superan a los Nombres Dependientes del Locale

The bug was subtle. An iOS widget displayed the correct country name in English but showed the wrong flag emoji in German. In French locale, a different country showed the wrong flag. The flags were not random — they were consistently wrong in a pattern that took a moment to recognise: the lookup was returning the flag for whatever country happened to have a name that, in the current locale, resembled the stored display name.

The root cause was a country lookup that used localised country names as dictionary keys. Names that are stable strings in one locale become different strings in another. Fixing it required rebuilding the lookup around ISO 3166-1 alpha-2 codes throughout, and the lesson extends well beyond flag emojis.


How the Bug Appeared

The widget displayed a list of countries with their corresponding flag emojis alongside other data. The original implementation stored country display names as the identifier — the string “Germany” or “France” — and used a dictionary keyed on those names to look up the corresponding flag emoji.

In English locale, this worked correctly. The stored name was “Germany”, the dictionary key was “Germany”, and the lookup returned the correct flag. But when the device locale was set to German, NSLocale.current.localizedString(forRegionCode: "DE") returns “Deutschland”. The widget was calling this method to resolve display names dynamically. So the displayed name became “Deutschland” — correct, in context — but the lookup against the English-keyed dictionary failed, fell through to a default, and displayed the wrong flag.

The more insidious variant was partial matches. In some locales, the localised name for one country happens to share a prefix or near-match with a different country’s English name. Without strict equality checks, approximate lookups returned plausible-looking but incorrect results. A QA process running only in English locale would never catch this.

Why Locale-Dependent Names Are a Trap

The fundamental problem is using a locale-dependent string as an identifier. Identifiers need to be stable — the same value regardless of who reads them, when, or in what language context. A country’s name in the current locale is a presentation value. It is correct and appropriate to display to a user. It is incorrect and fragile to use as a lookup key, a database identifier, a cache key, or any other form of stable reference.

This is not limited to country names. Currency names, language names, locale display names, and time zone descriptions all share the same property: they are locale-dependent strings that the system generates for display, not for identity. NSLocale.current.localizedString(forCurrencyCode: "EUR") returns “Euro” in English and “Euro” in German, which happens to coincide — but localizedString(forCurrencyCode: "GBP") returns “British Pound” in English and “Britisches Pfund” in German. Using either as a key produces the same category of bug.

The rule is simple: use the ISO code as the identifier everywhere in logic, and call the localisation method only when constructing a string for display to the user. Never store the localised string. Never use it as a key. Never compare it programmatically across locales.

Generating Flag Emojis from ISO Codes

Flag emojis are encoded in Unicode as pairs of Regional Indicator Symbols. The character sequence for a given country flag corresponds to the country’s ISO 3166-1 alpha-2 code, with each letter replaced by its Regional Indicator Symbol equivalent. The Regional Indicator Symbols run from U+1F1E6 (for A) through U+1F1FF (for Z).

In Swift, generating a flag emoji from an ISO code is a short utility function:

func flagEmoji(for isoCode: String) -> String {
    let base: UInt32 = 0x1F1E6 - 65 // offset from A
    return isoCode.uppercased().unicodeScalars.compactMap {
        UnicodeScalar(base + $0.value)
    }.map { String($0) }.joined()
}

This function takes “DE” and returns 🇩🇪. It takes “SG” and returns 🇸🇬. It requires no dictionary, no mapping file, and no maintenance. The mapping is defined by the Unicode standard and tracks ISO 3166-1 alpha-2 codes directly.

The only edge case worth handling is the fact that not all two-letter codes have corresponding flag emojis. UN, EU, and some other regional codes produce flag emojis that not all platforms render consistently. For a widget that deals with nation-state data, restricting input to valid UN member state codes and testing on the target platform is sufficient.

The 150+ Country Mapping We Still Needed

Generating flag emojis from ISO codes algorithmically handles the display side. But we also needed to store and query countries in a structured way — the widget filtered countries by region, sorted them by various criteria, and associated each with additional metadata. This required a proper mapping.

The mapping we built pairs ISO 3166-1 alpha-2 codes with a canonical English name (for internal use and logging), a numeric ISO 3166-1 code (for systems that require numeric identifiers), and a region grouping. The English name in this mapping is never used as a lookup key — it exists only for debugging and administrative display in non-localised contexts. All user-facing names are generated at render time via NSLocale.current.localizedString(forRegionCode:).

This separation is the key architectural decision. The data layer speaks ISO codes. The presentation layer translates to the current locale. They never meet in the middle. A flag lookup, a region grouping, a sort order — all of these operate on ISO codes. Only the final text rendered to the user goes through localisation.

Extending the Fix to Other Data

Once the flag bug was fixed, we audited the rest of the codebase for the same pattern. The same issue appeared in three other places: a currency display module that used localised currency names as cache keys, a language selector that stored localised language names in user preferences, and a date formatting module that compared locale-dependent month names.

The fix in each case followed the same pattern. Replace the locale-dependent string with its stable ISO or IETF equivalent: currency codes (ISO 4217) instead of currency names, BCP 47 language tags instead of localised language names, numeric month values instead of month name strings. The localised strings become view-layer concerns only.

The audit was worth doing systematically. Each of these bugs would have been invisible in English-locale testing and would have surfaced only in production, on devices configured for other languages. Given that the app targeted a multilingual audience, the impact would have been significant.

The Scope of the Problem in Production

One reason this class of bug persists is that it is invisible in single-locale development environments. A developer working in English locale, building an app for an international audience, can write and test the entire country display feature without ever triggering the bug. The bug only manifests when the device locale does not match the locale in which the dictionary or mapping was originally built — which is precisely the condition that describes the majority of the target users.

In a widget specifically, the gap is wider than in a main app because widgets execute in a constrained extension context. The widget extension runs with the current system locale, which may differ from the locale the developer tested against. There is no in-process debugging for widget renders — you cannot attach a debugger to a widget extension and inspect state at runtime in the same way you can with a main app process. Identifying locale-dependent rendering bugs in widgets requires Xcode’s timeline debugger and careful attention to the extension’s actual rendering context, which most developers encounter only after a user reports the issue in production.

The practical response is to treat all locale-dependent display logic as a category of risk, not just an individual bug to fix. Code review should include a check for locale-dependent strings used as identifiers. The review question is simple: is this string going through any localisation method before it is used as a key or comparison value? If yes, the design needs to change. ISO codes, numeric identifiers, and other locale-independent references should replace localised strings at every layer except the final display.

Testing Across Locales

The most direct lesson from this bug is that locale testing needs to be part of the standard test matrix, not an afterthought. iOS makes this relatively straightforward: the simulator supports switching locale without changing the system language, and UI tests can be parameterised by locale.

For flag and country display specifically, we added snapshot tests that run against a fixed set of device locales — English, German, French, Japanese, Arabic — and verify that the flag emoji for a fixed set of ISO codes remains correct across all of them. These tests catch regressions immediately if someone introduces a locale-dependent lookup in the display path.

The broader principle is that any feature that touches localisation, internationalisation, or region-dependent display should have automated tests that exercise multiple locales. Manual QA in a single locale is not a substitute. The category of bugs caused by locale-dependent assumptions is both common and consistently invisible to single-locale testing.

Related Insights

Artículos relacionados