Skip to content

Localize remaining OTPKit strings and OTP wire tokens - #149

Merged
aaronbrethorst merged 3 commits into
mainfrom
i18n
Jul 28, 2026
Merged

Localize remaining OTPKit strings and OTP wire tokens#149
aaronbrethorst merged 3 commits into
mainfrom
i18n

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Jul 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Extracts every remaining hardcoded user-facing string in OTPKit into the 13-locale Localizable.strings tables (83 → 180 keys per locale, all referenced, none orphaned).
  • Stops raw OTP wire tokens leaking into the UI: new LegMode and RelativeDirection enums map them to localized names, so a French user sees "Tournez à gauche sur Pine St" rather than "LEFT onto Pine St".
  • Lets the demo app declare the locales OTPKit ships, so it actually displays the translations.
  • Fixes three latent defects found along the way (details below).

Defects fixed

Text("Close") in a Swift package never reached the package's tables. SwiftUI's LocalizedStringKey initializers resolve against the main app bundle, so every one of these literals would have missed OTPKit's strings even if translations had existed. All call sites now route through OTPLoc, which passes bundle: .module.

TransportMode.displayName referenced four keys that existed in no strings file. NSLocalizedString echoes the key on failure, so users were seeing the literal text transport_mode_transit in the UI. Renamed to transport_mode.*, translated into all 13 locales.

The OTP API date/time query parameters were locale-fragile. formattedTripDate/formattedTripTime build the date= and time= params (RestAPIService.swift:44-45). They are now pinned to en_US_POSIX + Gregorian + an explicit local time zone. Without the calendar pin, a device on a Buddhist or Japanese Imperial calendar emits 05-10-2567; without the zone pin, iOS emits UTC and plans trips for the wrong time of day. Both are covered by tests.

Test plan

  • xcodebuild test — 165 tests in 13 suites pass (was 142)
  • swiftlint — 0 violations across 112 files
  • Ran the demo app in the iOS Simulator forced to Spanish; confirmed the OTPKit trip planner renders "Planificación de viaje" / "Desde" / "Hasta" / "Opciones" / "Buscar rutas"
  • Re-verified after the demo Info.plist change that language resolution flips from a clean build, and that the generated keys are still merged into the partial plist
  • Live request against otp.prod.sound.obaweb.org using the new pinned wire format — HTTP 200, 3 itineraries returned
  • Verified the live server's actual tokens (DEPART, LEFT, RIGHT, UTURN_RIGHT, WALK, BUS) are all covered by the new enums
  • Key parity, format-specifier parity, and no-empty-values checked across all 13 locales

Review notes

Host apps must declare CFBundleLocalizations. iOS resolves an app's language from the main bundle, so a host app that ships no localizations pins the process to English and OTPKit follows — the translations are present but never selected. Documented in a new README section. The OBA iOS app is already localized into this set, so it is unaffected. The demo app was not, which is why it rendered English; Demo/OTPKitDemo-Info.plist now declares the 13 locales. That change does not translate the demo's own UI, which stays in English by design.

Two Xcode mechanics that cost a build cycle each, worth knowing before editing that plist: INFOPLIST_KEY_CFBundleLocalizations is silently ignored (Xcode only passes through an allow-list of INFOPLIST_KEY_*), and the plist must live outside Demo/OTPKitDemo/ because that is a synchronized folder group — inside it, the file is copied as a resource too and the build fails with "Multiple commands produce ... Info.plist".

Unrelated bug, worth its own issue: the demo app crashes on launch under iOS 27 — its generated UIApplicationSceneManifest has an empty UISceneConfigurations dict, which now hard-traps in _UIApplicationEvaluateRuntimeIssueForNoSceneLifecycleAdoption. Pre-existing; I verified this branch on iOS 26.3.

