Skip to content

Use native iOS localization selection - #291

Merged
RDMurray merged 3 commits into
mainfrom
agent/use-native-localization-selection
Jul 23, 2026
Merged

Use native iOS localization selection#291
RDMurray merged 3 commits into
mainfrom
agent/use-native-localization-selection

Conversation

@RDMurray

@RDMurray RDMurray commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve a valid language selected inside Soundscape as the highest-priority choice
  • otherwise use iOS bundle localization matching instead of Soundscape's hand-written exact/language-only algorithm
  • fall missing strings directly back to the en-US development localization
  • add focused tests for selection precedence, missing-string fallback, and Spanish regional matching

Why

The existing fallback selected the first alphabetically sorted localization sharing a language code. That could disagree with iOS's regional matching. Delegating to Bundle.main.preferredLocalizations gives the system enough context to choose the appropriate supported localization.

This deliberately does not add an es-US bundle. With Soundscape's current es-ES and es-419 resources, Foundation selects es-419 for an es-US preference. Adding an empty exact-match bundle would prevent that behavior.

Validation

  • LocalizationContextTest: 6 tests passed on iPhone 17 Pro simulator, iOS 26.4
  • Soundscape target built successfully as part of the test action
  • localization linter completed successfully; it continues to report the existing en-US comment/whitespace warnings
  • Xcode project file passes plutil -lint
  • git diff --check passes

Summary by CodeRabbit

  • Bug Fixes

    • Refined locale resolution to consistently use the app-selected language when available, otherwise falling back through system preferences and the app’s development language, with safe handling for unsupported selections.
    • Improved localized string fallback behavior so missing translations can reliably fall back to an appropriate bundle, and missing keys return unchanged.
    • Ensures language updates propagate correctly for accessibility and locale-change handling.
  • Tests

    • Added unit tests covering locale selection logic and localized string fallback (including Spanish and missing-key scenarios).

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@RDMurray, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc5be28a-3ead-4bbd-997a-db21d0645fa1

📥 Commits

Reviewing files that changed from the base of the PR and between 7c3166e and 969d537.

📒 Files selected for processing (1)
  • apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift
📝 Walkthrough

Walkthrough

LocalizationContext now resolves locales through app selection, preferred localization, and development localization, validates locale bundles, and supports development-bundle string fallbacks. New unit tests cover these behaviors and are registered in the iOS unit test target.

Changes

Localization context

Layer / File(s) Summary
Locale resolution and bundle validation
apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift
Locale resolution now considers selected, preferred, supported, and development localizations, while locale updates require an available bundle.
Localization fallback and test integration
apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift, apps/ios/UnitTests/Language/LocalizationContextTest.swift, apps/ios/GuideDogs.xcodeproj/project.pbxproj
String lookup now falls back through the development bundle to the key, with tests covering resolution, preferred localizations, temporary bundles, and target registration.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsContext
  participant LocalizationContext
  participant Bundle
  SettingsContext->>LocalizationContext: provide selected locale
  LocalizationContext->>Bundle: resolve supported localization
  Bundle-->>LocalizationContext: return locale bundle
  LocalizationContext->>SettingsContext: store validated locale
  LocalizationContext->>Bundle: request localized string
  Bundle-->>LocalizationContext: return active or development translation
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: switching localization selection to iOS-native matching logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/use-native-localization-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@RDMurray

Copy link
Copy Markdown
Contributor Author

@JJGatchalian This makes the automatic translation selection based on device language and region more intelligent by using the built-in iOS selection logic. MS Soundscape had it's own, much more basic selection logic, that would fall back to English or possibly the wrong dialect if an exact match wasn't found. E.g. it should now choose es-419 when the device is set to es-US or es-MX.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift (1)

87-121: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unsynchronized static cache mutated inside a computed-property getter.

currentAppLocale's getter mutates the shared static _currentAppLocale/currentAppLocaleBundle on every access, and localizedString(_:) (line 215) forces this getter to run on every single localized-string lookup. If any callout/TTS generation or other work happens off the main thread while localizedString is also invoked elsewhere concurrently, these unsynchronized writes to shared static state are a data race that can corrupt the cached Bundle reference or cause crashes under contention. Relying on getter side effects to "warm" the cache (as done at line 215's _ = currentAppLocale) also makes the dependency implicit and easy to break in a future refactor.

