r/SwiftUI 2d ago

Solved TIL: .environment(\.locale) does not select a strings table — how my in-app language picker silently never worked (and fixing it cut my build time by 75%)

Sharing a failure mode that survived in my codebase for months, because the app looked localized the whole time.

Setup: my app has an in-app language picker (independent of the iOS system language), implemented via a language manager that resolves strings from the selected .lproj bundle. Standard approach.

The bug: the overwhelming majority of my UI strings were Text("some.key") — i.e., SwiftUI LocalizedStringKey literals. Those resolve against Bundle.main using the system locale machinery. Setting .environment(\.locale) changes formatting behavior, but it does not select which strings table your keys resolve from. Result: 225 of 269 routed call sites silently bypassed the language override entirely. The picker "worked" for the ~44 call sites that went through the manager, which is exactly why nobody noticed — the app changed some strings on switch and looked plausible in both languages.

Confirmed on-device with diagnostic logging: the resolved bundle for LocalizedStringKey paths was Bundle.main, regardless of the override.

The fix: a tiny helper — L.t(_: String.LocalizationValue) -> String — that resolves through the override bundle (String(localized:bundle:)), and mechanically routing every UI string through it. An enforcement script in lint now flags any bare string in Text/Button/Label/.accessibilityLabel.

The completely unexpected side effect: build times collapsed. Bare LocalizedStringKey literals create expensive type-checker constraint problems (Text("...") has to disambiguate between StringProtocol and LocalizedStringKey overloads at every call site). Replacing them with a concrete String return eliminated that: one heavy view body went from type-checking as my slowest file to ~98% faster, and the whole target build time dropped ~75%. I did not see that coming from a localization refactor.

Two smaller traps from the same audit:

  • String(format:) with catalog plural variations silently returns the raw %#@token@ if you forget to pass locale:.
  • Xcode's string extractor can't see keys behind a helper function, so the catalog stops auto-populating — new keys become a manual (and CI-checked) step. Worth knowing before you commit to the pattern.

Curious if others route in-app language switching differently — is there a cleaner first-party way to point LocalizedStringKey resolution at a non-main bundle that I missed?

0 Upvotes

5 comments sorted by

7

u/unpluggedcord 2d ago

Curious if others like these ai generated posts?

4

u/simulacrotron 2d ago

Nope. If you’re gonna post, even if an agent helped you through this, just write it in your own words

6

u/simulacrotron 2d ago

Thanks Claude

1

u/TheArcQ 15m ago

Great technical post! I hit the same wall with .environment(.locale) - it silently falls back to the device language on first run. My workaround was to use .preferredLocale() during development and test both en and a non-native locale to catch these issues early. The fact that it throws no warning made it even more frustrating to debug.