Deliberately deferred, all raised during review and reasonable follow-ups:

  1. Six sites still branch on raw leg.mode strings with three different normalizations (DirectionLegView, DirectionLegVehicleView, TripPlannerResultsView, MapCoordinator ×3, Leg.walkMode). Migrating them onto LegMode would unify that, but it changes routing behavior — a TRAIN leg would newly reach the vehicle branch — which is out of scope for a localization change.
  2. LegMode and the existing RouteType are two unbridged taxonomies on the same Leg.
  3. Walking-step instructions still compose a localized fragment into a localized template ("%1$@ onto %2$@, walk %3$@."), which yields "Take the elevator onto Pine St". Fixing properly means per-case whole-sentence keys and wants translator input.
  4. WalkingDistance labels are hardcoded imperial in English and hand-converted to metric elsewhere; unit choice keys off UI language rather than measurement system. Routing through Formatters.formatDistance would fix that but rounds 402 m to "0.2 mi", destroying the 0.25/0.5 distinction in the picker.
  5. CLAUDE.md says package tests use XCTest, but 6 of 7 files in OTPKit/Tests/ already use Swift Testing. The doc is stale.

Also note the options pill now reads "1 mile walk" where it read "1 mi walk" — slightly wider text in a capsule; no line limit, so it should just grow.

Extract every remaining hardcoded user-facing string in OTPKit into the
13-locale Localizable.strings tables. The demo app is deliberately out of
scope. 180 keys per locale, all referenced, none orphaned.

Notable pieces:

- SwiftUI's LocalizedStringKey initializers resolve against the *main* app
  bundle, so `Text("Close")` in a Swift package never reached the package's
  tables. All call sites now route through OTPLoc, which passes .module.

- Raw OTP wire tokens no longer leak into the UI. LegMode and
  RelativeDirection map them to localized names, tolerating casing, spaces
  and the BIKE/TRAIN aliases. An unrecognized token degrades to humanized
  English and is logged, matching the convention set in #146 for
  unrecognized error codes.

- TransportMode.displayName referenced four keys that existed in no table,
  so users saw the literal string "transport_mode_transit". Fixed and
  translated.

- The three fixed-format DateFormatters build OTP query parameters, not
  display text. They are now pinned to en_US_POSIX + Gregorian + an explicit
  local time zone; without the calendar pin a device on a Buddhist or
  Japanese Imperial calendar emits a year the server cannot parse, and
  without the zone pin iOS emits UTC and plans trips for the wrong time of
  day. Both are covered by tests.

- TripOptionsSummaryView's date formatter is genuine display text and now
  uses setLocalizedDateFormatFromTemplate, picking up locale field order
  and 12/24-hour preference.

- Formatters.formatDateToTime built a DateFormatter per call on a path that
  runs twice per itinerary row; it now reuses one, matching its siblings.

Tests cover token parsing, the wire formats, and string-table integrity
(key parity, format-specifier parity, and that every key resolves — an
unresolved key otherwise ships silently as its own name).
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 25 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07e6d0e3-2a50-4c53-815d-69c14b14fc75

📥 Commits

Reviewing files that changed from the base of the PR and between d012225 and aa0532a.

📒 Files selected for processing (20)
  • Demo/OTPKitDemo-Info.plist
  • Demo/OTPKitDemo.xcodeproj/project.pbxproj
  • OTPKit/Sources/OTPKit/Core/Extensions/DateFormatterExtension.swift
  • OTPKit/Sources/OTPKit/Core/Utilities/Formatters.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/Components/DirectionLegs/DirectionLegVehicleView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/Components/CurrentLocationButton.swift
  • OTPKit/Sources/OTPKit/Resources/ar.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/en.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/es.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/fil.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/fr.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/it.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/ko.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/pl.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/pt-BR.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/ru.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/vi.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/zh-Hans.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/zh-Hant.lproj/Localizable.strings
  • OTPKit/Tests/LocalizationTests.swift
📝 Walkthrough

Walkthrough

OTPKit centralizes OTP date formatting, adds typed and localized OTP token display, replaces hardcoded UI and service text with localized resources, expands translations across supported locales, and adds tests for formatting, parsing, localization integrity, and resource parity.

Changes

Localization and OTP presentation