Consider guarding the read/update of _currentAppLocale/currentAppLocaleBundle with a lock (or a serial queue), and exposing the "resolve + cache" step as an explicit function rather than as a side effect of reading a property.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift`
around lines 87 - 121, Make the currentAppLocale cache thread-safe by moving the
resolve-and-cache logic out of the computed-property getter into an explicit
function, such as a cache-refresh helper, and guard reads and updates of
_currentAppLocale and currentAppLocaleBundle with a lock or serial queue. Update
localizedString(_:) and other callers to invoke this helper explicitly instead
of relying on `_ = currentAppLocale`, while preserving the existing locale
resolution and bundle fallback behavior.
🧹 Nitpick comments (2)
apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift (2)

134-137: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Force-unwrapped Bundle(locale:) for the development bundle.

defaultDevelopmentBundle = Bundle(locale: developmentLocale)! crashes the app on first access if the development-locale .lproj bundle can't be resolved (e.g. resource-path or naming mismatch). Since this bundle is the ultimate fallback used by localizedString, a resolution failure here is exactly the scenario that fallback is supposed to survive. Consider falling back to Bundle.main (with a logged error) instead of force-unwrapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift`
around lines 134 - 137, Update defaultDevelopmentBundle in LocalizationContext
to avoid force-unwrapping Bundle(locale: developmentLocale); resolve the
development bundle safely, log a resolution failure, and fall back to
Bundle.main so localizedString remains usable when the development-locale bundle
is unavailable.

213-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Getter side-effect dependency is implicit.

_ = currentAppLocale is used purely to trigger the getter's cache-update side effect before reading currentAppLocaleBundle. This works but is easy to silently break (e.g. if the caching logic is later moved out of the getter). Consider a dedicated resolveCurrentBundle() helper that makes the dependency explicit instead of discarding the getter's return value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift`
around lines 213 - 222, Replace the discarded currentAppLocale getter call in
the localization lookup flow with a dedicated resolveCurrentBundle() helper that
explicitly performs the locale and bundle cache resolution before reading
currentAppLocaleBundle. Preserve the existing localizedString fallback behavior
and ensure the helper owns the required cache-update dependency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift`:
- Around line 87-121: Make the currentAppLocale cache thread-safe by moving the
resolve-and-cache logic out of the computed-property getter into an explicit
function, such as a cache-refresh helper, and guard reads and updates of
_currentAppLocale and currentAppLocaleBundle with a lock or serial queue. Update
localizedString(_:) and other callers to invoke this helper explicitly instead
of relying on `_ = currentAppLocale`, while preserving the existing locale
resolution and bundle fallback behavior.

---

Nitpick comments:
In `@apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift`:
- Around line 134-137: Update defaultDevelopmentBundle in LocalizationContext to
avoid force-unwrapping Bundle(locale: developmentLocale); resolve the
development bundle safely, log a resolution failure, and fall back to
Bundle.main so localizedString remains usable when the development-locale bundle
is unavailable.
- Around line 213-222: Replace the discarded currentAppLocale getter call in the
localization lookup flow with a dedicated resolveCurrentBundle() helper that
explicitly performs the locale and bundle cache resolution before reading
currentAppLocaleBundle. Preserve the existing localizedString fallback behavior
and ensure the helper owns the required cache-update dependency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0be1edee-2221-4b74-a819-64fc4e9242d5

📥 Commits

Reviewing files that changed from the base of the PR and between 18c2143 and 38be3c6.

📒 Files selected for processing (3)
  • apps/ios/GuideDogs.xcodeproj/project.pbxproj
  • apps/ios/GuideDogs/Code/Language/Localization/LocalizationContext.swift
  • apps/ios/UnitTests/Language/LocalizationContextTest.swift

@RDMurray
RDMurray merged commit 9d4b6e4 into main Jul 23, 2026
2 checks passed
@RDMurray
RDMurray deleted the agent/use-native-localization-selection branch July 23, 2026 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant