Skip to content

feat(l10n): complete Latin-American Spanish (es-419) - #49

Merged
Ivan-LB merged 6 commits into
devfrom
feature/es-localization
Sep 29, 2026
Merged

Ivan-LB merged 6 commits into
devfrom
feature/es-localization

Conversation

@Ivan-LB

@Ivan-LB Ivan-LB commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Stacked on #48. Merge #48 first. This branch is rebased on it, so after that merge the diff here shrinks to the two localization commits.

Summary

  • Catalog complete in es-419:
    • 134 keys added and 267 newly translated.
    • 69 existing values normalized to the product voice: tú, sentence case, no exclamation marks.
    • 51 stale keys from removed features deleted.
    • Plural forms added.
    • check-l10n.py passes: 452 keys, all translated, format specifiers match.
  • Hardcoded strings routed through Strings.swift:
    • Goal pace (On track / Behind / 1+ year / /wk), the greeting, the quick-add sheet and the auto-move banner (AttributedString(localized:)).
    • Trackers, goal flows, reports and PDF, achievements, notifications, receipt review, profile, and view-model errors.
    • Dates use Date.FormatStyle ("Lunes · 28 de septiembre").
  • Persisted identifiers stay English. ExpenseCategory/IncomeSource now have storageKey (stored) and displayName (shown), and the payday deposit note is the constant GoalDeposits.autoMoveNote. Without this the Spanish chips would have stored "Café" next to "Coffee". Old rows keep matching, and new CategoryPersistenceTests cover it.
  • Money tab: "Income" as a title or segment gets its own key, so Spanish reads "Gastos | Ingresos". A single income stays "Ingreso".
  • CI: the "Localization completeness" step runs scripts/check-l10n.py and fails when a live key lacks es-419 or its format specifiers drift.
  • Docs: gotchas.yaml#persisted-identifiers-never-localized; localization.md documents es-419, the gate, and the catalog's key order (localizedStandardCompare).

Why

Mexico is the primary market, but 1.0 had 139 of 368 keys untranslated and several screens building English in code. This is the headline of 1.1, and it also unblocks the es-MX screenshot set.

Test plan

  • xcodebuild test -scheme Savely on iPhone 17 Pro, skipping SavelyUITests like CI: TEST SUCCEEDED, 107 passed, 0 failed (re-run after the rebase on chore: stop bundling Config.plist, drop dead API records #48).
  • SavelyUITests compile (English labels unchanged).
  • swiftlint lint --strict clean; python3 scripts/check-l10n.py OK.
  • Visual pass in es-MX on the seeded Simulator (-AppleLanguages (es-419) -AppleLocale es_MX): Home, Goals, goal detail (Behind / 1+ year), quick-add sheet, log expense and log income with the auto-move banner, Money (Gastos / Ingresos, 7-month trend), Profile (achievements, settings, Data & privacy) and receipt review. No truncation, no leftover English outside demo data.
  • On device: switch iPhone language to Spanish, log an expense, switch back to English, and confirm the row still shows under Coffee/Café in both.

Risks

  • Pending reminders: their text is fixed when they're scheduled, so one keeps its old language until it's rescheduled.
  • Unused constants: 50+ unused Strings.swift constants were kept and translated. Removing them is a separate cleanup.
  • Tight tile: "METAS ACTIVAS" wraps to two lines in the Profile stat tile. It's readable but tight.

🤖 Generated with Claude Code

…on Pro

Config.plist sat in Copy Bundle Resources, so whatever key was on disk at
archive time shipped inside the IPA (1.0's key was revoked 2026-08-19).
The file stays on disk and gitignored for local dev; it is simply no longer
a build input.

- Drop the Resources build-phase entry (file reference kept).
- Retire the placeholder-plist steps in ci.yml and ci_post_clone.sh; a
  clean checkout now builds without it.
- ci_post_clone.sh keeps one gate: refuse any archive with
  FeatureFlags.tipsEnabled = true, since Tips has no safe key source yet.
- New invariant: gotchas.yaml#config-plist-never-bundled, CLAUDE.md §3.
- Pending Xcode change: SUPPORTS_MACCATALYST and the "Designed for
  iPhone/iPad" on Mac and Vision Pro destinations set to NO, plus Xcode's
  re-sort of the PBXBuildFile/PBXFileReference sections.
Savely 1.0 makes no network calls (Tips is flagged off), but Info.plist
still declared an App Transport Security exception for openai.com, visible
to App Review and at odds with the "Data Not Collected" privacy answers.
ITSAppUsesNonExemptEncryption stays; it is load-bearing for upload.

The leftover GoogleService-Info.plist was deleted locally (gitignored, never
bundled). The Firebase project itself is still open, owner-only; see
docs/plans/dead-api-records.md.
testPreviewFootage walks the seeded app at human pace (home, goal, expense,
payday auto-move, goal again, trend, privacy, receipt) while
`simctl io recordVideo` captures it, and prints BRAGMARK lines with
timestamps and tap centers so an editor can cut scenes and draw touch
indicators. It also answers the first-launch notification prompt.
Mexico is Savely's primary market; 1.0 shipped with 139 of 368 catalog keys
untranslated and several screens building English in code.

- Catalog: every live key translated (134 added, 267 newly translated),
  69 existing values normalized to the product voice (tú, sentence case,
  no exclamation marks), 51 stale keys from removed features deleted, es-419
  plural forms added. Keys kept in Xcode's order.
- Hardcoded strings routed through Strings.swift: goal pace status
  (On track / Behind / 1+ year / /wk), the dashboard greeting, quick-add
  sheet, the auto-move banner (AttributedString(localized:)), trackers,
  goal flows, reports and PDF, achievements, notifications, receipt review,
  profile, view-model errors. Display dates use Date.FormatStyle
  ("Lunes · 28 de septiembre"), with a sentenceCased helper.
- Money tab: "Income" as a title/segment gets its own key so Spanish reads
  "Ingresos" next to "Gastos"; a single income stays "Ingreso".
- Persisted identifiers stay English: ExpenseCategory/IncomeSource gain
  storageKey (stored) vs displayName (shown); the payday deposit note is the
  constant GoalDeposits.autoMoveNote. Old rows keep matching; new tests in
  CategoryPersistenceTests cover the round trip. GoalPaceTests pins en_US
  instead of assuming the simulator's region.
- Knowledge: gotchas.yaml#persisted-identifiers-never-localized,
  localization.md documents es-419 and the CI gate.
scripts/check-l10n.py (python3 stdlib) runs as the Localization completeness step after SwiftLint. It lists any live key without a translated es-419 value (plural variations included) and any translation whose format specifiers differ from the source.
@Ivan-LB
Ivan-LB merged commit 226465e into dev Sep 29, 2026
1 check passed
@Ivan-LB
Ivan-LB deleted the feature/es-localization branch September 29, 2026 04:19
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