Layer / File(s) Summary
OTP token and formatting contracts
OTPKit/Sources/OTPKit/Core/Extensions/*, OTPKit/Sources/OTPKit/Core/Models/OTP/*
OTP token normalization, leg-mode and relative-direction parsing, localized display names, current-location construction, and fixed API date formatters are introduced or updated.
Localized service and utility paths
OTPKit/Sources/OTPKit/Core/Helper/*, OTPKit/Sources/OTPKit/Core/Services/*, OTPKit/Sources/OTPKit/Core/Utilities/*, OTPKit/Sources/OTPKit/Network/*
Location fallbacks, saved-location errors, time formatting, and invalid API responses now use localized output and shared construction paths.
Localized trip-planning and directions UI
OTPKit/Sources/OTPKit/Presentation/*
Trip planner, location picker, search, library, directions, options, loading, error, and top-control views now resolve user-facing text through OTPLoc; leg and step views use localized display names.
Localized string tables
OTPKit/Sources/OTPKit/Resources/*/Localizable.strings
Supported locale tables add keys for the updated UI and OTP output while removing obsolete entries.
Localization validation and documentation
OTPKit/Tests/LocalizationTests.swift, README.markdown
Tests cover token parsing, date formatting, locale resource parity, translation values, and format specifiers; the README documents supported locales and resource-bundle behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: localizing remaining OTPKit strings and mapping OTP wire tokens.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch i18n

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.

iOS resolves an app's language from the main bundle, so the demo — which
declared no localizations — pinned itself to English and OTPKit's strings
followed. The translations were bundled correctly the whole time; the host
just never asked for them.

Declares the 13 locales OTPKit ships via a partial Info.plist. The remaining
keys are still generated by the build and merged into it.

Two mechanics worth knowing if this ever needs changing:

- INFOPLIST_KEY_CFBundleLocalizations does nothing. Xcode only passes
  through a known allow-list of INFOPLIST_KEY_* settings, and this isn't on
  it — the build succeeds and the key is silently absent.

- The plist lives at Demo/OTPKitDemo-Info.plist, outside the target's
  synchronized folder group. Inside Demo/OTPKitDemo/ it gets picked up as a
  resource as well as an Info.plist, which fails the build with "Multiple
  commands produce ... Info.plist".

This does not translate the demo's own UI, which stays in English.

@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.

Actionable comments posted: 5

🧹 Nitpick comments (1)
OTPKit/Tests/LocalizationTests.swift (1)

242-259: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Format-specifier check validates count only, not type/order.

count(_:) counts specifier occurrences but doesn't compare which specifier types appear at which position. A translation that swaps %@ for %d (or reorders two differing specifiers) at the same key would still pass this test, yet crash at runtime when String(format:arguments:) receives a String where an Int conversion is expected (or vice versa). Since this suite's whole purpose is guarding against exactly this class of localization bug, capturing the matched specifiers themselves (not just their count) would close the gap.

♻️ Compare specifier sequences instead of counts
-            func count(_ string: String) -> Int {
-                specifier.numberOfMatches(in: string, range: NSRange(string.startIndex..., in: string))
-            }
+            func specifiers(_ string: String) -> [String] {
+                specifier.matches(in: string, range: NSRange(string.startIndex..., in: string)).map {
+                    String(string[Range($0.range, in: string)!])
+                }
+            }

             for locale in Self.locales.dropFirst() {
                 let table = try Self.keys(in: locale)
                 for (key, baseValue) in base {
                     guard let translated = table[key] else { continue }
-                    `#expect`(count(baseValue) == count(translated),
+                    `#expect`(specifiers(baseValue) == specifiers(translated),
                             "\(locale) \(key): \(baseValue) vs \(translated)")
                 }
             }
🤖 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 `@OTPKit/Tests/LocalizationTests.swift` around lines 242 - 259, Update the
formatSpecifierParity test and its local count helper to extract each matched
format specifier in order, then compare the resulting sequences for base and
translated values. Preserve the existing locale/key iteration and expectation
message, but reject translations that change specifier types or ordering rather
than comparing only occurrence counts.
🤖 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.

Inline comments:
In `@OTPKit/Sources/OTPKit/Core/Extensions/DateFormatterExtension.swift`:
- Line 27: The cached formatters in DateFormatterExtension.swift and
Formatters.swift must follow updated device settings instead of capturing
.current or inherited defaults. Update the formatter configuration at
DateFormatterExtension.swift:27 and Formatters.swift:89-97 to use autoupdating
locale, calendar, time zone, and clock-style settings as appropriate, preserving
each formatter’s existing formatting behavior.

In `@OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift`:
- Line 13: Make both model enums conform to Codable: update LegMode in
OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift at lines 13-13 and
RelativeDirection in
OTPKit/Sources/OTPKit/Core/Models/OTP/RelativeDirection.swift at lines 13-13,
preserving their existing conformances.

In
`@OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/Components/DirectionLegs/DirectionLegVehicleView.swift`:
- Around line 37-40: Update the boarding identifier in DirectionLegVehicleView
to use leg.from.stopCode instead of leg.to.stopCode. Keep the
destination/alighting section below using leg.to.stopCode.

In
`@OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/ItineraryDetailsView.swift`:
- Around line 68-69: Update the navigationTitle in ItineraryDetailsView to use
the existing unknownLocation fallback when destination is nil, while preserving
destination?.title for non-nil destinations and removing the separate OTPLoc
fallback.

In
`@OTPKit/Sources/OTPKit/Presentation/Sheets/Library/Components/CurrentLocationButton.swift`:
- Line 65: Update the CurrentLocationButton error handling so the localized
messages assigned to errorMessage in both denied and unavailable paths are
actually rendered by the view, binding errorMessage to the intended alert or
text presentation. If no presentation is intended, remove these assignments and
their unused localization keys instead.

---

Nitpick comments:
In `@OTPKit/Tests/LocalizationTests.swift`:
- Around line 242-259: Update the formatSpecifierParity test and its local count
helper to extract each matched format specifier in order, then compare the
resulting sequences for base and translated values. Preserve the existing
locale/key iteration and expectation message, but reject translations that
change specifier types or ordering rather than comparing only occurrence counts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f03c48c-3ae9-4b88-9a12-3c17c7343837

📥 Commits

Reviewing files that changed from the base of the PR and between b613538 and d012225.

📒 Files selected for processing (52)
  • OTPKit/Sources/OTPKit/Core/Extensions/DateFormatterExtension.swift
  • OTPKit/Sources/OTPKit/Core/Extensions/StringExtension.swift
  • OTPKit/Sources/OTPKit/Core/Helper/Location/LocationManager.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/Itinerary.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/Location.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/RelativeDirection.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/TransportMode.swift
  • OTPKit/Sources/OTPKit/Core/Models/OTP/WalkingDistance.swift
  • OTPKit/Sources/OTPKit/Core/Services/UserDefaultsServices.swift
  • OTPKit/Sources/OTPKit/Core/Utilities/Formatters.swift
  • OTPKit/Sources/OTPKit/Network/RestAPIService.swift
  • OTPKit/Sources/OTPKit/Presentation/Common/Error/ErrorCardView.swift
  • OTPKit/Sources/OTPKit/Presentation/Common/Loading/LoadingOverlay.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/AdvancedOptions/AdvancedOptionsSheet.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/AdvancedOptions/Components/OptionRowView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/Components/DirectionLegs/DirectionLegUnknownView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/Components/DirectionLegs/DirectionLegVehicleView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/Components/DirectionLegs/DirectionLegWalkView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/DirectionsSheetView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/ItineraryDetailsView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/PagedDirectionsView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/Components/CurrentLocationButton.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/Components/FavoritesSectionView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/Components/RecentsSectionView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/FavouriteLocationsSheet.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/LocationOptionsSheet.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Library/RecentLocationsSheet.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Search/Components/SearchBar.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/Search/SearchSheetView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/TripPlanner/Components/ItineraryLegs/ItineraryLegUnknownView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/TripPlanner/Components/ItineraryPreviewView.swift
  • OTPKit/Sources/OTPKit/Presentation/Sheets/TripPlanner/TripPlannerResultsView.swift
  • OTPKit/Sources/OTPKit/Presentation/TopControls/TopControlsOverlay.swift
  • OTPKit/Sources/OTPKit/Presentation/TopControls/TripOptionsSummaryView.swift
  • OTPKit/Sources/OTPKit/Presentation/TripPlanner/TripPlannerView.swift
  • OTPKit/Sources/OTPKit/Presentation/ViewModel/TripPlannerViewModel.swift
  • OTPKit/Sources/OTPKit/Resources/ar.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/en.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/es.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/fil.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/fr.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/it.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/ko.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/pl.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/pt-BR.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/ru.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/vi.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/zh-Hans.lproj/Localizable.strings
  • OTPKit/Sources/OTPKit/Resources/zh-Hant.lproj/Localizable.strings
  • OTPKit/Tests/LocalizationTests.swift
  • README.markdown

Comment thread OTPKit/Sources/OTPKit/Core/Extensions/DateFormatterExtension.swift Outdated
///
/// OTP sends these as uppercase tokens (`CABLE_CAR`). Rendering the token directly leaks
/// English-shaped data into every locale, so callers should use ``displayName``.
public enum LegMode: String, CaseIterable, Sendable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make the new OTP model types Codable.

Both newly introduced model enums omit the repository’s required JSON serialization conformance. Add Codable to both declarations.

  • OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift#L13-L13: add Codable.
  • OTPKit/Sources/OTPKit/Core/Models/OTP/RelativeDirection.swift#L13-L13: add Codable.

As per coding guidelines, “All models must conform to Codable for JSON serialization.”

📍 Affects 2 files
  • OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift#L13-L13 (this comment)
  • OTPKit/Sources/OTPKit/Core/Models/OTP/RelativeDirection.swift#L13-L13
🤖 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 `@OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift` at line 13, Make both
model enums conform to Codable: update LegMode in
OTPKit/Sources/OTPKit/Core/Models/OTP/LegMode.swift at lines 13-13 and
RelativeDirection in
OTPKit/Sources/OTPKit/Core/Models/OTP/RelativeDirection.swift at lines 13-13,
preserving their existing conformances.

Source: Coding guidelines

Comment on lines +68 to 69
.navigationTitle(destination?.title ?? OTPLoc("map.destination", comment: "The destination of a trip"))
.toolbarTitleDisplayMode(.inlineLarge)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the unknown-location fallback for the navigation title.

When destination is nil, this resolves to localized "map.destination" instead of the already-computed unknownLocation, making the title inconsistent with the destination description.

Proposed fix
-            .navigationTitle(destination?.title ?? OTPLoc("map.destination", comment: "The destination of a trip"))
+            .navigationTitle(destination?.title ?? unknownLocation)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.navigationTitle(destination?.title ?? OTPLoc("map.destination", comment: "The destination of a trip"))
.toolbarTitleDisplayMode(.inlineLarge)
.navigationTitle(destination?.title ?? unknownLocation)
.toolbarTitleDisplayMode(.inlineLarge)
🤖 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
`@OTPKit/Sources/OTPKit/Presentation/Sheets/Directions/ItineraryDetailsView.swift`
around lines 68 - 69, Update the navigationTitle in ItineraryDetailsView to use
the existing unknownLocation fallback when destination is nil, while preserving
destination?.title for non-nil destinations and removing the separate OTPLoc
fallback.

Four fixes from CodeRabbit's review of #149:

- Cached formatters now follow the device. The wire formatters in
  DateFormatterExtension and the display formatter in Formatters are
  `static let`/`lazy var`, so a pinned `.current` time zone goes stale the
  moment the device's changes -- a rider who flies across a boundary keeps
  planning trips in the departure zone's offset until relaunch. Verified
  empirically: with TZ flipped from America/Los_Angeles to Asia/Tokyo, a
  cached formatter pinned to `.current` kept emitting 16:00 while an
  `.autoupdatingCurrent` one correctly moved to 09:00. The API formatters'
  locale and calendar stay pinned to en_US_POSIX/Gregorian -- those are
  deliberate wire-stability guarantees, and only the zone should track.

- Show the boarding stop code, not the alighting one.
  DirectionLegVehicleView printed `leg.to.stopCode` under the departure
  time, so the boarding row named the stop where the rider gets off.

- Drop the dead `errorMessage` state in CurrentLocationButton. It was
  assigned in two paths and never read; the button communicates failure
  through its title and subtitle. Removes the two now-orphaned keys from
  all 13 locale tables (180 -> 178 keys each).

- Compare format specifiers by type, not just count. A translation could
  swap %@ for %d and pass, then crash in String(format:). Comparing raw
  sequences would instead reject the positional reordering that %1$@/%2$@
  exist to permit and that several locales here use, so the check maps
  argument position to conversion type.

Two comments declined:

- Making LegMode/RelativeDirection `Codable` would add a second parsing
  path that disagrees with the real one: the synthesized conformance keys
  off rawValue alone, so decoding "BIKE" or "TRAIN" would throw where
  `init?(otpMode:)` resolves them through the alias table. These are
  presentation adapters over an untyped wire field, not wire models.

- The ItineraryDetailsView nav title keeps "Destination" rather than the
  "Unknown" used for missing places in rows. A title bar reading "Unknown"
  names nothing; the contexts differ.

The formatter pinning test now asserts the zone resolves to the device's
*and* is autoupdating -- `.autoupdatingCurrent != .current` even when both
resolve identically, so the first check compares identifiers.
@aaronbrethorst

Copy link
Copy Markdown
Member Author

Worked through the review feedback in aa0532a — four fixed, two declined.

Fixed

  1. Cached formatters now follow the device. Good catch, and it holds up under test. The linked web result in that comment claims a DateFormatter captures autoupdating values at assignment, which would make the fix a no-op — that's wrong. Flipping TZ from America/Los_Angeles to Asia/Tokyo on an already-built formatter:

    LA   autoupdating=16:00   pinned=16:00
    TYO  autoupdating=09:00   pinned=16:00   <- .current went stale
    

    So .autoupdatingCurrent it is. Only the time zone changed on the API formatters; en_US_POSIX and the Gregorian calendar stay pinned, since those are the deliberate wire-stability guarantees from QA1480 and must not track the device.

  2. Boarding stop code — correct, the boarding row was printing leg.to.stopCode under the departure time. Now leg.from.stopCode.

  3. CurrentLocationButton.errorMessage — confirmed dead: assigned twice, never read. Removed the state and the two now-orphaned keys across all 13 locale tables (180 → 178 each). The button already surfaces failure through its title and subtitle, so wiring up an alert would have been new UI rather than a fix.

  4. Format specifier parity (the nitpick) — taken, with one adjustment. Comparing raw sequences would have broken the build: ar, fil, es and others legitimately reorder %1$@/%2$@, which is the entire reason positional specifiers exist. The check now maps argument position → conversion type, so a %@%d swap still fails while reordering stays legal.

Declined

  1. Codable on LegMode / RelativeDirection. The CLAUDE.md rule is about wire models, and these aren't — Leg.mode decodes as a String, and these enums are presentation adapters over that untyped field. Adding the conformance would be worse than redundant: the synthesized version keys off rawValue alone, so decoding "BIKE" or "TRAIN" would throw, while init?(otpMode:) resolves both through the alias table and normalizes case and spacing. That's a second, quieter parsing path that disagrees with the one actually in use.

  2. unknownLocation for the ItineraryDetailsView nav title. Real inconsistency, but the contexts differ: "Unknown" labels a specific missing place inside a row, whereas line 68 titles the screen. A navigation bar reading "Unknown" names nothing, while "Destination" says what you're looking at.

165 tests in 13 suites passing, SwiftLint clean.

@aaronbrethorst
aaronbrethorst merged commit 372a65c into main Jul 28, 2026
5 checks passed
@aaronbrethorst
aaronbrethorst deleted the i18n branch July 28, 2026 21:25
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